MOLTED EMAIL

Storage & Attachments

How storage is tracked, attachment handling, plan limits, and retention windows.

Molted Email tracks storage usage per tenant and stores attachments in secure object storage.

Storage model

Storage is split into two categories:

  • Message bodies -- the text and HTML content of inbound messages, stored with your tenant data.
  • Attachments -- binary files in secure object storage, with metadata tracked alongside.

Both categories count toward your plan's storage limit.

Plan limits

PlanStorage limitRetention
Trial (and a paid plan during its 3-day trial)50 MB7 days
Free100 MB14 days
Solo (and legacy Starter)3 GB30 days
Team (and legacy Growth)10 GB90 days
EnterpriseUnlimited365 days

When your account is in the expired state it keeps its previous storage limit, and its retention becomes the previous retention plus 30 days, so nothing is purged the moment a plan lapses. Uploads are refused (see Uploads on an expired plan), but attachments of email you receive are still stored.

Retention windows

Attachments older than your plan's retention window are automatically cleaned up daily, up to 1,000 per tenant per run:

  1. The file is removed from storage.
  2. The attachment row is marked deleted.
  3. Storage usage counters are decremented.

Attachments of a message held for the inbound allowance are never expired while it is held; once it's released, retention counts from the release.

The plan's retention window above covers attachment files only. Message text is not removed by it.

Message content retention

There is a second, separate daily pass that clears message content, but it only runs for a workspace that has a content retention period set. For such a workspace it empties, for every message older than the period, the bodies (plain text and encrypted), the raw inbound message, the stored send payloads and the humanizer's before and after text. Subjects, addresses, timestamps and delivery status are kept, so threads and lists still render, and the body search index entries for those messages are deleted with them.

No plan sets a content retention period, and nothing you can reach sets one: not the portal, the API, the CLI or MCP. So today, message text is kept until you delete it yourself. To remove it, use Deleting message content for everything, or Deleting contacts for one person's messages.

Checking storage usage

Storage usage is included in the billing status endpoint:

curl
curl https://mailbox.molted.email/v1/billing/status \
  -H "Authorization: Bearer YOUR_API_KEY"

The response includes a storage field:

Storage usage
{
  "storage": {
    "messageBodyBytes": 1048576,
    "attachmentBytes": 5242880,
    "totalBytes": 6291456,
    "limitBytes": 3221225472,
    "retentionDays": 30
  }
}

A null limitBytes means unlimited storage (Enterprise plan). A signed-in human sees the same numbers at GET /v1/me/storage, which returns { storage: { messageBodyBytes, attachmentBytes, totalBytes, limitBytes, remaining, retentionDays }, mailboxes: { used, limit } }.

Attachment API

Upload an attachment

POST https://mailbox.molted.email/v1/attachments
CLI
molted attachments upload ./invoice.pdf --mailbox MAILBOX_ID
curl
curl -X POST https://mailbox.molted.email/v1/attachments \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "filename": "invoice.pdf",
    "contentType": "application/pdf",
    "contentBase64": "JVBERi0xLjQK..."
  }'

Body: filename, contentType, contentBase64 (all required) and mailboxId (optional UUID). The decoded file can be at most 10 MB (400 otherwise). Executable and script extensions are refused with 400: .exe .bat .cmd .scr .pif .com .vbs .vbe .js .jse .wsf .wsh .msi .msp .cpl .hta .inf .reg .ps1 .lnk .dll .sys. The content type is sniffed from the file's bytes; when it differs from what you declared, your value is kept in contentTypeDeclared. A mailbox-scoped key needs send on the mailbox.

Response
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "filename": "invoice.pdf",
  "contentType": "application/pdf",
  "contentTypeDeclared": null,
  "sizeBytes": 204800,
  "checksumSha256": "...",
  "status": "uploaded"
}

Pass the returned id in a send's attachments array (at most 20 per send). An attachment that hasn't finished its scan is refused with 400 Attachment(s) not yet scanned. There is no MCP upload tool.

