mirror of
https://github.com/mjl-/mox.git
synced 2025-07-13 11:34:37 +03:00
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:
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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{
|
||||
|
Reference in New Issue
Block a user