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üsselwortgewichtEinzelnes 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 auf evil.com) — hohe Schwere
  • Typosquatting bekannter Marken (paypa1.net, g00gle.io) und verdächtige TLDs (.xyz, .top, .club usw.) — 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. lateinisches a U+0061, griechisches ο U+03BF vs. lateinisches o usw.)
  • 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/reset in Kombination mit password/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:

SchwerePunkteBeispiel
Hoch20Homoglyphen-Spoofing, Credential-Phishing, hoch gewichtete Spam-Schlüsselwörter
Mittel10URL-Kürzer, mittelgewichtete Spam-Schlüsselwörter, Summe kombinierter schwacher Muster
Niedrig3Übermäßige Interpunktion, Betreff in GROSSBUCHSTABEN

Schwellenwerte:

PunktzahlStufeAktion
0–14SauberErlaubt
15–39VerdächtigErlaubt, Warnung wird gespeichert
40+BlockiertVersand 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:

DateitypMagic Bytes
PE-Executable (.exe, .dll)4D 5A (MZ) — gefährlich, blockiert
ELF-Binary7F 45 4C 46gefä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-InstallerD0 CF 11 E0 (OLE Compound) — gefährlich, blockiert
ISO-9660-Datenträgerabbild43 44 30 30 31 (CD001) an Offset 0x8001gefährlich, blockiert
PDF25 50 44 46 (%PDF)
PNG89 50 4E 47
JPEGFF D8 FF
ZIP/DOCX/XLSX50 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 blockiert
  • report.docx.js — erkannt und blockiert
  • photo.jpg.scr — erkannt und blockiert

Erlaubte Dateiendungen

Zulässige Dateitypen (alles andere wird blockiert):

KategorieEndungen
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 Versand
  • mediaAssets.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

  1. Alle URLs aus dem HTML-Inhalt der E-Mail extrahieren
  2. URLs normalisieren und hashen (SHA-256)
  3. Cache (Tabelle urlReputationCache) auf bekannte Verdikte prüfen
  4. Nicht gecachte URLs im Batch gegen die Safe Browsing API prüfen (bis zu 500 pro Anfrage)
  5. Ergebnisse cachen (24 h für sauber, 1 h für geflaggt)
  6. Geflaggte URLs in ContentFlag-Einträge umwandeln — malicious-Verdikte werden auf die Schwere hoch, suspicious-Verdikte auf mittel abgebildet (urlReputationToFlags)

Bedrohungstypen

BedrohungBeschreibung
MALWARESeiten, die Schadsoftware hosten
SOCIAL_ENGINEERINGPhishing- und Täuschungsseiten
UNWANTED_SOFTWARESeiten, die unerwünschte Software verbreiten
POTENTIALLY_HARMFUL_APPLICATIONBedrohungen durch mobile Apps

Kampagne vs. transaktional

E-Mail-TypVerhalten
KampagnenversandBlockierendes GatecheckUrlReputation läuft in campaigns/send.ts, und geflaggte URLs verhindern den Versand der Kampagne
Transaktionaler VersandWird 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:

  1. zINSTREAM\0 senden
  2. Binärdaten in Chunks senden (4-Byte-Längenpräfix in Big Endian + Daten)
  3. Terminator der Länge null senden
  4. Verdikt lesen: stream: OK\0 oder stream: <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:

WertVerhalten
clamav (Standard)createClamAvProvider() — kapselt den gepoolten clamd-TCP-Client
noopcreateNoopAntivirusProvider() — 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:

WertVerhalten
safe-browsing (Standard)createSafeBrowsingProvider() — Google Safe Browsing v4; ohne API-Schlüssel wird jede URL als safe zurückgegeben
noopcreateNoopUrlReputationProvider() — jede URL liefert safe
noop-Provider sind fail-open

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:

  1. Ein Zustell-Webhook liefert ein complaint-Event für einen Kampagnenversand
  2. Der Complaint-Reducer in apps/api/convex/delivery/sendLifecycle/feedbackReducers.ts gibt einen content_scan_complaint-Effekt aus, dessen Handler in apps/api/convex/delivery/sendLifecycle/effects.ts die contentScanResults-Zeile der Kampagne nachschlägt (gematcht über resourceType: 'campaign')
  3. An das flags-Array dieser Zeile wird ein suspicious_pattern-Flag geringer Schwere angehängt — Beschreibung "Spam complaint received (feedback loop)" —, wobei die beschwerdeführende Adresse im Feld match des Flags festgehalten wird
  4. 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

ScannerAufrufortBlockierend?Bei Fehlschlag
Inhalt (Spam + Homoglyphen)campaigns/send.ts, transactional/lifecycle.tsJaEntfällt (reines TS, läuft immer)
Dateityp-Validierungdelivery/worker.ts, mediaAssets.tsJaBlockieren (sicherer Standard)
URL-Reputation (Safe Browsing)campaigns/send.tsKampagnen: ja, transaktional: neinErlauben, stillschweigend überspringen
ClamAV-Malware-Scan (ausgehend)delivery/worker.ts über MTA /scan/attachmentJaErlauben, Warnung protokollieren (fail-open)
ClamAV-Malware-Scan (eingehend)mail/delivery.ts (scanInboundAttachments) über MTA /scan/attachmentInfiziert → 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/)
ClamAV läuft nur unter Node.js

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

DateiZweck
packages/email-scanner/src/content/index.tsscanContent() — Hauptorchestrator der Inhaltsprüfung
packages/email-scanner/src/files/index.tsvalidateFile() — Orchestrator der Dateivalidierung
packages/email-scanner/src/urls/index.tscheckUrlReputation() — Orchestrator der URL-Reputation
packages/email-scanner/src/clamav/index.tscreateClamClient() — Factory des ClamAV-Clients
apps/api/convex/campaigns/send.tsInhaltsprüfung + URL-Reputation im Kampagnenversand (importiert scanContent/checkUrlReputation direkt aus @owlat/email-scanner)
apps/api/convex/transactional/lifecycle.tsInhaltsprüfung beim transaktionalen Übergang nach published (nur scanContent)
apps/api/convex/delivery/worker.tsAnhangsvalidierung + ClamAV-Scan vor dem Versand
apps/api/convex/mail/delivery.tsClamAV-Scan eingehender (Postbox-)Anhänge bei empfangener Mail (scanInboundAttachments)
apps/api/convex/mediaAssets.tsDateivalidierung beim Upload (Endung + MIME-Typ)
apps/api/convex/schema/delivery.tsTabellendefinitionen für urlReputationCache + contentScanResults
apps/api/convex/delivery/sendLifecycle/feedbackReducers.tsBeschwerde-Feedback-Loop (gibt den content_scan_complaint-Effekt aus)
apps/api/convex/delivery/sendLifecycle/effects.tsBeschwerde-Feedback-Loop (Handler des content_scan_complaint-Effekts)
apps/mta/src/routes/scan.tsMTA-Endpunkte /scan/attachment und /scan/health
apps/mta/docker-compose.ymlKonfiguration des ClamAV-Sidecars