Eine Mail-Queue.
  • Python 98.9%
  • Dockerfile 1.1%
Find a file
Ralf Warmuth 7dafeb1e3b
All checks were successful
Test and Build Mail Queue Image / build-image (push) Successful in 19s
Registry-Compose-Betrieb dokumentieren
2026-07-15 00:13:26 +02:00
.forgejo/workflows Forgejo-CI für Tests und Image-Build ergänzen 2026-07-15 00:05:12 +02:00
config.example Initiale Docker-Mail-Queue implementieren 2026-07-13 15:19:14 +02:00
src/mailqueue_cli Private Laufzeitdateien absichern 2026-07-14 23:27:27 +02:00
tests Mehrstündigen Mailversand simulieren 2026-07-14 23:41:30 +02:00
.dockerignore Forgejo-CI für Tests und Image-Build ergänzen 2026-07-15 00:05:12 +02:00
.gitignore Initiale Docker-Mail-Queue implementieren 2026-07-13 15:19:14 +02:00
CHANGELOG.md Release v0.1.0 dokumentieren 2026-07-14 23:52:03 +02:00
compose.image.yaml Registry-Compose-Betrieb dokumentieren 2026-07-15 00:13:26 +02:00
compose.yaml Compose-Container weiter härten 2026-07-14 23:49:08 +02:00
Dockerfile Forgejo-CI für Tests und Image-Build ergänzen 2026-07-15 00:05:12 +02:00
NEXT_SESSION.md Registry-Compose-Betrieb dokumentieren 2026-07-15 00:13:26 +02:00
pyproject.toml Initiale Docker-Mail-Queue implementieren 2026-07-13 15:19:14 +02:00
README.md Registry-Compose-Betrieb dokumentieren 2026-07-15 00:13:26 +02:00
REALTEST.md Realtest und Session-Handoff dokumentieren 2026-07-13 15:23:36 +02:00

Mail Queue CLI

Kleine, persistente SMTP-Queue für personalisierte Bestätigungsmails. Die Anwendung liest Empfänger aus einer CSV, setzt deren Daten in ein Text-/HTML-Template ein und versendet die Mails kontrolliert über SMTP/TLS.

Identische Datensätze werden während eines Versandlaufs nur einmal verschickt. Dieselbe E-Mail-Adresse darf mit unterschiedlichen Namen, Vornamen oder Geschlechtsangaben mehrfach vorkommen und erhält dann jeweils eine eigene personalisierte Mail.

Aktueller stabiler Stand: v0.1.0. Änderungen stehen im Changelog.

Schnellstart mit lokal gebautem Image

Die Datei compose.yaml baut das Image aus dem aktuellen Checkout. Diese Variante ist für Entwicklung und lokale Tests gedacht.

cp -a config.example config
mkdir -p data
chmod 700 config data
chmod 600 config/config.toml config/recipients.csv config/template.mail
# SMTP-Daten, CSV und Template unter config/ bearbeiten

export MAILQUEUE_UID="$(id -u)"
export MAILQUEUE_GID="$(id -g)"
docker compose build
docker compose up -d
docker compose run --rm mailqueue plan
docker compose run --rm mailqueue enqueue
docker compose run --rm mailqueue status
docker compose logs -f

plan prüft vorab CSV, Template und Konfiguration, ohne Jobs einzureihen oder eine noch nicht vorhandene Datenbank anzulegen. Erst nach Kontrolle des Plans sollte enqueue ausgeführt werden.

enqueue kann jederzeit erneut aufgerufen werden, auch während der Worker läuft. Bereits wartende oder versendete identische Datensätze werden übersprungen.

Betrieb mit dem Registry-Image

Die Datei compose.image.yaml verwendet das von Forgejo Actions gebaute Image aus git.home.schumbi.de/ralf/mail-queue-cli. Auf dem Betriebsrechner werden deshalb weder Python noch ein lokaler Docker-Build benötigt. Erforderlich sind nur Docker mit Compose-Unterstützung, diese Compose-Datei sowie die lokalen Verzeichnisse config/ und data/.

