DMARC policy is found as “reject”, but failed authentication is returned as dmarc.none

Issue Description

Stalwart correctly resolves the sender domain’s DMARC record and reports its policy as reject, but when both SPF and DKIM fail, the resulting DMARC status is dmarc.none (“No DMARC record”) instead of dmarc.fail.

The spam filter consequently assigns DMARC_NA (1.00) instead of DMARC_POLICY_REJECT (4.00).

Expected Behavior

When a valid p=reject DMARC record exists and neither SPF nor DKIM provides an aligned pass, the DMARC result should be dmarc.fail.

The spam filter should assign DMARC_POLICY_REJECT, not DMARC_NA.

Actual Behavior

Stalwart finds the DMARC policy and reports policy = "reject", but returns:

result = No DMARC record (dmarc.none)

The spam classifier assigns DMARC_NA (1.00). The reproduced message received a total score of 10.90 and was accepted into the Spam mailbox.

Reproduction Steps

  1. Use a sender domain with a valid DMARC record containing p=reject. The issue was reproduced with both example.com and another independently managed domain.
  2. From an unauthenticated external SMTP connection, submit a message whose envelope sender and header From use that domain.
  3. Do not provide a DKIM signature and send from an IP address not authorized by SPF.
  4. Observe that SPF fails, DKIM does not pass, and Stalwart finds the DMARC policy reject.
  5. Observe that the DMARC result is nevertheless returned as dmarc.none, and the spam filter assigns DMARC_NA.

Relevant Log Output

SPF From check failed ... from = "[email protected]", result = SPF check failed (spf.fail)

DKIM verification failed ... result = []

DMARC check failed ... domain = "example.com", policy = "reject", result = No DMARC record (dmarc.none)

Relevant spam-filter result:

DMARC_NA (1.00)
SPF_FAIL (1.00)
VIOLATED_DIRECT_SPF (3.50)
X-Spam-Score: spam, score=10.90

Stalwart Version

v0.16.x

Installation Method

Binary (Linux)

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

Internal

Additional Context

Deployment: rootless Podman container on RHEL 9.

Image:

ghcr.io/stalwartlabs/stalwart@sha256:c11546a398589a59271b5685d397a2f291a1da4b896a3c944f0741e124ba5b1c

Image ID:

c65d9ec32478787288d08458764f443d773ae8e653cc4cf9fa721aaa96793f7b

The DMARC TXT records were verified successfully using:

  • the host resolver over UDP and TCP;
  • the container DNS forwarder over UDP and TCP;
  • the authoritative DNS servers;
  • DNSSEC validation.

A positive control from Gmail works correctly:

SPF From check passed
DKIM verification passed, domain = "gmail.com"
DMARC check passed, domain = "gmail.com", result = dmarc.pass

The positive-control message also receives DMARC_POLICY_ALLOW (-0.50). Therefore DNS resolution and the positive DMARC path are working; the inconsistent result occurs on the negative DMARC path.

I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.

on

I have searched this support forum (open and closed topics) and confirm this is not a duplicate.

on

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

Based on the source, this does look like a genuine bug rather than a misconfiguration on your side.

In crates/smtp/src/inbound/data.rs, the overall dmarc_result is derived like this:

let dmarc_result = if pass {
    DmarcResult::Pass
} else if dmarc_output.spf_result() != &DmarcResult::None {
    dmarc_output.spf_result().clone()
} else if dmarc_output.dkim_result() != &DmarcResult::None {
    dmarc_output.dkim_result().clone()
} else {
    DmarcResult::None
};

So the final dmarc.none you’re seeing means that, inside the DMARC evaluation itself, both the SPF-based and DKIM-based DMARC sub-results came back as DmarcResult::None rather than DmarcResult::Fail, even though your SPF check failed outright and no DKIM signature was present. According to the docs, dmarc.none is only supposed to mean “no DMARC policy record was found,” which clearly isn’t the case in your logs since the policy was resolved as reject.

This points to the DMARC alignment logic (in the underlying mail-auth verification library that Stalwart uses) not properly converting an outright SPF failure / missing DKIM signature into a DMARC Fail result when a policy record does exist, and instead defaulting both mechanisms to None, which then makes the overall combinator fall through to DmarcResult::None.

This is a code-level behavior, not something adjustable via configuration in the WebUI or CLI. It should be reported so a maintainer can look at the DMARC/SPF-alignment result mapping directly; a human will need to confirm and patch this.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

Fixed in v0.16.22 (yet to be released).