Skip to content

fix: match Unicode alias names without punycode local part - #614

Open
uniejo wants to merge 1 commit into
forwardemail:masterfrom
uniejo:fix/unicode-alias-username
Open

uniejo wants to merge 1 commit into
forwardemail:masterfrom
uniejo:fix/unicode-alias-username

Conversation

@uniejo

@uniejo uniejo commented Oct 6, 2026 •

Copy link
Copy Markdown

The recipient local part was converted to punycode before matching, so an address like "fællestest@example.com" became "xn--fllestest-g3a" and never matched regex aliases such as /^.*llestest$/ or exact Unicode alias names, falling back to the catch-all instead.

parseUsername now returns the NFC normalized, lowercase Unicode local part. Alias matching tries both the Unicode and punycode forms (via the new get-username-variants helper) so legacy "xn--" aliases keep working. Alias names and the lookup API username are NFC normalized as well.

Checklist

  • I have ensured my pull request is not behind the main or master branch of the original repository.
  • I have rebased all commits where necessary so that reviewing this pull request can be done without having to merge it first.
  • I have written a commit message that passes commitlint linting.
  • I have ensured that my code changes pass linting tests.
  • I have ensured that my code changes pass unit tests.
  • I have described my pull request and the reasons for code changes along with context if necessary.

Problem

The recipient local part is converted to punycode before alias matching
(helpers/parse-username.js runs punycode.toASCII() on it). An address like
fællestest@example.com becomes xn--fllestest-g3a, so:

  • regex aliases such as /^.*llestest$/ never match on the MX side
    (helpers/get-forwarding-addresses.js tests the regex against the punycode form only),
  • exact Unicode alias names (fællestest:…) never match either,
  • the mail falls through to the * catch-all instead.

helpers/get-forwarding-configuration.js already tested both forms
(regex.test(username) || regex.test(punycode.toUnicode(username))), so the
paid-plan lookup returned the right alias, but the MX side then dropped it.

Punycode is only defined for domain names. Unicode local parts belong to
SMTPUTF8 (RFC 6531), so xn--… in the local part is a different mailbox.

Separately, nothing normalizes Unicode. blåtest written with a decomposed å
(NFD, common on macOS) does not equal the composed (NFC) alias name.

Changes

  • parseUsername returns the NFC-normalized, lowercase Unicode local part (no punycode).
  • New helpers/get-username-variants.js returns the Unicode and punycode forms,
    so aliases created as xn--… keep matching (backwards compatible).
  • get-forwarding-addresses and get-forwarding-configuration match regex, exact,
    and !/!!/!!! aliases against both forms. The legacy toUnicode fallback
    query now uses $in with the variants.
  • Alias names (app/models/aliases.js) and the lookup API username parameter are
    NFC-normalized.
  • The From-header rewrite in app/models/emails.js keeps its ASCII conversion
    explicitly, so outbound behaviour is unchanged.

Tests

test/unicode-alias-forwarding.js (12 tests) covers TXT and paid-plan mappings,
regex, $1 substitution, exact, ignored, NFC/NFD, legacy xn-- aliases, and
ASCII regressions. 9 of these fail against the current master.

Notes

  • Existing alias names stored in NFD form are only normalized on their next save.
    A migration could normalize them in bulk.
  • Forwarding to a domain or IP (helpers/get-recipients.js) now passes the Unicode
    local part on instead of the xn-- form.

🤖 Generated with Claude Code

@uniejo

uniejo commented Oct 9, 2026

Copy link
Copy Markdown
Author

Full test suite on fix/unicode-alias-username (2026-10-09)

  • Full AVA suite ran on Node 22 with TZ=UTC and fail-fast off: 3,476 passed, 11 failed, 3h12m
  • Sieve suite: 457/457 passed
  • Branch test test/unicode-alias-forwarding.js: 12/12 passed
  • No failure traced to the branch change
  • 10 of the 11 failures were the test environment; all passed on rerun:
    • web › index homepage snapshots (2) and imap › large mailbox: SMTP_MESSAGE_MAX_SIZE wasn't set the way CI sets it
    • config › api-secrets (2): CI values weren't exported as environment variables
    • alias-password-reset, mx › hardening (2) and mx › partial-delivery (2): timeouts from the laptop being suspended or slow; partial-delivery is flaky locally (2 of 3 reruns passed)
  • 1 real bug, smtp › automatic openpgp support, which also fails on master
    • Cause: helpers/wkd.js breaks DNS lookups on Node 20 and later; CI on Node 18 isn't affected
    • Fixed on branch fix/wkd-lookup-node20 (6fc6b8a) with 4 new tests; not pushed yet
  • Not tested on Node 18, because the native modules are built for Node 22

The recipient local part was converted to punycode before matching, so
an address like "fællestest@example.com" became "xn--fllestest-g3a" and
never matched regex aliases such as /^.*llestest$/ or exact Unicode
alias names, falling back to the catch-all instead.

parseUsername now returns the NFC normalized, lowercase Unicode local
part. Alias matching tries both the Unicode and punycode forms (via the
new get-username-variants helper) so legacy "xn--" aliases keep working.
Alias names and the lookup API username are NFC normalized as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@uniejo
uniejo force-pushed the fix/unicode-alias-username branch from c0908ad to 8d6f0ef Compare October 12, 2026 08:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant