---
title: "Mail flow rules, the outbound content policy and what a domain does to a message"
url: https://www.hmailserver.co.uk/topic/28251/mail-flow-rules-the-outbound-content-policy-and-what-a-domain-does-to-a-message
author: "Progressiverobot"
published: 2026-09-27T19:33:13.731Z
updated: 2026-09-27T19:33:13.731Z
category: "Anti-spam & message filtering"
tags: ["administration","smtp","rest-api","security"]
site: hMailServer Community Forum
---

# Mail flow rules, the outbound content policy and what a domain does to a message

*Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) holds a domain's or an account's rule to its owner: it may no longer set `From`, `Sender`, `Resent-From`, `Resent-Sender`, `DKIM-Signature` or the loop count, and its Reply goes only from an address the owner may send as; such a rule is refused when it is saved, one stored before the upgrade runs without that action, and the application log names it. Its Warn sender notice, and the notice of a Reject met after acceptance, come from the postmaster of the rule owner's domain. A credential confined to named domains may no longer give a domain's or an account's rule a route, a bound address or a script function, and a Run function that is not a plain function name is refused when the rule is saved. A rule's forward of mail from outside whose `From` names a hosted domain's organisation is given an envelope sender in that organisation only when the message passed DMARC for it on arrival; otherwise it keeps the envelope sender it came with or, where that is itself at this server's host name or at a hosted domain in the organisation its `From` names, is not sent to an outside address at all, and the application log names each recipient. A message over 80 MB goes through the rules by its header alone: the conditions that need the body do not match, a script function is not run, and the disclaimer and the signature are not appended. See [the 6.3.7 announcement](/topic/28277).*

This topic covers the rules the server runs on every message, the conditions and actions 6.3.4 added to them to make an outbound content policy, and the three things a domain can be told to do to the mail passing through it: mark what comes from outside, say when a sender has never written before, and put a legal footer on what leaves. The one thing to know first is that these run at three different moments, and the moment decides what each can see. The server's and the domains' rules run in the deliverer on every delivery attempt, with `reject` also asked at the end of `DATA`, and an account's own rules once for their recipient; the external tag and the first-contact note are applied to the recipient's own copy at local delivery; and the disclaimer goes on at submission, before this server's DKIM signature is made.

### Three collections of rules

| Collection | Where it is edited | Runs for |
|---|---|---|
| The server's | `Application.Rules`, `/api/v1/rules`, the Control Panel's Rules page, the Deck's | every message, first |
| A domain's (since 6.3.4, schema 6063) | `Domain.Rules`, `/api/v1/domains/{name}/rules`, the same pages with the domain chosen at the top | every message with a recipient in the domain or a sender it hosts, after the server's |
| An account's | `Account.Rules`, `/api/v1/accounts/{address}/rules` | that account's mail, at local delivery |

The server's and the domains' rules run in `SMTPDeliverer` on every delivery attempt, after virus scanning and before DKIM signing and the split between local and external delivery. An account's own rules run after the split, at local delivery, on that recipient's own copy: once for their recipient, when its copy is stored. All three have the same conditions and actions, except that the wiki's Rules and Sieve page lists two actions, send using a route and bind to an address, as absent from an account's rules. Since 6.3.7 a domain's or an account's rule may not set `From`, `Sender`, `Resent-From`, `Resent-Sender` or `DKIM-Signature`, which are for the server's rules alone, nor the loop count, which no rule sets, and its *Reply* goes only from an address the owner may send as: one the account may send as for an account's rule, an address of the domain or of one of its domain aliases for a domain's; a credential confined to named domains may not give such a rule a route, a bound address or a script function. A rule stored before the upgrade runs without the refused action, and the application log names it; to set `From` on a domain's mail, make the rule a server rule with a condition on the domain. A domain's rules are how a domain administrator has rules of their own: a domain-restricted key reaches its domain's rules and no other's. The Control Panel and the COM objects are Windows-only; the Deck and REST serve both platforms.

**A repeated pass does not repeat the side effects.** A pass that repeats an attempt because this server's own [outbound shaping](/topic/28259) held the message, without counting a try, runs the rules again but does not forward, copy, reply or call a script function again. CHANGELOG records the fix: before it, a held message sent one copy per pass.

**What a rule queues goes at once.** Since 6.3.4 a rule's forward, copy, reply or notice to the sender, and an account's own forwarding, no longer wait up to a minute in the queue: each is queued inside the delivery that caused it, and the delivery manager is now told of it, where before it sat until something else woke the manager.

