Mailserver 8 von 10: Mails synchronisieren mit mailsync.phs

Jahrelang habe ich mich davor gedrückt, doch endlich ist es geschafft - die Einrichtung eines eigenen Mailservers. In einer kleinen Artikelserie möchte ich einmal meine Erfahrungen und Konfigurationsschritte festhalten. Diese Serie basiert auf zahllosen Tutorials, Best Practices, Gesprächen mit Mailserver-Betreibern, aus endlosem Dokumentationen lesen und dem Wissen der Bücher "Postfix" von Kyle D. Dent und "Postfix - Einrichtung, Betrieb und Wartung" von Ralf Hildebrandt und Patrick Ben Koetter. Vielleicht werden sie ja nützlich für andere Leute sein, die das gleiche erreichen wollen.

Doch eines vorab: Die Einrichtung und der Betrieb eines Mailservers ist eine komplexe Sache. Klar, die Software lässt sich schnell installieren, doch fertig ist man danach noch lange nicht, wenn man ernsthaft damit arbeiten möchte.

Der Fahrplan der Artikelserie:

  1. Einleitung
  2. Mailempfang und Mailversand mit Dovecot und Postfix
  3. Virenprüfung mit ClamAV
  4. Mailsignatur mit OpenDKIM
  5. Spamprüfung mit SpamAssassin
  6. Mailabruf mit Dovecot
  7. Mailboxen konfigurieren mit mailconf.phs
  8. Mails synchronisieren mit mailsync.phs
  9. Mailqueue überwachen mit mailqueue.phs
  10. SpamAssassin anlernen mit maillearn.phs

Diejenigen von euch, die den Mailempfang und den Mailabruf auf einem einzelnen Server bereitstellen, können sich dieses Mal zurücklehnen, denn im heutigen Artikel geht es darum, empfangene Mails auf den Server zu synchronisieren, von dem die Mails auch tatsächlich abgerufen werden.

Hierfür gibt es die unterschiedlichsten Varianten: In einem Rechenzentrum könntet ihr z.B. eine zentrale Dateiablage (NAS oder SAN) verwenden. Ihr könnt eine Weiterleitung per SMTP einrichten (out-of-scope). Ihr könnt die Dateien händisch umher kopieren (langsam). Ihr könnt rsync verwenden. Oder ihr könnt euch etwas völlig eigenes überlegen.

Ich jedenfalls habe mich dazu entschlossen, mir dafür ein eigenes PHP-Script - mailsync.phs - zu schreiben. Der Vorteil daran: Ich kann es nach meinen eigenen Vorstellungen gestalten, Checks einbauen, wie ich lustig bin und Sortierungen vornehmen. In diesem Artikel möchte ich das Script einfach einmal vorstellen. Ob ihr es schlussendlich nutzen wollt, bleibt eure Entscheidung.

Im Grunde, nimmt uns die Wahl der Mailablage den Großteil der Arbeit ab. Maildir arbeitet so, dass es für jeden Mailordner immer drei Systemordner gibt: "cur", "new" und "tmp". Im Ordner "new" befinden sich neue E-Mails, im Ordner "cur" befinden sich gelesene E-Mails und im Ordner "tmp" befinden sich temporäre Dateien - z.B. E-Mails die gerade empfangen werden.

Darüber hinaus kann man - seit IMAP - auch spezielle Ordner anlegen. Da wäre der Entwurfsordner (meist ".Drafts"), der Gesendet-Ordner (meist ".Sent"), der Papierkorb (meist ".Trash") und der Spamorder (meist ".Junk"). Auch diese haben wieder jeweils die Unterordner "cur", "new" und "tmp".

Die Idee dahinter ist, dass z.B. die Lesemarkierung einer E-Mail oder das Löschen einer Nachricht einfach durch das Verschieben der zugehörigen Datei in einen anderen Ordner realisiert wird. Es müssen keine Inhalte gelesen oder neue Dateien geschrieben werden. Das verringert das Risiko, Dinge kaputt zu machen. Das macht es uns natürlich auch bei einer Synchronisation ziemlich leicht. Im Endeffekt muss man nur alle Maildateien von dem einen Server nehmen und auf den anderen Server transferieren. Da zudem der Hostname des jeweiligen Servers in dessen Dateinamen enthalten ist, kann es nicht einmal zu Kollisionen bei den Dateinamen kommen.

