Skip to content

Receiving

Mail to your address is accepted or permanently rejected at SMTP time. Accepted mail goes through deterministic checks before it is stored, so there is never a moment when a message is readable but not yet checked. No language model runs on the receive path.

  1. Size (up to 10 MiB), recipient address, inbox state and your receive block list. Unknown, retired, paused or unclaimed inboxes and blocked senders are rejected permanently.
  2. Per-sender rate limits (50 messages per hour per sender per inbox; 2,000 per hour per organization). Mail over the limit is accepted and held, not bounced.
  3. The raw message is stored first, then parsed.
  4. Duplicates (same Message-ID and same content) are dropped. Same Message-ID with different content is kept and flagged msgid_collision.
  5. Threading (see Threads).
  6. Classification: normal, spam, auto_reply (RFC 3834 headers, vacation replies, and loop markers on mail sent by SiMail agents), bounce (delivery and feedback reports), otp (verification-code mail) and system.
  7. Prompt-injection heuristics (instruction phrasing, hidden or zero-width text, CSS-hidden content, fake system prompts, long encoded blobs) produce an injection_score; at or above the threshold (0.7 by default; your policy can make it stricter) the message is held.
  8. A screen for regulated or privileged material holds matching messages for a human.
  9. Reply extraction produces extracted_text: the new part of a reply without quoted history (including common Chinese quote markers).
  10. Everything is committed at once, then events are published.

Optional, off by default: attachment text extraction and model-based injection scoring run after storage and can only add a hold, never release one.

{
"message_id": "msg_…",
"thread_id": "thr_…",
"labels": ["received", "unread"],
"timestamp": "2026-10-18T17:02:11Z",
"from": "Ada <ada@example.com>",
"to": ["shop-helper@simail.email"],
"cc": [],
"subject": "Where is my order?",
"preview": "Hi, I ordered last week and…",
"text": "Hi, I ordered last week and…",
"extracted_text": "Hi, I ordered last week and haven't heard back.",
"attachments": [{ "attachment_id": "att_…", "filename": "receipt.pdf", "size": 48213, "content_type": "application/pdf" }],
"in_reply_to": null,
"references": [],
"size": 61840,
"created_at": "2026-10-18T17:02:12Z",
"trust_envelope": {
"auth": { "spf": null, "dkim": null, "dmarc": null, "computed_by": "none" },
"sender_known": false,
"first_contact": true,
"classification": "normal",
"hold": false,
"hold_reasons": [],
"injection_score": 0.05,
"flags": [],
"untrusted": true
}
}
  • html is returned only to keys with message_raw_read (and to humans), and SiMail never renders it for you. Prefer extracted_text and text.
  • trust_envelope.auth.computed_by is "none" for now: SiMail does not yet verify DKIM itself, so do not treat the From address as authenticated.
  • sender_known is true when the sender is a contact of this inbox (someone you wrote to first, or someone a human promoted). Mail from other SiMail customers is treated like any stranger.
  • Reply-To is stored but never used to decide where replies go.

A held message (hold: true, label held) shows metadata only on every channel (REST, MCP, webhooks, events, WebSocket): message_id, thread_id, sender address, received time, size, labels and hold_reasons. Subject, body and attachments are withheld. Only a human can release or reject it in the console. POST …/messages/{message_id}/agent-armor/release returns 403 human_required.

hold_reasons explains why, for example rate_limited or unparseable, or a hold from the injection, one-time-code or sensitive-material checks.

Method and path Permission
GET /v0/inboxes/{inbox_id}/messages message_read
GET /v0/inboxes/{inbox_id}/messages/{message_id} message_read
PATCH /v0/inboxes/{inbox_id}/messages/{message_id} (labels) message_update
DELETE /v0/inboxes/{inbox_id}/messages/{message_id} (to trash; purged after 30 days) message_delete
POST /v0/inboxes/{inbox_id}/messages/batch-get, batch-update message_read, message_update
GET /v0/inboxes/{inbox_id}/messages/search?q= message_read
GET /v0/inboxes/{inbox_id}/messages/{message_id}/raw message_raw_read
GET /v0/inboxes/{inbox_id}/messages/{message_id}/attachments/{attachment_id} message_read

Query parameters: labels, before, after, include_spam, include_blocked, include_trash (each needs the matching label_*_read permission), limit, page_token, ascending.

Search uses full-text matching on subject, sender and body and works for English and Chinese. Held messages are searchable by metadata only.

Attachment and raw responses do not contain the file. They return a short-lived, single-use download link on the isolated content domain:

{ "attachment_id": "att_…", "filename": "receipt.pdf", "size": 48213, "content_type": "application/pdf", "download_url": "https://view.siagentmail.com/…", "expires_at": "2026-10-18T17:07:12Z" }

Links expire after 5 minutes and work once. Files are always served as downloads (Content-Disposition: attachment), never rendered.

Each message.received event carries data.wake. Mail from known threads, contacts or your receive allow list wakes your agent. Mail from strangers wakes it only up to a daily budget per inbox and an hourly budget per sender; beyond that, events are still recorded and available from GET /v0/events, but webhooks and WebSocket skip them unless you subscribe with include_non_wake: true. A daily inbox.digest event reports the counts. Held mail never wakes.