Artikel mit ‘qmail’ getagged

Zum Stand von Maildir-Support bei CentOS 5

Montag, 14. Februar 2011

Wir setzen auf den meisten unserer Server qmail bzw. netqmail als MTA ein und erfreuen uns dabei an den vielen Vorzügen, die das Maildir-Format bietet. Zwar kommt dabei meistens ein zusätzliches Tool wie vpopmail oder vmailmgr zum Tragen, was virtualisierte Mailuser bietet; auf unserer Hosting-Plattform Uberspace.de bieten wir aber ganz offiziellen Support für ein „echtes“ Maildir des betreffenden Systemusers. Die netqmail-Dokumentation erläutert dazu in INSTALL.maildir:

The system administrator can set up Maildir as the default for everybody by creating a maildir in the new-user template directory and replacing ./Mailbox with ./Maildir/ in /var/qmail/rc.

Und so sieht das Setup bei uns dann auch aus: Ein /var/qmail/bin/maildirmake /etc/skel/Maildir legt ein Standard-Maildir an, was beim Anlegen eines neuen Users automatisch in dessen Home-Verzeichnis übertragen wird, und qmail-local stellt Mails, die an den Systemuser adressiert sind, dorthin zu.

Nicht völlig überraschend zeigt sich aber, dass traditionelle Mailboxen in einem zentralen Verzeichnis wie z.B. /var/spool/mail derart fest im Betriebssystem verankert sind, dass es mehr als einen Klimmzug braucht, das System sauber an Maildirs anzupassen. Das fängt schon ganz vorne an:

useradd

Beim Anlegen von Benutzern wird standardmäßig eine (leere) Mailbox in /var/spool/mail/$USER angelegt, bzw. in dem Verzeichnis, das in der /etc/login.defs (siehe dort) via MAIL_DIR angegeben ist. Maildirs werden aber nicht unterstützt; genausowenig Mailboxen, die nicht in einem zentralen Verzeichnis liegen, sondern im Home-Verzeichnis des Benutzers. Dieses Anlegen lässt sich nur über die Einstellung CREATE_MAIL_SPOOL=no in der /etc/default/useradd abschalten – was hier ja durchaus Sinn ergibt, denn durch das Anlegen eines Maildirs in /etc/skel haben wir diesen Punkt ohnehin erledigt. (Mit herkömmlichen Mailboxen funktionierte das nicht, da /etc/skel ja die Vorlage für das Home-Verzeichnis des Users ist, die herkömmlichen Mailboxen aber zentralisiert und damit außerhalb des Home-Verzeichnisses liegen.)

$MAIL

Diese Umgebungsvariable wird von verschiedenen Programmen benutzt, um zu identifizieren, wo denn die Mails eines Users liegen. Hier fackelt CentOS 5 nicht lange – in /etc/profile steht hart kodiert:

if [ -x /usr/bin/id ]; then
        USER="`id -un`"
        LOGNAME=$USER
        MAIL="/var/spool/mail/$USER"
fi

Das ist für unsere Zwecke ergo völlig unbrauchbar. Die /etc/profile selbst anzupassen, empfiehlt sich nicht, weil die Änderungen sonst bei jedem Update des setup-RPMs (zu dem diese Datei gehört) hinfällig wären. Stattdessen bietet sich ein Einzeiler in /etc/profile.d an:

# cat /etc/profile.d/maildir.sh 
export MAIL=~/Maildir/

/etc/login.defs

Die man page der login.defs erklärt offenherzig:

Much of the functionality that used to be provided by the shadow password suite is now handled by PAM. Thus, /etc/login.defs is no longer used by programs such as: login(1), passwd(1), su(1)

Trotzdem ist die Datei noch da und wird von einigen Programmen genutzt. Sie sieht im Kopf wie folgt aus:

# *REQUIRED*
#   Directory where mailboxes reside, _or_ name of file, relative to the
#   home directory.  If you _do_ define both, MAIL_DIR takes precedence.
#   QMAIL_DIR is for Qmail
#
#QMAIL_DIR      Maildir
MAIL_DIR        /var/spool/mail
#MAIL_FILE      .mail

Fängt man erstmal an, an dieser Datei – vermutlich erfolglos – herumzubasteln, bekommt man wirklich graue Haare. Die naheliegendste Variante, nämlich den Eintrag QMAIL_DIR durch Entfernen des Rautenzeichens zu aktivieren, ist überraschenderweise keine gute Idee; dass die man page diese Einstellung gar nicht erwähnt, lässt schon tief blicken. So sieht’s dann nämlich aus:

# useradd dummy
configuration error - unknown item 'QMAIL_DIR' (notify administrator)

Fan-tas-tisch. Die NEWS-Datei der shadow-utils erklärt denn auch, dass die Einstellung aus der login.defs entfernt wurde – und zwar 2005. Interessanterweise kennt die man page auch die Einstellung MAIL_FILE nicht, sondern ausschließlich MAIL_DIR. Sie weiß aber auch gleich zu verkünden, dass nur usermod und userdel diese Einstellung benutzen – und useradd sowieso nicht. Was aber sehen wir in src/useradd.c?

      if (strcasecmp (create_mail_spool, "yes") == 0) {
              spool = getdef_str ("MAIL_DIR") ? : "/var/mail";
              file = alloca (strlen (spool) + strlen (user_name) + 2);
              sprintf (file, "%s/%s", spool, user_name);
              fd = open (file, O_CREAT | O_WRONLY | O_TRUNC | O_EXCL, 0);
              [...]
      }

Ist also glatt gelogen; useradd benutzt die Einstellung sehr wohl. Die man page von useradd bekräftigt das auch und erwähnt nicht nur MAIL_DIR, sondern auch MAIL_FILE und behauptet:

The MAIL_DIR and MAIL_FILE variables are used by useradd, usermod, and userdel to create, move, or delete the user´s mail spool.

Das ist nun wiederum auch gelogen, denn wenn MAIL_FILE statt MAIL_DIR gesetzt ist, macht useradd damit … überhaupt nichts. Und auch usermod und userdel, die beim Löschen eines Users dessen Mailbox aus dem zentralen mit MAIL_DIR angegebenen Verzeichnis löschen, unternehmen in Bezug auf Mail schlicht gar nichts, wenn MAIL_FILE gesetzt ist.

