Files
inbound-email/AGENTS.md
T
mherrlein 1edb326519
Docker / build (push) Canceled after 0s
Document standalone systemd deployment
2026-09-02 12:33:37 +02:00

13 KiB

AGENTS.md

Diese Datei ist die operative Kurzreferenz für Codex-Instanzen und andere Automatisierungen, die an diesem Projekt arbeiten. Die produktive Installation läuft derzeit unter /opt/inbound-email auf einem Linux-Server.

Zweck und Architektur

Das Projekt nimmt E-Mails über SMTP an, parst sie und sendet den geparsten Inhalt per HTTP POST an WEBHOOK_URL.

Der Ablauf in server.js ist:

  1. smtp-server nimmt SMTP-Verbindungen auf dem konfigurierten Port an.
  2. services/emailParser.js verarbeitet die Nachricht mit mailparser.
  3. Anhänge werden über services/s3Service.js nach S3 hochgeladen, sofern sie nicht größer als MAX_FILE_SIZE sind.
  4. Die geparste Nachricht wird vor der SMTP-Bestätigung als JSON im persistenten Spool gespeichert.
  5. better-queue sendet die Spool-Datei an den Webhook.
  6. Die Spool-Datei wird erst nach erfolgreicher Webhook-Antwort gelöscht.

Die Zustellung ist damit grundsätzlich „at least once“: Bei einem Absturz nach erfolgreicher Webhook-Annahme, aber vor dem Löschen der Spool-Datei, kann eine Nachricht doppelt zugestellt werden. Der Webhook sollte deshalb idempotent sein.

Die Verzeichnisse haben folgende Bedeutung:

Pfad Zweck
SPOOL_DIR/pending Noch nicht erfolgreich zugestellte Nachrichten
SPOOL_DIR/failed Unlesbare oder ungültige Spool-Dateien zur manuellen Prüfung
LOG_DIR JSON-Anwendungslogs mit täglicher Rotation
/opt/inbound-email/logs Historische Logs der alten Installation; nicht als aktueller Spool verwenden

Der Spool wird beim Start und danach ungefähr jede Minute gescannt. Fehlende Webhook-Erreichbarkeit darf daher nicht durch manuelles Löschen von Dateien „behoben“ werden. Zuerst Fehlerursache und Payload prüfen.

Wichtige Betriebsregeln

  • Es darf nur eine Instanz auf dem SMTP-Port laufen. Niemals parallel npm start, PM2, eine zweite Supervisor-Definition oder eine separate systemd-App-Unit starten.
  • Produktionsseitig ist Supervisor der Prozessmanager für inbound-email. systemd verwaltet den Supervisor selbst, nicht zusätzlich dieselbe Node-App.
  • Der produktive Start verwendet absichtlich den festen Pfad /usr/bin/node /opt/inbound-email/server.js, nicht npm start und nicht eine NVM-Version. Dadurch ist der Start unabhängig von einer interaktiven Shell.
  • Supervisor wird nicht mit systemctl restart supervisor neu gestartet, wenn lediglich inbound-email geändert wurde; dadurch würden auch die Laravel- Worker beeinflusst. Für die App nur inbound-email neu laden bzw. starten.
  • Vor Änderungen an Laufzeit, Spool oder Logs immer den aktuellen Prozess und Port prüfen. Ein verwaister Altprozess kann sonst den Port blockieren.
  • Keine Spool-Dateien, .env-Dateien, Logs oder TLS-Schlüssel committen.

Konfiguration

Die Konfiguration wird aus .env geladen. WEBHOOK_URL ist zwingend und hat absichtlich keinen produktiven Defaultwert. Ungültige Werte führen beim Start zu einem kontrollierten Fehler, damit Supervisor den Fehler erkennen kann.

