various minor fixes and improvements
[distro-setup] / mailtest-check
index d5d30aa6ae2f275654ed391053bbaa1bafb87657..7ffa90a3f0082ff773710c5883e28e7e0f675f62 100755 (executable)
@@ -1,17 +1,40 @@
 #!/bin/bash
 #!/bin/bash
+# I, Ian Kelling, follow the GNU license recommendations at
+# https://www.gnu.org/licenses/license-recommendations.en.html. They
+# recommend that small programs, < 300 lines, be licensed under the
+# Apache License 2.0. This file contains or is part of one or more small
+# programs. If a small program grows beyond 300 lines, I plan to switch
+# its license to GPL.
 
 
-# Usage: mail-test-check [slow] [anything]
+# Copyright 2024 Ian Kelling
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+# Usage: mailtest-check [slow] [int|nonint]
 #
 # slow: do slow checks, like spamassassin
 #
 #
 # slow: do slow checks, like spamassassin
 #
-# anything: consider non-interactive, dont print unless something went
+# for  non-interactive, dont print unless something went
 # wrong
 
 # wrong
 
+#set -x
 
 
-source /b/errhandle/err
 
 [[ $EUID == 0 ]] || exec sudo -E "${BASH_SOURCE[0]}" "$@"
 
 
 [[ $EUID == 0 ]] || exec sudo -E "${BASH_SOURCE[0]}" "$@"
 
+source /b/bash-bear-trap/bash-bear
+
 shopt -s nullglob
 
 e() { $int || return 0; printf "mailtest-check: %s\n" "$*"; }
 shopt -s nullglob
 
 e() { $int || return 0; printf "mailtest-check: %s\n" "$*"; }
