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:
smtp-servernimmt SMTP-Verbindungen auf dem konfigurierten Port an.services/emailParser.jsverarbeitet die Nachricht mitmailparser.- Anhänge werden über
services/s3Service.jsnach S3 hochgeladen, sofern sie nicht größer alsMAX_FILE_SIZEsind. - Die geparste Nachricht wird vor der SMTP-Bestätigung als JSON im persistenten Spool gespeichert.
better-queuesendet die Spool-Datei an den Webhook.- 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, nichtnpm startund nicht eine NVM-Version. Dadurch ist der Start unabhängig von einer interaktiven Shell. - Supervisor wird nicht mit
systemctl restart supervisorneu gestartet, wenn lediglichinbound-emailgeändert wurde; dadurch würden auch die Laravel- Worker beeinflusst. Für die App nurinbound-emailneu 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 |
WEBHOOK_BEARER_TOKEN |
Optionaler Bearer Token für den Authorization-Header des Webhooks |
nicht gesetzt |
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=productionSPOOL_DIR=/var/lib/smtpwebhook/spoolLOG_DIR=/var/lib/smtpwebhook/logsumask=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=unexpectedstartsecs=5startretries=10stopsignal=TERMstopasgroup=trueundkillasgroup=truestopwaitsecs=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:
supervisorctl ... status inbound-email- Portbelegung mit
ss -ltnp - aktuelle Datei unter
/var/lib/smtpwebhook/logs - Anzahl und Alter der Dateien in
spool/pendingundspool/failed - HTTP-Status und Erreichbarkeit des Webhooks
- 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 undnode_modulesdürfen nicht in den Index gelangen.git diff --cached --name-onlyundgit diff --cached --checkprüfen.package-lock.jsonmit den Änderungen synchron halten; bevorzugtnpm cibei 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_URLniemals hartcodieren und keinen externen Default hinzufügen..enventhält Zugangsdaten und sollte produktiv etwaroot:<Dienstgruppe>gehören, Modus640; der Dienst benötigt nur Leserechte.- TLS-Privatschlüssel nicht nach
/opt/inbound-emailkopieren. BeiSMTP_SECURE=truemüssenTLS_KEY_PATHundTLS_CERT_PATHfürsmtpwebhooklesbar 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
onDataimmer sicherstellen, dass die Nachricht vorcallback()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.
DockerFilebaut 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.mdenthält überwiegend die ursprüngliche Projektdokumentation; dieseAGENTS.mdbeschreibt die aktuelle Serverinstallation und hat für Codex-Arbeiten Vorrang.