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
  • 0 Votes
    1 Posts
    136 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
    150 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
  • 0 Votes
    1 Posts
    119 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) adds the PROXY protocol to the IMAP and POP3 listeners: each takes the client's address from a PROXY header as the SMTP listener does, from the peers on a list of its own, and both are off by default; their switches and peer lists are on the IMAP and POP3 cards of the Control Panel's Protocols page. The SMTP listener's PROXY protocol and XCLIENT are a card on Incoming relays, not on Server limits & expert settings, and Ctrl+K takes every old title to its page. A state-changing REST request that carries a credential is now refused when the browser reports it as cross-site (Sec-Fetch-Site, or an Origin that is not this server's); scripts, hmctl and the PowerShell module send no such header and are unaffected. The ACME HTTP-01 challenge responder, which served one connection at a time, serves up to 64 at once. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026) no longer charges a failed sign-in through a proxy to the proxy: at the web services listener one from loopback or an address in WebServicesTrustedProxies, and at the REST listener an account's from loopback, is counted against the name alone, never towards a ban of the address, where up to 6.3.7 a ban could be written that shut the proxy, or 127.0.0.1, out of the SMTP, POP3, IMAP and ManageSieve listeners. The web services listener now also refuses, before the TLS handshake, an address whose IP range allows none of SMTP, POP3 and IMAP, an auto-ban's or one of your own, though never loopback or an address in WebServicesTrustedProxies: where mail reaches the server only through a relay or a VPN and the Internet range allows nothing, that range now closes ACME's http-01 validation when this listener answers it, MTA-STS, CalDAV, CardDAV, Autodiscover, autoconfiguration and the Web Key Directory to the addresses it holds, so check before upgrading that the certificate authority, the sending MTAs and these services' clients do not come from it. See the 6.3.8 announcement. This topic is for a server that does not face the internet alone: behind a hosted spam filter or a filtering appliance, behind a load balancer, or with a reverse proxy terminating HTTPS in front of the webmail, the Control Deck, CalDAV and CardDAV. The one thing to know first: hMailServer judges the address that connected to it, and behind a front end that address is the front end's until you tell the server otherwise. Auto-ban, the rate limits, the IP ranges and every spam test then reason about the wrong machine: SPF fails on every message, and a block list that lists your gateway blocks all your mail. Each kind of front end is handled in its own way, and behind an HTTP reverse proxy the address stays the proxy's. Which mechanism fits which front end Front end What reaches hMailServer What tells it the truth A filtering gateway, hosted or an appliance, that accepts the mail and sends it on A new SMTP session from the gateway, the sender's address in its Received header Incoming relays, XCLIENT if the gateway can send it, and the gateway setting profile A load balancer or TLS terminator in front of the SMTP listener The client's session, from the balancer's address HAProxy PROXY v1 or v2 A reverse proxy terminating HTTPS for /portal, / and /api/v1 HTTP from the proxy X-Forwarded-Proto: https from the proxy on the same machine, for the cookies; the address stays the proxy's A reverse proxy in front of CalDAV, CardDAV and the other web services HTTP from the proxy X-Forwarded-Proto: https, believed from loopback and from WebServicesTrustedProxies Incoming relays: what listing one changes Mail flow & delivery → Incoming relays in the Control Panel (Windows) lists servers that legitimately forward mail to you, each a name and an IP range (a lower and an upper address); a save applies at once. The same list is in the Control Deck, at /api/v1/incoming-relays over REST since 6.3.3 (a key confined to named domains is refused), and in COM Settings.IncomingRelays on Windows, where since 6.3.4 a change takes effect only at Save(). Listing a relay changes two things: When spam protection runs. A connection from a listed relay is switched from pre-transmission to post-transmission testing. DNSBL, PTR and HELO checks judge the connecting IP, so they wait until the body has arrived and the real origin can be read from it. Spam is then no longer refused before the body: with quarantine on, a message reaching the delete threshold is held for review; without it, it is refused with 554 5.7.1 after the body. Which IP counts as the origin. The Received chain is walked from the top, and the first address that is not a listed relay becomes the originating IP. SPF, DMARC and ARC are evaluated against it. Received: from gw2.example.net [198.51.100.8] listed relay: keep walking Received: from gw1.example.net [198.51.100.7] listed relay: keep walking Received: from mail.sender.example [203.0.113.44] not listed: the originating IP Since 6.2.24 the address taken is the one the relay observed, not the client's own HELO literal; thresholds tuned before then need checking again. Since 6.3.4 a listed relay changes more: A remote domain policy's inbound rule, Require TLS on mail from this domain (530 5.7.0 at MAIL FROM), is not applied to it. The hop that rule is about happened before the gateway, so require TLS on the gateway. Requiring TLS and remote domain policies covers the policies. Mail it hands over counts as coming from inside. One rule, MessageOrigin::IsExternalSender, decides that for the external-sender tag, the first-contact note and impersonation protection, so none of the three applies behind a listed gateway. DomainTransforms.md: the choice is between the tag and the relay declaration, and there is no third answer today. Mail flow rules and what a domain does to a message covers the tag. A spam trap never bans it; the sender is still judged. The domain white and black list sees the relay's own HELO name, so key rows for mail behind a relay on the envelope domain. Keep the range to the exact gateway addresses. An attacker inside a range listed too wide can insert a forged Received header and choose which IP you evaluate. And leave spam protection on for the IP range the gateway connects from: clearing Enable anti-spam on a range turns off every spam test, greylisting and the recipient tarpit for it, relay or not. Symptom Cause Every message is scored as though it came from your own filter; SPF always fails The front end is not listed as an incoming relay A DNSBL hit on the gateway's address blocks all mail The same Adding a relay stopped spam being rejected before the body Expected: testing moved to after the body. Quarantine and scoring still apply PROXY protocol and XCLIENT If the front end can pass the real client address itself, that is better than reconstructing it from headers. Two mechanisms have been accepted since 6.2.23-alpha1, both off by default and both honoured only from the addresses you list: Setting Default What it does SMTPProxyProtocolEnabled 0 HAProxy PROXY v1 and v2 on the SMTP listener, read before TLS and before the banner SMTPProxyProtocolTrustedIPs empty The real TCP peers allowed to send a PROXY header, comma-separated addresses or CIDR ranges. Empty means nobody SMTPXClientEnabled 0 The Postfix XCLIENT verb SMTPXClientTrustedIPs empty The peers allowed to use it. Empty means nobody Trust is decided against the real TCP peer, never an address a header supplied, so chained proxies cannot bootstrap trust. An entry that does not parse matches nothing and is logged once per run (HM6281). Once an address is listed for PROXY, the header is required from it. Configure the proxy to send it (in HAProxy, send-proxy or send-proxy-v2) or its connections are dropped; and a PROXY line from a peer that is not listed closes the connection without a reply. Change the proxy and the settings together. XCLIENT is advertised in EHLO only to a trusted peer, as XCLIENT ADDR NAME PORT PROTO HELO LOGIN; an untrusted peer never learns the verb exists and gets 550 if it tries. With LOGIN the upstream vouches for a signed-in account: RoomsAndEquipment.md counts such a submission as a signed-in session, like SMTP AUTH. Listing an address for XCLIENT trusts it with that too. What then sees the real client: auto-ban, the rate limits, the IP ranges, the spam tests, and since 6.3.4 the compromised-account detection's address signal (which never scores behind a proxy that speaks neither) and, with PROXY, the live view of one connection. What does not: country blocking (since 6.3.4). It judges the address the TCP connection came from, the proxy, and exempts loopback and private ranges; put the geographic policy on the proxy. Since 6.3.7 the IMAP and POP3 listeners take the client's address from a PROXY protocol header too, each from the peers on a list of its own (IMAPProxyProtocolEnabled and IMAPProxyProtocolTrustedIPs, POP3ProxyProtocolEnabled and POP3ProxyProtocolTrustedIPs), both off by default, the header read before an IMAPS or POP3S port's TLS handshake and before the greeting. No document describes either mechanism for ManageSieve. Where they are set. On Windows, the Control Panel's Maintenance → Server limits & expert settings (card Front-end proxies (PROXY protocol and XCLIENT); in 6.3.7's Control Panel the PROXY protocol and XCLIENT are a card on Incoming relays) or COM Settings.SetIniSetting. On either system, PUT /api/v1/settings/ini/{name} (the administrator password only; an API key is refused) or hmctl ini set <name> <value>; on Linux also hmailserver --set-setting. The Control Deck has no editor for these keys. All four apply when saved. Since 6.3.4 the database holds them, so a line edited in hMailServer.ini changes nothing the server uses; the store, the file's copy and HM5804 are in Settings: what is not stock and when a save takes effect. # Over REST with the administrator password (an API key is refused), from any machine that reaches the listener; applies when saved hmctl ini set SMTPProxyProtocolTrustedIPs 10.0.0.5,10.0.0.6 hmctl ini set SMTPProxyProtocolEnabled 1 # On a Linux server; a running service takes it at its next save, reinitialise or restart sudo hmailserver --set-setting SMTPProxyProtocolTrustedIPs=10.0.0.5,10.0.0.6 sudo hmailserver --set-setting SMTPProxyProtocolEnabled=1 The gateway profile (since 6.3.4) A setting profile is a named, reviewed set of settings, shown as a diff first. gateway is for a server whose inbound mail a filtering gateway has already judged. Its 17 entries include: Entry Value Why (Profiles.md, shortened) usegreylisting 0 It judges the connecting address, which is the gateway's even through Incoming relays SMTPXClientEnabled 1 Honoured only from SMTPXClientTrustedIPs, and from nobody while that is empty ASArcFilteringEnabled 1 With DMARC on, the DMARC penalty is offset when a sealer named in arc_trusted_sealers sealed a pass MaxSubmissionsPerIPPerMinute, SmtpTarpitCount 0 All inbound mail comes from the gateway's few addresses QuarantineEnabled, RejectFullMailboxAtRcpt 1 A message at the delete threshold is held, not refused back to the gateway; a full mailbox is refused at RCPT so the gateway returns the message usespf, ascheckhostinhelo, ascheckptr, ASDMARCEnabled 1 Through Incoming relays they judge the real sender AutoBanOnLogonFailureEnabled, SmtpAuthenticatedSenderCheck 1 For people who sign in here directly The rest switch on DKIM verification, the MX check and attachment blocking, all three off as shipped, and keep the reason headers on. The first four rows turn a check off or trust an address range, so applying asks for an explicit confirmation wherever one of them would change something. A profile never writes an incoming relay, an IP range, a port's TLS or the ARC sealer. The steps are yours, in this order: Add the gateway's addresses to Incoming relays before applying the profile, and leave spam protection on for their IP range. Until they are there, the tests judge the gateway's own address. If the gateway sends XCLIENT, put its addresses in SMTPXClientTrustedIPs. Name the ARC sealer the gateway signs as in arc_trusted_sealers (PUT /api/v1/settings/antispam, or COM AntiSpam.ArcTrustedSealers). Until then no seal is trusted. Read the diff and apply it, on the Control Panel's Setting profiles page under Maintenance, in the Deck's checklist, over REST or with hmctl, confirming the entries that ask. The apply is all or nothing and saves the values it replaces first; while an earlier profile is neither reverted nor discarded, another apply is refused with 409. The routes, the diff's token, the way back and the command-line verbs are in the profiles section of the unattended installs topic. Check "Require SSL/TLS when authenticating" on the Internet IP range, or make the submission port STARTTLS-required, for people who sign in here directly. A new installation turns it on; an upgraded one keeps what it had. On the listener the gateway delivers to, leave TLS optional unless the gateway offers it: a port's security is the port's, not a setting. A filter on the same machine Since 6.3.4 SPF is RFC 7208, and 127.0.0.0/8 and ::1 no longer pass unconditionally. A content filter that re-injects mail over loopback should authenticate, or connect from an IP range with anti-spam off. The HTTP listeners behind a TLS-terminating proxy REST listener Web-services listener Serves The webmail at /portal, the Control Deck at /, the API under /api/v1 ACME challenges, MTA-STS, security.txt, autoconfiguration, CalDAV, CardDAV, the OpenPGP key directory Plain HTTP behind the proxy Only on loopback: TLS is required unless bound to 127.0.0.1, localhost or ::1 WebServicesHttpPort Whose X-Forwarded-Proto: https counts The proxy on this machine Loopback, and WebServicesTrustedProxies (since 6.3.3) What it decides Secure on the session and sign-in cookies Whether CalDAV and CardDAV treat the request as HTTPS; the Apple .mobileconfig profile; the key directory A changed port or bind address (RestApiPort, RestApiBindAddress, WebServicesHttpPort, WebServicesHttpsPort) waits for a reinitialise (Apply now). WebServicesTrustedProxies applies when saved, and no document names a Control Panel field for it: set it with PUT /api/v1/settings/ini/{name}, hmctl ini set, COM Settings.SetIniSetting on Windows or hmailserver --set-setting on Linux (which a running service takes at its next save, reinitialise or restart). The cookies, since 6.3.4. hmailsession and the sign-in cookies hmoidc, hmsso and hmpasskey are marked Secure over TLS, or when the proxy on this machine says X-Forwarded-Proto: https. Before 6.3.4 only a TLS listener counted, so behind the usual proxy a browser would also send the session cookie over plain HTTP. A proxy that does not set the header still leaves them without Secure; a proxy on another machine can only reach a REST listener that speaks TLS itself. CalDAV (since 6.3.4) and CardDAV take HTTP Basic over HTTPS only: over plain HTTP the answer is 403 with the reason, unless a trusted proxy says X-Forwarded-Proto: https. From any other peer the header is ignored; an entry that does not parse matches nothing (HM6281). The Apple profile honours the header on the same terms, and the OpenPGP key directory (since 6.3.4) answers a plain request with the same 404 as an unknown address; point openpgpkey.<domain> at the proxy and forward /.well-known/openpgpkey/ with the header. The CalDAV and CardDAV topic covers the rest. The client address stays the proxy's. Nothing reads X-Forwarded-For, so: The auto-ban leaves 127.0.0.1 and ::1 alone, and behind a proxy on this machine every webmail and Deck request comes from there. What brakes guessing is the per-name lockout, when AccountLockoutThreshold is set, and since 6.3.4 each owner's second-factor budget: five wrong codes in fifteen minutes refuse that owner's codes for the next fifteen (429, second_factor_locked). The forgotten-password route (since 6.3.4) holds loopback to thirty requests in any sixty seconds, all together. A proxy on another host shares one per-client bucket of twenty an hour. Neither has a setting: put the proxy on the mail server or accept the cap. For CalDAV, CardDAV and Autodiscover, since 6.3.8 a failed sign-in from loopback or an address in WebServicesTrustedProxies is counted against the name alone, never towards a ban of the address, and the listener never refuses those addresses by an IP range, so only proxies belong in that list, and the per-name lockout (AccountLockoutThreshold, off by default) is the limit on the guesses through one. Up to 6.3.7 each such failure was charged to the proxy's address, and a ban written for it could shut the proxy, and every client behind it, out of the SMTP, POP3, IMAP and ManageSieve listeners (AutoBan.md). Passkeys (since 6.3.4). By default the relying party id is the Host header, which behind a TLS-terminating proxy is still what the browser sent, so the check holds. X-Forwarded-Proto is not consulted, which is why the origin check accepts https at the page's host and plain http only on a loopback name. A server reached at several names needs passkey_relying_party and passkey_allowed_origins together, the origins written as the browser writes them, port included; the topic on signing in has the rest. The address people use. The server cannot learn it from the proxy. The links in a message that expires, a password-protected message and a password reset use confidential_base_url (then quarantine_digest_base_url, then import_oauth_redirect_base_url); the quarantine digest's links use quarantine_digest_base_url alone, a mailing list's use list_base_url, and single sign-on (since 6.3.4) uses oidc_redirect_base_url. Which paths to publish is in The webmail portal at /portal. Compression: let one side do it (since 6.3.4) Both listeners compress when the client asks, and there is no setting. Negotiation. Accept-Encoding is the whole of it: gzip or deflate, gzip where both are acceptable, never br. deflate is the zlib format of RFC 1950, which a raw inflater fails on; ask for gzip. What. Text, JSON, XML, a manifest, message/rfc822 and an icon, between 1,024 bytes and 4 MB. An image, a PDF or a zip goes as it is, and a download (/api/v1/me/messages/{id}/attachments/{index}, /files/{token}) is never compressed. Headers. Content-Length is the bytes sent; no chunked transfer encoding, no byte ranges. Vary: Accept-Encoding is on every answer of a type it would compress, which matters for the web-services listener, whose answers may be cached; the webmail and the API are Cache-Control: no-store. Behind a reverse proxy, either leave Accept-Encoding alone and pass the answer through (the server will not encode twice, and the proxy must not either), or strip Accept-Encoding from the request and compress at the proxy. What does not is forwarding Accept-Encoding: gzip, receiving Content-Encoding: gzip, and handing the bytes to a client that did not ask. What it does not do It reads no forwarded-for header. XCLIENT is an SMTP setting only; since 6.3.7 the PROXY protocol is taken on the IMAP and POP3 listeners too, each from its own list of peers. A listed gateway turns off the external-sender tag, the first-contact note and impersonation protection for what it hands over. Compression has no switch. A client that wants none sends no Accept-Encoding, or Accept-Encoding: identity. A second machine behind a balancer is the warm standby and high availability topic. Where to read more Wiki: Routes and Relays §14.3, Security Hardening §17.8, Stopping Spam §10.1, Settings Overview, and chapter 14.3 of the documentation page. Documents at the 6.3.4 tag: Profiles.md, ResponseCompression.md, DomainTransforms.md, PasskeysAndSingleSignOn.md, DevicesAndSessions.md, AccountRecovery.md, CountryBlocking.md, SettingsApply.md, RoomsAndEquipment.md, and README.md for the [Settings] notes on PROXY, XCLIENT and the web services. On this forum: The webmail portal at /portal, The Control Deck, the anti-spam stack end to end, IP ranges, auto-ban, lockout and tarpits and TLS on the listeners. A way past one of these trust lists is a vulnerability: report it by a confidential GitLab issue or by email to the project's Service Desk address, contact-project+hmailserver-hmailserver-86730559-issue-@incoming.gitlab.com, which GitLab files as a confidential issue, and not in a forum post.
  • 0 Votes
    1 Posts
    132 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) adds reasons a message stays where it is. A forward, a redirect or a rule's forward of mail from outside whose From names the forwarding domain's own organisation, and that did not pass DMARC for it when it arrived, keeps the envelope sender it came with, with no SRS address or rewritten sender; where the copy would still carry an identity of that organisation (an empty envelope sender under this server's host name, or an envelope sender at this server's host name or at a hosted domain in that organisation) it is not sent to an outside address at all: the message stays in the mailbox, and the application log names each recipient. RuleLoopLimit 0 now stops out-of-office replies, as it stopped every other generated message. A delivery whose mail servers' DNS lookup failed tells the sender that the DNS query failed (4.4.4) and retries, where a failed lookup was read as a name with no addresses. On MySQL and MariaDB a lost database connection is reconnected and the statement run again, and an error log that was quiet may now name database failures that were hidden (HM5032). See the 6.3.7 announcement. This topic covers the case where mail is not moving and the server looks healthy: no crash, no error, the service running, and a log that seems to stop mid-transaction. The server used to be silent about it; it is no longer, and the answer is usually in one line of the log. The one thing to establish first is which half is stuck: the server accepting a message from a sender, or delivering one it has already accepted. The two fail differently, for different causes, under different settings. Which half is stuck? Accepting. The sending server connects, sends the message, and then waits. From its side there is a timeout after the message body was transmitted; Postfix reports it as timed out while sending end of data. The message never appears in your queue. Delivering. The message was accepted (the sender got a 250), is visible in the delivery queue, and never leaves. Monitoring & troubleshooting → Delivery queue in the Control Panel tells the two apart in one look, and so does hmailserver_delivery_queue_messages on the metrics endpoint once it has a port (see Commands). The Control Panel is Windows-only; on Linux the queue is GET /api/v1/queue, hmctl queue list (since 6.3.4) and the Control Deck's queue page. Turn on debug logging first Control Panel → Logging → tick Debug messages, and make sure logging is enabled; then reproduce the problem once. The stage lines below are written at debug level. The slow ones (ten seconds or more, for a whole stage or for any single spam test) are also written at application level, so a server with only application logging still records the important half. Turn debug logging off again afterwards on a busy server. Reading the logs covers where the logging keys live on each platform. Accepting: the sender times out after sending the message Between the 354 and the 250 the server runs the accept pipeline: spam tests, message modifications, archiving, the OnAcceptMessage script and the database save. All of it runs on a bounded pool of threads, and the reply is sent only when it finishes. The log times each stage: SMTPConnection - accept: start spam-protection. Spam test: SpamTestDNSBlackLists, Score: 0, Time: 12 ms Spam test: SpamTestSpamAssassin, Score: 0, Time: 120000 ms SMTPConnection - accept: done spam-protection in 120047 ms (session 5). SMTPConnection - accept: done message-modifications in 0 ms (session 5). SMTPConnection - accept: start script/save. SMTPConnection - accept: done script/save in 3 ms (session 5). How to read it. Read it as a sequence and look for the last line written; the stage that is stuck is the one after it. Two stages announce themselves with a start line (spam-protection and script/save), so for those a start with no matching done is the whole diagnosis. The middle stage, message modifications, has no start line, so a stall there shows as done spam-protection followed by silence. If every stage completes but one took a long time, the Time: or done ... in value names the culprit. What the log shows Cause SpamTestSpamAssassin with a large Time: spamd is unreachable, overloaded, or accepting connections without answering SpamTestDNSBlackLists, SpamTestSURBL or SpamTestSPF slow The resolver is not answering. Look in the TCP/IP log for DNS - Query timed out and DNS - Query failure. Do not reach for DNSServer as the fix (below) done spam-protection, then silence Message modifications: the spam headers, the signature, the List-* headers, or the write of the modified message back to disk script/save slow An OnAcceptMessage event script, or the database Nothing between 354 and silence A version older than 6.2.17, which added these lines. Upgrade The DNSServer setting. It was broken in 6.2.17 and 6.2.18 (CHANGELOG's 6.2.19 section names 6.2.16 as well) and fixed in 6.2.19: on the affected versions every lookup through a configured custom server failed, and DNSBL, SURBL, SPF, DKIM and MX lookups failed in silence, so such a server had quietly stopped most of its spam filtering while still accepting mail. One caveat survives the fix: a custom server list is only honoured together with DNS_QUERY_BYPASS_CACHE, so every lookup pays full price. Leave DNSServer empty unless you need a specific resolver. SPF, and SPF on Linux. In 6.3.3 and earlier the SPF evaluator resolved names through the Windows DNS client rather than through the server's resolver. On Windows an SPF check therefore went to the system resolvers whatever DNSServer said; on Linux every SPF check returned a temporary error, reported once per process as HM6406, and because SpamTestDMARC evaluates SPF, DMARC there lost its SPF half. The RFC 7208 evaluator that replaced it in 6.3.4 resolves through the same resolver as every other lookup. If you run 6.3.3 or earlier on Linux, an SPF or DMARC verdict from it is not a verdict. The deadline. Since 6.2.17 acceptance is bounded: if the pipeline exceeds FinalizationTimeout (240 seconds by default) the server answers 451 4.3.1 Server temporarily overloaded while accepting the message; please retry. and the sender retries rather than waiting for a reply that never comes. The accompanying error entry names the deadline that was hit, and the stage timings above it name what consumed the time. If every message stalls, not just one Look for these two lines: Task SMTP-accept session=42 ip=203.0.113.9 waited 8 seconds for a thread in work queue Asynchronous task queue. 14 task(s) are still queued. All 15 threads in work queue Asynchronous task queue have been busy for at least 120 seconds, so no further task on this queue can start. 14 task(s) are queued. Running: SMTP-accept session=42 ip=203.0.113.9 (thread 4120, 168s), ... Every worker is occupied, and one slow dependency does this to the whole server. The task names say which sessions are stuck; the stage timings say what they are stuck on. AsyncQueueStallThreshold (120 seconds by default) is how long every worker may be busy before the second report is written; 0 turns the report off. Delivering: how the queue works There is no in-memory queue to inspect. The delivery queue is the set of rows in hm_messages whose type is delivering, and the delivery manager is a loop that polls for them. Three properties of that loop explain most of what a stuck queue looks like: The selection is where messagetype = 1 and messagelocked = 0 and messagenexttrytime <= now, ordered by size, then try count, then id. Small and new goes first. The delivery pool is separate and smaller than the accept pool: maxdelivertythreads, 10 by default, on the work queue named SMTP delivery queue. It is not the Asynchronous task queue that finalises inbound mail, so a saturated delivery pool does not stop mail being accepted; it stops it leaving. The manager wakes on demand, or once a minute otherwise. A message whose next-try time has just passed is picked up within a minute, not instantly. At startup every message in the queue is unlocked (update hm_messages set messagelocked = 0), because locks held by a server that crashed would otherwise strand those messages for ever. That is also why exactly one hMailServer service may run against one database. The retry schedule, and when a message gives up Setting Where Default Effect Number of retries Mail flow & delivery → Delivery of e-mail (smtpnoofretries) 4 How many further attempts after the first Minutes between every retry The same page (smtpminutesbetweenretries) 60 The gap between them QuickRetries [Settings] 0 (off) The first N retries use the short gap below instead, for greylisting QuickRetriesMinutes [Settings] 6 That short gap QueueRandomnessMinutes [Settings] 0 (off) Adds a random 1..N minutes to every gap, so a burst does not retry in lockstep Route number of tries and minutes between tries Mail flow & delivery → Routes — Overrides both, per matching route; where several routes match, the largest of each wins With the shipped defaults that is five attempts an hour apart: a destination that is refusing temporarily holds a message for roughly four hours and then bounces it. The try count in the queue is the same messagecurnooftries the arithmetic uses. Two things do not advance that counter: Deliver now resets the next-try time without touching the count, and a message held because the virus scanner could not examine it is held on its own budget (below). Reading a stuck queue What the queue looks like What it means Where to look next Depth climbing, oldest message seconds old A busy server draining normally. Not a fault — Depth flat, oldest message hours old, try counts climbing The far end is deferring you The SMTP log for that destination; your IP's reputation and PTR Every stuck message shares one recipient domain That destination, not your server The route settings for it, and its MX Try counts stuck at 0, next-try times in the past, nothing moving Delivery threads are not running the tasks hmailserver_workqueue_depth{queue="SMTP delivery queue"}, and the saturation report above Messages appear and vanish without arriving Delivered to the wrong place, or discarded by a rule Mail flow & delivery → Rules; enable MessageTraceEnabled and read the trace Try count 1 and a next-try time of 1901-01-01 An ETRN hold: the one route these recipients match is an ETRN route Mail flow & delivery → Routes; the log line is SMTPDeliverer - Route Message: HOLD for later delivery.. Queue empty, but the sender got a 250 and nobody received it Delivered locally into a folder you are not looking at, or the row was removed The account's folders; the message trace Message N could not be delivered since the data file does not exist The row and the store disagree: a restore that missed files, or a store on a share that vanished Turn on MessageStoreConsistencyCheck and read the report When the virus scanner cannot examine a message. With AVFailAction=1 such a message is not delivered. It is held on its own schedule rather than on the SMTP retry budget: AVFailRetryMinutes apart (15 by default), at most AVFailMaxHolds times (16), and then returned to the sender. The log says where in that budget each hold is (... this was hold 3 of 16 before it is returned to the sender.). Each hold counts as a deferral in hmailserver_messages_deferred_total, so a scanner that has stopped answering shows as a deferral rate with no matching delivery rate. The usual causes of a genuinely stuck delivery A virus scanner that stops responding. ClamAV is contacted after the message is accepted, so a wedged clamd shows up as accepted but never delivered. Each socket operation is bounded by ClamMinTimeout / ClamMaxTimeout, but that is a deadline on one read or write, armed fresh each time, and the message is streamed in chunks, so one large message can take considerably longer than ClamMaxTimeout in total. A remote server that answers extremely slowly. The idle timeout is re-armed on every byte received, so a host that sends one byte occasionally used to hold a delivery thread indefinitely. Since 6.2.18 there is an absolute ceiling, ClientSessionCeiling (30 minutes by default), armed once and never re-armed. A custom virus scanner or external tool that hangs, bounded by ExternalProcessTimeout. A message held for a sandbox's verdict (since 6.3.4). With SandboxEnabled on, a message none of your own signed-in users submitted, whose attachment is of a submitted type, waits for the sandbox you named to open the file, up to SandboxTimeoutMinutes (15 by default). While it waits it is in no mailbox, not in the quarantine and not the queue's to retry: its Retry and Delete answer 409. It is listed by GET /api/v1/sandbox/held, by GET /api/v1/queue marked held: sandbox (the held column of hmctl queue list), and in the Deck's queue page under Held for a sandbox, where it can be delivered without the verdict or discarded. The database. Check for errors mentioning the connection pool. Since 6.3.4 a message can also wait in other places, each on purpose: held for review by compromised-account detection in enforce (/api/v1/review); in a destination's back-off window, with the last error Delivery deferred by this server's own shaping, not by a remote server. (GET /api/v1/queue/shaping; such a deferral does not spend the message's retries); under an IP warm-up ceiling; or behind a drain, when /readyz says draining. Section 19.11a of the Troubleshooting page lists each with what to do. Settings that bound each stage All are in seconds. On 6.3.3 and earlier they are [Settings] keys in hMailServer.ini. Since 6.3.4 they are stored in the database and changed in the Control Panel, over the REST API at /api/v1/settings/ini/<name> or with hmctl, and the save applies them. They still appear in the file's [Settings] section, but the database's value is the one used: where the service can write the file, an edit made there is reported (HM5804) and put back at the next start. Setting Default Bounds 0 means FinalizationTimeout 240 The whole accept pipeline, after which the sender gets a 451 no bound SAMaxTimeout 90 SpamAssassin: idle timeout, and a session ceiling of this plus 30s not "no bound" ClamMaxTimeout 90 ClamAV: idle timeout per socket operation not "no bound" DNSQueryTimeout 10 A single DNS query no bound ScriptTimeout 60 One event script invocation no bound ExternalProcessTimeout 300 An external scanner process no bound ClientSessionCeiling 1800 An entire outbound delivery session no bound AsyncQueueStallThreshold 120 How long every worker may be busy before the saturation report reporting off DBConnectionAcquireTimeout 60 Waiting for a pooled database connection no bound The two exceptions matter, because 0 tightens them instead of removing them. SAMaxTimeout and ClamMaxTimeout are not passed straight through: they go to a calculator that returns the minimum whenever the maximum is lower than it. So SAMaxTimeout=0 gives a 30-second idle timeout (SAMinTimeout) and a 30-second session ceiling, and ClamMaxTimeout=0 gives 15 seconds (ClamMinTimeout). To lengthen either, raise the value; to lengthen it a long way, raise the matching ...MinTimeout too. There is no way to make either unbounded. When the pool deadline expires, a recipient lookup answers 451 4.3.2 rather than 550: the server can tell "the database did not answer" from "no such address", so a database locked by a backup defers the mail instead of bouncing it. Commands The COM examples below are Windows-only. The metrics endpoint and the REST routes work on both platforms. Is it accepting, or is it delivering? The metrics listener is off until MetricsServerPort is set (9100 here; since 6.3.4 the port, the bind address and the listener's other settings, its credentials included, wait for a reinitialise). Once it is on, the endpoint needs no credential on a loopback bind: (Invoke-RestMethod http://127.0.0.1:9100/metrics) -split "`n" | Select-String '^hmailserver_delivery_queue_(messages|oldest_message_age_seconds) |^hmailserver_workqueue_depth|^hmailserver_messages_(delivered|deferred|bounced)_total ' A rising oldest_message_age_seconds with a flat delivered_total is the delivering half. A rising hmailserver_workqueue_depth{queue="Asynchronous task queue"} is the accepting half. The COM examples start from a signed-in $app: $app = New-Object -ComObject hMailServer.Application $app.Authenticate("Administrator", "<password>") | Out-Null Push one message to the front of the queue (resets its next-try time, leaves the try count alone, and releases an ETRN hold): $q = $app.GlobalObjects.DeliveryQueue $q.ResetDeliveryTime(4711) $q.StartDelivery() Wake the delivery manager without touching any message: $app.GlobalObjects.DeliveryQueue.StartDelivery() Remove one message from the queue (it is locked first, so a message a thread already has is not pulled from under it): $app.GlobalObjects.DeliveryQueue.Remove(4711) Follow the log while you reproduce it by tailing the day's file and filtering on accept: |Spam test:|waited .* for a thread|have been busy|SMTPDeliverer; Reading the logs has the tail command for each platform. Turn the message trace on (MessageTraceEnabled, off by default) to see what was done with one message. It has no acceptance, spam or virus event, so an empty trace does not show that the server never had the message; Troubleshooting by symptom says how to turn it on and what a row says. What to include in a report If none of the above identifies it, post in General support or open an issue with: the log from the 354 (or from the delivery attempt) onwards, including the stage timing lines; the ERROR_hmailserver_<date>.log for the same window; which scanners and event scripts are enabled; and whether the message eventually arrives, arrives twice, or never arrives. Say which half you have. Read this before you post covers the rest. Without a GitLab account, email the report to the project's Service Desk address, contact-project+hmailserver-hmailserver-86730559-issue-@incoming.gitlab.com, which GitLab files as a confidential issue; the maintainers answer by email. Where to read more Wiki: Diagnosing Stalled Mail and Troubleshooting, sections 19.7 and 19.11a. Repository: DiagnosingStalledMail.md; SettingsApply.md; Sandbox.md; SUPPORT.md. The public documentation: The delivery queue and the retry schedule and Troubleshooting. On this forum: Being trusted by the servers you send to, for when the far end is deferring you; Outbound shaping and IP warm-up, for when your own server is holding mail back; Monitoring and health, for turning the metrics listener on.
  • 0 Votes
    1 Posts
    52 Views
    ProgressiverobotP
    A SURBL lookup that failed was being read as a hit. The Spamhaus zones answer a query they refuse with a code in 127.255.255.0/24, and until now any answer at all counted as a listing. A server resolving through a public resolver such as 8.8.8.8, or sending too many queries, tagged every message carrying a link as spam (GitHub discussion 167). What the upgrade involved The database goes to schema 6038: steps 6032 to 6037 add contacts, account preferences, scheduled sends and snoozes, files sent as links, message keywords and S/MIME keys, and 6038 the SURBL expected result. 6.3.1 listed the 6029 to 6030 upgrade step as known and unfixed: on a database holding orphaned rows it could re-orphan rows it had already cleaned, then refuse its own foreign keys. It is fixed in all four backends. Changes Each SURBL server now has an expected result (SURBLServer.ExpectedResult, in the Control Panel's SURBL editor) in DNSBL syntax: 127.0.1.0-255, 127.0.0.2*, ranges and wildcards. With none set, any answer counts except the codes in 127.255.255.0/24. The debug log records what the zone answered and what was made of it. The portal at /portal is now a mail client, over /api/v1/me and the account's own credentials. Conversations by thread, search operators (from:, subject:, has:attachment, is:unread, label:), labels stored as IMAP keywords so every IMAP client sees them, undo send for up to thirty seconds, send later and snooze held on the server, oversize attachments sent as expiring links from /files/{token}, read receipts (RFC 8098) and one-click unsubscribe (RFC 8058). S/MIME runs in the browser on the Web Crypto API, the private key wrapped under a key derived from the account password. Not in this release: 3DES content, EC key agreement for encryption, legacy PKCS#12 encryption, OpenPGP. SASL GSSAPI (RFC 4752) on SMTP, IMAP and POP3, on Windows. Off unless GssapiEnabled=1 in [Settings]. Auto-ban can reach the firewall. AutoBanFirewall=1 writes an inbound block rule per banned address in Windows Defender Firewall, or keeps an nftables set on Linux. AutoBanCommand and AutoBanNeverBan go with it. All three are off as shipped. Making an app password now takes the account's own password, and an app password is never accepted for it. The 6.3.1 .deb depended on the builder's exact Boost sonames and would not install on Ubuntu 26.04. Boost is linked statically now. Full release notes, checksums and signatures
  • 0 Votes
    1 Posts
    51 Views
    ProgressiverobotP
    If your server accepts inbound mail from a trusted relay, a Proxmox/Postfix front end for example, and you run SpamAssassin, messages could stall after end-of-data and never get a 250. Reception was never the problem. The stall was in the accept/save work that runs after the terminating dot and before the reply. What the upgrade involved No database change. Schema version stays at 6005. The same audit found other unbounded waits, not fixed here: synchronous DNS lookups in the spam tests have no application-level timeout, the database connection pool has no acquisition deadline, event scripts have no execution limit, and the ClamAV read/write timeout on delivery is ineffective. Why it only ever hit relayed mail For a trusted incoming relay, hMailServer defers the whole spam battery to after end-of-data. For a direct or authenticated sender those tests run earlier, during MAIL FROM/RCPT TO, on the connection thread. The post-DATA work runs on a bounded pool, 15 threads by default, and holds the thread that sends the 250. SpamAssassin's wait had no overall ceiling: the connection's idle timeout is re-armed on every byte received, so a scanner that stalls or dribbles holds the thread indefinitely. Against a blackholed SpamAssassin endpoint, one message was acknowledged after 120 s. Eighteen concurrent messages produced zero acknowledgements: 15 workers blocked, the rest with no worker at all. The fixes SpamAssassin's wait is bounded. Hard ceiling of SAMaxTimeout + 30 s, after which the message is accepted without a verdict, the same outcome as spamd being down. New FinalizationTimeout, default 240 s, inside Postfix's 600 s data-done timeout, 0 disables it. Acceptance past that answers 451 4.3.1 and the sender retries. The check runs on the accepting thread and only before anything is saved, so it cannot duplicate mail. Acceptance is timed per stage and each spam test timed individually. A slow stage is logged at APPLICATION level, not only under debug: Spam test: SpamTestSpamAssassin, Score: 0, Time: 120031 ms. Upgrading is enough to turn silent stalls into either a completed delivery or a clean 451 retry. Enable debug logging and the per-stage timings name the scanner, DNS lookup or event script responsible. Also in this release: regression coverage for clients that vanish mid-operation (aborted DATA, truncated BDAT, IMAP APPEND literals cut short, POP3 disconnects during RETR), plus an installer smoke test on a clean machine. Full release notes, checksums and signatures
  • 0 Votes
    1 Posts
    57 Views
    ProgressiverobotP
    The headline fix is an SMTP DATA stall that only affected relayed mail. Relaying from Postfix or Proxmox Mail Gateway, the connection could hang after 354 OK, send. until the sending MTA gave up with "timed out while sending end of data", leaving a zero-byte spool file. The cause was a reverse-DNS (PTR) lookup on the network I/O thread while generating the Received header, so an internal relay with no reverse zone stalled the session through DNS retries. That lookup now runs on a worker thread and header generation never waits on it. Direct sending was unaffected. What the upgrade involved No database change. Schema version stays at 6005. Minimum OS is Windows 10 1607 / Server 2016, 64-bit, which the installer's version check now names. SMTP. A parse exception no longer wedges a connection, data pipelined in the same segment as DATA is consumed correctly, a rejected BDAT/CHUNKING command drains or terminates its payload instead of desyncing the session (it matters for Exchange), the EHLO SIZE keyword no longer overflows, and TCP_NODELAY is set on every connection. IMAP. A malformed partial-fetch range such as BODY[]<0.-1> could drive a near-SIZE_MAX allocation, or read heap memory from before the buffer and send it to an authenticated client. The octet range is now clamped and normalized. OnClientLogon now fires from every AUTHENTICATE mechanism (PLAIN, SCRAM-SHA-256, XOAUTH2/OAUTHBEARER). SpamAssassin. A malformed or truncated spamd response could spin a core and hang the session, write the raw SPAMD header into the message, or overwrite it with a zero-byte file. The 256 MB scan ceiling is now clamped to the 80 MB MIME parser limit, and the original message is preserved on any failure. Upgrade and installer. DBUpdater labels database versions 6002-6005, a failed database create or upgrade returns a real exit code instead of a false success, and a customised EventHandlers.vbs survives reinstall. Control Panel. Diagnostics no longer reports every test as FAILED, and restarting the service is elevation-aware. The regression suite passed 1026 of 1026 against the rebuilt 6.2.13 service. Full release notes, checksums and signatures