Repository navigation
Keep a copy of what was sent, where the sender looks for it - #11
Merged
Merged
Conversation
SMTP delivers a message and keeps nothing, and most IMAP hosts -- Soverin
among them -- file no copy server-side either. So a mail sent through
Squirrel really was gone the moment it left: with the recipient, and absent
from the sender's own Sent folder, which is where mail_search and every mail
client look. A send you cannot find afterwards reads as a send that never
happened.
So provider.send finishes the job. smtp.send hands back the exact RFC822
bytes it delivered plus the moment it did, and provider.py -- the one place
that sees both halves, exactly as with threading -- APPENDs them to the Sent
folder \Seen, with the send time as INTERNALDATE so the copy sorts where it
belongs. Rebuilding the message instead would mint a fresh Message-ID and
Date, and the copy would no longer be the mail the recipient got.
Four decisions hold it up:
* The folder is read, not guessed: the \Sent SPECIAL-USE attribute (RFC 6154)
off the same LIST mail_list_folders already returns. The name is localized
("Verzonden items"), sometimes under INBOX, and "Sent Items"/"Sent Messages"
are both common -- "Sent" is only the fallback when a server advertises
nothing.
* A failed APPEND does not fail the send. The message is with the recipient by
then, so raising would report a delivered mail as undelivered and invite a
retry that sends it twice. The outcome rides back as saved_to_sent /
sent_folder on SendResult, and the tool tells the user.
* The Bcc header stays on the copy and only comes off what leaves: your own
Sent folder is the only record of whom you blind-copied.
* A host that files its own copy gets one, not two -- the Message-ID is
searched for in the folder before the APPEND. Same reason the Graph backend
in the admin package reports saved_to_sent=True rather than filing anything:
/send puts the message in Sent Items server-side.
A reply travels this same path, so it is filed like any other message,
threading headers and all.
tests/test_sent_copy.py pins the discovery, the flags, the deduplication and
the never-fatal rule; the GreenMail e2e sends for real and reads the copy back
out of Sent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPDJU6WSgJ7cQdfp8GoHXC
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.
The bug
mail_sendfrom a Soverin mailbox reportedSentwith a real message id, therecipient got the mail — and
mail_searchonSentfound nothing. It neverhad. SMTP delivers a message and keeps nothing, and most IMAP hosts (Soverin
included) file no copy server-side either, so the sender's own record of the
message did not exist. A send you cannot find afterwards reads as a send that
never happened.
The fix
provider.sendfinishes the job.smtp.sendhands back the exact RFC822bytes it delivered plus the moment it did, and
provider.py— the one placethat sees both halves, exactly as with threading — APPENDs them to the Sent
folder
\Seen, with the send time as INTERNALDATE so the copy sorts where itbelongs. Rebuilding the message instead would mint a fresh Message-ID and Date,
and the copy would no longer be the mail the recipient got.
Four decisions hold it up:
\SentSPECIAL-USE attribute(RFC 6154) off the same LIST
mail_list_foldersalready returns. The name islocalized (
Verzonden items), sometimes under INBOX, andSent Items/Sent Messagesare both common —Sentis only the fallback when a serveradvertises nothing. Resolved once per session and cached; a failed listing
is deliberately not cached.
by then, so raising would report a delivered mail as undelivered and invite a
retry that sends it twice. The outcome rides back as
saved_to_sent/sent_folderonSendResult, and the tool docstring tells the client to sayso and not resend.
Sent folder is the only record of whom you blind-copied.
searched for in the folder before the APPEND.
A reply travels this same path, so it is filed like any other message,
threading headers and all.
Tests
tests/test_sent_copy.py(new): SPECIAL-USE discovery incl. a localizedname, the
Sentfallback, resolve-once, a failed listing not being cached,the APPEND's bytes/flags/INTERNALDATE, the no-duplicate probe, the provider
joining SMTP and IMAP, a reply being filed, an APPEND failure being reported
rather than raised, and Bcc on the copy but not on the wire.
tests/test_tools.py:mail_sendreports the copy; a failed copy is asuccessful send with
saved_to_sent=False.Sent, for anew message and for a reply. (Not run here — no Docker in this environment;
make testis green: 221 passed. Ruff + ty clean.)Companion change
pantalytics/squirrel-mcp-admin#…— the Graph backend reportssaved_to_sent=True/Sent Itemsinstead of filing anything, because/sendalready leaves the message in Sent Items server-side. That report iswhat keeps a second copy from being filed on top of Graph's. Shipping the fix
to the hosted product also needs
ARG SQUIRREL_MCP_REFin the adminDockerfilebumped to this commit once merged.🤖 Generated with Claude Code
https://claude.ai/code/session_01PPDJU6WSgJ7cQdfp8GoHXC
Generated by Claude Code