Generating two SPF records for a single domain is rejected by some mail servers

Issue Description

Stalwart is suggesting two SPF records for a domain, which could lead to rejection of some emails.

For example:

"v=spf1 a -all"
"v=spf1 mx -all"

This violates the SPF specification, which permits exactly one v=spf1 record per domain. As a result, SPF validation returns:

(tested with https://mecsa.jrc.ec.europa.eu/)

550 SPF Permanent Error: Two or more type TXT spf records found

Receiving mail servers may reject or distrust outgoing mail due to SPF PermError.

Expected Behavior

The generated SPF configuration should merge mechanisms into a single TXT record, for example:

"v=spf1 a mx -all"

Actual Behavior

"v=spf1 a -all"
"v=spf1 mx -all"

It is being generated for the domain zone file.

Reproduction Steps

I do not know how to reproduce it but this is only happening for one domain:
mail.example.com is my mail server domain and a mail send/receive domain. Stalwart generates two SPF records in the zone file.
notifications.example.com is another one, which only has "v=spf1 mx -all".
Also, example.com is configured for another mail provider.

Stalwart Version

v0.16.x

Installation Method

Docker

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

Internal

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

Let me look into how Stalwart generates SPF records for DNS zone files.

This is a confirmed bug in the SPF record generation code. Looking at the DnsRecordType::Spf branch in crates/common/src/network/dns/records.rs, the logic is:

  1. For each MX hostname that is a subdomain of the domain (e.g. mail.example.com), it emits: TXT mail.example.com. "v=spf1 a -all"
  2. Then unconditionally for the domain itself, it emits: TXT example.com. "v=spf1 mx -all"

The issue you’re hitting is specifically when mail.example.com is also configured as a send/receive domain in Stalwart. In that case, when the SPF records are generated for mail.example.com as a domain, it gets both records at the same name:

  • TXT mail.example.com. "v=spf1 a -all" (from the MX subdomain logic, because mail.example.com ends with .example.com)
  • TXT mail.example.com. "v=spf1 mx -all" (from the unconditional domain record)

This violates RFC 7208 which allows exactly one v=spf1 TXT record per name. The fix would need to either merge the mechanisms into a single record (v=spf1 a mx -all) or skip the a sub-record when the MX hostname equals the domain itself.

Workaround until this is fixed: Manually manage the DNS records for mail.example.com instead of using Stalwart’s auto-generated zone. Publish a single merged record:

TXT  mail.example.com.  "v=spf1 a mx -all"

This is a bug that should be reported to the development team – a community member or maintainer will follow up.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong. If you found it useful, mention @helpbot in any reply to ask follow-up questions and the bot will respond again.

One SPF record is for the domain and another one for the MX host. Is your MX/server hostname the apex domain? (For example domain.com)

Kind of. my apex domain does have a MX record, but not for stalwart. Stalwarts MX and A domain are mai.domain.com

This has been fixed and will be in v0.16.6. The issue was related to using apex domains as MX servers.