240 lines
8.7 KiB
Markdown
240 lines
8.7 KiB
Markdown
# Inbound Email (SMTP) to Webhook
|
||
|
||
Diese Version ist eine gehärtete Weiterentwicklung des ursprünglichen
|
||
Projekts. Sie nimmt E-Mails über SMTP an, parst Inhalt und Header, lädt Anhänge
|
||
optional nach Amazon S3 und sendet die Nachricht an einen HTTP(S)-Webhook.
|
||
|
||
Originalentwickler: Martin Krivosija – [LinkedIn](https://linkedin.com/in/martin-alexander-k)
|
||
|
||
Dieses Repository ist von [`kriiv/inbound-email`](https://github.com/kriiv/inbound-email)
|
||
geforkt und wird hier als eigenständige, gehärtete Weiterentwicklung gepflegt.
|
||
|
||
Das Repository liegt unter:
|
||
|
||
```text
|
||
https://git.hacker.schule/mherrlein/inbound-email
|
||
```
|
||
|
||
## Funktionsweise
|
||
|
||
Der Zustellweg lautet:
|
||
|
||
```text
|
||
SMTP -> mailparser -> optional S3 -> persistenter Spool -> Webhook
|
||
```
|
||
|
||
Eine Nachricht wird vor der SMTP-Bestätigung als JSON unter `SPOOL_DIR/pending`
|
||
gespeichert. Erst nach erfolgreicher Webhook-Antwort wird sie gelöscht. Dadurch
|
||
überlebt die Zustellung Neustarts und vorübergehende Webhook-Fehler.
|
||
|
||
Die Zustellung ist „at least once“. Wenn der Prozess nach erfolgreicher
|
||
Webhook-Annahme, aber vor dem Löschen der Spool-Datei abstürzt, kann die
|
||
Nachricht doppelt zugestellt werden. Der Webhook sollte deshalb idempotent sein.
|
||
|
||
## Voraussetzungen
|
||
|
||
- Node.js 18 oder neuer und npm
|
||
- ein erreichbarer HTTP(S)-Webhook
|
||
- AWS-S3-Zugangsdaten und ein Bucket, wenn Anhänge gespeichert werden sollen
|
||
- bei Produktion ein eigener Dienstbenutzer, z. B. `smtpwebhook`
|
||
|
||
## 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
|
||
```
|
||
|
||
`WEBHOOK_URL` muss gesetzt werden. Für einen lokalen Test kann ein nicht
|
||
privilegierter Port verwendet werden:
|
||
|
||
```bash
|
||
PORT=25252 \
|
||
SPOOL_DIR=/tmp/smtpwebhook-spool \
|
||
LOG_DIR=/tmp/smtpwebhook-logs \
|
||
npm start
|
||
```
|
||
|
||
Produktive Spool- und Log-Verzeichnisse müssen 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
|
||
```
|
||
|
||
## Konfiguration
|
||
|
||
Die Anwendung liest `.env` aus dem Arbeitsverzeichnis. `WEBHOOK_URL` ist
|
||
zwingend erforderlich und hat keinen öffentlichen Defaultwert.
|
||
|
||
| Variable | Bedeutung | Standardwert |
|
||
| --- | --- | --- |
|
||
| `WEBHOOK_URL` | Ziel-URL für die geparste Nachricht | erforderlich |
|
||
| `PORT` | SMTP-Port | `25` (Produktion: `2525`) |
|
||
| `SMTP_SECURE` | TLS/SMTPS beim Verbindungsaufbau | `false` |
|
||
| `TLS_KEY_PATH` | TLS-Privatschlüssel bei aktiviertem TLS | keiner |
|
||
| `TLS_CERT_PATH` | TLS-Zertifikat bei aktiviertem TLS | keines |
|
||
| `SMTP_ALLOWED_IPS` | Komma-separierte Quell-IP-Allowlist | leer = alle |
|
||
| `MAX_MESSAGE_SIZE` | Maximale Nachrichtengröße in Bytes; `0` deaktiviert das Limit | `26214400` (25 MiB) |
|
||
| `MAX_FILE_SIZE` | Maximale Größe eines S3-Anhangs in Bytes | `5242880` (5 MiB) |
|
||
| `MAX_SMTP_CLIENTS` | Maximale parallele SMTP-Verbindungen | `50` |
|
||
| `SMTP_SOCKET_TIMEOUT` | SMTP-Socket-Timeout in Millisekunden | `300000` |
|
||
| `SMTP_CLOSE_TIMEOUT` | Timeout für den Verbindungsabbau in Millisekunden | `10000` |
|
||
| `WEBHOOK_CONCURRENCY` | Parallele Webhook-Jobs | `5` |
|
||
| `WEBHOOK_QUEUE_RETRIES` | Queue-Wiederholungen bei Fehlern | `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 | keiner |
|
||
| `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 |
|
||
|
||
Wenn `MAX_FILE_SIZE=0` gesetzt ist, werden normale Anhänge nicht nach S3
|
||
hochgeladen. Für S3 müssen Bucket, Region und Zugangsdaten korrekt gesetzt sein.
|
||
Bei `SMTP_SECURE=true` müssen die TLS-Dateien für den Dienstbenutzer lesbar sein.
|
||
|
||
## Start und Produktion
|
||
|
||
Für einen Vordergrundstart:
|
||
|
||
```bash
|
||
npm start
|
||
```
|
||
|
||
Die produktive Installation verwendet Supervisor. Der Prozess wird mit
|
||
`/usr/bin/node /opt/inbound-email/server.js` unter dem Benutzer `smtpwebhook`
|
||
gestartet. Die Konfiguration liegt außerhalb des Repositories unter
|
||
`/etc/supervisor/conf.d/inbound-email.conf`.
|
||
|
||
Nach Änderungen an der Supervisor-Konfiguration:
|
||
|
||
```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 Änderungen an Code oder `.env` genügt normalerweise:
|
||
|
||
```bash
|
||
supervisorctl -c /etc/supervisor/supervisord.conf restart inbound-email
|
||
```
|
||
|
||
Falls Supervisor nicht verfügbar ist, kann die Anwendung alternativ direkt als
|
||
systemd-Unit `smtpwebhook.service` betrieben werden. Eine gehärtete Beispiel-
|
||
Unit mit `EnvironmentFile`, `User=smtpwebhook`, `Restart=on-failure`,
|
||
`ProtectSystem=strict` und persistentem Spool ist in [AGENTS.md](AGENTS.md)
|
||
dokumentiert. Supervisor und die systemd-App-Unit dürfen nicht gleichzeitig
|
||
dieselbe Instanz starten.
|
||
|
||
## Spool und Zustellung
|
||
|
||
Die Verzeichnisse haben folgende Bedeutung:
|
||
|
||
| Pfad | Zweck |
|
||
| --- | --- |
|
||
| `SPOOL_DIR/pending` | Noch nicht erfolgreich zugestellte Nachrichten |
|
||
| `SPOOL_DIR/failed` | Ungültige Spool-Dateien zur manuellen Prüfung |
|
||
| `LOG_DIR` | Aktuelle JSON-Anwendungslogs |
|
||
|
||
Der Spool wird beim Start und etwa jede Minute erneut eingelesen. Bei einem
|
||
Webhook-Fehler bleibt die Nachricht in `pending`; sie wird nicht verworfen.
|
||
Unlesbare Spool-Dateien werden nach `failed` verschoben. Die JSON-Datei wird
|
||
erst nach erfolgreicher Webhook-Antwort gelöscht.
|
||
|
||
Die Zustellung ist „at least once“. Der Webhook muss doppelte Zustellungen
|
||
vertragen können, insbesondere bei einem Absturz zwischen erfolgreicher
|
||
Webhook-Annahme und dem Löschen der Spool-Datei.
|
||
|
||
## Einsatzbeispiele
|
||
|
||
- E-Mail-to-Ticket-Systeme
|
||
- Dokumentenverarbeitung mit optionaler S3-Ablage
|
||
- Automatisierte Reports und Benachrichtigungen
|
||
- DMARC- oder andere maschinell erzeugte Reports
|
||
|
||
## Sicherheit und bekannte Grenzen
|
||
|
||
- `.env`, Zugangsdaten, private TLS-Schlüssel, Logs und Spool-Dateien niemals
|
||
committen.
|
||
- `.env` produktiv restriktiv schützen, z. B. Besitzer `root`, Dienstgruppe als
|
||
Gruppe und Modus `640`.
|
||
- TLS-Schlüssel außerhalb des Repositories aufbewahren.
|
||
- SMTP AUTH ist deaktiviert. Bei leerer `SMTP_ALLOWED_IPS` ist der Listener für
|
||
alle erreichbaren IPs offen; Firewall oder Allowlist bewusst konfigurieren.
|
||
- Keine zweite Prozessverwaltung wie PM2 parallel zu Supervisor/systemd
|
||
einsetzen.
|
||
- Rate-Limiting ist derzeit nicht implementiert.
|
||
|
||
## Logging und Fehleranalyse
|
||
|
||
`server.js` schreibt strukturierte JSON-Logs nach
|
||
`/var/lib/smtpwebhook/logs/application-YYYY-MM-DD.log`. Die Logs rotieren täglich
|
||
und ab 20 MiB; die Aufbewahrung beträgt ungefähr 90 Tage. Supervisor führt
|
||
zusätzlich `/var/log/inbound-email.out.log` und
|
||
`/var/log/inbound-email.err.log`.
|
||
|
||
Die wichtigsten Checks:
|
||
|
||
```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
|
||
find /var/lib/smtpwebhook/spool -maxdepth 2 -type f -ls
|
||
```
|
||
|
||
`EADDRINUSE` bedeutet normalerweise, dass eine zweite oder verwaiste Instanz
|
||
den Port belegt. Nicht einfach eine weitere Instanz starten, sondern zuerst den
|
||
Portbesitzer identifizieren. Die alten Logs unter `/opt/inbound-email/logs`
|
||
sind historische Daten und nicht der aktuelle Spool.
|
||
|
||
## Entwicklung und Tests
|
||
|
||
Abhängigkeiten reproduzierbar installieren:
|
||
|
||
```bash
|
||
npm ci
|
||
```
|
||
|
||
Syntaxchecks:
|
||
|
||
```bash
|
||
node --check server.js
|
||
node --check config.js
|
||
git diff --check
|
||
```
|
||
|
||
Es gibt derzeit keine automatisierten Testfälle. Ein SMTP-EHLO/QUIT-Handshake
|
||
gegen einen lokalen Testport prüft nur Listener und Protokollannahme; S3 und
|
||
Webhook werden dadurch nicht getestet. Keine Testmail an ein produktives
|
||
Webhook-Ziel senden.
|
||
|
||
Das vorhandene `DockerFile` klont noch das öffentliche GitHub-Original und
|
||
bildet die lokale gehärtete Installation nicht automatisch ab. Vor einem
|
||
produktiven Docker-Einsatz Clone-URL, Branch, Spool-Persistenz und Logging
|
||
anpassen bzw. prüfen.
|
||
|
||
## Git und weitere Dokumentation
|
||
|
||
`origin` zeigt auf das private Repository
|
||
`https://git.hacker.schule/mherrlein/inbound-email`; `upstream` zeigt auf das
|
||
ursprüngliche GitHub-Projekt [`kriiv/inbound-email`](https://github.com/kriiv/inbound-email).
|
||
Keine History per Force-Push überschreiben.
|
||
|
||
Die detaillierte Referenz für Codex-Instanzen und die vollständige systemd-
|
||
Beispiel-Unit steht in [AGENTS.md](AGENTS.md).
|
||
|
||
## Lizenz
|
||
|
||
Dieses Projekt steht unter der MIT-Lizenz. Siehe [LICENSE](LICENSE).
|