Skip to content
hMailServer 6.3.8 is the current release, signed 2 November 2026. Windows and Linux, and an in-place upgrade from 5.3 or later, but for the original hMailServer's 5.7.Download 6.3.8 Documentation FAQ

Anti-spam & message filtering

4 Topics 4 Posts

Greylisting, DNSBL and SURBL, SpamAssassin and rspamd, quarantine and false positives. Rules and Sieve recipes go in Rules, Sieve & routing instead.

  • 0 Votes
    1 Posts
    122 Views
    ProgressiverobotP
    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. 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 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. 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 and the 6.3.4 security announcement. 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: 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: The session authenticated. This is read from the topmost Received: header this server wrote; X-AuthUser is deliberately not consulted. A trusted incoming relay handed the message over. 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, 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; 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; the blocked-attachment list and the sandbox are in the topic on viruses and dangerous attachments. The two browser surfaces have topics of their own: The Control Deck and The webmail portal at /portal. 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
  • 0 Votes
    1 Posts
    118 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) calls the anti-virus action that strips attachments Remove all attachments and deliver the text (Delete infected attachments only in 6.3.6's Control Panel), and under it a message whose one part is an attachment, and an S/MIME message signed as one part, now arrive as the action's note alone, where every version to 6.3.6 delivered them as they arrived with what the scanner had found in them; a message the server cannot read is deleted and each of its recipients sent the "message deleted" notice whatever the two notify settings say, while an encrypted message, and one that is one text part, are delivered as they are. In the Control Panel the sidebar has a Virus scanning group, and Ctrl+K takes every old title to its page. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026) lets the Linux server reach clamd over its Unix socket: ClamAVHost takes the absolute path of clamd's LocalSocket, such as /var/run/clamav/clamd.ctl for Debian's and Ubuntu's clamav-daemon, which listens on nothing else by default, and ClamAVPort is then not used; no connection test is offered for it on Linux, so a test message and the log show the scan (ClamAV.md). See the 6.3.8 announcement. hMailServer does not scan for viruses itself. It hands each message to an engine you install (ClamAV, ClamWin or any command-line scanner) and acts on the answer, and nothing described here is enabled on a fresh install. This topic covers the three scanners, what happens when one finds something or cannot run, the blocked-attachment list, and what 6.3.4 added: the sandbox, and the handling of mail sealed end to end. The one thing to know first is the failure mode: a configuration that looks enabled and is inert. A scanner switched on with a wrong host or a missing executable does not refuse mail and does not disable itself; when every scanner has errored the verdict is NoVirusFound, which from the message's point of view is identical to having been examined and found clean. 6.3.4 fixed one such case in the custom scanner, described below. What runs, and when Scanning happens on the delivery side, not during the SMTP conversation: the message has already been accepted and spooled by the time any scanner sees it. Which messages are scanned. For SMTP, a message is flagged when the connecting IP's security range has the virus-protection option; both default ranges ("Internet" and "My computer") have it. For mail this server fetches over POP3 or IMAP, the flag is the external account's own "use anti-virus" setting. A flagged message larger than AntiVirus.MaximumMessageSize is not scanned at all, silently; the default is 0, no limit. Two passes. The whole spool file is scanned first; then every MIME attachment is written to a temporary file and scanned separately, which is what matters for a scanner that does not decode MIME itself. If the message cannot be parsed for the second pass, error HM6001 is reported and only the whole file was scanned; if one attachment cannot be written to the temp directory, HM6002 is reported and that attachment was not examined. Concurrency. At most 10 scans run at once (MaxRunningScanners). A thread that cannot get a slot waits up to 60 seconds and then scans anyway, writing Scanning message without an available virus scanner slot. to the debug log. The read-only overview. Spam & virus filtering → Virus scanning overview in the Control Panel (Windows only) answers the question the settings page cannot: will an infected message be caught here. The three scanners All three can be enabled at once. They run in the order ClamWin, custom, ClamAV, stopping at the first that reports a virus. A scanner that errors does not stop the others. ClamAV (clamd) ClamWin (clamscan.exe) Custom scanner Switch AntiVirus.ClamAVEnabled AntiVirus.ClamWinEnabled AntiVirus.CustomScannerEnabled Default off off off Reached by TCP socket, INSTREAM one process per file one process per file Settings AntiVirus.ClamAVHost (localhost), AntiVirus.ClamAVPort (3310) AntiVirus.ClamWinExecutable, AntiVirus.ClamWinDBFolder AntiVirus.CustomScannerExecutable "Infected" is a reply matching stream…: <name> FOUND exit code 1 exit code equal to AntiVirus.CustomScannerReturnValue Timeout ClamMinTimeout 15 s to ClamMaxTimeout 90 s ExternalProcessTimeout, 300 s ExternalProcessTimeout, 300 s Virus name reported the name clamd returned always Unknown always Unknown ClamAV. Install ClamAV, run freshclam to download signatures, run clamd listening on TCP 3310, set the host and port, and press Test ClamAV connection. The button does three things in order: It sends nPING and requires PONG, so "nothing is listening" is told apart from "something else is listening". It sends nVERSION and reports what answered. It streams a harmless sample and then the EICAR test string, both from memory, and requires the first to come back clean and the second FOUND. A success looks like ClamAV 1.4.2/27700/Mon Sep 8 09:12:11 2026. Result: stream: Eicar-Signature FOUND; a failure names the step, for example Unable to connect to ClamAV server at localhost:3310. The custom scanner. Any program that exits with a distinguishable code will do. Put %FILE% where the file to scan belongs; without it, the file name is appended as the single quoted argument. The Control Panel ships presets, all of them Windows programs: Preset Command line "Infected" exit code Microsoft Defender "C:\Program Files\Windows Defender\MpCmdRun.exe" -Scan -ScanType 3 -File "%FILE%" -DisableRemediation 2 Sophos "C:\Program Files\Sophos\Sophos Anti-Virus\savscan.exe" -ss -archive "%FILE%" 3 ESET "C:\Program Files\ESET\ESET Security\ecls.exe" "%FILE%" 50 Bitdefender "C:\Program Files\Bitdefender\Endpoint Security\bdscan.exe" "%FILE%" 1 Kaspersky "C:\Program Files (x86)\Kaspersky Lab\Kaspersky Endpoint Security\avp.com" SCAN "%FILE%" 2 The 6.3.4 fix: a bare %FILE%. CHANGELOG records that a custom scanner whose command line holds a bare, unquoted %FILE% was handed the message file's path cut at its first space, and the default data directory, under C:\Program Files, has one. The scanner therefore scanned no file, answered with an exit code other than the virus one, and the message was delivered as clean. Since 6.3.4 a %FILE% outside double quotes is replaced by the path in quotes. One already inside them, "%FILE%" as the documented examples above write it, was not affected and is replaced as before. If you run an earlier release with a command line of your own, check it for an unquoted %FILE%. The same fix removes a trailing backslash from ClamWin's database and temporary folders, which escaped the closing quote of --database="..." and took the file to scan with it. The service account must be able to run the scanner. A new 6.3.4 installation on Windows runs the service as NT SERVICE\hMailServer by default; an upgrade keeps the account it had, LocalSystem unless the installer's page is answered otherwise (docs/ServiceAccount.md; the wiki's "LocalSystem by default" describes earlier releases and upgraded servers). Under the virtual account a custom scanner or ClamWin runs as that account: it reads the message in the temp folder, which is granted, and its own program and signatures, which the Users group can normally read; a scanner that needs more has to be granted it (Installing on Windows covers the account and its grants). Whichever account runs yours, the executable, its database directory and the temp directory must all be reachable by it. The symptom of getting it wrong is Unable to launch the virus scanner… reported as HM5406, once per message. For an engine with no command-line interface, the wiki points to an OnAcceptMessage event script or the HTTP filter hook. If you edit hm_settings by hand, note that the custom scanner's return value is stored under the misspelled name customviursscannerreturnvalue. Testing it end to end The test buttons check the scanner. To check the pipeline, send yourself a message with the EICAR test file attached and watch the application log: Virus found in message from [email protected]. Taking actions SMTPDeliverer - Message 4711: Message will be deleted (contained virus Eicar-Signature). Two things spoil that test on Windows. The host antivirus eats the EICAR file before hMailServer sees it, so exclude the mail store and the temp directory from real-time scanning. And your own outbound mail is not scanned by this server if it never comes back in through SMTP from an unauthenticated range, so send the test from an external address. What happens when a virus is found AntiVirus.Action What happens Delete entire e-mail (0, the default) The message is delivered to nobody. Logged as Message will be deleted (contained virus X). Delete infected attachments only (1; Remove all attachments and deliver the text in 6.3.7's Control Panel) The message is rebuilt, not edited: the first text part is kept, everything else is discarded, the VIRUS_ATTACHMENT_REMOVED server message is prepended to the body and the subject is prefixed with the VIRUS_FOUND message. Logged as Message attachments stripped (contained virus X). Since 6.3.7 a message whose one part is an attachment arrives as the note alone, where every version to 6.3.6 delivered it as it arrived, and one the server cannot read is deleted, each recipient sent the "message deleted" notice whatever the two notify settings say Understand the rebuild before you choose it: an HTML alternative, inline images and every clean attachment go with the infected one. AntiVirus.NotifySender and AntiVirus.NotifyReceiver are both off by default, and the wiki's advice is to leave them off: the sender address on a virus is virtually always forged, so a notification is backscatter. When a scanner cannot run Setting Default Meaning AVFailAction 0 0 delivers the message anyway. 1 never delivers unscanned mail AVFailRetryMinutes 15 Minutes between scan attempts while a message is held AVFailMaxHolds 16 Holds before the message is returned to its sender. 0 tells the sender immediately They are on Anti-virus settings → General, card When a scanner cannot run. The wiki describes them as stored in hMailServer.INI; since 6.3.4 the database is the settings store and a [Settings] line edited in the file is put back, so change them in the Control Panel, over REST (PUT /api/v1/settings/ini/{name}) or, on Linux, with hmailserver --set-setting NAME=VALUE. At the defaults, AVFailAction 1 gives a message about four hours to be scanned before its sender is told. The hold counter is the policy's own, not the message's try counter, because incrementing that would make the pass that finally scans the message skip DKIM signing. It lives in memory, so a restart gives every held message a fresh budget, and it is bounded at 100,000 tracked messages; beyond that messages are still held, never delivered unscanned, and HM6195 is reported. Every scanner error is reported as HM5406 with the failing scanner's own text. Blocked attachments Spam & virus filtering → Blocked attachments refuses files by name pattern. The master switch is AntiVirus.EnableAttachmentBlocking, default off. The wiki's starting list: *.exe *.scr *.pif *.bat *.cmd *.com *.vbs *.js *.jse *.wsf *.wsh *.hta *.jar *.msi *.msp *.cpl *.lnk *.reg *.ps1 *.iso *.img The attachment is not deleted. Its file name has .txt appended and its content is replaced by the ATTACHMENT_REMOVED server message, which names the original file through the %MACRO_FILE% macro, so the recipient receives invoice.exe.txt containing an explanation. Matching is a case-insensitive wildcard over the declared file name. It does not look inside archives and cannot see the real type of a file whose extension lies: it is a policy control, not a scanner. Since 6.3.4 the rewrite is logged. A rewritten message produces VirusScanner - Message 1234 from [email protected]: the attachment(s) invoice.exe matched the blocked-attachment list. Their content has been replaced with a notice and the message rewritten. Before this the rewrite left no record anywhere but inside the message itself. Sealed mail: what the list and the scanner do with it Since 6.3.4. A message encrypted end to end, S/MIME or PGP/MIME, is opaque to the server, which decides that from the body, never from the Content-Type label, so a message only labelled encrypted is blocked, stripped and scanned like any other. What opaque means exactly, and what the rest of the server does with such a message, is in the topic on end-to-end encryption and sealed mail. For the list and the scanner, none of it is a setting: The blocked-attachment list never rewrites an opaque message. In 6.3.3 and earlier blocking *.asc, *.p7m, *.p7s or *.* replaced the content of every sealed message with the notice, with no log line, and the plaintext existed nowhere else. It is now delivered as it arrived, and the application log says it was not checked against the list. A signed message is checked like any other, with its detached signature left alone. The scanner still scans it, and what it examines is ciphertext, so a clean result is not a verdict on the message; the log says its content COULD NOT BE EXAMINED for viruses. It is deliberately not treated as a scanner failure, so a server with AVFailAction 1 does not hold every encrypted message it receives; a policy that unscannable mail must not be delivered belongs in a rule or a Sieve script, not the scanner. The "delete attachments" action never strips an encrypted message. When the scanner objects to one, the message is left as it arrived and the log ends Set the anti-virus action to delete the message if such a message must not be delivered. That is not a quiet pass: the message still carries what the scanner objected to. To 6.3.6 this held for every opaque message; since 6.3.7 an S/MIME message signed as one part (application/pkcs7-mime holding signed-data, which is opaque but not encrypted) is replaced by the action's note, as any other single-part attachment is. The sandbox Since 6.3.4 (schema 6091). ClamAV says whether a file is known to be bad. What an unknown file does when it is opened takes a sandbox, a service outside this server which you run or subscribe to. It does nothing until you name one: SandboxEnabled is off, and cannot be saved on while SandboxSubmitUrl or SandboxVerdictUrl is empty. Both addresses must be https, or plain http to this machine. After the anti-virus scan and before the rules, a message that no signed-in user of this server submitted, with an attachment whose extension or media type is in SandboxTypes, is held: in no mailbox, not in the quarantine, and in nobody's digest. The default types are: exe,dll,scr,js,jse,vbs,wsf,hta,msi,bat,cmd,ps1,jar,lnk,iso,img,docm,xlsm,pptm, doc,xls,ppt,rtf,pdf,zip,7z,rar The sandbox says What happens every file clean delivered, with an X-hMailServer-Sandbox header reading clean (<n> file(s), <seconds>s) any file malicious, or suspicious at or above SandboxScoreThreshold (70) quarantined with the verdict as the reason; returned to its sender with 5.7.1 while QuarantineEnabled is off an error, once every file has answered, or no answer within SandboxTimeoutMinutes (15) SandboxTimeoutAction: deliver (0, the default) delivers with a header saying so; bounce (1) returns it to its sender The contract. One POST per file to SandboxSubmitUrl as multipart/form-data with four fields (file, filename, sha256, message_id), then a GET to SandboxVerdictUrl every SandboxPollSeconds (30), answered with JSON such as {"state":"done","verdict":"clean","score":3,"detail":"opened, nothing happened"}. With SandboxToken set, every request carries Authorization: Bearer <token>; the token is not in the backup archive. No vendor adapter is shipped. The document notes that a service which shares what it is sent, as VirusTotal shares uploads with its subscribers, is a place the attachments of the mail you receive go. What is exempt. What your own signed-in users submit (authenticated SMTP, the webmail, the send API) is not uploaded unless SandboxInternalMail is on. The document states the threat model of that default: a stolen password used to send attachments to colleagues is not examined by the sandbox, though ClamAV, the blocked-attachment list and compromised-account detection still apply. Limits. An attachment larger than SandboxMaxSizeKB (20480) is not submitted. A message with more files to submit than SandboxMaxFiles (10) is not held, and SandboxTimeoutAction decides it at once. Archives are submitted whole; links are not opened anywhere. A clean or malicious verdict is remembered by the file's SHA-256 for SandboxCacheHours (24), server-wide. What the administrator sees. GET /api/v1/sandbox/held lists what is held; POST /api/v1/sandbox/held/{queue_id}/release delivers without the verdict and DELETE /api/v1/sandbox/held/{queue_id} discards, both on the audit trail. The Control Deck's queue page has a Held for a sandbox box, and hmctl queue list has a held column. The settings are on the Control Panel's Anti-virus page, Sandbox tab (Windows only), and on PUT /api/v1/settings. Rules, and the message store Since 6.3.4 the mail flow rules can match the attachment's name and, in the outbound content policy, its type read from its bytes, so a rule can hold or refuse what the blocked-attachment list would only rename; that is in the topic on mail flow rules. The message store options added in 6.3.4 change nothing for a scanner: a message is scanned as plain RFC 822 in the queue and only placed in the mailbox in its stored form afterwards. A scanner pointed at the data directory from outside the server reads a compressed or encrypted container, not a message. Error codes Code Meaning HM5406 An enabled scanner errored; the detail names the scanner and the failure HM6001 The message could not be parsed, so only the whole file was scanned HM6002 An attachment could not be written to the temp directory for scanning HM6195 100,000 messages are already held for want of a scan; further holds are not counted HM7272 The sandbox called a message malicious and the quarantine could not store it; it is held again, never delivered Where to read more Wiki: https://gitlab.com/hmailserver/hmailserver/-/wikis/Stopping-Viruses https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/Sandbox.md https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SealedMail.md Public documentation: https://www.progressiverobot.com/hmailserver-documentation/#11-stopping-viruses, https://www.progressiverobot.com/hmailserver-documentation/#111-clamav and https://www.progressiverobot.com/hmailserver-documentation/#112-blocked-attachments Upgrading to 6.3.4 and the 6.3.4 security announcement
  • 0 Votes
    1 Posts
    133 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) reads a link's host that is not ASCII by UTS 46, and an HTML part's character references as a browser does, for the URL entries of the allow and block lists, so a message whose links escaped a list that way can now be tagged or held; a URL entry written in Unicode is stored in its xn-- form and covers a link written either way, and one with a label UTS 46 refuses is refused when it is saved. In the Control Panel the Spam filtering group's six allow and block lists are under Lists, and Ctrl+K takes every old title to its page. See the 6.3.7 announcement. This topic covers five layers that sit beside the score-based spam tests rather than inside them: the allow and block lists at three levels, the quarantine digest, spam traps, country blocking and impersonation protection. All five are new since 6.3.4, and the one thing to know first is that every one of them is off, or inert, until an administrator sets it: a server upgraded to 6.3.4 and left alone has no list entries, sends no digest, has no traps, judges nothing by country and checks no display name. The allow and block list: one table, three levels Since 6.3.4 (schema 6046). The older lists (the anti-spam white list, Blocked senders, the greylisting white list) act during the SMTP conversation, for every recipient at once. The allow and block list is per recipient. It is one table, hm_senderlist, with a scope on each row: each account's own safe and blocked senders, a domain's list, and the server's. Entry Covers [email protected] That one address, in any case. No wildcards example.com example.com and every subdomain; not notexample.com, not example.com.attacker.net @example.com The same as example.com, and stored as example.com https://bad.example/anything A URL: stored as bad.example/, covering a link in the message to that host or a subdomain, whoever sent it An entry covers a message when it matches either the envelope sender or the address in the From header, because mail sent through a mailing service carries the service's address in the envelope and the brand's in the header. A message with the null sender is never decided by any list. URL entries are in 6.3.4. CHANGELOG's 6.3.4 section lists them, and they are consulted only when neither address decided anything. The closing section of docs/SenderListsAndDigests.md, which says "URLs are not list entries", is older text left in the file. What allow and block do. The lists are read at local delivery, once per recipient, after the spam tests have run. The message was classified as spam It was not Allowed Delivered unmarked for this recipient: the spam flag, the X-hMailServer-Spam and reason headers and the subject tag are removed from this copy Delivered as it is Blocked Held in the quarantine for this recipient when the quarantine is on; marked as spam and delivered when it is not The same A block never deletes, and no score is recorded on a blocked message, because nothing measured it. A message released from the quarantine is never held again by a list. The other recipients of the same message are not affected. Precedence is nearest first: the account, then its domain, then the server. The first of the three that has anything to say about the sender is the only one consulted, so an account's allow beats a domain's block. That is the reverse of Microsoft 365's order, and deliberate: the person the message is addressed to can tell whether they wanted it. An administrator who needs a sender stopped for everyone still has Blocked senders, which nobody's own list overrules. Inside one list the more specific entry wins (an address beats a domain, a longer domain beats a shorter one), and on an exact tie the block wins. Neither address is proof. Both are whatever the sender wrote, so an allow entry is as forgeable as the address it names. The document's advice: keep allow entries to correspondents whose domains publish DMARC, and prefer a single address to a whole domain. List Webmail Control Panel (Windows) Control Deck REST The account's own Settings → Safe senders none none /api/v1/me/sender-list A domain's none Domain → Allow and block list tab Domain editor → Allow and block list /api/v1/domains/{domain}/sender-list The server's none Spam & virus filtering → Allow and block list Lists → Allow and block list /api/v1/sender-list Over COM (Windows only) the domain's list is Domain.SenderList and the server's Settings.AntiSpam.SenderList. There is no administrator path to an account's own list. Block this sender and Report phishing in the webmail now add the sender to the account's blocked senders; they used to write a rule into the Sieve script, and rules written that way are left where they are. A list holds at most a thousand entries; adding a value already there rewrites the entry (200 rather than 201). Expiry can be set over REST, in the Deck and in the Control Panel's two list editors (a switch and a date); the webmail does not offer it. docs/SenderListsAndDigests.md still says the Control Panel has no expiry; that passage predates the change, which the repository's roadmap records as made after 6.3.3. The quarantine digest Once a day, one plain-text message goes to each account of a domain that has something newly held for it, listing each message's subject, sender, when it was held and why, with a link that delivers each entry, a link that delivers everything listed, and a footer link that stops the digest (also the List-Unsubscribe header). An empty digest is never sent, and an account that has had one is not sent another for twenty hours. The message comes from the domain's postmaster with the null envelope sender and Auto-Submitted: auto-generated, and never asks for a password. Setting Default QuarantineDigestBaseUrl, the address the links point at, such as https://mail.example.com empty: no digest is sent QuarantineDigestLinkHours, 1 to 720 72 The domain's switch and hour (quarantine_digest, quarantine_digest_hour; 0 to 23, local time), which an account may override off; 7 All three are in the database, none in hMailServer.ini. The quarantine itself (QuarantineEnabled) has to be on, and the address must be an absolute http:// or https:// URL of the listener the webmail is served from. When it is sent. The task wakes every five minutes and, within the digest hour, sends only its share, so a domain of a thousand accounts is spread across the hour. At most five hundred digests go in one run. Over COM (Windows only), AntiSpam.SendQuarantineDigests() sends now, ignoring the hour and the twenty-hour rule and nothing else. The links. Each is 32 random bytes recorded against one account and one entry, usable once, and no longer valid after QuarantineDigestLinkHours. Following a link does not act on its own: it opens a page with one button, because mail security products follow every link in a message, and a link that released mail on its own would have one of them releasing every held message. Stopping the digest does not stop mail being held. Spam traps Since 6.3.4 (schema 6058). A trap is an address that was never real and never given to anybody, so a message to one comes from a harvested, bought or guessed list. At RCPT TO a trap is refused exactly as any unknown address is, 550 Unknown user, made by the same code, so the sender cannot tell. The check runs before the delivery lookups, so a catch-all, greylisting or a backup-MX callout cannot answer first. What happens Setting The connecting address Banned through the server's own auto-ban, as an expiring range named Auto-ban: spam trap <address> SpamTrapBanMinutes, 1440. 0 bans nobody The envelope sender Added to the server's block list with an expiry, only when its domain's SPF passes for the connecting address SpamTrapBlockDays, 30. 0 blocks nobody The hit Recorded in hm_spamtraphits and counted in hmailserver_spam_trap_hits_total SpamTrapHitRetentionDays, 90 The application log gets one line per hit and one per ban: "Auto-ban: 203.0.113.9 is banned for 1440 minutes after addressing the spam trap [email protected]" The packaged fail2ban filter matches that line, so a Linux installation that runs fail2ban with the packaged jail turned on bans the address at its own firewall as well. The jail ships disabled; the wiki's Installing on Linux page turns it on with: printf '[hmailserver]\nenabled = true\n' > /etc/fail2ban/jail.d/hmailserver.local systemctl reload fail2ban The SPF condition. As first written, in the source tree, a trap put the envelope sender of every message sent to it on the block list, whatever that sender was; spam traps are new in 6.3.4, which carries the fix, so no release did. CHANGELOG records why that changed: the envelope is whatever the client types, so one connection naming a bank's address could have had the bank's real mail held or marked for every mailbox. Now only an SPF pass blocks; the address is banned either way. The wiki's summary in Stopping Spam §10.15 does not mention the condition; docs/SpamTraps.md and CHANGELOG do. Who is never banned: an authenticated session, an address whose IP range does not run the spam tests, an address or sender the anti-spam white list covers, an incoming relay, and an address on AutoBanNeverBan. The sender is not blocked for the null sender, for a sender claiming a domain this server hosts, or where the server's allow list already names it. Training. A trap hit trains nothing by itself, because no content arrives. With SpamTrapTeachSpamAssassin on (off by default; it needs SpamAssassin enabled and a spamd started with --allow-tell), a message sent to a real mailbox in the same transaction is taught to spamd as spam. Declaring a trap. It must be an address in the domain it is declared on, and one nobody uses: an account, alias or list with that address is refused, and none can be given a trap's address while the trap exists. Where How Control Deck Domain editor → Spam traps; the Spam traps view for the record across every domain REST GET/POST /api/v1/domains/{domain}/spam-traps, DELETE /api/v1/spam-traps/{address}, GET/DELETE /api/v1/spam-traps/hits Command line hmctl spamtrap create --domain example.com --set [email protected], hmctl traphits list PowerShell Add-HmSpamTrap, Get-HmSpamTrapHit COM (Windows only) Domain.AddSpamTrap, Utilities.GetSpamTrapHits A good trap is an address never published anywhere, or one published once, long ago, and unused for years (give it a year of refusals first). A bad trap is a typo of a real address such as saels@, or any address in a domain with a catch-all: there every other unknown address is accepted and the trap is not, which is the difference a sender can measure. Clearing the record of hits undoes nothing; the bans and blocks stay until they expire or are removed. The traps are in the backup with their domain; the record of hits is not. Country blocking Since 6.3.4 (schema 6052). Two kinds of policy, in one grammar. A policy per listener family (SMTP, IMAP, POP3, and the HTTP listeners) is applied when a connection is accepted: before the greeting, before the TLS handshake and before anything touches the database. A policy per domain is applied when somebody signs in to an account of that domain, on any protocol, and covers OAuth2 bearer tokens, client certificates, Kerberos tickets, passkeys and single sign-on as well as passwords. Value What it means (empty) Nothing is judged by country. The default everywhere allow:GB,IE,FR Only these countries may connect, or sign in block:CN,RU These countries may not connect, or sign in The database is yours to supply. Point GeoIpDatabaseFile at a MaxMind .mmdb file, such as /var/lib/GeoIP/GeoLite2-Country.mmdb or C:\GeoIP\GeoLite2-Country.mmdb. The server ships none and never fetches one. Replace the file in place and the server reloads it within five minutes. GeoIpDatabaseMaxAgeDays (45) is how old the file's build date may be before the log says so; a stale database is still used. What is exempt. CountryBlockingExemptRanges defaults to the loopback addresses and every private network: 127.0.0.0/8,::1/128,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,100.64.0.0/10,fe80::/10,fc00::/7 There is no second list compiled into the server. If you empty it, do so with country_policy_http off, or the Control Deck is the next thing you lose. An open session is never disturbed; a policy takes effect on the next connection. Which way it fails. With no database at all (none configured, or the file missing or unreadable) everything is allowed, whatever the policies say, and the reason is logged once. With a database loaded that has no country for an address, an allow list refuses it and a block list allows it. What a refused client is told: nothing. A refused connection is closed with no banner. A refused sign-in gets the ordinary invalid-credentials reply, byte-identical to a wrong password, and is counted against neither the account's lockout nor the address's auto-ban, since the person refused is very often the account's real owner, travelling. Your copy is in the application log: Country blocking: refused SMTP from 203.0.113.9 (CN) for connection. Country blocking: refused sign-in from 203.0.113.9 (CN) for example.com. The counters are hmailserver_country_blocked_total and hmailserver_country_blocked_by_country_total{country="CN"}, and the report is GET /api/v1/reports/countries. Setting it up. In the Control Deck, Settings → Server holds the server's settings and Domains → (a domain) → Country blocking the domain's policy. From a shell: hmctl settings set --set geoip_database_file=/var/lib/GeoIP/GeoLite2-Country.mmdb hmctl settings set --set country_policy_smtp=block:CN,RU hmctl domain update example.com --set active=true --set country_policy=allow:GB,IE The REST keys are country_policy_smtp, country_policy_imap, country_policy_pop3, country_policy_http and, on a domain, country_policy. Over COM (Windows only) it is Settings.CountryPolicy("smtp") and Domain.CountryPolicy. What it does not do. It is not a per-message control. It says nothing about a proxy: behind HAProxy with the PROXY protocol, the address judged is the proxy's, so put the policy on the proxy. And it is only as true as the database; it is not an authentication control. Impersonation protection Since 6.3.4 (schema 6087). Per domain, off on every existing and new domain. It runs at local delivery on the recipient's own copy, only on mail from outside, and makes two checks. A borrowed name. The From display name, folded, equals the name of an active account of the recipient's domain or of a protected person, while the address is not theirs. A lookalike domain. The From domain, and the envelope sender's when it differs, is equal to a hosted domain, an alias domain or a protected partner after the confusable folding while not equal before it (examp1e.test, exarnple.test), or is one Damerau-Levenshtein edit away inside one label (examplee.test, exampel.test). Distance 1 is the rule and 2 is not. A DMARC pass changes nothing here: the attacker publishes the lookalike's records exactly as a legitimate domain would. X-hMailServer-Impersonation: name "Ada L0velace" matches Ada Lovelace ([email protected]); from [email protected] X-hMailServer-Impersonation: domain examp1e.test looks like example.test; from [email protected] Windows and Linux differ in one step of the folding. Unicode NFKC normalisation, which turns a full-width or ligature spelling into its letters, runs on Windows. It does not run on Linux, which has no Unicode tables in this build; there the step is skipped and the confusable table does the rest. The other steps run on both. What slips past. Word order and decorations are not folded: Holloway, Chris and Chris Holloway (CEO) are not Chris Holloway. A name carried only in the local part of the address is not compared. And a domain with an account named Support or Info will have every vendor's "Support" flagged. impersonation_action What happens off (0) Nothing is read flag (1) The copy gets the X-hMailServer-Impersonation header. The subject is left alone and the sender's DKIM signature is untouched. The webmail shows a warning bar and a Suspicious sender mark quarantine (2) The copy goes to the quarantine with the reason and score 0. A release does not re-check. With QuarantineEnabled off, the domain gets flag instead What is exempt: a sender the recipient's own safe-sender list allows (or the domain's or server's allow list, where nothing nearer decides), and a From domain that is itself hosted here or a protected partner. Nothing else: a person's own name from their personal address at another provider is flagged until that address is on the safe-sender list. The setting is on the Impersonation protection tab of the Control Panel's domain dialog (Windows only), in the Deck's domain editor, and on PUT /api/v1/domains/{domain} as impersonation_action, protected_persons and protected_partners. Each list holds at most 2,000 characters. Where to read more The score-based pipeline these layers sit beside is in the anti-spam stack end to end, the outside-sender tag in the topic on mail flow rules, and the auto-ban a trap uses in IP ranges, auto-ban, lockout and tarpits. For the Deck itself, see The Control Deck. Wiki, §10.10a and §10.15: https://gitlab.com/hmailserver/hmailserver/-/wikis/Stopping-Spam https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SenderListsAndDigests.md https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SpamTraps.md https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/CountryBlocking.md https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/ImpersonationProtection.md Public documentation: https://www.progressiverobot.com/hmailserver-documentation/#104-white-lists
  • 0 Votes
    1 Posts
    149 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) reads a DNS lookup that fails as a failure, not as a name with no addresses: the HELO and PTR tests no longer score a name they could not look up, a DNS blacklist and SURBL no longer read a failed lookup as "not listed", and a DKIM selector that is a CNAME whose query failed is TempFail, not PermFail. SURBL reads a link's host that is not ASCII by UTS 46 and an HTML part's character references as a browser does, and SpamAssassin is sent a collected message's own Return-Path, so its SPF and envelope rules may score such mail differently. The DMARC check's pass now also decides whether a forward of mail from outside whose From names a hosted domain's organisation is given an envelope sender in that organisation, and no pass is recorded for a message the check did not judge: DMARC off, a sender on the white list or an IP range with spam protection off, a message over AntiSpam.MaximumMessageSize, or one the tests before it had already scored to the higher threshold. In the Control Panel the sidebar has a Spam filtering group, with its six allow and block lists under Lists, and a Virus scanning group, and Ctrl+K takes every old title to its page. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026) lets Rspamd learn from the Junk folder beside SpamAssassin: a message a user moves into Junk is taught as spam, and one moved out as ham, to the filter MoveLearningBackend names, SpamAssassin by default and taught as before, and nothing is sent to Rspamd until it is chosen and its controller and classifier are set (JunkFolderLearning.md). See the 6.3.8 announcement. This topic walks through every layer hMailServer 6.3.4 puts between an SMTP session and a mailbox: the two thresholds, the twelve tests in the order they run, greylisting and its 6.3.4 hardening, the DNS and URL blocklists, SPF, DKIM, DMARC and ARC, SpamAssassin, and the quarantine. The one thing to know first: none of these tests is a verdict on its own. Each failing test adds a score, two thresholds decide what the total means, and three kinds of session are never tested at all: an authenticated session, a sender or IP on the anti-spam white list, and any session from an IP range with spam protection switched off. Where the decisions are made Everything happens in one of two moments: before the body (at MAIL FROM or RCPT TO) and after the body (after DATA or the last BDAT). The pre-transmission tests run at MAIL FROM by default: DNSBLChecksAfterMailFrom defaults to 1, and 0 moves them to the first RCPT TO, which leaks less to an address-probing client but accepts more of the conversation first. A session from a configured incoming relay has both phases deferred to after DATA. Who is never tested. SMTPConnection::GetDoSpamProtection_ returns false the moment the session is authenticated, so your own users' outbound mail is never scored; the sending limits and rate limits are the controls for that. A sender or IP on the anti-spam White list skips the tests entirely, not merely the scoring: SPF is not evaluated, no Authentication-Results verdict is recorded and no reason header is written. The read-only overview. Spam & virus filtering → Spam filtering overview in the Control Panel (Windows only) shows on one screen which checks are on, what each scores, what the thresholds then do, and which checks are switched on but cannot run. The score and the two thresholds Both thresholds are on Spam & virus filtering → Anti-spam settings → General. Setting Default At or above it 0 means AntiSpam.SpamMarkThreshold 5 Delivered but tagged: X-hMailServer-Spam: YES, X-hMailServer-Reason-* headers and, optionally, a subject prefix Never mark AntiSpam.SpamDeleteThreshold 20 Refused during the conversation, or quarantined if quarantine is on and the verdict came after the body Never refuse AntiSpam.MaximumMessageSize 1024 KB Post-transmission tests are skipped for a larger message No limit of its own; the MIME parser's 80 MB ceiling still applies The higher of the two thresholds is also the early-out: SpamTestRunner::RunSpamTest stops calling tests once the running total reaches it, which is why a message condemned by the blocked-senders list never pays for a DNS lookup. With both thresholds at 0 the early-out is skipped, because greylisting's SPF bypass needs the SPF result and SPF is the sixth test. With AddHeaderSpam and AddHeaderReason both on (the defaults), a marked message carries: X-hMailServer-Spam: YES X-hMailServer-Reason-1: Blocked by SPF. (example.com does not designate 203.0.113.9) - (Score: 3) X-hMailServer-Reason-2: Rejected by Spamhaus. - (Score: 3) X-hMailServer-Reason-Score: 6 Subject: [SPAM] Your invoice Only failing tests get a Reason-N line. X-hMailServer-Reason-Score is the sum of the failing tests, and with AddHeaderReason off it is not written at all. The subject prefix is off by default; its text is [SPAM] (antispamprependsubjecttext). Start by tagging, not deleting. The wiki's advice is a fortnight with the delete threshold at 0, reading what got tagged, before turning deletion on well above anything legitimate has scored. Every test, in the order it runs This is the registration order in SpamTestRunner::LoadSpamTests; nothing reorders it at runtime. Tests 1 to 6 run before the body, 7 to 12 after it. # Test Switched on by Default Default score 1 Blocked senders a non-empty Blocked senders list list empty 100 on a new entry 2 DNS blacklists an active row on DNS blacklists two rows, both inactive 3 3 HELO host AntiSpam.CheckHostInHelo off 2 4 PTR record AntiSpam.CheckPTR off 1 5 Sender MX AntiSpam.UseMXChecks off 2 6 SPF AntiSpam.UseSPF off 3 7 SURBL an active row on SURBL servers one row, inactive 3 8 DKIM AntiSpam.DKIMVerificationEnabled off 5 9 ARC AntiSpam.ArcFilteringEnabled, a trusted-sealer list and DMARC on off minus the DMARC failure score 10 DMARC AntiSpam.DMARCEnabled on 5 11 SpamAssassin AntiSpam.SpamAssassinEnabled off 5, or the merged score 12 Filter hook a non-empty FilterHookUrl empty the engine's score; 100 for a reject DMARC is the only test on by default, and at 5 a DMARC failure marks a message without refusing it. That is the shipped posture: tell the recipient, do not destroy the mail. What makes a test fail matters as much as its score. SPF scores only a hard Fail. DKIM scores only PermFail; a message with no signature is None, not a failure. DMARC scores a p=reject or p=quarantine policy the message failed and, separately, a message with more than one From header field; p=none failures are logged and score nothing. The blocked-senders list never matches the null sender, and its 550 says only "Sender address is blocked." without naming the entry. Every DNS-backed check fails open: a lookup that does not complete is read as "not spam". Since 6.3.7 a lookup that fails is a failure, not a name with no addresses: the HELO and PTR tests no longer score a name they could not look up, a DNS blacklist and SURBL no longer read a failed lookup as "not listed" (the blacklist says so in the TCP/IP log and, at most every 15 minutes, the application log), and a DKIM selector that is a CNAME whose query failed is TempFail, not PermFail, so it no longer scores. Greylisting Greylisting is not a score. It is a temporary refusal of a triplet (sender address, recipient address, connecting IP) this server has not seen before. The settings are on Anti-spam settings → Greylisting. Setting Default Meaning AntiSpam.GreyListingEnabled off Master switch AntiSpam.GreyListingInitialDelay 30 minutes How long a new triplet is refused for AntiSpam.GreyListingInitialDelete 24 hours An unconfirmed triplet is forgotten after this AntiSpam.GreyListingFinalDelete 864 hours (36 days) A confirmed triplet's expiry, pushed forward on every accepted message AntiSpam.BypassGreylistingOnSPFSuccess on Needs the SPF test enabled and to have run AntiSpam.BypassGreylistingOnMailFromMX off Costs an A and an MX lookup per message A greylisted recipient gets 451 Please try again later at RCPT TO; when the envelope sender is empty the refusal is postponed to the end of DATA and answered 451 (docs/Greylisting.md; the wiki prints 450), because an empty sender at RCPT TO is very often another server's callout. Greylisting is skipped for a recipient domain with Enable greylisting for this domain unticked, for an IP on the greylisting white list, and for a sender or IP on the anti-spam white list. The wiki records that GreylistingEnabledDuringRecordExpiration=0 did nothing at 6.2.28, so greylisting stays in force during the expiry sweep whatever it says. Hardened since 6.3.4 (schema 6072; docs/Greylisting.md The triplet key no longer truncates. Addresses used to be stored cut to 200 characters, so two long addresses sharing their first 200 were one triplet. Each is now keyed by an MD5 digest of the whole address with its domain lower-cased. This is the one change at the defaults: on PostgreSQL, and on MySQL with a binary collation, [email protected] and [email protected] were two triplets and are now one, so a sender waits once less, never once more. Old rows are not rewritten at upgrade. A sliding wait and an abuse lock, both off. With GreyListingSlideOnEarlyRetry on, each retry before the window opens moves the window to now plus the initial delay. GreyListingAbuseThreshold (0; the operator who asked for it uses 20) is the attempt answered 550 5.7.1 Too many delivery attempts before the greylisting delay had passed. ..., with the triplet locked for GreyListingAbuseLockMinutes (60). It counts attempts, not retries: a server delivering twenty queued messages to one recipient down one connection makes twenty attempts in seconds. A triplet that has passed once is never refused. Clear greylisting triplets lifts every lock, at the cost of greylisting every sender once more. The greylisting bypass list (/api/v1/greylisting-bypass, table hm_greylisting_bypass a domain such as example.com, or a suffix such as .mail.example.com, matched on the envelope domain, the HELO host or either, passes greylisting only when SPF, DKIM or DMARC passes for that name. At least one requirement is needed. An entry requiring DKIM or DMARC cannot be judged at RCPT TO, so its greylisting runs after DATA, where a 451 or 550 ends the transaction for every recipient. The test an entry requires must be switched on: an entry requiring DKIM while DKIM verification is off (the default), or DMARC while DMARC is off, can never be satisfied, so the message is greylisted at RCPT TO as if the entry were not there; for an entry matched on the HELO, the SPF check of the HELO name is made only while SPF is switched on. It skips greylisting only; every spam test still runs. BypassGreylistingOnSPFSuccess, by contrast, covers every SPF pass in the world, a hijacked mailbox at a large provider included. DNS blacklists and SURBL Spam & virus filtering → DNS blacklists seeds two rows, both inactive until you tick them: DNS host Expected result Reject message Score zen.spamhaus.org 127.0.0.2-8\|127.0.0.10-11 Rejected by Spamhaus. 3 bl.spamcop.net 127.0.0.2 Rejected by SpamCop. 3 The expected-result field takes | for alternatives, a range on the last octet and wildcards; a malformed range is error HM5342. Every lookup, match or not, is written to the TCP/IP log, which is how to prove a listing to a sender who disputes it: DNS lookup: 9.113.0.203.zen.spamhaus.org, 1 addresses found: 127.0.0.4, Match: 1 The wiki warns that public mirrors of the large lists rate-limit or refuse resolvers that use a public DNS service; if a list stops matching, check its operator's terms first. SURBL checks the links in the body, so it runs after DATA. Collection stops once more than 15 distinct addresses are found, the run for one list aborts after 10 seconds, and, to 6.3.6, the first failed lookup abandoned the rest of the list; since 6.3.7 a failed lookup is a failure rather than "not listed", and SURBL goes on to ask the message's other names. One row is seeded inactive: multi.surbl.org, "Rejected by SURBL.", score 3. SPF, DKIM, DMARC and ARC SPF (AntiSpam.UseSPF, off, score 3). Evaluation is bounded at 10 DNS-querying terms and at SpfVoidLookupLimit void lookups (default 2; 0 switches that limit off). Since 6.3.4 the evaluator follows RFC 7208, ported from upstream hMailServer, in place of the RFC 4408 library the server carried: 6.3.3 and earlier Since 6.3.4 DNS Its own lookups; DNSServer, the cache and the timeout were ignored on Windows The server's own resolver settings apply Linux Every check was TempError (HM6406), so DMARC lost its SPF half Works Loopback clients 127.0.0.0/8 and ::1 passed before any policy was read No exemption Authentication-Results Everything but pass and fail written as neutral Each of the seven results under its own keyword Nothing is scored differently: only a fail scores, as before. The loopback row is the one to check when upgrading. CHANGELOG says that where the SPF test or DMARC runs for a loopback client, mail from a domain whose policy does not name the address is judged like any other, so a content filter that re-injects mail should authenticate, or connect from an IP range with anti-spam off. DKIM (AntiSpam.DKIMVerificationEnabled, off, score 5). RSA keys shorter than 1024 bits are refused outright, and rsa-sha1 signatures are invalid unless DkimAcceptSha1=1. DMARC (AntiSpam.DMARCEnabled, on, score 5). More than one From field is scored before any alignment is computed, because DKIM signs the last From while the header reader answers with the first. ARC (AntiSpam.ArcFilteringEnabled, off; AntiSpam.ArcTrustedSealers, empty). Its only output is an offset of exactly minus the DMARC failure score, applied when a chain whose every sealer is trusted validates and DMARC is about to fail. With the list empty it does nothing, deliberately: anyone can fabricate a chain and seal it with their own keys. Matching is exact. Since 6.3.4: policy the score cannot express (schema 6073; all off by default; docs/SmtpPolicy.md Setting What it does UseSPFSoftFailScore, UseSPFTempErrorScore, UseSPFPermErrorScore Scores of their own for SoftFail (~all), TempError and PermError. 0 is "not scored", as before SPFTempErrorDefer Answers a TempError with 451 4.4.3, so a resolver blip defers the message. Never a 451 on PermError RejectMultiRcptNullSender A null sender's second and every later distinct recipient, and its DATA, are refused 550 5.7.1. A single-recipient null sender is never touched BanMultiRcptNullSender, BanMultiRcptNullSenderHours Also bans the address through the auto-ban, once per session, for 24 hours by default (1 to 8760); never an address on AutoBanNeverBan or an incoming relay. Does nothing while RejectMultiRcptNullSender is off The domain white and black list (Spam & virus filtering → Domain white and black list; /api/v1/domain-policy-list) is keyed by the envelope domain or the HELO suffix rather than by IP, for the correspondent whose sending hosts change every week. It is consulted at MAIL FROM for unauthenticated sessions of a range that runs the spam tests. Black is 550 5.7.1 Mail from <name> is refused by this server's policy. before anything else runs. White skips greylisting, is not deferred for a TempError and is not refused or quarantined on the score, while every test still runs and the mark threshold still marks. It never overrides authentication. The list is ASCII, so a U-label such as bücher.example is refused, and hmctl has no verb for it. SpamAssassin and the filter hook hMailServer speaks the spamd protocol over TCP to any SpamAssassin daemon, on this machine or on a Linux host. Set AntiSpam.SpamAssassinHost (127.0.0.1) and AntiSpam.SpamAssassinPort (783) and press Test SpamAssassin connection. AntiSpam.SpamAssassinMergeScore (off) uses the integer part of SpamAssassin's own score= in place of the fixed AntiSpam.SpamAssassinScore (5). The timeout moves between SAMinTimeout and SAMaxTimeout (30 and 90 seconds) with load. SpamAssassinLearnOnMove (off) tells spamd that a message moved or copied into Junk is spam, and one moved out of it ham, with spamc's TELL, which spamd refuses unless it was started with --allow-tell. An unresolvable host is HM5507; a scan that does not finish in time is HM5508, and the message is accepted with no verdict; a message that cannot be reloaded after the scan is HM5509 and is left as SpamAssassin wrote it. FilterHookUrl puts rspamd, or anything answering JSON over HTTP, in the path of every accepted message; http://127.0.0.1:11333/checkv2 needs no adapter. FilterHookTimeoutSeconds is 10, and with FilterHookFailClosed (off) turned on an unanswered check scores FilterHookRejectScore (100) and reports HM5541. Quarantine, and what a refused sender sees Quarantine (QuarantineEnabled, off; QuarantineRetentionDays, 30) changes what the delete threshold does after the body: the message is answered 250 Queued for delivery and held on Spam & virus filtering → Quarantine. A verdict reached before the body has no message to hold, so it stays a refusal, and a quarantine that fails to store falls through to refusing, so a spam refusal never becomes silent deletion. Since 6.3.4 the quarantine also holds what a mail flow rule sends there (Rule: <name>), what impersonation protection holds and what a recipient's blocked-senders list holds. Per-account overrides, on the account dialog's Spam tab, re-judge that account's own copy at delivery. They only read X-hMailServer-Reason-Score, and only while Add X-hMailServer-Reason header is on; with it off the value could be the sender's own, so the overrides decline to act. Moment Client sees Score over delete threshold, before the body 550 5.7.1 <text of the first failing test> Score over delete threshold, after the body 554 5.7.1 <text of the first failing test> Same, but quarantined 250 2.0.0 Queued for delivery Greylisted, real sender 451 Please try again later. Greylisted, null sender 451 at end of DATA (the wiki prints 450) Per-test timings go to the debug log (Spam test: SpamTestSPF, Score: 3, Time: 41 ms); a test taking 10 seconds or more is promoted to the application log. Because every DNS-backed check fails open, a resolver that has stopped answering looks like a run of clean mail, so DKIM, DMARC and SURBL, and since 6.3.7 a DNS blacklist, each write one application-log line at most every 15 minutes when their lookups stop completing. A configuration that works The wiki's order for a small server: Leave the thresholds at 5 and 20 and leave DMARC on. Watch for a fortnight. Activate zen.spamhaus.org (score 3). Turn on SPF (score 3) and DKIM verification (score 5). With a DNSBL hit they reach 11, still under the delete threshold. Turn on AuthenticationResultsEnabled, so every delivered message records what the server concluded. Add greylisting if your users can tolerate a first-message delay, leaving bypass on SPF success on. Only then add SpamAssassin or an external engine, and consider raising the delete threshold rather than lowering it. What this topic does not cover, and where to read more The per-recipient allow and block lists, the quarantine digest, spam traps, country blocking and impersonation protection are in allow and block lists, digests and spam traps, and viruses in viruses and dangerous attachments. The other direction is Being trusted by the servers you send to. The Control Panel and COM are Windows-only; the 6.3.4 switches are also on the Settings view of the Control Deck and PUT /api/v1/settings/antispam. Wiki: https://gitlab.com/hmailserver/hmailserver/-/wikis/Stopping-Spam https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/Greylisting.md https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SmtpPolicy.md Public documentation: https://www.progressiverobot.com/hmailserver-documentation/#10-stopping-spam and https://www.progressiverobot.com/hmailserver-documentation/#265-the-anti-spam-pipeline The 6.3.4 security announcement and Upgrading to 6.3.4