Genau diese naive Herangehensweise macht sich auch mailsync.phs zunutze: Es wird auf dem Abrufserver gestartet, baut eine SSH-Verbindung zum Empfangsserver auf, holt sich die Liste der neuen E-Mails, liest die E-Mails nacheinander ein und schreibt sie lokal in Dateien. Zum Schluss werden die erfolgreich kopierten Dateien auf dem Empfangsserver entweder gelöscht oder dahin verschoben, wo sie bei der nächsten Synchronisation nicht mehr stören.

Natürlich werden währenddessen ein paar Checks durchgeführt - wir wollen ja schließlich keine Mails kaputt machen. So wird z.B. die Dateigröße vor dem Einlesen und nach dem Wegschreiben verglichen. Zudem werden die Dateien im Ordner "tmp" zwischengespeichert, sodass die Checks ohne die Beeinflussung des Betriebs ablaufen können.
Es werden nur Mails verarbeitet, die die maximale Mailgröße nicht überschreiten (siehe "message_size_limit"-Einstellung von Postfix) und es besteht die Möglichkeit, bereits während der Synchronisation E-Mails mit Viren- oder Spambefund in den Spamordner einzusortieren.

Um mailsync.phs zu installieren und zu nutzen, müssen erst einmal ein paar Vorbereitungen getroffen werden. Zuerst müssen PHP und Mercurial installiert werden, falls ihr das nicht sowieso schon getan habt. Wir brauchen auch das PHP-Modul "ssh2", um SSH-Verbindungen zu unterstützen:

1
2
sudo apt-get update
sudo apt-get install php5-cli php5-curl php5-ssh2 mercurial

Anschließend müssen wir die entsprechenden Repositories abrufen:

1
2
3
sudo hg clone https://hg.nhg.name/mailsync/
sudo hg clone https://hg.nhg.name/pushinfo/
sudo hg clone https://hg.nhg.name/unchroot/

Die README gibt die ersten Schritte vor, die wir durchführen sollten:

1
2
3
4
5
6
7
8
9
10
11
README:

1.) Copy all mailsync.* files to a location that can not be accessed remotely.
2.) Install https://hg.nhg.name/pushinfo/
3.) Install https://hg.nhg.name/unchroot/
4.) Configure mailsync.conf.phs and make sure that all paths are accessible.
5.) Configure mailsync.conf.example.phs and make sure that all paths are accessible.
6.) mailsync.phs is called this way: sudo php mailsync.phs <user> <server>
7.) mailsync.phs takes the configuration from mailsync.conf.<server>.phs
8.) Configure CRON to call mailsync.phs regularly:
    */5 * * * * root php /path/to/mailsync.phs <user> <server> >/dev/null 2>&1

Mailsync.phs schickt euch Pushnachrichten auf's Handy, wenn während der Synchronisation einer E-Mail ein Fehler auftritt. Bei dem Benachrichtigungsweg habe ich mich für Pushinfo und den tollen Service von PushOver.net entschieden.

An dieser Stelle werde ich jedoch nur bzgl. der Konfiguration von mailsync.phs ins Detail gehen. Zuerst einmal: mailsync.phs ist so gestaltet, dass es prinzipiell die Synchronisation mehrerer Server unterstützt. Das ist so realisiert, dass die Konfiguration des Scripts in mehrere Dateien aufgebrochen werden kann. Per Default gibt es die beiden Dateien "mailsync.conf.phs" und "mailsync.conf.example.phs".

