Erfahrungsbericht CRM114 als After-Queue-Spam-Filter für Postfix

obsigna

Well-Known Member
Die Vorgeschichte
Zwischen 2003 und 2012 war ich für eine Reihe von kleinen Mailservern (damals Postfix/Cyrus IMAP) verantwortlich und mit den verschiedenen „Before Queue“-Maßnahmen konnten wir damals schon etwa 90 % des hereinkommenden Spams abblocken. Allerdings gab es und gibt es immer noch diesen Rest, der auf die eine oder andere Weise dann doch durchkommt, und dafür hatte ich damals einen After-Queue-Spam-Filter mit CRM114 aufgesetzt. Der basiert auf einem „Markov Random Field“-Algorithmus der im Gegensatz zum einfachen Bayes-Spamfilter ganze Phrasen statistisch auswertet und von daher mit einer wesentlich höheren Präzision arbeitet. Das konnte ich damals bestätigen, mit dem Bayes-Spamfilter in SpamAssassin lag die Fehlerrate bei uns bei ca. 30 %. Beim CRM114, dessen Datensatz strikt via TOE (Training on Error) gefüttert wurde, lag die falsche Klassifizierung bei etwa 1 von 2000, d. h. 0,025 % falsch positive und 0,025 % falsch negative E-Mails. Die Gesamteffizienz der Spam-Bekämpfung lag also bei 99,9975 % (90 % "Before Queue" + 9,9975 % "After Queue"), d.h. nur 1 von 40000 Spams schaffte es in den Posteingang eines Benutzers.

11 Jahre später
Anfang diesen Jahres, habe ich wieder einen Mail-Server, diesmal mit Postfix/Dovecot unter FreeBSD aufgesetzt, wieder für eine kleine Firma, etwa 50 Benutzer. Einige der Konten waren mehr als 20 Jahre in Betrieb, und trotz der „Before Queue“-Maßnahmen kam da unerträglich viel Müll durch. Einige Benutzer haben mir geholfen, eine Stichprobe von knapp 1500-Spam-Mails zu sammeln. Aus einigen einschlägigen Postfächern haben wir dann noch 1500 (verifiziert) gute Mails zusammengestellt, und CRM114 mit dem Markov-Algorithmus damit abwechselnd per TOE trainiert, nämlich jeweils 1 Spam-Mail, dann 1 Good-Mail.

Vor einer Woche habe ich den CRM114-Filter schließlich aktiviert. An den ersten 2 Tagen gab es noch eine Handvoll falsche Klassifizierungen, die trainiert wurden. Seitdem ist Ruhe im Karton - 100 Spams pro Tag, die nicht ihr Ziel erreichen sondern in einem speziellen Junk-Konto landen.

Wenn die Empfänger im Schnitt 1 min ihrer Arbeitszeit mit Spam opfern (manche Spams sind schneller aussortiert, bei anderen muß man in die Header schauen bzw. schauen lassen, um zu sehen daß es sich z.B. um eine Fake-Rechnung handelt), dann kommt man bei Arbeitskosten von ca. 50 €/h im Jahr auf Spam-Kosten für ein kleines Unternehmen von 100 min/d * 365 d / (60 min/h) * 50 €/h = 30416,67 €. Das haben wir jetzt gespart, und wieder frage ich mich, wieso CRM114 so ein Schattendasein führt.
 
Zuletzt bearbeitet:
Würdest Du beschreiben, wie Du crm114 in Deinen Mailstack einbaust und/oder könntest Du ein gutes HowTo nennen?
 
Würdest Du beschreiben, wie Du crm114 in Deinen Mailstack einbaust und/oder könntest Du ein gutes HowTo nennen?

1. Ich habe sowieso vor einen BLog-Artikel zum Thema zu verfassen - als Erweiterung von https://obsigna.com/edit/articles/1539726598.html. Ich arbeite aber im Moment an dringenden Projekten mit Dead-Line Ende August. Vorher wird das nichts.

