Hi @helpbot,
Phishers are using a technique that evades our current setup. I want to strictly drop these emails based on DMARC, but switching dmarcVerify to a global strict mode will also reject legitimate emails from domains with no DMARC record (dmarc.none).
Log: 2026-09-14T14:32:55Z INFO DMARC check failed (smtp.dmarc-fail) listenerId = "smtp", localPort = 25, remoteIp = xxxx, remotePort = 52710, strict = false, domain = "``mydomain.com``", policy = "reject", result = DMARC check failed (dmarc.fail) { causedBy = Policy not aligned (mail-auth.policy-not-aligned) }, elapsed = 24ms
My goal is to target only explicit DMARC failures caused specifically by alignment issues (mail-auth.policy-not-aligned).
if local_port == 25 && dmarc.fail && dmarc.reason == 'mail-auth.policy-not-aligned' then strict if local_port == 25 then relaxed else disable
Will the following expression block work correctly to achieve this?
Thank for help.
The proposed expression won’t work, because dmarcVerify doesn’t have access to the verification result at the point it’s evaluated. It’s a pre-check expression: it decides in which mode DMARC verification should run for a connection before the check itself is performed. The only variables it can see are those in MtaRcptToVariable (sender, sender_domain, recipients, rcpt, rcpt_domain, authenticated_as, listener, remote_ip, remote_port, local_ip, local_port, protocol, is_tls, priority, helo_domain, asn, country) — there’s no dmarc.fail or dmarc.reason variable exposed to it, so a condition like dmarc.fail && dmarc.reason == 'mail-auth.policy-not-aligned' will simply never evaluate true.
In practice, dmarcVerify can only return one of three values for a given connection: relaxed (verify and report), strict (verify and reject on failure), or disable. It can’t distinguish “DMARC fail due to alignment” from “DMARC fail because the domain has no policy” — that distinction only exists downstream in the actual DMARC evaluation and its logged event (mail-auth.policy-not-aligned is a cause code attached to the dmarc.fail event, not something the dmarcVerify expression can react to).
If you want to reject only messages that fail alignment while accepting domains that simply have p=none, that’s determined by the domain’s own published DMARC policy, not by your server config — strict mode already only rejects when the sender’s own policy says reject/quarantine and the message fails; it does not reject mail from domains whose DMARC policy is p=none. So switching to strict should be safe for dmarc.none domains; it only enforces the sending domain’s own stated policy.
That expression won’t compile. dmarcVerify decides whether the DMARC check runs at all, so it is evaluated before the check happens and no dmarc.* variable exists in its scope; what you get there is the MTA RCPT TO set, so local_port, remote_ip, sender, sender_domain, authenticated_as, helo_domain, asn, country and similar. An unknown variable doesn’t fail loudly either: it is logged as a config validation error and the field quietly reverts to its built-in default, so you would have been running stock behaviour while thinking the rule was in place.
You don’t need it, though, because strict doesn’t mean what the sentence in our docs says it means. It rejects only when the sending domain publishes p=reject and the result is a fail or a temp error. A domain with no DMARC record comes back with a None result and a policy that isn’t reject, so strict cannot touch it, and p=quarantine is never rejected either. What you have now is relaxed, which runs the full check, writes Authentication-Results, sends the reports and hands the result to the spam filter, but never enforces: that is the strict = false in your log line sitting next to policy = “reject”.
So what you want is the stock expression with relaxed swapped for strict on port 25. If you would rather not reject at SMTP time, a p=reject alignment failure already carries the DMARC_POLICY_REJECT tag at 4.0, and raising that or setting scoreReject drops the message on score instead.