Skip to content

Mail-MCP spam verdict parser

The mail-mcp server's spam false-positive tools (spam_verdicts, explain_verdict, fp_candidates, symbol_impact, sender_history) read the Kafka topic myra-spam-compare, which carries one rspamd verdict per record from mailx's spam-compare.csv. Every record is untrusted — it contains attacker-influenced email content (Subject, From, Message-ID, rspamd symbol names) shipped through Vector and AKHQ. The parser (scripts/mail_mcp/spamlog_parser.py) is fail-closed: it never raises, never hangs, and never emits a half-populated verdict.

Accepted shape

A record is a JSON object with a message string holding one CSV row of exactly seven fields (RFC4180, "-quoted, ""-escaped):

timestamp, message_id, from, subject, rspamd_score, rspamd_action, rspamd_symbols
  • timestamp — ISO-8601, mailx-local Europe/Berlin, whole-second or microsecond precision. Parsed to UTC epoch (the verdict-time window key).
  • message_id, from, subject — opaque; source-truncated to 80 chars. May be empty (a rejected message often has no Message-ID). May contain embedded newlines inside the quoted field (MIME-folded subjects).
  • rspamd_score — a finite float, or the literal U (rspamd headers absent).
  • rspamd_action — one of reject, no action, add header, greylist, soft reject, rewrite subject, or U.
  • rspamd_symbols — space-separated symbol names (may be empty).

A valid record yields {ts, ingested, host, host_fqdn, message_id, sender, subject, score, action, symbols[], _raw}. score is null when the field is U/non-numeric (the row is still kept).

What is rejected (dropped as None, never a crash or partial record)

Input Result
Non-object record, or message missing / not a string / empty dropped
message larger than 64 KiB dropped
Not exactly 7 CSV fields dropped
The header row (timestamp,message_id,…) dropped
timestamp not a parseable ISO date dropped
Unbalanced quote / a bare continuation half-line (un-reassembled) dropped
rspamd_score = U / nan / inf / non-numeric kept, score = null
rspamd_action unknown string kept, action verbatim

Tool arguments are validated separately: action accepts the rspamd_action values listed above except U (with _↔space normalization); an unrecognised action — U included — is rejected with -32602 invalid action. limit must be an integer in 1..100 — an out-of-range value is rejected with -32602 invalid limit, not silently clamped. min_score must be a finite number, and since/until accept ISO-8601 or a relative N{s,m,h,d}. Unparseable arguments are rejected with JSON-RPC -32602, never a 500.