In der Datei "mailsync.conf.phs" befindet sich die Konfiguration, die für alle Synchronisationsvorgänge - egal mit welchem Server - identisch sein sollten. Das sind primär die ganzen lokalen Pfade, Header zum Erkennen von Spam und Viren, sowie die maximale Dateigröße. Beachtet, dass die synchronisierten Mails in den Speicher geladen werden. Das Memory-Limit für PHP-Prozesse sollte vorsichtshalber auf mindestens das ca. 2,5-fache der maximalen Mailgröße angehoben werden.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
<?php

  # define timeout for PHP execution (as int)
 # alternatively defines a lock file (as string):
 # * this path MUST point to a single file
 # * the optional placeholder {%server} may be used which
 #   contains the parameter given to the script for execution
 # * the optional placeholder {%user} may be used which
 #   contains the parameter given to the script for execution
 define("TIMEOUT", "/srv/mailusers/{%user}/lock.{%server}");

  # defines where the script stores its status:
 # * this path MUST point to a single file
 # * the optional placeholder {%server} may be used which
 #   contains the parameter given to the script for execution
 # * the optional placeholder {%user} may be used which
 #   contains the parameter given to the script for execution
 define("STATUS_PATH", "/srv/mailusers/{%user}/status.{%server}");

  # defines where the script looks for mails that
 # shall be synchronized again despite a previous
 # synchronization failure:
 # * this path MUST point to a single file
 # * the optional placeholder {%server} may be used which
 #   contains the parameter given to the script for execution
 # * the optional placeholder {%user} may be used which
 #   contains the parameter given to the script for execution
 define("RETRY_STATUS_PATH", "/srv/mailusers/{%user}/retry.{%server}");

  # defines where the push notification is located:
 # * this path MUST point to a single file
 # * the optional placeholder {%server} may be used which
 #   contains the parameter given to the script for execution
 # * the optional placeholder {%user} may be used which
 #   contains the parameter given to the script for execution
 define("PUSHINFO_PATH", "/srv/pushinfo/messages/mailsync.phs_{%user}_{%server}");

  # defines the local path to the maildir:
 # * this path MUST point to a single directory
 # * do NOT forget the trailing slash
 # * the optional placeholder {%server} may be used which
 #   contains the parameter given to the script for execution
 # * the optional placeholder {%user} may be used which
 #   contains the parameter given to the script for execution
 define("LOCAL_MAILDIR", "/srv/mailusers/{%user}/Maildir/");

  # defines the local path to the maildir new:
 # * this path MUST point to a single directory
 # * do NOT forget the trailing slash
 # * the optional placeholder {%server} may be used which
 #   contains the parameter given to the script for execution
 # * the optional placeholder {%user} may be used which
 #   contains the parameter given to the script for execution
 define("LOCAL_MAILDIR_NEW", LOCAL_MAILDIR . "new/");

  # defines the local path to the maildir tmp:
 # * this path MUST point to a single directory
 # * do NOT forget the trailing slash
 # * the optional placeholder {%server} may be used which
 #   contains the parameter given to the script for execution
 # * the optional placeholder {%user} may be used which
 #   contains the parameter given to the script for execution
 define("LOCAL_MAILDIR_TMP", LOCAL_MAILDIR . "tmp/");

  # defines the local path to the SPAM maildir new:
 # * this path MUST point to a single directory
 # * do NOT forget the trailing slash
 # * the optional placeholder {%server} may be used which
 #   contains the parameter given to the script for execution
 # * the optional placeholder {%user} may be used which
 #   contains the parameter given to the script for execution
 define("LOCAL_MAILDIR_SPAM_NEW", LOCAL_MAILDIR . ".Junk/new/");

  # defines if the retrieved mails are deleted or
 # moved to the remote ./cur folder
 define("DELETE_ON_SUCCESS", false);

  # defines that successfully retrieved should be deleted
 # from the remote server after a certain number of days:
 # * must be bigger than zero
 # * DELETE_ON_SUCCESS must be set to false
 define("DELETE_AFTER_DAYS", -1);

  # defines if mails that failed to be synchronized
 # shall be synchronized again during the next
 # execution - if this is set to FALSE each failed
 # file needs to be added to the RETRY_STATUS_PATH
 # file in order to be resynchronized - this helps to
 # prevent multiple retrievals and other problems
 # without prior handling by the administrator
 define("RETRY_ON_FAILURE", false);

  # defines the maximum mail size in bytes
 # should be bigger or equal to message_size_limit
 # of Postfix - remember to set memory_limit of PHP
 # to about 2.5 * MAX_MAIL_SIZE
 define("MAX_MAIL_SIZE", 52428800);

  # defines how mails containing spam are detected
 define("SPAM_HEADER_START", "X-Spam-Status: Yes");

  # defines how mails containing viruses are detected
 define("VIRUS_HEADER_START", "X-Virus-Status: Infected");

  # defines the push message content
 define("PUSHINFO_MESSAGE",         " mail synchronizations failed: ");
  define("PUSHINFO_MESSAGE_NEW",     " new, ");
  define("PUSHINFO_MESSAGE_OLD",     " old, ");
  define("PUSHINFO_MESSAGE_GONE",    " gone, ");
  define("PUSHINFO_MESSAGE_DEL",     " deleted, ");
  define("PUSHINFO_MESSAGE_DELFAIL", " deletes failed, ");
  define("PUSHINFO_MESSAGE_DELKEEP", " deletes ignored");

