MRS
Receive mail on a domain and deliver it to your own services.
MRS (Mail Receiving Service) accepts inbound email for a domain you control. Point the domain's MX record at AnythingCloud, and MRS resolves each recipient, checks the sender's authentication, scores spam, and sends the message to the destinations you choose. Sending stays on AMS; a domain can send, receive, both, or neither.
Overview
MRS is built into AnythingCloud and uses the same account, verified domains, Workers, queues, and balance as the rest of the platform. The pieces are:
| Piece | What it is |
|---|---|
| Inbound domain | A receiving hostname under a verified root, such as inbox.example.com. It is separate from an AMS sending domain. |
| Shared receiver | mx.anything-cloud.com accepts SMTP for every member domain. Members publish one MX record; there is no per-domain server. |
| Route | An exact address such as [email protected] or a catch-all such as *@example.com. |
| Destination | A verified email mailbox, an ANS topic, an AQS queue, a Worker, an HTTPS webhook, or the built-in store. |
| Verified email | A mailbox confirmed by its owner. One verification works across every receiving domain on the account. |
| Spam policy | A per-domain switch, threshold, and action. Spam is never billed. |
MRS does not provide mailboxes, IMAP, or POP3. It receives a message, optionally stores a short-lived copy for debugging, and hands it to the destinations on the matching routes.
Get started
- Verify the root domain under Networking. Its status must be Verified. See Custom domains for the TXT record.
- Open MRS and choose Add domain. Select the verified root and enter a label. A subdomain such as
inbox.example.comis the safe default; using the apex changes the domain's existing MX and needs extra care. - Publish the MX record from the table below. Use the Cloudflare one-button flow if the root is connected there, or add the record manually at your DNS provider.
- Choose Verify. When the MX record resolves to
mx.anything-cloud.com, the domain becomes active and starts accepting mail. - Add a route for an exact address or a catch-all, and choose its destination. Send a test message to confirm the route and payload.
| Purpose | Type | Name | Value | Priority |
|---|---|---|---|---|
| Inbound MX | MX | <hostname> | mx.anything-cloud.com | 10 |
| DMARC (optional) | TXT | _dmarc.<hostname> | v=DMARC1; p=none; | — |
The MX name is the inbound domain itself, not a subdomain of it. Some DNS providers append the zone name automatically; copy the name and value from the console rather than retyping them. No SPF record is needed to receive mail, and MRS does not modify an existing SPF record.
DNS and Cloudflare
The domain page shows both setup paths together: a Cloudflare card for automatic records and a manual card with the exact MX and optional DMARC records to copy into any DNS provider. A Cloudflare API token lets MRS create the records for you. The token is stored encrypted and is never rendered again; the console shows only the connection name.
Connect Cloudflare
Create a scoped API token in the Cloudflare dashboard with exactly these permissions:
Zone:Zone:Read— find the zone for the verified root.Zone:DNS:Edit— create or update the MX record and the optional DMARC record.
Paste the token in MRS and connect it. MRS validates it with
GET /client/v4/user/tokens/verify and stores the sealed token.
When you apply records for a domain, MRS resolves the zone for the verified
root using the longest matching zone suffix and caches that mapping. A
token that cannot be verified is rejected before it is saved; the root's
zone must be visible to the token when you apply the records.
One-button DNS
If no Cloudflare connection exists, paste the token and choose Connect and create records; MRS validates the token and creates the records in one step. If a connection already exists, choose Create DNS records. MRS lists the existing MX records at the hostname before changing anything.
- If an MX record that does not point at MRS already exists and replacement is not confirmed, MRS reports the conflict, shows the existing records, and makes no change.
- After you explicitly confirm replacement, MRS removes the other MX records at that name and creates or updates the MRS MX record at priority 10.
- If the optional DMARC checkbox is on and no
_dmarc.<hostname>record exists, MRS createsv=DMARC1; p=none;. An existing DMARC record is left alone.
Existing mail records are never deleted silently. Confirm replacement only when you intend to move all mail for that hostname to MRS.
Apex domains
At the apex, the MX record controls all mail for the root domain.
Replacing it can stop delivery to existing mailboxes and forwarding rules.
Use a subdomain such as inbox.example.com unless you
deliberately want the apex itself to receive through MRS. MRS will not
replace an existing MX record until you confirm the replacement.
Routes and destinations
A route matches one address at an inbound domain and names one destination. Each domain can have exact routes and a catch-all.
Resolution order
MRS tries the exact local part first, then *. Every enabled
route at the winning specificity receives a copy, so create one route per
destination. A message that matches no route is accepted and counted as
received, but no destination receives it; its metadata is still dropped
after the retention window. Add a catch-all store route when you want to
keep mail that would otherwise go unrouted.
Destination kinds
| Kind | What happens | Durability |
|---|---|---|
| Forward | Sends a copy to a verified mailbox through AMS. The original sender becomes the Reply-To address. | Queued through the AMS delivery pipeline. |
| ANS topic | Publishes an email.received event with source mrs. The email object is the event data. | Durable fan-out to the topic's subscriptions. |
| AQS queue | Publishes the email object as a JSON message to the selected queue. | Durable. The recommended path when every message must be retained. |
| Worker | Invokes the Worker's existing event() handler with the email object. | Best effort. Use AQS when delivery must not be lost. |
| Webhook | POSTs the email object to an HTTPS endpoint with X-MRS-Timestamp, X-MRS-Signature, X-MRS-Message-Id, and X-MRS-Route headers. | Single attempt. Use ANS for retries and dead letters. |
| Store | Keeps message metadata and the parsed body for 30 days. | Debugging only. It is not a mailbox and does not accept replies. |
Destinations are owned by the same account. An ANS topic, AQS queue, or Worker must already exist before it can be selected. Webhook URLs must use HTTPS and a public address; MRS does not follow redirects and gives the endpoint 10 seconds to respond. Webhook requests are signed with HMAC-SHA256 over the timestamp and the request body.
Worker events
A Worker destination calls the Worker's event() handler. MRS
posts {"slug": "...", "payload": {"kind": "event", "source": "mrs",
"email": {...}}} to the node's /__cloud_event endpoint.
The handler receives that payload object, so the email is the request
body's payload.email field, exposed as event.email.
There is no separate email() entrypoint.
export default {
async event(event) {
if (event.source !== "mrs") return;
const email = event.email;
console.log(email.subject, email.envelope.to);
},
};
Worker invocation is a best-effort HTTP call. A failure is recorded on the delivery but does not reject the SMTP message. Use an AQS destination when the Worker must be able to catch up after downtime.
Email payload
Every destination receives the same normalized email object. It contains envelope addresses, the parsed headers, text and HTML bodies, attachment metadata, authentication results, the spam score, and the route that matched.
{
"id": "mrs_...",
"type": "mrs.email",
"received_at": "2026-11-06T12:00:00Z",
"envelope": {"from": "[email protected]", "to": "[email protected]"},
"from": {"address": "[email protected]", "name": "Sender"},
"to": [{"address": "[email protected]", "name": null}],
"subject": "Hello",
"message_id": "<...>",
"date": "...",
"headers": {"from": "...", "to": "...", "subject": "..."},
"text": "...",
"html": "...",
"raw_size": 12345,
"truncated": false,
"attachments": [
{
"filename": "a.pdf",
"content_type": "application/pdf",
"size": 1234,
"content": "JVBERi0xLjQK...",
"content_available": true
}
],
"auth": {"spf": "pass", "dkim": "pass", "dmarc": "pass"},
"spam": {"score": 0.0, "reasons": []},
"route": {"pattern": "support", "destination": "queue"}
}
Attachment content is included as standard base64 in
content when it fits the destination. If a destination is
too small, the attachment metadata is kept and the entry is marked
content_truncated: true. Large bodies are trimmed to fit the
ANS topic's max_message_bytes, 900 KB for AQS, or 500 KB for a
Worker event; truncated is set on the email object when that
happens.
Attachments
Add a store route when you want message bodies and
attachments kept for the 30-day retention window. Stored attachments are
listed on the message page in Activity with a download link, and the
member API returns their metadata and optional base64 content. Messages
without a store route still forward attachment content to destinations
when it fits, but nothing is retained after delivery.
The receiver forwards up to 20 attachments, 5 MB per file and 10 MB in total. Larger files keep their filename, content type, and size, and are marked as truncated. Sending attachments through AMS uses the same base64 shape; see the AMS sending API.
Idempotency and delivery records
Each message has one id. ANS and AQS publish with an idempotency key
built from the message id and route id, so a message routed to two
destinations is delivered once per route. A message with two destinations
is still one accepted message for billing. The AQS key is
mrs:<message_id>:<route_id>.
Every (message, route) pair gets its own delivery record with
a status, attempt count, and last error. ANS, AQS, and webhook delivery run
during ingest; a destination failure is recorded and does not reject the
SMTP message. Deduplicate in your consumer on email.id; the
webhook and Worker paths are single attempts, while ANS and AQS provide the
durable, at-least-once path.
Forwarding
Forwarding sends a copy of an accepted message to a mailbox you own. Open Verified emails to add a mailbox, then use Forwarding to create the routes. The flow is deliberately small:
- Choose Add email and enter the mailbox.
- We email a verification link to that mailbox. Open it and choose Verify email address. The link confirms the mailbox, so it works without signing in first.
- Open Forwarding and choose Create forwarding route. Pick an address to forward, the receiving domain, and the verified mailbox. Use Add verified email in the same dialog if the mailbox is not listed yet. A route is created immediately.
Verified emails belong to the account, so one verified mailbox can serve every receiving domain. A pending address cannot be selected until it is confirmed, and an address used by a route cannot be removed until those routes are removed. Verification links expire after 24 hours; the resend action sends a fresh one.
Sending identity
Forwarded mail is sent through AMS from the member's own sending domain.
Once a receiving domain has an active AMS sending domain, the console
creates forward@<sending domain> automatically. If no
sending domain is active, the Forwarding page shows a
Setup needed card. With a connected Cloudflare account,
Set up sending creates the DKIM, Return-Path SPF,
Return-Path MX, and optional DMARC records and checks them immediately.
Without a DNS connection, copy the listed records into the provider and
choose Finish setup.
A receiving domain does not need its own AMS application. Forwarding uses the AMS delivery queue and does not count as an AMS accepted message, so it does not consume the AMS monthly allowance or appear in AMS metrics.
What the recipient sees
The forwarded message keeps the original subject and body. Its
From is forward@<sending domain> so DKIM
and DMARC align with a domain the account controls, and its
Reply-To is the original sender. An
Auto-Submitted: auto-forwarded marker and an
X-MRS-Forward-Count header make loops detectable. The
original sender, date, subject, and recipient are repeated at the top of
the forwarded copy. Attachments are included when they fit the AMS send
limits; otherwise the copy notes how many were omitted.
Spam filtering still applies first. Spam is quarantined before it can be forwarded unless the domain's spam policy explicitly tags and delivers it. Forwarding also has a hard safety score cap (10.0 by default) that applies even when the domain filter is off, so a receiving domain cannot be used as an unrestricted outbound relay. Allowlisted senders bypass the cap. A message that returns through forwarding is quarantined as a loop and is not billed.
A forwarding delivery row means the copy entered the AMS delivery queue. AMS performs the final SMTP delivery and its own retries; a later remote bounce is not reflected back on the MRS delivery row.
Spam filtering
Spam filtering is enabled per domain by default. Authentication, DNSBL lookups, and conservative structural checks score every message. The score and its reasons are recorded so a decision can be explained. There is no content keyword blocking in this release.
Filtering strength
Choose a preset in the domain's Spam tab, or enter a custom threshold from 0 to 20. Lower values catch more spam; higher values accept more mail.
| Preset | Threshold | Best for |
|---|---|---|
| Lenient | 7.0 | Domains that must not miss legitimate mail |
| Balanced | 5.0 | The default for most domains |
| Strict | 3.0 | Domains that receive a lot of unwanted mail |
What is scored
| Signal | Score |
|---|---|
| SPF fail | +2.5 |
| SPF softfail | +1.0 |
| DKIM fail (a checked, invalid signature) | +2.0 |
| DMARC fail | +2.0 |
| Sender address does not match the authenticated sender | +1.5 |
| DMARC fails and the sender's domain quarantines unauthenticated mail | +1.0 additional |
| DMARC fails and the sender's domain rejects unauthenticated mail | +3.0 additional |
| DNSBL hit (Spamhaus ZEN or SpamCop) | +3.0 each, capped at +6 |
Missing Message-ID | +0.5 |
Missing Date | +0.5 |
| Mostly uppercase subject (at least 8 characters) | +0.8 |
| More than 10 URLs in the text | +1.0 |
| HTML with a form or password input | +1.5 |
| Reply-To domain differs from the From domain | +0.5 |
| From display name contains an address on another domain | +1.0 |
HTML contains a javascript: or data:text/html link | +1.0 |
| HTML has no text and at least one image | +0.5 |
More than 8 Received headers | +0.5 |
A message with no DKIM signature is not treated as a failure. A signature that cannot be checked because of a DNS or temporary lookup problem is also not scored. An unknown DNSBL result never blocks mail. DNS lookups have short timeouts, so a slow resolver does not hold up the SMTP transaction.
MRS also compares the From address with the authenticated sender. A different envelope domain is normal for an email provider when DKIM or DMARC aligns the From identity, so it is left alone. A sender domain that rejects unauthenticated mail adds a strong signal when DMARC fails; a quarantine policy adds a smaller one. The display-name and Reply-To checks stay in place.
Sender rules
Allow or block a specific sender address ([email protected])
or a whole domain (example.com). A domain rule also covers its
subdomains. Each domain can have up to 100 rules.
- Allow: skip the spam decision and route the message
normally. The message is billable and its reasons include
Sender is allowlisted. - Block: always treat the sender as spam, even when the
filter is off. Blocked mail is quarantined unless the domain action is
Reject; it is never tagged and delivered. It is not billed and its reasons
include
Sender is blocklisted.
Matching tries the exact address first, then the exact domain, then the longest subdomain suffix. At the same level, a block rule wins over an allow rule.
Threshold and actions
A message at or above the threshold is spam. The domain's action decides what happens next:
- Quarantine (default): accept at SMTP, store the metadata and score, count the message as spam, do not route it, and do not bill it.
- Tag: route normally and add
X-MRS-Spam: yesandX-MRS-Spam-Scoreheaders for the destination. - Reject: return
550 5.7.1to the sending server and record the rejection. - Filter off: route every message and record the score, but treat the message as non-spam. This is an explicit choice and the message is billable. Sender block rules still apply.
Quarantined and rejected spam never counts toward the free allowance or the paid count. Raising the threshold accepts more mail as legitimate; lowering it catches more as spam. Use the tag action first when tuning a domain. The Spam tab and the Metrics page show the most common spam reasons for the selected window.
Spam reporting
This release has no spam-report action or report table. When managed organization inboxes ship, reports will attach to the retained message id, authentication result, and score, and will feed sender reputation. The current message record and per-domain sender rules are the extension point.
Metrics
The MRS Metrics page and each domain's Metrics tab show daily counts for the selected period, with per-domain and per-day breakdowns:
| Counter | Meaning |
|---|---|
received | Messages evaluated by MRS for the account or domain. |
accepted | Non-spam messages. Each message counts once, no matter how many destinations it reaches. |
spam | Messages classified as spam. Each message counts once. |
routed | Successful destination deliveries. Two destinations count twice. |
failed | Destination deliveries that did not complete. |
SPF, DKIM, and DMARC pass rates are reported beside the message counters.
A message counts once in received and once in exactly one of
accepted or spam. Message metadata is kept for 30
days; daily metrics are kept for 400 days so a full billing period is
always available.
Billing
Email receiving is billed from the Cloud account balance and appears as Email receiving in the monthly service breakdown. The first 10,000 non-spam messages in a calendar month are free. Above that, every non-spam message in that month is billed at $0.09 per 1,000, applied to the whole month, not only the messages over the allowance.
| Item | Value |
|---|---|
| Free allowance | 10,000 non-spam messages per account per calendar month |
| Rate above the allowance | $0.09 per 1,000 non-spam messages |
| Charged count | All non-spam messages in the month once the allowance is exceeded |
| Spam | Never counted, never charged |
| Deposit requirement | None; any active Cloud account can use the allowance |
A message counts once when it is accepted as non-spam, regardless of how many routes receive it. Delivery failures do not change the accepted count. Examples for one month:
- 9,999 non-spam messages: $0.00.
- 10,001 non-spam messages: 10,001 × $0.00009 = $0.90.
- 100,000 non-spam messages: 100,000 × $0.00009 = $9.00.
- 100,000 non-spam plus 50,000 spam: still $9.00.
The console shows the current month's usage on the MRS overview. Rates and the monthly total are also on Billing and metrics and Cloud → Billing.
Limits
| Limit | Value |
|---|---|
| Raw message size | 25 MiB (26,214,400 bytes) |
| Recipients per message | 10 |
| Inbound domains | 10 per account |
| Routes | 20 per domain |
| Sender rules | 100 per domain |
| Forwarding addresses | 20 per account; verification links expire after 24 hours |
| Forwarded messages | 2,000 per account in a rolling 24 hours; hard safety cap at spam score 10.0 |
| Forwarding attachments | The AMS send limits: 5 files, 400 KB each, 600 KB total |
| ANS payload | The selected topic's max_message_bytes; 256 KiB by default |
| AQS payload | 900 KB |
| Worker event payload | 500 KB |
| Attachments per message | 20 |
| Attachment size | 5 MB each, 10 MB total forwarded or stored |
| Message metadata | 30 days |
| Daily metrics | 400 days |
The receiver enforces the raw size and recipient count before reading the
whole message. A message over either limit is rejected at SMTP. Payload
limits apply after parsing and trimming; the email object's
truncated flag says whether the body was shortened.
Internal API contract
These endpoints are the contract between the MRS SMTP receiver and the
control plane. They are not part of the member API and should not be called
from applications. The receiver authenticates with a
mrs_worker_ bearer token created from
Admin → Cloud → Email Receiving Worker, which
is available to administrators. Tokens are hashed at rest, limited to 10 active tokens,
and revocable.
| Method and path | Purpose |
|---|---|
GET /internal/mrs/health | Public health check. Returns {"ok": true}. |
POST /internal/mrs/recipients | Resolve addresses and return the domain's spam policy. No route details or secrets. |
POST /internal/mrs/messages | Ingest one normalized message event and return the SMTP decision and delivery results. |
POST /internal/mrs/events | Batch variant for future senders, up to 25 events. The current receiver sends one event per recipient. |
Recipient lookup
The receiver sends the envelope recipients before it reads the message, up to 20 addresses per lookup. The response says which addresses are deliverable and carries the domain's spam settings. Results are cached for 30 to 60 seconds, so a route added just after a lookup can take up to a minute to accept mail.
POST /internal/mrs/recipients
Authorization: Bearer mrs_worker_...
Content-Type: application/json
{"addresses": ["[email protected]", "[email protected]"]}
An unknown or inactive domain or route is rejected at SMTP with
550 5.1.1. A paused domain or an account over its limit is
rejected with 451 so the sender retries later. If the control
plane is unavailable, the receiver writes the event to a bounded local
spool and retries with backoff; a full spool returns a temporary
452.
Message ingest
The receiver parses MIME, evaluates SPF, DKIM, DMARC, DNSBL, and the heuristics, then posts one normalized event per recipient. The event uses the same email object shown under Email payload.
{
"id": "mrs_...",
"type": "mrs.email",
"envelope": {"from": "[email protected]", "to": "[email protected]"},
"from": {"address": "[email protected]", "name": "Sender"},
"to": [{"address": "[email protected]", "name": null}],
"subject": "Hello",
"message_id": "<...>",
"auth": {"spf": "pass", "dkim": "pass", "dmarc": "pass"},
"spam": {"score": 0.0, "reasons": []}
}
The response tells the receiver whether to accept, quarantine-accept, or reject the message, and includes the SMTP response for a rejection:
{
"accept": true,
"message_id": "mrs_...",
"status": "routed",
"deliveries": [
{"route_id": 12, "destination_kind": "queue", "status": "delivered"}
],
"smtp_code": 250,
"smtp_message": "2.0.0 Message accepted"
}
Ingest is idempotent on the message id: a repeated post returns the original decision instead of delivering again. A message that is accepted for one recipient does not roll back the other recipients; the receiver splits a multi-recipient message into one event per recipient so each has one owner, one route set, and one billing unit.
Member API
Manage receiving domains, routes, spam settings, sender allow/block rules, Cloudflare connections, messages, and metrics with a personal API key. The full endpoint list is in the API reference.
curl -X POST https://anything.solutions/api/v1/mrs/domains \
-H "Authorization: Bearer $ANYTHING_API_KEY" \
-H "Content-Type: application/json" \
-d '{"root_id": 1, "label": "inbox"}'
Add a route with destination_kind and
destination_config:
{
"pattern": "support",
"destination_kind": "queue",
"destination_config": {"queue_id": 12},
"enabled": true,
"priority": 0
}
GET /api/v1/mrs/messages/{id}?include_body=1 returns the
stored text, HTML, and base64 attachment content when available.
GET /api/v1/mrs/messages/{id}/attachments/{attachment_id}
downloads one file.
Roadmap
- Managed organization inbox. A later destination adapter will add a hosted inbox on the same route and message model.
- Multi-region. The control-plane contract is region-neutral. The first release runs one receiver on the existing inbound host.