{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreicgvksa3tds4b46kgrasbzcx4j6e6v6cbmojrbdvr3bc5eeiwjola",
    "uri": "at://did:plc:nkxz2ojdvmieg2nhphvputvp/app.bsky.feed.post/3mmjdlmqe2hk2"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreicbpgbltbaq4gycyfpuowhowro6pnxqhib7tgumdztr6gr6qoeyqu"
    },
    "mimeType": "image/png",
    "size": 142591
  },
  "description": "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.\n\nUpdate 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 […]",
  "path": "/2025/09/30/gpt-in-rspamd-aktivieren/",
  "publishedAt": "2026-05-23T11:24:11.000Z",
  "site": "https://www.kernel-error.de",
  "tags": [
    "AI",
    "Email",
    "InfoSec",
    "MailServer",
    "RSPAMD",
    "SelfHosted",
    "SPAM",
    "platform.openai.com",
    "Ollama",
    "rspamd mit Dovecot und IMAPSieve",
    "Einfach melden."
  ],
  "textContent": "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.\n\n**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.\n\n### Voraussetzungen\n\n  * Rspamd >= 3.12 mit GPT-Plugin (bei mir aktuell 3.14.0 auf FreeBSD 15.0)\n  * Ein OpenAI API-Key (oder kompatibler Endpoint)\n  * Grundverständnis von Rspamd Metrics und Actions\n\n\n\n### OpenAI API-Key anlegen\n\nWer 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.\n\n### Die Konfiguration: gpt.conf\n\nHier meine aktuelle `/usr/local/etc/rspamd/local.d/gpt.conf`:\n\n\n    enabled = true;\n    type = \"openai\";\n    model = \"gpt-4o-mini\";\n    api_key = \"GEHEIMER-KEY\";\n\n    model_parameters {\n      gpt-4o-mini {\n        max_tokens = 160;\n        temperature = 0.0;\n      }\n    }\n\n    timeout = 10s;\n    allow_ham = true;\n    allow_passthrough = false;\n    json = true;\n\n    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.\";\n\n    symbols_to_except {\n      RCVD_IN_DNSWL_MED   = -0.1;\n      RCVD_IN_DNSWL_HI    = -0.1;\n      DWL_DNSWL_MED        = -0.1;\n      WHITELIST_RECP_ADDR = -0.1;\n      BAYES_HAM           = -0.1;\n      SPAMTRAP            = 0;\n      RCPT_IN_SPAMTRAP    = 0;\n      SPAMTRAP_ADDR       = 0;\n      RCVD_VIA_SMTP_AUTH  = 0;\n      LOCAL_CLIENT        = 0;\n      FROM_LOCAL          = 0;\n    }\n\n### Was hat sich gegenüber der alten Version geändert?\n\n### json = true und der Custom Prompt\n\nDas 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.\n\nMit `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.\n\n**Und hier kommt der Fallstrick:** Der Default-Prompt von Rspamd passt nicht zum JSON-Modus. Er fordert das Modell auf, nummerierte Textzeilen zurückzugeben:\n\n\n    Output ONLY 2 lines:\n    1. Numeric score: 0.00-1.00\n    2. One-sentence reason...\n\nDer JSON-Parser erwartet aber:\n\n\n    {\"probability\": 0.85, \"reason\": \"...\"}\n\nDas 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.\n\nLösung: ein **Custom Prompt** , der explizit JSON mit dem `probability`-Feld verlangt. Damit funktioniert die Kette:\n\n  1. Rspamd sendet Mail + Prompt an OpenAI\n  2. OpenAI antwortet mit `{\"probability\": 0.9, \"reason\": \"...\"}`\n  3. Rspamd parst das JSON, findet `probability`, mappt auf GPT_SPAM/GPT_HAM/GPT_SUSPICIOUS\n\n\n\n### reason_header entfernt\n\nIn 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.\n\n### symbols_to_except angepasst\n\nÄnderungen gegenüber der alten Version:\n\n  * **GREYLIST entfernt:** Greylisting ist kein Vertrauens-Signal. Eine Mail die Greylisting besteht, kann trotzdem Spam sein. GPT soll diese Mails weiterhin bewerten.\n  * **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.\n  * **SPAMTRAP-Symbole hinzugefügt:** Mails an Spamtrap-Adressen brauchen keine GPT-Analyse, die sind per Definition Spam.\n\n\n\n### Scoring: Gewichte und Thresholds\n\nDie GPT-Symbole und ihre Gewichte in der `metrics.conf` (bzw. `local.d/groups.conf`):\n\n\n    symbols {\n      GPT_SPAM       { weight = 9.0;  description = \"GPT: classified as SPAM\"; }\n      GPT_SUSPICIOUS { weight = 4.5;  description = \"GPT: classified as SUSPICIOUS\"; }\n      GPT_HAM        { weight = -0.5; one_shot = true; description = \"GPT: classified as HAM\"; }\n    }\n\n**Warum diese Gewichte?**\n\n  * **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.\n  * **GPT_SUSPICIOUS (4.5):** Schiebt Grenzfälle in Richtung Greylist oder Add-Header. Genau dafür ist GPT am nützlichsten.\n  * **GPT_HAM (-0.5):** Bewusst niedrig und `one_shot`. GPT soll Spam erkennen, nicht Ham retten.\n\n\n\nDazu die Action-Thresholds:\n\n\n    actions {\n      greylist   = 4;\n      add_header = 6;\n      reject     = 12;\n    }\n\nReject-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.\n\n### Praxis-Beispiel\n\nHier eine echte Spam-Mail aus dem Log, bei der GPT korrekt angeschlagen hat:\n\n\n    rspamd_task_write_log: (default: T (reject): [13.83/12.00]\n      [BAYES_SPAM(5.10){100.00%;},\n       ABUSE_SURBL(5.00){next.schnapper-empfehlung.de:url;...},\n       GPT_SPAM(2.40){0.9;},\n       FROM_NEQ_ENVFROM(0.50){...},\n       FORGED_SENDER(0.30){...},\n       ...]\n\nWas man hier sieht:\n\n  * `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.\n  * Zusammen mit BAYES_SPAM (5.10) und ABUSE_SURBL (5.00) kommt die Mail auf 13.83 — deutlich über dem Reject-Threshold von 12.\n  * GPT war hier nicht das ausschlaggebende Signal, hat aber zur Gesamtbewertung beigetragen.\n\n\n\nDas ist genau das Verhalten, das ich will: GPT als ein Baustein unter vielen, der bei Grenzfällen den Ausschlag geben kann.\n\n### Datenschutz\n\nDas 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.\n\nFür meinen privaten Mailserver ist das Risiko vertretbar — und die Ergebnisse sprechen für sich.\n\n### Zusammenfassung\n\nParameter| Wert| Warum\n---|---|---\n`json`| `true`| Strukturiertes Parsing, zuverlässiger als Freitext\n`prompt`| Custom| **Pflicht bei json=true!** Default-Prompt liefert Textformat, Parser erwartet JSON\n`temperature`| 0.0| Deterministische Antworten, kein Kreativitäts-Bonus beim Spamfiltern\n`allow_ham`| true| Kleines positives Signal für legitime Mails\n`symbols_to_except`| BAYES_HAM, DNSWL, Whitelists, SMTP_AUTH, Spamtraps| Unnötige API-Calls vermeiden\n`reason_header`| nicht gesetzt| Nicht nötig mit json=true, interne Details gehören nicht in den Header\n\n**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)`.\n\nSiehe auch: rspamd mit Dovecot und IMAPSieve\n\nFragen? Einfach melden.",
  "title": "GPT in Rspamd aktivieren: so nutze ich das LLM-Signal im Score"
}