Erstmalige Einrichtung

Die folgenden Befehle werden im Projektverzeichnis ausgeführt:

cp -a config.example config
mkdir -p data
chmod 700 config data
chmod 600 config/config.toml config/recipients.csv config/template.mail

export MAILQUEUE_UID="$(id -u)"
export MAILQUEUE_GID="$(id -g)"

MAILQUEUE_UID und MAILQUEUE_GID müssen zu dem Host-Benutzer gehören, dem config/ und data/ gehören. Ohne Angabe verwendet Compose jeweils 1000. Bei einer anderen Benutzerkennung könnte der Container die geschützten Dateien nicht lesen oder die Datenbank nicht schreiben. Die Variablen müssen in jeder neuen Shell erneut gesetzt oder dauerhaft in der lokalen Betriebsumgebung hinterlegt werden.

Konfiguration

Vor dem ersten Start werden diese Dateien angepasst:

  • config/config.toml: SMTP-Zugang und Versandlimit
  • config/recipients.csv: Empfänger und Personalisierungsdaten
  • config/template.mail: Betreff sowie Text- und optionaler HTML-Inhalt

Die wichtigsten Werte in config/config.toml sind:

Wert Bedeutung
smtp.host, smtp.port SMTP-Server und Port
smtp.security starttls oder tls
smtp.username, smtp.password SMTP-Anmeldedaten
smtp.from_address, smtp.from_name sichtbarer Absender
smtp.reply_to optionale Antwortadresse
queue.max_emails_per_hour maximales Versandtempo, erlaubt sind 1 bis 60

Die Beispieldatei enthält alle weiteren optionalen Werte. Da config/config.toml das SMTP-Passwort enthält, darf sie nicht für andere Benutzer lesbar sein. config/ wird im Container nur lesend eingebunden; Queue-Zustand und Sperrdatei liegen persistent in data/.

Image-Version auswählen

Ohne weitere Angabe verwendet Compose den Tag latest. Das ist für einen ersten Test praktisch. Für den produktiven Betrieb sollte der vollständige, von Forgejo Actions veröffentlichte Commit-SHA verwendet werden:

export MAILQUEUE_IMAGE_TAG="VOLLSTAENDIGEN_COMMIT_SHA_EINTRAGEN"
docker compose -f compose.image.yaml pull

Damit bleibt die eingesetzte Version reproduzierbar. Für ein Rollback wird der Tag auf den zuvor verwendeten Commit-SHA zurückgesetzt und das Image erneut gestartet. Ist die Registry nicht öffentlich lesbar, muss der Host vorher mit docker login git.home.schumbi.de angemeldet werden.

Konfiguration prüfen und Worker starten

plan validiert SMTP-Konfiguration, CSV und Template, verändert aber weder die Queue noch legt es eine fehlende Datenbank an:

docker compose -f compose.image.yaml run --rm --no-deps mailqueue plan
docker compose -f compose.image.yaml up -d
docker compose -f compose.image.yaml ps

Wenn der Plan stimmt, werden die Mails eingereiht. Der bereits laufende Worker beginnt anschließend mit dem kontrollierten Versand:

docker compose -f compose.image.yaml run --rm mailqueue enqueue
docker compose -f compose.image.yaml run --rm mailqueue status
docker compose -f compose.image.yaml logs -f mailqueue

Die Loganzeige läuft fortlaufend und wird mit Strg+C beendet; der Worker läuft dabei im Hintergrund weiter. In der Ausgabe von ps sollte der Dienst nach der Startphase als healthy erscheinen.

Aktualisieren und Konfigurationsänderungen

Nach Auswahl eines neuen Image-Tags wird der Dienst so aktualisiert:

export MAILQUEUE_IMAGE_TAG="NEUEN_VOLLSTAENDIGEN_COMMIT_SHA_EINTRAGEN"
docker compose -f compose.image.yaml pull
docker compose -f compose.image.yaml up -d
docker compose -f compose.image.yaml ps

