Mailpostfach beim Hoster sichern: mbsync und ein Archiv, das nichts löschen kann

Schema: mbsync holt das Postfach beim Hoster in ein Archiv im Homelab, Dovecot zeigt es nur lesend im Mailprogramm an

Verfasst von

in

Mein wichtigstes Postfach liegt nicht im Homelab, sondern bei einem Webhoster. Es ist ein Catch-all-Postfach: Jede Adresse meiner Domains landet dort, und für jeden Dienst, bei dem ich mich anmelde, erfinde ich eine eigene Adresse. Das ist praktisch, aber das Postfach ist über die Jahre groß und unsortiert geworden, und eine Sicherung gab es nicht. Der Hoster sichert zwar seine Server, aber wenn ich selbst etwas lösche oder ein Mailprogramm beim Aufräumen falsch abgleicht, hilft mir das nicht. Bevor ich aufräume, wollte ich deshalb eine vollständige Kopie zu Hause haben, die sich täglich ergänzt und die ich im Mailprogramm durchsuchen kann. Hier steht, wie das mit mbsync und Dovecot aussieht und was dabei schiefging.

Erst zählen, dann planen

Ich hatte das Postfach auf etwa 60 GB geschätzt. Ein Blick per IMAP ergab etwas ganz anderes: rund 91.000 Nachrichten mit zusammen 7 GB. Das Kundenmenü des Hosters zeigte 48 GB belegten Speicher, aber der Großteil davon lag im Webspace, in längst vergessenen Webanwendungen. Die Größe eines Postfachs per IMAP zu bestimmen, ist nicht trivial: Mein Hoster beantwortet weder GETQUOTAROOT noch STATUS … SIZE. Geholfen hat, jeden Ordner schreibgeschützt zu öffnen und die Größen der einzelnen Nachrichten abzufragen. Das liest nur Kopfdaten und ändert nichts.

import imaplib, re
m = imaplib.IMAP4_SSL("imap.example.net", 993)
m.login(benutzer, kennwort)
for zeile in m.list()[1]:
    ordner = re.search(rb'"[^"]*" (.*)$', zeile).group(1).decode()
    typ, d = m.select(ordner, readonly=True)          # nur lesen
    if int(d[0]):
        typ, d = m.fetch("1:*", "(RFC822.SIZE)")
        groessen = [int(x) for x in re.findall(rb"RFC822\.SIZE (\d+)", b" ".join(
            x if isinstance(x, bytes) else x[0] for x in d))]
        print(f"{sum(groessen)/1e9:6.2f} GB {len(groessen):>7}  {ordner}")

Die Zahl je Ordner habe ich mir notiert. Sie ist später der Maßstab dafür, ob die Sicherung vollständig ist.

Der Aufbau

Schema: mbsync holt das Postfach beim Hoster in ein Archiv im Homelab, Dovecot zeigt es nur lesend im Mailprogramm an

Für das Archiv habe ich einen eigenen, kleinen LXC-Container auf Proxmox angelegt: Debian 13, eine CPU, 1 GB RAM. Er ist nur im LAN erreichbar und steht automatisch im nächtlichen Backup-Job. Darin laufen zwei Programme:

  • mbsync (Paket isync) holt die Postfächer per IMAP und legt sie als Maildir ab. Es arbeitet nur in eine Richtung und ändert beim Hoster nichts.
  • Dovecot stellt das Archiv per IMAPS bereit, nur lesend, damit ich es in Thunderbird oder auf dem Handy durchsuchen kann.

mbsync: holen, nie löschen

In der Konfiguration zählen die vier Regeln am Ende des Kanals. Sync Pull holt nur. Create Near legt neue Ordner lokal an. Remove None und Expunge None sorgen dafür, dass im Archiv nie etwas verschwindet, auch wenn es beim Hoster gelöscht wird. Das Kennwort steht nicht in der Datei, sondern wird mit PassCmd aus einer Datei mit restriktiven Rechten gelesen.

IMAPAccount hauptpostfach
Host imap.example.net
User postfachname
PassCmd "cat /etc/mailarchiv/hauptpostfach.pass"
TLSType IMAPS

IMAPStore hauptpostfach-fern
Account hauptpostfach

MaildirStore hauptpostfach-lokal
Path /srv/mail/hauptpostfach/
Inbox /srv/mail/hauptpostfach/INBOX
SubFolders Verbatim

Channel hauptpostfach
Far :hauptpostfach-fern:
Near :hauptpostfach-lokal:
Patterns *
Create Near
Remove None
Expunge None
Sync Pull
SyncState *
CopyArrivalDate yes

Die erste Sicherung lief bei mir etwa eine halbe Stunde, danach dauert der tägliche Abgleich nur noch Sekunden. Ein kleines Skript ruft mbsync -a auf, schreibt Zeit, Rückgabewert und Größe in eine Statusdatei, und ein systemd-Timer startet es jede Nacht.

Unterordner landen unter einem Ordner ohne Namen

