Embed an image in the body, not just beside it - #8
Merged
Conversation
Outgoing attachments landed in #7 with inline deliberately left out, for a reason its own docstring spelled out: inline means nothing without an HTML body referencing the part, and a multipart/related holding an image nothing points at renders differently in every client. This is the HTML compose path that was waiting on, and the reason is now enforced rather than avoided. body_html is added as an *alternative*, never a replacement -- body stays the text a plain-text client shows, and both halves are the same message. An attachment marked inline is only *placed* inline when there is HTML that could carry the cid: reference, and degrades to an ordinary attachment when there is not, keeping its Content-ID for a client that wants to show it anyway. The placement is the whole feature and no unit above the MIME layer can see it. An inline part goes inside multipart/related next to the HTML, because a client resolves cid: within that group and nowhere else; the same image parked in the outer multipart/mixed beside the paperclips renders in some clients and arrives as a second paperclip in the rest. So the shape is mixed[alternative[text, related[html, image]], file] -- and a message with neither HTML nor attachments is still the single text/plain part it always was. Every inline part ends up with a content_id whether the caller gave one or not, since a part nothing can name is a part nothing can show, and the three spellings a model reaches for -- logo, cid:logo, <logo> -- reduce to one. source_uid and inline are orthogonal: a signature image already in the mailbox can be re-embedded without routing the bytes through the model. tests/test_attachments_inline.py pins the trees and the id handling; the GreenMail e2e reads the delivered multipart/related back off a real server, Content-ID and both bodies included. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ssg4B9jnGCrYKNK9DbqKbv
rutgerhofste
force-pushed
the
claude/microsoft-agent-attachments-3wijn2
branch
from
August 7, 2026 13:19
54c44ba to
1048c2d
Compare
rutgerhofste
marked this pull request as ready for review
August 7, 2026 13:25
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.
Rebased onto #7. This branch originally defined its own attachments seam, because the Graph provider in the admin package needed one to exist. #7 merged first with a better one —
source_uidforwarding that never routes bytes through the model,supports_outgoing_attachmentsso a silent backend cannot swallow a file, a size limit read from SMTP's ownSIZEextension. All of that is kept; my duplicate is gone. What remains is the one thing #7 deliberately left out.From
OutgoingAttachment's docstring as merged:This is that path. The caution is not dropped, it is enforced.
What it adds
body_html, as an alternative —bodystays the text a plain-text client shows, and both halves are the same message. Andinline/content_idonOutgoingAttachment, where inline is only placed inline when there is HTML that could carry the reference, and degrades to an ordinary attachment when there is not, keeping itsContent-IDfor a client that wants it anyway.The placement is the feature
mime._add_parts. An inline part goes insidemultipart/relatednext to the HTML, because a client resolvescid:within that group and nowhere else — the same image parked in the outermultipart/mixedbeside the paperclips renders in some clients and arrives as a second paperclip in the rest.text/plain— unchangedalternative[text, html]alternative[text, related[html, image]]mixed[alternative[text, related[html, image]], file]mixed[text, image]— degraded, honestlyEvery inline part ends up with a
content_idwhether the caller gave one or not, since a part nothing can name is a part nothing can show, and the three spellings a model reaches for (logo,cid:logo,<logo>) reduce to one.source_uidandinlineare orthogonal, so a signature image already in the mailbox can be re-embedded without routing the bytes through the model.Testing
tests/test_attachments_inline.py(new, 17 tests): the trees above, the content-id handling, and the tool seam. Only what inline adds —test_attachments_outgoing.pystill owns the argument forms and the refusal rules.multipart/relatedback off a real server, Content-ID and both bodies included. Not run locally (no Docker daemon here); theintegration-greenmailCI job covers it.make test155 passed (Send attachments over SMTP, not just read them #7's suite included),make lintclean.Sibling: pantalytics/squirrel-mcp-admin#58 does the same for Outlook over Graph, and needs this branch for
body_html.🤖 Generated with Claude Code
https://claude.ai/code/session_01Ssg4B9jnGCrYKNK9DbqKbv