Nun, noch weiter in die Tiefe wollen wir hier nicht gehen. Belassen wir es dabei, dass das, was in der /etc/login.defs steht, das, was in der zugehörigen man page steht, das, was in den man pages von useradd, usermod und userdel steht, und schließlich das, was im Code der shadow-utils steht, erheblich voneinander abweicht und sich oftmals sogar widerspricht – kurz, das Ganze funktioniert nur dann sauber, wenn man exakt den traditionellen Standard benutzt: Mailboxen in einem zentralen Verzeichnis.

mailx

CentOS benutzt das mailx-Paket, um den Befehl mail bereitzustellen. Die meisten kennen den Befehl vornehmlich zu dem Zweck, um auf der Kommandozeile direkt Mails via Pipe versenden zu können (some-command | mail recipient@domain.tld). Allerdings ist mail auch ein Mailreader, der auf die Umgebungsvariable MAIL zurückgreift – was von daher erstaunlich ist, weil diese Variable gerade nicht zu denen gehört, die mail laut man page konsultiert:

Mail utilizes the HOME, USER, SHELL, DEAD, PAGER, LISTER, EDITOR, VISUAL and MBOX environment variables.

Trotzdem wird sie benutzt und zeigt dann auch gleich, wieso hier ohnehin Hopfen und Malz verloren ist:

$ mail
/home/jonas/Maildir/: Is a directory

Das mailx-Paket von CentOS hat also schlicht keinen Maildir-Support. Mit dem aktualisierten Paket, das sich in CentOS 6 findet, wird das offenbar kein Problem mehr sein, aber darum soll es hier ja nicht gehen.

bash

Hier endlich mal ein Lichtblick: Die bash unterstützt die Umgebungsvariable MAIL und kann auch korrekt mit Maildirs umgehen. Sie benutzt es, um standardmäßig alle 60 Sekunden nach neuen Mails zu suchen und den Benutzer zu informieren, und das klappt auch:

$ echo | mail $USER ; sleep 60 ; true
You have new mail in /home/jonas/Maildir/

PAM

Möchte man auch direkt beim SSH-Login über neue Mails informiert werden, so kann dies zeitgemäß mit pam_mail.so erledigt werden. Dazu braucht’s nur eine Zeile (danach sshd-Neustart nicht vergessen, sonst wirkt es nicht):

$ tail -1 /etc/pam.d/sshd 
session    optional     pam_mail.so dir=~/Maildir

Die Angabe von dir ist nötig, weil pam_mail.so ansonsten ganz traditionell in /var/mail nach einer Mailbox oder einem Maildir sucht (mit anderen Worten: Überraschung! Dass evtl. ein anderes Verzeichnis als MAIL_DIR in /etc/login.defs definiert ist, spielt überhaupt keine Rolle). Es erkennt automatisch, ob es eine Mailbox oder ein Maildir ist, denn eine Mailbox wäre ja eine Datei, während ein Maildir ein Verzeichnis wäre. So sieht es dann in der Anwendung aus:

$ ssh jonas@helium.uberspace.de
You have new mail in folder /home/jonas/Maildir.

Nur ein Doppelpunkt

Donnerstag, 11. November 2010

Wenn ich eine Software schreibe, die eine E-Mail konstruiert – Header, Body, das Ganze in einer adäquaten Kodierung – dann habe ich verschiedene Möglichkeiten, um zu prüfen, ob ich alles richtig gemacht habe. Ich kann möglichst viele verschiedene Varianten samt jeglicher hypothetischer Sonderfälle raus in die Weltgeschichte schicken und schauen, ob alle damit klarkommen: Ob die Mail von Mailservern wie Sendmail, qmail, Postfix, Courier, Exchange, Exim oder Lotus Domino problemlos durchgeleitet werden kann (und viele davon laufen nicht nur unter einem Betriebssystem, sondern unter vielen verschiedenen, wenigstens unter verschiedenen Versionen des gleichen Betriebssystems), und ob hinterher Outlook, Outlook Express, Thunderbird, Eudora, Evolution, Mail.app, Lotus Notes, Sylpheed, Balsa, verschiedene Webmailer und Smartphone-Clients meine Mail korrekt anzeigen.

Oder ich halte mich einfach an den Standard, wie eine Mail auszusehen hat: RFC 822.

Die heutige kleine Geschichte dreht sich um einen Doppelpunkt; genauer gesagt, um zwei. Ein User auf einem unserer Server arbeitet für einen Verein, dessen Corporate Identity einen doppelten Doppelpunkt im Schriftzug des Vereins vorsieht, im Sinne von mein::verein. Als Klartextname für E-Mails ist entsprechend vorgesehen: Vorname Nachname | mein::verein. Darüber kann man streiten, aber fest steht: Es ist natürlich völlig legitim, im Klartextnamen auch Sonderzeichen verwenden.

Das Problem entzündet sich nun daran, dass jener User als Mailclient Thunderbird einsetzt, und Thunderbird erstellt aus dem Klartextnamen und der E-Mail-Adresse folgenden Absender:

From: Vorname Nachname | mein::verein <vorname.nachname@mein-verein.de>

Um’s direkt vorwegzunehmen: So ist es falsch. Richtig wäre vielmehr:

From: "Vorname Nachname | mein::verein" <vorname.nachname@mein-verein.de>

Der Grund dafür liegt darin, dass der Doppelpunkt zur Zeichenklasse der „specials“ gehört, was schon von daher nicht verwunderlich ist, als dass er bereits das Trennzeichen zwischen der Headerbezeichnung („From“) und dem Inhalt des Headers ist. RFC 822 formuliert sogar ausdrücklich aus:

     specials    =  "(" / ")" / "<" / ">" / "@"  ; Must be in quoted-
                 /  "," / ";" / ":" / "\" / <">  ;  string, to use
                 /  "." / "[" / "]"              ;  within a word.

Keine große Sache, möchte man meinen. Trivial geradezu. Aber Thunderbird macht’s eben nicht.

