Skip to main content

Attachments

Attach up to 10 files to an email you send, and download the files on mail your agents receive.

Send attachments

Add an attachments array to POST /v1/messages: up to 10 files, 25 MB in all. Each file has a filename (1 to 255 characters) and its bytes, given one of two ways:

  • content_base64: the bytes, base64-encoded, with the file’s content_type. They travel inside the request, so together they can come to about 3 MB.
  • path: an https URL that mails.ai downloads when it sends the email, which is how a larger file goes. content_type may be left out, and the type the URL answers with is used.

Give a file a content_id to show it inside the HTML body instead of attaching it: <img src="cid:logo"> shows the file whose content_id is logo. A content_id takes letters, digits and . _ - @ + = $, up to 127 characters, and names one file in the message. Mail with no HTML body has nowhere to show it, so the file is sent as an ordinary attachment under its filename.

import { readFile } from "node:fs/promises";
import { mails } from "@mailsai/sdk";

const pdf = await readFile("invoice.pdf");

const sent = await mails.send({
  from: "hello",
  to: "reply@test.mails.ai",
  subject: "Your invoice",
  body_html: '<img src="cid:logo" alt="Acme"><p>Your invoice for September is attached.</p>',
  attachments: [
    // Shown where the HTML says cid:logo, downloaded from its URL when the email is sent
    {
      filename: "logo.png",
      path: "https://files.example.com/logo.png",
      content_id: "logo",
    },
    // Attached, from its bytes
    {
      filename: "invoice.pdf",
      content_base64: pdf.toString("base64"),
      content_type: "application/pdf",
    },
  ],
});
console.log(sent.id);

A file given by path must answer within 10 seconds, after at most 3 redirects, each to https, and must not be a private, loopback, link-local or cloud metadata address. One that cannot be fetched refuses the whole send with 400 invalid_field, its param naming the file (attachments.0.path).

Only a single send takes files: a batch, a reply, a forward and a draft refuse attachments with 422 invalid_field. A forward of a received message carries that message’s files by itself.

mails.ai keeps the name, type and size of each file you send, not its bytes, so the sent message’s attachments lists those three, and the content_id of a file shown inside the HTML.

Attach files only to a send that goes out now. Because the bytes are not kept, a send with a future scheduled_at cannot carry them: that combination is refused with 400 invalid_field.

Files on received mail

A received message has has_attachment and an attachments list. Images embedded in the HTML body are in the list with inline: true, but they do not set has_attachment. Files that were not kept are listed under attachments_omitted instead, each with the reason.

Each file in the list:

FieldDescription
idstring Required. Attachment id (att_…), unique within the message.
filenamestring Required. The file's name, cleaned: no path, no control characters, at most 255 characters.
content_typestring Required. The MIME type the sender declared, e.g. application/pdf.
sizeinteger Required. Size in bytes of the decoded file.
content_idstring | null Required. The part's Content-ID without angle brackets, which the HTML body references as cid:.
inlineboolean Required. True for an image embedded in the HTML body rather than attached as a file.

Each file that was not kept:

FieldDescription
filenamestring Required. The file's name, cleaned like a kept attachment's: no path, no control characters, at most 255 characters. A file the email did not name reads attachment.<ext>.
content_typestring Required. The MIME type the sender declared, in lower case; application/octet-stream when it was missing or malformed.
sizeinteger | null Required. Size in bytes when known.
reasonstring Required. Why the file was not kept: the reasons of raw_omitted, or too_many for files past the first 100 of one message. One of: too_large, budget, over_quota, unavailable, too_many.

A download always comes back as a file to save, never as a page to display: HTML and SVG files are never served as themselves, and ?inline=1 is honoured only for PNG, JPEG, GIF, WebP, AVIF and BMP images.

import { writeFile } from "node:fs/promises";
import { createClient } from "@mailsai/sdk";

const client = createClient(); // reads MAILS_API_KEY
const messageId = "rcv_01JZX8K3M9Q4P7VN2YB6RTDC0E";

// The files on a received message
const { data: files } = await client.received.attachments.list(messageId);

// Save each one under its own name
for (const file of files) {
  const bytes = await client.received.attachments.download(messageId, file.id);
  await writeFile(file.filename, bytes);
}

Treat every received file as untrusted input: an agent should check a file’s type and size before it opens it, the same way it treats the message text.

Test with a raw email

With a mk_test_ key, send a complete email as raw_base64 (up to 3 MB) to POST /v1/test/inbound. Its files become downloadable exactly as on real mail, and from, subject and body_text default to the email’s own.

curl -X POST https://api.mails.ai/v1/test/inbound \
  -H "Authorization: Bearer $MAILS_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"agent\": \"hello\", \"raw_base64\": \"$(base64 < message.eml | tr -d '\n')\"}"

Next steps

Full request and response shapes are in the API reference.

Was this page helpful?