2. Die Präzision von lernenden Mail-Filtern steht und fällt mit der Qualität des Lern-Materials. Bei Mail-Servern mit einer geringen Zahl von Benutzern hat man üblicherweise Schwierigkeiten genügend einschlägige Spams für den ersten TOE-Lernvorgang zu sammeln. Bei meinem Home-Mail-Server kommen max. 10 im Monat zusammen und nach 3 Monaten sind die nicht mehr einschlägig, weil dann schon wieder eine neue Spam-Sau durch’s Dorf läuft. Das ist auch der Hauptgrund, wieso ich das nicht schon längst mal wieder installiert hatte, denn für einen privaten Mail-Server lohnt die Mühe nicht.

3. Bei Firmen baucht man eine Betriebsvereinbarung, die die Benutzung des Mail-Accounts und die Art und Weise der Mail-Filterung regelt. Hier ein altes Dokument vom BSI, das sehr schön die rechtliche Seite behandelt -- https://www.yumpu.com/de/document/v...en-unerwunschte-e-mails-erkennen-und-abwehren:
6.5.4 Tipps für rechtskonforme Mailfilterung

Allein das Vorliegen der Tatbestandsmerkmale sagt juristisch allerdings noch nichts darüber aus, ob letztlich auch eine Strafbarkeit vorliegt.

So schließt das Einverständnis des Empfängers in die Filtermaßnahmen bereits die Tatbestandsmäßigkeit der §§ 206 und 303a StGB und damit auch die Strafbarkeit aus. Äußert der Empfänger seine Zustimmung zum Löschen der Mails, so wird das Vertrauen der Allgemeinheit in die Wahrung des Post- und Fernmeldegeheimnisses nicht mehr berührt. Dieses Einverständnis muss allerdings ausdrücklich und insbesondere vor Beginn der Filterung von allen Betroffenen erteilt werden.

Eine mutmaßliche Einwilligung der Empfänger ist zumindest bei Spam nicht anzunehmen. ...
Ohne die ausdrückliche Zustimmung der Benutzer läßt man es besser, denn der Admin und dessen Vorgesetzter stehen sonst mit einem Bein im Gefängnis. In normalen Firmen mit durchgehend kollegialem Verhältnis sollte das aber kein Problem sein. Da reicht eine halbe Seite.

4. Die Umsetzung fängt mit der Einrichtung eines separaten Junk-Kontos und von Auto-Subscribe-Ordnern „Learn Junk“ unter jedem User-Account an -- bei Dovecot unter namespace inbox:
Code:
  mailbox "Learn Junk" {
    auto = subscribe
}

5. Man läßt die Benutzer ihre Spams nicht löschen sondern sie sollen sie nach „Learn Junk“ abschieben. Wenn der Mail-Client auch filtert, dann sollen auch diese Mails nicht gelöscht werden sondern auch hin und wieder nach „Learn Junk“ verschoben werden.

6. Wenn genügend Spam-Mails zusammen gekommen sind, dann startet man den ersten TOE-Lernvorgang, in etwa wie folgt:
cd /var/mail/users/junk\@example.com
find .Learn\ Good/cur -not -type d -print > ~/good.txt
find .Learn\ Junk/cur -not -type d -print > ~/junk.txt
paste -d '\n' ~/good.txt ~/junk.txt > ~/goodjunk.txt
./initlearn.sh ~/goodjunk.txt -s

Unter „Learn Good“ und „Learn Spam“ hatte ich jeweils etwa 1500 Mails gesammelt.

Bash:
#!/bin/sh

if [ $1 != "" ] && [ -f "$1" ]; then
   learn=`cat "$1"`
   IFS=$'\n'

   for path in $learn ; do
      if [ -z "${path##*Good*}" ]; then
         class=good
      elif [ -z "${path##*Junk*}" ]; then
         class=junk
      else
         class=""
      fi

      echo "$path"
      if [ "$class" != "" ]; then
         classify -l $class $2 "$path"
      else
         classify $2 "$path"
         if [ "$?" = "1" ]; then
            mv "$path" "/var/mail/users/junk@example.com/cur/"
            chown 99999 "/var/mail/users/junk@example.com/$path"
         fi
      fi
   done
fi