Problematisch wird das nun, wenn die Mail an ein Programm gelangt, das sich an den Standard hält und dann entsprechend nicht weiß, wie es mit dieser Zeile umgehen soll. Das ist vielleicht nicht ganz einfach zu verstehen, denn als Mensch schaut man auf die Zeile drauf und sieht natürlich, wie sie gemeint ist. Software ist aber kein Mensch, sondern arbeitet nach strikten Abläufen, und diese Abläufe orientieren sich im Fall von Software, die Mails verarbeiten muss, logischerweise daran, wie die Regeln laut RFC den Aufbau vorschreiben. Im konkreten Fall ist es qmail-inject, das – weil der Empfänger seine Adresse als Weiterleitung konfiguriert hat – die Mail verarbeiten muss, um sie wieder der Queue des Mailservers zuzuführen, und das endet dann mit einer Bouncemail, die uns sagt:

qmail-inject: fatal: unable to parse this line:
From: Vorname Nachname | mein::verein <vorname.nachname@mein-verein.de>

Dass der Header falsch war, war auf den ersten Blick ersichtlich. Die wichtige Frage ist aber: Warum? Hat der Absender da etwas falsch gemacht – oder gar Thunderbird?

Es stellt sich heraus: Es gibt bereits eine ganze Reihe von Bug-Reports für Thunderbird dazu. Um nur einige zu nennen:

  • 524471 – Address from is incorrectly quoted in To: header field
  • 273037 – RFC 822 ’specials‘ not being properly quoted as per spec
  • 317296 – Recipient headers can be invalid if there are invalid chars that aren’t quoted
  • 474136 – from header line – no quoting if required by a colon within display name

(Der Fehler betrifft neben dem „From:“-Header genauso auch den „To:“-Header, insofern sind die entsprechenden Bug-Reports zu letzterem ebenfalls als zu unserem Problem passend anzusehen.)

Besonderes Augenmerk wäre hier auf den zweiten zu legen, in dem ansatzweise eine Diskussion dazu stattfand, während in den anderen überhaupt nichts Signifikantes passiert ist. Im fraglichen Ticket beklagt sich ein Anwender, bei der Verarbeitung der Mail auf Empfängerseite mittels javamail würde die Zielanwendung crashen. Nun ist das ohne Frage unschön und sollte nicht „so“ passieren, aber immerhin haben wir hier auch zwar nicht mit einem Crash zu tun, aber doch immerhin mit einem sauberen Abbruch, der ordentlich ausgibt, warum der Mailheader nicht verarbeitet werden konnte. Ob nun Crash oder sauberer Abbruch: Für den Enduser ist das nicht relevant. Die Mail wird nicht weitergeleitet. Das ist schlecht.

Einer der Thunderbird-Entwickler stellt nun zunächst ganz sachlich fest:

I assume he’s speaking about RFC 822, which states merely that : is a special, and „must“ be quoted if not being used as a delimiter. We may be not adhering to this spec.

Einsicht ist der erste Schritt zur Besserung, möchte man meinen. Doch weit gefehlt:

I receive mails daily where the colon is not quoted, and Mozilla doesn’t crash. Nor did Eudora, Outlook or Outlook Express, Pine, nor any other mail client I’ve used. It should be able to gracefully handle errors, and somethign as simple as a colon in the TO: field line should not crash it.

Das sind so die Momente, wo ich froh bin, nur vor einem Monitor zu sitzen, um nicht einem realen Menschen meine akute Sprachlosigkeit demonstrieren zu müssen. „Andere Mailclients haben da auch kein Problem mit“ ist nun wirklich überhaupt kein Grund, eine Missachtung von Standards zu rechtfertigen. Wenn ich selbst einen Mailclient schreiben wollte, dann interessiere ich mich nicht die Bohne dafür, welcher Mailclient in welchen Aspekten irgendwie rummurkst: Entweder er macht es richtig, oder er macht es falsch. Thunderbird macht es falsch, und es ist nicht meine Aufgabe, in meiner Software Workarounds für Bugs in Thunderbird zu schreiben.

Nun ist es natürlich völlig legitim, zu sagen: Naja gut, javamail sollte nun aber nicht gleich crashen. Ist ja auch richtig. „Be liberal in what you accept“ und so. Aber davon unabhängig kann man ja trotzdem auch schon mal den Fehler auf der eigenen Seite beheben, gerade nachdem man schon eingesehen hat, dass es einen gibt. Aber, Überraschung:

In fact, no character there should crash the app. In fact, the app should never crash due to malformed mails. That’s just bad form. […] Good luck to the javamail guys in fixing their crasher.

Mit anderen Worten: Ist mir egal. Die Gegenseite soll sich nicht so anstellen. Viel Glück.

Ich glaube, was mich an dieser Sache so bestürzt, ist, dass ich diese Geisteshaltung ausgerechnet bei einem Open-Source-Projekt erlebe. Schließlich ist freie Software ideell eng mit offenen Standards und darauf basierend mit einem starken Bestreben nach Interoperabilität verbunden. Man stelle sich vor, im Bereich der Entwickler von Browsern einerseits und Webservern andererseits gäbe es ähnliches Gezetere, weil man sich eben nur so vage und nicht ganz genau an den HTTP-Standard hielte, mit der Folge, dass ein Internet Explorer nur noch mit einem IIS so richtig kommunizieren könnte und ein Firefox nur noch mit einem Apache. Das wäre natürlich total lächerlich und würde die Entwicklung des Webs um Jahre zurückwerfen.

Vielleicht sehe ich die Sache auch deshalb so eng, weil ich seit Beginn meines Arbeitslebens eng mit der Software von Dan Bernstein verbunden bin, der unter anderem auch den qmail-Mailserver entwickelt hat, dessen Komponente qmail-inject sich gerade an dem fehlerhaften Header stört. Bernstein gehört zu den Leuten, die geradezu eine innige Liebe zu Standards und Interoperabilität entwickelt haben – und vor allem eine um so innigere Abneigung gegenüber Produkten und Personen, die diese Liebe nicht teilen. Auf den Mailinglisten weht ein rauer Wind, und nicht wenige haben über die Jahre hinweg geradezu eine „Nee, wenn das Programm von Bernstein ist, dann benutze ich das nicht, der Typ ist ein A***“-Haltung kultiviert. Letztlich ist das aber genau so ein Personenkult (nur eben ein negativer), wie Bernstein ihn stets abgelehnt hat: Für ihn war und ist wichtig, dass Software korrekt und berechenbar funktioniert, unabhängig davon, ob dahinter ein netter Mensch steht oder ein A***. Für diese Haltung hege ich große Sympathie.