?>

Die Dateien der Form "mailsync.conf.<x>.phs" wiederum enthalten die Konfigurationen der jeweiligen Server, mit denen die Synchronisation durchgeführt werden soll. Das sind primär die Verbindungsdaten, sowie die auf dem entfernten Server gültigen Pfade.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php

  # defines the remote path to the maildir cur:
 # * this path MUST point to a single directory
 # * do NOT forget the trailing slash
 # * the optional placeholder {%server} may be used which
 #   contains the parameter given to the script for execution
 # * the optional placeholder {%user} may be used which
 #   contains the parameter given to the script for execution
 define("REMOTE_MAILDIR_CUR", "/srv/mailusers/{%user}/Maildir/cur/");

  # defines the remote path to the maildir new:
 # * this path MUST point to a single directory
 # * do NOT forget the trailing slash
 # * the optional placeholder {%server} may be used which
 #   contains the parameter given to the script for execution
 # * the optional placeholder {%user} may be used which
 #   contains the parameter given to the script for execution
 define("REMOTE_MAILDIR_NEW", "/srv/mailusers/{%user}/Maildir/new/");

  # defines the remote hostname
 define("REMOTE_HOSTNAME", "<REPLACE-ME>");

  # defines the remote port
 define("REMOTE_PORT", 22);

  # defines the remote user
 define("REMOTE_USERNAME", "<REPLACE-ME>");

  # defines the remote password
 define("REMOTE_PASSWORD", "<REPLACE-ME>");

?>

Aufgerufen wird mailsync.phs immer wie folgt:

1
sudo php mailsync.phs <user> <server>

Ich werde an dieser Stelle einmal versuchen, Schritt für Schritt zu erklären, wie mailsync.phs arbeitet, wenn es aufgerufen wird. Die angegebenen Pfade beziehen sich dabei auf die obige Beispielkonfiguration:

  1. Nachdem mailsync.phs gestartet wurde, prüft es, ob die Datei "mailsync.conf.<server>.conf" existiert. Falls das der Fall ist, wird sie eingebunden, um die serverspezifischen Konfigurationswerte zu erhalten.
  2. Es prüft, ob der Ordner "/srv/mailusers/<user>" existiert. Zudem wird geprüft, ob die Ordner "/srv/mailusers/<user>/Maildir/new", "/srv/mailusers/<user>/Maildir/tmp" und "/srv/mailusers/<user>/Maildir/.Junk/new" existieren.
  3. Falls das der Fall ist, liest das Script die Nutzer- und Gruppenberechtigung des Ordners "/srv/mailusers/<user>" aus und verringert seine Prozessprivilegien auf diesen Nutzer und diese Gruppe.
  4. Anschließend wird die Lock-Datei "/srv/mailusers/<user>/lock.<server>" erzeugt, um Mehrfachstarts des Scripts zu verhindern (beachtet den Parameter TIMEOUT!).
  5. Nun wird die SSH-Verbindung zum Remote-Host mit dem entsprechenden Benutzernamen und Passwort aufgebaut.
  6. Wenn das funktioniert hat, wird auf dem Remote-Host der Inhalt des Ordner "/srv/mailusers/<user>/Maildir/new" ausgelesen. Jede Datei in diesem Ordner entspricht einer empfangenen E-Mail.
  7. Nun werden die E-Mails einzeln durchgearbeitet.
  8. Es wird geprüft, ob die E-Mails schon einmal synchronisiert werden sollten und dabei einen Fehler verursacht haben (was in der Datei STATUS_PATH festgehalten wird). An dieser Stelle gibt es die Möglichkeit, dass die Dateien erneut synchronisiert werden (wenn RETRY_ON_FAILURE auf true gesetzt ist). Alternativ werden die Dateien übersprungen, wenn sie nicht explizit in der Datei RETRY_STATUS_PATH aufgelistet sind. Das soll sich wiederholende Fehler bei der Synchronisation verhindern.
  9. Für die Dateien, die synchronisiert werden sollen, wird die Dateigröße abgerufen und geprüft, ob diese kleiner oder gleich dem Wert MAX_MAIL_SIZE sind.
  10. Die einzelnen Dateien werden eingelesen und in den Ordner "/srv/mailusers/<user>/Maildir/tmp" geschrieben. Nach dem Schreiben wird zudem geprüft, ob die geschriebenen Dateien die gleiche Größe haben, wie die vormals eingelesenen Dateien.
  11. Nach diesem Schritt erfolgt die Einsortierung in den Posteingang oder aber in den Spamordner. E-Mails, die einen Header mit dem Inhalt von SPAM_HEADER_START oder VIRUS_HEADER_START enthalten, gelten als Spam. Alle anderen wandern in den Posteingang.
  12. Mails, die in den Posteingang gehören, werden in den Ordner "/srv/mailusers/<user>/Maildir/new" verschoben. Mails, die als Spam einsortiert werden, gelangen stattdessen in den Ordner "/srv/mailusers/<user>/Maildir/.Junk/new"
  13. Erfolgreich bearbeitete Dateien werden auf dem Remote-Host entweder gelöscht, oder aber dort in den Ordner "/srv/mailusers/<user>/Maildir/cur" verschoben. Dies wird durch den Wert DELETE_ON_SUCCESS eingestellt.
  14. Sollte DELETE_ON_SUCCESS auf false gestellt sein und DELETE_AFTER_DAYS einen Wert größer null enthalten, werden die Dateien im Ordner "/srv/mailusers/<user>/Maildir/cur" daraufhin geprüft, ob sie DELETE_AFTER_DAYS Tage alt sind. Ist das der Fall, werden sie gelöscht.
  15. Sollten Fehler während der Synchronisation auftreten, dann wird eine Nachricht in die Datei "/srv/pushinfo/messages/mailsync.phs_<server>" geschrieben.
  16. Wenn die Synchronisation beendet ist, wird abschließend die Lock-Datei "/srv/mailusers/<user>/lock.<server>" wieder entfernt.

