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— ISO-8601, mailx-localEurope/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 literalU(rspamd headers absent).rspamd_action— one ofreject,no action,add header,greylist,soft reject,rewrite subject, orU.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.