Docs

Rules

Typed conditions and actions that run on every message, with a written order of evaluation.

View as Markdown

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 priority ascending 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).
  • reject is not a consequence: it is an outcome. If any evaluated rule rejects, the message is refused with 550 and 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_to only accepts an address that is one of your inboxes or on a domain you verified. Anything else is a 422 unverified_forward_address at creation, not silence at three in the morning.
  • A message that already carries Auto-Submitted: auto-forwarded is never forwarded again, and a forward adds that header, so two hops cannot form a ring.
  • auto_reply never 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 carry Auto-Submitted: auto-replied (RFC 3834).
  • A reject reason 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 webhook action must name a live webhook of your organization. It emits rule.matched after 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.
  • allow beats block: an allowed address passes through a blocked domain, which is the reason to keep an allow list.
  • A refused sender gets 550 with a fixed sentence that names no address, and nothing of the message is stored. A domain entry matches that domain exactly, never a suffix that merely ends the same way.
  • DELETE names the entry in the query string (kind, value, and list_kind when it is not block). Removing an entry that is not there is a 404, 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

NameTypeNotes
targetinbox | domain | orgin query, Only rules of this kind
target_idstring (uuid)in query, Only rules of this inbox or domain
enabledbooleanin query

Responses

  • 200 OK: object
  • 403 Error envelope: Error
  • 501 Error envelope: Error

POST/v0/rules

Create a rule (requires rule_write, audited)

Body (RuleInput)

NameTypeNotes
namestringrequired, max length 100
targetRuleTarget
priorityintegermin 0, max 100000, default 100
enabledbooleandefault true
include_spambooleandefault false
conditionsRuleConditionsrequired
actionsRuleAction[]required, at least 1 item, at most 20 items

Responses

  • 201 Created: Rule
  • 400 Error envelope: Error
  • 403 Error envelope: Error
  • 404 Error envelope: Error
  • 422 Error envelope: Error
  • 429 Error envelope: Error
  • 501 Error envelope: Error

GET/v0/rules/{rule}

Read one rule (requires rule_read)

Parameters

NameTypeNotes
rulestring (uuid)in path, required

Responses

  • 200 OK: Rule
  • 403 Error envelope: Error
  • 404 Error envelope: Error
  • 501 Error envelope: Error

PATCH/v0/rules/{rule}

Change a rule, or switch it off (requires rule_write, audited)

Parameters

NameTypeNotes
rulestring (uuid)in path, required

Body (RulePatch)

NameTypeNotes
namestringmax length 100
targetRuleTarget
priorityintegermin 0, max 100000
enabledboolean
include_spamboolean
conditionsRuleConditions
actionsRuleAction[]at least 1 item, at most 20 items

Responses

  • 200 OK: Rule
  • 400 Error envelope: Error
  • 403 Error envelope: Error
  • 404 Error envelope: Error
  • 422 Error envelope: Error
  • 501 Error envelope: Error

DELETE/v0/rules/{rule}

Delete a rule and any apply job queued for it (requires rule_write, audited)

Parameters

NameTypeNotes
rulestring (uuid)in path, required

Responses

  • 204 Deleted
  • 403 Error envelope: Error
  • 404 Error envelope: Error
  • 501 Error envelope: Error

POST/v0/rules/{rule}/apply

Apply a rule to the messages that already exist (requires rule_write, audited)

Parameters

NameTypeNotes
rulestring (uuid)in path, required

Responses

  • 202 Queued: RuleApplyReport
  • 403 Error envelope: Error
  • 404 Error envelope: Error
  • 409 Error envelope: Error
  • 501 Error envelope: Error

GET/v0/rules/applies/{apply}

The state of one apply job (requires rule_read)

Parameters

NameTypeNotes
applystring (uuid)in path, required

Responses

  • 200 OK: RuleApplyReport
  • 403 Error envelope: Error
  • 404 Error envelope: Error
  • 501 Error envelope: Error

Rule object

NameTypeNotes
rule_idstring (uuid)
namestring
targetRuleTarget
priorityintegerLower is evaluated first; ties go to the older rule
enabledboolean
include_spambooleanFalse means the rule never sees a message with the spam verdict
conditionsRuleConditions
actionsRuleAction[]at most 20 items
created_atstring (date-time)
updated_atstring (date-time)

RuleInput object

NameTypeNotes
namestringrequired, max length 100
targetRuleTarget
priorityintegermin 0, max 100000, default 100
enabledbooleandefault true
include_spambooleandefault false
conditionsRuleConditionsrequired
actionsRuleAction[]required, at least 1 item, at most 20 items

RulePatch object

NameTypeNotes
namestringmax length 100
targetRuleTarget
priorityintegermin 0, max 100000
enabledboolean
include_spamboolean
conditionsRuleConditions
actionsRuleAction[]at least 1 item, at most 20 items

RuleConditions object

NameTypeNotes
fromstring[]Sender, case and accent insensitive substring, ORed, at most 32 items
tostring[]Recipient (To or Cc), ORed, at most 32 items
subjectstring[]Subject substring, ORed, at most 32 items
querystringFull text, same parser as search, max length 512
negated_querystringThe message must not match this, max length 512
has_attachmentboolean
sizeRuleSize
spam_verdictclean | review | spam[]at most 3 items, one of clean, review, spam

RuleAction object

NameTypeNotes
typeadd_label | remove_label | archive | move_to_folder | mark_read | forward_to | auto_reply | reject | webhook | assignrequired
labelstringadd_label, remove_label, move_to_folder: the label, which is also the folder, max length 64
addressstring (email)forward_to: a verified address of the organization
reasonstringreject: one line, no address, sent back in the 550, max length 200
webhook_idstring (uuid)webhook: the endpoint that receives rule.matched
subjectstringauto_reply: the subject of the answer
textstringauto_reply: the body of the answer, max length 4000
assigneestring (uuid)assign: the member who takes the ticket

RuleApplyReport object

NameTypeNotes
job_idstring (uuid)
rule_idstring (uuid)
statuspending | running | done
matchedinteger
appliedinteger
skippedobject
created_atstring (date-time)
finished_atstring (date-time)