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

322 lines
13 KiB
Markdown

# 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:
```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:<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.