Rules

Table of contents

  1. What a rule sees
  2. Leaf matchers
  3. Boolean combinators
  4. Example rule JSON
  5. Operational notes

Routes only run a transform pipeline when their routing rule matches the incoming message. Rules are stored as arbitrary JSON, validated, and compiled into predicates in. The notes below explain what data is available to a rule, how each field behaves, and how to combine matchers.

What a rule sees

Before matching, the inbound email is normalized into a dictionary:

  • subject: lowercased string.
  • to_emails, from_emails: lists of lowercased email addresses. Empty when the parsed message omitted the field.
  • from_domains: the domain portion (after the @) for every from_emails entry that contains a domain.
  • headers: map of header name unfolded string, with header names lowercased and missing values coerced to "".
  • attachments_content_types: list of lowercased MIME types for every persisted attachment (e.g., "image/png"). Empty when there are no attachments.

Every string comparison performed by the compiler lowercases both sides, so the rule JSON can use any casing. Leading/trailing whitespace is stripped by rule validators. Unknown keys are accepted but ignored by the compiler, allowing forward-compatible storage.

Leaf matchers

Each populated field is AND-ed at the top level (a message must satisfy all of them unless you wrap logic in any/all/none/negate). Empty arrays or objects mean “no constraint” for that field.

KeyTypeBehavior
subject_containslist[str]Passes when any entry is a substring of the subject. Comparisons are case-insensitive and operate on the normalized subject.
subject_regexlist[str]Python regexes tested (case-insensitive) against the subject. Patterns are linted via safe_regex; invalid expressions are skipped and logged.
to_contains / from_containslist[str]Substring match against each recipient/sender email. If any address contains any substring, the predicate passes.
to_emails / from_emailslist[str]Exact (case-insensitive) match against normalized addresses.
from_domainslist[str]Exact match against any normalized sender domain.
headers_equalsdict[str,str]Header must exist and exactly equal the provided value (name comparison is case-insensitive).
headers_containsdict[str,str]Header must include the provided substring. Useful for prefixes like "x-priority": "high".
attachments_mimelist[str]Match attachment content types with glob patterns, enabling negation against / for no‑attachment matching.

Boolean combinators

Rules can branch using nested boolean structures—each child entry uses the same schema as a normal rule:

  • all: list of subrules. Equivalent to logical AND.
  • any: list of subrules. Equivalent to logical OR; an empty list evaluates to True, so omit the key (instead of sending []) when you want “no constraint.”
  • none: list of subrules. Passes only if none of the child subrules match.
  • negate: single subrule whose result is inverted.

These combinators can be nested arbitrarily, so constructs like “match invoices but not auto-replies” or “subject contains term and headers mention Plan X” are expressible.

Example rule JSON

{
  "to_emails": ["alerts@example.com"],
  "from_domains": ["vendor.com"],
  "subject_contains": ["invoice", "receipt"],
  "any": [
    { "headers_contains": { "x-priority": "high" } },
    { "subject_regex": ["(?i)urgent"] }
  ],
  "none": [
    { "from_emails": ["bot@vendor.com"] }
  ]
}

With this rule saved on a route, only messages addressed to alerts@example.com and sent by vendor.com will reach the route’s transform pipeline. The any branch makes high-priority or “urgent” subjects eligible, while the none branch blocks a known autoresponder address.

Operational notes

  • Storing a rule with zero populated fields yields a “match everything” route, which is useful when you only rely on downstream mappers.
  • Regex patterns run through safe_regex, so catastrophic backtracking is mitigated; nevertheless, keep expressions narrow and test them locally.
  • The UI and API return rules exactly as saved (RouteOut.rule), so you can clone an existing route by copying its JSON block verbatim.