Summary
Outbound attachments can only be supplied as inline base64. For an agent caller, that means the file's bytes must pass through the model's context window on every send — there is no upload handle, no URL, and no path. The read and forward paths already avoid this; only the send path does not.
Direction: back outbound attachments with AgentDrive so callers reference stored objects instead of inlining bytes.
Current interface
send_message and forward_message accept attachments[]:
maxItems: 10, ≤10 MB each decoded, ≤25 MB combined.
The asymmetry
| Path |
Indirection |
Bytes through caller context |
get_attachment (read) |
download_url + expires_at, explicitly "no size limit" |
No |
forward_message |
originals carried over server-side by default |
No |
send_message (originate) |
none |
Yes, always |
The read path caps opt-in inline base64 at 256 KB precisely because inlining is expensive. The write path accepts 10 MB of the same encoding — a 40× larger ceiling in the direction that was deliberately constrained elsewhere.
Why the documented limits are not reachable
Base64 expands by ~4/3, so the stated ceilings imply an agent emitting ~13.3 MB (single) or ~33 MB (combined) of high-entropy text in one tool call. In practice the binding constraint is per-call output, not the documented size limit.
Measured on a real send: an 11,549-byte zip became a 15,400-character data string, and emitting it was the slowest step of the task by a wide margin. Extrapolating, a 1 MB PDF is ~1.4M characters — roughly two to three orders of magnitude below the documented cap.
Integrity
There is no checksum field anywhere in the attachment interface, so nothing validates that what the caller emitted is what the server stored.
The data pattern ^[A-Za-z0-9+/]+={0,2}$ constrains the alphabet only. A dropped or inserted character usually changes the decoded length or fails the decode, so it tends to surface. A substitution of one valid base64 character for another preserves length, decodes cleanly, and silently yields different bytes. Nothing in the API detects this.
Whether that corruption is noticed today depends entirely on the container the caller happened to pick:
- zip / tar.gz — per-entry CRC32, so the recipient gets a loud error
- PDF, plain text, most images — no internal check, corrupts silently
That is luck, not a guarantee.
Proposed
- AgentDrive-backed handles. Accept a stored-object reference as an alternative to
data in attachments[] — one of data or the handle, not both. Mirrors what get_attachment's download_url already does for the read path, and lets an agent that produced a file in AgentDrive attach it without ever materializing the bytes in context.
- Optional
sha256 per attachment entry. Server rejects on mismatch rather than mailing a corrupted file. Cheap, and it makes integrity assertable end to end for the inline path that will remain for small files.
- Reconcile the inline ceilings. If 256 KB is the right inline limit for reads, the 10 MB write limit deserves the same reasoning applied to it — or documentation stating the practical limit for agent callers.
Notes
- Discovered while sending a ~11 KB zip attachment through the MCP surface; the send itself succeeded and delivered intact (server-reported
size_bytes matched the local file exactly).
- Not urgent for correctness of small sends today. It is a blocker for any workflow that originates a non-trivial file — reports, exports, generated documents — which is a plausible thing to want from email infrastructure for agents.
Summary
Outbound attachments can only be supplied as inline base64. For an agent caller, that means the file's bytes must pass through the model's context window on every send — there is no upload handle, no URL, and no path. The read and forward paths already avoid this; only the send path does not.
Direction: back outbound attachments with AgentDrive so callers reference stored objects instead of inlining bytes.
Current interface
send_messageandforward_messageacceptattachments[]:{ "filename": "report.pdf", // 1–255 chars "content_type": "application/pdf", // ^[a-zA-Z][a-zA-Z0-9.+-]*\/[a-zA-Z0-9.+-]+$ "data": "JVBERi0xLjQK…" // ^[A-Za-z0-9+/]+={0,2}$ — base64 only }maxItems: 10, ≤10 MB each decoded, ≤25 MB combined.The asymmetry
get_attachment(read)download_url+expires_at, explicitly "no size limit"forward_messagesend_message(originate)The read path caps opt-in inline base64 at 256 KB precisely because inlining is expensive. The write path accepts 10 MB of the same encoding — a 40× larger ceiling in the direction that was deliberately constrained elsewhere.
Why the documented limits are not reachable
Base64 expands by ~4/3, so the stated ceilings imply an agent emitting ~13.3 MB (single) or ~33 MB (combined) of high-entropy text in one tool call. In practice the binding constraint is per-call output, not the documented size limit.
Measured on a real send: an 11,549-byte zip became a 15,400-character
datastring, and emitting it was the slowest step of the task by a wide margin. Extrapolating, a 1 MB PDF is ~1.4M characters — roughly two to three orders of magnitude below the documented cap.Integrity
There is no checksum field anywhere in the attachment interface, so nothing validates that what the caller emitted is what the server stored.
The
datapattern^[A-Za-z0-9+/]+={0,2}$constrains the alphabet only. A dropped or inserted character usually changes the decoded length or fails the decode, so it tends to surface. A substitution of one valid base64 character for another preserves length, decodes cleanly, and silently yields different bytes. Nothing in the API detects this.Whether that corruption is noticed today depends entirely on the container the caller happened to pick:
That is luck, not a guarantee.
Proposed
datainattachments[]— one ofdataor the handle, not both. Mirrors whatget_attachment'sdownload_urlalready does for the read path, and lets an agent that produced a file in AgentDrive attach it without ever materializing the bytes in context.sha256per attachment entry. Server rejects on mismatch rather than mailing a corrupted file. Cheap, and it makes integrity assertable end to end for the inline path that will remain for small files.Notes
size_bytesmatched the local file exactly).