Doch kurz zurück zum Bug-Report. Dessen bizarrster Punkt ist nämlich noch gar nicht gekommen. Der kommt erst jetzt, in der kurzen Code-Analyse eines anderen Community-Mitglieds:

According to nsMsgHeaderParser.cpp we’re not quoting :s so as not to break some systems which aren’t expecting them to be quoted…

Also, nur um das nochmal festzuhalten: Quoting spezieller Zeichen ist eine ausdrückliche Vorgabe von RFC 822. Und weil es nun „einige Systeme“ gibt, die mit einem solchen Quoting nicht umgehen können, sich also nicht an den Standard halten … spart Thunderbird sich das Quoting einfach und hält sich so ebenfalls nicht mehr an den Standard – und empfiehlt dann denen, die sich an den Standard halten und sich somit legitimerweise an dem fehlerhaften Header stören, doch gefälligst mal Fünfe gerade sein zu lassen. Bizarr finde ich das vor allem deshalb, weil alle anderen mir bekannten Mailclients (um mich mal kurz auf das argumentative Niveau des Bug-Reports herabzulassen) in Tests den Klartextnamen mit den Doppelpunkten korrekt in Anführungszeichen setzen, sogar Outlook und Outlook Express, die in Sachen Standardkonformität sich nun wirklich über Jahre hinweg einen außerordentlich schlechten Ruf erarbeitet haben.

Insbesondere die Arroganz, mit der hier die Veranwortung für den eigenen Fehler auf Dritte geschoben wird, hat mich bei der Lektüre wirklich erstaunt. „Be liberal in what you accept“ sollte aus meiner Sicht ein Ansatz sein, um die Interoperabilität zwischen Applikationen zu erhöhen – nicht aber eine Rechtfertigung dafür, selbst fehlerhafte Daten generieren zu dürfen: „Be conservative in what you send“ ist schließlich ebenfalls eine Vorgabe des gleichen Robustheitsprinzips. Übrigens die, die zuerst genannt wird. Und ich finde, von anderen zu verlangen, ihre Hausaufgaben zu machen, steht in erster Linie denen zu, die ihre eigenen bereits gemacht haben. Das sieht unser Thunderbird-Entwickler aber nicht so und polemisiert:

I think clients these days are generally smart enough to know what’s a header and what isn’t (usually).

Na und? Vielleicht stehe ich mit dieser Ansicht zunehmend alleine da. Aber ich finde wichtig, dass Software berechenbar ist und nicht „smart“. Dass sie Fehler in Daten als Fehler in Daten benennt und diese sauber und ordentlich ablehnt. Zu verlangen, dass eine Software Daten als fehlerhaft erkennt, daraus eigene Schlüsse zu ziehen versucht und dann statt dem, wozu man sie beauftragt hat, das macht, was sie für vermeintlich richtig hält, würde ich als Ursache vieler Übel betrachten.

Demjenigen, der den Fehler überhaupt erst an die Thunderbird-Entwickler gemeldet hat, gebührt das Schlusswort des Bug-Reports, und er offenbart dabei von allen Beteiligten die größte Weisheit:

I agree it is dumb to not have the client gracefully handle this. Yet if it is part of a standard, it makes sense. Otherwise standards become pointless; not all reasons for a given standard are always obvious up front, even when we know most of the reasons.

Und das ist genau der Punkt: Kaum eine einzelne Person kann die Vielzahl von Gründen dafür ermessen, warum sich ein Standard so und nicht anders entwickelt hat. Vor allem kann kaum eine einzelne Person die Folgen einer Abweichung vom Standard einschätzen – hier versäumt man zwei Anführungszeichen; da crasht deswegen ein Mailclient, und dort funktioniert eine Weiterleitung nicht mehr. Die Entwickler wären gut beraten, ein wenig Demut walten zu lassen und sich zu sagen: Im Zweifelsfall wissen wir zwar nicht, warum der Standard das so vorschreibt. Vielleicht finden wir es auch dumm, wie es der Standard will. Aber: Dutzende von Mailservern auf Dutzenden von Betriebssystemen sowie hunderte von Mailclients verlassen sich darauf, dass alle es so machen, wie der Standard es verlangt – also sollten wir uns lieber auch daran halten.

Lassen wir zum Schluss noch einmal Bernstein zu Wort kommen, der sich bereits vor über einem Jahrzehnt reichlich desillusioniert zum Thema Protokolldesign äußerte:

So you want to design a protocol? I’ll let you in on an evil secret: most implementors will never read your spec.
Instead, they’ll look at some common examples and write the most obvious code that works for those examples.

Thunderbird hat mir heute ein weiteres frustrierendes Beispiel für diese Geisteshaltung geliefert. Der letzte Eintrag des Tickets ist übrigens aus dem Dezember 2004. Jüngere Tickets, die das Problem ebenfalls beschreiben, blieben bisher unbearbeitet. Für mich entspricht das einem unausgesprochenen WONTFIX.

In der Plesk-Hölle: qmail-smtpd, SSL … und spamdyke

Dienstag, 27. Juli 2010

Wenn man etwas weiter hinter die Kulissen schaut, findet man manchmal ganz erstaunliche Dinge. Heute soll es mal um verschlüsseltes SMTP mit dem qmail-Paket von Plesk gehen. Für Verschlüsselung gibt es, um das ganz kurz zu erläutern, zwei Verfahren: TLS und SSL. TLS bedeutet: Man verbindet sich mit dem unverschlüsselten Port (normalerweise 25), der Server annonciert, dass er STARTTLS beherrscht, der Client sendet jenes STARTTLS, und die Verbindung ist verschlüsselt. Alternativ benutzt man SSL: Hier stellt der Server einen separaten Port bereit (normalerweise 465), auf dem dann direkt eine verschlüsselte Verbindung entgegengenommen wird.

