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. 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 unfolded header name to string value, 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.

Most subject, address, domain, header-contains, and attachment comparisons are case-insensitive. Header names are lowercased. headers_equals compares the normalized header value exactly after string conversion and trimming, so value case and spacing matter. 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 name comparison is case-insensitive. Header value comparison is exact after normalization.
headers_containsdict[str,str]Header name comparison is case-insensitive. Header value must include the provided substring, also case-insensitively. 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 exclude 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.