3// todo: test delivery with failing spf/dkim/dmarc
4// todo: test delivering a message to multiple recipients, and with some of them failing.
10 cryptorand "crypto/rand"
18 "mime/quotedprintable"
27 "github.com/mjl-/bstore"
29 "github.com/mjl-/mox/config"
30 "github.com/mjl-/mox/dkim"
31 "github.com/mjl-/mox/dmarcdb"
32 "github.com/mjl-/mox/dns"
33 "github.com/mjl-/mox/mlog"
34 "github.com/mjl-/mox/mox-"
35 "github.com/mjl-/mox/queue"
36 "github.com/mjl-/mox/sasl"
37 "github.com/mjl-/mox/smtp"
38 "github.com/mjl-/mox/smtpclient"
39 "github.com/mjl-/mox/store"
40 "github.com/mjl-/mox/subjectpass"
41 "github.com/mjl-/mox/tlsrptdb"
42 "github.com/mjl-/mox/webops"
45var ctxbg = context.Background()
48 // Don't make tests slow.
51 unknownRecipientsDelay = 0
54func tcheck(t *testing.T, err error, msg string) {
57 t.Fatalf("%s: %s", msg, err)
61var submitMessage = strings.ReplaceAll(`From: <mjl@mox.example>
62To: <remote@example.org>
64Message-Id: <test@mox.example>
69var deliverMessage = strings.ReplaceAll(`From: <remote@example.org>
72Message-Id: <test@example.org>
77var deliverMessage2 = strings.ReplaceAll(`From: <remote@example.org>
80Message-Id: <test2@example.org>
85type testserver struct {
92 auth func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error)
95 serverConfig *tls.Config
96 clientConfig *tls.Config
97 clientCert *tls.Certificate // Passed to smtpclient for starttls authentication.
101 tlsmode smtpclient.TLSMode
106const password0 = "te\u0301st \u00a0\u2002\u200a" // NFD and various unicode spaces.
107const password1 = "tést " // PRECIS normalized, with NFC.
109func newTestServer(t *testing.T, configPath string, resolver dns.Resolver) *testserver {
110 limitersInit() // Reset rate limiters.
112 log := mlog.New("smtpserver", nil)
114 checkf := func(ctx context.Context, err error, format string, args ...any) {
115 tcheck(t, err, fmt.Sprintf(format, args...))
118 DBWrite: func(ctx context.Context, acc *store.Account, fn func(tx *bstore.Tx)) {
119 err := acc.DB.Write(ctx, func(tx *bstore.Tx) error {
123 tcheck(t, err, "db write")
133 tlsmode: smtpclient.TLSOpportunistic,
134 serverConfig: &tls.Config{
135 Certificates: []tls.Certificate{fakeCert(t, false)},
140 // Ensure session keys, for tests that check resume and authentication.
141 ctx, cancel := context.WithCancel(ctxbg)
143 mox.StartTLSSessionTicketKeyRefresher(ctx, log, ts.serverConfig)
146 mox.ConfigStaticPath = configPath
147 mox.MustLoadConfig(true, false)
148 dataDir := mox.ConfigDirPath(mox.Conf.Static.DataDir)
149 os.RemoveAll(dataDir)
151 err := dmarcdb.Init()
152 tcheck(t, err, "dmarcdb init")
153 err = tlsrptdb.Init()
154 tcheck(t, err, "tlsrptdb init")
155 err = store.Init(ctxbg)
156 tcheck(t, err, "store init")
158 ts.switchStop = store.Switchboard()
160 tcheck(t, err, "queue init")
162 ts.acc, err = store.OpenAccount(log, "mjl", false)
163 tcheck(t, err, "open account")
164 err = ts.acc.SetPassword(log, password0)
165 tcheck(t, err, "set password")
167 ts.comm = store.RegisterComm(ts.acc)
172func (ts *testserver) close() {
176 err := dmarcdb.Close()
177 tcheck(ts.t, err, "dmarcdb close")
178 err = tlsrptdb.Close()
179 tcheck(ts.t, err, "tlsrptdb close")
183 tcheck(ts.t, err, "closing account")
188 tcheck(ts.t, err, "store close")
191func (ts *testserver) checkCount(mailboxName string, expect int) {
194 q := bstore.QueryDB[store.Mailbox](ctxbg, ts.acc.DB)
195 q.FilterNonzero(store.Mailbox{Name: mailboxName})
196 q.FilterEqual("Expunged", false)
198 tcheck(t, err, "get mailbox")
199 qm := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB)
200 qm.FilterNonzero(store.Message{MailboxID: mb.ID})
201 qm.FilterEqual("Expunged", false)
203 tcheck(t, err, "count messages in mailbox")
205 t.Fatalf("messages in mailbox, found %d, expected %d", n, expect)
209func (ts *testserver) run(fn func(client *smtpclient.Client)) {
211 ts.runx(func(helloErr error, client *smtpclient.Client) {
213 tcheck(ts.t, helloErr, "hello")
218func (ts *testserver) runx(fn func(helloErr error, client *smtpclient.Client)) {
220 ts.runRaw(func(conn net.Conn) {
224 if auth == nil && ts.user != "" {
225 auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
226 return sasl.NewClientPlain(ts.user, ts.pass), nil
230 ourHostname := mox.Conf.Static.HostnameDomain
231 remoteHostname := dns.Domain{ASCII: "mox.example"}
232 opts := smtpclient.Opts{
234 RootCAs: mox.Conf.Static.TLS.CertPool,
235 ClientCert: ts.clientCert,
237 log := pkglog.WithCid(ts.cid - 1)
238 client, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, ts.tlspkix, ourHostname, remoteHostname, opts)
248func (ts *testserver) runRaw(fn func(clientConn net.Conn)) {
253 serverConn, clientConn := net.Pipe()
254 defer serverConn.Close()
255 // clientConn is closed as part of closing client.
256 serverdone := make(chan struct{})
257 defer func() { <-serverdone }()
260 serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, ts.serverConfig, serverConn, ts.resolver, ts.submission, ts.immediateTLS, false, 100<<20, false, false, ts.requiretls, ts.dnsbls, 0)
265 clientConn = tls.Client(clientConn, ts.clientConfig)
271func (ts *testserver) smtpErr(err error, expErr *smtpclient.Error) *smtpclient.Error {
274 var cerr smtpclient.Error
275 if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Permanent != expErr.Permanent || cerr.Code != expErr.Code || cerr.Secode != expErr.Secode) {
276 t.Fatalf("got err:\n%#v (%q)\nexpected:\n%#v", err, err, expErr)
281// Just a cert that appears valid. SMTP client will not verify anything about it
282// (that is opportunistic TLS for you, "better some than none"). Let's enjoy this
283// one moment where it makes life easier.
284func fakeCert(t *testing.T, randomkey bool) tls.Certificate {
285 seed := make([]byte, ed25519.SeedSize)
287 cryptorand.Read(seed)
289 privKey := ed25519.NewKeyFromSeed(seed) // Fake key, don't use this for real!
290 template := &x509.Certificate{
291 SerialNumber: big.NewInt(1), // Required field...
292 // Valid period is needed to get session resumption enabled.
293 NotBefore: time.Now().Add(-time.Minute),
294 NotAfter: time.Now().Add(time.Hour),
296 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
298 t.Fatalf("making certificate: %s", err)
300 cert, err := x509.ParseCertificate(localCertBuf)
302 t.Fatalf("parsing generated certificate: %s", err)
304 c := tls.Certificate{
305 Certificate: [][]byte{localCertBuf},
312// check expected dmarc evaluations for outgoing aggregate reports.
313func checkEvaluationCount(t *testing.T, n int) []dmarcdb.Evaluation {
315 l, err := dmarcdb.Evaluations(ctxbg)
316 tcheck(t, err, "get dmarc evaluations")
317 tcompare(t, len(l), n)
321// Test submission from authenticated user.
322func TestSubmission(t *testing.T) {
323 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
326 // Set DKIM signing config.
327 dom, _ := mox.Conf.Domain(dns.Domain{ASCII: "mox.example"})
328 sel := config.Selector{
329 HashEffective: "sha256",
330 HeadersEffective: []string{"From", "To", "Subject"},
331 Key: ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)), // Fake key, don't use for real.
332 Domain: dns.Domain{ASCII: "mox.example"},
334 dom.DKIM = config.DKIM{
335 Selectors: map[string]config.Selector{"testsel": sel},
336 Sign: []string{"testsel"},
338 mox.Conf.Dynamic.Domains["mox.example"] = dom
340 testAuth := func(authfn func(user, pass string, cs *tls.ConnectionState) sasl.Client, user, pass string, expErr *smtpclient.Error) {
343 ts.auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
344 return authfn(user, pass, cs), nil
349 ts.runx(func(err error, client *smtpclient.Client) {
350 mailFrom := "mjl@mox.example"
351 rcptTo := "remote@example.org"
353 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
355 var cerr smtpclient.Error
356 if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Code != expErr.Code || cerr.Secode != expErr.Secode) {
357 t.Fatalf("got err:\n%#v (%q)\nexpected:\n%#v", err, err, expErr)
359 checkEvaluationCount(t, 0)
363 acc, err := store.OpenAccount(pkglog, "disabled", false)
364 tcheck(t, err, "open account")
365 err = acc.SetPassword(pkglog, "test1234")
366 tcheck(t, err, "set password")
368 tcheck(t, err, "close account")
371 testAuth(nil, "", "", &smtpclient.Error{Permanent: true, Code: smtp.C530SecurityRequired, Secode: smtp.SePol7Other0})
372 authfns := []func(user, pass string, cs *tls.ConnectionState) sasl.Client{
373 func(user, pass string, cs *tls.ConnectionState) sasl.Client { return sasl.NewClientPlain(user, pass) },
374 func(user, pass string, cs *tls.ConnectionState) sasl.Client { return sasl.NewClientLogin(user, pass) },
375 func(user, pass string, cs *tls.ConnectionState) sasl.Client { return sasl.NewClientCRAMMD5(user, pass) },
376 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
377 return sasl.NewClientSCRAMSHA1(user, pass, false)
379 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
380 return sasl.NewClientSCRAMSHA256(user, pass, false)
382 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
383 return sasl.NewClientSCRAMSHA1PLUS(user, pass, *cs)
385 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
386 return sasl.NewClientSCRAMSHA256PLUS(user, pass, *cs)
389 for _, fn := range authfns {
390 testAuth(fn, "mjl@mox.example", "test", &smtpclient.Error{Code: smtp.C535AuthBadCreds, Secode: smtp.SePol7AuthBadCreds8}) // Bad (short) password.
391 testAuth(fn, "mjl@mox.example", password0+"test", &smtpclient.Error{Code: smtp.C535AuthBadCreds, Secode: smtp.SePol7AuthBadCreds8}) // Bad password.
392 testAuth(fn, "mjl@mox.example", password0, nil)
393 testAuth(fn, "mjl@mox.example", password1, nil)
394 testAuth(fn, "móx@mox.example", password0, nil)
395 testAuth(fn, "móx@mox.example", password1, nil)
396 testAuth(fn, "mo\u0301x@mox.example", password0, nil)
397 testAuth(fn, "mo\u0301x@mox.example", password1, nil)
398 testAuth(fn, "disabled@mox.example", "test1234", &smtpclient.Error{Code: smtp.C525AccountDisabled, Secode: smtp.SePol7AccountDisabled13})
399 testAuth(fn, "disabled@mox.example", "bogus", &smtpclient.Error{Code: smtp.C535AuthBadCreds, Secode: smtp.SePol7AuthBadCreds8})
402 // Create a certificate, register its public key with account, and make a tls
403 // client config that sends the certificate.
404 clientCert0 := fakeCert(ts.t, true)
405 tlspubkey, err := store.ParseTLSPublicKeyCert(clientCert0.Certificate[0])
406 tcheck(t, err, "parse certificate")
407 tlspubkey.Account = "mjl"
408 tlspubkey.LoginAddress = "mjl@mox.example"
409 err = store.TLSPublicKeyAdd(ctxbg, &tlspubkey)
410 tcheck(t, err, "add tls public key to account")
411 ts.immediateTLS = true
412 ts.clientConfig = &tls.Config{
413 InsecureSkipVerify: true,
414 Certificates: []tls.Certificate{
419 // No explicit address in EXTERNAL.
420 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
421 return sasl.NewClientExternal(user)
424 // Same username in EXTERNAL as configured for key.
425 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
426 return sasl.NewClientExternal(user)
427 }, "mjl@mox.example", "", nil)
429 // Different username in EXTERNAL as configured for key, but same account.
430 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
431 return sasl.NewClientExternal(user)
432 }, "móx@mox.example", "", nil)
434 // Different username as configured for key, but same account, but not EXTERNAL auth.
435 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
436 return sasl.NewClientSCRAMSHA256PLUS(user, pass, *cs)
437 }, "móx@mox.example", password0, nil)
439 // Different account results in error.
440 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
441 return sasl.NewClientExternal(user)
442 }, "☺@mox.example", "", &smtpclient.Error{Code: smtp.C535AuthBadCreds, Secode: smtp.SePol7AuthBadCreds8})
444 // Starttls with client cert should authenticate too.
445 ts.immediateTLS = false
446 ts.clientCert = &clientCert0
447 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
448 return sasl.NewClientExternal(user)
450 ts.immediateTLS = true
453 // Add a client session cache, so our connections will be resumed. We are testing
454 // that the credentials are applied to resumed connections too.
455 ts.clientConfig.ClientSessionCache = tls.NewLRUClientSessionCache(10)
456 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
458 panic("tls connection was resumed")
460 return sasl.NewClientExternal(user)
462 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
464 panic("tls connection was not resumed")
466 return sasl.NewClientExternal(user)
469 // Unknown client certificate should fail the connection.
470 serverConn, clientConn := net.Pipe()
471 serverdone := make(chan struct{})
472 defer func() { <-serverdone }()
475 defer serverConn.Close()
476 tlsConfig := &tls.Config{
477 Certificates: []tls.Certificate{fakeCert(ts.t, false)},
479 serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, tlsConfig, serverConn, ts.resolver, ts.submission, ts.immediateTLS, false, 100<<20, false, false, false, ts.dnsbls, 0)
483 defer clientConn.Close()
485 // Authentication with an unknown/untrusted certificate should fail.
486 clientCert1 := fakeCert(ts.t, true)
487 ts.clientConfig.ClientSessionCache = nil
488 ts.clientConfig.Certificates = []tls.Certificate{
491 clientConn = tls.Client(clientConn, ts.clientConfig)
492 // note: It's not enough to do a handshake and check if that was successful. If the
493 // client cert is not acceptable, we only learn after the handshake, when the first
494 // data messages are exchanged.
495 buf := make([]byte, 100)
496 _, err = clientConn.Read(buf)
498 t.Fatalf("tls handshake with unknown client certificate succeeded")
500 if alert, ok := mox.AsTLSAlert(err); !ok || alert != 42 {
501 t.Fatalf("got err %#v, expected tls 'bad certificate' alert", err)
505func TestDomainDisabled(t *testing.T) {
506 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
510 ts.user = "mjl@mox.example"
513 // Submission with SMTP MAIL FROM of disabled domain must fail.
514 ts.run(func(client *smtpclient.Client) {
515 mailFrom := "mjl@disabled.example" // Disabled.
516 rcptTo := "remote@example.org"
517 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
518 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
519 checkEvaluationCount(t, 0)
522 // Message From-address has disabled domain, must fail.
523 var submitMessage2 = strings.ReplaceAll(`From: <mjl@disabled.example>
524To: <remote@example.org>
526Message-Id: <test@mox.example>
530 ts.run(func(client *smtpclient.Client) {
531 mailFrom := "mjl@mox.example"
532 rcptTo := "remote@example.org"
533 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage2)), strings.NewReader(submitMessage2), false, false, false)
534 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
535 checkEvaluationCount(t, 0)
539// Test delivery from external MTA.
540func TestDelivery(t *testing.T) {
541 resolver := dns.MockResolver{
542 A: map[string][]string{
543 "example.org.": {"127.0.0.10"}, // For mx check.
545 PTR: map[string][]string{},
547 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
550 ts.run(func(client *smtpclient.Client) {
551 mailFrom := "remote@example.org"
552 rcptTo := "mjl@[127.0.0.10]"
553 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
554 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
557 ts.run(func(client *smtpclient.Client) {
558 mailFrom := "remote@example.org"
559 rcptTo := "mjl@[IPv6:::1]"
560 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
561 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
564 ts.run(func(client *smtpclient.Client) {
565 mailFrom := "remote@example.org"
566 rcptTo := "mjl@test.example" // Not configured as destination.
567 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
568 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
571 ts.run(func(client *smtpclient.Client) {
572 mailFrom := "remote@example.org"
573 rcptTo := "unknown@mox.example" // User unknown.
574 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
575 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
578 ts.run(func(client *smtpclient.Client) {
579 mailFrom := "remote@example.org"
580 rcptTo := "mjl@mox.example"
581 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
582 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
585 // Set up iprev to get delivery from unknown user to be accepted.
586 resolver.PTR["127.0.0.10"] = []string{"example.org."}
588 // Only ascii o@ is configured, not the greek and cyrillic lookalikes.
589 ts.run(func(client *smtpclient.Client) {
590 mailFrom := "remote@example.org"
591 rcptTo := "ο@mox.example" // omicron \u03bf, looks like the configured o@
592 msg := strings.ReplaceAll(deliverMessage, "mjl@mox.example", rcptTo)
593 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, true, false)
594 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
597 // Deliveries to disabled domain are rejected with temporary error.
598 ts.run(func(client *smtpclient.Client) {
599 mailFrom := "remote@example.org"
600 rcptTo := "mjl@disabled.example"
601 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
602 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C450MailboxUnavail, Secode: smtp.SeMailbox2Disabled1})
605 ts.run(func(client *smtpclient.Client) {
606 recipients := []string{
608 "o@mox.example", // ascii o, as configured
609 "\u2126@mox.example", // ohm sign, as configured
610 "ω@mox.example", // lower-case omega, we match case-insensitively and this is the lowercase of ohm (!)
611 "\u03a9@mox.example", // capital omega, also lowercased to omega.
612 "móx@mox.example", // NFC
613 "mo\u0301x@mox.example", // not NFC, but normalized as móx@, see https://go.dev/blog/normalization
616 for _, rcptTo := range recipients {
617 // Ensure SMTP RCPT TO and message address headers are the same, otherwise the junk
618 // filter treats us more strictly.
619 msg := strings.ReplaceAll(deliverMessage, "mjl@mox.example", rcptTo)
621 mailFrom := "remote@example.org"
622 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, true, false)
623 tcheck(t, err, "deliver to remote")
625 changes := make(chan []store.Change)
627 changes <- ts.comm.Get()
630 timer := time.NewTimer(time.Second)
635 t.Fatalf("no delivery in 1s")
640 checkEvaluationCount(t, 0)
643func tinsertmsg(t *testing.T, acc *store.Account, mailbox string, m *store.Message, msg string) {
644 mf, err := store.CreateMessageTemp(pkglog, "insertmsg")
645 tcheck(t, err, "temp message")
646 defer os.Remove(mf.Name())
648 _, err = mf.Write([]byte(msg))
649 tcheck(t, err, "write message")
651 acc.WithWLock(func() {
652 err = acc.DeliverMailbox(pkglog, mailbox, m, mf)
653 tcheck(t, err, "deliver message")
657func tretrain(t *testing.T, acc *store.Account) {
660 // Fresh empty junkfilter.
661 basePath := mox.DataDirPath("accounts")
662 dbPath := filepath.Join(basePath, acc.Name, "junkfilter.db")
663 bloomPath := filepath.Join(basePath, acc.Name, "junkfilter.bloom")
666 jf, _, err := acc.OpenJunkFilter(ctxbg, pkglog)
667 tcheck(t, err, "open junk filter")
670 // Fetch messags to retrain on.
671 q := bstore.QueryDB[store.Message](ctxbg, acc.DB)
672 q.FilterEqual("Expunged", false)
673 q.FilterFn(func(m store.Message) bool {
674 return m.Flags.Junk != m.Flags.Notjunk
676 msgs, err := q.List()
677 tcheck(t, err, "fetch messages")
679 // Retrain the messages.
680 for _, m := range msgs {
681 ham := m.Flags.Notjunk
683 f, err := os.Open(acc.MessagePath(m.ID))
684 tcheck(t, err, "open message")
685 r := store.FileMsgReader(m.MsgPrefix, f)
687 jf.TrainMessage(ctxbg, r, m.Size, ham)
690 tcheck(t, err, "close message")
694 tcheck(t, err, "save junkfilter")
697// Test accept/reject with DMARC reputation and with spammy content.
698func TestSpam(t *testing.T) {
699 resolver := &dns.MockResolver{
700 A: map[string][]string{
701 "example.org.": {"127.0.0.1"}, // For mx check.
703 TXT: map[string][]string{
704 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
705 "_dmarc.example.org.": {"v=DMARC1;p=reject; rua=mailto:dmarcrpt@example.org"},
708 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
711 // Insert spammy messages. No junkfilter training yet.
713 RemoteIP: "127.0.0.10",
714 RemoteIPMasked1: "127.0.0.10",
715 RemoteIPMasked2: "127.0.0.0",
716 RemoteIPMasked3: "127.0.0.0",
717 MailFrom: "remote@example.org",
718 MailFromLocalpart: smtp.Localpart("remote"),
719 MailFromDomain: "example.org",
720 RcptToLocalpart: smtp.Localpart("mjl"),
721 RcptToDomain: "mox.example",
722 MsgFromLocalpart: smtp.Localpart("remote"),
723 MsgFromDomain: "example.org",
724 MsgFromOrgDomain: "example.org",
725 MsgFromValidated: true,
726 MsgFromValidation: store.ValidationStrict,
727 Flags: store.Flags{Seen: true, Junk: true},
728 Size: int64(len(deliverMessage)),
732 tinsertmsg(t, ts.acc, "Inbox", &nm, deliverMessage)
734 tinsertmsg(t, ts.acc, "mjl2", &nm, deliverMessage)
737 // Delivery from sender with bad reputation should fail.
738 ts.run(func(client *smtpclient.Client) {
739 mailFrom := "remote@example.org"
740 rcptTo := "mjl@mox.example"
741 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
742 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
744 ts.checkCount("Rejects", 1)
745 checkEvaluationCount(t, 0) // No positive interactions yet.
748 // Delivery from sender with bad reputation matching AcceptRejectsToMailbox should
749 // result in accepted delivery to the mailbox.
750 ts.run(func(client *smtpclient.Client) {
751 mailFrom := "remote@example.org"
752 rcptTo := "mjl2@mox.example"
753 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage2)), strings.NewReader(deliverMessage2), false, false, false)
754 tcheck(t, err, "deliver")
756 ts.checkCount("mjl2junk", 1) // In ruleset rejects mailbox.
757 ts.checkCount("Rejects", 1) // Same as before.
758 checkEvaluationCount(t, 0) // This is not an actual accept.
761 // Mark the messages as having good reputation.
763 err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).FilterEqual("Expunged", false).ForEach(func(m store.Message) error {
764 ids = append(ids, m.ID)
767 tcheck(t, err, "get message ids")
768 ts.xops.MessageFlagsClear(ctxbg, pkglog, ts.acc, ids, []string{"$Junk"})
769 ts.xops.MessageFlagsAdd(ctxbg, pkglog, ts.acc, ids, []string{"$NotJunk"})
771 // Message should now be accepted.
772 ts.run(func(client *smtpclient.Client) {
773 tcheck(t, err, "hello")
774 mailFrom := "remote@example.org"
775 rcptTo := "mjl@mox.example"
776 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
777 tcheck(t, err, "deliver")
779 // Message should now be removed from Rejects mailboxes.
780 ts.checkCount("Rejects", 0)
781 ts.checkCount("mjl2junk", 1)
782 checkEvaluationCount(t, 1)
785 // Undo dmarc pass, mark messages as junk, and train the filter.
787 q := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB)
788 q.FilterEqual("Expunged", false)
789 _, err = q.UpdateFields(map[string]any{"Junk": true, "Notjunk": false})
790 tcheck(t, err, "update junkiness")
793 // Message should be refused for spammy content.
794 ts.run(func(client *smtpclient.Client) {
795 tcheck(t, err, "hello")
796 mailFrom := "remote@example.org"
797 rcptTo := "mjl@mox.example"
798 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
799 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
800 checkEvaluationCount(t, 1) // No new evaluation, this isn't a DMARC reject.
804// Test accept/reject with forwarded messages, DMARC ignored, no IP/EHLO/MAIL
805// FROM-based reputation.
806func TestForward(t *testing.T) {
807 // Do a run without forwarding, and with.
808 check := func(forward bool) {
810 resolver := &dns.MockResolver{
811 A: map[string][]string{
812 "bad.example.": {"127.0.0.1"}, // For mx check.
813 "good.example.": {"127.0.0.1"}, // For mx check.
814 "forward.example.": {"127.0.0.10"}, // For mx check.
816 TXT: map[string][]string{
817 "bad.example.": {"v=spf1 ip4:127.0.0.1 -all"},
818 "good.example.": {"v=spf1 ip4:127.0.0.1 -all"},
819 "forward.example.": {"v=spf1 ip4:127.0.0.10 -all"},
820 "_dmarc.bad.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@bad.example"},
821 "_dmarc.good.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@good.example"},
822 "_dmarc.forward.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@forward.example"},
824 PTR: map[string][]string{
825 "127.0.0.10": {"forward.example."}, // For iprev check.
828 rcptTo := "mjl3@mox.example"
830 // For SPF and DMARC pass, otherwise the test ends quickly.
831 resolver.TXT["bad.example."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
832 resolver.TXT["good.example."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
833 rcptTo = "mjl@mox.example" // Without IsForward rule.
836 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
839 totalEvaluations := 0
841 var msgBad = strings.ReplaceAll(`From: <remote@bad.example>
844Message-Id: <bad@example.org>
848 var msgOK = strings.ReplaceAll(`From: <remote@good.example>
851Message-Id: <good@example.org>
855 var msgOK2 = strings.ReplaceAll(`From: <other@forward.example>
858Message-Id: <regular@example.org>
860happens to come from forwarding mail server.
863 // Deliver forwarded messages, then classify as junk. Normally enough to treat
864 // other unrelated messages from IP as junk, but not for forwarded messages.
865 ts.run(func(client *smtpclient.Client) {
866 mailFrom := "remote@forward.example"
868 mailFrom = "remote@bad.example"
872 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgBad)), strings.NewReader(msgBad), false, false, false)
873 tcheck(t, err, "deliver message")
875 totalEvaluations += 10
877 n, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).UpdateFields(map[string]any{"Junk": true, "MsgFromValidated": true})
878 tcheck(t, err, "marking messages as junk")
882 // Next delivery will fail, with negative "message From" signal.
883 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgBad)), strings.NewReader(msgBad), false, false, false)
884 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
886 checkEvaluationCount(t, totalEvaluations)
889 // Delivery from different "message From" without reputation, but from same
890 // forwarding email server, should succeed under forwarding, not as regular sending
892 ts.run(func(client *smtpclient.Client) {
893 mailFrom := "remote@forward.example"
895 mailFrom = "remote@good.example"
898 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgOK)), strings.NewReader(msgOK), false, false, false)
900 tcheck(t, err, "deliver")
901 totalEvaluations += 1
903 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
905 checkEvaluationCount(t, totalEvaluations)
908 // Delivery from forwarding server that isn't a forward should get same treatment.
909 ts.run(func(client *smtpclient.Client) {
910 mailFrom := "other@forward.example"
912 // Ensure To header matches.
915 msg = strings.ReplaceAll(msg, "<mjl@mox.example>", "<mjl3@mox.example>")
918 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
920 tcheck(t, err, "deliver")
921 totalEvaluations += 1
923 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
925 checkEvaluationCount(t, totalEvaluations)
933// Messages that we sent to, that have passing DMARC, but that are otherwise spammy, should be accepted.
934func TestDMARCSent(t *testing.T) {
935 resolver := &dns.MockResolver{
936 A: map[string][]string{
937 "example.org.": {"127.0.0.1"}, // For mx check.
939 TXT: map[string][]string{
940 "example.org.": {"v=spf1 ip4:127.0.0.1 -all"},
941 "_dmarc.example.org.": {"v=DMARC1;p=reject;rua=mailto:dmarcrpt@example.org"},
944 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
947 // First check that DMARC policy rejects message and results in optional evaluation.
948 ts.run(func(client *smtpclient.Client) {
949 mailFrom := "remote@example.org"
950 rcptTo := "mjl@mox.example"
951 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
952 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7MultiAuthFails26})
953 l := checkEvaluationCount(t, 1)
954 tcompare(t, l[0].Optional, true)
957 // Update DNS for an SPF pass, and DMARC pass.
958 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
960 // Insert hammy & spammy messages not related to the test message.
962 MailFrom: "remote@test.example",
963 RcptToLocalpart: smtp.Localpart("mjl"),
964 RcptToDomain: "mox.example",
965 Flags: store.Flags{Seen: true},
966 Size: int64(len(deliverMessage)),
968 // We need at least 50 ham messages for the junk filter to become significant. We
969 // offset it with negative messages for mediocre score.
973 tinsertmsg(t, ts.acc, "Archive", &nm, deliverMessage)
977 tinsertmsg(t, ts.acc, "Archive", &nm, deliverMessage)
981 // Baseline, message should be refused for spammy content.
982 ts.run(func(client *smtpclient.Client) {
983 mailFrom := "remote@example.org"
984 rcptTo := "mjl@mox.example"
985 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
986 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
987 checkEvaluationCount(t, 1) // No new evaluation.
990 // Insert a message that we sent to the address that is about to send to us.
991 sentMsg := store.Message{Size: int64(len(deliverMessage))}
992 tinsertmsg(t, ts.acc, "Sent", &sentMsg, deliverMessage)
993 err := ts.acc.DB.Insert(ctxbg, &store.Recipient{MessageID: sentMsg.ID, Localpart: "remote", Domain: "example.org", OrgDomain: "example.org", Sent: time.Now()})
994 tcheck(t, err, "inserting message recipient")
996 // Reject a message due to DMARC again. Since we sent a message to the domain, it
997 // is no longer unknown and we should see a non-optional evaluation that will
998 // result in a DMARC report.
999 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.1 -all"}
1000 ts.run(func(client *smtpclient.Client) {
1001 mailFrom := "remote@example.org"
1002 rcptTo := "mjl@mox.example"
1003 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1004 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7MultiAuthFails26})
1005 l := checkEvaluationCount(t, 2) // New evaluation.
1006 tcompare(t, l[1].Optional, false)
1009 // We should now be accepting the message because we recently sent a message.
1010 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
1011 ts.run(func(client *smtpclient.Client) {
1012 mailFrom := "remote@example.org"
1013 rcptTo := "mjl@mox.example"
1014 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1015 tcheck(t, err, "deliver")
1016 l := checkEvaluationCount(t, 3) // New evaluation.
1017 tcompare(t, l[2].Optional, false)
1021// Test DNSBL, then getting through with subjectpass.
1022func TestBlocklistedSubjectpass(t *testing.T) {
1023 // Set up a DNSBL on dnsbl.example, and get DMARC pass.
1024 resolver := &dns.MockResolver{
1025 A: map[string][]string{
1026 "example.org.": {"127.0.0.10"}, // For mx check.
1027 "2.0.0.127.dnsbl.example.": {"127.0.0.2"}, // For healthcheck.
1028 "10.0.0.127.dnsbl.example.": {"127.0.0.10"}, // Where our connection pretends to come from.
1030 TXT: map[string][]string{
1031 "10.0.0.127.dnsbl.example.": {"blocklisted"},
1032 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
1033 "_dmarc.example.org.": {"v=DMARC1;p=reject"},
1035 PTR: map[string][]string{
1036 "127.0.0.10": {"example.org."}, // For iprev check.
1039 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1040 ts.dnsbls = []dns.Domain{{ASCII: "dnsbl.example"}}
1043 // Message should be refused softly (temporary error) due to DNSBL.
1044 ts.run(func(client *smtpclient.Client) {
1045 mailFrom := "remote@example.org"
1046 rcptTo := "mjl@mox.example"
1047 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1048 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
1051 // Set up subjectpass on account.
1052 acc := mox.Conf.Dynamic.Accounts[ts.acc.Name]
1053 acc.SubjectPass.Period = time.Hour
1054 mox.Conf.Dynamic.Accounts[ts.acc.Name] = acc
1056 // Message should be refused quickly (permanent error) due to DNSBL and Subjectkey.
1058 ts.run(func(client *smtpclient.Client) {
1059 mailFrom := "remote@example.org"
1060 rcptTo := "mjl@mox.example"
1061 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1062 cerr := ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7DeliveryUnauth1})
1063 i := strings.Index(cerr.Line, subjectpass.Explanation)
1065 t.Fatalf("got error line %q, expected error line with subjectpass", cerr.Line)
1067 pass = cerr.Line[i+len(subjectpass.Explanation):]
1070 ts.run(func(client *smtpclient.Client) {
1071 mailFrom := "remote@example.org"
1072 rcptTo := "mjl@mox.example"
1073 passMessage := strings.Replace(deliverMessage, "Subject: test", "Subject: test "+pass, 1)
1074 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(passMessage)), strings.NewReader(passMessage), false, false, false)
1075 tcheck(t, err, "deliver with subjectpass")
1079// Test accepting a DMARC report.
1080func TestDMARCReport(t *testing.T) {
1081 resolver := &dns.MockResolver{
1082 A: map[string][]string{
1083 "example.org.": {"127.0.0.10"}, // For mx check.
1085 TXT: map[string][]string{
1086 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
1087 "_dmarc.example.org.": {"v=DMARC1;p=reject; rua=mailto:dmarcrpt@example.org"},
1089 PTR: map[string][]string{
1090 "127.0.0.10": {"example.org."}, // For iprev check.
1093 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/dmarcreport/mox.conf"), resolver)
1096 run := func(report string, n int) {
1098 ts.run(func(client *smtpclient.Client) {
1101 mailFrom := "remote@example.org"
1102 rcptTo := "mjl@mox.example"
1104 msgb := &bytes.Buffer{}
1105 _, xerr := fmt.Fprintf(msgb, "From: %s\r\nTo: %s\r\nSubject: dmarc report\r\nMIME-Version: 1.0\r\nContent-Type: text/xml\r\n\r\n", mailFrom, rcptTo)
1106 tcheck(t, xerr, "write msg headers")
1107 w := quotedprintable.NewWriter(msgb)
1108 _, xerr = w.Write([]byte(strings.ReplaceAll(report, "\n", "\r\n")))
1109 tcheck(t, xerr, "write message")
1110 msg := msgb.String()
1112 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1113 tcheck(t, err, "deliver")
1115 records, err := dmarcdb.Records(ctxbg)
1116 tcheck(t, err, "dmarcdb records")
1117 if len(records) != n {
1118 t.Fatalf("got %d dmarcdb records, expected %d or more", len(records), n)
1124 run(strings.ReplaceAll(dmarcReport, "xmox.nl", "mox.example"), 1)
1126 // We always store as an evaluation, but as optional for reports.
1127 evals := checkEvaluationCount(t, 2)
1128 tcompare(t, evals[0].Optional, true)
1129 tcompare(t, evals[1].Optional, true)
1132const dmarcReport = `<?xml version="1.0" encoding="UTF-8" ?>
1135 <org_name>example.org</org_name>
1136 <email>postmaster@example.org</email>
1137 <report_id>1</report_id>
1139 <begin>1596412800</begin>
1140 <end>1596499199</end>
1144 <domain>xmox.nl</domain>
1153 <source_ip>127.0.0.10</source_ip>
1156 <disposition>none</disposition>
1162 <header_from>xmox.nl</header_from>
1166 <domain>xmox.nl</domain>
1167 <result>pass</result>
1168 <selector>testsel</selector>
1171 <domain>xmox.nl</domain>
1172 <result>pass</result>
1179// Test accepting a TLS report.
1180func TestTLSReport(t *testing.T) {
1181 // Requires setting up DKIM.
1182 privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
1183 dkimRecord := dkim.Record{
1185 Hashes: []string{"sha256"},
1186 Flags: []string{"s"},
1187 PublicKey: privKey.Public(),
1190 dkimTxt, err := dkimRecord.Record()
1191 tcheck(t, err, "dkim record")
1193 sel := config.Selector{
1194 HashEffective: "sha256",
1195 HeadersEffective: []string{"From", "To", "Subject", "Date"},
1197 Domain: dns.Domain{ASCII: "testsel"},
1199 dkimConf := config.DKIM{
1200 Selectors: map[string]config.Selector{"testsel": sel},
1201 Sign: []string{"testsel"},
1204 resolver := &dns.MockResolver{
1205 A: map[string][]string{
1206 "example.org.": {"127.0.0.10"}, // For mx check.
1208 TXT: map[string][]string{
1209 "testsel._domainkey.example.org.": {dkimTxt},
1210 "_dmarc.example.org.": {"v=DMARC1;p=reject;rua=mailto:dmarcrpt@example.org"},
1212 PTR: map[string][]string{
1213 "127.0.0.10": {"example.org."}, // For iprev check.
1216 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/tlsrpt/mox.conf"), resolver)
1219 run := func(rcptTo, tlsrpt string, n int) {
1221 ts.run(func(client *smtpclient.Client) {
1224 mailFrom := "remote@example.org"
1226 msgb := &bytes.Buffer{}
1227 _, xerr := fmt.Fprintf(msgb, "From: %s\r\nTo: %s\r\nSubject: tlsrpt report\r\nMIME-Version: 1.0\r\nContent-Type: application/tlsrpt+json\r\n\r\n%s\r\n", mailFrom, rcptTo, tlsrpt)
1228 tcheck(t, xerr, "write msg")
1229 msg := msgb.String()
1231 selectors := mox.DKIMSelectors(dkimConf)
1232 headers, xerr := dkim.Sign(ctxbg, pkglog.Logger, "remote", dns.Domain{ASCII: "example.org"}, selectors, false, strings.NewReader(msg))
1233 tcheck(t, xerr, "dkim sign")
1236 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1237 tcheck(t, err, "deliver")
1239 records, err := tlsrptdb.Records(ctxbg)
1240 tcheck(t, err, "tlsrptdb records")
1241 if len(records) != n {
1242 t.Fatalf("got %d tlsrptdb records, expected %d", len(records), n)
1247 const tlsrpt = `{"organization-name":"Example.org","date-range":{"start-datetime":"2022-01-07T00:00:00Z","end-datetime":"2022-01-07T23:59:59Z"},"contact-info":"tlsrpt@example.org","report-id":"1","policies":[{"policy":{"policy-type":"no-policy-found","policy-domain":"xmox.nl"},"summary":{"total-successful-session-count":1,"total-failure-session-count":0}}]}`
1249 run("mjl@mox.example", tlsrpt, 0)
1250 run("mjl@mox.example", strings.ReplaceAll(tlsrpt, "xmox.nl", "mox.example"), 1)
1251 run("mjl@mailhost.mox.example", strings.ReplaceAll(tlsrpt, "xmox.nl", "mailhost.mox.example"), 2)
1253 // We always store as an evaluation, but as optional for reports.
1254 evals := checkEvaluationCount(t, 3)
1255 tcompare(t, evals[0].Optional, true)
1256 tcompare(t, evals[1].Optional, true)
1257 tcompare(t, evals[2].Optional, true)
1260func TestRatelimitConnectionrate(t *testing.T) {
1261 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1264 // We'll be creating 300 connections, no TLS and reduce noise.
1265 ts.tlsmode = smtpclient.TLSSkip
1266 mlog.SetConfig(map[string]slog.Level{"": mlog.LevelInfo})
1267 defer mlog.SetConfig(map[string]slog.Level{"": mlog.LevelDebug})
1269 // We may be passing a window boundary during this tests. The limit is 300/minute.
1270 // So make twice that many connections and hope the tests don't take too long.
1271 for i := 0; i <= 2*300; i++ {
1272 ts.runx(func(err error, client *smtpclient.Client) {
1274 if err != nil && i < 300 {
1275 t.Fatalf("expected smtp connection, got %v", err)
1277 if err == nil && i == 600 {
1278 t.Fatalf("expected no smtp connection due to connection rate limit, got connection")
1287func TestRatelimitAuth(t *testing.T) {
1288 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1291 ts.submission = true
1292 ts.tlsmode = smtpclient.TLSSkip
1296 // We may be passing a window boundary during this tests. The limit is 10 auth
1297 // failures/minute. So make twice that many connections and hope the tests don't
1299 for i := 0; i <= 2*10; i++ {
1300 ts.runx(func(err error, client *smtpclient.Client) {
1303 t.Fatalf("got auth success with bad credentials")
1305 var cerr smtpclient.Error
1306 badauth := errors.As(err, &cerr) && cerr.Code == smtp.C535AuthBadCreds
1307 if !badauth && i < 10 {
1308 t.Fatalf("expected auth failure, got %v", err)
1310 if badauth && i == 20 {
1311 t.Fatalf("expected no smtp connection due to failed auth rate limit, got other error %v", err)
1320func TestRatelimitDelivery(t *testing.T) {
1321 resolver := dns.MockResolver{
1322 A: map[string][]string{
1323 "example.org.": {"127.0.0.10"}, // For mx check.
1325 PTR: map[string][]string{
1326 "127.0.0.10": {"example.org."},
1329 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1332 orig := limitIPMasked1MessagesPerMinute
1333 limitIPMasked1MessagesPerMinute = 1
1335 limitIPMasked1MessagesPerMinute = orig
1338 ts.run(func(client *smtpclient.Client) {
1339 mailFrom := "remote@example.org"
1340 rcptTo := "mjl@mox.example"
1341 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1342 tcheck(t, err, "deliver to remote")
1344 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1345 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C452StorageFull, Secode: smtp.SeMailbox2Full2})
1348 limitIPMasked1MessagesPerMinute = orig
1350 origSize := limitIPMasked1SizePerMinute
1351 // Message was already delivered once. We'll do another one. But the 3rd will fail.
1352 // We need the actual size with prepended headers, since that is used in the
1354 msg, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).Get()
1356 t.Fatalf("getting delivered message for its size: %v", err)
1358 limitIPMasked1SizePerMinute = 2*msg.Size + int64(len(deliverMessage)/2)
1360 limitIPMasked1SizePerMinute = origSize
1362 ts.run(func(client *smtpclient.Client) {
1363 mailFrom := "remote@example.org"
1364 rcptTo := "mjl@mox.example"
1365 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1366 tcheck(t, err, "deliver to remote")
1368 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1369 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C452StorageFull, Secode: smtp.SeMailbox2Full2})
1373func TestNonSMTP(t *testing.T) {
1374 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1378 serverConn, clientConn := net.Pipe()
1379 defer serverConn.Close()
1380 serverdone := make(chan struct{})
1381 defer func() { <-serverdone }()
1384 tlsConfig := &tls.Config{
1385 Certificates: []tls.Certificate{fakeCert(ts.t, false)},
1387 serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, tlsConfig, serverConn, ts.resolver, ts.submission, false, false, 100<<20, false, false, false, ts.dnsbls, 0)
1391 defer clientConn.Close()
1393 buf := make([]byte, 128)
1395 // Read and ignore hello.
1396 if _, err := clientConn.Read(buf); err != nil {
1397 t.Fatalf("reading hello: %v", err)
1400 if _, err := fmt.Fprintf(clientConn, "bogus\r\n"); err != nil {
1401 t.Fatalf("write command: %v", err)
1403 n, err := clientConn.Read(buf)
1405 t.Fatalf("read response line: %v", err)
1407 s := string(buf[:n])
1408 if !strings.HasPrefix(s, "500 5.5.2 ") {
1409 t.Fatalf(`got %q, expected "500 5.5.2 ...`, s)
1411 if _, err := clientConn.Read(buf); err == nil {
1412 t.Fatalf("connection not closed after bogus command")
1416// Test limits on outgoing messages.
1417func TestLimitOutgoing(t *testing.T) {
1418 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpserversendlimit/mox.conf"), dns.MockResolver{})
1421 ts.user = "mjl@mox.example"
1423 ts.submission = true
1425 err := ts.acc.DB.Insert(ctxbg, &store.Outgoing{Recipient: "a@other.example", Submitted: time.Now().Add(-24*time.Hour - time.Minute)})
1426 tcheck(t, err, "inserting outgoing/recipient past 24h window")
1428 testSubmit := func(rcptTo string, expErr *smtpclient.Error) {
1430 ts.run(func(client *smtpclient.Client) {
1432 mailFrom := "mjl@mox.example"
1433 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
1434 ts.smtpErr(err, expErr)
1438 // Limits are set to 4 messages a day, 2 first-time recipients.
1439 testSubmit("b@other.example", nil)
1440 testSubmit("c@other.example", nil)
1441 testSubmit("d@other.example", &smtpclient.Error{Code: smtp.C451LocalErr, Secode: smtp.SePol7DeliveryUnauth1}) // Would be 3rd recipient.
1442 testSubmit("b@other.example", nil)
1443 testSubmit("b@other.example", nil)
1444 testSubmit("b@other.example", &smtpclient.Error{Code: smtp.C451LocalErr, Secode: smtp.SePol7DeliveryUnauth1}) // Would be 5th message.
1447// Test account size limit enforcement.
1448func TestQuota(t *testing.T) {
1449 resolver := dns.MockResolver{
1450 A: map[string][]string{
1451 "other.example.": {"127.0.0.10"}, // For mx check.
1453 PTR: map[string][]string{
1454 "127.0.0.10": {"other.example."},
1457 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpserverquota/mox.conf"), resolver)
1460 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1461 ts.run(func(client *smtpclient.Client) {
1462 mailFrom := "mjl@other.example"
1463 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1464 ts.smtpErr(err, expErr)
1468 testDeliver("mjl@mox.example", &smtpclient.Error{Code: smtp.C452StorageFull, Secode: smtp.SeMailbox2Full2})
1471// Test with catchall destination address.
1472func TestCatchall(t *testing.T) {
1473 resolver := dns.MockResolver{
1474 A: map[string][]string{
1475 "other.example.": {"127.0.0.10"}, // For mx check.
1477 PTR: map[string][]string{
1478 "127.0.0.10": {"other.example."},
1481 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpservercatchall/mox.conf"), resolver)
1484 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1486 ts.run(func(client *smtpclient.Client) {
1488 mailFrom := "mjl@other.example"
1489 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
1490 ts.smtpErr(err, expErr)
1494 testDeliver("mjl@mox.example", nil) // Exact match.
1495 testDeliver("mjl+test@mox.example", nil) // Domain localpart catchall separator.
1496 testDeliver("MJL+TEST@mox.example", nil) // Again, and case insensitive.
1498 n, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).Count()
1499 tcheck(t, err, "checking delivered messages")
1502 testDeliver("unknown@mox.example", nil) // Catchall address, to account catchall.
1504 acc, err := store.OpenAccount(pkglog, "catchall", false)
1505 tcheck(t, err, "open account")
1510 n, err = bstore.QueryDB[store.Message](ctxbg, acc.DB).Count()
1511 tcheck(t, err, "checking delivered messages to catchall account")
1514 testDeliver("mjl-test@mox2.example", nil) // Second catchall separator.
1515 testDeliver("mjl-test+test@mox2.example", nil) // Silly, both separators in address.
1516 testDeliver("mjl+test-test@mox2.example", nil)
1517 n, err = bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).Count()
1518 tcheck(t, err, "checking delivered messages")
1522// Test DKIM signing for outgoing messages.
1523func TestDKIMSign(t *testing.T) {
1524 resolver := dns.MockResolver{
1525 A: map[string][]string{
1526 "mox.example.": {"127.0.0.10"}, // For mx check.
1528 PTR: map[string][]string{
1529 "127.0.0.10": {"mox.example."},
1533 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1536 // Set DKIM signing config.
1538 genDKIM := func(domain string) string {
1539 dom, _ := mox.Conf.Domain(dns.Domain{ASCII: domain})
1541 privkey := make([]byte, ed25519.SeedSize) // Fake key, don't use for real.
1543 privkey[0] = byte(gen)
1545 sel := config.Selector{
1546 HashEffective: "sha256",
1547 HeadersEffective: []string{"From", "To", "Subject"},
1548 Key: ed25519.NewKeyFromSeed(privkey),
1549 Domain: dns.Domain{ASCII: "testsel"},
1551 dom.DKIM = config.DKIM{
1552 Selectors: map[string]config.Selector{"testsel": sel},
1553 Sign: []string{"testsel"},
1555 mox.Conf.Dynamic.Domains[domain] = dom
1556 pubkey := sel.Key.Public().(ed25519.PublicKey)
1557 return "v=DKIM1;k=ed25519;p=" + base64.StdEncoding.EncodeToString(pubkey)
1560 dkimtxt := genDKIM("mox.example")
1561 dkimtxt2 := genDKIM("mox2.example")
1563 // DKIM verify needs to find the key.
1564 resolver.TXT = map[string][]string{
1565 "testsel._domainkey.mox.example.": {dkimtxt},
1566 "testsel._domainkey.mox2.example.": {dkimtxt2},
1569 ts.submission = true
1570 ts.user = "mjl@mox.example"
1574 testSubmit := func(mailFrom, msgFrom string) {
1576 ts.run(func(client *smtpclient.Client) {
1579 msg := strings.ReplaceAll(fmt.Sprintf(`From: <%s>
1580To: <remote@example.org>
1582Message-Id: <test@mox.example>
1585`, msgFrom), "\n", "\r\n")
1587 rcptTo := "remote@example.org"
1588 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1589 tcheck(t, err, "deliver")
1591 msgs, err := queue.List(ctxbg, queue.Filter{}, queue.Sort{})
1592 tcheck(t, err, "listing queue")
1594 tcompare(t, len(msgs), n)
1595 sort.Slice(msgs, func(i, j int) bool {
1596 return msgs[i].ID > msgs[j].ID
1598 f, err := queue.OpenMessage(ctxbg, msgs[0].ID)
1599 tcheck(t, err, "open message in queue")
1601 results, err := dkim.Verify(ctxbg, pkglog.Logger, resolver, false, dkim.DefaultPolicy, f, false)
1602 tcheck(t, err, "verifying dkim message")
1603 tcompare(t, len(results), 1)
1604 tcompare(t, results[0].Status, dkim.StatusPass)
1605 tcompare(t, results[0].Sig.Domain.ASCII, strings.Split(msgFrom, "@")[1])
1609 testSubmit("mjl@mox.example", "mjl@mox.example")
1610 testSubmit("mjl@mox.example", "mjl@mox2.example") // DKIM signature will be for mox2.example.
1613// Test to postmaster addresses.
1614func TestPostmaster(t *testing.T) {
1615 resolver := dns.MockResolver{
1616 A: map[string][]string{
1617 "other.example.": {"127.0.0.10"}, // For mx check.
1619 PTR: map[string][]string{
1620 "127.0.0.10": {"other.example."},
1623 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/postmaster/mox.conf"), resolver)
1626 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1628 ts.run(func(client *smtpclient.Client) {
1630 mailFrom := "mjl@other.example"
1631 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1632 ts.smtpErr(err, expErr)
1636 testDeliver("postmaster", nil) // Plain postmaster address without domain.
1637 testDeliver("postmaster@host.mox.example", nil) // Postmaster address with configured mail server hostname.
1638 testDeliver("postmaster@mox.example", nil) // Postmaster address without explicitly configured destination.
1639 testDeliver("postmaster@unknown.example", &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
1642// Test to address with empty localpart.
1643func TestEmptylocalpart(t *testing.T) {
1644 resolver := dns.MockResolver{
1645 A: map[string][]string{
1646 "other.example.": {"127.0.0.10"}, // For mx check.
1648 PTR: map[string][]string{
1649 "127.0.0.10": {"other.example."},
1652 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1655 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1656 ts.run(func(client *smtpclient.Client) {
1657 mailFrom := `""@other.example`
1658 msg := strings.ReplaceAll(deliverMessage, "To: <mjl@mox.example>", `To: <""@mox.example>`)
1659 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1660 ts.smtpErr(err, expErr)
1664 testDeliver(`""@mox.example`, nil)
1667// Test handling REQUIRETLS and TLS-Required: No.
1668func TestRequireTLS(t *testing.T) {
1669 resolver := dns.MockResolver{
1670 A: map[string][]string{
1671 "mox.example.": {"127.0.0.10"}, // For mx check.
1673 PTR: map[string][]string{
1674 "127.0.0.10": {"mox.example."},
1678 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1681 ts.submission = true
1682 ts.requiretls = true
1683 ts.user = "mjl@mox.example"
1689 msg0 := strings.ReplaceAll(`From: <mjl@mox.example>
1690To: <remote@example.org>
1692Message-Id: <test@mox.example>
1698 msg1 := strings.ReplaceAll(`From: <mjl@mox.example>
1699To: <remote@example.org>
1701Message-Id: <test@mox.example>
1708 msg2 := strings.ReplaceAll(`From: <mjl@mox.example>
1709To: <remote@example.org>
1711Message-Id: <test@mox.example>
1716 testSubmit := func(msg string, requiretls bool, expRequireTLS *bool) {
1718 ts.run(func(client *smtpclient.Client) {
1721 rcptTo := "remote@example.org"
1722 err := client.Deliver(ctxbg, "mjl@mox.example", rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, requiretls)
1723 tcheck(t, err, "deliver")
1725 msgs, err := queue.List(ctxbg, queue.Filter{}, queue.Sort{})
1726 tcheck(t, err, "listing queue")
1727 tcompare(t, len(msgs), 1)
1728 tcompare(t, msgs[0].RequireTLS, expRequireTLS)
1729 _, err = queue.Drop(ctxbg, pkglog, queue.Filter{IDs: []int64{msgs[0].ID}})
1730 tcheck(t, err, "deleting message from queue")
1734 testSubmit(msg0, true, &yes) // Header ignored, requiretls applied.
1735 testSubmit(msg0, false, &no) // TLS-Required header applied.
1736 testSubmit(msg1, true, &yes) // Bad headers ignored, requiretls applied.
1737 testSubmit(msg1, false, nil) // Inconsistent multiple headers ignored.
1738 testSubmit(msg2, false, nil) // Regular message, no RequireTLS setting.
1739 testSubmit(msg2, true, &yes) // Requiretls applied.
1741 // Check that we get an error if remote SMTP server does not support the requiretls
1743 ts.requiretls = false
1744 ts.run(func(client *smtpclient.Client) {
1745 rcptTo := "remote@example.org"
1746 err := client.Deliver(ctxbg, "mjl@mox.example", rcptTo, int64(len(msg0)), strings.NewReader(msg0), false, false, true)
1748 t.Fatalf("delivered with requiretls to server without requiretls")
1750 if !errors.Is(err, smtpclient.ErrRequireTLSUnsupported) {
1751 t.Fatalf("got err %v, expected ErrRequireTLSUnsupported", err)
1756func TestSmuggle(t *testing.T) {
1757 resolver := dns.MockResolver{
1758 A: map[string][]string{
1759 "example.org.": {"127.0.0.10"}, // For mx check.
1761 PTR: map[string][]string{
1762 "127.0.0.10": {"example.org."}, // For iprev check.
1765 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpsmuggle/mox.conf"), resolver)
1766 ts.tlsmode = smtpclient.TLSSkip
1769 test := func(data string) {
1772 ts.runRaw(func(conn net.Conn) {
1775 ourHostname := mox.Conf.Static.HostnameDomain
1776 remoteHostname := dns.Domain{ASCII: "mox.example"}
1777 opts := smtpclient.Opts{
1778 RootCAs: mox.Conf.Static.TLS.CertPool,
1780 log := pkglog.WithCid(ts.cid - 1)
1781 _, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, ts.tlspkix, ourHostname, remoteHostname, opts)
1782 tcheck(t, err, "smtpclient")
1785 write := func(s string) {
1786 _, err := conn.Write([]byte(s))
1787 tcheck(t, err, "write")
1790 readPrefixLine := func(prefix string) string {
1792 buf := make([]byte, 512)
1793 n, err := conn.Read(buf)
1794 tcheck(t, err, "read")
1795 s := strings.TrimRight(string(buf[:n]), "\r\n")
1796 if !strings.HasPrefix(s, prefix) {
1797 t.Fatalf("got smtp response %q, expected line with prefix %q", s, prefix)
1802 write("MAIL FROM:<remote@example.org>\r\n")
1804 write("RCPT TO:<mjl@mox.example>\r\n")
1809 write("\r\n") // Empty header.
1811 write("\r\n.\r\n") // End of message.
1812 line := readPrefixLine("5")
1813 if !strings.Contains(line, "smug") {
1814 t.Errorf("got 5xx error with message %q, expected error text containing smug", line)
1825func TestFutureRelease(t *testing.T) {
1826 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1827 ts.tlsmode = smtpclient.TLSSkip
1828 ts.user = "mjl@mox.example"
1830 ts.submission = true
1833 ts.auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
1834 return sasl.NewClientPlain(ts.user, ts.pass), nil
1837 test := func(mailtoMore, expResponsePrefix string) {
1840 ts.runRaw(func(conn net.Conn) {
1843 ourHostname := mox.Conf.Static.HostnameDomain
1844 remoteHostname := dns.Domain{ASCII: "mox.example"}
1845 opts := smtpclient.Opts{Auth: ts.auth}
1846 log := pkglog.WithCid(ts.cid - 1)
1847 _, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, false, ourHostname, remoteHostname, opts)
1848 tcheck(t, err, "smtpclient")
1851 write := func(s string) {
1852 _, err := conn.Write([]byte(s))
1853 tcheck(t, err, "write")
1856 readPrefixLine := func(prefix string) string {
1858 buf := make([]byte, 512)
1859 n, err := conn.Read(buf)
1860 tcheck(t, err, "read")
1861 s := strings.TrimRight(string(buf[:n]), "\r\n")
1862 if !strings.HasPrefix(s, prefix) {
1863 t.Fatalf("got smtp response %q, expected line with prefix %q", s, prefix)
1868 write(fmt.Sprintf("MAIL FROM:<mjl@mox.example>%s\r\n", mailtoMore))
1869 readPrefixLine(expResponsePrefix)
1870 if expResponsePrefix != "2" {
1873 write("RCPT TO:<mjl@mox.example>\r\n")
1878 write("From: <mjl@mox.example>\r\n\r\nbody\r\n\r\n.\r\n")
1883 test(" HOLDFOR=1", "2")
1884 test(" HOLDUNTIL="+time.Now().Add(time.Minute).UTC().Format(time.RFC3339), "2")
1885 test(" HOLDUNTIL="+time.Now().Add(time.Minute).UTC().Format(time.RFC3339Nano), "2")
1887 test(" HOLDFOR=0", "501") // 0 is invalid syntax.
1888 test(fmt.Sprintf(" HOLDFOR=%d", int64((queue.FutureReleaseIntervalMax+time.Minute)/time.Second)), "554") // Too far in the future.
1889 test(" HOLDUNTIL="+time.Now().Add(-time.Minute).UTC().Format(time.RFC3339), "554") // In the past.
1890 test(" HOLDUNTIL="+time.Now().Add(queue.FutureReleaseIntervalMax+time.Minute).UTC().Format(time.RFC3339), "554") // Too far in the future.
1891 test(" HOLDUNTIL=2024-02-10T17:28:00+00:00", "501") // "Z" required.
1892 test(" HOLDUNTIL=24-02-10T17:28:00Z", "501") // Invalid.
1893 test(" HOLDFOR=1 HOLDFOR=1", "501") // Duplicate.
1894 test(" HOLDFOR=1 HOLDUNTIL="+time.Now().Add(time.Hour).UTC().Format(time.RFC3339), "501") // Duplicate.
1898func TestSMTPUTF8(t *testing.T) {
1899 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1902 ts.user = "mjl@mox.example"
1904 ts.submission = true
1906 test := func(mailFrom string, rcptTo string, headerValue string, filename string, clientSmtputf8 bool, expectedSmtputf8 bool, expErr *smtpclient.Error) {
1909 ts.run(func(client *smtpclient.Client) {
1911 msg := strings.ReplaceAll(fmt.Sprintf(`From: <%s>
1914X-Custom-Test-Header: %s
1916Content-type: multipart/mixed; boundary="simple boundary"
1919Content-Type: text/plain; charset=UTF-8;
1920Content-Disposition: attachment; filename="%s"
1921Content-Transfer-Encoding: base64
1923QW4gYXR0YWNoZWQgdGV4dCBmaWxlLg==
1926`, mailFrom, rcptTo, headerValue, filename), "\n", "\r\n")
1928 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), true, clientSmtputf8, false)
1929 ts.smtpErr(err, expErr)
1934 msgs, _ := queue.List(ctxbg, queue.Filter{}, queue.Sort{Field: "Queued", Asc: false})
1935 queuedMsg := msgs[0]
1936 if queuedMsg.SMTPUTF8 != expectedSmtputf8 {
1937 t.Fatalf("[%s / %s / %s / %s] got SMTPUTF8 %t, expected %t", mailFrom, rcptTo, headerValue, filename, queuedMsg.SMTPUTF8, expectedSmtputf8)
1942 test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", false, false, nil)
1943 test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", true, false, nil)
1944 test(`mjl@mox.example`, `🙂@example.org`, "header-ascii", "ascii.txt", true, true, nil)
1945 test(`mjl@mox.example`, `🙂@example.org`, "header-ascii", "ascii.txt", false, true, &smtpclient.Error{Permanent: true, Code: smtp.C553BadMailbox, Secode: smtp.SeMsg6NonASCIIAddrNotPermitted7})
1946 test(`Ω@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", true, true, nil)
1947 test(`Ω@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", false, true, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeMsg6NonASCIIAddrNotPermitted7})
1948 test(`mjl@mox.example`, `remote@example.org`, "header-utf8-😍", "ascii.txt", true, true, nil)
1949 test(`mjl@mox.example`, `remote@example.org`, "header-utf8-😍", "ascii.txt", false, true, nil)
1950 test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "utf8-🫠️.txt", true, true, nil)
1951 test(`Ω@mox.example`, `🙂@example.org`, "header-utf8-😍", "utf8-🫠️.txt", true, true, nil)
1952 test(`mjl@mox.example`, `remote@xn--vg8h.example.org`, "header-ascii", "ascii.txt", true, false, nil)
1955// TestExtra checks whether submission of messages with "X-Mox-Extra-<key>: value"
1956// headers cause those those key/value pairs to be added to the Extra field in the
1958func TestExtra(t *testing.T) {
1959 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1962 ts.user = "mjl@mox.example"
1964 ts.submission = true
1966 extraMsg := strings.ReplaceAll(`From: <mjl@mox.example>
1967To: <remote@example.org>
1969X-Mox-Extra-Test: testvalue
1972X-Mox-Extra-x-cANONICAL-z: ok
1973Message-Id: <test@mox.example>
1978 ts.run(func(client *smtpclient.Client) {
1979 mailFrom := "mjl@mox.example"
1980 rcptTo := "mjl@mox.example"
1981 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
1982 tcheck(t, err, "deliver")
1984 msgs, err := queue.List(ctxbg, queue.Filter{}, queue.Sort{})
1985 tcheck(t, err, "queue list")
1986 tcompare(t, len(msgs), 1)
1987 tcompare(t, msgs[0].Extra, map[string]string{
1988 "Test": "testvalue",
1991 "X-Canonical-Z": "ok",
1993 // note: these headers currently stay in the message.
1996// TestExtraDup checks for an error for duplicate x-mox-extra-* keys.
1997func TestExtraDup(t *testing.T) {
1998 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
2001 ts.user = "mjl@mox.example"
2003 ts.submission = true
2005 extraMsg := strings.ReplaceAll(`From: <mjl@mox.example>
2006To: <remote@example.org>
2008X-Mox-Extra-Test: testvalue
2009X-Mox-Extra-Test: testvalue
2010Message-Id: <test@mox.example>
2015 ts.run(func(client *smtpclient.Client) {
2016 mailFrom := "mjl@mox.example"
2017 rcptTo := "mjl@mox.example"
2018 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2019 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C554TransactionFailed, Secode: smtp.SeMsg6Other0})
2023// FromID can be specified during submission, but must be unique, with single recipient.
2024func TestUniqueFromID(t *testing.T) {
2025 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpfromid/mox.conf"), dns.MockResolver{})
2028 ts.user = "mjl+fromid@mox.example"
2030 ts.submission = true
2032 extraMsg := strings.ReplaceAll(`From: <mjl@mox.example>
2033To: <remote@example.org>
2039 // Specify our own unique id when queueing.
2040 ts.run(func(client *smtpclient.Client) {
2041 mailFrom := "mjl+unique@mox.example"
2042 rcptTo := "mjl@mox.example"
2043 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2044 ts.smtpErr(err, nil)
2047 // But we can only use it once.
2048 ts.run(func(client *smtpclient.Client) {
2049 mailFrom := "mjl+unique@mox.example"
2050 rcptTo := "mjl@mox.example"
2051 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2052 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C554TransactionFailed, Secode: smtp.SeAddr1SenderSyntax7})
2055 // We cannot use our own fromid with multiple recipients.
2056 ts.run(func(client *smtpclient.Client) {
2057 mailFrom := "mjl+unique2@mox.example"
2058 rcptTo := []string{"mjl@mox.example", "mjl@mox.example"}
2059 _, err := client.DeliverMultiple(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2060 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C554TransactionFailed, Secode: smtp.SeProto5TooManyRcpts3})
2064// TestDestinationSMTPError checks delivery to a destination with an SMTPError is rejected as configured.
2065func TestDestinationSMTPError(t *testing.T) {
2066 resolver := dns.MockResolver{
2067 A: map[string][]string{
2068 "example.org.": {"127.0.0.10"}, // For mx check.
2072 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
2075 ts.run(func(client *smtpclient.Client) {
2076 mailFrom := "mjl@example.org"
2077 rcptTo := "blocked@mox.example"
2078 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
2079 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
2083// TestDestinationMessageAuthRequiredSMTPError checks delivery to a destination
2084// with an MessageAuthRequiredSMTPError is accepted/rejected as configured.
2085func TestDestinationMessageAuthRequiredSMTPError(t *testing.T) {
2086 resolver := dns.MockResolver{
2087 A: map[string][]string{
2088 "example.org.": {"127.0.0.10"}, // For mx check.
2090 PTR: map[string][]string{
2091 "127.0.0.10": {"example.org."},
2093 TXT: map[string][]string{},
2096 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
2099 ts.run(func(client *smtpclient.Client) {
2100 mailFrom := "mjl@example.org"
2101 rcptTo := "msgauthrequired@mox.example"
2102 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
2103 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7MultiAuthFails26})
2106 // Ensure SPF pass, message should now be accepted.
2107 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
2108 ts.run(func(client *smtpclient.Client) {
2109 mailFrom := "mjl@example.org"
2110 rcptTo := "msgauthrequired@mox.example"
2111 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
2112 ts.smtpErr(err, nil)