Variable Bedeutung Standardwert
WEBHOOK_URL Ziel-URL für HTTP POST erforderlich
PORT SMTP-Port 25 (Produktion: 2525)
SMTP_SECURE SMTPS/TLS beim Verbindungsaufbau aktivieren false
TLS_KEY_PATH TLS-Privatschlüssel, wenn SMTP_SECURE=true keiner
TLS_CERT_PATH TLS-Zertifikat, wenn SMTP_SECURE=true keines
SMTP_ALLOWED_IPS Optionale, komma-separierte Quell-IP-Allowlist leer = alle
MAX_MESSAGE_SIZE Maximale SMTP-Nachricht in Bytes; 0 deaktiviert das Limit 26214400 (25 MiB)
MAX_FILE_SIZE Maximale S3-Anhanggröße in Bytes 5242880 (5 MiB)
MAX_SMTP_CLIENTS Maximale parallele SMTP-Clients 50
SMTP_SOCKET_TIMEOUT SMTP-Socket-Timeout in Millisekunden 300000
SMTP_CLOSE_TIMEOUT Timeout für SMTP-Verbindungsabbau in Millisekunden 10000
WEBHOOK_CONCURRENCY Parallele Webhook-Jobs 5
WEBHOOK_QUEUE_RETRIES Queue-Wiederholungen nach einem fehlgeschlagenen Job 10
WEBHOOK_QUEUE_RETRY_DELAY Wartezeit zwischen Queue-Wiederholungen in Millisekunden 60000
WEBHOOK_QUEUE_TIMEOUT Maximale Dauer eines Queue-Jobs in Millisekunden 35000
SPOOL_DIR Basisverzeichnis des persistenten Spools /var/lib/smtpwebhook/spool
LOG_DIR Verzeichnis der aktuellen Anwendungslogs /var/lib/smtpwebhook/logs
AWS_REGION AWS-Region abhängig von AWS SDK
AWS_ACCESS_KEY_ID AWS-Zugangsschlüssel keiner
AWS_SECRET_ACCESS_KEY AWS-Geheimschlüssel keiner
S3_BUCKET_NAME S3-Bucket für Anhänge keiner

Die aktuelle produktive Supervisor-Definition setzt zusätzlich:

  • NODE_ENV=production
  • SPOOL_DIR=/var/lib/smtpwebhook/spool
  • LOG_DIR=/var/lib/smtpwebhook/logs
  • umask=027
  • Benutzer smtpwebhook

SMTP_ALLOWED_IPS ist derzeit nicht gesetzt. Da SMTP AUTH deaktiviert ist, sollte der Netzwerkzugriff durch Firewall oder IP-Allowlist auf bekannte Absender begrenzt werden, sobald diese IPs feststehen.

Installation und lokaler Start

Voraussetzungen sind Node.js 18 oder neuer, npm, ein erreichbarer Webhook und bei aktivierter Anhangspeicherung passende S3-Berechtigungen.

Beispiel für eine neue Installation:

git clone https://git.hacker.schule/mherrlein/inbound-email.git
cd inbound-email
npm ci
cp .env.example .env
${EDITOR:-vi} .env
chmod 640 .env

Für einen lokalen Vordergrundtest müssen mindestens WEBHOOK_URL und ein nicht privilegierter Port gesetzt sein. dotenv lädt .env relativ zum aktuellen Arbeitsverzeichnis; daher aus dem Projektverzeichnis starten:

PORT=25252 \
SPOOL_DIR=/tmp/smtpwebhook-spool \
LOG_DIR=/tmp/smtpwebhook-logs \
npm start

Für produktive Pfade müssen die Verzeichnisse vor dem Start dem Dienstbenutzer gehören und restriktiv geschützt sein:

install -d -o smtpwebhook -g smtpwebhook -m 700 \
  /var/lib/smtpwebhook \
  /var/lib/smtpwebhook/spool/pending \
  /var/lib/smtpwebhook/spool/failed \
  /var/lib/smtpwebhook/logs

Die konkrete Produktivdefinition liegt außerhalb des Repositories unter /etc/supervisor/conf.d/inbound-email.conf. Nach Änderung dieser Datei:

