August 12, 2026
Email Finally Learns to Carry Files
Email can now download and send attachments end to end — and the fix that came with it caught attached file content leaking straight into message bodies.
Email could read and write messages for a while now, but couldn’t touch a file. That gap closed today — attachments flow both directions now, and fixing the last mile of it caught a bug that had been quietly corrupting message bodies.
Download side first: pull attachments off a message and they land in a swept tmp directory, paths expire after 24 hours so nothing lingers past its use. Filenames get sanitized — hostile input, unicode-only names, whatever a message actually contains — with loud fallbacks instead of a silent rename nobody notices, and duplicates get numbered rather than overwriting each other. Send side is the mirror: hand send, reply, or forward a list of absolute paths and they get loaded and MIME-attached through the same builder every outgoing message already runs through, which meant migrating that builder off the old MIMEText API onto EmailMessage to do it properly instead of bolting attachments onto the wrong foundation. Paths get validated before the connected-account gate, not after — a missing file fails loud and names the path, instead of failing deep inside a send that already looked committed.
The bug came out sideways. extract_body — the function that turns a raw message into readable text — was splicing attached text/plain parts straight into the body it returned, on both reads and forwards. Attach a text file to a message and its contents showed up glued onto the message body, no attachment needed to see it. Nobody built that on purpose; it’s what happens when a body-extraction function was never taught that “this part is an attachment” is a different case from “this part is content.” Fixed in the same pass that gave attachments a real path of their own, which is the right order — you don’t trust a feature enough to expose it until the thing it depends on stops lying about what’s actually in a message.
Second commit was smaller and duller on purpose: three functions that existed only to call another function immediately, wired out and replaced with the direct call at every site. Deleting them isn’t a story. It’s the tax that keeps not getting paid if this doesn’t happen regularly.
Attachments were the obvious hole in email for a while — read and reply worked, but the moment a message needed a file attached, there was nothing to hand it. That’s closed now. What’s left to prove is the messier end: threads with mixed content types, and whether a 24-hour tmp expiry holds up against someone who downloads a file and comes back to it the next day.
-
New download action for email attachments — files land in a swept tmp dir, paths expire after 24 hours, hostile and unicode filenames get sanitized with loud fallbacks
-
Send, reply, and forward all take an attachments param now, MIME-attached through a shared builder migrated off MIMEText onto EmailMessage
-
Fixed extract_body splicing attached text/plain content straight into message bodies on read and forward — attachment content was leaking into text nobody asked to see
-
Search and read results now carry attachment metadata (filename, size, mime, has_attachments) so the model knows what’s on a message before opening it
-
Removed three pass-through wrapper functions, rewiring their call sites to call the target directly