Uppercase in recipient address → Mail not delivered

Issue Description

When receiving a mail with the local part of the destination email in UPPERCASE, the mail is not delivered in the mailbox.

Expected Behavior

The email should be delivered without being impacted by any case sensitivity.

Actual Behavior

The email is not delivered and an error message of type “Undelivered Mail Returned to Sender” is sent to the original mail’s sender address.

Reproduction Steps

  1. Install Stalwart
  2. Create a domain (for example “domain.tld”)
  3. Create an account in this domain (for example “user”)
  4. Send a mail to “user @ domain.tld” and check that it is well received
  5. Send a mail to “USER @ domain.tld” and check the mail is not received + error mail sent

Stalwart Version

v0.16.x

Installation Method

Docker

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

Internal

Additional Context

My Docker container runs within a Synology NAS and behind DSM’s reverse proxy.

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

Case can’t be the cause on our side. The whole recipient address, local part included, is lowercased before any lookup happens, and lowercased again before local delivery, so [email protected] and [email protected] are indistinguishable by the time anything is resolved.

The bounce is the useful clue here. “Undelivered Mail Returned to Sender” is Postfix’s wording, not ours; our DSNs say “Failed to deliver message”. So that bounce was written by the sending side, and its body will name the host that actually rejected and the SMTP code it used, which should settle where this is happening. Could you post the full bounce, including the Reporting-MTA line?

Alongside that, the Stalwart-side log for the transaction filtered on the sender’s IP. If there’s no smtp.rcpt-to event at all then the message never reached us. And if there is one, let me know whether you have a Sieve script, an address rewrite, a milter or an MTA hook configured on the RCPT stage, because that’s the only place in your setup where a case-sensitive comparison could live. The Synology proxy isn’t relevant to this one, it only fronts HTTP and port 25 doesn’t pass through it.

I have inspected a bit more the error message, and you are right, I should have seen that it does not come from Stalwart, but from my domain name registrar (OVH), that I am using as a SMTP relay for sending mails.

In fact, things are going like this:

  • I am sending a test message from an external mailbox (other domain) to my local mailbox (“domain.tld”)
  • Since the local part is in UPPERCASE, it seems that my MTA outbound strategy (“IF is_local_address(rcpt) THEN ‘local’ ELSE ‘relay’”) chooses the “relay” route instead of “local”
  • The mail is then transferred to OVH without modifying the original sender address, which triggers its anti-spoofing policy, because this external domain is not managed by the account I am using to authenticate to use the SMTP relay
  • OVH sends back to my internal mailbox the error message I mentioned earlier, which includes a 550 5.7.1 error code (‘Rejected by policy: From header domain does not align with authenticated domain’), with the subject ‘Undelivered Mail Returned to Sender’

I am not using any Sieve script, address rewrite or mail filters (at least that I know of). Since the problem could be linked to my MTA routing strategy which may not recognize the uppercase local part, I have tried modifying it to “IF is_local_address(to_lowercase(rcpt)) THEN ‘local’ ELSE ‘relay’”), but it does not seem to do any good: the result remain the same.

I attach an error mail that I just received from another test, in hope it can lead to something…

Error_Mail.txt (7.4 KB)

That routing rule is the case-sensitive comparison we were looking for. In a route expression, rcpt is the envelope recipient with the local part exactly as it was received, since only the domain gets lowercased on the way into the queue. is_local_address() then does an exact directory lookup, so [email protected] misses, the rule falls through to relay, and OVH quite reasonably refuses to relay for a domain you aren’t authenticated for. That’s the whole chain.

Change that first branch to is_local_domain(rcpt_domain), which is what the shipped default route uses. It can’t be tripped by case, because the domain is always lowercased. I’d move off is_local_address for this test regardless of the case problem: it also returns false for sub-addressed recipients, for catch-all domains and for domains you relay for, so it will route mail you do accept out to OVH as well.

If it still misroutes after that change, post the full route expression including the else branch and one queue log line for a test message, so I can see which arm is being selected. That will also show whether the to_lowercase() edit landed on the route rather than on another strategy object.

Thanks for your feedback!

I have tried to put “is_local_domain(rcpt_domain)” instead of “is_local_address(rcpt)” and indeed I got the email correctly this time. And when I inspect its headers, I can see that the uppercase is kept both in the local part and domain part.

Here, I must mention an important point: I had to reload the server settings in order for this change of result to occur… It seems the policy is not really applied when I save it, I do not know whether if this is intentional or not?

Anyway, Since I had already run into this reload-settings quirk before (when I first set my policy to “is_local_address(rcpt)”), it occurred to me that my earlier “to_lowercase()” test might have failed simply because I hadn’t reloaded after making that change. So I tried again with “is_local_address(to_lowercase(rcpt))”, this time reloading the settings afterwards — and now it works! My tests of sending mails with a destination address in uppercase are OK.

I could stay like this and call the problem solved, but as a matter of fact, I willfully changed my policy to “is_local_address(rcpt)” because I need to use split-delivery because some mailboxes of my domain are managed on another server, so I wanted Stalwart to deliver messages only to mailboxes directly managed by him and relay the rest to my relay at OVH.

Since you seem to think I should not be using “is_local_address()” because of other problems it may lead to, do you think there would be a better solution to keep split-delivery and avoid this uppercase problem?