322 lines
13 KiB
Markdown
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.
|