Der aufmerksame Leser wird bereits eins ahnen: SSL über einen separaten Port kann man in der Art eines Wrappers um beliebige Dienste „drumherumstricken“, z.B. mit stunnel oder ucspi-ssl, ohne dass man den Dienst selbst anpassen muss. Dass ein Dienst hingegen ein Schlüsselwort wie STARTTLS versteht und daraufhin die Verschlüsselung einschaltet – da dürfte auf der Hand liegen, dass das nur mit einem Patch geht. Für qmail gibt es dafür seit langem einen etablierten Patch von Frederik Vermeulen, der (unter anderem) qmail-smtpd beibringt, auf STARTTLS zu reagieren. Im Vorbeigehen unterstützt er auch noch die traditionelle Variante mit dem separaten Port – ganz ohne Wrapper: Es reicht aus, die Umgebungsvariable SMTPS zu setzen, bevor qmail-smtpd aufgerufen wird, und das war’s.

Nun schauen wir uns mal an, wie das bei Plesk aussieht. Ich habe die Konfigurationsdateien jeweils auf das Wesentliche gekürzt, also bitte nicht wundern. Erstmal für den Standard-SMTP-Port 25:

# cat /etc/xinetd.d/smtp_psa
service smtp
{
  [...]
  server      = /var/qmail/bin/tcp-env
  server_args = /var/qmail/bin/relaylock /var/qmail/bin/qmail-smtpd [...]
}

… und schließlich für den Standard-SMTPS-Port 465:

# cat /etc/xinetd.d/smtps_psa
service smtps
{
  [...]
  server      = /var/qmail/bin/tcp-env
  server_args = /var/qmail/bin/relaylock /var/qmail/bin/qmail-smtpd [...]
}

Der xinetd erkennt an der service-Bezeichnung den numerischen Port, in dem er ihn über /etc/services auflöst. So weit, so gut.

Nun stellt sich aber doch eine Frage: Woher weiß qmail-smtpd im zweiten Fall denn, dass es bitte SSL sprechen möge und nicht Plaintext? Plesk arbeitet hier genauso mit dem etablierten TLS-Patch, der eigentlich verlangt, dass die Umgebungsvariable SMTPS gesetzt sein muss. Die wird hier aber doch gar nicht gesetzt..?!

Des Rätsels Lösung ist, dass Plesk hier ganz gewaltig trickst. Nicht etwa, dass es ein Leichtes gewesen wäre, in der /etc/xinet.d/smtpd_psa kurzerhand env = SMTPS=1 einzutragen: Nein, das wäre doch viel zu nachvollziehbar gewesen.

Stattdessen patcht Plesk. Und patcht. Und patcht. Details dazu finden sich in der Plesk-Knowledgebase. Wir schnappen uns das erste der Patche-Archive und schauen mal rein. Darin findet sich ein Haufen Patches mit so klangvollen Namen wie patch-pb, patch-pe oder auch patch-ps. Oder, und darum geht’s: patch-pu-tls. Darin steckt so eine Art Variante des etablierten qmail-TLS-Patches, „natürlich“ bereinigt um jegliche Kommentare oder Dokumentation. Und ganz am Ende, da findet sich plötzlich ein Teil, den es im Original nicht gibt, nämlich diesen hier:

--- ./tcp-env.c.orig    Thu Feb 20 11:33:43 2003
+++ ./tcp-env.c Thu Feb 20 11:37:05 2003
@@ -70,6 +70,9 @@
    temp[fmt_ulong(temp,localport)] = 0;
    if (!env_put2("TCPLOCALPORT",temp)) die();
 
+   if (localport == 465)
+      if (!env_put2("SMTPS","true")) die();
+
    byte_copy(&iplocal,4,&salocal.sin_addr);
    temp[ip_fmt(temp,&iplocal)] = 0;
    if (!env_put2("TCPLOCALIP",temp)) die();

Dass qmail-smtpd unter Plesk also auf „magische“ Weise plötzlich SSL spricht, liegt daran, dass das vorgeschaltete tcp-env die Variable SMTPS kurzerhand setzt, sobald es feststellt: Oh, ich laufe unter Port 465! Unnötig zu sagen, dass Plesk die man page von tcp-env nicht angerührt hat – die verrät von diesem Verhalten nichts. Da muss man sich dann auch nicht wundern, wenn in Foren Fragen wie diese auftauchen – und unbeantwortet bleiben (Hand aufs Herz, die Frage ist von 2007, und in dem Forum müsste ich mich auch erstmal registrieren, um antworten zu können):

When i copy smtps_psa to another port / service, SSL does not work. Is SSL hard-coded somewhere in Plesk’s qmail ? For instance : i would like to run smtps on port 665

Aber nun gut, lassen wir das für einen Moment so stehen und wenden uns dem Thema zu, das mich erst auf diese ganze Thematik gebracht hat, und das für mich vor allem deshalb so ein „Aufreger“ ist, weil es dazu Howtos über Howtos gibt und praktisch alle in puncto SSL falsch sind – wahrscheinlich schreiben alle voneinander ab. Hier findet sich in zig Howtos der Hinweis, dass Spamdyke auf Plesk-Systemen in /etc/xinetd.d/smtp_psa und /etc/xinetd.d/smtps_psa eingetragen werden muss. Das ist so auch nicht ganz falsch – allerdings geben fast alle Howtos an, dass man dort das Gleiche eintragen soll; viele liefern sogar konkrete Konfigurationsbeispiele, die aber vor Veröffentlichung definitiv nie getestet worden sein können. Dann hätte nämlich auffallen müssen, dass sie in der Realität überhaupt nicht funktionieren – nicht mit SSL.

Und so finden wir falsche Howtos im offiziellen Plesk-Forum, im inoffiziellen Plesk-Forum, im Serversupport-Forum, nochmal dort, im Howto des RootForums, in der Anleitung von huschi.net, bei Blue Orix oder bei Haggybear, um nur eine kleine Auswahl an Quellen zu nennen. Hier und da (aber Ausnahmen bestätigen ja bekanntlich die Regel) findet sich auch jemand, der es richtig macht, zum Beispiel The BOFH. Andere kriegen es auch hin, so wie Alexander Koch, wenngleich auf ganz andere Weise (nämlich, in dem gar nicht von der SMTPS-Funktionalität Gebrauch gemacht wird, sondern in dem stunnel als Wrapper eingesetzt wird – was aber fraglos auch eine legitime Lösung ist, nur eben eine aufwendige).

Im Endeffekt laufen alle Howtos darauf hinaus, dass man spamdyke vor den Aufruf von qmail-smtpd setzen soll. Aus

server_args = [...] /var/qmail/bin/qmail-smtpd [...]

soll also werden:

