Docs/Lanes Link/Reference/Attachments

Attachments

Sending a file through an agent, without the bytes ever passing through the model

gmail.send_message and icloud_mail.send_message both take an attachments list. The bytes never pass through the model. A file is named, and the endpoint reads it.

That is the whole design. What follows is how to use it.

Naming a file

Each entry carries exactly one source. Naming two is an error rather than a precedence rule, because silently preferring one would make the other look like it worked.

jsonc
{ "path": "/Users/you/Downloads/invoice.pdf" }         // read from this machine
{ "url": "https://example.com/invoice.pdf" }           // fetched here, over HTTPS only
{ "handle": "att_01j7k…" }                             // staged earlier, see below
{ "message_id": "18f…", "attachment_id": "quote.pdf" } // already in this mailbox
{ "data": "JVBERi0…", "filename": "invoice.pdf" }      // inline base64, last resort

filename and content_type are optional overrides on any of them. attachment_id may be a filename, a 1-based position, or, on Gmail, the vendor's own id. Omit it when the message has one attachment.

Forwarding costs nothing

message_id resolves inside the endpoint, so re-sending a PDF that arrived by mail never materialises it anywhere a context window can see. Prefer it over reading an attachment and passing the bytes back. The read side returns metadata only, so that route does not exist anyway.

Sending from a deployed endpoint

path means the filesystem the server can see, which on Cloud Run is a container. Stage the bytes instead:

console
$ lanes link attach ~/Downloads/invoice.pdf --connection gmail.you
att_75f5be471a18b0f7…  invoice.pdf  239104 bytes

Or POST /attachments?connection=gmail.you with the file as the body, an X-Filename header, and the usual bearer token. Either way you get a handle to pass as { "handle": "…" }.

Handles belong to the connection they were staged for, expire after a day, and are swept on the next upload. A handle staged for assets/main is deliberately unresolvable from gmail/main, so use lanes link attach --connection gmail.you to mail a file you keep in assets.

Size

Gmail accepts 35 MiB and iCloud 20 MB, both counted encoded. Attachments travel as base64, so usable file weight is about three quarters of that. An oversized send is refused before anything is submitted, because a message rejected part-way through DATA reads like a dropped connection.

What the log keeps

Recipients and bodies are withheld, as always. Per attachment it records the filename, byte length, content type, SHA-256, and where the bytes came from, including the resolved absolute path.

path is deliberately unrestricted, so that record is the only trace of which file left the machine. It is what makes "was this ever mailed out" an answerable question. The tool result is the same receipt, never the content.

Give a connection a display name

Only the SMTP path needs this. Gmail is asked for no From at all, so it fills the header from the credential, display name included. SMTP submits exactly what is composed, so without a name the header is a bare address:

YAML
connections:
  - id: rin_shaw
    provider: icloud_mail
    account: rin.shaw@example.com
    config:
      from_name: Ada Lovelace

from_name on the call overrides it.

This is not a spam control. What decides placement is alignment, and that is already healthy: a message sent this way arrives dkim=pass header.i=@icloud.com and spf=pass, signed and sent by iCloud's own infrastructure, because the endpoint submits through the account rather than spoofing it. A missing display name is a trust problem rather than a deliverability one, and for client-facing mail that is reason enough.

An older iCloud connection needs re-connecting once

IMAP capabilities are discovered and cached in the profile database, so a connection made before attachments existed keeps the old schema and never shows attachments, however new the code is:

console
$ lanes link connect icloud_mail --profile personal --workspace local

The stored app password is reused, so it is non-interactive. Gmail needs nothing, because its send is authored rather than discovered.

Watch the failure mode in between: an endpoint still running the old code advertises attachments and silently ignores it, which is a mail that says it has an attachment and does not. lanes link plan does not warn about it, because it compares capability names and this changed a schema.


Next: Google and iCloud are the two accounts that send mail.