various fixes
[distro-setup] / mailtest-check
1 #!/bin/bash
2
3 # Usage: mail-test-check [slow] [anything]
4 #
5 # slow: do slow checks, like spamassassin
6 #
7 # anything: consider non-interactive, dont print unless something went
8 # wrong
9
10
11 source /b/errhandle/err
12
13 [[ $EUID == 0 ]] || exec sudo -E "${BASH_SOURCE[0]}" "$@"
14
15 shopt -s nullglob
16
17 e() { $int || return 0; printf "mailtest-check: %s\n" "$*"; }
18
19 getspamdpid() {
20 if [[ ! $spamdpid || ! -d /proc/$spamdpid ]]; then
21 # try twice in case we are restarting, it happens.
22 for i in 1 2; do
23 spamdpid=$(systemctl show --property MainPID --value spamassassin | sed 's/^[10]$//' ||:)
24 if [[ $spamdpid ]]; then
25 break
26 fi
27 sleep 30
28 done
29 fi
30 }
31 pr() {
32 if $doprom && [[ -e /var/lib/prometheus/node-exporter ]]; then
33 cat >>/var/lib/prometheus/node-exporter/mailtest-check.prom.$$
34 fi
35 }
36
37
38 #### begin arg processing ####
39 # spamassassin checking takes about 8 seconds.
40 slow=false
41 if [[ $1 == slow ]]; then
42 slow=true
43 shift
44 fi
45
46 int=false
47 if [[ $SUDO_USER || $SSH_CONNECTION ]]; then
48 int=true
49 fi
50
51 if [[ $1 == int ]]; then
52 int=true
53 fi
54
55 if [[ $1 == nonint ]]; then
56 int=false
57 fi
58 #### end arg processing ####
59
60
61 if ! $int; then
62 sleep 60
63 fi
64
65
66 # TODO, get je to deliver the local mailbox: /m/md/INBOX
67 # dovecot appears to setup, i can t be sure.
68
69 source /a/bin/bash_unpublished/source-state
70
71 doprom=false
72 case $HOSTNAME in
73 $MAIL_HOST|bk|je)
74 doprom=true
75 ;;
76 *)
77 rm -f /var/lib/prometheus/node-exporter/mailtest-check.prom*
78 ;;
79 esac
80
81 main() {
82
83 case $HOSTNAME in
84 bk)
85 folders=(/m/md/{expertpathologyreview.com,amnimal.ninja}/testignore)
86 froms=(ian@iankelling.org z@zroe.org testignore@je.b8.nz iank@gnu.org)
87 ;;
88 je)
89 froms=(ian@iankelling.org z@zroe.org iank@gnu.org testignore@amnimal.ninja)
90 folders=(/m/md/je.b8.nz/testignore)
91 ;;
92 *)
93 folders=(/m/md/l/testignore)
94 froms=(testignore@je.b8.nz testignore@expertpathologyreview.com testignore@amnimal.ninja ian@iankelling.org z@zroe.org iank@gnu.org)
95 if ! $int; then
96 timeout 120 rsync --chown iank:iank -e "ssh -oIdentitiesOnly=yes -F /dev/null -i /root/.ssh/jtuttle" -t --inplace -r 'jtuttle@fencepost.gnu.org:/home/j/jtuttle/Maildir/new/' /m/md/l/testignore/new
97 fi
98 ;;
99 esac
100
101
102 # avoid errors like this:
103 # Nov 8 08:16:05.439 [6080] warn: plugin: failed to parse plugin (from @INC): Can't locate Mail/SpamAssassin/Plugin/WLBLEval.pm: lib/Mail/SpamAssassin/Plugin/WLBLEval.pm: Permission denied at (eval 59) line 1.
104 #Nov 8 08:16:05.439 [6080] warn: plugin: failed to parse plugin (from @INC): Can't locate Mail/SpamAssassin/Plugin/VBounce.pm: lib/Mail/SpamAssassin/Plugin/VBounce.pm: Permission denied at (eval 60) line 1.
105 # i dont know why, i just found the solution online
106 cd /m/md
107
108
109 getspamdpid
110 # first time we write, overwrite anything existing
111 if [[ -e /var/lib/prometheus/node-exporter ]]; then
112 cat >/var/lib/prometheus/node-exporter/mailtest-check.prom.$$ <<EOF
113 mailtest_check_found_spamd_pid_bool $(( ${spamdpid:-0} > 0 ))
114 EOF
115 fi
116 e spamdpid: $spamdpid
117 if [[ ! $spamdpid ]]; then
118 echo mailtest spamd pid not found. systemctl status spamassassin:
119 systemctl status spamassassin
120 fi
121 tmpfile=$(mktemp)
122 declare -i unexpected=0
123 declare -i missing_dnswl=0
124 for folder in ${folders[@]}; do
125 for from in ${froms[@]}; do
126 latest=
127 last_sec=0
128
129 if ! grep -rlFx "From: $from" $folder/{new,cur} >$tmpfile; then
130 echo "no message found from: $from"
131 continue
132 fi
133 # webmail sends them to cur it seems
134 while read -r file; do
135 file_sec=$(awk '/^Subject: / {print $4}' $file)
136 if [[ $file_sec ]] && (( file_sec > last_sec )); then
137 latest=$file
138 last_sec="$file_sec"
139 fi
140 done <$tmpfile
141
142 to=$(awk '/^Envelope-to: / {print $2}' $latest)
143
144 if $slow; then
145 if ! $int; then
146 find $folder/new $folder/cur -type f -mmin +1080 -delete
147 fi
148 getspamdpid
149 if [[ $spamdpid ]]; then
150 if [[ $(readlink /proc/$$/ns/net) != "$(readlink /proc/$spamdpid/ns/net)" ]]; then
151 spamcpre="nsenter -t $spamdpid -n -m"
152 fi
153 unset results
154 declare -A results
155 # pyzor fails for our test message, so dont put useless load on their
156 # servers.
157 # example line that sed is parsing:
158 # (-0.1 / 5.0 requ) DKIM_SIGNED=0.1,DKIM_VALID=-0.1,DKIM_VALID_AU=-0.1,SPF_HELO_PASS=-0.001,SPF_PASS=-0.001,TVD_SPACE_RATIO=0.001 autolearn=_AUTOLEARN
159 resultfile=$(mktemp)
160 $spamcpre sudo -u Debian-exim spamassassin -D -t --cf='score PYZOR_CHECK 0' <"$latest" &>$resultfile
161
162 # note: on some mail, its 1 line after the send-test-forward, on others its 2 with a blank inbetween.
163 # I use the sed -n to filter this.
164 raw_results="$(tail $resultfile | grep -A2 -Fx /usr/local/bin/send-test-forward | tail -n+2 | sed -nr 's/^\([^)]*\) *//;s/=[^, ]*([, ]|$)/ /gp')"
165 for r in $raw_results; do
166 case $r in
167 # got this in an update 2022-01. dun care
168 T_SCC_BODY_TEXT_LINE|SCC_BODY_SINGLE_WORD) : ;;
169 # we have a new domain, ignore this.
170 # it seems like some versions of spamassassin do BODY_SINGLE_WORD, others dont, we dun care.
171 # bayes_00 is a new one indicating ham, we dont care if its missing.
172 BAYES_00|BODY_SINGLE_WORD|FROM_FMBLA_NEWDOM*|autolearn) : ;;
173 SPF_HELO_NEUTRAL)
174 # some of my domains use neutral spf, treat them the same.
175 results[SPF_HELO_PASS]=t
176 ;;
177 *)
178 results[$r]=t
179 ;;
180 esac
181 done
182 # debugging
183 # e results = ${!results[@]}
184 missing=()
185
186 keys=(DKIM_SIGNED DKIM_VALID{,_AU,_EF} SPF_HELO_PASS SPF_PASS TVD_SPACE_RATIO)
187 if [[ $to == *@gnu.org && $from == *@gnu.org ]]; then
188 keys=(ALL_TRUSTED TVD_SPACE_RATIO)
189 elif [[ $to == *@gnu.org ]]; then
190 # eggs has RCVD_IN_DNSWL_MED
191 keys+=(RCVD_IN_DNSWL_MED)
192 elif [[ $from == *@gnu.org ]]; then
193 # eggs has these
194 keys+=(RCVD_IN_DNSWL_MED DKIMWL_WL_HIGH)
195 fi
196
197 for t in ${keys[@]}; do
198 if [[ ${results[$t]} ]]; then
199 unset "results[$t]"
200 elif [[ $t == DKIM_VALID_EF && $from == *@[^.]*.[^.]*.[^.]* ]]; then
201 :
202 # third level domains dont hit this. its because
203 # /usr/share/perl5/Mail/SpamAssassin/Plugin/DKIM.pm checks
204 # if its signed with the registryboundaries domain. afaik:
205 # we need the actual domain to sign it, this would result in
206 # a second signature. I only use second level domains for
207 # testing atm, fsf doesnt use them for anything but the
208 # forum and I dont expect that to have any deliverability
209 # problems. So, not bothering atm.
210 else
211 missing+=($t)
212 fi
213 done
214 if (( ${#results[@]} || ${#missing[@]} )); then
215 printf "$HOSTNAME spamtest %s\n" "$latest"
216 if (( ${#results[@]} )); then
217 printf "unexpected %s" "${!results[*]} "
218 fi
219 if (( ${#missing[@]} )); then
220 printf "missing %s" "${missing[*]}"
221 fi
222 echo # ends our printf string buildup
223 cat $resultfile
224 echo mailtest-check: end of spam debug results
225 # lets just handle 1 failure at a time in interactive mode.
226 if $int; then
227 echo mailtest-check: from: $from, to: $to
228 exit 0
229 fi
230
231 # less verbose debug output, commented since I might want it another time.
232 # if $int; then
233 # echo mailtest-check: cat $latest:
234 # cat $latest
235 # echo mailtest-check: end of cat
236 # echo "$(tput setaf 5 2>/dev/null ||:)█$(tput sgr0 2>/dev/null||:)%.0s" $(eval echo "{1..${COLUMNS:-60}}")
237 #fi
238 fi
239 rm -f $resultfile
240 unexpected=$(( unexpected + ${#results[@]} ))
241 for miss in ${missing[@]}; do
242 # We expect dns reputation services to go down from time to time, so
243 # we count them separately and alert differently.
244 case $miss in
245 RCVD_IN_DNSWL_MED|DKIMWL_WL_HIGH)
246 missing_dnswl+=1
247 ;;
248 *)
249 unexpected+=1
250 ;;
251 esac
252 done
253 fi # if spamdpid
254 fi # if $slow
255
256 now=$EPOCHSECONDS
257 age_sec=$(( now - last_sec ))
258 e $((age_sec / 60)):$(( age_sec % 60 )) ago. to:$to from:$from $latest
259
260 # usec = unix seconds
261 pr <<EOF
262 mailtest_check_last_usec{folder="$folder",from="$from"} $last_sec
263 EOF
264 done # end for from in ${froms[@]}
265 done # end for folder in ${folders[@]}
266 if $slow; then
267 pr <<EOF
268 mailtest_check_missing_dnswl $missing_dnswl
269 mailtest_check_unexpected_spamd_results $unexpected
270 EOF
271 fi
272
273 dir=/var/lib/prometheus/node-exporter
274 if [[ -e $dir ]]; then
275 mv $dir/mailtest-check.prom.$$ $dir/mailtest-check.prom
276 # note: node_textfile_mtime_seconds will tell us when this last happened. useful for debugging.
277 fi
278 }
279
280 loop-main() {
281 # When running under systemd, the system just started. Ve nice and
282 # give programs some time to finish their startup.
283 sleep 5
284 while true; do
285 premain_sec=$EPOCHSECONDS
286 main
287 sleep $(( 300 - ( EPOCHSECONDS - premain_sec ) ))
288 done
289 }
290
291
292 if [[ $INVOCATION_ID ]]; then
293 loop-main
294 else
295 main
296 fi
297
298 exit 0