server_args = [...] /usr/local/bin/spamdyke -f /etc/spamdyke.conf \
              /var/qmail/bin/qmail-smtpd [...]

Und genau da liegt der Knackpunkt. Die TCP-Verbindung kommt jetzt ja nun nicht mehr bei qmail-smtpd an, sondern bei spamdyke. Und so schön und gut es auch ist, dass qmail-smtpd eine Umgebungsvariable namens SMTPS interpretiert, und so tricky und gruselig es ist auch ist, dass Plesks tcp-env diese Umgebungsvariable heimlich setzt: spamdyke hat deswegen noch lange keine Ahnung davon – und spricht deshalb auch auf Port 465 schlicht und einfach Plaintext. Warum um alles in der Welt sollte es auch etwas anderes tun?

Praktischerweise kann man spamdyke sagen, dass es SSL sprechen soll. Nämlich, in dem man tls-level=smtps setzt anstelle des Defaults tls-level=smtp. Nur tut das keins der oben genannten Howtos! Es kann also überhaupt nicht funktionieren.

Die Abhilfe ist dann letztlich trivial. Entweder man legt sich eine zweite Konfigurationsdatei für spamdyke an, zum Beispiel eine spamdyke-smtps.conf, die eine Kopie des Originals ist, aber tls-level=smtps beinhaltet. In der xinetd-Konfiguration sieht das dann so aus:

# cat /etc/xinetd.d/smtps_psa
service smtps
{
  [...]
  server      = /var/qmail/bin/tcp-env
  server_args = /var/qmail/bin/relaylock \
                /usr/local/bin/spamdyke -f /etc/spamdyke-smtps.conf \
                /var/qmail/bin/qmail-smtpd [...]
}

Oder man hat verständlicherweise keine Lust, eine zweite Konfigurationsdatei zu pflegen, und setzt die Angabe direkt als Parameter mit in den Programmaufruf ein:

# cat /etc/xinetd.d/smtps_psa
service smtps
{
  [...]
  server      = /var/qmail/bin/tcp-env
  server_args = /var/qmail/bin/relaylock \
                /usr/local/bin/spamdyke -f /etc/spamdyke.conf --tls-level=smtps \
                /var/qmail/bin/qmail-smtpd [...]
}

So, und damit wir jetzt zum Schluss alle nochmal herzhaft lachen können:

Alle, wir alle inklusive mir, hätten eigentlich nur die ganz offizielle INSTALL.txt von spamdyke lesen müssen. Dort lesen wir schnöde vier Zeilen:

   Plesk users can also use spamdyke for their SMTPS connections by adding it to
   the /etc/xinetd.d/smtps_psa file.  spamdyke's configuration in that file will
   need to include the options "tls-level" (set to "smtps") and
   "tls-certificate-file".

Das wär’s auch schon gewesen. Aber andererseits: Ansonsten wäre ich nie darauf gekommen, wie Plesk das mit dem SSL eigentlich macht. Insofern hat sich’s doch gelohnt …

greylite kann nur 8 Minuten

Donnerstag, 25. März 2010

An sich ist auch das Übertragen größerer Dateien per SMTP kein Problem mit qmail, solange die Mail vom Umfang her unter dem in control/databytes angegeben Limit bleibt. Abbrüche aufgrund von Timeouts nimmt qmail-smtpd nur dann vor, wenn es 20 Minuten lang überhaupt keine Daten mehr vom Client bekommt (einstellbar in control/timeoutsmtpd). Solange Daten übertragen werden, werden die auch verarbeitet – auch wenn’s Stunden dauert.

Nun beklagte sich ein „Kundeskunde“, der nur eine 64-kbit/s-Leitung sein eigen nennt, darüber, dass beim Versenden größerer Dateien die Verbindung immer nach einigen Minuten abbrechen würde, ohne eine Fehlermeldung des Servers. Thunderbird sagt dann:

The message could not be sent because the connection to SMTP server $HOSTNAME was lost in the middle of the transaction.

Wir konnten uns zunächst keinen Reim darauf machen. Selbst wenn qmail-smtpd die Verbindung wegen eines Timeouts dichtmachen würde, dann doch zumindest mit einer entsprechenden Meldung (wobei fraglich wäre, ob diese den Client dann noch erreicht). Also doch ein Netzproblem?

Im Logfile von qmail-smtpd fand sich nur das hier:

2010-03-24 14:42:49.222521500 6412 > [EOF]
2010-03-24 14:42:49.222721500 tcpserver: end 6412 status 14

Die erste Zeile resultiert daraus, dass wir ein recordio haben mitlaufen lassen, um dem Problem auf die Spur zu kommen. Schnell war analysiert, dass derartige Abbrüche immer nach exakt 8 Minuten verzeichnet werden. Passt also auch nicht zur qmail-smtpd-Konfiguration. Mein Kollege Andreas schlug vor, die Sourcen der beteiligten Programme mal ganz stumpf nach „480“ (Sekunden = 8 Minuten) zu durchsuchen. Im gesamten qmail-Source fand sich wie erwartet nichts. Auch nicht im Source von ucspi-tcp. Aber dann, ein Treffer in greylite.c:

/* timeout in seconds for one read/write operation (seconds) */
#define TIMEOUT                     40
/* big timeout after which exiting (seconds) */
#define BIG_TIMEOUT                 480

Kurz darauf wird dann ein Alarm-Handler auf diesen Timeout gesetzt, der an sighand verzweigt:

    signal(SIGALRM, sighand);
    signal(SIGPIPE, sighand);
    alarm(BIG_TIMEOUT);
    [...]
void sighand(int sig) {
    switch (sig) {
        case SIGALRM:
            logmsg(LOG_WARNING, "Big timeout reached from '%s'!", client_metainfo.address);
            break;
        case SIGPIPE:
            logmsg(LOG_INFO, "Client '%s' closed connection prematurely.", client_metainfo.address);
            break;
    }
    exit(100);
}

Also: Nach 8 Minuten kappt greylite die Verbindung, egal was gerade ist oder ob gerade noch eine Mail überträgt. Hm, das ist ja mal ’ne tolle Wurst. Wir haben aus der 480 mal eine 1800 gemacht, neu kompiliert, und der Kunde bestätigt: Jetzt gibt’s kein Problem mehr.

