Email Attachments

email_event messages expose provider-neutral attachment metadata plus an opaque retrieval handle. Events never inline attachment bytes; use uxc email attachment get to download content on demand.

Attachment Metadata

Each entry under message.attachments carries:

FieldTypeNotes
idstringProvider part/attachment id (IMAP section number, Gmail/Graph attachment id, JMAP part id)
filenamestring | nullDecoded from RFC 2047 / RFC 2231 forms; null when absent
content_typestring | nullMIME type of the part
sizenumber | nullDecoded byte size when known
dispositionstring | nullattachment, inline, or null
content_idstring | nullContent-ID for inline HTML resources
handleobjectOpaque retrieval handle for lazy download

A part counts as an attachment when:

Plain text/plain and text/html alternatives of the message body are never attachments.

Provider Notes

ProviderMapping
IMAPParsed from the raw MIME tree; id is the RFC 3501 section number
GmailRecursed over payload.parts; id is the body.attachmentId
Microsoft GraphMapped from attachments[]; requires $expand=attachments on the poll endpoint
JMAPMapped from attachments[] (partId, blobId, type, size)

Fields a provider does not report stay null; the original provider JSON is preserved verbatim under raw.provider_payload.

Microsoft Graph Without Expansion

Graph list endpoints do not return attachment metadata by default. When the poll endpoint lacks $expand=attachments but the message reports hasAttachments: true, the event keeps the provider's signal without inventing metadata:

{
  "attachments": [],
  "has_attachments": true,
  "attachment_count": null
}

attachment_count: null means "the provider says attachments exist but their metadata was not expanded", as opposed to 0 for a message without attachments. Consumers that need per-attachment entries should subscribe with $expand=attachments.

Retrieval Handles

handle objects are credential-free. They reference an auth profile by name, and that profile is re-resolved from the local auth store at download time:

{
  "type": "email_attachment",
  "provider": "gmail",
  "endpoint": "https://gmail.googleapis.com/gmail/v1/users/me/messages",
  "account": "user@example.com",
  "mailbox": "INBOX",
  "message_id": "<msg@example.com>",
  "uid": "18c2f4a9e3b1d000",
  "auth_profile": "gmail-primary",
  "part": { "attachment_id": "ANGjdJ_..." }
}

The part field is provider-specific: IMAP uses {"section": "2.1"} (plus uidvalidity guarding against stale UIDs), Gmail and Graph use {"attachment_id": "..."}, and JMAP uses {"blob_id": "..."}.

Treat handles as opaque values: copy them from the event and pass them back to UXC. Do not parse or construct them by hand; the fields above are documented for transparency only.

Lazy Download

uxc email attachment get --handle '{"type":"email_attachment", ...}'
uxc email attachment get --handle @handle.json --output ./report.pdf
OptionDescription
--handle <json|@file>The attachments[].handle object from an email_event, or a file containing it
--profile <name>Auth profile override; takes precedence over the handle reference
--output <path>Explicit output path; defaults to a cache path under ~/.uxc/email-attachments/
--max-bytes <n>Reject larger attachments; defaults to 25 MiB, 0 disables the limit

Successful downloads return a kind=email_attachment_get_result envelope:

{
  "ok": true,
  "kind": "email_attachment_get_result",
  "data": {
    "provider": "gmail",
    "account": "user@example.com",
    "message_id": "<msg@example.com>",
    "attachment_id": "ANGjdJ_...",
    "filename": "report.pdf",
    "content_type": "application/pdf",
    "size_bytes": 102400,
    "sha256": "9f2b...",
    "saved_path": "/home/user/.uxc/email-attachments/report.pdf"
  }
}

Content is always written to a file; it is never inlined into the JSON envelope. The response includes sha256 and size_bytes so callers can verify the download.

Errors

CodeMeaning
invalid_attachment_handleThe handle is not valid JSON, has the wrong type, or misses required provider fields
attachment_not_foundThe provider no longer has the referenced attachment
message_not_foundThe referenced message no longer exists
uid_invalidIMAP mailbox UIDVALIDITY changed since the event; re-fetch the message
auth_profile_missingThe referenced auth profile is absent from the local store
auth_failedThe provider rejected the re-resolved credentials
size_limit_exceededThe attachment exceeds --max-bytes
provider_request_failedThe provider request itself failed

Compatibility

Messages without attachments keep attachments: [], has_attachments: false, and attachment_count: 0, so existing email_event consumers are unaffected by attachment support.