7. Das Kommandozeilen-Programm classify ist ein Wrapper in C für normalizemime.cc und libcrm114 den ich für meine Zwecke geschrieben habe:
C:
//  classify.c
//  CRM114 classify & learn
//
//  Created by Dr. Rolf Jansen on 2023-07-23.
//  Copyright © 2023 Cyclaero Ltda. - Dr. Rolf Jansen. All rights reserved.


#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <syslog.h>
#include <unistd.h>
#include <sys/stat.h>

#include "crm114_sysincludes.h"
#include "crm114_config.h"
#include "crm114_structs.h"
#include "crm114_lib.h"

#include "normalize.h"


#define DB_FNAME "/var/mail/filter/crm114.db"

CRM114_MATCHRESULT classify(char *text, int len, CRM114_DATABLOCK **db)
{
   struct stat st;
   if (stat(DB_FNAME, &st) == 0 && S_ISREG(st.st_mode))
   {
      int fd;
      if ((fd = open(DB_FNAME, O_RDONLY)) != -1)
      {
         if (*db = malloc(st.st_size))
         {
            if (read(fd, *db , st.st_size) != st.st_size)
            {
               syslog(LOG_ERR, "Couldn't read the classifuer's datablock.\n");
               free(*db );
               close(fd);
               exit(-1);
            }
         }

         else
         {
            syslog(LOG_ERR, "Couldn't allocate memory for holding the classifuer's datablock.\n");
            close(fd);
            exit(-1);
         }

         close(fd);
      }

      else
      {
         syslog(LOG_ERR, "Couldn't open the classifuer's datablock file.\n");
         exit(-1);
      }
   }

   else
   {
      CRM114_CONTROLBLOCK *cb;

      if ((cb = crm114_new_cb()) == NULL)
      {
         syslog(LOG_ERR, "Couldn't allocate CRM114_CONTROLBLOCK.");
         exit(-1);
      };

      if (crm114_cb_setflags(cb, CRM114_MARKOVIAN|CRM114_MICROGROOM) != CRM114_OK)
      {
         syslog(LOG_ERR, "Couldn't set the classifier type CRM114_MARKOVIAN|CRM114_MICROGROOM.");
         exit(-1);
      };

      crm114_cb_setclassdefaults(cb);
      strcpy(cb->class[0].name, "good");
      strcpy(cb->class[1].name, "junk");

      cb->datablock_size = 8388608;    // 8 MB datablock size
      crm114_cb_setblockdefaults(cb);

      if ((*db  = crm114_new_db(cb)) == NULL)
      {
         syslog(LOG_ERR, "Couldn't create the datablock.\n");
         exit(-1);
      }
   }

   CRM114_MATCHRESULT result;
   CRM114_ERR err = crm114_classify_text(*db, text, len, &result);
   return (err == CRM114_OK) ? result : (CRM114_MATCHRESULT){};
}


int learn(char *text, int len, int class, CRM114_DATABLOCK **db)
{
   int rc = -1;
   if (crm114_learn_text(db, class, text, len) == CRM114_OK)
   {
      int fd;
      if ((fd = open(DB_FNAME, O_WRONLY|O_CREAT|O_TRUNC|O_EXLOCK, 0660)) != -1)
      {
         if (write(fd, *db, (*db)->cb.datablock_size) != -1)
            rc = 0;
         close(fd);
      }
   }

   return rc;
}


void usage(char *executable)
{
   const char *r = executable + strlen(executable);
   while (--r >= executable && r && *r != '/');
   printf("\nUsage: %s [-f] [-l (good|junk)] [-s] [-h] infile [outfile]\n"
          " no option       only classifiy the text passed by infile\n"
          " -f              force learning, i.e. do not ask the classifier whether it is necessary.\n"
          " -l (good|junk)  learn the text message as either of 'good' or 'junk'.\n"
          " -s              show the classification statistics.\n"
          " infile          the file to be classified and optionally learned, a dash (-) would be stdin.\n"
          " outfile         the file for storing the resulting text of mime normalization, a dash (-) would be stdout.\n"
          " -h              shows these usage instructions.\n\n", ++r);
}

