Three new tools complete the planned Phase C scope:
- mail_mark { uid, action, folder? }: action is one of read, unread,
flagged, unflagged, trash, archive. read/unread toggle \Seen via UID
STORE +/-FLAGS.SILENT (idempotent, no fetch round-trip). flagged/
unflagged the same for \Flagged. trash is a MOVE to Trash. archive
errors out with a clear pointer to mail_move because Sulkta's Dovecot
doesn't ship a canonical Archive folder.
- mail_attachment_get { uid, attachment_index, folder? }: fetches the
full RFC822 (within the existing 20 MB raw_eml cap), parses with
mail-parser, returns the N-th attachment as base64. The index matches
mail_inbox_read's attachments[] ordering. Returns {filename,
mime_type, size, content_base64}. SAFETY note in the tool description
warns the LLM not to execute / render / open attachment bytes
blindly.
- mail_reply { uid, body, body_html?, attachments?, reply_all?,
to_override? }: fetches the original to pull From / Subject /
Message-Id / References, then sends with proper In-Reply-To +
References + 'Re: ' subject prefix (skipped if already prefixed).
reply_all=true echoes the original Cc. to_override replaces To.
Threading headers still set against the original regardless of
to_override.
Smoke verified 2026-05-21:
- Send kayos->kayos with a 54-byte text attachment
- mail_inbox_read shows attachments=[('smoke.txt', 54)]
- mail_attachment_get returns the exact bytes (b'Hello from mail-mcp
Phase C smoke!\r\nLine 2.\r\nLine 3.\r\n', 54 bytes)
- mail_mark unread -> flags=[] (\Seen cleared)
- mail_mark flagged -> flags=['\\Flagged']
- mail_reply -> message lands as 'Re: mail-mcp phase-C smoke' with
In-Reply-To = parent Message-Id and References = parent Message-Id
ServerHandler instructions updated to enumerate all 10 tools + the new
attachment-safety note. Tools live on the wire: mail_send,
mail_inbox_list, mail_inbox_read, mail_folder_list, mail_search,
mail_thread, mail_move, mail_mark, mail_attachment_get, mail_reply.
Test count 27 -> 33: 2 for MarkAction::parse (alias coverage + unknown
rejection), 4 for tools::extract_addr (display-name strip + bare-addr
passthrough + garbage tolerance + ToField unwrap).