supervisorctl -c /etc/supervisor/supervisord.conf reread
supervisorctl -c /etc/supervisor/supervisord.conf update inbound-email
supervisorctl -c /etc/supervisor/supervisord.conf status inbound-email

Bei reinem Code- oder .env-Update genügt normalerweise:

supervisorctl -c /etc/supervisor/supervisord.conf restart inbound-email

Supervisor und systemd

Die aktuelle App-Definition verwendet unter anderem:

  • autorestart=unexpected
  • startsecs=5
  • startretries=10
  • stopsignal=TERM
  • stopasgroup=true und killasgroup=true
  • stopwaitsecs=30
  • begrenzte Supervisor-Logs mit Rotation

Zusätzlich existiert außerhalb des Repositories der Drop-in /etc/systemd/system/supervisor.service.d/10-kill-children.conf. Er setzt KillMode=control-group, damit beim Neustart des Supervisor-Managers keine verwaisten App-Prozesse zurückbleiben, sowie Restart=on-failure und RestartSec=5s.

Nach Änderungen am Drop-in ist nur ein Reload der Unitdefinition erforderlich:

systemctl daemon-reload
systemctl show supervisor.service -p KillMode -p Restart -p RestartUSec

Der aktuelle Laufzeitcheck sollte mindestens Folgendes bestätigen:

supervisorctl -c /etc/supervisor/supervisord.conf status inbound-email
ss -ltnp | grep ':2525'
ps -C node -o user,group,pid,ppid,stat,etime,args

Erwartet wird genau ein Node-Prozess für /opt/inbound-email/server.js unter smtpwebhook und genau ein Listener auf dem konfigurierten Port.

Alternative: eigenständige systemd-Unit

Wenn Supervisor auf einem Zielsystem nicht vorhanden ist, kann inbound-email direkt durch systemd gestartet werden. Dafür eine Unit unter /etc/systemd/system/smtpwebhook.service anlegen:

[Unit]
Description=SMTP to Webhook bridge
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=smtpwebhook
WorkingDirectory=/opt/inbound-email
EnvironmentFile=/opt/inbound-email/.env
Environment=NODE_ENV=production
Environment=SPOOL_DIR=/var/lib/smtpwebhook/spool
Environment=LOG_DIR=/var/lib/smtpwebhook/logs
ExecStart=/usr/bin/node /opt/inbound-email/server.js
Restart=on-failure
RestartSec=5s
KillMode=control-group
TimeoutStopSec=30s
UMask=0027
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadOnlyPaths=/opt/inbound-email
ReadWritePaths=/var/lib/smtpwebhook

[Install]
WantedBy=multi-user.target

Die Pfade für Spool und Logs müssen vor dem Start existieren und dem Benutzer smtpwebhook gehören. Auf Systemen mit anderer Benutzer- oder Gruppenstruktur die Unit und die Dateirechte entsprechend anpassen. Danach:

systemctl daemon-reload
systemctl enable --now smtpwebhook.service
systemctl status smtpwebhook.service
journalctl -u smtpwebhook.service -f

Supervisor und diese systemd-App-Unit dürfen niemals gleichzeitig dieselbe Instanz starten. Bei einer Umstellung zuerst den inbound-email-Block aus der Supervisor-Konfiguration entfernen oder deaktivieren, danach reread/update ausführen und erst dann smtpwebhook.service aktivieren. Den Supervisor selbst nicht stoppen, wenn er noch andere Programme wie Laravel-Worker verwaltet. Beim Wechsel zurück zu Supervisor die systemd-Unit zuerst stoppen und deaktivieren, anschließend den Supervisor-Block wieder aktivieren.

Logging und Fehleranalyse

Die Hauptlogs von server.js werden als JSON in LOG_DIR geschrieben und von Winston täglich sowie ab 20 MiB rotiert; Aufbewahrung ist auf ungefähr 90 Tage begrenzt. Supervisor führt zusätzlich die Dateien /var/log/inbound-email.out.log und /var/log/inbound-email.err.log.