Nach dem Konfigurieren des Scripts müsst ihr nur noch dafür sorgen, dass es auch regelmäßig für alle eure Nutzer aufgerufen wird. Dafür könnt ihr z.B. einen Cronjob einrichten, der alle paar Minuten läuft. Je seltener das Script arbeitet, desto länger müsst ihr potentiell auf den Erhalt einer E-Mail warten:

1
*/5 * * * * root php /path/to/mailsync.phs <user> <server> >/dev/null 2>&1

Wie gesagt: Es ist ein einfaches Script, das sich die Ablageform der E-Mails stark zu Nutze macht und naiv seine Arbeit verrichtet. Das Thema der serverübergreifenden Synchronisation ist eines für sich, mit vielen Ansätzen und unendlich vielen Möglichkeiten. Welche Lösung ihr an dieser Stelle verwendet, bleibt am Ende ganz euch selbst überlassen. 🙂

Update:
mailsync.phs kann inzwischen Push Notifications mit Hilfe von Pushinfo und dem Dienst PushOver.net versenden. Zudem werden E-Mails, bei denen eine Synchronisation fehlgeschlagen ist, nicht mehr zwingend erneut synchronisiert. Das soll Mehrfachzustellungen und andere Probleme verhindern.

Update:
Die komplette Pfadkonfiguration wurde neu geschrieben. Anstelle von relativen Pfadangaben, die stupide zusammengesetzt wurden, können Pfade nun mit Hilfe von Platzhaltern verständlicher und effektiver konfiguriert werden. Zudem wurden ausführlichere Kommentare zu den einzelnen Pfadangaben verfasst.

Update:
Es ist nun möglich, erfolgreich abgerufene E-Mails nach einer bestimmten Anzahl an Tagen löschen zu lassen. Hierfür wurde der Parameter DELETE_AFTER_DAYS eingeführt.

Synchronisierende Grüße, Kenny

Schreibe einen Kommentar

Um Ihnen beim weiteren Kommentieren auf dieser Webseite die erneute Eingabe Ihrer Daten zu ersparen, wird beim Absenden Ihres Kommentars ein Cookie an Ihren Browser gesendet und von diesem gespeichert. Mit dem Absenden eines Kommentars auf dieser Webseite stimmen Sie der Speicherung und Übertragung dieses Cookies explizit zu.

Pflichtfelder sind mit * markiert.