
Seit einiger Zeit nutze ich das GPT-Modul von Rspamd, um bei der Spam-Erkennung ein zusätzliches Signal zu bekommen. Es ersetzt nichts — kein Bayes, kein DKIM, kein RBL — sondern ist ein weiterer Sensor im Gesamtbild. Wer sich fragt, wie das in der Praxis aussieht und worauf man achten muss: hier mein aktuelles Setup.
Update 2026-02-13: Dieser Beitrag wurde komplett überarbeitet. Die ursprüngliche Version nutzte json=false, was zu Parse-Problemen führte. Außerdem fehlte ein Custom Prompt — und genau das ist der entscheidende Punkt, wie sich herausgestellt hat.
Voraussetzungen
- Rspamd >= 3.12 mit GPT-Plugin (bei mir aktuell 3.14.0 auf FreeBSD 15.0)
- Ein OpenAI API-Key (oder kompatibler Endpoint)
- Grundverständnis von Rspamd Metrics und Actions
OpenAI API-Key anlegen
Wer noch keinen Key hat: Auf platform.openai.com einloggen, unter API Keys einen neuen Service-Account-Key erzeugen. Der Key wird nur einmal angezeigt — sicher ablegen. Den Verbrauch sieht man im Dashboard. Bei gpt-4o-mini und Mailfiltering sind die Kosten minimal.
Die Konfiguration: gpt.conf
Hier meine aktuelle /usr/local/etc/rspamd/local.d/gpt.conf:
enabled = true;
type = "openai";
model = "gpt-4o-mini";
api_key = "GEHEIMER-KEY";
model_parameters {
gpt-4o-mini {
max_tokens = 160;
temperature = 0.0;
}
}
timeout = 10s;
allow_ham = true;
allow_passthrough = false;
json = true;
prompt = "You are an email spam detector. Analyze the email and respond with ONLY a JSON object, no other text. The JSON must have these fields: "probability" (number 0.00-1.00 where 1.0=spam, 0.0=ham), "reason" (one sentence citing the strongest indicator). Example: {"probability": 0.85, "reason": "Unsolicited offer with urgent language and suspicious links."} LEGITIMATE patterns: verification emails with codes, transactional emails (receipts, confirmations), newsletter unsubscribe links. Flag as spam only with MULTIPLE red flags: urgent threats, domain impersonation, requests for credentials, mismatched URLs.";
symbols_to_except {
RCVD_IN_DNSWL_MED = -0.1;
RCVD_IN_DNSWL_HI = -0.1;
DWL_DNSWL_MED = -0.1;
WHITELIST_RECP_ADDR = -0.1;
BAYES_HAM = -0.1;
SPAMTRAP = 0;
RCPT_IN_SPAMTRAP = 0;
SPAMTRAP_ADDR = 0;
RCVD_VIA_SMTP_AUTH = 0;
LOCAL_CLIENT = 0;
FROM_LOCAL = 0;
}
Was hat sich gegenüber der alten Version geändert?
json = true und der Custom Prompt
Das ist die wichtigste Änderung. In meiner ursprünglichen Konfiguration stand json = false. Das funktionierte, hatte aber einen Haken: die Antwort des Modells wurde als Freitext geparst, was unzuverlässig war.
Mit json = true aktiviert Rspamd den JSON-Modus. Das Modell wird angewiesen, strukturiertes JSON zurückzuliefern, und der Parser erwartet ein Feld probability in der Antwort.
Und hier kommt der Fallstrick: Der Default-Prompt von Rspamd passt nicht zum JSON-Modus. Er fordert das Modell auf, nummerierte Textzeilen zurückzugeben:
Output ONLY 2 lines: 1. Numeric score: 0.00-1.00 2. One-sentence reason...
Der JSON-Parser erwartet aber:
{"probability": 0.85, "reason": "..."}
Das Ergebnis: cannot convert spam score im Log und GPT_UNCERTAIN(0.00) bei jeder Mail. Das GPT-Modul lief, lieferte aber nie ein verwertbares Ergebnis.
Lösung: ein Custom Prompt, der explizit JSON mit dem probability-Feld verlangt. Damit funktioniert die Kette:
- Rspamd sendet Mail + Prompt an OpenAI
- OpenAI antwortet mit
{"probability": 0.9, "reason": "..."} - Rspamd parst das JSON, findet
probability, mappt auf GPT_SPAM/GPT_HAM/GPT_SUSPICIOUS
reason_header entfernt
In der alten Version hatte ich reason_header = "X-GPT-Reason" gesetzt. Das schrieb die GPT-Begründung als eigenen Header in die Mail. Mit json = true ist das nicht mehr nötig — die Reason steckt im JSON und taucht im Rspamd-Log auf. Außerdem entferne ich ohnehin GPT-Header per Milter-Config, damit keine internen Analyse-Details an den Empfänger durchsickern.
symbols_to_except angepasst
Änderungen gegenüber der alten Version:
- GREYLIST entfernt: Greylisting ist kein Vertrauens-Signal. Eine Mail die Greylisting besteht, kann trotzdem Spam sein. GPT soll diese Mails weiterhin bewerten.
- BAYES_HAM hinzugefügt: Wenn Bayes die Mail bereits sicher als Ham einstuft, spart man sich den GPT-Call. Sinnvoll für Newsletter und regelmäßige Korrespondenz.
- SPAMTRAP-Symbole hinzugefügt: Mails an Spamtrap-Adressen brauchen keine GPT-Analyse, die sind per Definition Spam.
Scoring: Gewichte und Thresholds
Die GPT-Symbole und ihre Gewichte in der metrics.conf (bzw. local.d/groups.conf):
symbols {
GPT_SPAM { weight = 9.0; description = "GPT: classified as SPAM"; }
GPT_SUSPICIOUS { weight = 4.5; description = "GPT: classified as SUSPICIOUS"; }
GPT_HAM { weight = -0.5; one_shot = true; description = "GPT: classified as HAM"; }
}
Warum diese Gewichte?
- GPT_SPAM (9.0): Kräftig, aber alleine nicht genug zum Rejecten. Erst in Kombination mit anderen Signalen (Bayes, RBL, fehlende Auth) wird der Reject-Threshold erreicht.
- GPT_SUSPICIOUS (4.5): Schiebt Grenzfälle in Richtung Greylist oder Add-Header. Genau dafür ist GPT am nützlichsten.
- GPT_HAM (-0.5): Bewusst niedrig und
one_shot. GPT soll Spam erkennen, nicht Ham retten.
Dazu die Action-Thresholds:
actions {
greylist = 4;
add_header = 6;
reject = 12;
}
Reject-Threshold bei mir: 12 statt Default 15. Das geht, weil die traditionellen Checks (SPF, DKIM, DMARC, RBL, Bayes, DNSBL) bereits solide arbeiten. GPT kommt als zusätzliches Signal obendrauf.
Praxis-Beispiel
Hier eine echte Spam-Mail aus dem Log, bei der GPT korrekt angeschlagen hat:
rspamd_task_write_log: (default: T (reject): [13.83/12.00]
[BAYES_SPAM(5.10){100.00%;},
ABUSE_SURBL(5.00){next.schnapper-empfehlung.de:url;...},
GPT_SPAM(2.40){0.9;},
FROM_NEQ_ENVFROM(0.50){...},
FORGED_SENDER(0.30){...},
...]
Was man hier sieht:
GPT_SPAM(2.40){0.9;}— GPT hat Probability 0.9 (90% Spam) zurückgeliefert. Rspamd mappt den Probability-Wert nicht 1:1 auf das konfigurierte Gewicht, sondern skaliert intern — hier ergeben sich 2.40 von maximal 9.0 Punkten.- Zusammen mit BAYES_SPAM (5.10) und ABUSE_SURBL (5.00) kommt die Mail auf 13.83 — deutlich über dem Reject-Threshold von 12.
- GPT war hier nicht das ausschlaggebende Signal, hat aber zur Gesamtbewertung beigetragen.
Das ist genau das Verhalten, das ich will: GPT als ein Baustein unter vielen, der bei Grenzfällen den Ausschlag geben kann.
Datenschutz
Das muss gesagt werden: Mit diesem Setup fließen Mailinhalte an OpenAI. Wer personenbezogene Daten verarbeitet oder in einem regulierten Umfeld arbeitet, muss prüfen ob das zulässig ist. Alternative: selbst gehostete Modelle über Ollama oder kompatible lokale Endpoints. Rspamd unterstützt das über den type-Parameter.
Für meinen privaten Mailserver ist das Risiko vertretbar — und die Ergebnisse sprechen für sich.
Zusammenfassung
| Parameter | Wert | Warum |
|---|---|---|
json | true | Strukturiertes Parsing, zuverlässiger als Freitext |
prompt | Custom | Pflicht bei json=true! Default-Prompt liefert Textformat, Parser erwartet JSON |
temperature | 0.0 | Deterministische Antworten, kein Kreativitäts-Bonus beim Spamfiltern |
allow_ham | true | Kleines positives Signal für legitime Mails |
symbols_to_except | BAYES_HAM, DNSWL, Whitelists, SMTP_AUTH, Spamtraps | Unnötige API-Calls vermeiden |
reason_header | nicht gesetzt | Nicht nötig mit json=true, interne Details gehören nicht in den Header |
Die wichtigste Erkenntnis: json = true ohne Custom Prompt ist kaputt. Der Default-Prompt und der JSON-Parser sprechen unterschiedliche Sprachen. Wer json = true setzt, muss einen Prompt mitliefern, der JSON mit einem probability-Feld verlangt. Sonst steht im Log cannot convert spam score und GPT liefert nur GPT_UNCERTAIN(0.00).

