- Python 98.9%
- Dockerfile 1.1%
|
All checks were successful
Test and Build Mail Queue Image / build-image (push) Successful in 19s
|
||
|---|---|---|
| .forgejo/workflows | ||
| config.example | ||
| src/mailqueue_cli | ||
| tests | ||
| .dockerignore | ||
| .gitignore | ||
| CHANGELOG.md | ||
| compose.image.yaml | ||
| compose.yaml | ||
| Dockerfile | ||
| NEXT_SESSION.md | ||
| pyproject.toml | ||
| README.md | ||
| REALTEST.md | ||
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 Versandlimitconfig/recipients.csv: Empfänger und Personalisierungsdatenconfig/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.sqlite3im 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 |
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_hourist 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_failedliegen. - Ein nach SMTP-Übergabe abgebrochener Versuch wird als
delivery_unknownnicht automatisch wiederholt. - Ein Prozessabbruch während
sendingwird 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.