Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) holds a rule and a Sieve script to their owner: a domain's or an account's rule may no longer set From, Sender, Resent-From, Resent-Sender, DKIM-Signature or the loop count, and its Reply goes only from an address its owner may send as; a Sieve script may no longer add or delete those fields, Auto-Submitted or the loop count, and its notify goes from the account's own address unless its :from reaches the account. One stored before the upgrade still runs without that action or edit, and the application log names it. A loop count that is not digits alone, or is above the limit, reads as the limit, and RuleLoopLimit 0 now stops out-of-office replies too. See the 6.3.7 announcement.
hMailServer filters mail with two systems that both run during delivery and do not know about each other. Rules are the server's own engine: criteria and actions, edited in the Control Panel, and the only one of the two that can run server-wide. Sieve (RFC 5228) is the standard mail filtering language, owned by the mailbox user and edited from a mail client over ManageSieve or on the Control Panel. This topic covers the order they run in, rules and the Run function action, Sieve and vacation, the out-of-office policy, and what both see on sealed mail. It describes 6.3.4 (27 September 2026, schema 6104). The one thing to know first: they run at different points, they see the message at different stages, and when they disagree about where a message goes, Sieve wins.
The order they run in
The filtering pipeline for one inbound message:
During the SMTP conversation, while the sender is still connected. The pre-transmission spam tests (SPF, DNSBL, HELO, greylisting) run at MAIL FROM and RCPT TO; the script event OnSMTPData runs at the DATA command, before any of the body is sent; once the body is in come the post-transmission tests (SURBL, DKIM, ARC, SpamAssassin). A message over the delete threshold is refused, with 550 when the verdict came before the body and with 554 after it; with quarantine on, one judged after the body is held in quarantine instead and the sender answered 250. Otherwise the script event OnAcceptMessage runs and the message is queued.
On the delivery thread, once per message and on every delivery attempt: OnDeliveryStart, blocked attachments and the virus scan, the global rules, OnDeliverMessage, then DKIM signing and mirroring (first attempt only).
The message is split into local and external recipients. External recipients go to relays, routes and MX delivery.
For each local recipient, on that recipient's own copy: per-account spam overrides, the account auto-reply, the account rules, account forwarding, trace headers, the Sieve script, the domain-wide auto-reply (only if the account said nothing), Sieve reject then discard, Sieve flags, the folder decision, and the save to the mailbox.
Because
It follows that
Spam scoring happens during the SMTP conversation
A rule cannot make a message spam; it can only read the flag, or a header the filter wrote
Virus scanning happens before global rules
A rule never sees an infected message
Global rules run on the shared message, before the split
Delete e-mail in a global rule destroys the message for every recipient; in an account rule, only that recipient's copy
Global rules re-run on every delivery attempt
Forward, Create copy and Reply can fire more than once for a deferred message. Write Delivery attempts equals 1 to prevent it
Sieve runs after account rules
A Sieve fileinto overrides the folder a rule chose. A header a rule set, or a copy it queued, has already happened and Sieve cannot undo it
What a rule is
Rules in the Control Panel sidebar is the server-wide set ("Global rules"); each account has its own on the Rules tab of its account dialog. A rule is a name, an enabled flag, a match mode - Match ALL criteria (AND) or Match ANY criterion (OR) - an ordered list of criteria and an ordered list of actions.
Criterion
What is compared
Note
From, To, CC
The From:, To: and Cc: headers
Not the envelope. Mail that reached you by Bcc, an alias or a list has no To: naming you
Body
The plain-text body concatenated with the HTML body
A phrase inside an attachment is not matched
Message size
The file's size in bytes
25 MB is 26214400
Recipient list
Every SMTP envelope recipient, joined with ;
The field that sees a Bcc, an alias expansion or a list member
Delivery attempts
The retry count plus one
The first attempt is 1
Custom header
The named header's value
Name it without the colon
Match types. equals, does not equal, contains and does not contain are case-insensitive. is less than and is greater than parse both sides as numbers, and a non-numeric string parses as 0. matches wildcard and matches regex require the whole value to match; a regex is also case-sensitive, so to find a word anywhere write .*invoice.*, and (?i).*invoice.* to ignore case. does not equal and does not contain are true when the field is empty: Subject does not contain [ok] -> Delete e-mail deletes every message that has no Subject at all. Add a positive criterion under match mode ALL when the action is Delete.
Action
What it does
Delete e-mail
Global: the message is dropped and Message will be deleted. Action triggered by a global rule (<name>) is logged. Account: only this recipient's copy
Forward e-mail
Queues a second message to the named address, loop-count guarded
Reply
Queues a new message to the envelope sender, stamped Auto-Submitted; never answers auto-submitted mail
Move to IMAP folder
Happens at the end of local delivery. INBOX.Archive; #Public.Name for a public folder, subject to the insert permission; %YEAR%, %MONTH% and %DAY% come from the server's local date. A missing folder is created
Run script function
Calls a function in the event script - next section
Stop rule processing
Stops the rule set after this rule's remaining actions have run
Set header value
Rewrites the message file; %MACRO_ORIGINAL_HEADER% is the header's current value
Create copy
Queues a duplicate carrying X-CopyRule: <rule name>
Send using route, Bind to address
Force a named route, or the local address the connection leaves from. Absent from an account's rules
The Run function action, since 6.3.4
The action the Control Panel calls Run script function calls the named function in the event script as <function>(HMAILSERVER_MESSAGE), then reloads the message from disk, so a script that edits and saves is seen by the rest of the rule set. It needs scripting enabled.
Since 6.3.4 the name must be a plain identifier: ASCII letters, digits, _ and $, not starting with a digit. On Windows, in either script language (VBScript or JScript), any other name is refused as HM5018 each time the rule fires. Before 6.3.4 the name was written into the call unescaped, in either language, so whoever may edit a rule, a domain's own administrator included, could run code in the server's process: that is CVE-2026-100741, in every release from 6.0.0 to 6.3.3, and event scripts and the 6.3.4 security fix has the flaw and what to do until a server is upgraded.
On Linux the action works since 6.3.4, where event scripts are JavaScript. The Linux engine looks a handler up by name instead of building a call, so such a name is never evaluated and finds no function: nothing is run, and the ERROR log has a Script Error line, not HM5018.
Since 6.3.7 such a name is refused when the rule is saved, over COM and REST, for a rule of every owner; and the action on a domain's or an account's rule is the server administrator's to set, since it reaches a function of the server's event script: a credential confined to named domains - such as a domain administrator, or an API key issued for its domains - is refused it.
Evaluation: a matching rule does not stop the set
For each active rule, in order, the criteria are evaluated under the match mode and, if they matched, every action runs. Only a Stop rule processing action ends the set, so make that the last action of every rule that should be terminal.
A rule with no criteria runs no actions, in either match mode. A catch-all rule needs a criterion that is always true.
If the message file cannot be loaded, the whole set is skipped and the message is delivered unfiltered, with HM6040 in the log, rather than run against an empty message.
When several choose a folder, a Sieve fileinto beats an account rule's folder, which beats a global rule's.
The regular-expression breaker. A regex runs against a value the sender wrote, so it has a budget of 250 ms per evaluation. A pattern that will not compile is reported as HM6042; one that takes 250 ms or more as HM6043; either is suspended for 300 seconds, keyed on the pattern text, so a corrected pattern takes effect on the next message. A suspended pattern does not match. The same breaker covers Sieve's :regex, and suspension is shared.
Loop protection. Every generated message carries X-hMailServer-LoopCount. Forward, Reply and Create copy check it against RuleLoopLimit (COM Settings.RuleLoopLimit; Mail flow & delivery → Delivery of e-mail → Rules, since 6.3.7 a card on the Rules page; default 5); at or above it nothing is generated and HM5064, HM5065, HM5201 or HM4404 is logged. An Auto-Submitted header stops Reply and bounce reports only; Forward deliberately ignores it.
What else 6.3.4 changed for rules
Three collections instead of two. A domain's rules (Domain.Rules, /api/v1/domains/{name}/rules) run after the server's. Account rules are on /api/v1/accounts/{address}/rules.
Five more actions: Add a disclaimer, Reject with a text, Require TLS, Quarantine and Warn the sender.
A legal hold keeps, among the account's deleted items, what an account rule's Delete or Reject, or a Sieve discard, reject or redirect that keeps no local copy, dropped. It does not reach the server's or a domain's rules: a Delete there drops the message for everybody, held accounts included.
A domain's rules do not always act on the whole message. Mail flow rules and what a domain does to a message says when they act on all of it and when on the domain's own recipients only, what each new action does, and two 6.3.4 fixes to what a rule queues.
Sieve: where a script lives and how it becomes active
Each account has one active script, plus any number of named scripts. They are files, not database rows, under [Directories] DataFolder:
<DataFolder>\
Sieve\
_global\ administrator-placed scripts, for include :global
example.com\
alice\
active.sieve THE ACTIVE SCRIPT - this is what delivery runs
vacation.responses who has already had a vacation reply, and until when
duplicate.seen identifiers the duplicate test has seen
scripts\
holiday.sieve named scripts, from ManageSieve PUTSCRIPT
Four routes write that same active.sieve, and all four check the syntax first:
Route
Where
Control Panel (Windows)
Account dialog, Sieve tab
COM (Windows)
Account.SieveScript; no Save() is needed
REST
GET and PUT /api/v1/me/filters, as the account itself; 256 KB cap; an empty script removes the filter
ManageSieve
PUTSCRIPT then SETACTIVE
Deactivating a script also clears that account's vacation-response records.
ManageSieve is off by default: ManageSieveServerPort=0 means the listener is never created. Set the port (the wiki's example is 4190) and ManageSieveServerBindAddress on the ManageSieve (RFC 5804) card of the Control Panel's API & monitoring page on Windows (since 6.3.7 a ManageSieve tab of the Protocols page; Ctrl+K takes the old title to it), or on either platform with PUT /api/v1/settings/ini/{name} or hmctl ini set. Since 6.3.4 both wait for a reinitialise, not a restart: Apply now in the Control Panel or the Deck, or POST /api/v1/server/reinitialize. A script is at most 1 MB. With no certificate, STARTTLS is not advertised at all; with no certificate and no range policy, keep the listener on 127.0.0.1.
What this server's Sieve accepts and refuses
require accepts fileinto, envelope, copy, relational, subaddress, vacation, vacation-seconds, imap4flags, body, mailbox, regex, ihave, environment, date, index, spamtest, spamtestplus, duplicate, editheader, variables, reject, ereject, include and enotify, and the comparators comparator-i;ascii-casemap, comparator-i;octet and comparator-i;ascii-numeric. What matters in practice:
fileinto :copy is refused at upload. This server stores exactly one local copy. Use :copy on redirect.
redirect without :copy cancels the implicit keep, and the message leaves the mailbox entirely.
imap4flags stores only the five system flags, \Seen \Answered \Flagged \Deleted \Draft. A keyword is accepted, logged, and stored nowhere.
spamtest reads the server's internal flag, never a header a sender could forge: "0" when the message was not classified, "10" when it was. Any threshold from 1 to 10 behaves identically.
duplicate keys on Message-ID over 7 days by default, and its store fails open, because a wrong "duplicate" destroys mail.
reject and ereject send a non-delivery report with status 5.7.1 to whoever the envelope names. Against forged senders use discard.
editheader refuses Received and Return-Path, and since 6.3.7 From, Sender, Resent-From, Resent-Sender, DKIM-Signature, Auto-Submitted and X-hMailServer-LoopCount, at upload and at delivery, where a script stored before the upgrade still runs without the edit; enotify accepts mailto: only, and since 6.3.7 a notify goes from the account's own address unless its :from is the account's own address or an active alias pointing at it; include nests at most three levels.
The everyday script:
require ["fileinto", "mailbox", "spamtest", "relational", "comparator-i;ascii-numeric"];
if spamtest :value "ge" :comparator "i;ascii-numeric" "5" {
fileinto :create "INBOX.Junk";
stop;
}
if header :contains "List-Id" "announce.example.com" {
fileinto :create "INBOX.Lists.announce";
stop;
}
Vacation, and the out-of-office policy
require ["vacation"];
vacation
:days 7
:subject "Out of the office until 22 September"
:addresses ["
[email protected]", "
[email protected]"]
"I am away until 22 September. For anything urgent please write to
[email protected].";
:addresses is not decoration: a reply is sent only when one of the user's own addresses appears in the envelope or the headers, and that list is what "the user's own addresses" means. :days defaults to 7; vacation-seconds gives :seconds, and :seconds 0 means "reply every time". The window is per sender, in vacation.responses, and that store fails closed: if it cannot be read, no reply is sent.
The account's Auto-reply tab and the domain's Out of office tab are the server's own reply, separate from Sieve. At most one reply answers any message. The account's own, when it is on and inside its dates, comes first; a domain's reply answers only for accounts that have none. If both an account-level auto-reply and a Sieve vacation are configured, the account-level one wins.
Account
Domain
General text
VacationSubject, VacationMessage
VacationSubject, VacationMessage
Text for colleagues
VacationInternalSubject, VacationInternalMessage
the same two
Colleagues only
VacationMessageInternalOnly
leave the general text empty
Strangers get the domain's text
-
VacationExternalOverride
A sender is internal when the envelope sender resolves to an account hosted here or to an active alias. That is whatever the submitting client said, so an internal text must never carry anything that cannot be shown to a stranger. Each sender is answered once per absence; a window - VacationMessageDays on the account, VacationDays on the domain, 0 to 3650 - answers the same sender again after that many days. The memory is the table hm_autoreplies (schema 6060), so since 6.3.4 a restart no longer repeats every reply.
Never answered: a null return path; Auto-Submitted other than no; Precedence: bulk, list or junk; anything with a List-* header; a sender that set X-Auto-Response-Suppress; postmaster and mailer-daemon; ourselves; and marked spam when VacationMessageAbortSpamFlagged says so; and since 6.3.7 a message whose X-hMailServer-LoopCount has reached RuleLoopLimit, so RuleLoopLimit 0 stops out-of-office replies as it stops every other generated message. Every reply goes out with a null return path and Auto-Submitted: auto-replied.
What rules and Sieve see on sealed mail
A message encrypted end to end (S/MIME or PGP/MIME) is opaque: the server holds no key to it. How the server decides that, and what the rest of the server does with such a message, is in end-to-end encryption and sealed mail. For rules and Sieve:
A rule's body criterion on an opaque message does not match, whichever way it is phrased, and one line per rule per message in the log says so. Under AND, a rule with a body criterion does not fire; under OR, the other criteria are still evaluated. Before 6.3.4, does not contain matched every encrypted message. A multipart/signed message is read as it always was.
To catch encrypted mail, test the Content-Type header for multipart/encrypted or application/pkcs7-mime with a custom-header criterion.
Sieve's evaluation is unchanged; a log line is new. An encrypted message has no text part, so body :contains "x" is false and not body :contains "x" is true, and the delivery writes a log line saying so, once per message and recipient. body :raw is the undecoded body exactly as it arrived, so if body :raw :contains "BEGIN PGP MESSAGE" is the honest way a script can recognise a sealed message.
What it does not do
Sieve cannot run server-wide, force a route or bind an outbound address.
A rule's Delete e-mail is silent. The sender was already answered 250. To tell the sender, use Sieve reject, or since 6.3.4 the Reject with a text action.
A journal rule does not see a rule's forward, copy or reply, or a Sieve redirect: the server queues those itself.
Where to read more
Wiki: Rules and Sieve, The COM API and Scripting
Documents at the 6.3.4 tag: OutOfOffice.md, SealedMail.md, MailFlowRules.md, EventScripts.md
The public documentation: chapter 12 and the rules cookbook
On this forum: the 6.3.4 release announcement, the Linux guide, mail flow rules and domain rules, event scripts, and when a saved setting takes effect for the ManageSieve keys