Skip to content
On this site

Receiving email

You do not poll for mail. A webhook fires, you read the message it names, and you delete it when you are done. This page is that loop.

The loop

Subscribe a webhook to email.received, and everything else follows from the payload it delivers.

// 1. verify the webhook, as with every Rasket event
const event = verifyWebhook(rawBody, headers, process.env.RASKET_WEBHOOK_SECRET);
if (event.type !== "email.received") return;

// 2. route on what the payload already carries — no call needed
const queue = event.data.received_for.includes("billing@inbound.acme.example")
  ? "billing"
  : "support";

// 3. read the parts the payload deliberately leaves out
const { data: message } = await rasket.emails.receiving.get(event.data.email_id);
const { data: files } = await rasket.emails.receiving.attachments.list(event.data.email_id);

// 4. each part carries a fresh signed link, good for fifteen minutes
for (const file of files.data) {
  if (file.download_url === undefined) continue; // a part we would not store
  await archive(file.filename, await fetch(file.download_url));
}

// 5. give the storage back
await rasket.emails.receiving.remove(event.data.email_id);

Why the payload is small

The event carries the envelope and the attachment list, and nothing else. That is a decision rather than an omission, and each half of it has a reason worth knowing:

  • No bodies. A webhook is retried until your endpoint accepts it. A body that was retried for six hours is six hours of duplicated content arriving at your endpoint, for a message you may not even want.
  • No download links. A link lives fifteen minutes. One minted at delivery would be dead by the time a retry succeeded, which is a worse failure than no link at all — it looks like it should work.
  • Enough to route. received_for, from, subject and the attachment list are all there, so the common case needs no call.

Route on received_for — the addresses of yours the message was accepted for — and not on to, which is whatever the sender wrote in the header.

Before the loop can run

Be reachable. There are two ways, and they differ only in which address people write to; everything downstream is identical.

  • The address you already have. Every team is assigned a managed receiving address — anything@<word>.<our host> — on first visit to the Receiving screen. Anything before the @ reaches you, so you can tag threads without registering anything.
  • Your own domain. Turn receiving on for a verified domain in an inbound-capable region and publish the one MX record we hand back. It sits at the inbound label unless you choose otherwise, so anything@inbound.acme.example lands in the same place. Set receiving_host to another label, or to @ for anything@acme.example itself — but an MX at @ takes the domain's mail from whichever mailbox provider has it today, so choose it only for a domain whose every address should reach us.

Receiving is available in two regions. A verified domain outside them cannot receive at all, and its domain page says so in place of an address rather than offering a toggle that would do nothing.

A message larger than 150 KB, attachments included, never enters the loop: the sending server is refused before the message reaches us, so there is no event and no record. We do not hold it, and we do not truncate it.

When the loop gets something odd

A message we accepted but would not store is still recorded, with its bodies null and no attachments, so that "nothing arrived" and "something arrived and we would not keep it" are never the same silence.

Unusual received messages
You seeIt means
dropped_reason: "virus"Scanned and refused. The envelope is kept, the content is not.
dropped_reason: "storage_quota"Your inbound storage was full. Delete some mail, or raise the cap.
dropped_reason: "daily_cap"You had already received your plan's messages for the day.
dropped_reason: "parse_failed"We could not read the MIME structure. The raw source is still downloadable.
404Retention removed the message, or it was never this team's. The two are deliberately indistinguishable.

Storage, and getting it back

Received mail counts against your plan's inbound storage, and there is a daily message cap as well. Over either, new messages are recorded with a dropped_reason rather than stored — so the loop keeps running and you find out from the payload.

DELETE /emails/receiving/{email_id} is the only way to free inbound storage before retention does. It removes the stored objects as well as the record and returns the bytes to your quota; there is no undo and no soft delete. Retention removes what you leave behind, whole — a message past it answers 404 rather than coming back with an empty body.

Where to go next

  • The Receiving reference — all seven operations, with the shape of every response.
  • Events — the email.received payload in full, beside every other event.