int main(int argc, char *const argv[])
{
   int  rc = 0, cl = -1;
   bool force = false, showstats = false;
   char ch, *cmd = argv[0];

   while ((ch = getopt(argc, argv, "fl:sh")) != -1)
   {
      switch (ch)
      {
         case 'f':
            force = true;
            break;

         case 'l':
            if (optarg[4] == '\0'
             && (*(uint32_t *)optarg == *(uint32_t *)"good"
              || *(uint32_t *)optarg == *(uint32_t *)"junk"))
            {
               cl = (*optarg == 'g') ? 0 : 1;
               break;
            }
            else
            {
               usage(cmd);
               return 0;
            }

         case 's':
            showstats = true;
            break;

         case 'h':
         default:
            usage(cmd);
            return 0;
      }
   }

   argc -= optind;
   argv += optind;

   if (argc < 1 || force && cl < 0)
   {
      usage(cmd);
      return 0;
   }


   struct stat st = {};
   FILE *infile, *outfile;
   char *buffer, *b;
   ssize_t len = 0;

   if (*(uint16_t *)argv[0] == *(uint16_t *)"-")
   {
      #define bufsize 1232896
      if (buffer = b = malloc(bufsize + 1))
      {
         ssize_t siz = 0, tot = 0, cap = bufsize;
         while (buffer && (siz = read(0, b, bufsize - len)) > 0)
         {
            len += siz;
            tot += siz;
            if (len == bufsize)
               buffer = reallocf(buffer, (cap += bufsize) + 1), len = 0;
            b = buffer + tot;
         }

         if (buffer)
         {
            if (siz >= 0)
               buffer[len = tot] = '\0';
            else
            {
               free(buffer);
               return -1;
            }
         }
         else
            return -1;
      }
      else
         return -1;
   }

   else if (stat(argv[0], &st) != -1
         && S_ISREG(st.st_mode)
         && st.st_size > 0
         && (infile = fopen(argv[0], "r")))
   {
      if (buffer = malloc(st.st_size+1))
      {
         if (fread(buffer, st.st_size, 1, infile) == 1)
            buffer[len = st.st_size] = '\0';
         else
            free(buffer);
      }
      fclose(infile);
   }

   else
      return -1;

   char *text = NULL;
   len = normalize_data(buffer, &text);

   if (text)
   {
      CRM114_DATABLOCK  *db = NULL;
      CRM114_MATCHRESULT result = classify(text, len, &db);
      if (showstats)
         crm114_show_result("Classifier result", &result);

      if (cl != -1
       && (cl == 0 && (result.overall_pR <  10.0 || force)
        || cl == 1 && (result.overall_pR > -10.0 || force)))
      {
         learn(text, len, cl, &db);
         result = classify(text, len, &db);
         if (showstats)
            crm114_show_result("Classifier result", &result);
      }

      rc = result.bestmatch_index;

      if (argc >= 2)
      {
         if (*(uint16_t *)argv[1] == *(uint16_t *)"-")
            fwrite(text, len, 1, stdout);
         else if (outfile = fopen(argv[1], "w"))
         {
            fwrite(text, len, 1, outfile);
            fclose(outfile);
         }
         else
            return -1;
      }

      free(text);
   }

   else
      free(buffer);

   return rc;
}

Makefile:
#  BSD Makefile for building libtre, libcrm114 and classify
#
#  Created by Dr. Rolf Jansen on 2023-07-32.
#  Copyright © 2023 Dr. Rolf Jansen. All rights reserved.
#
#  Usage examples:
#    make
#    make clean
#    make update
#    make install clean
#    make clean install CDEFS="-DDEBUG"

CC = clang
CP = clang++
AR = ar

.if exists(.git)
.ifmake update
COMCNT != git pull origin >/dev/null 2>&1; git rev-list --count HEAD
.else
COMCNT != git rev-list --count HEAD
.endif
REVNUM != echo $(COMCNT) + 1 | bc
MODCNT != git status -s | wc -l
.if $(MODCNT) > 0
MODIED  = m
.else
MODIED  =
.endif
.else
REVNUM != cut -d= -f2 scmrev.xcconfig
.endif