Fraglich bleibt, wieso wir keine „Big timeout reached“-Meldung im Logfile vorgefunden haben. Manchmal gibt’s die nämlich durchaus. Aber manchmal stirbt das greylite auch kommentarlos weg – aber eben unzweifelhaft nach jenen 8 Minuten.

DSPAM automatisch trainieren

Dienstag, 23. März 2010

Wir setzen auf einigen Accounts DSPAM zur Filterung ein. Den Brückenschlag zwischen qmail und DSPAM schafft hierbei maildrop:

$ cat info/.qmail
|preline maildrop $HOME/dspamfilter

Der entsprechende Abschnitt in dspamfilter sieht dann so aus:

# DSPAM die Mail bewerten lassen
xfilter "/command/dspam --deliver=innocent,spam --stdout"

# Mail wegsortieren, wenn DSPAM das meint
if( /^X-DSPAM-Result: Spam/ )
{
  to "./Maildir/.0 Spamfilter.als Spam erkannt/"
}

# Ansonsten: In normales Maildir zustellen
to ./Maildir/

Nun basiert DSPAM letztlich auf Algorithmen aus dem Bereich der Statistik. Grob vereinfacht gesagt führt es eine Datenbank, in der er Wörter und Wortgruppen mit einem Wert zwischen 0 und 1 versieht, der aussagt, ob die Verwendung eher auf Spam (0) oder legitime Mails, also Ham (1) hindeutet. Anfangs haben alle Wörter einen mittleren Wert, der sich dann mit der Zeit durch Training nach oben oder unten verschiebt – oder in der Mitte bleibt, wenn ein Wort sowohl in Spam als auch in Ham häufig vorkommt.

Nun gibt es ab und zu aber Fälle, wo selbst das mehrfache Umlernen einer nicht als Spam erkannten Mail via „als Spam lernen“-Ordner einfach nicht den gewünschten Effekt erzielt; insbesondere dann, wenn sich DSPAM sowieso schon nicht so sicher ist, weil selbst die signifikantesten gefundenen Wörter und Wortgruppen eher im Mittelfeld liegen als gegen 0 oder 1 zu tendieren. Die Hauptforderung von Endkunden lautet dann schnell: „Setzt doch den Absender auf die Blacklist“. Diese Möglichkeit bietet qmail natürlich durch aus (wie control/badmailfrom), aber so global wollen wir solche Blacklists nicht pflegen. Aber in der Filterkonfiguration des Users, der sich das Blacklisting wünscht, können wir so etwas natürlich realisieren. Die einfachste Möglichkeit wäre sicherlich so etwas hier:

if( /^From: boeserspammer/ )
{
  to "./Maildir/.0 Spamfilter.als Spam erkannt/"
}

Allerdings würden wir ja schon gerne davon profitieren, dass die DSPAM-Treffergenauigkeit auch weiterhin berücksichtigt, dass der User diese Mails nicht haben will. Also haben wir an diesem Punkt ein automatisches Umlernen mit eingebaut, und damit das Filterscript flexibel bleibt, benutzen wir für die Liste der zu blacklistenden Adressen keinen fest eingebauten regulären Ausdruck, sondern die lookup-Funktion von maildrop, die reguläre Ausdrücke aus einer externen Datei einlesen kann.

Und so sieht’s aus:

# DSPAM die Mail bewerten lassen
xfilter "/command/dspam --deliver=innocent,spam --stdout"

# Wenn Mails von bekannten Spammern nicht als Spam erkannt wurden, als Spam umlernen
`test -f "dspamfilter-autolearn-spam"`
if( $RETURNCODE == 0 && ! /^X-DSPAM-Result: Spam/ && /^From:.*/ && lookup($MATCH, "dspamfilter-autolearn-spam") )
{
  xfilter "/command/dspam --class=spam --source=error --deliver=innocent,spam --stdout"
}

# Mail wegsortieren, wenn DSPAM das meint
if( /^X-DSPAM-Result: Spam/ || /^X-DSPAM-Reclassified: Spam/ )
{
  to "./Maildir/.0 Spamfilter.als Spam erkannt/"
}

# Ansonsten: In normales Maildir zustellen
to ./Maildir/

Es ist zu beachten, dass das X-DSPAM-Result trotz Umlernen identisch bleibt – DSPAM fügt lediglich noch einen X-DSPAM-Reclassified: Spam-Header hinzu. Also haben wir das Kriterium fürs Wegsortieren noch entsprechend erweitert.

Funktioniert prima; Kunde zufrieden. Die Pflege der Liste unerwünschter Adressen per einfacher Textdatei braucht praktisch keine Einarbeitung und kann schnell nebenbei erfolgen.

greylite mit MySQL

Montag, 18. Januar 2010

Im Rahmen unserer Arbeiten mit qmail-tauglichen Greylisting-Implementierungen sind wir auf greylite gestoßen, das zu einem der am einfachsten zu installierenden und zu benutzenden Tools gehört. Es unterstützt MySQL als Backend, was insbesondere deshalb für uns wichtig ist, weil wir mehrere Filterserver einsetzen, die sich eine Greylisting-Datenbank teilen sollen.

Auch wenn alles von Anfang an prima klappte, eins haute nicht hin: Das automatische Aufräumen von veralteten Einträgen. Zwar war in der Dokumentation nichts davon erwähnt; aus dem Sourcecode ging aber klar hervor, dass ein derartiges Aufräumen durchaus eingebaut ist.

Mit LOG_DEBUG als Log-Level war schließlich das Problem schnell identifiziert:

2010-01-18 19:51:39.804329500 greylite: Cleaning up stale verified entries.
2010-01-18 19:51:39.804348500 greylite: Query: DELETE FROM verified WHERE NOW() - ts > interval '480 hours'
2010-01-18 19:51:39.805073500 greylite: Query failed: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' at line 1

Für die Tabelle pending galt übertragen das gleiche. Das Problem ist also, dass der Autor hier eine Syntax verwendet, die von MySQL nicht unterstützt wird: INTERVAL kann dort nur zusammen mit DATE_ADD/DATE_SUB oder direkter Addition oder Subtraktion mit einem Datum verwendet. „Alleinstehend“ auf einer Seite eines Vergleichs kann es nicht genutzt werden. Damit nicht genug heißt die entsprechende Einheit in MySQL nicht etwa HOURS, sondern immer HOUR im Singular, auch wenn eine Formulierung wie INTERVAL 480 HOUR sicherlich sprachlich zweifelhaft ist. Und schließlich ist HOUR ein Schlüsselwort, das nicht gequotet werden darf. Mit anderen Worten: So kann’s überhaupt nicht gehen.