Bei Problemen in dieser Reihenfolge prüfen:

  1. supervisorctl ... status inbound-email
  2. Portbelegung mit ss -ltnp
  3. aktuelle Datei unter /var/lib/smtpwebhook/logs
  4. Anzahl und Alter der Dateien in spool/pending und spool/failed
  5. HTTP-Status und Erreichbarkeit des Webhooks
  6. S3-Berechtigungen und Netzwerkzugriff, falls Anhänge verarbeitet werden

Die Einträge EADDRINUSE im Log weisen auf eine doppelte oder verwaiste Instanz hin. Nicht einfach eine zweite App starten; zuerst den Besitzer des Ports identifizieren und den alten Prozess kontrolliert beenden.

Die EPIPE-Probleme vom August 2026 wurden durch die frühere Winston-Console- Transport-Konfiguration begünstigt. Die Hauptlogger-Konfiguration verwendet deshalb einen File-Transport. Neue console.log-/console.error-Ausgaben in der App sollten vermieden und bei Gelegenheit über denselben Logger geführt werden.

Änderungen, Tests und Git

Vor Änderungen zuerst git status und die bestehende Konfiguration lesen. Das Repository enthält eine abweichende, gehärtete Weiterentwicklung des ursprüng- lichen GitHub-Projekts. upstream zeigt weiterhin auf das Original; origin zeigt auf https://git.hacker.schule/mherrlein/inbound-email.

Mindestens diese Prüfungen ausführen:

node --check server.js
node --check config.js
git diff --check

Aktuell existieren keine automatisierten Testfälle. Ein SMTP-Handshake gegen localhost prüft nur Listener und Protokollannahme; er testet weder S3 noch den Webhook und darf keine Testmail an ein produktives Ziel senden.

Vor einem Commit:

  • .env, private Schlüssel, Logs, Spool-Dateien und node_modules dürfen nicht in den Index gelangen.
  • git diff --cached --name-only und git diff --cached --check prüfen.
  • package-lock.json mit den Änderungen synchron halten; bevorzugt npm ci bei einer sauberen Installation verwenden.
  • Keine History des Ziel-Repositories per Force-Push überschreiben.
  • Ein Push zu einem externen Remote benötigt eine explizite Benutzerfreigabe.

Sicherheits- und Änderungsfallen

  • WEBHOOK_URL niemals hartcodieren und keinen externen Default hinzufügen.
  • .env enthält Zugangsdaten und sollte produktiv etwa root:<Dienstgruppe> gehören, Modus 640; der Dienst benötigt nur Leserechte.
  • TLS-Privatschlüssel nicht nach /opt/inbound-email kopieren. Bei SMTP_SECURE=true müssen TLS_KEY_PATH und TLS_CERT_PATH für smtpwebhook lesbar sein.
  • Der SMTP-Server deaktiviert AUTH und ist bei leerer Allowlist für alle erreichbaren IPs offen. Netzwerkfreigaben daher bewusst konfigurieren.
  • Spool-Zustellung nicht wieder vollständig in eine reine In-Memory-Queue zurückbauen. Die Queue ist nur der Dispatcher; die JSON-Datei ist die Restart-Garantie.
  • Bei Änderungen an onData immer sicherstellen, dass die Nachricht vor callback() persistent gespeichert ist.
  • Beim Löschen oder Verschieben von Spool-Dateien zuerst Zweck, Alter und Inhalt prüfen. Das Verzeichnis ist operative Nutzdatenhaltung, kein Cache.
  • DockerFile baut derzeit aus dem öffentlichen GitHub-Original und bildet die lokale gehärtete Installation nicht automatisch ab. Nicht für ein produktives Update verwenden, ohne Clone-URL, Branch, Spool-Persistenz und Logging zu prüfen bzw. anzupassen.
  • readme.md enthält überwiegend die ursprüngliche Projektdokumentation; diese AGENTS.md beschreibt die aktuelle Serverinstallation und hat für Codex-Arbeiten Vorrang.