Reply in the thread instead of starting a new one - #6
Merged
Conversation
mail_send and mail_draft had no way to say "this answers that message", so every reply left as a fresh conversation carrying nothing but a "Re:" subject. Gmail usually guessed it back into the thread on the participants; Outlook generally did not, and the tool's own answer gave no way to tell which had happened. Both now take reply_to_uid (+ reply_to_folder). The threading itself is the provider's job -- the uid travels down untouched, and the IMAP/SMTP backend reads the parent's Message-ID and References and sends the matching headers. That leaves room for a backend that threads differently: the private Graph provider joins Outlook's own conversation rather than writing headers. What the tool layer derives is only what a person would see: the recipient (Reply-To when the sender set one, else From), the "Re:" subject without stacking a prefix per hop, and reply-all's cc list minus yourself. So to and subject may be omitted on a reply -- and are still required without one. Two quieter halves: editing a reply draft keeps its threading (the edit rewrites the message, so a reviewed reply would otherwise turn back into a new conversation at the moment it is sent), and a long References is trimmed to the thread root plus the nearest ancestors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EodnnscF7qqhJrnDVqJSoK
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
mail_sendandmail_drafthad no way to say "this answers that message". Every reply left as a fresh conversation carrying nothing but aRe:subject. Gmail usually guessed it back into the thread from the participants; Outlook generally did not — and the tool's own answer gave no way to tell which had happened.What this does
Both tools take
reply_to_uid(+reply_to_folder, defaultINBOX).Threading is the provider's job. The uid travels down to the backend untouched, because how you join a thread is the transport's business:
Message-IDandReferences(imap.reply_headers, headers-only fetch) and sends the matchingIn-Reply-To/References— joined inprovider.py, the only thing that sees both halves.createReply, so Outlook'sconversationIdstays Outlook's to assign.The tool layer derives only what a person would see: the recipient (
Reply-Towhen the sender set one, elseFrom), theRe:subject without stacking a prefix per hop, and reply-all's cc list minus yourself. Sotoandsubjectmay be omitted on a reply — and are still required without one.SendResult.in_reply_tonames the message that was answered, so a client can report "sent as a reply" honestly.Two quieter halves:
update_draftrewrites the message, so a reviewed reply would otherwise turn back into a new conversation at the moment it is sent.Referencesis trimmed to the thread root plus the nearest ancestors (MAX_REFERENCES), the way every mail client does it.MessageDetail/MailBodygainreply_to_addrs(defaulted, so a backend written before this still constructs).Compatibility
to,subjectandbodymove from schema-required to tool-validated onmail_send/mail_draft, since they are genuinely optional when replying. A call that omits one without areply_to_uidgets the same refusal it always did, now with a clearer message.Testing
tests/test_reply_threading.py— the headers, the chain trim, the subject/address derivation, the tools through FastMCP, and the IMAP↔SMTP hand-off inSoverinMailProvider.tests/test_imap_threading.py—reply_headersagainst a fake mailbox (chain building, the In-Reply-To fallback, a parent with no Message-ID replying unthreaded rather than failing, headers-only), andupdate_draftcarrying threading over.make testandmake lintgreen locally; the integration and docker/browser jobs run in CI (no Docker in this environment).Generated by Claude Code