@@ -19,8 +42,8 @@ e() { $int || return 0; printf "mailtest-check: %s\n" "$*"; }
 getspamdpid() {
   if [[ ! $spamdpid || ! -d /proc/$spamdpid ]]; then
     # try twice in case we are restarting, it happens.
 getspamdpid() {
   if [[ ! $spamdpid || ! -d /proc/$spamdpid ]]; then
     # try twice in case we are restarting, it happens.
-    for i in 1 2; do
-      spamdpid=$(systemctl show --property MainPID --value spamassassin | sed 's/^[10]$//' ||:)
+    for (( i=0; i<2; i++ )); do
+      spamdpid=$(systemctl show --property MainPID --value $spamd_ser | sed 's/^[10]$//' ||:)
       if [[ $spamdpid ]]; then
         break
       fi
       if [[ $spamdpid ]]; then
         break
       fi
@@ -29,8 +52,145 @@ getspamdpid() {
   fi
 }
 
   fi
 }
 
+parse-rspamd() {
+  # rspamc uses $3.
+  awk '$1 == "Symbol:" && $2 !~ /\(0\.00\)/ && $3 !~ /\(0\.00\)/ {print $2}' | sed 's/(.*//'
+}
+
+rspamc-process() {
+
+  # note, this could in theory break since we aren't limiting it to the
+  # specific header. but that is unlikely, I'm doing all the header generation.
+  # example header:
+  # X-Spam_report: Action: no action
+  # Symbol: HFILTER_HOSTNAME_UNKNOWN(2.50)
+  # Symbol: RCVD_COUNT_TWO(0.00)
+  # Symbol: FROM_EQ_ENVFROM(0.00)
+  # Symbol: DMARC_POLICY_ALLOW(-0.50)
+  # Symbol: TO_DN_NONE(0.00)
+  # Symbol: TO_MATCH_ENVRCPT_SOME(0.00)
+  # Symbol: RCVD_TLS_LAST(0.00)
+  # Symbol: RBL_SENDERSCORE_FAIL(0.00)
+  # Symbol: R_DKIM_ALLOW(-0.20)
+  # Symbol: MIME_GOOD(-0.10)
+  # Symbol: MID_RHS_MATCH_FROM(0.00)
+  # Symbol: RCVD_IN_DNSWL_FAIL(0.00)
+  # Symbol: SINGLE_SHORT_PART(0.00)
+  # Symbol: R_SPF_ALLOW(-0.20)
+  # Symbol: ARC_NA(0.00)
+  # Symbol: ASN(0.00)
+  # Symbol: FROM_NO_DN(0.00)
+  # Symbol: MIME_TRACE(0.00)
+  # Symbol: MISSING_XM_UA(0.00)
+  # Symbol: RCPT_COUNT_THREE(0.00)
+  # Symbol: DKIM_TRACE(0.00)
+  # Message-ID: E1sLckD-004Ucv-P2@je.b8.nz
+
+  if [[ $to == jtuttle@gnu.org ]]; then
+    raw_results=$($spamcpre sudo -u _rspamd rspamc --helo=mail.iankelling.org --hostname=mail.iankelling.org  <"$latest" |& parse-rspamd)
+  else
+    raw_results=$( parse-rspamd <"$latest")
+  fi
+  for r in $raw_results; do
+    case $r in
+      # based on my spamassassin experience, these may change and are not important.
+      RCVD_IN_DNSWL_MED|RCVD_DKIM_ARC_DNSWL_MED) : ;;
+      *)
+        results[$r]=t
+        ;;
+    esac
+  done
+  keys=(DMARC_POLICY_ALLOW R_DKIM_ALLOW MIME_GOOD R_SPF_ALLOW)
+  for t in  ${keys[@]}; do
+    if [[ ${results[$t]} ]]; then
+      unset "results[$t]"
+    else
+      missing+=($t)
+    fi
+  done
+}
+
+spamc-process() {
+  # pyzor fails for our test message, so dont put useless load on their
+  # servers.
+  # example line that sed is parsing:
+  # (-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
+  # add -D for debug info. i haven't found it to be useful so it is off by default
+  resultstr=$($spamcpre sudo -u Debian-exim spamassassin -t --cf='score PYZOR_CHECK 0' <"$latest" 2>&1)
+  #resultstr=$($spamcpre sudo -u _rspamd rspamc  <"$latest" 2>&1)
+
+  # note: on some mail, its 1 line after the send-test-forward,
+  # on others its 2 with a blank in between.  I use the sed -n to
+  # filter this.
+  ## spamassassin parsing. disabled, using rspamd
+  raw_results="$(printf "%s\n" "$resultstr"| tail | grep -A2 -Fx /usr/local/bin/send-test-forward | tail -n+2 | sed -nr 's/^\([^)]*\) *//;s/=[^, ]*([, ]|$)/ /gp')"
+
+  # consider results we want to ignore or pre-process in some way.
+  for r in $raw_results; do
+    case $r in
+      # This came in t12, but its just dkim + spf, and my
+      # systems aren't all t12, so ignore it for now.
+      DMARC_PASS) : ;;
+      # got this in an update 2022-01. dun care
+      T_SCC_BODY_TEXT_LINE|SCC_BODY_SINGLE_WORD) : ;;
+      # we have a new domain, ignore this.
+      # it seems like some versions of spamassassin do BODY_SINGLE_WORD, others dont, we dun care.
+      # bayes_00 is a new one indicating ham, we dont care if its missing.
+      BAYES_00|BODY_SINGLE_WORD|FROM_FMBLA_NEWDOM*|autolearn) : ;;
+
+      # These have somewhat randomly been added and removed, resulting in useless alerts, so ignore them.
+      RCVD_IN_DNSWL_MED|DKIMWL_WL_HIGH) : ;;
+
+      SPF_HELO_NEUTRAL)
+        # some of my domains use neutral spf, treat them the same.
+        results[SPF_HELO_PASS]=t
+        ;;
+      *)
+        results[$r]=t
+        ;;
+    esac
+  done
+  # debugging
+  # e results = ${!results[@]}
+
+  keys=(DKIM_SIGNED DKIM_VALID{,_AU,_EF} SPF_HELO_PASS SPF_PASS TVD_SPACE_RATIO)
+  if [[ $to == *@gnu.org && $from == *@gnu.org ]]; then
+    keys=(ALL_TRUSTED TVD_SPACE_RATIO)
+    # from eggs had DKIMWL_WL_HIGH sometime in 2022, then DKIMWL_WL_MED unti march 2023
+  fi
+
+  for t in  ${keys[@]}; do
+    if [[ ${results[$t]} ]]; then
+      unset "results[$t]"
+    elif [[ $t == DKIM_VALID_EF && $from == *@[^.]*.[^.]*.[^.]* ]]; then
+      :
+      # third level domains dont hit this. its because
+      # /usr/share/perl5/Mail/SpamAssassin/Plugin/DKIM.pm checks
+      # if its signed with the registryboundaries domain. afaik:
+      # we need the actual domain to sign it, this would result in
+      # a second signature. I only use second level domains for
+      # testing atm, fsf doesnt use them for anything but the
+      # forum and I dont expect that to have any deliverability
+      # problems.  So, not bothering atm.
+    else
+      missing+=($t)
+    fi
+  done
+}
 
 #### begin arg processing ####
 
 #### begin arg processing ####