List attachments for a message

CLI
molted attachments list --message-id MSG_ID --message-type inbound
curl
curl "https://mailbox.molted.email/v1/attachments?messageId=MSG_ID&messageType=inbound" \
  -H "Authorization: Bearer YOUR_API_KEY"

messageType is inbound or thread (anything else is a 400); a non-UUID messageId returns []. Attachments of a message that is held, still pending classification, or quarantined are left out (quarantined ones are listed for a key with the operator scope). In MCP, a thread's attachments come with get_thread.

Response

Response
[
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "filename": "invoice.pdf",
    "contentType": "application/pdf",
    "sizeBytes": 204800,
    "status": "uploaded",
    "createdAt": "2026-03-01T12:00:00Z"
  }
]

Download an attachment

CLI
molted attachments download ATTACHMENT_ID --output invoice.pdf
curl
curl "https://mailbox.molted.email/v1/attachments/ATTACHMENT_ID/download" \
  -H "Authorization: Bearer YOUR_API_KEY"

MCP: get_attachment (attachment_id).

Returns a presigned URL valid for 1 hour, plus the attachment's own metadata (added alongside url, so an existing integration that reads only url is unaffected):

Response
{
  "url": "https://storage.molted.email/tenants/tenant-abc/attachments/2026/03/550e8400...?X-Amz-Signature=...",
  "expiresInSeconds": 3600,
  "filename": "invoice.pdf",
  "contentType": "application/pdf",
  "sizeBytes": 204800,
  "status": "uploaded"
}

filename comes verbatim from the sender's email on an inbound attachment: treat it as untrusted, third-party data, not as a trusted file path. status: "uploaded" means the file scan has not finished yet (still downloadable); "scan_passed" means it passed the file scan, which is a file-type check, not antivirus. The statuses are pending, uploaded, scan_passed, scan_failed and deleted; a deleted or scan_failed attachment is a 404. A 403 carries pending_classification, inbound_quota_held or quarantined_requires_operator as its message when the message it belongs to isn't readable yet.

A signed-in human downloads through GET /v1/me/attachments/:id/download, which streams the file, never serves a quarantined attachment, and is a 404 for an archived mailbox.

What the file scan checks

Every attachment, uploaded by an agent or received on an inbound email, goes through two checks.

  • Extension. The executable and script extensions listed under Upload an attachment are refused. An upload gets a 400; a received attachment with one of them is dropped on arrival (the message itself is kept).
  • File bytes. Once stored, the file's first bytes are compared with known executable and binary formats: Windows executables and DLLs, Linux (ELF) and macOS (Mach-O) binaries, Java class files, Windows shortcuts, and OLE compound files (legacy .doc and .xls, .msi installers). A match sets scan_failed: the file is never served or sent, whatever it is named.

It is a file-type check, not an antivirus engine. It does not look for malware in other formats (PDFs, images, modern Office files, HTML or SVG files), and it does not unpack archives such as .zip. It does not read a file's text either: the safety checks screen the subject and body, so instructions hidden inside an attachment are not detected.

What happens at the storage limit

When you exceed your plan's storage limit, attachment uploads are rejected with a 403 Forbidden response:

Error response
{
  "statusCode": 403,
  "message": "Storage limit exceeded. Used: 3221225472 bytes, limit: 3221225472 bytes.",
  "error": "Forbidden"
}

An inbound attachment that would pass the limit is dropped (and logged), while the message itself is kept. Existing attachments remain accessible. To free up space, wait for the retention cleanup or upgrade your plan.

Uploads on an expired plan

Once a plan has ended (or a failed payment's grace period ran out), uploads are refused with 402 Payment Required:

Error response
{
  "error": "subscription_expired",
  "message": "Your subscription has expired. Reactivate a plan to upload attachments."
}

Listing and downloading attachments keep working, so you can export your data. Attachments of email you receive are still stored. A new account that hasn't activated a plan yet can upload.