**A domain's rules act only on what is that domain's.** Domain rules are new in 6.3.4 and shipped with this limit.

- **The sending domain's rules act on the whole message.** The sending domain is that of the account that submitted the message (SMTP `AUTH`, the webmail, a scheduled send), and the envelope sender's domain when that is vouched for: its account submitted the message, DMARC passed for the domain on mail from outside, or the message reaches no other domain's mailboxes. An envelope sender nobody vouched for does not choose a domain's rules for mail to another domain's mailboxes.
- **A recipient domain's rules act on the whole message only when every recipient is its own.** Otherwise they act on the domain's own recipients: a delete or a reject takes those recipients off the message, a quarantine keeps a copy for them under the rule's name, and a copy goes to them alone. Such a reject is met at delivery, not in the SMTP session, whose one answer is for every recipient; a sender this server hosts is mailed a notice naming the recipients refused. A header, a disclaimer, a script function, a route, a bound address and required TLS are not applied, since they would change the message for everybody.

As domain rules were first written in the source tree, without this limit, one tenant's delete, reject, header or route reached another tenant's recipients; no release had that.

### Conditions and actions

The long-standing conditions (From, To, CC, Subject, body, size, the recipient list, delivery attempts, any named header), the match types, the long-standing actions and how a rule set is evaluated are the rule engine's, and are set out in [Rules and Sieve: what runs when](/topic/28268). Since 6.3.4 there is one more condition, **the attachment name** (`attachment_name`): every attachment's file name, with `;` between them, so `contains .exe` or a wildcard `*.zip` reads as "any attachment is".

**Run a script function takes a plain function name since 6.3.4**, without a switch: ASCII letters, digits, `_` and `$`, not starting with a digit. On Windows any other name is refused as `HM5018` each time the rule fires; on Linux it finds no function, nothing is run, and the error log has a Script Error line. Since 6.3.7 such a rule is also refused when it is saved, over COM and REST, for a rule of every owner. Give every such rule a plain function name before you upgrade, and look for `HM5018` after; why it changed is in [event scripts and the 6.3.4 security fix](/topic/28261) and [the 6.3.4 security announcement](/topic/28218).

Since 6.3.4 there are five more actions:

| Action | REST word | What it does |
|---|---|---|
| Add a disclaimer | `add_disclaimer` | Appends the text to the message's plain and HTML parts when the rule matches, with `X-hMailServer-Disclaimer` naming the rule. The domain disclaimer stays the unconditional footer it was |
| Reject with a text | `reject` | The SMTP session answers `550 5.7.1` with the text before the message is accepted. A reject with no text of its own names the rule |
| Require TLS | `require_tls` | Every external delivery of the message needs STARTTLS. A destination that offers none is deferred with a reason naming the rule; the message is not sent in the clear |
| Quarantine | `quarantine` | Held in the quarantine under `Rule: <name>` for an administrator to release or delete. A quarantine that could not store it delivers the message rather than lose it, and says so in the error log |
| Warn the sender | `warn_sender` | The message goes where the rule's other actions send it, and a sender this server hosts is mailed a notice |

**Where `reject` is asked.** The server's rules, and those of every domain whose rules act on the whole message, are asked at the end of `DATA`, and the message is parsed for the match only when one of them has a reject action at all. The webmail's send route asks the same rules before it queues, and answers `403`, code `policy_rejected`, with the text. A message that reached delivery another way is dropped, as a delete is, with the rule named in the log.

### The outbound content policy

*Since 6.3.4.* What Exchange calls data loss prevention is here four more conditions and one more action in the same rules, with no second engine. Each condition answers as a list with `;` between the entries, so `contains x` reads as "any of them is x" and `equals ""` as "none".

| Condition | REST word | Answers |
|---|---|---|
| Sensitive content | `sensitive_content` | The detectors that found something in the readable text: `card_number`, `iban`, `account_number` |
| Attachment type | `attachment_type` | Every attachment's type from its first bytes, not its name: `executable`, `elf`, `pdf`, `zip`, `office_xml`, `jar`, `office`, `rar`, `7z`, `gzip`, `png`, `jpeg`, `gif`, `rtf`, `script`, `text`, `empty`, `binary` |
| External recipients | `external_recipient` | Every envelope recipient this server does not host; empty when every recipient is local |
| Readable text | `content` | The text itself, for a pattern of the rule's own |

**The readable text** is the plain part, the HTML part with its tags removed, and every attachment whose bytes are text, a `.csv` sent as `application/octet-stream` included. It is bounded at a megabyte.

**The detectors.**