Compose ersetzt dabei den Container, lässt aber config/ und data/ auf dem Host unverändert. Nach einer Änderung von config/config.toml muss der Worker mit docker compose -f compose.image.yaml restart mailqueue neu gestartet werden. CSV und Template sollten für einen begonnenen Versandlauf nicht mehr geändert werden.

Stoppen und Entfernen

docker compose -f compose.image.yaml stop
docker compose -f compose.image.yaml down

stop hält den Worker an. down entfernt zusätzlich den Container und das Compose-Netzwerk. Beide Befehle behalten config/ und data/ auf dem Host. Die Queue wird nur mit dem weiter unten beschriebenen, ausdrücklich bestätigten clear --yes gelöscht.

Alle nachfolgenden Beispiele verwenden die lokale Standarddatei compose.yaml. Beim Betrieb mit dem Registry-Image wird dem jeweiligen Befehl einfach -f compose.image.yaml hinzugefügt.

Nach dem April- beziehungsweise September-Lauf:

docker compose run --rm mailqueue status
docker compose stop mailqueue
docker compose run --rm mailqueue clear --yes

clear verweigert das Löschen, solange der Worker läuft oder noch Jobs ausstehen. So kann der Worker nach dem Löschen keine Zustandsdaten neu anlegen. Sind dauerhafte oder unklare Fehler vorhanden, müssen diese zuerst geprüft und erneut freigegeben werden:

docker compose run --rm mailqueue retry-failed
docker compose run --rm mailqueue retry-failed --include-unknown

Sollen Fehler bewusst verworfen werden, ist das beim Löschen ausdrücklich anzugeben:

docker compose run --rm mailqueue clear --yes --discard-failures

Eingabedateien

Der Container erwartet standardmäßig:

  • /config/config.toml
  • /config/recipients.csv
  • /config/template.mail
  • /data/queue.sqlite3 im persistent eingebundenen Verzeichnis ./data

Compose startet den Prozess unter MAILQUEUE_UID und MAILQUEUE_GID. Dadurch kann der Container die nur für den Host-Benutzer lesbare SMTP-Konfiguration verwenden, ohne als root zu laufen. Auf einem üblichen Einzelbenutzersystem sind beide Werte 1000; für abweichende Benutzerkennungen müssen sie wie im Schnellstart exportiert werden.

Der Container läuft zusätzlich mit schreibgeschütztem Root-Dateisystem, ohne Linux-Capabilities und mit no-new-privileges. Schreibzugriff besteht nur auf das persistente Datenverzeichnis und das kleine /tmp-Dateisystem.

config/ und data/ müssen Modus 0700 haben. SMTP-Konfiguration, Empfänger-CSV, Template, SQLite-Datenbank und Worker-Lockdatei dürfen nur für den Eigentümer zugänglich sein (0600). Die Anwendung verweigert zu offene Berechtigungen mit einem passenden chmod-Hinweis. Neue Datenbanken und Lockdateien werden direkt mit 0600 angelegt.

Die CSV darf Komma oder Semikolon verwenden und UTF-8 beziehungsweise UTF-8 mit BOM enthalten. Erkannte Pflichtspalten sind:

Inhalt Akzeptierte Überschriften
Geschlecht Geschlecht, Anrede
Name Name, Nachname
Vorname Vorname
E-Mail Email, E-Mail, E-Mail-Adresse

Weitere Spalten werden ignoriert. Ungültige Zeilen brechen den gesamten Import ohne Teiländerung ab. Vollständig identische Datensätze werden übersprungen und in der Zusammenfassung gezählt.

Das Templateformat:

Subject: Bestätigung Ihrer Anmeldung

--- text/plain ---
Hallo {{vorname}} {{name}},

vielen Dank für Ihre Anmeldung.

--- text/html ---
<p>Hallo {{vorname}} {{name}},</p>
<p>Vielen Dank für Ihre Anmeldung.</p>

Der HTML-Abschnitt ist optional. Erlaubte Platzhalter sind {{geschlecht}}, {{name}}, {{vorname}} und {{email}}. Während eines laufenden Versands darf das Template nicht gewechselt werden; dafür muss der vorherige Lauf zuerst abgeschlossen und mit clear entfernt werden.