Ich habe daraufhin die beiden betreffenden SQL-Statements in der db-mysql.c schnell umgeschrieben und getestet. Der Einfachheit halber habe ich das Ergebnis als Patch bereitgestellt (greylite-3.0pre2-db-mysql.patch). Den Autor von greylite habe ich informiert; mit etwas Glück ist der Patch bei der nächsten Version nicht mehr nötig.

qmail und Greylisting: Implementierung

Dienstag, 05. Mai 2009

Auf unseren internen System evaluieren wir derzeit den Einsatz von Greylisting mittels qmail. Am Beginn der Suche stand hier zunächst, einen geeigneten Patch zu finden. Genauer gesagt sind es gleich mehrere. Um das Ergebnis vorwegzunehmen: Die finale Struktur sieht wie folgt aus …

  1. qmail-smtpd wird derart gepatcht, dass es bin/qmail-envelope-scanner aufruft. Der Patch hierfür ist klein und überschaubar und agnostisch, was den eigentlichen die Header beurteilenden Vorgang betrifft. Er sorgt lediglich dafür, dass der Exitcode von qmail-envelope-scanner ausgewertet wird und dementsprechend qmail-smtpd dann einen temporären oder permanenten Fehler zurückliefert. Einen Autor für diesen Patch habe ich nicht finden können; Jeremy Kusnetz hat offensichtlich die „beste“ Version aus verschiedenen Schnipseln zusammengesetzt, die auf der Mailingliste herumgingen.
  2. Der eigentliche qmail-envelope-scanner stammt von Martin Dempsey. Martin hatte zunächst local_scan als Greylisting-Erweiterung für Exim entwickelt. Als Standalone-Tool war es aber nicht weit davon entfernt, auch von qmail aus genutzt zu werden. Der qmail-envelope-scanner ist nur ein Wrapper (33 Zeilen) um das eigentliche local_scan herum.

Die aus meiner Sicht praktikabelste Zusammenstellung der Patches stammt schließlich von Bill Shupp. Wer hier weiter nach unten scrollt, findet dort den Abschnitt „EXPERIMENTAL: greylisting patch“. Er fasst die beiden obigen Patches zusammen mit einem Patch für das Makefile von netqmail-1.05 zusammen.

Bei uns waren noch zwei Änderungen nötig, die jedoch spezifisch für das von uns eingesetzte CentOS sind:

  • Der Verweis auf /usr/lib/libmysqlclient.a im Makefile muss auf /usr/lib/mysql/libmysqlclient.a geändert werden, da die Bibliotheken bei CentOS in diesem Unterverzeichnis liegen.
  • In conf-cc muss noch ein „-lssl“ ergänzt werden, da die libmysqlclient bei CentOS mit SSL-Support kompiliert wurde und sich von daher nicht benutzen lässt, wenn nicht auch gleichzeitig gegen die OpenSSL-Library gelinkt wird.

Ansonsten müssen nur noch Bills Anweisungen befolgt werden: Es sollte ein periodischer Job eingerichtet werden, der veraltete Einträge aus der Datenbank entfernt werden, und es muss eine Tabelle für die Daten angelegt werden.

Die Konfiguration kann in dieser Version sehr praktisch – Dank an Joshua Megerman – durch Umgebungsvariablen geregelt werden, für die ich empfehlen würde, sie in /service/qmail-smtpd/run unterzubringen. Bei dieser Gelegenheit würde ich auch die Expire-Werte anpassen, da mir ein erstes Akzeptieren eines Tripels erst 55 Minuten nach dem ersten Zustellversuch doch reichlich spät erscheint:

export MYSQLHOST="..."
export MYSQLUSER="..."
export MYSQLPASS="..."
export MYSQLDB="..."
# minutes until email is accepted
export BLOCK_EXPIRE="5"
# minutes until record expires
export RECORD_EXPIRE="500"
# days until record expires after accepting email
export RECORD_EXPIRE_GOOD="36"
export LOCAL_SCAN_DEBUG="1"

(Hier würde sich empfehlen, die Zugangsdaten zu MySQL in einem separaten Shellscript auszulagern, das dann sowohl vom rund-Script als auch vom Aufräumjob eingebunden wird. Ich hab’s nicht so gern, wenn Daten auf zig Orte verstreut werden.)

Erstes Ergebnis: Nur rund 1,5 Prozent aller Zustellversuche werden wiederholt. Es ist also davon ausgehen, dass die restlichen 98,5 Prozent in erster Linie Spammer sein werden. Wir bleiben dran.

simscan mit ClamAV > 0.90

Dienstag, 25. März 2008

Das für den Einsatz mit qmail entwickelte Scannerpaket simscan, das diverse Scan-Enginges (darunter SpamAssassin und ClamAV) einbinden kann, hat eine Komponente namens simscanmk, die unter anderem die Versionsstände der eingesetzten Tools zusammenstellt. Diese liegen dann vorgefertigt in einer .cdb-Datei, damit simscan diese Daten simpel in einen Mailheader packen kann, ohne die Infos jedes Mal aufwendig zusammenstellen zu müssen. Seit einiger Zeit meldet ein entsprechender Cronjob aber:

# /etc/cron.daily/simscanmk:
LibClamAV Error: cl_cvdhead: Can't open file /var/clamav/daily.cvd

Hintergrund ist, dass ClamAV in neueren Versionen keine daily.cvd mehr führt, sondern inkrementelle Updates im Verzeichnis daily.inc ablegt. Davon weiß simscan allerdings nichts und findet folglich die Versionsinfo nicht mehr. Das ist eher kosmetisch, weil es deshalb trotzdem immer noch korrekt scannen kann, aber kosmetisch ist das natürlich inakzeptabel.

Bis das in der Distribution gefixt ist, kann man sich mit diesem Patch von John M. Simpson behelfen, der nebenbei für den irrsinnig praktischen validrcptto.cdb-Patch verantwortlich zeichnet. Dankeschön!


Impressum