E-Mail-Sicherheit
Inhaltsprüfung, Anhangsvalidierung, Prüfung der URL-Reputation und Malware-Erkennung für ausgehende E-Mails.
Sämtliche ausgehende E-Mail durchläuft vor der Zustellung eine mehrschichtige Sicherheitsprüfung. Das Paket @owlat/email-scanner stellt die gesamte Scan-Logik als gemeinsame Bibliothek bereit, die sowohl von apps/api (Convex) als auch von apps/mta genutzt wird.
Inhaltsprüfung
Analysiert Betreff und HTML-Body einer E-Mail auf bösartige oder unerwünschte Inhalte. Alle Scanner sind reines TypeScript ohne Abhängigkeiten und damit sicher für die serverlose Convex-Laufzeitumgebung.
Spam-Schlüsselwörter
Rund 40 gewichtete Muster (packages/email-scanner/src/content/spamKeywords.ts) erkennen gängige Spam-Formulierungen. Jedes Schlüsselwort trägt ein internes Gewicht, erzeugt aber nur dann ein eigenes Flag, wenn dieses Gewicht 3 oder höher ist — hohe Schwere bei einem Gewicht ab 5, sonst mittlere. Leichtere Schlüsselwörter (Gewicht 1–2) werden nie einzeln geflaggt; sie summieren sich lediglich zu einem Gesamtwert auf.
| Schlüsselwortgewicht | Einzelnes Flag |
|---|---|
| 5+ (z. B. "free money", "congratulations you won", "viagra") | Hoch |
| 3–4 (z. B. "large money claim", "verify your account", "online gambling") | Mittel |
| 1–2 (z. B. "act now", "limited time", "click here", "buy now") | Nicht einzeln geflaggt |
Wurde kein einzelnes Schlüsselwort geflaggt, erreicht aber das kombinierte Gewicht aller Treffer 8 oder mehr, wird ein einzelnes Flag mittlerer Schwere ausgegeben, das auf die Erkennung mehrerer Spam-Muster geringer Schwere hinweist.
Erkennung von Phishing-URLs
- Erkennung von URL-Kürzern (bit.ly, t.co, goo.gl usw.) — mittlere Schwere
- Abweichung zwischen Anchor- und href-Domain (der Linktext lautet
paypal.com, verlinkt wird aber aufevil.com) — hohe Schwere - Typosquatting bekannter Marken (
paypa1.net,g00gle.io) und verdächtige TLDs (.xyz,.top,.clubusw.) — hohe Schwere - Gefährliche URI-Schemata (
data:,javascript:) — hohe Schwere
Homoglyphen- / Unicode-Spoofing
Erkennt Zeichen aus gemischten Schriftsystemen, die zur Nachahmung legitimer Domains verwendet werden:
- Rund 50 Zuordnungen verwechselbarer Zeichen (kyrillisches
аU+0430 vs. lateinischesaU+0061, griechischesοU+03BF vs. lateinischesousw.) - Erkennung gemischter Schriftsysteme in Linktexten und URL-Hostnamen
- Schwere: hoch (20 Punkte) — Homoglyphen-Spoofing ist ein starker Phishing-Indikator
Verbotene Inhalte
Mustererkennung für Betrugsinhalte hoher Schwere (jeder Treffer hat die Schwere hoch):
- Muster des Vorschussbetrugs (
nigerian prince,advance fee,419 scam) - Dringende Aufforderungen zu Geldtransfers (
wire transfer/western union/moneygram+immediately/urgently/today) - Anfragen nach sensiblen persönlichen Daten (
social security-/ssn-/credit card-Nummern) - Credential-Phishing (
confirm/verify/update/resetin Kombination mitpassword/login/credential)
Analyse der Betreffzeile
- Missbrauch von GROSSSCHREIBUNG (>50 % Großbuchstaben)
- Übermäßige Interpunktion (3 oder mehr
!-Zeichen bzw. 3 oder mehr?-Zeichen im Betreff)
Bewertung
Alle Flags fließen in einen Gesamtwert ein:
| Schwere | Punkte | Beispiel |
|---|---|---|
| Hoch | 20 | Homoglyphen-Spoofing, Credential-Phishing, hoch gewichtete Spam-Schlüsselwörter |
| Mittel | 10 | URL-Kürzer, mittelgewichtete Spam-Schlüsselwörter, Summe kombinierter schwacher Muster |
| Niedrig | 3 | Übermäßige Interpunktion, Betreff in GROSSBUCHSTABEN |
Schwellenwerte:
| Punktzahl | Stufe | Aktion |
|---|---|---|
| 0–14 | Sauber | Erlaubt |
| 15–39 | Verdächtig | Erlaubt, Warnung wird gespeichert |
| 40+ | Blockiert | Versand abgelehnt |
Dateivalidierung
Validiert Anhänge und Medien-Uploads vor Speicherung oder Versand. Reines TypeScript, keine externen Abhängigkeiten.
Erkennung über Magic Bytes
Ermittelt den tatsächlichen Dateityp aus dem Binärheader (erste 16 Bytes), unabhängig von der Dateiendung:
| Dateityp | Magic Bytes |
|---|---|
| PE-Executable (.exe, .dll) | 4D 5A (MZ) — gefährlich, blockiert |
| ELF-Binary | 7F 45 4C 46 — gefährlich, blockiert |
| Mach-O-Executable (macOS) | FE ED FA CE / FE ED FA CF / CF FA ED FE / CA FE BA BE (32 Bit, 64 Bit, 64 Bit LE, universal) — gefährlich, blockiert |
| MSI-Installer | D0 CF 11 E0 (OLE Compound) — gefährlich, blockiert |
| ISO-9660-Datenträgerabbild | 43 44 30 30 31 (CD001) an Offset 0x8001 — gefährlich, blockiert |
25 50 44 46 (%PDF) | |
| PNG | 89 50 4E 47 |
| JPEG | FF D8 FF |
| ZIP/DOCX/XLSX | 50 4B 03 04 (PK) |
Der Volume Descriptor von ISO 9660 liegt jenseits des Fensters der ersten Bytes und wird daher über eine optionale Tiefenprüfung erfasst: Aufrufer übergeben die fünf Bytes an Offset 0x8001 als Argument isoProbe an detectFileType / validateFile, um umbenannte Datenträgerabbilder zu flaggen.
Erkennung doppelter Dateiendungen
Fängt Angriffe ab, die ausführbare Endungen hinter Dokumentendungen verstecken:
invoice.pdf.exe— erkannt und blockiertreport.docx.js— erkannt und blockiertphoto.jpg.scr— erkannt und blockiert
Erlaubte Dateiendungen
Zulässige Dateitypen (alles andere wird blockiert):
| Kategorie | Endungen |
|---|---|
| Bilder | .jpg, .jpeg, .png, .gif, .webp, .ico, .bmp, .tiff, .tif |
| Dokumente | .pdf, .doc, .docx, .odt, .rtf, .txt, .html, .htm |
| Tabellen | .xls, .xlsx, .csv, .ods |
| Präsentationen | .ppt, .pptx, .odp |
| Archive | .zip, .gz, .tar, .tgz |
| Audio/Video | .mp3, .wav, .ogg, .mp4, .webm |
| Daten | .json, .xml, .yaml, .yml |
| Schriftarten | .woff, .woff2, .ttf, .otf |
| Kalender | .ics, .ical |
| vCard | .vcf |
SVG ist standardmäßig bewusst ausgeschlossen, weil es skriptfähig ist und Code ausführen kann, wenn ein Empfänger den Anhang direkt öffnet; pro Aufrufstelle lässt es sich über mergePolicy wieder zulassen.
Erlaubte MIME-Typen
Validiert deklarierte Content-Types gegen eine explizit aufgezählte Positivliste sicherer MIME-Typen (z. B. image/jpeg, image/png, application/pdf, text/plain). Die Liste enthält keine Platzhalter — image/svg+xml etwa ist ausgeschlossen.
Integrationspunkte
delivery/worker.ts— validiert jeden Anhangs-Buffer vor dem VersandmediaAssets.ts— validiert Uploads vor der Speicherung im Convex-Dateispeicher
URL-Reputation
Prüft URLs im E-Mail-Inhalt gegen die Google Safe Browsing API v4.
Funktionsweise
- Alle URLs aus dem HTML-Inhalt der E-Mail extrahieren
- URLs normalisieren und hashen (SHA-256)
- Cache (Tabelle
urlReputationCache) auf bekannte Verdikte prüfen - Nicht gecachte URLs im Batch gegen die Safe Browsing API prüfen (bis zu 500 pro Anfrage)
- Ergebnisse cachen (24 h für sauber, 1 h für geflaggt)
- Geflaggte URLs in
ContentFlag-Einträge umwandeln —malicious-Verdikte werden auf die Schwere hoch,suspicious-Verdikte auf mittel abgebildet (urlReputationToFlags)
Bedrohungstypen
| Bedrohung | Beschreibung |
|---|---|
MALWARE | Seiten, die Schadsoftware hosten |
SOCIAL_ENGINEERING | Phishing- und Täuschungsseiten |
UNWANTED_SOFTWARE | Seiten, die unerwünschte Software verbreiten |
POTENTIALLY_HARMFUL_APPLICATION | Bedrohungen durch mobile Apps |
Kampagne vs. transaktional
| E-Mail-Typ | Verhalten |
|---|---|
| Kampagnenversand | Blockierendes Gate — checkUrlReputation läuft in campaigns/send.ts, und geflaggte URLs verhindern den Versand der Kampagne |
| Transaktionaler Versand | Wird nicht ausgeführt — der transaktionale Reducer in transactional/lifecycle.ts führt nur scanContent aus und ruft checkUrlReputation nie auf |
Konfiguration
Erfordert die im Convex-Dashboard gesetzte Umgebungsvariable GOOGLE_SAFE_BROWSING_API_KEY. Die kostenlose Stufe erlaubt 10.000 Anfragen pro Tag. Ist kein API-Schlüssel konfiguriert, wird die Prüfung der URL-Reputation stillschweigend übersprungen.
ClamAV-Malware-Scan
Prüft die Binärdaten von Anhängen mit ClamAV auf bekannte Malware-Signaturen; ClamAV läuft als Docker-Sidecar neben dem MTA.
Architektur
Convex delivery/worker.ts
│
│ POST /scan/attachment
│ (binary data + X-Filename header)
▼
MTA scan endpoint (src/routes/scan.ts)
│
├─ File type validation (magic bytes, extensions)
│
└─ ClamAV scan (TCP INSTREAM protocol)
│
▼
clamd (port 3310)
INSTREAM-Protokoll
Der ClamAV-Client von @owlat/email-scanner kommuniziert über TCP mit clamd und verwendet dabei das INSTREAM-Protokoll:
zINSTREAM\0senden- Binärdaten in Chunks senden (4-Byte-Längenpräfix in Big Endian + Daten)
- Terminator der Länge null senden
- Verdikt lesen:
stream: OK\0oderstream: <virus_name> FOUND\0
Fail-Open-Design
Ist ClamAV nicht verfügbar (Container läuft nicht, Netzwerkfehler, Timeout), liefert der Scan { clean: true, skipped: true } und protokolliert eine Warnung. Das verhindert, dass ClamAV-Ausfälle die gesamte E-Mail-Zustellung blockieren.
Health Check
GET /scan/health (apps/mta/src/routes/scan.ts) liefert den Status des ClamAV-Clients und die Warteschlangentiefe:
{
"clamav": {
"healthy": true,
"pingOk": true,
"activeScanCount": 0,
"pendingCount": 0
}
}
healthy gibt den zuletzt bekannten Zustand des gepoolten Clients wieder, pingOk ist ein live durchgeführter PING-Umlauf, und die beiden Zähler legen die laufende und die eingereihte Arbeit des Connection Pools offen.
Scan eingehender Anhänge
ClamAV wirkt nicht nur ausgehend. Auch jede empfangene Postbox-Nachricht wird als Defense-in-Depth geprüft: scanInboundAttachments (apps/api/convex/mail/delivery.ts) extrahiert jedes nicht eingebettete Anhangs-Blatt aus dem rohen MIME und POSTet es an denselben MTA-Endpunkt /scan/attachment. Ein bestätigter Infektionsbefund setzt virusVerdict: 'infected' und routet die Nachricht nach Spam/Quarantäne; ein Ausfall des Scanners führt zu Fail-Open mit virusVerdict: 'skipped' (die Nachricht wird dennoch zugestellt und über scannerHealth.warnScanSkipped sichtbar gemacht). Die Zahl der Scans pro Blatt ist gedeckelt (passend zum Anhangslimit beim Verfassen), damit eine präparierte .eml die Scan-Kosten nicht aufblähen kann.
Konfiguration
Docker-Einrichtung und Umgebungsvariablen finden Sie unter MTA-System > ClamAV-Sidecar.
Erweiterbarkeit
Der Scanner ist um drei austauschbare Nahtstellen herum gebaut, sodass Sie Regeln ergänzen oder Backends wechseln können, ohne das Paket zu forken.
Registry für Inhaltsregeln
scanContent() ruft die einzelnen Scanner nicht mehr fest verdrahtet auf. Stattdessen verarbeitet es die E-Mail einmal vor (HTML entfernen, URLs extrahieren) zu einem ScanInput und iteriert über jede Regel, die in der Registry contentRules (packages/email-scanner/src/content/rule.ts) installiert ist. Die eingebauten Regeln — spam-keywords, phishing-urls, homoglyphs, caps-abuse, excessive-punctuation, prohibited-content — registrieren sich beim Laden des Moduls in dieser Reihenfolge, die zugleich die Iterationsreihenfolge der Flags ist.
Eine ContentScanRule ist eine reine, synchrone Funktion mit stabiler ID:
import { registerContentRule, type ScanInput } from '@owlat/email-scanner';
registerContentRule({
id: 'block-competitor-domains',
scan(input: ScanInput) {
const hit = input.urls.find((u) => u.href.includes('competitor.example'));
return hit
? [{ type: 'suspicious_pattern', severity: 'medium', description: 'Competitor link', match: hit.href }]
: [];
},
});
Mit unregisterContentRule(id) entfernen Sie eine Regel (liefert true, wenn eine Regel entfernt wurde). Beide Helfer werden aus dem Paket-Einstiegspunkt exportiert. Eine Regel, die eine Exception wirft, bricht den Scan nicht ab — scanContent fängt sie ab, überspringt diese Regel und hängt ein Flag geringer Schwere an, das die ID der fehlerhaften Regel benennt, damit ein fehlverhaltendes Plugin nicht stillschweigend legitime Flags gesunder Regeln unterdrücken kann.
Antivirus-Provider
packages/email-scanner/src/clamav/provider.ts definiert eine AntivirusProvider-Fassade (scan, ping, getProviderName) über dem bestehenden ClamAV-Client. getAntivirusProvider() liest die Umgebungsvariable ANTIVIRUS_PROVIDER:
| Wert | Verhalten |
|---|---|
clamav (Standard) | createClamAvProvider() — kapselt den gepoolten clamd-TCP-Client |
noop | createNoopAntivirusProvider() — jeder Scan liefert { clean: true, skipped: true } |
Provider für URL-Reputation
packages/email-scanner/src/urls/provider.ts definiert eine UrlReputationProvider-Fassade (check, getProviderName). getUrlReputationProvider() liest die Umgebungsvariable URL_REPUTATION_PROVIDER:
| Wert | Verhalten |
|---|---|
safe-browsing (Standard) | createSafeBrowsingProvider() — Google Safe Browsing v4; ohne API-Schlüssel wird jede URL als safe zurückgegeben |
noop | createNoopUrlReputationProvider() — jede URL liefert safe |
Die noop-Provider für Antivirus und URL-Reputation existieren für lokale Entwicklung und Tests, wo der Betrieb von clamd oder das Vorhalten eines Safe-Browsing-Schlüssels unpraktisch ist. Sie melden immer „sauber“, wählen Sie sie daher niemals in der Produktion. Die Provider-Factories sind kein Teil der öffentlichen Exportfläche des Pakets — das Barrel (@owlat/email-scanner) re-exportiert die Helfer für Inhaltsregeln (registerContentRule/unregisterContentRule/contentRules), nicht jedoch die Provider-Factories, und die Subpath-Einstiegspunkte ./clamav und ./urls re-exportieren nur die zugrunde liegenden Clients/Utilities. Importieren Sie die Provider über relative Pfade aus den Modulen clamav/provider.ts und urls/provider.ts (so wie es die paketeigenen Tests tun).
Feedback Loops
Spam-Beschwerden aus Provider-Feedback-Loops werden auf die Ergebnisse der Inhaltsprüfung der Kampagne zurückgeführt:
- Ein Zustell-Webhook liefert ein
complaint-Event für einen Kampagnenversand - Der Complaint-Reducer in
apps/api/convex/delivery/sendLifecycle/feedbackReducers.tsgibt einencontent_scan_complaint-Effekt aus, dessen Handler inapps/api/convex/delivery/sendLifecycle/effects.tsdiecontentScanResults-Zeile der Kampagne nachschlägt (gematcht überresourceType: 'campaign') - An das
flags-Array dieser Zeile wird einsuspicious_pattern-Flag geringer Schwere angehängt — Beschreibung"Spam complaint received (feedback loop)"—, wobei die beschwerdeführende Adresse im Feldmatchdes Flags festgehalten wird - Weil die Verknüpfung auf
resourceType: 'campaign'beruht, erhalten nur Inhaltsprüfungsergebnisse von Kampagnen Beschwerde-Flags; transaktionale Scans werden auf diese Weise nicht annotiert
Integrationsübersicht
| Scanner | Aufrufort | Blockierend? | Bei Fehlschlag |
|---|---|---|---|
| Inhalt (Spam + Homoglyphen) | campaigns/send.ts, transactional/lifecycle.ts | Ja | Entfällt (reines TS, läuft immer) |
| Dateityp-Validierung | delivery/worker.ts, mediaAssets.ts | Ja | Blockieren (sicherer Standard) |
| URL-Reputation (Safe Browsing) | campaigns/send.ts | Kampagnen: ja, transaktional: nein | Erlauben, stillschweigend überspringen |
| ClamAV-Malware-Scan (ausgehend) | delivery/worker.ts über MTA /scan/attachment | Ja | Erlauben, Warnung protokollieren (fail-open) |
| ClamAV-Malware-Scan (eingehend) | mail/delivery.ts (scanInboundAttachments) über MTA /scan/attachment | Infiziert → nach Spam geroutet | Überspringen + warnen (fail-open) |
Paketstruktur
packages/email-scanner/src/
├── content/ # Content analysis (pure TS, Convex-safe)
│ ├── index.ts # scanContent() — iterates the rule registry
│ ├── rule.ts # contentRules registry + ScanInput/ContentScanRule types
│ ├── spamKeywords.ts # ~40 weighted spam patterns
│ ├── phishingUrls.ts # URL shorteners, anchor/href mismatch
│ ├── homoglyphs.ts # Unicode spoofing detection
│ ├── prohibitedContent.ts # Advance fee fraud, credential phishing
│ └── subjectAnalysis.ts # ALL CAPS, excessive punctuation
├── files/ # File type validation (pure TS)
│ ├── index.ts # validateFile() orchestrator
│ ├── magicBytes.ts # Binary header detection
│ ├── doubleExtension.ts # invoice.pdf.exe detection
│ └── filePolicy.ts # Allowlist/blocklist engine
├── urls/ # URL reputation (uses fetch)
│ ├── index.ts # checkUrlReputation() orchestrator
│ ├── provider.ts # UrlReputationProvider abstraction (URL_REPUTATION_PROVIDER)
│ ├── safeBrowsing.ts # Google Safe Browsing API v4 client
│ └── cache.ts # Abstract cache interface
├── clamav/ # ClamAV TCP client (Node.js net, MTA only)
│ ├── index.ts # createClamClient() factory
│ ├── provider.ts # AntivirusProvider abstraction (ANTIVIRUS_PROVIDER)
│ ├── client.ts # clamd INSTREAM protocol implementation
│ └── pool.ts # Connection pooling
├── types.ts # Shared types (ContentFlag, ScanResult, etc.)
└── index.ts # Barrel export (excludes clamav/)
Das Modul clamav/ nutzt Node.js net für TCP-Verbindungen und ist nur aus dem MTA heraus importierbar. Die Module content/, files/ und urls/ sind reines TS und funktionieren sowohl in Convex- als auch in Node.js-Umgebungen.
Wichtige Dateien
| Datei | Zweck |
|---|---|
packages/email-scanner/src/content/index.ts | scanContent() — Hauptorchestrator der Inhaltsprüfung |
packages/email-scanner/src/files/index.ts | validateFile() — Orchestrator der Dateivalidierung |
packages/email-scanner/src/urls/index.ts | checkUrlReputation() — Orchestrator der URL-Reputation |
packages/email-scanner/src/clamav/index.ts | createClamClient() — Factory des ClamAV-Clients |
apps/api/convex/campaigns/send.ts | Inhaltsprüfung + URL-Reputation im Kampagnenversand (importiert scanContent/checkUrlReputation direkt aus @owlat/email-scanner) |
apps/api/convex/transactional/lifecycle.ts | Inhaltsprüfung beim transaktionalen Übergang nach published (nur scanContent) |
apps/api/convex/delivery/worker.ts | Anhangsvalidierung + ClamAV-Scan vor dem Versand |
apps/api/convex/mail/delivery.ts | ClamAV-Scan eingehender (Postbox-)Anhänge bei empfangener Mail (scanInboundAttachments) |
apps/api/convex/mediaAssets.ts | Dateivalidierung beim Upload (Endung + MIME-Typ) |
apps/api/convex/schema/delivery.ts | Tabellendefinitionen für urlReputationCache + contentScanResults |
apps/api/convex/delivery/sendLifecycle/feedbackReducers.ts | Beschwerde-Feedback-Loop (gibt den content_scan_complaint-Effekt aus) |
apps/api/convex/delivery/sendLifecycle/effects.ts | Beschwerde-Feedback-Loop (Handler des content_scan_complaint-Effekts) |
apps/mta/src/routes/scan.ts | MTA-Endpunkte /scan/attachment und /scan/health |
apps/mta/docker-compose.yml | Konfiguration des ClamAV-Sidecars |