Attachments
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’scontent_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_typemay 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);import base64
from mailsai import Client
client = Client()
with open("invoice.pdf", "rb") as f:
pdf = base64.b64encode(f.read()).decode()
message = {
"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,
"content_type": "application/pdf",
},
],
}
sent = client.send(**message)
print(sent["id"])curl -X POST https://api.mails.ai/v1/messages \
-H "Authorization: Bearer $MAILS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "hello",
"to": "reply@test.mails.ai",
"subject": "Notes from today",
"body_html": "<img src=\"cid:logo\" alt=\"Acme\"><p>The notes are attached.</p>",
"attachments": [
{
"filename": "logo.png",
"path": "https://files.example.com/logo.png",
"content_id": "logo"
},
{
"filename": "notes.txt",
"content_base64": "SGVsbG8sIHdvcmxkIQ==",
"content_type": "text/plain"
}
]
}'# --attach takes a local file or an https URL; --inline shows one inside the HTML
mails emails send --from hello --to reply@test.mails.ai \
--subject "Your invoice" \
--html '<img src="cid:logo" alt="Acme"><p>Your invoice is attached.</p>' \
--inline logo=https://files.example.com/logo.png \
--attach ./invoice.pdfA 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.
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.
GET /List a received message's attachments. Needs thev1/ messages/ received/ {id}/ attachments readscope.GET /Download an attachment. Needs thev1/ messages/ received/ {id}/ attachments/ {attachment_id} readscope.
Each file in the list:
| Field | Description |
|---|---|
id | string Required. Attachment id (att_…), unique within the message. |
filename | string Required. The file's name, cleaned: no path, no control characters, at most 255 characters. |
content_type | string Required. The MIME type the sender declared, e.g. application/pdf. |
size | integer Required. Size in bytes of the decoded file. |
content_id | string | null Required. The part's Content-ID without angle brackets, which the HTML body references as cid:. |
inline | boolean Required. True for an image embedded in the HTML body rather than attached as a file. |
Each file that was not kept:
| Field | Description |
|---|---|
filename | string 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_type | string Required. The MIME type the sender declared, in lower case; application/octet-stream when it was missing or malformed. |
size | integer | null Required. Size in bytes when known. |
reason | string 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);
}from mailsai import create_client
client = create_client() # reads MAILS_API_KEY
message_id = "rcv_01JZX8K3M9Q4P7VN2YB6RTDC0E"
# The files on a received message
files = client.list_attachments(message_id)["data"]
# Save each one under its own name
for file in files:
data = client.download_attachment(message_id, file["id"])
with open(file["filename"], "wb") as f:
f.write(data)# The files on a received message
curl "https://api.mails.ai/v1/messages/received/$MESSAGE_ID/attachments" \
-H "Authorization: Bearer $MAILS_API_KEY"
# Save one, under the name in its Content-Disposition header
curl -OJ "https://api.mails.ai/v1/messages/received/$MESSAGE_ID/attachments/$ATTACHMENT_ID" \
-H "Authorization: Bearer $MAILS_API_KEY"# The files on a received message
mails receiving attachments rcv_01JZX8K3M9Q4P7VN2YB6RTDC0E
# Save one, under a name you choose
mails receiving attachment rcv_01JZX8K3M9Q4P7VN2YB6RTDC0E \
att_01JZX8K3M9Q4P7VN2YB6RTDC0E --output invoice.pdfTreat 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?