Docs
Rules
Typed conditions and actions that run on every message, with a written order of evaluation.
A rule is read by the platform as mail arrives and does one thing or several: label a message, put it in a folder, forward it to a verified address, answer it once, or refuse it. Conditions and actions are a closed and typed vocabulary, never a script: an unknown field or an unknown action is a 400 validation_error, not something silently ignored. There is no user language to execute, so the outcome of a rule is the same every time.
The order of evaluation
- The rules whose target covers the message are collected: one inbox, every inbox of a domain, or the organization. A disabled rule is not collected.
- They are all evaluated, in
priorityascending and then by age. No rule wins and stops the others: the consequences accumulate. - Metadata actions come first (
add_label,remove_label,mark_read), then placement (archive,move_to_folder), then the actions with external effects (forward_to,auto_reply,webhook,assign). rejectis not a consequence: it is an outcome. If any evaluated rule rejects, the message is refused with550and the reason of the most specific rejecting rule, and every other consequence is discarded.- A conflict of placement goes to the more specific rule (inbox over domain over organization), and at equal specificity to the older one. Losing a conflict does not cancel the other actions of that rule.
- Actions are idempotent: the same label from two rules is one label, and marking an already read message read changes nothing.
The spam guard
A rule never sees a message with the spam verdict unless it sets include_spam: true. The guard is applied before the conditions are read, so no rule written without it can reach spam, which is the structural version of the anti-spam preamble every filter carries elsewhere. spam_verdict inside the conditions is how a rule asks for one verdict or another once it is allowed to look.
The guards that are checked when the rule is written
forward_toonly accepts an address that is one of your inboxes or on a domain you verified. Anything else is a422 unverified_forward_addressat creation, not silence at three in the morning.- A message that already carries
Auto-Submitted: auto-forwardedis never forwarded again, and a forward adds that header, so two hops cannot form a ring. auto_replynever answers an automatic message (Auto-Submitted), a delivery report, a complaint or a bulk message, and it answers a given sender at most once every 24 hours per inbox. The answers carryAuto-Submitted: auto-replied(RFC 3834).- A
rejectreason must be one line and must not contain an address: it leaves in the SMTP reply, and it must not confirm that an address exists. A rejection is final and produces no bounce. - A
webhookaction must name a live webhook of your organization. It emitsrule.matchedafter the inbox transaction commits, with the same signature as every other delivery, and it is a no-op recorded in the audit log when the endpoint has been disabled.
Folders are labels
There is one taxonomy in this product: a label. A folder is a label, and the archive is the archived label, so move_to_folder and archive move a message in the same way search finds it with label:.
The automatic answer of an inbox, with a schedule
PUT /v0/inboxes/{inbox}/auto-reply configures the automatic answer of one inbox in one of five forms, each with an explicit IANA timezone (the server's clock is never used): fixed with two RFC 3339 instants, daily with two HH:MM clock times, weekly with two clock times and weekday tokens in days (mon..sun), monthly with two clock times and day-of-month numbers in days, and permanent with no window at all. An end before its start wraps past midnight and belongs to the day it started on.
The configuration is one ordinary rule of the engine: target the inbox, condition to the address, action auto_reply. That is what makes the guards the same ones: no answer to a message carrying Auto-Submitted, to a delivery report (multipart/report, which is also what an ARF complaint is) or to bulk mail, at most one answer per sender per inbox every 24 hours, and the answer queued like any other send so it pays the plan limits. The answer carries Auto-Submitted: auto-replied (RFC 3834) and rule_id in the response is the rule it created, visible in GET /v0/rules. restrict_to_domain answers only senders on the inbox's own domain or on a domain the organization verified.
Block and allow lists
POST /v0/inboxes/{inbox}/blocklist and POST /v0/organization/blocklist take {kind: sender|domain, value} plus an optional list_kind (block by default, or allow). The list of one inbox is inbox_write; the organization wide list is its own permission, org_blocklist_write, because one entry there covers every inbox.
- They are evaluated in the ingest before any rule runs, so a blocked sender never reaches a rule that would label, forward or answer the message.
allowbeatsblock: an allowed address passes through a blocked domain, which is the reason to keep an allow list.- A refused sender gets
550with a fixed sentence that names no address, and nothing of the message is stored. Adomainentry matches that domain exactly, never a suffix that merely ends the same way. DELETEnames the entry in the query string (kind,value, andlist_kindwhen it is notblock). Removing an entry that is not there is a404, so a typo is not a silent no-op.
Applying a rule to the messages that already exist
POST /v0/rules/{rule}/apply is an asynchronous job: the answer is 202 with a report, and the mailbox is walked in batches. A rule that is switched off is refused with 409 rule_disabled, because applying it would be the one way to make a disabled rule act. The past is treated as if the rule had existed, so labels, placement and ticket assignment are applied, while forward_to, auto_reply and webhook are skipped and counted: nobody receives a forward for a rule written today. The job is repeatable and never duplicates a label: a second run reports already_applied.
Permissions and the feature flag
Reading needs rule_read; creating, changing, deleting and applying need rule_write, which is separate from message_update because applying a rule rewrites the mailbox it covers. The engine is switched per organization by the platform operator, and MAI_RULES_ENABLED is the deployment's kill switch: both have to be on. With either off every route under /v0/rules answers 501 rules_disabled, which is a feature switched off and not a fault, and no rule runs on the organization's mail.
Endpoints
GET/v0/rules
List the rules of the organization (requires rule_read)
Parameters
| Name | Type | Notes |
|---|---|---|
target | inbox | domain | org | in query, Only rules of this kind |
target_id | string (uuid) | in query, Only rules of this inbox or domain |
enabled | boolean | in query |
Responses
200OK: object403Error envelope: Error501Error envelope: Error
POST/v0/rules
Create a rule (requires rule_write, audited)
Body (RuleInput)
| Name | Type | Notes |
|---|---|---|
name | string | required, max length 100 |
target | RuleTarget | |
priority | integer | min 0, max 100000, default 100 |
enabled | boolean | default true |
include_spam | boolean | default false |
conditions | RuleConditions | required |
actions | RuleAction[] | required, at least 1 item, at most 20 items |
Responses
201Created: Rule400Error envelope: Error403Error envelope: Error404Error envelope: Error422Error envelope: Error429Error envelope: Error501Error envelope: Error
GET/v0/rules/{rule}
Read one rule (requires rule_read)
Parameters
| Name | Type | Notes |
|---|---|---|
rule | string (uuid) | in path, required |
Responses
200OK: Rule403Error envelope: Error404Error envelope: Error501Error envelope: Error
PATCH/v0/rules/{rule}
Change a rule, or switch it off (requires rule_write, audited)
Parameters
| Name | Type | Notes |
|---|---|---|
rule | string (uuid) | in path, required |
Body (RulePatch)
| Name | Type | Notes |
|---|---|---|
name | string | max length 100 |
target | RuleTarget | |
priority | integer | min 0, max 100000 |
enabled | boolean | |
include_spam | boolean | |
conditions | RuleConditions | |
actions | RuleAction[] | at least 1 item, at most 20 items |
Responses
200OK: Rule400Error envelope: Error403Error envelope: Error404Error envelope: Error422Error envelope: Error501Error envelope: Error
DELETE/v0/rules/{rule}
Delete a rule and any apply job queued for it (requires rule_write, audited)
Parameters
| Name | Type | Notes |
|---|---|---|
rule | string (uuid) | in path, required |
Responses
204Deleted403Error envelope: Error404Error envelope: Error501Error envelope: Error
POST/v0/rules/{rule}/apply
Apply a rule to the messages that already exist (requires rule_write, audited)
Parameters
| Name | Type | Notes |
|---|---|---|
rule | string (uuid) | in path, required |
Responses
202Queued: RuleApplyReport403Error envelope: Error404Error envelope: Error409Error envelope: Error501Error envelope: Error
GET/v0/rules/applies/{apply}
The state of one apply job (requires rule_read)
Parameters
| Name | Type | Notes |
|---|---|---|
apply | string (uuid) | in path, required |
Responses
200OK: RuleApplyReport403Error envelope: Error404Error envelope: Error501Error envelope: Error
Rule object
| Name | Type | Notes |
|---|---|---|
rule_id | string (uuid) | |
name | string | |
target | RuleTarget | |
priority | integer | Lower is evaluated first; ties go to the older rule |
enabled | boolean | |
include_spam | boolean | False means the rule never sees a message with the spam verdict |
conditions | RuleConditions | |
actions | RuleAction[] | at most 20 items |
created_at | string (date-time) | |
updated_at | string (date-time) |
RuleInput object
| Name | Type | Notes |
|---|---|---|
name | string | required, max length 100 |
target | RuleTarget | |
priority | integer | min 0, max 100000, default 100 |
enabled | boolean | default true |
include_spam | boolean | default false |
conditions | RuleConditions | required |
actions | RuleAction[] | required, at least 1 item, at most 20 items |
RulePatch object
| Name | Type | Notes |
|---|---|---|
name | string | max length 100 |
target | RuleTarget | |
priority | integer | min 0, max 100000 |
enabled | boolean | |
include_spam | boolean | |
conditions | RuleConditions | |
actions | RuleAction[] | at least 1 item, at most 20 items |
RuleConditions object
| Name | Type | Notes |
|---|---|---|
from | string[] | Sender, case and accent insensitive substring, ORed, at most 32 items |
to | string[] | Recipient (To or Cc), ORed, at most 32 items |
subject | string[] | Subject substring, ORed, at most 32 items |
query | string | Full text, same parser as search, max length 512 |
negated_query | string | The message must not match this, max length 512 |
has_attachment | boolean | |
size | RuleSize | |
spam_verdict | clean | review | spam[] | at most 3 items, one of clean, review, spam |
RuleAction object
| Name | Type | Notes |
|---|---|---|
type | add_label | remove_label | archive | move_to_folder | mark_read | forward_to | auto_reply | reject | webhook | assign | required |
label | string | add_label, remove_label, move_to_folder: the label, which is also the folder, max length 64 |
address | string (email) | forward_to: a verified address of the organization |
reason | string | reject: one line, no address, sent back in the 550, max length 200 |
webhook_id | string (uuid) | webhook: the endpoint that receives rule.matched |
subject | string | auto_reply: the subject of the answer |
text | string | auto_reply: the body of the answer, max length 4000 |
assignee | string (uuid) | assign: the member who takes the ticket |
RuleApplyReport object
| Name | Type | Notes |
|---|---|---|
job_id | string (uuid) | |
rule_id | string (uuid) | |
status | pending | running | done | |
matched | integer | |
applied | integer | |
skipped | object | |
created_at | string (date-time) | |
finished_at | string (date-time) |