.ifmake debug
CFLAGS = $(CDEFS) -g -O0 -DTESTING
.elifmake diagnostic
CFLAGS = $(CDEFS) -g -O0 -DDEBUG -DTESTING
.else
CFLAGS = $(CDEFS) -g0 -O3
STRIP = -s
.endif

.if $(MACHINE) == "i386" || $(MACHINE) == "amd64" || $(MACHINE) == "x86_64"
CFLAGS += -mssse3
.elif $(MACHINE) == "arm" || $(MACHINE) == "arm64" || $(MACHINE) == "aarch64"
CFLAGS += -fsigned-char
.endif

CFLAGS += -DSCMREV="\"$(REVNUM)$(MODIED)\"" -fno-pic -fvisibility=hidden -fstack-protector \
          -Wno-multichar -Wno-parentheses -Wno-switch -Wno-deprecated-declarations
LDFLAGS = -L/usr/local/lib

TRESRCS = tre/lib/regcomp.c \
          tre/lib/regerror.c \
          tre/lib/regexec.c \
          tre/lib/tre-ast.c \
          tre/lib/tre-compile.c \
          tre/lib/tre-match-approx.c \
          tre/lib/tre-match-backtrack.c \
          tre/lib/tre-match-parallel.c \
          tre/lib/tre-mem.c \
          tre/lib/tre-parse.c \
          tre/lib/tre-stack.c \
          tre/lib/xmalloc.c
TREOBJS = $(TRESRCS:.c=.o)
TRELIB  = libtre.a

CRMSRCS = libcrm114/crm114_base.c \
          libcrm114/crm114_markov.c \
          libcrm114/crm114_markov_microgroom.c \
          libcrm114/crm114_bit_entropy.c \
          libcrm114/crm114_hyperspace.c \
          libcrm114/crm114_svm.c \
          libcrm114/crm114_svm_lib_fncts.c \
          libcrm114/crm114_svm_quad_prog.c \
          libcrm114/crm114_fast_substring_compression.c \
          libcrm114/crm114_pca.c \
          libcrm114/crm114_pca_lib_fncts.c \
          libcrm114/crm114_matrix.c \
          libcrm114/crm114_matrix_util.c \
          libcrm114/crm114_datalib.c \
          libcrm114/crm114_vector_tokenize.c \
          libcrm114/crm114_strnhash.c \
          libcrm114/crm114_util.c \
          libcrm114/crm114_regex_tre.c
CRMOBJS = $(CRMSRCS:.c=.o)
CRMLIB  = libcrm114.a

all: $(TRESRCS) $(TRELIB) $(CRMSRCS) $(CRMLIB) filter/normalize.cpp filter/classify.c classify

depend:
    $(CC) $(CFLAGS) -E -MM $(TRESRCS) $(CRMSRCS) filter/normalize.cpp filter/classify.c > .depend

$(TREOBJS): Makefile
    $(CC) -std=gnu11 $(CFLAGS) -Wno-constant-logical-operand -DHAVE_CONFIG_H=1 -I../tre -I../tre/lib -c $< -o $@

$(TRELIB): $(TREOBJS)
    $(AR) -r $@ $(TREOBJS)

$(CRMOBJS): Makefile
    $(CC) -std=gnu11 $(CFLAGS) -I../libcrm114 -c $< -o $@

$(CRMLIB): $(CRMOBJS)
    $(AR) -r $@ $(CRMOBJS)

classify: $(TRELIB) $(CRMLIB) filter/normalize.cpp filter/classify.c
    $(CC) $(CFLAGS) -std=gnu11 -c ../filter/classify.c -I../libcrm114 -o classify.o
    $(CP) $(CFLAGS) -std=gnu++20 ../filter/normalize.cpp classify.o libtre.a libcrm114.a $(LDFLAGS) -liconv -o $@

clean:
    rm -rf ${.CURDIR}/obj
    mkdir -p ${.CURDIR}/obj/tre/lib
    mkdir -p ${.CURDIR}/obj/libcrm114