+
+# choose between rspamd and spamassassin
+use_rspamd=false
+if $use_rspamd; then
+  spamd_ser=rspamd
+else
+  spamd_ser=spamd
+  if systemctl cat spamassassin &>/dev/null; then
+    spamd_ser=spamassassin
+  fi
+fi
+
 # spamassassin checking takes about 8 seconds.
 slow=false
 if [[ $1 == slow ]]; then
 # spamassassin checking takes about 8 seconds.
 slow=false
 if [[ $1 == slow ]]; then
@@ -62,6 +222,8 @@ fi
 # TODO, get je to deliver the local mailbox: /m/md/INBOX
 # dovecot appears to setup, i can t be sure.
 
 # TODO, get je to deliver the local mailbox: /m/md/INBOX
 # dovecot appears to setup, i can t be sure.
 
+maini=0
+
 source /a/bin/bash_unpublished/source-state
 
 doprom=false
 source /a/bin/bash_unpublished/source-state
 
 doprom=false
@@ -88,7 +250,8 @@ main() {
       ;;
     *)
       folders=(/m/md/l/testignore)
       ;;
     *)
       folders=(/m/md/l/testignore)
-      froms=(testignore@je.b8.nz testignore@expertpathologyreview.com testignore@amnimal.ninja ian@iankelling.org z@zroe.org iank@gnu.org)
+      # save some cpu cycles
+      froms=(testignore@je.b8.nz testignore@expertpathologyreview.com testignore@amnimal.ninja ian@iankelling.org z@zroe.org)
       if ! $int; then
         ### begin rsyncing fencepost email ###
         # We dont want to exit if rsync fails, that will get caught by
       if ! $int; then
         ### begin rsyncing fencepost email ###
         # We dont want to exit if rsync fails, that will get caught by
