normalize localparts with unicode nfc when parsing

both when parsing our configs, and for incoming on smtp or in messages.
so we properly compare things like é and e+accent as equal, and accept the
different encodings of that same address.
This commit is contained in:
Mechiel Lukkien
2024-03-08 21:08:40 +01:00
parent 4fbd7abb57
commit 8e6fe7459b
23 changed files with 134 additions and 59 deletions

View File

@ -388,7 +388,8 @@ func analyze(ctx context.Context, log mlog.Log, resolver dns.Resolver, d deliver
if err != nil {
continue
}
if dom == d.rcptAcc.rcptTo.IPDomain.Domain && smtp.Localpart(a.User) == d.rcptAcc.rcptTo.Localpart {
lp, err := smtp.ParseLocalpart(a.User)
if err == nil && dom == d.rcptAcc.rcptTo.IPDomain.Domain && lp == d.rcptAcc.rcptTo.Localpart {
return true
}
}

View File

@ -8,6 +8,8 @@ import (
"strings"
"time"
"golang.org/x/text/unicode/norm"
"github.com/mjl-/mox/dns"
"github.com/mjl-/mox/mox-"
"github.com/mjl-/mox/smtp"
@ -342,7 +344,7 @@ func (p *parser) xlocalpart() smtp.Localpart {
// ../rfc/5321:3486
p.xerrorf("localpart longer than 64 octets")
}
return smtp.Localpart(s)
return smtp.Localpart(norm.NFC.String(s))
}
// ../rfc/5321:2324

View File

@ -125,7 +125,13 @@ func TestReputation(t *testing.T) {
rcptToDomain, err := dns.ParseDomain(hm.RcptToDomain)
tcheck(t, err, "parse rcptToDomain")
rcptToOrgDomain := publicsuffix.Lookup(ctxbg, log.Logger, rcptToDomain)
r := store.Recipient{MessageID: hm.ID, Localpart: hm.RcptToLocalpart, Domain: hm.RcptToDomain, OrgDomain: rcptToOrgDomain.Name(), Sent: hm.Received}
r := store.Recipient{
MessageID: hm.ID,
Localpart: hm.RcptToLocalpart.String(),
Domain: hm.RcptToDomain,
OrgDomain: rcptToOrgDomain.Name(),
Sent: hm.Received,
}
err = tx.Insert(&r)
tcheck(t, err, "insert recipient")
}

View File

@ -352,25 +352,55 @@ func TestDelivery(t *testing.T) {
// Set up iprev to get delivery from unknown user to be accepted.
resolver.PTR["127.0.0.10"] = []string{"example.org."}
// Only ascii o@ is configured, not the greek and cyrillic lookalikes.
ts.run(func(err error, client *smtpclient.Client) {
mailFrom := "remote@example.org"
rcptTo := "mjl@mox.example"
rcptTo := "ο@mox.example" // omicron \u03bf, looks like the configured o@
msg := strings.ReplaceAll(deliverMessage, "mjl@mox.example", rcptTo)
if err == nil {
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, true, false)
}
tcheck(t, err, "deliver to remote")
var cerr smtpclient.Error
if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
t.Fatalf("deliver to omicron @ instead of ascii o @, got err %v, expected smtpclient.Error with code %d", err, smtp.C550MailboxUnavail)
}
})
changes := make(chan []store.Change)
go func() {
changes <- ts.comm.Get()
}()
ts.run(func(err error, client *smtpclient.Client) {
recipients := []string{
"mjl@mox.example",
"o@mox.example", // ascii o, as configured
"\u2126@mox.example", // ohm sign, as configured
"ω@mox.example", // lower-case omega, we match case-insensitively and this is the lowercase of ohm (!)
"\u03a9@mox.example", // capital omega, also lowercased to omega.
"tést@mox.example", // NFC
"te\u0301st@mox.example", // not NFC, but normalized as tést@, see https://go.dev/blog/normalization
}
timer := time.NewTimer(time.Second)
defer timer.Stop()
select {
case <-changes:
case <-timer.C:
t.Fatalf("no delivery in 1s")
for _, rcptTo := range recipients {
// Ensure SMTP RCPT TO and message address headers are the same, otherwise the junk
// filter treats us more strictly.
msg := strings.ReplaceAll(deliverMessage, "mjl@mox.example", rcptTo)
mailFrom := "remote@example.org"
if err == nil {
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, true, false)
}
tcheck(t, err, "deliver to remote")
changes := make(chan []store.Change)
go func() {
changes <- ts.comm.Get()
}()
timer := time.NewTimer(time.Second)
defer timer.Stop()
select {
case <-changes:
case <-timer.C:
t.Fatalf("no delivery in 1s")
}
}
})
@ -1005,7 +1035,6 @@ func TestTLSReport(t *testing.T) {
},
TXT: map[string][]string{
"testsel._domainkey.example.org.": {dkimTxt},
"example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
"_dmarc.example.org.": {"v=DMARC1;p=reject;rua=mailto:dmarcrpt@example.org"},
},
PTR: map[string][]string{