debug: all

update: clean all

install: classify
    install $(STRIP) classify /usr/local/bin/

8. Zur Einbindung in Postfix dient das Filter-Script filter.sh:
Bash:
#!/bin/sh
#
#  filter.sh
#  crm114 - classify
#
#  Created by Dr. Rolf Jansen on 2023-08-06.
#  Copyright © 2023 Cyclaero Ltda. - Dr. Rolf Jansen. All rights reserved.
#
#  Usage: filter.sh -f sender -- recipient1 [[[recipient2] recipient3] ...]

SENDMAIL="/usr/sbin/sendmail -G -i"
FILTERDIR=/var/spool/filter

# Exit codes from <sysexits.h>
EX_TEMPFAIL=75
EX_UNAVAILABLE=69

# Clean up when done or when aborting.
trap "/bin/rm -f in.$$" 0 1 2 3 15

cd $FILTERDIR || { echo $FILTERDIR does not exist; exit $EX_TEMPFAIL; }
/bin/cat > in.$$ || { echo Cannot save mail to file; exit $EX_TEMPFAIL; }

/usr/local/bin/classify in.$$
if [ "$?" = "1" ]; then
   echo "X-Orig-From-To: $@" | /bin/cat - in.$$ | $SENDMAIL -f "$2" -- "junk@example.com"
else
   $SENDMAIL "$@" < in.$$
fi

exit $?

In master.cf fügt man hinzu:
Code:
...
filter    unix  -       n       n       -       3       pipe
  flags=Rq user=filter null_sender=
  argv=/root/config/filter.sh -f ${sender} -- ${recipient}

Schließlich habe ich noch in main.cf:
Code:
...
smtpd_recipient_restrictions        = permit_mynetworks,
                                      permit_sasl_authenticated,
                                      check_recipient_access hash:/usr/local/etc/postfix/disposed_addresses,
                                      check_sender_access hash:/usr/local/etc/postfix/sender_whitelist,
                                      check_client_access hash:/usr/local/etc/postfix/client_whitelist,
                                      check_client_access pcre:/usr/local/etc/postfix/client_whitelist.pcre,
                                      check_policy_service unix:private/greyfix,
                                      check_recipient_access hash:/usr/local/etc/postfix/filtered_domains,
                                      permit
...

Und filtered_domains enthält:
Code:
example.com        FILTER filter:dummy
exampl1.com        FILTER filter:dummy
exampl2.com        FILTER filter:dummy
exampl3.com        FILTER filter:dummy
...

9. Los geht’s mit postfix reload.

Zum Schluß noch einige Maintenance-Scripts, von denen check.sh und learn.sh in /etc/crontab eingetragen gehören:
Bash:
#!/bin/sh
#
#  check.sh
#  crm114 - classify
#
#  Created by Dr. Rolf Jansen on 2023-07-23.
#  Copyright © 2023 Cyclaero Ltda. - Dr. Rolf Jansen. All rights reserved.
#
#  Usage: check.sh
#  shall be added to a cronjob for automatically moving all messages pretended to be learned as Junk to the "Check Junk" directory

