trustContacts override silently requires the sender to pass DMARC — documentation describes it as unconditional

Issue Description

The documentation describes trustContacts as causing a message from an
address-book contact to bypass the spam decision and reach the inbox
whatever its score. The implementation applies a further condition that
is not documented anywhere: the sender must be authenticated, which for
inbound SMTP means the message passed DMARC. When the sending domain
publishes no DMARC record, the override never fires — even though the
contact is found and every other condition holds.

To be clear about the scope: I believe the condition itself is deliberate
and sound, and I am not asking for it to be removed (evidence in
Additional Context). This report is about the documentation describing
different behaviour, and about the operator having no way to observe or
configure the condition.

Expected Behavior

Per the Trusted senders page (Trusted senders | Stalwart)
and the SpamSettings reference (SpamSettings | Stalwart), both of which present
the feature as unconditional, a message whose sender is present in the
recipient’s address book should be delivered to the inbox regardless of
the score assigned by the filter.

Actual Behavior

The message is filed into Junk with “X-Spam-Status: Yes”.

The card-exists override at crates/email/src/message/ingest.rs:262-296
is skipped because is_sender_authenticated is false, which follows from
the sending domain having no DMARC record.

Worth noting separately: the deviation is invisible. When the override
fires, the header carries “reason=card-exists”. When it is skipped, no
header, log entry or trace event records that the override was
considered and rejected, so there is nothing to debug from.

Reproduction Steps

  1. Leave trustContacts = true (the default).
  2. Add a contact to the recipient’s default address book whose email
    address belongs to a domain that publishes NO DMARC record. Confirm
    the server sees it: ContactCard/query with filter
    {“inAddressBook”: “”, “email”: “”} returns the card id.
  3. Have that sender deliver a message over inbound SMTP that scores at
    or above scoreSpam. SPF and DKIM may both pass — only the DMARC
    record must be absent.
  4. The message is filed into Junk. Re-run the ContactCard/query from
    step 2: the card is still there.

Step 3 works with any domain lacking a DMARC record; I reproduced it
with a Microsoft 365 tenant.

Relevant Log Output

Authentication-Results: ;
dkim=pass header.d=.onmicrosoft.com;
spf=pass smtp.mailfrom=@;
iprev=pass;
dmarc=none header.from= policy.dmarc=none

X-Spam-Result: DKIM_ALLOW (-0.20), SPF_ALLOW (-0.20),
DMARC_NA (1.00), HELO_IPREV_MISMATCH (1.00),
MID_RHS_MATCH_FROM (1.00), MIME_BASE64_TEXT_BOGUS (1.00),
PYZOR (3.50)
X-Spam-Score: spam, score=7.10
X-Spam-Status: Yes

server-side confirmation that the contact is visible to Stalwart

ContactCard/query {“inAddressBook”: “d”, “email”: “”}
→ {“ids”: [“bmc”], “total”: null}

Stalwart Version

v0.16.x

Installation Method

Binary (Linux)

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

Internal

Additional Context

Two pieces of evidence that the DMARC condition is deliberate rather
than an oversight, which is why I am asking for documentation rather
than for a code change:

  1. CHANGELOG.md, 0.12.5 (2025-06-25), under Fixed:
    “Antispam: Skip card-is-ham override when sender does not pass
    DMARC (#1648).”
    The feature shipped in 0.12.0; the condition was added a month later.

  2. tests/src/system/delivery.rs:275-333 exercises the override using a
    contact at [email protected]. The “dmarc-” prefix exists solely
    to trigger the #[cfg(feature = “test_mode”)] branch at
    crates/smtp/src/inbound/data.rs:749-754, which forces
    dmarc_pass = true. The test had to construct a DMARC-passing sender
    for the override to fire; the preceding case in the same test, using
    [email protected], lands in Junk.

Code path, v0.16.20 (6d5c658914006a25a8a58e11d3cc3afc8474ee3f):

  • email/src/message/ingest.rs:262-296 — override requires
    is_sender_authenticated
  • smtp/src/outbound/local.rs:48-50 — sender_authenticated =
    flags & (FROM_UNAUTHENTICATED_DMARC | FROM_AUTHENTICATED) != 0
  • smtp/src/inbound/data.rs:746-761 — unauthenticated inbound sessions
    get MessageSource::Unauthenticated { dmarc_pass }
  • smtp/src/inbound/data.rs:384 — dmarc_pass needs SPF or DKIM to
    return DmarcResult::Pass; with no DMARC record the result is None

What I would ask for, in order of usefulness:

  1. Document the condition on the Trusted senders page and in the
    SpamSettings reference.
  2. Record the skipped override somewhere observable — a reason on
    X-Spam-Status, or a trace event.
  3. Possibly a trustContactsRequireDmarc field on SpamSettings,
    defaulting to true, for operators who want to make the trade-off
    explicitly. I am happy to open this separately under Feature Ideas
    if that is the better place for it.

A substantial share of legitimate domains still publish no DMARC record,
so in practice trustContacts is inert for a fair number of
correspondents with no signal to the operator that this is the case.

Client: Thunderbird with all its own junk filtering disabled, so the
classification is Stalwart’s.

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

Confirmed. The Trusted senders page and the SpamSettings reference for trustContacts both describe the address-book override as applying “regardless of the score that would otherwise be assigned,” with no mention of a DMARC/authentication precondition:

“If the sender of a message is present in the recipient’s address book, the message is treated as trusted… the message bypasses spam filtering decisions and is placed in the recipient’s inbox, regardless of the score that would otherwise be assigned.”

Nothing on that page or in the spam-settings reference qualifies this with an authentication requirement, so your reading of the documented behavior is correct, and it doesn’t match what you observed with dmarc=none senders. This is a documentation gap (and possibly an observability gap, since there’s no header/log signal when the override is skipped) rather than something I can resolve here — a human maintainer will need to update the docs and weigh in on your suggestions for surfacing the skip reason or adding a configurable field.

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