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 _, l := ts.comm.Get()
631 timer := time.NewTimer(time.Second)
636 t.Fatalf("no delivery in 1s")
641 checkEvaluationCount(t, 0)
644func tinsertmsg(t *testing.T, acc *store.Account, mailbox string, m *store.Message, msg string) {
645 mf, err := store.CreateMessageTemp(pkglog, "insertmsg")
646 tcheck(t, err, "temp message")
647 defer os.Remove(mf.Name())
649 _, err = mf.Write([]byte(msg))
650 tcheck(t, err, "write message")
652 acc.WithWLock(func() {
653 err = acc.DeliverMailbox(pkglog, mailbox, m, mf)
654 tcheck(t, err, "deliver message")
658func tretrain(t *testing.T, acc *store.Account) {
661 // Fresh empty junkfilter.
662 basePath := mox.DataDirPath("accounts")
663 dbPath := filepath.Join(basePath, acc.Name, "junkfilter.db")
664 bloomPath := filepath.Join(basePath, acc.Name, "junkfilter.bloom")
667 jf, _, err := acc.OpenJunkFilter(ctxbg, pkglog)
668 tcheck(t, err, "open junk filter")
671 // Fetch messags to retrain on.
672 q := bstore.QueryDB[store.Message](ctxbg, acc.DB)
673 q.FilterEqual("Expunged", false)
674 q.FilterFn(func(m store.Message) bool {
675 return m.Flags.Junk != m.Flags.Notjunk
677 msgs, err := q.List()
678 tcheck(t, err, "fetch messages")
680 // Retrain the messages.
681 for _, m := range msgs {
682 ham := m.Flags.Notjunk
684 f, err := os.Open(acc.MessagePath(m.ID))
685 tcheck(t, err, "open message")
686 r := store.FileMsgReader(m.MsgPrefix, f)
688 jf.TrainMessage(ctxbg, r, m.Size, ham)
691 tcheck(t, err, "close message")
695 tcheck(t, err, "save junkfilter")
698// Test accept/reject with DMARC reputation and with spammy content.
699func TestSpam(t *testing.T) {
700 resolver := &dns.MockResolver{
701 A: map[string][]string{
702 "example.org.": {"127.0.0.1"}, // For mx check.
704 TXT: map[string][]string{
705 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
706 "_dmarc.example.org.": {"v=DMARC1;p=reject; rua=mailto:dmarcrpt@example.org"},
709 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
712 // Insert spammy messages. No junkfilter training yet.
714 RemoteIP: "127.0.0.10",
715 RemoteIPMasked1: "127.0.0.10",
716 RemoteIPMasked2: "127.0.0.0",
717 RemoteIPMasked3: "127.0.0.0",
718 MailFrom: "remote@example.org",
719 MailFromLocalpart: smtp.Localpart("remote"),
720 MailFromDomain: "example.org",
721 RcptToLocalpart: smtp.Localpart("mjl"),
722 RcptToDomain: "mox.example",
723 MsgFromLocalpart: smtp.Localpart("remote"),
724 MsgFromDomain: "example.org",
725 MsgFromOrgDomain: "example.org",
726 MsgFromValidated: true,
727 MsgFromValidation: store.ValidationStrict,
728 Flags: store.Flags{Seen: true, Junk: true},
729 Size: int64(len(deliverMessage)),
733 tinsertmsg(t, ts.acc, "Inbox", &nm, deliverMessage)
735 tinsertmsg(t, ts.acc, "mjl2", &nm, deliverMessage)
738 // Delivery from sender with bad reputation should fail.
739 ts.run(func(client *smtpclient.Client) {
740 mailFrom := "remote@example.org"
741 rcptTo := "mjl@mox.example"
742 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
743 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
745 ts.checkCount("Rejects", 1)
746 checkEvaluationCount(t, 0) // No positive interactions yet.
749 // Delivery from sender with bad reputation matching AcceptRejectsToMailbox should
750 // result in accepted delivery to the mailbox.
751 ts.run(func(client *smtpclient.Client) {
752 mailFrom := "remote@example.org"
753 rcptTo := "mjl2@mox.example"
754 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage2)), strings.NewReader(deliverMessage2), false, false, false)
755 tcheck(t, err, "deliver")
757 ts.checkCount("mjl2junk", 1) // In ruleset rejects mailbox.
758 ts.checkCount("Rejects", 1) // Same as before.
759 checkEvaluationCount(t, 0) // This is not an actual accept.
762 // Mark the messages as having good reputation.
764 err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).FilterEqual("Expunged", false).ForEach(func(m store.Message) error {
765 ids = append(ids, m.ID)
768 tcheck(t, err, "get message ids")
769 ts.xops.MessageFlagsClear(ctxbg, pkglog, ts.acc, ids, []string{"$Junk"})
770 ts.xops.MessageFlagsAdd(ctxbg, pkglog, ts.acc, ids, []string{"$NotJunk"})
772 // Message should now be accepted.
773 ts.run(func(client *smtpclient.Client) {
774 tcheck(t, err, "hello")
775 mailFrom := "remote@example.org"
776 rcptTo := "mjl@mox.example"
777 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
778 tcheck(t, err, "deliver")
780 // Message should now be removed from Rejects mailboxes.
781 ts.checkCount("Rejects", 0)
782 ts.checkCount("mjl2junk", 1)
783 checkEvaluationCount(t, 1)
786 // Undo dmarc pass, mark messages as junk, and train the filter.
788 q := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB)
789 q.FilterEqual("Expunged", false)
790 _, err = q.UpdateFields(map[string]any{"Junk": true, "Notjunk": false})
791 tcheck(t, err, "update junkiness")
794 // Message should be refused for spammy content.
795 ts.run(func(client *smtpclient.Client) {
796 tcheck(t, err, "hello")
797 mailFrom := "remote@example.org"
798 rcptTo := "mjl@mox.example"
799 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
800 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
801 checkEvaluationCount(t, 1) // No new evaluation, this isn't a DMARC reject.
805// Test accept/reject with forwarded messages, DMARC ignored, no IP/EHLO/MAIL
806// FROM-based reputation.
807func TestForward(t *testing.T) {
808 // Do a run without forwarding, and with.
809 check := func(forward bool) {
811 resolver := &dns.MockResolver{
812 A: map[string][]string{
813 "bad.example.": {"127.0.0.1"}, // For mx check.
814 "good.example.": {"127.0.0.1"}, // For mx check.
815 "forward.example.": {"127.0.0.10"}, // For mx check.
817 TXT: map[string][]string{
818 "bad.example.": {"v=spf1 ip4:127.0.0.1 -all"},
819 "good.example.": {"v=spf1 ip4:127.0.0.1 -all"},
820 "forward.example.": {"v=spf1 ip4:127.0.0.10 -all"},
821 "_dmarc.bad.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@bad.example"},
822 "_dmarc.good.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@good.example"},
823 "_dmarc.forward.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@forward.example"},
825 PTR: map[string][]string{
826 "127.0.0.10": {"forward.example."}, // For iprev check.
829 rcptTo := "mjl3@mox.example"
831 // For SPF and DMARC pass, otherwise the test ends quickly.
832 resolver.TXT["bad.example."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
833 resolver.TXT["good.example."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
834 rcptTo = "mjl@mox.example" // Without IsForward rule.
837 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
840 totalEvaluations := 0
842 var msgBad = strings.ReplaceAll(`From: <remote@bad.example>
845Message-Id: <bad@example.org>
849 var msgOK = strings.ReplaceAll(`From: <remote@good.example>
852Message-Id: <good@example.org>
856 var msgOK2 = strings.ReplaceAll(`From: <other@forward.example>
859Message-Id: <regular@example.org>
861happens to come from forwarding mail server.
864 // Deliver forwarded messages, then classify as junk. Normally enough to treat
865 // other unrelated messages from IP as junk, but not for forwarded messages.
866 ts.run(func(client *smtpclient.Client) {
867 mailFrom := "remote@forward.example"
869 mailFrom = "remote@bad.example"
873 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgBad)), strings.NewReader(msgBad), false, false, false)
874 tcheck(t, err, "deliver message")
876 totalEvaluations += 10
878 n, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).UpdateFields(map[string]any{"Junk": true, "MsgFromValidated": true})
879 tcheck(t, err, "marking messages as junk")
883 // Next delivery will fail, with negative "message From" signal.
884 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgBad)), strings.NewReader(msgBad), false, false, false)
885 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
887 checkEvaluationCount(t, totalEvaluations)
890 // Delivery from different "message From" without reputation, but from same
891 // forwarding email server, should succeed under forwarding, not as regular sending
893 ts.run(func(client *smtpclient.Client) {
894 mailFrom := "remote@forward.example"
896 mailFrom = "remote@good.example"
899 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgOK)), strings.NewReader(msgOK), false, false, false)
901 tcheck(t, err, "deliver")
902 totalEvaluations += 1
904 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
906 checkEvaluationCount(t, totalEvaluations)
909 // Delivery from forwarding server that isn't a forward should get same treatment.
910 ts.run(func(client *smtpclient.Client) {
911 mailFrom := "other@forward.example"
913 // Ensure To header matches.
916 msg = strings.ReplaceAll(msg, "<mjl@mox.example>", "<mjl3@mox.example>")
919 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
921 tcheck(t, err, "deliver")
922 totalEvaluations += 1
924 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
926 checkEvaluationCount(t, totalEvaluations)
934// Messages that we sent to, that have passing DMARC, but that are otherwise spammy, should be accepted.
935func TestDMARCSent(t *testing.T) {
936 resolver := &dns.MockResolver{
937 A: map[string][]string{
938 "example.org.": {"127.0.0.1"}, // For mx check.
940 TXT: map[string][]string{
941 "example.org.": {"v=spf1 ip4:127.0.0.1 -all"},
942 "_dmarc.example.org.": {"v=DMARC1;p=reject;rua=mailto:dmarcrpt@example.org"},
945 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
948 // First check that DMARC policy rejects message and results in optional evaluation.
949 ts.run(func(client *smtpclient.Client) {
950 mailFrom := "remote@example.org"
951 rcptTo := "mjl@mox.example"
952 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
953 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7MultiAuthFails26})
954 l := checkEvaluationCount(t, 1)
955 tcompare(t, l[0].Optional, true)
958 // Update DNS for an SPF pass, and DMARC pass.
959 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
961 // Insert hammy & spammy messages not related to the test message.
963 MailFrom: "remote@test.example",
964 RcptToLocalpart: smtp.Localpart("mjl"),
965 RcptToDomain: "mox.example",
966 Flags: store.Flags{Seen: true},
967 Size: int64(len(deliverMessage)),
969 // We need at least 50 ham messages for the junk filter to become significant. We
970 // offset it with negative messages for mediocre score.
974 tinsertmsg(t, ts.acc, "Archive", &nm, deliverMessage)
978 tinsertmsg(t, ts.acc, "Archive", &nm, deliverMessage)
982 // Baseline, message should be refused for spammy content.
983 ts.run(func(client *smtpclient.Client) {
984 mailFrom := "remote@example.org"
985 rcptTo := "mjl@mox.example"
986 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
987 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
988 checkEvaluationCount(t, 1) // No new evaluation.
991 // Insert a message that we sent to the address that is about to send to us.
992 sentMsg := store.Message{Size: int64(len(deliverMessage))}
993 tinsertmsg(t, ts.acc, "Sent", &sentMsg, deliverMessage)
994 err := ts.acc.DB.Insert(ctxbg, &store.Recipient{MessageID: sentMsg.ID, Localpart: "remote", Domain: "example.org", OrgDomain: "example.org", Sent: time.Now()})
995 tcheck(t, err, "inserting message recipient")
997 // Reject a message due to DMARC again. Since we sent a message to the domain, it
998 // is no longer unknown and we should see a non-optional evaluation that will
999 // result in a DMARC report.
1000 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.1 -all"}
1001 ts.run(func(client *smtpclient.Client) {
1002 mailFrom := "remote@example.org"
1003 rcptTo := "mjl@mox.example"
1004 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1005 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7MultiAuthFails26})
1006 l := checkEvaluationCount(t, 2) // New evaluation.
1007 tcompare(t, l[1].Optional, false)
1010 // We should now be accepting the message because we recently sent a message.
1011 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
1012 ts.run(func(client *smtpclient.Client) {
1013 mailFrom := "remote@example.org"
1014 rcptTo := "mjl@mox.example"
1015 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1016 tcheck(t, err, "deliver")
1017 l := checkEvaluationCount(t, 3) // New evaluation.
1018 tcompare(t, l[2].Optional, false)
1022// Test DNSBL, then getting through with subjectpass.
1023func TestBlocklistedSubjectpass(t *testing.T) {
1024 // Set up a DNSBL on dnsbl.example, and get DMARC pass.
1025 resolver := &dns.MockResolver{
1026 A: map[string][]string{
1027 "example.org.": {"127.0.0.10"}, // For mx check.
1028 "2.0.0.127.dnsbl.example.": {"127.0.0.2"}, // For healthcheck.
1029 "10.0.0.127.dnsbl.example.": {"127.0.0.10"}, // Where our connection pretends to come from.
1031 TXT: map[string][]string{
1032 "10.0.0.127.dnsbl.example.": {"blocklisted"},
1033 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
1034 "_dmarc.example.org.": {"v=DMARC1;p=reject"},
1036 PTR: map[string][]string{
1037 "127.0.0.10": {"example.org."}, // For iprev check.
1040 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1041 ts.dnsbls = []dns.Domain{{ASCII: "dnsbl.example"}}
1044 // Message should be refused softly (temporary error) due to DNSBL.
1045 ts.run(func(client *smtpclient.Client) {
1046 mailFrom := "remote@example.org"
1047 rcptTo := "mjl@mox.example"
1048 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1049 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
1052 // Set up subjectpass on account.
1053 acc := mox.Conf.Dynamic.Accounts[ts.acc.Name]
1054 acc.SubjectPass.Period = time.Hour
1055 mox.Conf.Dynamic.Accounts[ts.acc.Name] = acc
1057 // Message should be refused quickly (permanent error) due to DNSBL and Subjectkey.
1059 ts.run(func(client *smtpclient.Client) {
1060 mailFrom := "remote@example.org"
1061 rcptTo := "mjl@mox.example"
1062 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1063 cerr := ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7DeliveryUnauth1})
1064 i := strings.Index(cerr.Line, subjectpass.Explanation)
1066 t.Fatalf("got error line %q, expected error line with subjectpass", cerr.Line)
1068 pass = cerr.Line[i+len(subjectpass.Explanation):]
1071 ts.run(func(client *smtpclient.Client) {
1072 mailFrom := "remote@example.org"
1073 rcptTo := "mjl@mox.example"
1074 passMessage := strings.Replace(deliverMessage, "Subject: test", "Subject: test "+pass, 1)
1075 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(passMessage)), strings.NewReader(passMessage), false, false, false)
1076 tcheck(t, err, "deliver with subjectpass")
1080// Test accepting a DMARC report.
1081func TestDMARCReport(t *testing.T) {
1082 resolver := &dns.MockResolver{
1083 A: map[string][]string{
1084 "example.org.": {"127.0.0.10"}, // For mx check.
1086 TXT: map[string][]string{
1087 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
1088 "_dmarc.example.org.": {"v=DMARC1;p=reject; rua=mailto:dmarcrpt@example.org"},
1090 PTR: map[string][]string{
1091 "127.0.0.10": {"example.org."}, // For iprev check.
1094 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/dmarcreport/mox.conf"), resolver)
1097 run := func(rcptTo, report string, n int) {
1099 ts.run(func(client *smtpclient.Client) {
1102 mailFrom := "remote@example.org"
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("dmarc-reports@mox.example", dmarcReport, 0) // Wrong domain in report.
1126 report := strings.ReplaceAll(dmarcReport, "xmox.nl", "mox.example")
1128 run("dmarc-reports@mox.example", report, n)
1130 // We always store as an evaluation, but as optional for reports.
1131 evals := checkEvaluationCount(t, 2)
1132 tcompare(t, evals[0].Optional, true)
1133 tcompare(t, evals[1].Optional, true)
1135 // Not a dmarc recipient, delivery should succeed.
1136 run("mjl@mox.example", report, n)
1137 run("mjl-test@mox.example", report, n)
1138 run("mjl+test@mox.example", report, n)
1139 // Likewise, address that is prefix of reporting address.
1140 run("dmarc@mox.example", report, n)
1141 run("Dmarc-test@mox.example", report, n)
1142 run("dmarc+test@mox.example", report, n)
1144 // Localpart catchall separators work for dmarc reporting addresses too.
1146 run("Dmarc-reports-test@mox.example", report, n)
1149 run("dmarc-Reports+test@mox.example", report, n)
1152const dmarcReport = `<?xml version="1.0" encoding="UTF-8" ?>
1155 <org_name>example.org</org_name>
1156 <email>postmaster@example.org</email>
1157 <report_id>1</report_id>
1159 <begin>1596412800</begin>
1160 <end>1596499199</end>
1164 <domain>xmox.nl</domain>
1173 <source_ip>127.0.0.10</source_ip>
1176 <disposition>none</disposition>
1182 <header_from>xmox.nl</header_from>
1186 <domain>xmox.nl</domain>
1187 <result>pass</result>
1188 <selector>testsel</selector>
1191 <domain>xmox.nl</domain>
1192 <result>pass</result>
1199// Test accepting a TLS report.
1200func TestTLSReport(t *testing.T) {
1201 // Requires setting up DKIM.
1202 privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
1203 dkimRecord := dkim.Record{
1205 Hashes: []string{"sha256"},
1206 Flags: []string{"s"},
1207 PublicKey: privKey.Public(),
1210 dkimTxt, err := dkimRecord.Record()
1211 tcheck(t, err, "dkim record")
1213 sel := config.Selector{
1214 HashEffective: "sha256",
1215 HeadersEffective: []string{"From", "To", "Subject", "Date"},
1217 Domain: dns.Domain{ASCII: "testsel"},
1219 dkimConf := config.DKIM{
1220 Selectors: map[string]config.Selector{"testsel": sel},
1221 Sign: []string{"testsel"},
1224 resolver := &dns.MockResolver{
1225 A: map[string][]string{
1226 "example.org.": {"127.0.0.10"}, // For mx check.
1228 TXT: map[string][]string{
1229 "testsel._domainkey.example.org.": {dkimTxt},
1230 "_dmarc.example.org.": {"v=DMARC1;p=reject;rua=mailto:dmarcrpt@example.org"},
1232 PTR: map[string][]string{
1233 "127.0.0.10": {"example.org."}, // For iprev check.
1236 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/tlsrpt/mox.conf"), resolver)
1239 run := func(rcptTo, tlsrpt string, n int) {
1241 ts.run(func(client *smtpclient.Client) {
1244 mailFrom := "remote@example.org"
1246 msgb := &bytes.Buffer{}
1247 _, 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)
1248 tcheck(t, xerr, "write msg")
1249 msg := msgb.String()
1251 selectors := mox.DKIMSelectors(dkimConf)
1252 headers, xerr := dkim.Sign(ctxbg, pkglog.Logger, "remote", dns.Domain{ASCII: "example.org"}, selectors, false, strings.NewReader(msg))
1253 tcheck(t, xerr, "dkim sign")
1256 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1257 tcheck(t, err, "deliver")
1259 records, err := tlsrptdb.Records(ctxbg)
1260 tcheck(t, err, "tlsrptdb records")
1261 if len(records) != n {
1262 t.Fatalf("got %d tlsrptdb records, expected %d", len(records), n)
1267 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}}]}`
1270 run("tls-reports@mox.example", tlsrpt, n) // Wrong domain in report.
1272 tlsrptdom := strings.ReplaceAll(tlsrpt, "xmox.nl", "mox.example")
1274 run("tls-reports@mox.example", tlsrptdom, n)
1276 tlsrpthost := strings.ReplaceAll(tlsrpt, "xmox.nl", "mailhost.mox.example")
1278 run("tls-reports@mailhost.mox.example", tlsrpthost, n)
1280 // We always store as an evaluation, but as optional for reports.
1281 evals := checkEvaluationCount(t, 3)
1282 tcompare(t, evals[0].Optional, true)
1283 tcompare(t, evals[1].Optional, true)
1284 tcompare(t, evals[2].Optional, true)
1286 // Catchall separators work for reporting address too.
1288 run("Tls-reports+more@mox.example", tlsrptdom, n)
1290 run("tls-Reports-more@mox.example", tlsrptdom, n)
1292 // Non-reporting addresses, mail accepted, but not as report.
1293 run("mjl@mox.example", tlsrptdom, n)
1294 run("Mjl-other@mox.example", tlsrptdom, n)
1295 run("mjl+other@mox.example", tlsrptdom, n)
1296 // Likewise, address that is prefix of reporting address.
1297 run("tls@mox.example", tlsrptdom, n)
1298 run("Tls-other@mox.example", tlsrptdom, n)
1299 run("tls+other@mox.example", tlsrptdom, n)
1302func TestRatelimitConnectionrate(t *testing.T) {
1303 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1306 // We'll be creating 300 connections, no TLS and reduce noise.
1307 ts.tlsmode = smtpclient.TLSSkip
1308 mlog.SetConfig(map[string]slog.Level{"": mlog.LevelInfo})
1309 defer mlog.SetConfig(map[string]slog.Level{"": mlog.LevelDebug})
1311 // We may be passing a window boundary during this tests. The limit is 300/minute.
1312 // So make twice that many connections and hope the tests don't take too long.
1313 for i := 0; i <= 2*300; i++ {
1314 ts.runx(func(err error, client *smtpclient.Client) {
1316 if err != nil && i < 300 {
1317 t.Fatalf("expected smtp connection, got %v", err)
1319 if err == nil && i == 600 {
1320 t.Fatalf("expected no smtp connection due to connection rate limit, got connection")
1329func TestRatelimitAuth(t *testing.T) {
1330 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1333 ts.submission = true
1334 ts.tlsmode = smtpclient.TLSSkip
1338 // We may be passing a window boundary during this tests. The limit is 10 auth
1339 // failures/minute. So make twice that many connections and hope the tests don't
1341 for i := 0; i <= 2*10; i++ {
1342 ts.runx(func(err error, client *smtpclient.Client) {
1345 t.Fatalf("got auth success with bad credentials")
1347 var cerr smtpclient.Error
1348 badauth := errors.As(err, &cerr) && cerr.Code == smtp.C535AuthBadCreds
1349 if !badauth && i < 10 {
1350 t.Fatalf("expected auth failure, got %v", err)
1352 if badauth && i == 20 {
1353 t.Fatalf("expected no smtp connection due to failed auth rate limit, got other error %v", err)
1362func TestRatelimitDelivery(t *testing.T) {
1363 resolver := dns.MockResolver{
1364 A: map[string][]string{
1365 "example.org.": {"127.0.0.10"}, // For mx check.
1367 PTR: map[string][]string{
1368 "127.0.0.10": {"example.org."},
1371 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1374 orig := limitIPMasked1MessagesPerMinute
1375 limitIPMasked1MessagesPerMinute = 1
1377 limitIPMasked1MessagesPerMinute = orig
1380 ts.run(func(client *smtpclient.Client) {
1381 mailFrom := "remote@example.org"
1382 rcptTo := "mjl@mox.example"
1383 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1384 tcheck(t, err, "deliver to remote")
1386 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1387 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C452StorageFull, Secode: smtp.SeMailbox2Full2})
1390 limitIPMasked1MessagesPerMinute = orig
1392 origSize := limitIPMasked1SizePerMinute
1393 // Message was already delivered once. We'll do another one. But the 3rd will fail.
1394 // We need the actual size with prepended headers, since that is used in the
1396 msg, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).Get()
1398 t.Fatalf("getting delivered message for its size: %v", err)
1400 limitIPMasked1SizePerMinute = 2*msg.Size + int64(len(deliverMessage)/2)
1402 limitIPMasked1SizePerMinute = origSize
1404 ts.run(func(client *smtpclient.Client) {
1405 mailFrom := "remote@example.org"
1406 rcptTo := "mjl@mox.example"
1407 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1408 tcheck(t, err, "deliver to remote")
1410 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1411 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C452StorageFull, Secode: smtp.SeMailbox2Full2})
1415func TestNonSMTP(t *testing.T) {
1416 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1420 serverConn, clientConn := net.Pipe()
1421 defer serverConn.Close()
1422 serverdone := make(chan struct{})
1423 defer func() { <-serverdone }()
1426 tlsConfig := &tls.Config{
1427 Certificates: []tls.Certificate{fakeCert(ts.t, false)},
1429 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)
1433 defer clientConn.Close()
1435 buf := make([]byte, 128)
1437 // Read and ignore hello.
1438 if _, err := clientConn.Read(buf); err != nil {
1439 t.Fatalf("reading hello: %v", err)
1442 if _, err := fmt.Fprintf(clientConn, "bogus\r\n"); err != nil {
1443 t.Fatalf("write command: %v", err)
1445 n, err := clientConn.Read(buf)
1447 t.Fatalf("read response line: %v", err)
1449 s := string(buf[:n])
1450 if !strings.HasPrefix(s, "500 5.5.2 ") {
1451 t.Fatalf(`got %q, expected "500 5.5.2 ...`, s)
1453 if _, err := clientConn.Read(buf); err == nil {
1454 t.Fatalf("connection not closed after bogus command")
1458// Test limits on outgoing messages.
1459func TestLimitOutgoing(t *testing.T) {
1460 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpserversendlimit/mox.conf"), dns.MockResolver{})
1463 ts.user = "mjl@mox.example"
1465 ts.submission = true
1467 err := ts.acc.DB.Insert(ctxbg, &store.Outgoing{Recipient: "a@other.example", Submitted: time.Now().Add(-24*time.Hour - time.Minute)})
1468 tcheck(t, err, "inserting outgoing/recipient past 24h window")
1470 testSubmit := func(rcptTo string, expErr *smtpclient.Error) {
1472 ts.run(func(client *smtpclient.Client) {
1474 mailFrom := "mjl@mox.example"
1475 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
1476 ts.smtpErr(err, expErr)
1480 // Limits are set to 4 messages a day, 2 first-time recipients.
1481 testSubmit("b@other.example", nil)
1482 testSubmit("c@other.example", nil)
1483 testSubmit("d@other.example", &smtpclient.Error{Code: smtp.C451LocalErr, Secode: smtp.SePol7DeliveryUnauth1}) // Would be 3rd recipient.
1484 testSubmit("b@other.example", nil)
1485 testSubmit("b@other.example", nil)
1486 testSubmit("b@other.example", &smtpclient.Error{Code: smtp.C451LocalErr, Secode: smtp.SePol7DeliveryUnauth1}) // Would be 5th message.
1489// Test account size limit enforcement.
1490func TestQuota(t *testing.T) {
1491 resolver := dns.MockResolver{
1492 A: map[string][]string{
1493 "other.example.": {"127.0.0.10"}, // For mx check.
1495 PTR: map[string][]string{
1496 "127.0.0.10": {"other.example."},
1499 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpserverquota/mox.conf"), resolver)
1502 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1503 ts.run(func(client *smtpclient.Client) {
1504 mailFrom := "mjl@other.example"
1505 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1506 ts.smtpErr(err, expErr)
1510 testDeliver("mjl@mox.example", &smtpclient.Error{Code: smtp.C452StorageFull, Secode: smtp.SeMailbox2Full2})
1513// Test with catchall destination address.
1514func TestCatchall(t *testing.T) {
1515 resolver := dns.MockResolver{
1516 A: map[string][]string{
1517 "other.example.": {"127.0.0.10"}, // For mx check.
1519 PTR: map[string][]string{
1520 "127.0.0.10": {"other.example."},
1523 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpservercatchall/mox.conf"), resolver)
1526 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1528 ts.run(func(client *smtpclient.Client) {
1530 mailFrom := "mjl@other.example"
1531 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
1532 ts.smtpErr(err, expErr)
1536 testDeliver("mjl@mox.example", nil) // Exact match.
1537 testDeliver("mjl+test@mox.example", nil) // Domain localpart catchall separator.
1538 testDeliver("MJL+TEST@mox.example", nil) // Again, and case insensitive.
1540 n, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).Count()
1541 tcheck(t, err, "checking delivered messages")
1544 testDeliver("unknown@mox.example", nil) // Catchall address, to account catchall.
1546 acc, err := store.OpenAccount(pkglog, "catchall", false)
1547 tcheck(t, err, "open account")
1552 n, err = bstore.QueryDB[store.Message](ctxbg, acc.DB).Count()
1553 tcheck(t, err, "checking delivered messages to catchall account")
1556 testDeliver("mjl-test@mox2.example", nil) // Second catchall separator.
1557 testDeliver("mjl-test+test@mox2.example", nil) // Silly, both separators in address.
1558 testDeliver("mjl+test-test@mox2.example", nil)
1559 n, err = bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).Count()
1560 tcheck(t, err, "checking delivered messages")
1564// Test DKIM signing for outgoing messages.
1565func TestDKIMSign(t *testing.T) {
1566 resolver := dns.MockResolver{
1567 A: map[string][]string{
1568 "mox.example.": {"127.0.0.10"}, // For mx check.
1570 PTR: map[string][]string{
1571 "127.0.0.10": {"mox.example."},
1575 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1578 // Set DKIM signing config.
1580 genDKIM := func(domain string) string {
1581 dom, _ := mox.Conf.Domain(dns.Domain{ASCII: domain})
1583 privkey := make([]byte, ed25519.SeedSize) // Fake key, don't use for real.
1585 privkey[0] = byte(gen)
1587 sel := config.Selector{
1588 HashEffective: "sha256",
1589 HeadersEffective: []string{"From", "To", "Subject"},
1590 Key: ed25519.NewKeyFromSeed(privkey),
1591 Domain: dns.Domain{ASCII: "testsel"},
1593 dom.DKIM = config.DKIM{
1594 Selectors: map[string]config.Selector{"testsel": sel},
1595 Sign: []string{"testsel"},
1597 mox.Conf.Dynamic.Domains[domain] = dom
1598 pubkey := sel.Key.Public().(ed25519.PublicKey)
1599 return "v=DKIM1;k=ed25519;p=" + base64.StdEncoding.EncodeToString(pubkey)
1602 dkimtxt := genDKIM("mox.example")
1603 dkimtxt2 := genDKIM("mox2.example")
1605 // DKIM verify needs to find the key.
1606 resolver.TXT = map[string][]string{
1607 "testsel._domainkey.mox.example.": {dkimtxt},
1608 "testsel._domainkey.mox2.example.": {dkimtxt2},
1611 ts.submission = true
1612 ts.user = "mjl@mox.example"
1616 testSubmit := func(mailFrom, msgFrom string) {
1618 ts.run(func(client *smtpclient.Client) {
1621 msg := strings.ReplaceAll(fmt.Sprintf(`From: <%s>
1622To: <remote@example.org>
1624Message-Id: <test@mox.example>
1627`, msgFrom), "\n", "\r\n")
1629 rcptTo := "remote@example.org"
1630 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1631 tcheck(t, err, "deliver")
1633 msgs, err := queue.List(ctxbg, queue.Filter{}, queue.Sort{})
1634 tcheck(t, err, "listing queue")
1636 tcompare(t, len(msgs), n)
1637 sort.Slice(msgs, func(i, j int) bool {
1638 return msgs[i].ID > msgs[j].ID
1640 f, err := queue.OpenMessage(ctxbg, msgs[0].ID)
1641 tcheck(t, err, "open message in queue")
1643 results, err := dkim.Verify(ctxbg, pkglog.Logger, resolver, false, dkim.DefaultPolicy, f, false)
1644 tcheck(t, err, "verifying dkim message")
1645 tcompare(t, len(results), 1)
1646 tcompare(t, results[0].Status, dkim.StatusPass)
1647 tcompare(t, results[0].Sig.Domain.ASCII, strings.Split(msgFrom, "@")[1])
1651 testSubmit("mjl@mox.example", "mjl@mox.example")
1652 testSubmit("mjl@mox.example", "mjl@mox2.example") // DKIM signature will be for mox2.example.
1655// Test to postmaster addresses.
1656func TestPostmaster(t *testing.T) {
1657 resolver := dns.MockResolver{
1658 A: map[string][]string{
1659 "other.example.": {"127.0.0.10"}, // For mx check.
1661 PTR: map[string][]string{
1662 "127.0.0.10": {"other.example."},
1665 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/postmaster/mox.conf"), resolver)
1668 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1670 ts.run(func(client *smtpclient.Client) {
1672 mailFrom := "mjl@other.example"
1673 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1674 ts.smtpErr(err, expErr)
1678 testDeliver("postmaster", nil) // Plain postmaster address without domain.
1679 testDeliver("postmaster@host.mox.example", nil) // Postmaster address with configured mail server hostname.
1680 testDeliver("postmaster@mox.example", nil) // Postmaster address without explicitly configured destination.
1681 testDeliver("postmaster@unknown.example", &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
1684// Test to address with empty localpart.
1685func TestEmptylocalpart(t *testing.T) {
1686 resolver := dns.MockResolver{
1687 A: map[string][]string{
1688 "other.example.": {"127.0.0.10"}, // For mx check.
1690 PTR: map[string][]string{
1691 "127.0.0.10": {"other.example."},
1694 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1697 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1698 ts.run(func(client *smtpclient.Client) {
1699 mailFrom := `""@other.example`
1700 msg := strings.ReplaceAll(deliverMessage, "To: <mjl@mox.example>", `To: <""@mox.example>`)
1701 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1702 ts.smtpErr(err, expErr)
1706 testDeliver(`""@mox.example`, nil)
1709// Test handling REQUIRETLS and TLS-Required: No.
1710func TestRequireTLS(t *testing.T) {
1711 resolver := dns.MockResolver{
1712 A: map[string][]string{
1713 "mox.example.": {"127.0.0.10"}, // For mx check.
1715 PTR: map[string][]string{
1716 "127.0.0.10": {"mox.example."},
1720 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1723 ts.submission = true
1724 ts.requiretls = true
1725 ts.user = "mjl@mox.example"
1731 msg0 := strings.ReplaceAll(`From: <mjl@mox.example>
1732To: <remote@example.org>
1734Message-Id: <test@mox.example>
1740 msg1 := strings.ReplaceAll(`From: <mjl@mox.example>
1741To: <remote@example.org>
1743Message-Id: <test@mox.example>
1750 msg2 := strings.ReplaceAll(`From: <mjl@mox.example>
1751To: <remote@example.org>
1753Message-Id: <test@mox.example>
1758 testSubmit := func(msg string, requiretls bool, expRequireTLS *bool) {
1760 ts.run(func(client *smtpclient.Client) {
1763 rcptTo := "remote@example.org"
1764 err := client.Deliver(ctxbg, "mjl@mox.example", rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, requiretls)
1765 tcheck(t, err, "deliver")
1767 msgs, err := queue.List(ctxbg, queue.Filter{}, queue.Sort{})
1768 tcheck(t, err, "listing queue")
1769 tcompare(t, len(msgs), 1)
1770 tcompare(t, msgs[0].RequireTLS, expRequireTLS)
1771 _, err = queue.Drop(ctxbg, pkglog, queue.Filter{IDs: []int64{msgs[0].ID}})
1772 tcheck(t, err, "deleting message from queue")
1776 testSubmit(msg0, true, &yes) // Header ignored, requiretls applied.
1777 testSubmit(msg0, false, &no) // TLS-Required header applied.
1778 testSubmit(msg1, true, &yes) // Bad headers ignored, requiretls applied.
1779 testSubmit(msg1, false, nil) // Inconsistent multiple headers ignored.
1780 testSubmit(msg2, false, nil) // Regular message, no RequireTLS setting.
1781 testSubmit(msg2, true, &yes) // Requiretls applied.
1783 // Check that we get an error if remote SMTP server does not support the requiretls
1785 ts.requiretls = false
1786 ts.run(func(client *smtpclient.Client) {
1787 rcptTo := "remote@example.org"
1788 err := client.Deliver(ctxbg, "mjl@mox.example", rcptTo, int64(len(msg0)), strings.NewReader(msg0), false, false, true)
1790 t.Fatalf("delivered with requiretls to server without requiretls")
1792 if !errors.Is(err, smtpclient.ErrRequireTLSUnsupported) {
1793 t.Fatalf("got err %v, expected ErrRequireTLSUnsupported", err)
1798func TestSmuggle(t *testing.T) {
1799 resolver := dns.MockResolver{
1800 A: map[string][]string{
1801 "example.org.": {"127.0.0.10"}, // For mx check.
1803 PTR: map[string][]string{
1804 "127.0.0.10": {"example.org."}, // For iprev check.
1807 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpsmuggle/mox.conf"), resolver)
1808 ts.tlsmode = smtpclient.TLSSkip
1811 test := func(data string) {
1814 ts.runRaw(func(conn net.Conn) {
1817 ourHostname := mox.Conf.Static.HostnameDomain
1818 remoteHostname := dns.Domain{ASCII: "mox.example"}
1819 opts := smtpclient.Opts{
1820 RootCAs: mox.Conf.Static.TLS.CertPool,
1822 log := pkglog.WithCid(ts.cid - 1)
1823 _, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, ts.tlspkix, ourHostname, remoteHostname, opts)
1824 tcheck(t, err, "smtpclient")
1827 write := func(s string) {
1828 _, err := conn.Write([]byte(s))
1829 tcheck(t, err, "write")
1832 readPrefixLine := func(prefix string) string {
1834 buf := make([]byte, 512)
1835 n, err := conn.Read(buf)
1836 tcheck(t, err, "read")
1837 s := strings.TrimRight(string(buf[:n]), "\r\n")
1838 if !strings.HasPrefix(s, prefix) {
1839 t.Fatalf("got smtp response %q, expected line with prefix %q", s, prefix)
1844 write("MAIL FROM:<remote@example.org>\r\n")
1846 write("RCPT TO:<mjl@mox.example>\r\n")
1851 write("\r\n") // Empty header.
1853 write("\r\n.\r\n") // End of message.
1854 line := readPrefixLine("5")
1855 if !strings.Contains(line, "smug") {
1856 t.Errorf("got 5xx error with message %q, expected error text containing smug", line)
1867func TestFutureRelease(t *testing.T) {
1868 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1869 ts.tlsmode = smtpclient.TLSSkip
1870 ts.user = "mjl@mox.example"
1872 ts.submission = true
1875 ts.auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
1876 return sasl.NewClientPlain(ts.user, ts.pass), nil
1879 test := func(mailtoMore, expResponsePrefix string) {
1882 ts.runRaw(func(conn net.Conn) {
1885 ourHostname := mox.Conf.Static.HostnameDomain
1886 remoteHostname := dns.Domain{ASCII: "mox.example"}
1887 opts := smtpclient.Opts{Auth: ts.auth}
1888 log := pkglog.WithCid(ts.cid - 1)
1889 _, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, false, ourHostname, remoteHostname, opts)
1890 tcheck(t, err, "smtpclient")
1893 write := func(s string) {
1894 _, err := conn.Write([]byte(s))
1895 tcheck(t, err, "write")
1898 readPrefixLine := func(prefix string) string {
1900 buf := make([]byte, 512)
1901 n, err := conn.Read(buf)
1902 tcheck(t, err, "read")
1903 s := strings.TrimRight(string(buf[:n]), "\r\n")
1904 if !strings.HasPrefix(s, prefix) {
1905 t.Fatalf("got smtp response %q, expected line with prefix %q", s, prefix)
1910 write(fmt.Sprintf("MAIL FROM:<mjl@mox.example>%s\r\n", mailtoMore))
1911 readPrefixLine(expResponsePrefix)
1912 if expResponsePrefix != "2" {
1915 write("RCPT TO:<mjl@mox.example>\r\n")
1920 write("From: <mjl@mox.example>\r\n\r\nbody\r\n\r\n.\r\n")
1925 test(" HOLDFOR=1", "2")
1926 test(" HOLDUNTIL="+time.Now().Add(time.Minute).UTC().Format(time.RFC3339), "2")
1927 test(" HOLDUNTIL="+time.Now().Add(time.Minute).UTC().Format(time.RFC3339Nano), "2")
1929 test(" HOLDFOR=0", "501") // 0 is invalid syntax.
1930 test(fmt.Sprintf(" HOLDFOR=%d", int64((queue.FutureReleaseIntervalMax+time.Minute)/time.Second)), "554") // Too far in the future.
1931 test(" HOLDUNTIL="+time.Now().Add(-time.Minute).UTC().Format(time.RFC3339), "554") // In the past.
1932 test(" HOLDUNTIL="+time.Now().Add(queue.FutureReleaseIntervalMax+time.Minute).UTC().Format(time.RFC3339), "554") // Too far in the future.
1933 test(" HOLDUNTIL=2024-02-10T17:28:00+00:00", "501") // "Z" required.
1934 test(" HOLDUNTIL=24-02-10T17:28:00Z", "501") // Invalid.
1935 test(" HOLDFOR=1 HOLDFOR=1", "501") // Duplicate.
1936 test(" HOLDFOR=1 HOLDUNTIL="+time.Now().Add(time.Hour).UTC().Format(time.RFC3339), "501") // Duplicate.
1940func TestSMTPUTF8(t *testing.T) {
1941 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1944 ts.user = "mjl@mox.example"
1946 ts.submission = true
1948 test := func(mailFrom string, rcptTo string, headerValue string, filename string, clientSmtputf8 bool, expectedSmtputf8 bool, expErr *smtpclient.Error) {
1951 ts.run(func(client *smtpclient.Client) {
1953 msg := strings.ReplaceAll(fmt.Sprintf(`From: <%s>
1956X-Custom-Test-Header: %s
1958Content-type: multipart/mixed; boundary="simple boundary"
1961Content-Type: text/plain; charset=UTF-8;
1962Content-Disposition: attachment; filename="%s"
1963Content-Transfer-Encoding: base64
1965QW4gYXR0YWNoZWQgdGV4dCBmaWxlLg==
1968`, mailFrom, rcptTo, headerValue, filename), "\n", "\r\n")
1970 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), true, clientSmtputf8, false)
1971 ts.smtpErr(err, expErr)
1976 msgs, _ := queue.List(ctxbg, queue.Filter{}, queue.Sort{Field: "Queued", Asc: false})
1977 queuedMsg := msgs[0]
1978 if queuedMsg.SMTPUTF8 != expectedSmtputf8 {
1979 t.Fatalf("[%s / %s / %s / %s] got SMTPUTF8 %t, expected %t", mailFrom, rcptTo, headerValue, filename, queuedMsg.SMTPUTF8, expectedSmtputf8)
1984 test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", false, false, nil)
1985 test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", true, false, nil)
1986 test(`mjl@mox.example`, `🙂@example.org`, "header-ascii", "ascii.txt", true, true, nil)
1987 test(`mjl@mox.example`, `🙂@example.org`, "header-ascii", "ascii.txt", false, true, &smtpclient.Error{Permanent: true, Code: smtp.C553BadMailbox, Secode: smtp.SeMsg6NonASCIIAddrNotPermitted7})
1988 test(`Ω@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", true, true, nil)
1989 test(`Ω@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", false, true, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeMsg6NonASCIIAddrNotPermitted7})
1990 test(`mjl@mox.example`, `remote@example.org`, "header-utf8-😍", "ascii.txt", true, true, nil)
1991 test(`mjl@mox.example`, `remote@example.org`, "header-utf8-😍", "ascii.txt", false, true, nil)
1992 test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "utf8-🫠️.txt", true, true, nil)
1993 test(`Ω@mox.example`, `🙂@example.org`, "header-utf8-😍", "utf8-🫠️.txt", true, true, nil)
1994 test(`mjl@mox.example`, `remote@xn--vg8h.example.org`, "header-ascii", "ascii.txt", true, false, nil)
1997// TestExtra checks whether submission of messages with "X-Mox-Extra-<key>: value"
1998// headers cause those those key/value pairs to be added to the Extra field in the
2000func TestExtra(t *testing.T) {
2001 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
2004 ts.user = "mjl@mox.example"
2006 ts.submission = true
2008 extraMsg := strings.ReplaceAll(`From: <mjl@mox.example>
2009To: <remote@example.org>
2011X-Mox-Extra-Test: testvalue
2014X-Mox-Extra-x-cANONICAL-z: ok
2015Message-Id: <test@mox.example>
2020 ts.run(func(client *smtpclient.Client) {
2021 mailFrom := "mjl@mox.example"
2022 rcptTo := "mjl@mox.example"
2023 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2024 tcheck(t, err, "deliver")
2026 msgs, err := queue.List(ctxbg, queue.Filter{}, queue.Sort{})
2027 tcheck(t, err, "queue list")
2028 tcompare(t, len(msgs), 1)
2029 tcompare(t, msgs[0].Extra, map[string]string{
2030 "Test": "testvalue",
2033 "X-Canonical-Z": "ok",
2035 // note: these headers currently stay in the message.
2038// TestExtraDup checks for an error for duplicate x-mox-extra-* keys.
2039func TestExtraDup(t *testing.T) {
2040 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
2043 ts.user = "mjl@mox.example"
2045 ts.submission = true
2047 extraMsg := strings.ReplaceAll(`From: <mjl@mox.example>
2048To: <remote@example.org>
2050X-Mox-Extra-Test: testvalue
2051X-Mox-Extra-Test: testvalue
2052Message-Id: <test@mox.example>
2057 ts.run(func(client *smtpclient.Client) {
2058 mailFrom := "mjl@mox.example"
2059 rcptTo := "mjl@mox.example"
2060 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2061 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C554TransactionFailed, Secode: smtp.SeMsg6Other0})
2065// FromID can be specified during submission, but must be unique, with single recipient.
2066func TestUniqueFromID(t *testing.T) {
2067 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpfromid/mox.conf"), dns.MockResolver{})
2070 ts.user = "mjl+fromid@mox.example"
2072 ts.submission = true
2074 extraMsg := strings.ReplaceAll(`From: <mjl@mox.example>
2075To: <remote@example.org>
2081 // Specify our own unique id when queueing.
2082 ts.run(func(client *smtpclient.Client) {
2083 mailFrom := "mjl+unique@mox.example"
2084 rcptTo := "mjl@mox.example"
2085 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2086 ts.smtpErr(err, nil)
2089 // But we can only use it once.
2090 ts.run(func(client *smtpclient.Client) {
2091 mailFrom := "mjl+unique@mox.example"
2092 rcptTo := "mjl@mox.example"
2093 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2094 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C554TransactionFailed, Secode: smtp.SeAddr1SenderSyntax7})
2097 // We cannot use our own fromid with multiple recipients.
2098 ts.run(func(client *smtpclient.Client) {
2099 mailFrom := "mjl+unique2@mox.example"
2100 rcptTo := []string{"mjl@mox.example", "mjl@mox.example"}
2101 _, err := client.DeliverMultiple(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2102 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C554TransactionFailed, Secode: smtp.SeProto5TooManyRcpts3})
2106// TestDestinationSMTPError checks delivery to a destination with an SMTPError is rejected as configured.
2107func TestDestinationSMTPError(t *testing.T) {
2108 resolver := dns.MockResolver{
2109 A: map[string][]string{
2110 "example.org.": {"127.0.0.10"}, // For mx check.
2114 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
2117 ts.run(func(client *smtpclient.Client) {
2118 mailFrom := "mjl@example.org"
2119 rcptTo := "blocked@mox.example"
2120 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
2121 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
2125// TestDestinationMessageAuthRequiredSMTPError checks delivery to a destination
2126// with an MessageAuthRequiredSMTPError is accepted/rejected as configured.
2127func TestDestinationMessageAuthRequiredSMTPError(t *testing.T) {
2128 resolver := dns.MockResolver{
2129 A: map[string][]string{
2130 "example.org.": {"127.0.0.10"}, // For mx check.
2132 PTR: map[string][]string{
2133 "127.0.0.10": {"example.org."},
2135 TXT: map[string][]string{},
2138 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
2141 ts.run(func(client *smtpclient.Client) {
2142 mailFrom := "mjl@example.org"
2143 rcptTo := "msgauthrequired@mox.example"
2144 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
2145 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7MultiAuthFails26})
2148 // Ensure SPF pass, message should now be accepted.
2149 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
2150 ts.run(func(client *smtpclient.Client) {
2151 mailFrom := "mjl@example.org"
2152 rcptTo := "msgauthrequired@mox.example"
2153 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
2154 ts.smtpErr(err, nil)