<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[The anti-spam stack end to end: score, greylisting, SPF, DKIM, DMARC, DNSBL, SURBL]]></title><description><![CDATA[<p dir="auto"><em>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 <code>TempFail</code>, not <code>PermFail</code>. 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 <code>Return-Path</code>, 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 <code>From</code> 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 <code>AntiSpam.MaximumMessageSize</code>, 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 <a href="/topic/28277">the 6.3.7 announcement</a>. 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 <code>MoveLearningBackend</code> 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 (<a href="https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.8/hmailserver/docs/JunkFolderLearning.md" rel="nofollow ugc">JunkFolderLearning.md</a>). See <a href="/topic/28278">the 6.3.8 announcement</a>.</em></p>
<p dir="auto">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.</p>
<h3><a class="anchor-offset" name="where-the-decisions-are-made"></a>Where the decisions are made</h3>
<p dir="auto">Everything happens in one of two moments: before the body (at <code>MAIL FROM</code> or <code>RCPT TO</code>) and after the body (after <code>DATA</code> or the last <code>BDAT</code>). The pre-transmission tests run at <code>MAIL FROM</code> by default: <code>DNSBLChecksAfterMailFrom</code> defaults to <code>1</code>, and <code>0</code> moves them to the first <code>RCPT TO</code>, which leaks less to an address-probing client but accepts more of the conversation first. A session from <a href="/topic/28240">a configured incoming relay</a> has both phases deferred to after <code>DATA</code>.</p>
<p dir="auto"><strong>Who is never tested.</strong> <code>SMTPConnection::GetDoSpamProtection_</code> returns false the moment the session is authenticated, so your own users' outbound mail is never scored; <a href="/topic/28239">the sending limits and rate limits</a> are the controls for that. A sender or IP on the anti-spam <strong>White list</strong> skips the tests entirely, not merely the scoring: SPF is not evaluated, no <code>Authentication-Results</code> verdict is recorded and no reason header is written.</p>
<p dir="auto"><strong>The read-only overview.</strong> <strong>Spam &amp; virus filtering → Spam filtering overview</strong> 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.</p>
<h3><a class="anchor-offset" name="the-score-and-the-two-thresholds"></a>The score and the two thresholds</h3>
<p dir="auto">Both thresholds are on <strong>Spam &amp; virus filtering → Anti-spam settings → General</strong>.</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Setting</th>
<th>Default</th>
<th>At or above it</th>
<th><code>0</code> means</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>AntiSpam.SpamMarkThreshold</code></td>
<td>5</td>
<td>Delivered but tagged: <code>X-hMailServer-Spam: YES</code>, <code>X-hMailServer-Reason-*</code> headers and, optionally, a subject prefix</td>
<td>Never mark</td>
</tr>
<tr>
<td><code>AntiSpam.SpamDeleteThreshold</code></td>
<td>20</td>
<td>Refused during the conversation, or quarantined if quarantine is on and the verdict came after the body</td>
<td>Never refuse</td>
</tr>
<tr>
<td><code>AntiSpam.MaximumMessageSize</code></td>
<td>1024 KB</td>
<td>Post-transmission tests are skipped for a larger message</td>
<td>No limit of its own; the MIME parser's 80 MB ceiling still applies</td>
</tr>
</tbody>
</table>
<p dir="auto">The higher of the two thresholds is also the early-out: <code>SpamTestRunner::RunSpamTest</code> 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.</p>
<p dir="auto">With <code>AddHeaderSpam</code> and <code>AddHeaderReason</code> both on (the defaults), a marked message carries:</p>
<pre><code class="language-text">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
</code></pre>
<p dir="auto">Only failing tests get a <code>Reason-N</code> line. <code>X-hMailServer-Reason-Score</code> is the sum of the failing tests, and with <code>AddHeaderReason</code> off it is not written at all. The subject prefix is off by default; its text is <code>[SPAM]</code> (<code>antispamprependsubjecttext</code>).</p>
<p dir="auto"><strong>Start by tagging, not deleting.</strong> The wiki's advice is a fortnight with the delete threshold at <code>0</code>, reading what got tagged, before turning deletion on well above anything legitimate has scored.</p>
<h3><a class="anchor-offset" name="every-test-in-the-order-it-runs"></a>Every test, in the order it runs</h3>
<p dir="auto">This is the registration order in <code>SpamTestRunner::LoadSpamTests</code>; nothing reorders it at runtime. Tests 1 to 6 run before the body, 7 to 12 after it.</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>#</th>
<th>Test</th>
<th>Switched on by</th>
<th>Default</th>
<th>Default score</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Blocked senders</td>
<td>a non-empty <strong>Blocked senders</strong> list</td>
<td>list empty</td>
<td>100 on a new entry</td>
</tr>
<tr>
<td>2</td>
<td>DNS blacklists</td>
<td>an active row on <strong>DNS blacklists</strong></td>
<td>two rows, both inactive</td>
<td>3</td>
</tr>
<tr>
<td>3</td>
<td>HELO host</td>
<td><code>AntiSpam.CheckHostInHelo</code></td>
<td>off</td>
<td>2</td>
</tr>
<tr>
<td>4</td>
<td>PTR record</td>
<td><code>AntiSpam.CheckPTR</code></td>
<td>off</td>
<td>1</td>
</tr>
<tr>
<td>5</td>
<td>Sender MX</td>
<td><code>AntiSpam.UseMXChecks</code></td>
<td>off</td>
<td>2</td>
</tr>
<tr>
<td>6</td>
<td>SPF</td>
<td><code>AntiSpam.UseSPF</code></td>
<td>off</td>
<td>3</td>
</tr>
<tr>
<td>7</td>
<td>SURBL</td>
<td>an active row on <strong>SURBL servers</strong></td>
<td>one row, inactive</td>
<td>3</td>
</tr>
<tr>
<td>8</td>
<td>DKIM</td>
<td><code>AntiSpam.DKIMVerificationEnabled</code></td>
<td>off</td>
<td>5</td>
</tr>
<tr>
<td>9</td>
<td>ARC</td>
<td><code>AntiSpam.ArcFilteringEnabled</code>, a trusted-sealer list and DMARC on</td>
<td>off</td>
<td>minus the DMARC failure score</td>
</tr>
<tr>
<td>10</td>
<td>DMARC</td>
<td><code>AntiSpam.DMARCEnabled</code></td>
<td><strong>on</strong></td>
<td>5</td>
</tr>
<tr>
<td>11</td>
<td>SpamAssassin</td>
<td><code>AntiSpam.SpamAssassinEnabled</code></td>
<td>off</td>
<td>5, or the merged score</td>
</tr>
<tr>
<td>12</td>
<td>Filter hook</td>
<td>a non-empty <code>FilterHookUrl</code></td>
<td>empty</td>
<td>the engine's score; 100 for a reject</td>
</tr>
</tbody>
</table>
<p dir="auto"><strong>DMARC is the only test on by default</strong>, 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.</p>
<p dir="auto">What makes a test fail matters as much as its score. SPF scores only a hard <code>Fail</code>. DKIM scores only <code>PermFail</code>; a message with no signature is <code>None</code>, not a failure. DMARC scores a <code>p=reject</code> or <code>p=quarantine</code> policy the message failed and, separately, a message with more than one <code>From</code> header field; <code>p=none</code> failures are logged and score nothing. The blocked-senders list never matches the null sender, and its <code>550</code> says only "Sender address is blocked." without naming the entry. <strong>Every DNS-backed check fails open</strong>: 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 <code>TempFail</code>, not <code>PermFail</code>, so it no longer scores.</p>
<h3><a class="anchor-offset" name="greylisting"></a>Greylisting</h3>
<p dir="auto">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 <strong>Anti-spam settings → Greylisting</strong>.</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Setting</th>
<th>Default</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>AntiSpam.GreyListingEnabled</code></td>
<td>off</td>
<td>Master switch</td>
</tr>
<tr>
<td><code>AntiSpam.GreyListingInitialDelay</code></td>
<td>30 minutes</td>
<td>How long a new triplet is refused for</td>
</tr>
<tr>
<td><code>AntiSpam.GreyListingInitialDelete</code></td>
<td>24 hours</td>
<td>An unconfirmed triplet is forgotten after this</td>
</tr>
<tr>
<td><code>AntiSpam.GreyListingFinalDelete</code></td>
<td>864 hours (36 days)</td>
<td>A confirmed triplet's expiry, pushed forward on every accepted message</td>
</tr>
<tr>
<td><code>AntiSpam.BypassGreylistingOnSPFSuccess</code></td>
<td>on</td>
<td>Needs the SPF test enabled and to have run</td>
</tr>
<tr>
<td><code>AntiSpam.BypassGreylistingOnMailFromMX</code></td>
<td>off</td>
<td>Costs an A and an MX lookup per message</td>
</tr>
</tbody>
</table>
<p dir="auto">A greylisted recipient gets <code>451 Please try again later</code> at <code>RCPT TO</code>; when the envelope sender is empty the refusal is postponed to the end of <code>DATA</code> and answered <code>451</code> (<a href="https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/Greylisting.md" rel="nofollow ugc">docs/Greylisting.md</a>; the wiki prints <code>450</code>), because an empty sender at <code>RCPT TO</code> is very often another server's callout. Greylisting is skipped for a recipient domain with <strong>Enable greylisting for this domain</strong> 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 <code>GreylistingEnabledDuringRecordExpiration=0</code> did nothing at 6.2.28, so greylisting stays in force during the expiry sweep whatever it says.</p>
<p dir="auto"><strong>Hardened since 6.3.4</strong> (schema 6072; <a href="https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/Greylisting.md" rel="nofollow ugc">docs/Greylisting.md</a><img src="https://www.hmailserver.co.uk/assets/plugins/nodebb-plugin-emoji/emoji/android/1f61e.png?v=6eecd33477c" class="not-responsive emoji emoji-android emoji--disappointed" style="height:23px;width:auto;vertical-align:middle" title="):" alt="😞" /></p>
<ul>
<li><strong>The triplet key no longer truncates.</strong> 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, <code>A@Example.com</code> and <code>a@example.com</code> were two triplets and are now one, so a sender waits once less, never once more. Old rows are not rewritten at upgrade.</li>
<li><strong>A sliding wait and an abuse lock</strong>, both off. With <code>GreyListingSlideOnEarlyRetry</code> on, each retry before the window opens moves the window to now plus the initial delay. <code>GreyListingAbuseThreshold</code> (0; the operator who asked for it uses 20) is the attempt answered <code>550 5.7.1 Too many delivery attempts before the greylisting delay had passed. ...</code>, with the triplet locked for <code>GreyListingAbuseLockMinutes</code> (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. <strong>Clear greylisting triplets</strong> lifts every lock, at the cost of greylisting every sender once more.</li>
<li><strong>The greylisting bypass list</strong> (<code>/api/v1/greylisting-bypass</code>, table <code>hm_greylisting_bypass</code><img src="https://www.hmailserver.co.uk/assets/plugins/nodebb-plugin-emoji/emoji/android/1f61e.png?v=6eecd33477c" class="not-responsive emoji emoji-android emoji--disappointed" style="height:23px;width:auto;vertical-align:middle" title="):" alt="😞" /> a domain such as <code>example.com</code>, or a suffix such as <code>.mail.example.com</code>, matched on the envelope domain, the HELO host or either, passes greylisting only when SPF, DKIM or DMARC passes <strong>for that name</strong>. At least one requirement is needed. An entry requiring DKIM or DMARC cannot be judged at <code>RCPT TO</code>, so its greylisting runs after <code>DATA</code>, where a <code>451</code> or <code>550</code> ends the transaction for every recipient. <strong>The test an entry requires must be switched on</strong>: 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 <code>RCPT TO</code> 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. <code>BypassGreylistingOnSPFSuccess</code>, by contrast, covers every SPF pass in the world, a hijacked mailbox at a large provider included.</li>
</ul>
<h3><a class="anchor-offset" name="dns-blacklists-and-surbl"></a>DNS blacklists and SURBL</h3>
<p dir="auto"><strong>Spam &amp; virus filtering → DNS blacklists</strong> seeds two rows, both inactive until you tick them:</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>DNS host</th>
<th>Expected result</th>
<th>Reject message</th>
<th>Score</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>zen.spamhaus.org</code></td>
<td><code>127.0.0.2-8\|127.0.0.10-11</code></td>
<td>Rejected by Spamhaus.</td>
<td>3</td>
</tr>
<tr>
<td><code>bl.spamcop.net</code></td>
<td><code>127.0.0.2</code></td>
<td>Rejected by SpamCop.</td>
<td>3</td>
</tr>
</tbody>
</table>
<p dir="auto">The expected-result field takes <code>|</code> for alternatives, a range on the last octet and wildcards; a malformed range is error <code>HM5342</code>. 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:</p>
<pre><code class="language-text">DNS lookup: 9.113.0.203.zen.spamhaus.org, 1 addresses found: 127.0.0.4, Match: 1
</code></pre>
<p dir="auto">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.</p>
<p dir="auto"><strong>SURBL</strong> checks the links in the body, so it runs after <code>DATA</code>. 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: <code>multi.surbl.org</code>, "Rejected by SURBL.", score 3.</p>
<h3><a class="anchor-offset" name="spf-dkim-dmarc-and-arc"></a>SPF, DKIM, DMARC and ARC</h3>
<p dir="auto"><strong>SPF</strong> (<code>AntiSpam.UseSPF</code>, off, score 3). Evaluation is bounded at 10 DNS-querying terms and at <code>SpfVoidLookupLimit</code> void lookups (default 2; <code>0</code> 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:</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th></th>
<th>6.3.3 and earlier</th>
<th>Since 6.3.4</th>
</tr>
</thead>
<tbody>
<tr>
<td>DNS</td>
<td>Its own lookups; <code>DNSServer</code>, the cache and the timeout were ignored on Windows</td>
<td>The server's own resolver settings apply</td>
</tr>
<tr>
<td>Linux</td>
<td>Every check was <code>TempError</code> (<code>HM6406</code>), so DMARC lost its SPF half</td>
<td>Works</td>
</tr>
<tr>
<td>Loopback clients</td>
<td><code>127.0.0.0/8</code> and <code>::1</code> passed before any policy was read</td>
<td>No exemption</td>
</tr>
<tr>
<td><code>Authentication-Results</code></td>
<td>Everything but <code>pass</code> and <code>fail</code> written as <code>neutral</code></td>
<td>Each of the seven results under its own keyword</td>
</tr>
</tbody>
</table>
<p dir="auto">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 <strong>a content filter that re-injects mail should authenticate, or connect from an IP range with anti-spam off</strong>.</p>
<p dir="auto"><strong>DKIM</strong> (<code>AntiSpam.DKIMVerificationEnabled</code>, off, score 5). RSA keys shorter than 1024 bits are refused outright, and <code>rsa-sha1</code> signatures are invalid unless <code>DkimAcceptSha1=1</code>.</p>
<p dir="auto"><strong>DMARC</strong> (<code>AntiSpam.DMARCEnabled</code>, on, score 5). More than one <code>From</code> field is scored before any alignment is computed, because DKIM signs the last <code>From</code> while the header reader answers with the first.</p>
<p dir="auto"><strong>ARC</strong> (<code>AntiSpam.ArcFilteringEnabled</code>, off; <code>AntiSpam.ArcTrustedSealers</code>, 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.</p>
<p dir="auto"><strong>Since 6.3.4: policy the score cannot express</strong> (schema 6073; all off by default; <a href="https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SmtpPolicy.md" rel="nofollow ugc">docs/SmtpPolicy.md</a><img src="https://www.hmailserver.co.uk/assets/plugins/nodebb-plugin-emoji/emoji/android/1f61e.png?v=6eecd33477c" class="not-responsive emoji emoji-android emoji--disappointed" style="height:23px;width:auto;vertical-align:middle" title="):" alt="😞" /></p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Setting</th>
<th>What it does</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>UseSPFSoftFailScore</code>, <code>UseSPFTempErrorScore</code>, <code>UseSPFPermErrorScore</code></td>
<td>Scores of their own for SoftFail (<code>~all</code>), TempError and PermError. <code>0</code> is "not scored", as before</td>
</tr>
<tr>
<td><code>SPFTempErrorDefer</code></td>
<td>Answers a TempError with <code>451 4.4.3</code>, so a resolver blip defers the message. Never a <code>451</code> on PermError</td>
</tr>
<tr>
<td><code>RejectMultiRcptNullSender</code></td>
<td>A null sender's second and every later distinct recipient, and its <code>DATA</code>, are refused <code>550 5.7.1</code>. A single-recipient null sender is never touched</td>
</tr>
<tr>
<td><code>BanMultiRcptNullSender</code>, <code>BanMultiRcptNullSenderHours</code></td>
<td>Also bans the address through the auto-ban, once per session, for 24 hours by default (1 to 8760); never an address on <code>AutoBanNeverBan</code> or an incoming relay. Does nothing while <code>RejectMultiRcptNullSender</code> is off</td>
</tr>
</tbody>
</table>
<p dir="auto">The <strong>domain white and black list</strong> (<strong>Spam &amp; virus filtering → Domain white and black list</strong>; <code>/api/v1/domain-policy-list</code>) 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 <code>MAIL FROM</code> for unauthenticated sessions of a range that runs the spam tests. <strong>Black</strong> is <code>550 5.7.1 Mail from &lt;name&gt; is refused by this server's policy.</code> before anything else runs. <strong>White</strong> 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 <code>bücher.example</code> is refused, and <code>hmctl</code> has no verb for it.</p>
<h3><a class="anchor-offset" name="spamassassin-and-the-filter-hook"></a>SpamAssassin and the filter hook</h3>
<p dir="auto">hMailServer speaks the spamd protocol over TCP to any SpamAssassin daemon, on this machine or on a Linux host. Set <code>AntiSpam.SpamAssassinHost</code> (<code>127.0.0.1</code>) and <code>AntiSpam.SpamAssassinPort</code> (<code>783</code>) and press <strong>Test SpamAssassin connection</strong>. <code>AntiSpam.SpamAssassinMergeScore</code> (off) uses the integer part of SpamAssassin's own <code>score=</code> in place of the fixed <code>AntiSpam.SpamAssassinScore</code> (5). The timeout moves between <code>SAMinTimeout</code> and <code>SAMaxTimeout</code> (30 and 90 seconds) with load. <code>SpamAssassinLearnOnMove</code> (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 <code>--allow-tell</code>. An unresolvable host is <code>HM5507</code>; a scan that does not finish in time is <code>HM5508</code>, and the message is accepted with no verdict; a message that cannot be reloaded after the scan is <code>HM5509</code> and is left as SpamAssassin wrote it.</p>
<p dir="auto"><code>FilterHookUrl</code> puts rspamd, or anything answering JSON over HTTP, in the path of every accepted message; <code>http://127.0.0.1:11333/checkv2</code> needs no adapter. <code>FilterHookTimeoutSeconds</code> is 10, and with <code>FilterHookFailClosed</code> (off) turned on an unanswered check scores <code>FilterHookRejectScore</code> (100) and reports <code>HM5541</code>.</p>
<h3><a class="anchor-offset" name="quarantine-and-what-a-refused-sender-sees"></a>Quarantine, and what a refused sender sees</h3>
<p dir="auto"><strong>Quarantine</strong> (<code>QuarantineEnabled</code>, off; <code>QuarantineRetentionDays</code>, 30) changes what the delete threshold does after the body: the message is answered <code>250 Queued for delivery</code> and held on <strong>Spam &amp; virus filtering → Quarantine</strong>. 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 href="/topic/28251">a mail flow rule</a> sends there (<code>Rule: &lt;name&gt;</code>), what impersonation protection holds and what a recipient's blocked-senders list holds.</p>
<p dir="auto"><strong>Per-account overrides</strong>, on the account dialog's <strong>Spam</strong> tab, re-judge that account's own copy at delivery. They only read <code>X-hMailServer-Reason-Score</code>, and only while <strong>Add X-hMailServer-Reason header</strong> is on; with it off the value could be the sender's own, so the overrides decline to act.</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Moment</th>
<th>Client sees</th>
</tr>
</thead>
<tbody>
<tr>
<td>Score over delete threshold, before the body</td>
<td><code>550 5.7.1 &lt;text of the first failing test&gt;</code></td>
</tr>
<tr>
<td>Score over delete threshold, after the body</td>
<td><code>554 5.7.1 &lt;text of the first failing test&gt;</code></td>
</tr>
<tr>
<td>Same, but quarantined</td>
<td><code>250 2.0.0 Queued for delivery</code></td>
</tr>
<tr>
<td>Greylisted, real sender</td>
<td><code>451 Please try again later.</code></td>
</tr>
<tr>
<td>Greylisted, null sender</td>
<td><code>451</code> at end of <code>DATA</code> (the wiki prints <code>450</code>)</td>
</tr>
</tbody>
</table>
<p dir="auto">Per-test timings go to the debug log (<code>Spam test: SpamTestSPF, Score: 3, Time: 41 ms</code>); 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.</p>
<h3><a class="anchor-offset" name="a-configuration-that-works"></a>A configuration that works</h3>
<p dir="auto">The wiki's order for a small server:</p>
<ol>
<li>Leave the thresholds at 5 and 20 and leave DMARC on. Watch for a fortnight.</li>
<li>Activate <code>zen.spamhaus.org</code> (score 3).</li>
<li>Turn on SPF (score 3) and DKIM verification (score 5). With a DNSBL hit they reach 11, still under the delete threshold.</li>
<li>Turn on <code>AuthenticationResultsEnabled</code>, so every delivered message records what the server concluded.</li>
<li>Add greylisting if your users can tolerate a first-message delay, leaving bypass on SPF success on.</li>
<li>Only then add SpamAssassin or an external engine, and consider raising the delete threshold rather than lowering it.</li>
</ol>
<h3><a class="anchor-offset" name="what-this-topic-does-not-cover-and-where-to-read-more"></a>What this topic does not cover, and where to read more</h3>
<p dir="auto">The per-recipient allow and block lists, the quarantine digest, spam traps, country blocking and impersonation protection are in <a href="/topic/28249">allow and block lists, digests and spam traps</a>, and viruses in <a href="/topic/28250">viruses and dangerous attachments</a>. The other direction is <a href="/topic/28188">Being trusted by the servers you send to</a>. The Control Panel and COM are Windows-only; the 6.3.4 switches are also on the Settings view of <a href="/topic/28192">the Control Deck</a> and <code>PUT /api/v1/settings/antispam</code>.</p>
<ul>
<li>Wiki: <a href="https://gitlab.com/hmailserver/hmailserver/-/wikis/Stopping-Spam" rel="nofollow ugc">https://gitlab.com/hmailserver/hmailserver/-/wikis/Stopping-Spam</a></li>
<li><a href="https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/Greylisting.md" rel="nofollow ugc">https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/Greylisting.md</a></li>
<li><a href="https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SmtpPolicy.md" rel="nofollow ugc">https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SmtpPolicy.md</a></li>
<li>Public documentation: <a href="https://www.progressiverobot.com/hmailserver-documentation/#10-stopping-spam" rel="nofollow ugc">https://www.progressiverobot.com/hmailserver-documentation/#10-stopping-spam</a> and <a href="https://www.progressiverobot.com/hmailserver-documentation/#265-the-anti-spam-pipeline" rel="nofollow ugc">https://www.progressiverobot.com/hmailserver-documentation/#265-the-anti-spam-pipeline</a></li>
<li><a href="/topic/28218">The 6.3.4 security announcement</a> and <a href="/topic/28193">Upgrading to 6.3.4</a></li>
</ul>
]]></description><link>https://www.hmailserver.co.uk/topic/28248/the-anti-spam-stack-end-to-end-score-greylisting-spf-dkim-dmarc-dnsbl-surbl</link><generator>RSS for Node</generator><lastBuildDate>Fri, 06 Nov 2026 09:05:50 GMT</lastBuildDate><atom:link href="https://www.hmailserver.co.uk/topic/28248.rss" rel="self" type="application/rss+xml"/><pubDate>Sun, 27 Sep 2026 19:30:13 GMT</pubDate><ttl>60</ttl></channel></rss>