- `card_number`: a run of 13 to 19 digits, with at most one space or hyphen between any two, that passes the Luhn check and is not one digit repeated. A run longer than 19 digits is not searched for a card inside it.
- `iban`: two letters of a country in the IBAN registry and two digits, then exactly that country's number of characters, passing the ISO 13616 check.
- `account_number`: the regular expression in the `dlp_account_number_pattern` setting, such as `\b\d{2}-\d{2}-\d{2} \d{8}\b` for a sort code and account. Empty, the default, is a detector that never fires. A pattern that does not compile is reported as `HM6821`, and a search given up as too complex, or one that takes over the 250 ms budget, as `HM6822`; either way the pattern is rested for five minutes, finding nothing meanwhile. That is fail-open, on purpose and said out loud.

**Marked internal** needs no new condition: `header` `Sensitivity` `contains` `Company-Confidential`, combined with `external_recipient` `contains` `@`.

**A sealed message**, encrypted end to end and judged from its body rather than its label, has no text the server can read, so `sensitive_content` and `content` do not match it, whichever way they are phrased, and the application log says why once per rule. `attachment_type` and `external_recipient` still answer.

**A worked policy.** From the document:

```json
POST /api/v1/rules
{
  "name": "No card numbers leave",
  "all_criteria": true,
  "criteria": [
    { "field": "external_recipient", "match": "contains", "value": "@" },
    { "field": "sensitive_content", "match": "contains", "value": "card_number" }
  ],
  "actions": [
    { "type": "reject", "value": "Card numbers may not be sent by mail. Use the payment portal." }
  ]
}
```

A rule with `external_recipient contains @` fires only on a message with a recipient this server does not host; a rule without that condition applies to inbound mail too. A policy is a rule of the server or of the sender's domain. It cannot be an account's rule, because those are the recipient's own filters and never see the account's own outgoing mail.

**The notice `warn_sender` sends** comes from `postmaster@<their domain>` (to 6.3.6; since 6.3.7 from the postmaster of the rule owner's domain, and for a server rule of the default domain when one is set and hosted here, else of the hosted domain the message was addressed to, else of the sender's own, signed as that domain) with the subject `Policy notice: <rule name>`, the headers `X-hMailServer-Policy: <rule name>` and `X-hMailServer-Policy-Outcome: delivered`, and `Auto-Submitted: auto-generated`. It is sent only when this server hosts the sender's domain, because a notice to an outside sender is backscatter, and it goes once per message, on the first delivery attempt. A rule with only that action delivers the message.

**A released message is not held again** by the rule that quarantined it; the rule's other actions still run.

**What the policy does not do.** It is pattern matching, and patterns have false positives: the document says one 16-digit reference number in ten passes Luhn, and recommends `warn_sender` or `quarantine` as the first choice for a new pattern rather than `reject`. It reads text, not documents: a card number inside a PDF, a Word file or a spreadsheet is not found, an image of a card is an image, and an encrypted zip is `zip`. There are no fingerprints, classifiers or dictionaries, no per-user exceptions, and no policy tip in the sender's mail client before they press Send.

### What a domain does to a message

*Since 6.3.4 (schema 6044).* Three per-domain features, **off on every existing and every new domain**, costing nothing while off. They are set in the Control Panel's domain dialog, tab **Tagging and disclaimer** (Windows only); in the Control Deck's domain editor, **What this domain does to a message**; or with `PUT /api/v1/domains/{name}` and the keys `external_tag_subject`, `external_tag_header`, `external_tag_text`, `first_contact_tip`, `disclaimer_enabled`, `disclaimer_plain_text` and `disclaimer_html`.

**Who counts as outside** is one rule, `MessageOrigin::IsExternalSender`. A sender is outside when none of these is true:

1. The session authenticated. This is read from the topmost `Received:` header this server wrote; `X-AuthUser` is deliberately not consulted.
2. A trusted incoming relay handed the message over.
3. Every address the message names, the envelope sender and the `From:` address, is at a domain this server hosts. Either being outside is enough.

**The sharp edge: an inbound gateway.** If you run a filtering gateway in front of this server and list it as [an incoming relay](/topic/28240), nothing arriving through it is ever tagged. The choice is between the tag and the relay declaration; the document says there is no third answer today.

**It is not anti-spoofing.** A message that forges a hosted address in both its envelope and its `From` is judged inside and is not tagged. The control for that is DMARC with a policy on your own domain.

