Skip to content

Commit 5678b03

Browse files
committed
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?= <[email protected]>". 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!
1 parent 0bb4501 commit 5678b03

File tree

6 files changed

+64
-15
lines changed

6 files changed

+64
-15
lines changed

message/addr.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package message
2+
3+
import (
4+
"fmt"
5+
"net/mail"
6+
7+
"github.com/mjl-/mox/smtp"
8+
)
9+
10+
// ParseAddressList parses a string as an address list header value
11+
// (potentially multiple addresses, comma-separated, with optional display
12+
// name).
13+
func ParseAddressList(s string) ([]Address, error) {
14+
parser := mail.AddressParser{WordDecoder: &wordDecoder}
15+
addrs, err := parser.ParseList(s)
16+
if err != nil {
17+
return nil, fmt.Errorf("parsing address list: %v", err)
18+
}
19+
r := make([]Address, len(addrs))
20+
for i, a := range addrs {
21+
addr, err := smtp.ParseNetMailAddress(a.Address)
22+
if err != nil {
23+
return nil, fmt.Errorf("parsing adjusted address %q: %v", a.Address, err)
24+
}
25+
r[i] = Address{a.Name, addr.Localpart.String(), addr.Domain.ASCII}
26+
27+
}
28+
return r, nil
29+
}

message/addr_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package message
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestParseAddressList(t *testing.T) {
8+
l, err := ParseAddressList("=?iso-8859-2?Q?Krist=FDna?= <[email protected]>, [email protected]")
9+
tcheck(t, err, "parsing address list")
10+
tcompare(t, l, []Address{{"Kristýna", "k", "example.com"}, {"", "mjl", "mox.example"}})
11+
}

message/part.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,12 @@ func parseEnvelope(log mlog.Log, h mail.Header) (*Envelope, error) {
489489

490490
func parseAddressList(log mlog.Log, h mail.Header, k string) []Address {
491491
// todo: possibly work around ios mail generating incorrect q-encoded "phrases" with unencoded double quotes? ../rfc/2047:382
492-
l, err := h.AddressList(k)
492+
v := h.Get(k)
493+
if v == "" {
494+
return nil
495+
}
496+
parser := mail.AddressParser{WordDecoder: &wordDecoder}
497+
l, err := parser.ParseList(v)
493498
if err != nil {
494499
return nil
495500
}

message/part_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,3 +603,10 @@ func TestNetMailAddress(t *testing.T) {
603603
tcheck(t, err, "parse")
604604
tcompare(t, p.Envelope.From, []Address{{"", `" "`, "example.com"}})
605605
}
606+
607+
func TestParseQuotedCharset(t *testing.T) {
608+
const s = "From: =?iso-8859-2?Q?Krist=FDna?= <[email protected]>\r\n\r\nbody\r\n"
609+
p, err := EnsurePart(pkglog.Logger, false, strings.NewReader(s), int64(len(s)))
610+
tcheck(t, err, "parse")
611+
tcompare(t, p.Envelope.From, []Address{{"Kristýna", "k", "example.com"}})
612+
}

sendmail.go

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"io"
1010
"log"
1111
"net"
12-
"net/mail"
1312
"os"
1413
"path/filepath"
1514
"slices"
@@ -19,6 +18,7 @@ import (
1918
"github.com/mjl-/sconf"
2019

2120
"github.com/mjl-/mox/dns"
21+
"github.com/mjl-/mox/message"
2222
"github.com/mjl-/mox/mox-"
2323
"github.com/mjl-/mox/sasl"
2424
"github.com/mjl-/mox/smtp"
@@ -196,16 +196,12 @@ binary should be setgid that group:
196196
}
197197
recipient = submitconf.DefaultDestination
198198
} else {
199-
addrs, err := mail.ParseAddressList(s)
199+
addrs, err := message.ParseAddressList(s)
200200
xcheckf(err, "parsing To address list")
201201
if len(addrs) != 1 {
202202
log.Fatalf("only single address allowed in To header")
203203
}
204-
addr, err := smtp.ParseNetMailAddress(addrs[0].Address)
205-
if err != nil {
206-
log.Fatalf("parsing address: %v", err)
207-
}
208-
recipient = addr.Pack(false)
204+
recipient = addrs[0].User + "@" + addrs[0].Host
209205
}
210206
}
211207
if k == "to" {

webmail/api.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -517,13 +517,14 @@ type File struct {
517517
// parseAddress expects either a plain email address like "user@domain", or a
518518
// single address as used in a message header, like "name <user@domain>".
519519
func parseAddress(msghdr string) (message.NameAddress, error) {
520-
a, err := mail.ParseAddress(msghdr)
520+
// todo: parse more fully according to ../rfc/5322:959
521+
parser := mail.AddressParser{WordDecoder: &wordDecoder}
522+
a, err := parser.Parse(msghdr)
521523
if err != nil {
522524
return message.NameAddress{}, err
523525
}
524526

525-
// todo: parse more fully according to ../rfc/5322:959
526-
path, err := smtp.ParseAddress(a.Address)
527+
path, err := smtp.ParseNetMailAddress(a.Address)
527528
if err != nil {
528529
return message.NameAddress{}, err
529530
}
@@ -1658,12 +1659,12 @@ func recipientSecurity(ctx context.Context, log mlog.Log, resolver dns.Resolver,
16581659
SecurityResultUnknown,
16591660
}
16601661

1661-
msgAddr, err := mail.ParseAddress(messageAddressee)
1662+
parser := mail.AddressParser{WordDecoder: &wordDecoder}
1663+
msgAddr, err := parser.Parse(messageAddressee)
16621664
if err != nil {
1663-
return rs, fmt.Errorf("parsing message addressee: %v", err)
1665+
return rs, fmt.Errorf("parsing addressee: %v", err)
16641666
}
1665-
1666-
addr, err := smtp.ParseAddress(msgAddr.Address)
1667+
addr, err := smtp.ParseNetMailAddress(msgAddr.Address)
16671668
if err != nil {
16681669
return rs, fmt.Errorf("parsing address: %v", err)
16691670
}

0 commit comments

Comments
 (0)