@@ -106,7 +269,7 @@ main() {
           if (( try_left < timeout )); then
             timeout=$try_left
           fi
           if (( try_left < timeout )); then
             timeout=$try_left
           fi
-          if timeout $timeout 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; then
+          if timeout $timeout rsync --chown iank:iank -e "ssh -oIdentitiesOnly=yes -F none -i /root/.ssh/jtuttle" -t --inplace -r 'jtuttle@fencepost.gnu.org:/home/j/jtuttle/Maildir/new/' /m/md/l/testignore/new; then
             did_rsync=true
           else
             sleep 4
             did_rsync=true
           else
             sleep 4
@@ -137,32 +300,26 @@ EOF
   fi
   e spamdpid: $spamdpid
   if [[ ! $spamdpid ]]; then
   fi
   e spamdpid: $spamdpid
   if [[ ! $spamdpid ]]; then
-    echo mailtest spamd pid not found. systemctl status spamassassin:
-    systemctl status spamassassin
+    echo mailtest spamd pid not found. systemctl status $spamd_ser:
+    systemctl status $spamd_ser
   fi
   tmpfile=$(mktemp)
   declare -i unexpected=0
   for folder in ${folders[@]}; do
   fi
   tmpfile=$(mktemp)
   declare -i unexpected=0
   for folder in ${folders[@]}; do
+    awk '/^Subject: / {t=$4}; /^From: / {f=$2}; ENDFILE {print t, f, FILENAME}' $folder/new/* $folder/cur/* | sort -rn >$tmpfile
     for from in ${froms[@]}; do
       declare -i missing_dnswl=0
     for from in ${froms[@]}; do
       declare -i missing_dnswl=0
-      declare -i dnsfail=0
+      #declare -i dnsfail=0
       declare -i unexpected=0
       latest=
       last_sec=0
       declare -i unexpected=0
       latest=
       last_sec=0
+      tmp=$(awk '$2 == "'$from'" {print $1,$3; exit}' $tmpfile)
+      read -r last_sec latest <<<"$tmp"
+      if [[ ! $latest ]]; then
 
 
-      if ! grep -rlFx "From: $from" $folder/{new,cur} >$tmpfile; then
         echo "no message found from: $from"
         continue
       fi
         echo "no message found from: $from"
         continue
       fi
-      # webmail sends them to cur it seems
-      while read -r file; do
-        file_sec=$(awk '/^Subject: / {print $4}' $file)
-        if [[ $file_sec ]] && (( file_sec > last_sec )); then
-          latest=$file
-          last_sec="$file_sec"
-        fi
-      done <$tmpfile
-      rm -f $tmpfile
 
       to=$(awk '/^Envelope-to: / {print $2}' $latest)
 
 
       to=$(awk '/^Envelope-to: / {print $2}' $latest)
 
@@ -175,67 +332,20 @@ EOF
           if [[ $(readlink /proc/$$/ns/net) != "$(readlink /proc/$spamdpid/ns/net)" ]]; then
             spamcpre="nsenter -t $spamdpid -n -m"
           fi
           if [[ $(readlink /proc/$$/ns/net) != "$(readlink /proc/$spamdpid/ns/net)" ]]; then
             spamcpre="nsenter -t $spamdpid -n -m"
           fi
+          missing=()
           unset results
           declare -A results
           unset results
           declare -A results
-          # pyzor fails for our test message, so dont put useless load on their
-          # servers.
-          # example line that sed is parsing:
-          # (-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
-          resultfile=$(mktemp)
-          $spamcpre sudo -u Debian-exim spamassassin -D -t --cf='score PYZOR_CHECK 0' <"$latest" &>$resultfile
-
-          # note: on some mail, its 1 line after the send-test-forward, on others its 2 with a blank inbetween.
-          # I use the sed -n to filter this.
-          raw_results="$(tail $resultfile | grep -A2 -Fx /usr/local/bin/send-test-forward | tail -n+2 | sed -nr 's/^\([^)]*\) *//;s/=[^, ]*([, ]|$)/ /gp')"
-          for r in $raw_results; do
-            case $r in
-              # got this in an update 2022-01. dun care
-              T_SCC_BODY_TEXT_LINE|SCC_BODY_SINGLE_WORD) : ;;
-              # we have a new domain, ignore this.
-              # it seems like some versions of spamassassin do BODY_SINGLE_WORD, others dont, we dun care.
-              # bayes_00 is a new one indicating ham, we dont care if its missing.
-              BAYES_00|BODY_SINGLE_WORD|FROM_FMBLA_NEWDOM*|autolearn) : ;;
-              SPF_HELO_NEUTRAL)
-                # some of my domains use neutral spf, treat them the same.
-                results[SPF_HELO_PASS]=t
-                ;;
-              *)
-                results[$r]=t
-                ;;
-            esac
-          done
-          # debugging
-          # e results = ${!results[@]}
-          missing=()
+          # It would be useful for debugging & development to optionally
+          # run rspamc here but I haven't totally figured out
+          # rspamc, i might need to pass --helo=helo_string to avoid
+          # hostname_unknown result.
 
 
-          keys=(DKIM_SIGNED DKIM_VALID{,_AU,_EF} SPF_HELO_PASS SPF_PASS TVD_SPACE_RATIO)
-          if [[ $to == *@gnu.org && $from == *@gnu.org ]]; then
-            keys=(ALL_TRUSTED TVD_SPACE_RATIO)
-          elif [[ $to == *@gnu.org ]]; then
-            # eggs has RCVD_IN_DNSWL_MED
-            keys+=(RCVD_IN_DNSWL_MED)
-          elif [[ $from == *@gnu.org ]]; then
-            # eggs has this. it used to have DKIMWL_WL_HIGH sometime in 2022
-            keys+=(RCVD_IN_DNSWL_MED)
+          if $use_rspamd; then
+            rspamc-process
+          else
+            spamc-process
           fi
 
           fi
 