| Switch | What the reader gets | Cost |
|---|---|---|
| `external_tag_subject` | The tag text at the front of the Subject: `external_tag_text`, at most 100 characters, `[EXTERNAL]` when empty. A subject that already begins with it gets no second copy | **Breaks any DKIM signature the sender made over the Subject** for anyone who verifies downstream, as the spam subject prefix does. The verdict this server reached is unaffected |
| `external_tag_header` | `X-hMailServer-External: YES`, which the webmail shows as *This message came from outside your organisation.* | None to the signature. Prefer this wherever the reader's client can show it |
| `first_contact_tip` | `X-hMailServer-First-Contact: YES` when an outside sender writes to an account that has had mail from other people but never from them. The webmail shows *You do not usually get mail from this sender.* | One indexed `SELECT` per delivered copy, then an `UPDATE`, or a further `SELECT` and an `INSERT` the first time |

The tag is applied to the recipient account's own copy, above the account's rules and its Sieve script, so both can match on the header, and below the per-account spam settings. A domain with tagging on does not share one delivered file between several recipients (`DeliveryHardLinks`). Where a domain has turned the feature on, a forged copy of the header on a message judged inside is removed.

**The first-contact memory** is the table `hm_knownsenders`, one row per account and sender. When an account that signed in sends a message, each recipient is recorded as somebody that account knows, so their reply is not announced; an unauthenticated session records nothing. The wiki adds that mail sent from the webmail or by send-later does not record its recipients, so a reply to a message first sent that way can be announced once. Two limits are by design. After a **cold start**, a new installation or the day the switch is turned on, most senders look new; the document says to expect a week. After a **restore**, the memory is as of the backup.

### The domain disclaimer

`disclaimer_enabled` appends the domain's legal footer to mail leaving the organisation. Filling in only `disclaimer_plain_text` uses it for both forms. It runs where the domain signature runs, before `DKIMSigner::Sign`, so this server's own signature covers the footer. The footer is appended to the `text/plain` part and the `text/html` part in place; a message with neither is left alone.

**It is never added to:**

- a signed or encrypted message (`multipart/signed`, `multipart/encrypted`, `application/pkcs7-mime`, `application/x-pkcs7-mime`, `application/pgp-encrypted` or `application/pgp-signature`), which is sent exactly as written, with a line in the application log;
- an automatic reply, a vacation message or a delivery report;
- a bounce;
- a list posting (`List-Id`, `List-Unsubscribe`, or a `Precedence:` of bulk, list or junk);
- a message with no recipient outside this server;
- a message this server already footed, which carries `X-hMailServer-Disclaimer`;
- a message relayed in from outside that merely claims a hosted sender domain.

A reply quoting a message that already carried the footer does not collect a second copy. A footer shorter than eight characters is never treated as already present.

**Webmail and send-later get the footers too, since 6.3.4.** The domain's signature and disclaimer go on mail sent from the webmail (`POST /api/v1/me/messages`, which is also its reply, forward and phishing report) and from its send-later, through the same function as SMTP ([docs/DomainTransforms.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/DomainTransforms.md); CHANGELOG). In 6.3.3 and earlier only the SMTP path had either footer, which is what the wiki's Stopping Spam §10.7a still describes.

**Two sharp edges on the webmail paths.** Send-later sends with the account as the envelope sender, so a draft scheduled with a chosen From at another hosted domain gets the account's domain's footers. And with the domain's signature method set to overwrite the account's, the webmail has already written the account's signature into the body, which the server does not take out.

**Signed and encrypted messages get no signature either**, since 6.3.4. Before, a signature was appended inside a `multipart/signed`, breaking the S/MIME signature. This is a change on the SMTP path too: an installation that relied on a signature inside signed mail will now see none there.

### Where to read more

Sieve, the account's own filters and the rule engine itself are in [Rules and Sieve](/topic/28268); the blocked-attachment list and the sandbox are in [the topic on viruses and dangerous attachments](/topic/28250). The two browser surfaces have topics of their own: [The Control Deck](/topic/28192) and [The webmail portal at /portal](/topic/28191).

- https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/MailFlowRules.md
- https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/OutboundContentPolicy.md
- https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/DomainTransforms.md
- Wiki: https://gitlab.com/hmailserver/hmailserver/-/wikis/Rules-and-Sieve and, for the tag and the note, https://gitlab.com/hmailserver/hmailserver/-/wikis/Stopping-Spam
- Public documentation: https://www.progressiverobot.com/hmailserver-documentation/#121-hmailserver-rules and https://www.progressiverobot.com/hmailserver-documentation/#27-rules-in-practice-a-cookbook