Mein erster Versuch nutzte SubFolders Maildir++, weil Dovecot dieses Format kennt. Das ging schief. Die Unterordner meines Posteingangs, etwa INBOX/Rechnungen, legte mbsync als ..Rechnungen ab, und Dovecot zeigte sie dann unter einem Ordner ohne Namen an. Benutzbar wäre das gewesen, schön ist es nicht. Besser ist das Layout Verbatim mit eigenem Pfad für den Posteingang: Dann liegt INBOX/Rechnungen auch auf der Platte in INBOX/Rechnungen/, und Dovecot liest es mit mailbox_list_layout = fs genauso. Ich habe die Erstsicherung dafür einmal verworfen und neu gestartet. Das kostete eine halbe Stunde.

Umlaute in Ordnernamen

IMAP schreibt Ordnernamen in einer eigenen Kodierung: Aus „Entwürfe“ wird Entw&APw-rfe. mbsync legt den Ordner auf der Platte aber als normales UTF-8 an. Mein Vergleichsskript meldete deshalb zunächst eine Lücke von zwölf Mails, die in Wahrheit vollständig da waren, nur unter dem anderen Namen. Dovecot erwartet auf der Platte standardmäßig ebenfalls die IMAP-Kodierung. Mit mailbox_list_utf8 = yes passt es zu mbsync.

Dovecot nur lesend

Ein Archiv, in dem ich versehentlich etwas verschieben oder löschen kann, ist kein Archiv. Dovecot hat keinen Schalter „nur lesen“, aber das Dateisystem hilft. Die Mails gehören dem Benutzer archiv, unter dem mbsync läuft. Dovecot greift als eigener Benutzer leser zu, der in der Gruppe archiv ist und deshalb lesen, aber nicht schreiben darf. Seine Indexdateien legt Dovecot getrennt vom Archiv ab. Erkennt Dovecot, dass ein Ordner nicht beschreibbar ist, öffnet er ihn schreibgeschützt.

mail_driver = maildir
mail_path = /srv/mail/%{user}
mail_inbox_path = /srv/mail/%{user}/INBOX
mailbox_list_layout = fs
mailbox_list_utf8 = yes
mail_index_path = /var/lib/mailarchiv-index/%{user}
mail_control_path = /var/lib/mailarchiv-index/%{user}

userdb static {
  fields {
    uid = leser
    gid = archiv
    home = /var/lib/mailarchiv-index/%{user}
  }
}

Ein Haken dabei: mbsync legt neue Dateien nur für sich selbst lesbar an (Rechte 600). Deshalb setzt mein Sync-Skript nach jedem Lauf chmod -R g+rX auf das Archiv. Dass es wirklich nur lesend ist, habe ich ausprobiert: Der Versuch, eine Mail als gelöscht zu markieren, endet mit „INBOX is not writable“.

Prüfen statt hoffen

Eine Sicherung gilt bei mir erst, wenn ich sie gegen das Original gezählt habe. Ein zweites Skript fragt beim Hoster je Ordner die Zahl der Nachrichten ab und zählt die Dateien im Archiv. Weil das Archiv nie löscht, darf es mehr enthalten, aber nie weniger. Bei mir stand am Ende 91.296 zu 91.296. Erst danach habe ich überhaupt angefangen, beim Hoster etwas zu ändern.

PrüfungWie
VollständigZahl der Nachrichten je Ordner, Archiv ≥ Hoster
AktuellStatusdatei mit Zeit und Rückgabewert, Warnung nach 30 Stunden ohne Lauf
LesbarArchiv im Mailprogramm öffnen, eine alte Mail suchen
GeschütztLöschversuch über Dovecot muss scheitern

Die Statusdatei liest ein Local-Check meines Monitorings. Fehlt der nächtliche Lauf oder schlägt er fehl, sehe ich das am nächsten Morgen, ohne nachzuschauen.

Was beim Aufräumen passiert

Seit die Kopie vollständig ist, räume ich das Postfach beim Hoster auf, und zwar nur durch Verschieben. Verschiebe ich dort eine Mail in einen anderen Ordner, liegt sie im Archiv danach zweimal, im alten und im neuen Ordner. Das ist gewollt: Das Archiv zeigt den Stand vor dem Aufräumen und alles, was seitdem dazugekommen ist. Für die Sortierung selbst werte ich nur die Kopfzeilen aus, also Empfängeradresse, Absender, Datum und Größe. Beim Catch-all verrät die Empfängeradresse, von welchem Dienst eine Mail kommt und ob eine Adresse weitergegeben wurde.

Gut 47.000 Mails, darunter Newsletter, Spam und Papierkorb, liegen inzwischen in Sammelordnern zum Löschen. Geleert werden sie frühestens am 11. November, 30 Tage nach dem Verschieben, und im Archiv bleiben sie auch dann.

Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht, deine IP-Adresse wird nicht gespeichert. Kommentare erscheinen erst nach Freigabe. Was mit deinen Angaben passiert, steht in der Datenschutzerklärung. Pflichtfelder sind mit * markiert.