-          for t in  ${keys[@]}; do
-            if [[ ${results[$t]} ]]; then
-              unset "results[$t]"
-            elif [[ $t == DKIM_VALID_EF && $from == *@[^.]*.[^.]*.[^.]* ]]; then
-              :
-              # third level domains dont hit this. its because
-              # /usr/share/perl5/Mail/SpamAssassin/Plugin/DKIM.pm checks
-              # if its signed with the registryboundaries domain. afaik:
-              # we need the actual domain to sign it, this would result in
-              # a second signature. I only use second level domains for
-              # testing atm, fsf doesnt use them for anything but the
-              # forum and I dont expect that to have any deliverability
-              # problems.  So, not bothering atm.
-            else
-              missing+=($t)
-            fi
-          done
           if (( ${#results[@]} || ${#missing[@]} )); then
             printf "$HOSTNAME spamtest %s\n" "$latest"
             if (( ${#results[@]} )); then
           if (( ${#results[@]} || ${#missing[@]} )); then
             printf "$HOSTNAME spamtest %s\n" "$latest"
             if (( ${#results[@]} )); then
@@ -245,7 +355,7 @@ EOF
               printf "missing %s" "${missing[*]}"
             fi
             echo # ends our printf string buildup
               printf "missing %s" "${missing[*]}"
             fi
             echo # ends our printf string buildup
-            cat $resultfile
+            if [[ $resultstr ]]; then printf "%s\n" "$resultstr"; fi
             echo mailtest-check: end of spam debug results
             # lets just handle 1 failure at a time in interactive mode.
             if $int; then
             echo mailtest-check: end of spam debug results
             # lets just handle 1 failure at a time in interactive mode.
             if $int; then
@@ -258,13 +368,12 @@ EOF
             #   echo mailtest-check: cat $latest:
             #   cat $latest
             #   echo mailtest-check: end of cat
             #   echo mailtest-check: cat $latest:
             #   cat $latest
             #   echo mailtest-check: end of cat
-            #   echo "$(tput setaf 5 2>/dev/null ||:)█$(tput sgr0 2>/dev/null||:)%.0s" $(eval echo "{1..${COLUMNS:-60}}")
             #fi
           fi
             #fi
           fi
-          rm -f $resultfile
           for r in ${results[@]}; do
             case $r in
           for r in ${results[@]}; do
             case $r in
-              # iank: for when we want to handle dns errors differently
+              # iank: for when we want to handle dns errors differently.
+              # also uncomment declaration of dnsfail above.
               # DKIM_INVALID|T_SPF_TEMPERROR|T_SPF_HELO_TEMPERROR)
               #   dnsfail+=1
               #   ;;
               # DKIM_INVALID|T_SPF_TEMPERROR|T_SPF_HELO_TEMPERROR)
               #   dnsfail+=1
               #   ;;
@@ -274,14 +383,10 @@ EOF
             esac
           done
           for miss in ${missing[@]}; do
             esac
           done
           for miss in ${missing[@]}; do
-            # We expect dns failures from time to time, so
-            # we count them separately and alert differently.
+            # At some point we had annoying dns failures that we couldn't solve so we
+            # we counted dns fail related results separately and alert differently.
+            # DKIM_VALID|DKIM_VALID_AU|DKIM_VALID_EF|SPF_HELO_PASS|SPF_PASS|
             case $miss in
             case $miss in
-              # iank: dns fail
-              # DKIM_VALID|DKIM_VALID_AU|DKIM_VALID_EF|SPF_HELO_PASS|SPF_PASS|
-              RCVD_IN_DNSWL_MED|DKIMWL_WL_HIGH)
-                missing_dnswl+=1
-                ;;
               *)
                 unexpected+=1
                 ;;
               *)
                 unexpected+=1
                 ;;
@@ -306,13 +411,14 @@ mailtest_check_last_usec{folder="$folder",from="$from"} $last_sec
 EOF
     done # end for from in ${froms[@]}
   done # end for folder in ${folders[@]}
 EOF
     done # end for from in ${froms[@]}
   done # end for folder in ${folders[@]}
+  rm -f $tmpfile
 
   dir=/var/lib/prometheus/node-exporter
   path=$dir/mailtest-check.prom.$$
   if $doprom && [[ -e $dir  ]]; then
     for l in "${p_unexpected_spamd_results[@]}"; do
       printf "%s\n" "$l" >>$path
 
   dir=/var/lib/prometheus/node-exporter
   path=$dir/mailtest-check.prom.$$
   if $doprom && [[ -e $dir  ]]; then
     for l in "${p_unexpected_spamd_results[@]}"; do
       printf "%s\n" "$l" >>$path
-      done
+    done
     for l in "${p_missing_dnswl[@]}"; do
       printf "%s\n" "$l" >>$path
     done
     for l in "${p_missing_dnswl[@]}"; do
       printf "%s\n" "$l" >>$path
     done
@@ -331,12 +437,13 @@ loop-main() {
   while true; do
     premain_sec=$EPOCHSECONDS
     main
   while true; do
     premain_sec=$EPOCHSECONDS
     main
+    maini=$((maini + 1))
     sleep $(( 300 - ( EPOCHSECONDS - premain_sec ) ))
   done
 }
 
 
     sleep $(( 300 - ( EPOCHSECONDS - premain_sec ) ))
   done
 }
 
 
-if [[ $INVOCATION_ID ]]; then
+if [[ $PPID == 1 ]]; then
   loop-main
 else
   main
   loop-main
 else
   main