Versand vorab prüfen

docker compose run --rm mailqueue plan
docker compose run --rm mailqueue plan --json

Der Plan zeigt:

  • Gesamtzahl und eindeutige Datensätze der CSV
  • Duplikate innerhalb der CSV
  • bereits in SQLite vorhandene Datensätze
  • neu einzureihende und schon ausstehende Jobs
  • das konfigurierte Stundenlimit
  • die geschätzte Zeit bis zur letzten SMTP-Übergabe

Eine vorhandene Datenbank wird dafür ausschließlich lesend geöffnet. Fehlt sie, wird mit einer leeren Queue gerechnet und keine Datenbank angelegt. Die Zeitprognose berücksichtigt das rollierende Stundenlimit, bereits gespeicherte Rate-Events, einen aktiven SMTP-Verbindungs-Backoff und ausstehende Jobs. Zusätzliche zukünftige Retries, Netzwerkfehler und die eigentliche Zustellzeit des Empfängerproviders sind nicht enthalten.

Versandverhalten

  • max_emails_per_hour ist eine harte Obergrenze in jedem rollierenden 60-Minuten-Fenster.
  • Die Versuche werden gleichmäßig verteilt. Bei 40 Mails pro Stunde wartet der Worker mindestens 90 Sekunden zwischen zwei Versuchen.
  • Fehler beim SMTP-Verbindungsaufbau oder Login warten global 1, 5, 15 und 60 Minuten, danach 6 Stunden. Der Zustand übersteht einen Worker-Neustart und wird nach erfolgreichem Verbindungsaufbau zurückgesetzt.
  • Verbindungsfehler verbrauchen weder einen Job-Versuch noch einen Rate-Limit-Slot.
  • SMTP-4xx werden mit Backoff erneut versucht, höchstens zehnmal.
  • Empfängerbezogene SMTP-5xx bleiben als permanent_failed liegen.
  • Ein nach SMTP-Übergabe abgebrochener Versuch wird als delivery_unknown nicht automatisch wiederholt.
  • Ein Prozessabbruch während sending wird beim Neustart ebenfalls als unklare Zustellung behandelt. Das vermeidet automatischen Doppelversand, kann aber eine manuelle Prüfung erfordern.
  • Eine erfolgreiche SMTP-Annahme bestätigt nur die Übergabe an den Mailserver, nicht die Zustellung im Empfängerpostfach.

Betrieb ohne Docker

python3 -m venv .venv
. .venv/bin/activate
pip install .

mailqueue --database ./data/queue.sqlite3 plan \
  --csv ./config/recipients.csv \
  --template ./config/template.mail \
  --config ./config/config.toml

mailqueue --database ./data/queue.sqlite3 enqueue \
  --csv ./config/recipients.csv \
  --template ./config/template.mail

mailqueue --database ./data/queue.sqlite3 worker \
  --config ./config/config.toml

Die Verzeichnisse und Eingabedateien müssen auch ohne Docker wie oben beschrieben mit 0700 beziehungsweise 0600 geschützt sein.

Tests

Die Tests enthalten eine vollständig zeit-simulierte Queue mit 100 Mails bei 40 Mails pro Stunde. Sie prüft rund zweieinhalb Stunden Versandzeit in wenigen Sekunden, ohne SMTP-Netzwerkzugriff oder reale Wartezeit. Dabei werden Plan-Prognose, Personalisierung, Mindestabstand und rollierendes Stundenfenster gemeinsam kontrolliert.

PYTHONPATH=src python3 -m unittest discover -s tests -v
python3 -m compileall -q src tests
docker compose build

Forgejo Actions führt dieselben Tests bei Pushes auf main und bei Pull Requests im Docker-Test-Stage aus. Erfolgreiche Pushes auf main veröffentlichen das Runtime-Image als git.home.schumbi.de/ralf/mail-queue-cli:latest sowie mit dem vollständigen Commit-SHA als unveränderlichem Tag.