/usr/bin/find /var/mail/users/*\@example.com/.Learn\ Junk/cur -not -type d -not -path "/var/mail/users/junk\@example.com/*" -print0 | /usr/bin/xargs -0 -n1 -I % /bin/mv % /var/mail/users/junk\@example.com/.Check\ Junk/cur/
/usr/bin/find /var/mail/users/*\@example.com/.Junk/cur -not -type d -not -path "/var/mail/users/junk\@example.com/*" -print0 | /usr/bin/xargs -0 -n1 -I % /bin/mv % /var/mail/users/junk\@example.com/.Check\ Junk/cur/
/usr/sbin/chown 99999 /var/mail/users/junk\@example.com/.Check\ Junk/cur/*
exit 0
Bash:
#!/bin/sh
#
#  learn.sh
#  crm114 - classify
#
#  Created by Dr. Rolf Jansen on 2023-07-23.
#  Copyright © 2023 Cyclaero Ltda. - Dr. Rolf Jansen. All rights reserved.
#
#  Usage: learn.sh
#  shall be added to a cronjob for automatically learning all messages in the "Learn Good" and "Learn Junk" directories

LEARNGOOD="/var/mail/users/junk@example.com/.Learn Good/cur"
if [ -d "$LEARNGOOD" ]; then

   /usr/bin/find "$LEARNGOOD" -not -type d -print0 | /usr/bin/xargs -0 -n1 -I % /root/config/good.sh "%"

fi

LEARNJUNK="/var/mail/users/junk@example.com/.Learn Junk/cur"
if [ -d "$LEARNJUNK" ]; then

   /usr/bin/find "$LEARNJUNK" -not -type d -print0 | /usr/bin/xargs -0 -n1 -I % /root/config/junk.sh "%"

fi

exit 0
Bash:
#!/bin/sh
#
#  good.sh
#  crm114 - classify
#
#  Created by Dr. Rolf Jansen on 2023-07-23.
#  Copyright © 2023 Cyclaero Ltda. - Dr. Rolf Jansen. All rights reserved.
#
#  Usage: good.sh <message-file-name> [nolearn]

if [ "$1" != "" ] && [ -e "$1" ]; then

   # Exit codes from <sysexits.h>
   EX_TEMPFAIL=75
   EX_UNAVAILABLE=69
   ORIGFROMTO=`/usr/bin/sed -n '/^X-Orig-From-To: /{s///;p;}' "$1"`

   if [ "$ORIGFROMTO" != "" ]; then

      # Clean up when done or when aborting.
      trap "/bin/rm -f good.$$" 0 1 2 3 15

      /usr/bin/sed -e '1,/^X-Orig-From-To:.*$/d' "$1" > /var/spool/filter/good.$$
      if [ "$2" != "nolearn" ]; then
         /usr/local/bin/classify -l good /var/spool/filter/good.$$
      fi
      sudo -u filter /usr/sbin/sendmail -G -i $ORIGFROMTO < /var/spool/filter/good.$$
      if [ "$?" = "0" ]; then
         /bin/rm -f "$1"
      fi

   else

      echo "The file '$1' does not contain a 'X-Orig-From-To:' header, and cannot be automatically proceessed."
      exit $EX_UNAVAILABLE

   fi
fi
Bash:
#!/bin/sh
#
#  junk.sh
#  crm114 - classify
#
#  Created by Dr. Rolf Jansen on 2023-07-23.
#  Copyright © 2023 Cyclaero Ltda. - Dr. Rolf Jansen. All rights reserved.
#
#  Usage: junk.sh <message-file-name>

if [ "$1" != "" ] && [ -e "$1" ]; then

   /usr/local/bin/classify -l junk "$1"
   /bin/rm -f "$1"

fi
 
Zuletzt bearbeitet:
Erstmal viele Daumen hoch fuer's posten; ein leidiges Thema!

Sidenote:
Der Link zum Blogartikel schreit nach basic-auth und zum Copyright sollte
dann auch ein Lizenzverweis gehoeren, um's noch runder zu machen :-)
 
Erstmal viele Daumen hoch fuer's posten; ein leidiges Thema!

Sidenote:
Der Link zum Blogartikel schreit nach basic-auth und zum Copyright sollte
dann auch ein Lizenzverweis gehoeren, um's noch runder zu machen :-)
Leider kann ich den obigen Beitrag nicht mehr bearbeiten. Der Link ist in der Tat falsch. Da muß das „edit/“ raus, sorry. Es ist ausserdem nicht Basic sondern Digest. Der richtige Link ist: https://obsigna.com/articles/1539726598.html.

Mein Verständnis von Lizenz ist, das alles erlaubt ist, was nicht verboten ist. Insofern ist die Null-Lizenz doch ziemlich eindeutig.

Der Copyright-Hinweis bedeutet nur, daß ich selber auf immer und ewig damit machen kann was ich will, selbst wenn jemand meinen Code mit einer restriktiven Lizenz versehen sollte. Ausserdem hat man ja auch seinen Stolz, nicht wahr?
 
Zurück
Oben