From 5678b03324864f01668f47c6a78e2d56efe456e5 Mon Sep 17 00:00:00 2001 From: Mechiel Lukkien Date: Thu, 22 Aug 2024 17:36:49 +0200 Subject: [PATCH] recognize more charsets than utf-8/iso-8859-1/us-ascii when parsing message headers with address as they occur in From/To headers, for example: "From: =?iso-8859-2?Q?Krist=FDna?= ". we are using net/mail to parse such headers. most address-parsing functions in that package will only decode charsets utf-8, iso-8859-1 and us-ascii. we have to be careful to always use net/mail.AddressParser with a WordDecoder that understands more that the basics. for issue #204 by morki, thanks for reporting! --- message/addr.go | 29 +++++++++++++++++++++++++++++ message/addr_test.go | 11 +++++++++++ message/part.go | 7 ++++++- message/part_test.go | 7 +++++++ sendmail.go | 10 +++------- webmail/api.go | 15 ++++++++------- 6 files changed, 64 insertions(+), 15 deletions(-) create mode 100644 message/addr.go create mode 100644 message/addr_test.go diff --git a/message/addr.go b/message/addr.go new file mode 100644 index 0000000000..e551ae280a --- /dev/null +++ b/message/addr.go @@ -0,0 +1,29 @@ +package message + +import ( + "fmt" + "net/mail" + + "github.com/mjl-/mox/smtp" +) + +// ParseAddressList parses a string as an address list header value +// (potentially multiple addresses, comma-separated, with optional display +// name). +func ParseAddressList(s string) ([]Address, error) { + parser := mail.AddressParser{WordDecoder: &wordDecoder} + addrs, err := parser.ParseList(s) + if err != nil { + return nil, fmt.Errorf("parsing address list: %v", err) + } + r := make([]Address, len(addrs)) + for i, a := range addrs { + addr, err := smtp.ParseNetMailAddress(a.Address) + if err != nil { + return nil, fmt.Errorf("parsing adjusted address %q: %v", a.Address, err) + } + r[i] = Address{a.Name, addr.Localpart.String(), addr.Domain.ASCII} + + } + return r, nil +} diff --git a/message/addr_test.go b/message/addr_test.go new file mode 100644 index 0000000000..1d00bbbdec --- /dev/null +++ b/message/addr_test.go @@ -0,0 +1,11 @@ +package message + +import ( + "testing" +) + +func TestParseAddressList(t *testing.T) { + l, err := ParseAddressList("=?iso-8859-2?Q?Krist=FDna?= , mjl@mox.example") + tcheck(t, err, "parsing address list") + tcompare(t, l, []Address{{"Kristýna", "k", "example.com"}, {"", "mjl", "mox.example"}}) +} diff --git a/message/part.go b/message/part.go index 00d2a915af..21b3f474f0 100644 --- a/message/part.go +++ b/message/part.go @@ -489,7 +489,12 @@ func parseEnvelope(log mlog.Log, h mail.Header) (*Envelope, error) { func parseAddressList(log mlog.Log, h mail.Header, k string) []Address { // todo: possibly work around ios mail generating incorrect q-encoded "phrases" with unencoded double quotes? ../rfc/2047:382 - l, err := h.AddressList(k) + v := h.Get(k) + if v == "" { + return nil + } + parser := mail.AddressParser{WordDecoder: &wordDecoder} + l, err := parser.ParseList(v) if err != nil { return nil } diff --git a/message/part_test.go b/message/part_test.go index 4a42e3d6da..e8995ee395 100644 --- a/message/part_test.go +++ b/message/part_test.go @@ -603,3 +603,10 @@ func TestNetMailAddress(t *testing.T) { tcheck(t, err, "parse") tcompare(t, p.Envelope.From, []Address{{"", `" "`, "example.com"}}) } + +func TestParseQuotedCharset(t *testing.T) { + const s = "From: =?iso-8859-2?Q?Krist=FDna?= \r\n\r\nbody\r\n" + p, err := EnsurePart(pkglog.Logger, false, strings.NewReader(s), int64(len(s))) + tcheck(t, err, "parse") + tcompare(t, p.Envelope.From, []Address{{"Kristýna", "k", "example.com"}}) +} diff --git a/sendmail.go b/sendmail.go index 06e8a08c1a..041e71ae0c 100644 --- a/sendmail.go +++ b/sendmail.go @@ -9,7 +9,6 @@ import ( "io" "log" "net" - "net/mail" "os" "path/filepath" "slices" @@ -19,6 +18,7 @@ import ( "github.com/mjl-/sconf" "github.com/mjl-/mox/dns" + "github.com/mjl-/mox/message" "github.com/mjl-/mox/mox-" "github.com/mjl-/mox/sasl" "github.com/mjl-/mox/smtp" @@ -196,16 +196,12 @@ binary should be setgid that group: } recipient = submitconf.DefaultDestination } else { - addrs, err := mail.ParseAddressList(s) + addrs, err := message.ParseAddressList(s) xcheckf(err, "parsing To address list") if len(addrs) != 1 { log.Fatalf("only single address allowed in To header") } - addr, err := smtp.ParseNetMailAddress(addrs[0].Address) - if err != nil { - log.Fatalf("parsing address: %v", err) - } - recipient = addr.Pack(false) + recipient = addrs[0].User + "@" + addrs[0].Host } } if k == "to" { diff --git a/webmail/api.go b/webmail/api.go index 87051098a3..c23a83b2d5 100644 --- a/webmail/api.go +++ b/webmail/api.go @@ -517,13 +517,14 @@ type File struct { // parseAddress expects either a plain email address like "user@domain", or a // single address as used in a message header, like "name ". func parseAddress(msghdr string) (message.NameAddress, error) { - a, err := mail.ParseAddress(msghdr) + // todo: parse more fully according to ../rfc/5322:959 + parser := mail.AddressParser{WordDecoder: &wordDecoder} + a, err := parser.Parse(msghdr) if err != nil { return message.NameAddress{}, err } - // todo: parse more fully according to ../rfc/5322:959 - path, err := smtp.ParseAddress(a.Address) + path, err := smtp.ParseNetMailAddress(a.Address) if err != nil { return message.NameAddress{}, err } @@ -1658,12 +1659,12 @@ func recipientSecurity(ctx context.Context, log mlog.Log, resolver dns.Resolver, SecurityResultUnknown, } - msgAddr, err := mail.ParseAddress(messageAddressee) + parser := mail.AddressParser{WordDecoder: &wordDecoder} + msgAddr, err := parser.Parse(messageAddressee) if err != nil { - return rs, fmt.Errorf("parsing message addressee: %v", err) + return rs, fmt.Errorf("parsing addressee: %v", err) } - - addr, err := smtp.ParseAddress(msgAddr.Address) + addr, err := smtp.ParseNetMailAddress(msgAddr.Address) if err != nil { return rs, fmt.Errorf("parsing address: %v", err) }