do earlier smtputf8-check

This commit is contained in:
Laurent Meunier 2024-03-30 17:22:33 +01:00
parent 3484651691
commit 08735690f3
5 changed files with 133 additions and 75 deletions

View file

@ -37,7 +37,7 @@ func ExampleVerify() {
// Message to verify.
msg := strings.NewReader("From: <sender@example.com>\r\nMore: headers\r\n\r\nBody\r\n")
msgFrom, _, _, err := message.From(slog.Default(), true, msg)
msgFrom, _, _, err := message.From(slog.Default(), true, msg, nil)
if err != nil {
log.Fatalf("parsing message for from header: %v", err)
}

View file

@ -2176,7 +2176,7 @@ can be found in message headers.
data, err := io.ReadAll(os.Stdin)
xcheckf(err, "read message")
dmarcFrom, _, _, err := message.From(c.log.Logger, false, bytes.NewReader(data))
dmarcFrom, _, _, err := message.From(c.log.Logger, false, bytes.NewReader(data), nil)
xcheckf(err, "extract dmarc from message")
const ignoreTestMode = false

View file

@ -18,17 +18,22 @@ import (
// From headers may be present. From returns an error if there is not exactly
// one address. This address can be used for evaluating a DMARC policy against
// SPF and DKIM results.
func From(elog *slog.Logger, strict bool, r io.ReaderAt) (raddr smtp.Address, envelope *Envelope, header textproto.MIMEHeader, rerr error) {
func From(elog *slog.Logger, strict bool, r io.ReaderAt, p *Part) (raddr smtp.Address, envelope *Envelope, header textproto.MIMEHeader, rerr error) {
log := mlog.New("message", elog)
// ../rfc/7489:1243
// todo: only allow utf8 if enabled in session/message?
p, err := Parse(log.Logger, strict, r)
if err != nil {
// todo: should we continue with p, perhaps headers can be parsed?
return raddr, nil, nil, fmt.Errorf("parsing message: %v", err)
var err error
if p == nil {
var pp Part
pp, err = Parse(log.Logger, strict, r)
if err != nil {
// todo: should we continue with p, perhaps headers can be parsed?
return raddr, nil, nil, fmt.Errorf("parsing message: %v", err)
}
p = &pp
}
header, err = p.Header()
if err != nil {

View file

@ -333,6 +333,7 @@ type conn struct {
futureReleaseRequest string // For use in DSNs, either "for;" or "until;" plus original value. ../rfc/4865:305
has8bitmime bool // If MAIL FROM parameter BODY=8BITMIME was sent. Required for SMTPUTF8.
smtputf8 bool // todo future: we should keep track of this per recipient. perhaps only a specific recipient requires smtputf8, e.g. due to a utf8 localpart.
msgsmtputf8 bool // Is SMTPUTF8 required for the received message. Default to the same value as `smtputf8`, but is re-evaluated after the whole message (envelope and data) is received.
recipients []rcptAccount
}
@ -373,6 +374,7 @@ func (c *conn) rset() {
c.futureReleaseRequest = ""
c.has8bitmime = false
c.smtputf8 = false
c.msgsmtputf8 = false
c.recipients = nil
}
@ -765,7 +767,7 @@ func command(c *conn) {
c.cmd = cmdl
c.cmdStart = time.Now()
p := newParser(args, c.smtputf8, c)
p := newParser(args, c.msgsmtputf8, c)
fn, ok := commands[cmdl]
if !ok {
c.cmd = "(unknown)"
@ -1420,6 +1422,7 @@ func (c *conn) cmdMail(p *parser) {
case "SMTPUTF8":
// ../rfc/6531:213
c.smtputf8 = true
c.msgsmtputf8 = true
case "REQUIRETLS":
// ../rfc/8689:155
if !c.tls {
@ -1469,7 +1472,7 @@ func (c *conn) cmdMail(p *parser) {
}
// We now know if we have to parse the address with support for utf8.
pp := newParser(rawRevPath, c.smtputf8, c)
pp := newParser(rawRevPath, c.msgsmtputf8, c)
rpath := pp.xbareReversePath()
pp.xempty()
pp = nil
@ -1655,6 +1658,53 @@ func (c *conn) cmdRcpt(p *parser) {
c.bwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "now on the list", nil)
}
// ../rfc/6531:497
func (c *conn) isSmtpUtf8Required(part *message.Part) bool {
hasNonASCII := func(r io.Reader) bool {
br := bufio.NewReader(r)
for {
b, err := br.ReadByte()
if err == io.EOF {
break
}
xcheckf(err, "read header")
if b > unicode.MaxASCII {
return true
}
}
return false
}
var hasNonASCIIPartHeader func(p *message.Part) bool
hasNonASCIIPartHeader = func(p *message.Part) bool {
if hasNonASCII(p.HeaderReader()) {
return true
}
for _, pp := range p.Parts {
if hasNonASCIIPartHeader(&pp) {
return true
}
}
return false
}
// Check "MAIL FROM"
if hasNonASCII(strings.NewReader(string(c.mailFrom.Localpart))) {
return true
}
// Check all "RCPT TO"
for _, rcpt := range c.recipients {
if hasNonASCII(strings.NewReader(string(rcpt.rcptTo.Localpart))) {
return true
}
}
// Check header in all message parts
if hasNonASCIIPartHeader(part) {
return true
}
return false
}
// ../rfc/5321:1992 ../rfc/5321:1098
func (c *conn) cmdData(p *parser) {
c.xneedHello()
@ -1753,6 +1803,26 @@ func (c *conn) cmdData(p *parser) {
}
}
// Now that we have all the whole message (envelope + data), we can check if the SMTPUTF8 extension is required.
// The check happens only when the client required the SMTPUTF8 extension.
var part *message.Part
if c.smtputf8 {
// Try to parse the message.
// Do nothing if something bad happen during Parse and Walk, just keep the current value for c.msgsmtputf8.
p, err := message.Parse(c.log.Logger, true, dataFile)
if err == nil {
// Message parsed without error. Keep the result to avoid parsing the message again.
part = &p
err = part.Walk(c.log.Logger, nil)
if err == nil {
c.msgsmtputf8 = c.isSmtpUtf8Required(part)
}
}
if c.smtputf8 != c.msgsmtputf8 {
c.log.Debug("SMTPUTF8 flag changed", slog.Bool("received SMTPUTF8", c.smtputf8), slog.Bool("evaluated SMTPUTF8", c.msgsmtputf8))
}
}
// Prepare "Received" header.
// ../rfc/5321:2051 ../rfc/5321:3302
// ../rfc/5321:3311 ../rfc/6531:578
@ -1762,14 +1832,14 @@ func (c *conn) cmdData(p *parser) {
if c.submission {
// Hide internal hosts.
// todo future: make this a config option, where admins specify ip ranges that they don't want exposed. also see ../rfc/5321:4321
recvFrom = message.HeaderCommentDomain(mox.Conf.Static.HostnameDomain, c.smtputf8)
recvFrom = message.HeaderCommentDomain(mox.Conf.Static.HostnameDomain, c.msgsmtputf8)
} else {
if len(c.hello.IP) > 0 {
recvFrom = smtp.AddressLiteral(c.hello.IP)
} else {
// ASCII-only version added after the extended-domain syntax below, because the
// comment belongs to "BY" which comes immediately after "FROM".
recvFrom = c.hello.Domain.XName(c.smtputf8)
recvFrom = c.hello.Domain.XName(c.msgsmtputf8)
}
iprevctx, iprevcancel := context.WithTimeout(cmdctx, time.Minute)
var revName string
@ -1788,24 +1858,24 @@ func (c *conn) cmdData(p *parser) {
}
name = strings.TrimSuffix(name, ".")
recvFrom += " ("
if name != "" && name != c.hello.Domain.XName(c.smtputf8) {
if name != "" && name != c.hello.Domain.XName(c.msgsmtputf8) {
recvFrom += name + " "
}
recvFrom += smtp.AddressLiteral(c.remoteIP) + ")"
if c.smtputf8 && c.hello.Domain.Unicode != "" {
if c.msgsmtputf8 && c.hello.Domain.Unicode != "" {
recvFrom += " (" + c.hello.Domain.ASCII + ")"
}
}
recvBy := mox.Conf.Static.HostnameDomain.XName(c.smtputf8)
recvBy := mox.Conf.Static.HostnameDomain.XName(c.msgsmtputf8)
recvBy += " (" + smtp.AddressLiteral(c.localIP) + ")" // todo: hide ip if internal?
if c.smtputf8 && mox.Conf.Static.HostnameDomain.Unicode != "" {
if c.msgsmtputf8 && mox.Conf.Static.HostnameDomain.Unicode != "" {
// This syntax is part of "VIA".
recvBy += " (" + mox.Conf.Static.HostnameDomain.ASCII + ")"
}
// ../rfc/3848:34 ../rfc/6531:791
with := "SMTP"
if c.smtputf8 {
if c.msgsmtputf8 {
with = "UTF8SMTP"
} else if c.ehlo {
with = "ESMTP"
@ -1850,7 +1920,7 @@ func (c *conn) cmdData(p *parser) {
// handle it first, and leave the rest of the function for handling wild west
// internet traffic.
if c.submission {
c.submit(cmdctx, recvHdrFor, msgWriter, dataFile)
c.submit(cmdctx, recvHdrFor, msgWriter, dataFile, part)
} else {
c.deliver(cmdctx, recvHdrFor, msgWriter, iprevStatus, iprevAuthentic, dataFile)
}
@ -1874,7 +1944,7 @@ func hasTLSRequiredNo(h textproto.MIMEHeader) bool {
}
// submit is used for mail from authenticated users that we will try to deliver.
func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, dataFile *os.File) {
func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, dataFile *os.File, part *message.Part) {
// Similar between ../smtpserver/server.go:/submit\( and ../webmail/webmail.go:/MessageSubmit\(
var msgPrefix []byte
@ -1883,7 +1953,7 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
// for other users.
// We don't check the Sender field, there is no expectation of verification, ../rfc/7489:2948
// and with Resent headers it seems valid to have someone else as Sender. ../rfc/5322:1578
msgFrom, _, header, err := message.From(c.log.Logger, true, dataFile)
msgFrom, _, header, err := message.From(c.log.Logger, true, dataFile, part)
if err != nil {
metricSubmission.WithLabelValues("badmessage").Inc()
c.log.Infox("parsing message From address", err, slog.String("user", c.username))
@ -1900,34 +1970,6 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "must match authenticated user")
}
// Check if the message contains non-ascii characters. If no such characters are found,
// the SMTPUTF8 extension is not required.
// ../rfc/6531:497
isASCII := func(s string) bool {
for _, c := range s {
if c > unicode.MaxASCII {
return false
}
}
return true
}
c.smtputf8 = !isASCII(c.mailFrom.Localpart.String())
for _, rcpt := range c.recipients {
if !isASCII(rcpt.rcptTo.Localpart.String()) {
c.smtputf8 = true
break
}
}
for _, values := range header {
for _, value := range values {
if !isASCII(value) {
c.smtputf8 = true
break
}
}
}
// TLS-Required: No header makes us not enforce recipient domain's TLS policy.
// ../rfc/8689:206
// Only when requiretls smtp extension wasn't used. ../rfc/8689:246
@ -1948,7 +1990,7 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
// ../rfc/5321:4131 ../rfc/6409:751
messageID := header.Get("Message-Id")
if messageID == "" {
messageID = mox.MessageIDGen(c.smtputf8)
messageID = mox.MessageIDGen(c.msgsmtputf8)
msgPrefix = append(msgPrefix, fmt.Sprintf("Message-Id: <%s>\r\n", messageID)...)
}
@ -1989,7 +2031,7 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
if len(selectors) > 0 {
if canonical, err := mox.CanonicalLocalpart(msgFrom.Localpart, confDom); err != nil {
c.log.Errorx("determining canonical localpart for dkim signing", err, slog.Any("localpart", msgFrom.Localpart))
} else if dkimHeaders, err := dkim.Sign(ctx, c.log.Logger, canonical, msgFrom.Domain, selectors, c.smtputf8, store.FileMsgReader(msgPrefix, dataFile)); err != nil {
} else if dkimHeaders, err := dkim.Sign(ctx, c.log.Logger, canonical, msgFrom.Domain, selectors, c.msgsmtputf8, store.FileMsgReader(msgPrefix, dataFile)); err != nil {
c.log.Errorx("dkim sign for domain", err, slog.Any("domain", msgFrom.Domain))
metricServerErrors.WithLabelValues("dkimsign").Inc()
} else {
@ -1998,14 +2040,14 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
}
authResults := message.AuthResults{
Hostname: mox.Conf.Static.HostnameDomain.XName(c.smtputf8),
Comment: mox.Conf.Static.HostnameDomain.ASCIIExtra(c.smtputf8),
Hostname: mox.Conf.Static.HostnameDomain.XName(c.msgsmtputf8),
Comment: mox.Conf.Static.HostnameDomain.ASCIIExtra(c.msgsmtputf8),
Methods: []message.AuthMethod{
{
Method: "auth",
Result: "pass",
Props: []message.AuthProp{
message.MakeAuthProp("smtp", "mailfrom", c.mailFrom.XString(c.smtputf8), true, c.mailFrom.ASCIIExtra(c.smtputf8)),
message.MakeAuthProp("smtp", "mailfrom", c.mailFrom.XString(c.msgsmtputf8), true, c.mailFrom.ASCIIExtra(c.msgsmtputf8)),
},
},
},
@ -2039,7 +2081,7 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
}
xmsgPrefix := append([]byte(recvHdrFor(rcptTo)), msgPrefix...)
msgSize := int64(len(xmsgPrefix)) + msgWriter.Size
qm := queue.MakeMsg(*c.mailFrom, rcptAcc.rcptTo, msgWriter.Has8bit, c.smtputf8, msgSize, messageID, xmsgPrefix, c.requireTLS, now)
qm := queue.MakeMsg(*c.mailFrom, rcptAcc.rcptTo, msgWriter.Has8bit, c.msgsmtputf8, msgSize, messageID, xmsgPrefix, c.requireTLS, now)
if !c.futureRelease.IsZero() {
qm.NextAttempt = c.futureRelease
qm.FutureReleaseRequest = c.futureReleaseRequest
@ -2061,6 +2103,7 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
slog.Any("mailfrom", *c.mailFrom),
slog.Any("rcptto", rcptAcc.rcptTo),
slog.Bool("smtputf8", c.smtputf8),
slog.Bool("msgsmtputf8", c.msgsmtputf8),
slog.Int64("msgsize", qml[i].Size))
}
@ -2136,7 +2179,7 @@ func (c *conn) xlocalserveError(lp smtp.Localpart) {
func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, iprevStatus iprev.Status, iprevAuthentic bool, dataFile *os.File) {
// todo: in decision making process, if we run into (some) temporary errors, attempt to continue. if we decide to accept, all good. if we decide to reject, we'll make it a temporary reject.
msgFrom, envelope, headers, err := message.From(c.log.Logger, false, dataFile)
msgFrom, envelope, headers, err := message.From(c.log.Logger, false, dataFile, nil)
if err != nil {
c.log.Infox("parsing message for From address", err)
}
@ -2158,7 +2201,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
// We'll be building up an Authentication-Results header.
authResults := message.AuthResults{
Hostname: mox.Conf.Static.HostnameDomain.XName(c.smtputf8),
Hostname: mox.Conf.Static.HostnameDomain.XName(c.msgsmtputf8),
}
commentAuthentic := func(v bool) string {
@ -2204,7 +2247,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
dkimctx, dkimcancel := context.WithTimeout(ctx, time.Minute)
defer dkimcancel()
// todo future: we could let user configure which dkim headers they require
dkimResults, dkimErr = dkim.Verify(dkimctx, c.log.Logger, c.resolver, c.smtputf8, dkim.DefaultPolicy, dataFile, ignoreTestMode)
dkimResults, dkimErr = dkim.Verify(dkimctx, c.log.Logger, c.resolver, c.msgsmtputf8, dkim.DefaultPolicy, dataFile, ignoreTestMode)
dkimcancel()
}()
@ -2300,8 +2343,8 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
sig := base64.StdEncoding.EncodeToString(r.Sig.Signature)
sig = sig[:12] // Must be at least 8 characters and unique among the signatures.
props = []message.AuthProp{
message.MakeAuthProp("header", "d", r.Sig.Domain.XName(c.smtputf8), true, r.Sig.Domain.ASCIIExtra(c.smtputf8)),
message.MakeAuthProp("header", "s", r.Sig.Selector.XName(c.smtputf8), true, r.Sig.Selector.ASCIIExtra(c.smtputf8)),
message.MakeAuthProp("header", "d", r.Sig.Domain.XName(c.msgsmtputf8), true, r.Sig.Domain.ASCIIExtra(c.msgsmtputf8)),
message.MakeAuthProp("header", "s", r.Sig.Selector.XName(c.msgsmtputf8), true, r.Sig.Selector.ASCIIExtra(c.msgsmtputf8)),
message.MakeAuthProp("header", "a", r.Sig.Algorithm(), false, ""),
message.MakeAuthProp("header", "b", sig, false, ""), // ../rfc/6008:147
}
@ -2347,7 +2390,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
}
var props []message.AuthProp
if spfIdentity != nil {
props = []message.AuthProp{message.MakeAuthProp("smtp", string(receivedSPF.Identity), spfIdentity.XName(c.smtputf8), true, spfIdentity.ASCIIExtra(c.smtputf8))}
props = []message.AuthProp{message.MakeAuthProp("smtp", string(receivedSPF.Identity), spfIdentity.XName(c.msgsmtputf8), true, spfIdentity.ASCIIExtra(c.msgsmtputf8))}
}
var spfComment string
if spfAuthentic {
@ -2436,7 +2479,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
Comment: comment,
Props: []message.AuthProp{
// ../rfc/7489:1489
message.MakeAuthProp("header", "from", msgFrom.Domain.ASCII, true, msgFrom.Domain.ASCIIExtra(c.smtputf8)),
message.MakeAuthProp("header", "from", msgFrom.Domain.ASCII, true, msgFrom.Domain.ASCIIExtra(c.msgsmtputf8)),
},
}
@ -2693,7 +2736,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
// Received-SPF header goes before Received. ../rfc/7208:2038
m.MsgPrefix = []byte(
xmox +
"Delivered-To: " + rcptAcc.rcptTo.XString(c.smtputf8) + "\r\n" + // ../rfc/9228:274
"Delivered-To: " + rcptAcc.rcptTo.XString(c.msgsmtputf8) + "\r\n" + // ../rfc/9228:274
"Return-Path: <" + c.mailFrom.String() + ">\r\n" + // ../rfc/5321:3300
rcptAuthResults.Header() +
receivedSPF.Header() +
@ -2996,7 +3039,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
if len(deliverErrors) > 0 {
now := time.Now()
dsnMsg := dsn.Message{
SMTPUTF8: c.smtputf8,
SMTPUTF8: c.msgsmtputf8,
From: smtp.Path{Localpart: "postmaster", IPDomain: deliverErrors[0].rcptTo.IPDomain},
To: *c.mailFrom,
Subject: "mail delivery failure",

View file

@ -1777,7 +1777,7 @@ func TestSMTPUTF8(t *testing.T) {
ts.pass = password0
ts.submission = true
test := func(mailFrom string, rcptTo string, headerValue string, clientSmtputf8 bool, expectedSmtputf8 bool, expErr *smtpclient.Error) {
test := func(mailFrom string, rcptTo string, headerValue string, filename string, clientSmtputf8 bool, expectedSmtputf8 bool, expErr *smtpclient.Error) {
t.Helper()
ts.run(func(_ error, client *smtpclient.Client) {
@ -1786,9 +1786,18 @@ func TestSMTPUTF8(t *testing.T) {
To: <%s>
Subject: test
X-Custom-Test-Header: %s
MIME-Version: 1.0
Content-type: multipart/mixed; boundary="simple boundary"
test email
`, mailFrom, rcptTo, headerValue), "\n", "\r\n")
--simple boundary
Content-Type: text/plain; charset=UTF-8;
Content-Disposition: attachment; filename="%s"
Content-Transfer-Encoding: base64
QW4gYXR0YWNoZWQgdGV4dCBmaWxlLg==
--simple boundary--
`, mailFrom, rcptTo, headerValue, filename), "\n", "\r\n")
err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, clientSmtputf8, false)
var cerr smtpclient.Error
@ -1802,18 +1811,19 @@ test email
msgs, _ := queue.List(ctxbg, queue.Filter{})
queuedMsg := msgs[len(msgs)-1]
if queuedMsg.SMTPUTF8 != expectedSmtputf8 {
t.Fatalf("[%s / %s / %s] got SMTPUTF8 %t, expected %t", mailFrom, rcptTo, headerValue, queuedMsg.SMTPUTF8, expectedSmtputf8)
t.Fatalf("[%s / %s / %s / %s] got SMTPUTF8 %t, expected %t", mailFrom, rcptTo, headerValue, filename, queuedMsg.SMTPUTF8, expectedSmtputf8)
}
})
}
test(`mjl@mox.example`, `remote@example.org`, "ascii", false, false, nil)
test(`mjl@mox.example`, `remote@example.org`, "ascii", true, false, nil)
test(`mjl@mox.example`, `🙂@example.org`, "ascii", true, true, nil)
test(`mjl@mox.example`, `🙂@example.org`, "ascii", false, true, &smtpclient.Error{Permanent: true, Code: smtp.C553BadMailbox, Secode: smtp.SeMsg6NonASCIIAddrNotPermitted7})
test(`Ω@mox.example`, `remote@example.org`, "ascii", true, true, nil)
test(`Ω@mox.example`, `remote@example.org`, "ascii", false, true, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeMsg6NonASCIIAddrNotPermitted7})
test(`mjl@mox.example`, `remote@example.org`, "non-ascii-😍", false, true, nil)
test(`mjl@mox.example`, `remote@example.org`, "non-ascii-😍", true, true, nil)
test(`Ω@mox.example`, `🙂@example.org`, "non-ascii-😍", true, true, nil)
test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", false, false, nil)
test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", true, false, nil)
test(`mjl@mox.example`, `🙂@example.org`, "header-ascii", "ascii.txt", true, true, nil)
test(`mjl@mox.example`, `🙂@example.org`, "header-ascii", "ascii.txt", false, true, &smtpclient.Error{Permanent: true, Code: smtp.C553BadMailbox, Secode: smtp.SeMsg6NonASCIIAddrNotPermitted7})
test(`Ω@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", true, true, nil)
test(`Ω@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", false, true, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeMsg6NonASCIIAddrNotPermitted7})
test(`mjl@mox.example`, `remote@example.org`, "header-utf8-😍", "ascii.txt", true, true, nil)
test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "utf8-🫠️.txt", true, true, nil)
test(`Ω@mox.example`, `🙂@example.org`, "header-utf8-😍", "utf8-🫠️.txt", true, true, nil)
test(`mjl@mox.example`, `remote@idn-🌏️.org`, "header-ascii", "ascii.txt", true, true, nil)
}