# 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 | | `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=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: ```bash 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: ```bash 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: ```bash 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: ```bash 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: ```bash 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: ```bash systemctl daemon-reload systemctl show supervisor.service -p KillMode -p Restart -p RestartUSec ``` Der aktuelle Laufzeitcheck sollte mindestens Folgendes bestätigen: ```bash 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: ```ini [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: ```bash 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: ```bash 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:` 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.