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"
44var ctxbg = context.Background()
47 // Don't make tests slow.
50 unknownRecipientsDelay = 0
53func tcheck(t *testing.T, err error, msg string) {
56 t.Fatalf("%s: %s", msg, err)
60var submitMessage = strings.ReplaceAll(`From: <mjl@mox.example>
61To: <remote@example.org>
63Message-Id: <test@mox.example>
68var deliverMessage = strings.ReplaceAll(`From: <remote@example.org>
71Message-Id: <test@example.org>
76var deliverMessage2 = strings.ReplaceAll(`From: <remote@example.org>
79Message-Id: <test2@example.org>
84type testserver struct {
91 auth func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error)
94 serverConfig *tls.Config
95 clientConfig *tls.Config
96 clientCert *tls.Certificate // Passed to smtpclient for starttls authentication.
100 tlsmode smtpclient.TLSMode
104const password0 = "te\u0301st \u00a0\u2002\u200a" // NFD and various unicode spaces.
105const password1 = "tést " // PRECIS normalized, with NFC.
107func newTestServer(t *testing.T, configPath string, resolver dns.Resolver) *testserver {
108 limitersInit() // Reset rate limiters.
110 log := mlog.New("smtpserver", nil)
116 tlsmode: smtpclient.TLSOpportunistic,
117 serverConfig: &tls.Config{
118 Certificates: []tls.Certificate{fakeCert(t, false)},
122 // Ensure session keys, for tests that check resume and authentication.
123 ctx, cancel := context.WithCancel(ctxbg)
125 mox.StartTLSSessionTicketKeyRefresher(ctx, log, ts.serverConfig)
128 mox.ConfigStaticPath = configPath
129 mox.MustLoadConfig(true, false)
130 dataDir := mox.ConfigDirPath(mox.Conf.Static.DataDir)
131 os.RemoveAll(dataDir)
133 err := dmarcdb.Init()
134 tcheck(t, err, "dmarcdb init")
135 err = tlsrptdb.Init()
136 tcheck(t, err, "tlsrptdb init")
137 err = store.Init(ctxbg)
138 tcheck(t, err, "store init")
140 ts.acc, err = store.OpenAccount(log, "mjl", false)
141 tcheck(t, err, "open account")
142 err = ts.acc.SetPassword(log, password0)
143 tcheck(t, err, "set password")
145 ts.switchStop = store.Switchboard()
147 tcheck(t, err, "queue init")
149 ts.comm = store.RegisterComm(ts.acc)
154func (ts *testserver) close() {
158 err := dmarcdb.Close()
159 tcheck(ts.t, err, "dmarcdb close")
160 err = tlsrptdb.Close()
161 tcheck(ts.t, err, "tlsrptdb close")
163 tcheck(ts.t, err, "store close")
168 tcheck(ts.t, err, "closing account")
173func (ts *testserver) checkCount(mailboxName string, expect int) {
176 q := bstore.QueryDB[store.Mailbox](ctxbg, ts.acc.DB)
177 q.FilterNonzero(store.Mailbox{Name: mailboxName})
179 tcheck(t, err, "get mailbox")
180 qm := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB)
181 qm.FilterNonzero(store.Message{MailboxID: mb.ID})
182 qm.FilterEqual("Expunged", false)
184 tcheck(t, err, "count messages in mailbox")
186 t.Fatalf("messages in mailbox, found %d, expected %d", n, expect)
190func (ts *testserver) run(fn func(client *smtpclient.Client)) {
192 ts.runx(func(helloErr error, client *smtpclient.Client) {
194 tcheck(ts.t, helloErr, "hello")
199func (ts *testserver) runx(fn func(helloErr error, client *smtpclient.Client)) {
201 ts.runRaw(func(conn net.Conn) {
205 if auth == nil && ts.user != "" {
206 auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
207 return sasl.NewClientPlain(ts.user, ts.pass), nil
211 ourHostname := mox.Conf.Static.HostnameDomain
212 remoteHostname := dns.Domain{ASCII: "mox.example"}
213 opts := smtpclient.Opts{
215 RootCAs: mox.Conf.Static.TLS.CertPool,
216 ClientCert: ts.clientCert,
218 log := pkglog.WithCid(ts.cid - 1)
219 client, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, ts.tlspkix, ourHostname, remoteHostname, opts)
229func (ts *testserver) runRaw(fn func(clientConn net.Conn)) {
234 serverConn, clientConn := net.Pipe()
235 defer serverConn.Close()
236 // clientConn is closed as part of closing client.
237 serverdone := make(chan struct{})
238 defer func() { <-serverdone }()
241 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)
246 clientConn = tls.Client(clientConn, ts.clientConfig)
252func (ts *testserver) smtpErr(err error, expErr *smtpclient.Error) *smtpclient.Error {
255 var cerr smtpclient.Error
256 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) {
257 t.Fatalf("got err:\n%#v (%q)\nexpected:\n%#v", err, err, expErr)
262// Just a cert that appears valid. SMTP client will not verify anything about it
263// (that is opportunistic TLS for you, "better some than none"). Let's enjoy this
264// one moment where it makes life easier.
265func fakeCert(t *testing.T, randomkey bool) tls.Certificate {
266 seed := make([]byte, ed25519.SeedSize)
268 cryptorand.Read(seed)
270 privKey := ed25519.NewKeyFromSeed(seed) // Fake key, don't use this for real!
271 template := &x509.Certificate{
272 SerialNumber: big.NewInt(1), // Required field...
273 // Valid period is needed to get session resumption enabled.
274 NotBefore: time.Now().Add(-time.Minute),
275 NotAfter: time.Now().Add(time.Hour),
277 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
279 t.Fatalf("making certificate: %s", err)
281 cert, err := x509.ParseCertificate(localCertBuf)
283 t.Fatalf("parsing generated certificate: %s", err)
285 c := tls.Certificate{
286 Certificate: [][]byte{localCertBuf},
293// check expected dmarc evaluations for outgoing aggregate reports.
294func checkEvaluationCount(t *testing.T, n int) []dmarcdb.Evaluation {
296 l, err := dmarcdb.Evaluations(ctxbg)
297 tcheck(t, err, "get dmarc evaluations")
298 tcompare(t, len(l), n)
302// Test submission from authenticated user.
303func TestSubmission(t *testing.T) {
304 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
307 // Set DKIM signing config.
308 dom, _ := mox.Conf.Domain(dns.Domain{ASCII: "mox.example"})
309 sel := config.Selector{
310 HashEffective: "sha256",
311 HeadersEffective: []string{"From", "To", "Subject"},
312 Key: ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)), // Fake key, don't use for real.
313 Domain: dns.Domain{ASCII: "mox.example"},
315 dom.DKIM = config.DKIM{
316 Selectors: map[string]config.Selector{"testsel": sel},
317 Sign: []string{"testsel"},
319 mox.Conf.Dynamic.Domains["mox.example"] = dom
321 testAuth := func(authfn func(user, pass string, cs *tls.ConnectionState) sasl.Client, user, pass string, expErr *smtpclient.Error) {
324 ts.auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
325 return authfn(user, pass, cs), nil
330 ts.runx(func(err error, client *smtpclient.Client) {
331 mailFrom := "mjl@mox.example"
332 rcptTo := "remote@example.org"
334 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
336 var cerr smtpclient.Error
337 if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Code != expErr.Code || cerr.Secode != expErr.Secode) {
338 t.Fatalf("got err:\n%#v (%q)\nexpected:\n%#v", err, err, expErr)
340 checkEvaluationCount(t, 0)
344 acc, err := store.OpenAccount(pkglog, "disabled", false)
345 tcheck(t, err, "open account")
346 err = acc.SetPassword(pkglog, "test1234")
347 tcheck(t, err, "set password")
349 tcheck(t, err, "close account")
352 testAuth(nil, "", "", &smtpclient.Error{Permanent: true, Code: smtp.C530SecurityRequired, Secode: smtp.SePol7Other0})
353 authfns := []func(user, pass string, cs *tls.ConnectionState) sasl.Client{
354 func(user, pass string, cs *tls.ConnectionState) sasl.Client { return sasl.NewClientPlain(user, pass) },
355 func(user, pass string, cs *tls.ConnectionState) sasl.Client { return sasl.NewClientLogin(user, pass) },
356 func(user, pass string, cs *tls.ConnectionState) sasl.Client { return sasl.NewClientCRAMMD5(user, pass) },
357 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
358 return sasl.NewClientSCRAMSHA1(user, pass, false)
360 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
361 return sasl.NewClientSCRAMSHA256(user, pass, false)
363 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
364 return sasl.NewClientSCRAMSHA1PLUS(user, pass, *cs)
366 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
367 return sasl.NewClientSCRAMSHA256PLUS(user, pass, *cs)
370 for _, fn := range authfns {
371 testAuth(fn, "mjl@mox.example", "test", &smtpclient.Error{Code: smtp.C535AuthBadCreds, Secode: smtp.SePol7AuthBadCreds8}) // Bad (short) password.
372 testAuth(fn, "mjl@mox.example", password0+"test", &smtpclient.Error{Code: smtp.C535AuthBadCreds, Secode: smtp.SePol7AuthBadCreds8}) // Bad password.
373 testAuth(fn, "mjl@mox.example", password0, nil)
374 testAuth(fn, "mjl@mox.example", password1, nil)
375 testAuth(fn, "móx@mox.example", password0, nil)
376 testAuth(fn, "móx@mox.example", password1, nil)
377 testAuth(fn, "mo\u0301x@mox.example", password0, nil)
378 testAuth(fn, "mo\u0301x@mox.example", password1, nil)
379 testAuth(fn, "disabled@mox.example", "test1234", &smtpclient.Error{Code: smtp.C525AccountDisabled, Secode: smtp.SePol7AccountDisabled13})
380 testAuth(fn, "disabled@mox.example", "bogus", &smtpclient.Error{Code: smtp.C535AuthBadCreds, Secode: smtp.SePol7AuthBadCreds8})
383 // Create a certificate, register its public key with account, and make a tls
384 // client config that sends the certificate.
385 clientCert0 := fakeCert(ts.t, true)
386 tlspubkey, err := store.ParseTLSPublicKeyCert(clientCert0.Certificate[0])
387 tcheck(t, err, "parse certificate")
388 tlspubkey.Account = "mjl"
389 tlspubkey.LoginAddress = "mjl@mox.example"
390 err = store.TLSPublicKeyAdd(ctxbg, &tlspubkey)
391 tcheck(t, err, "add tls public key to account")
392 ts.immediateTLS = true
393 ts.clientConfig = &tls.Config{
394 InsecureSkipVerify: true,
395 Certificates: []tls.Certificate{
400 // No explicit address in EXTERNAL.
401 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
402 return sasl.NewClientExternal(user)
405 // Same username in EXTERNAL as configured for key.
406 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
407 return sasl.NewClientExternal(user)
408 }, "mjl@mox.example", "", nil)
410 // Different username in EXTERNAL as configured for key, but same account.
411 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
412 return sasl.NewClientExternal(user)
413 }, "móx@mox.example", "", nil)
415 // Different username as configured for key, but same account, but not EXTERNAL auth.
416 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
417 return sasl.NewClientSCRAMSHA256PLUS(user, pass, *cs)
418 }, "móx@mox.example", password0, nil)
420 // Different account results in error.
421 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
422 return sasl.NewClientExternal(user)
423 }, "☺@mox.example", "", &smtpclient.Error{Code: smtp.C535AuthBadCreds, Secode: smtp.SePol7AuthBadCreds8})
425 // Starttls with client cert should authenticate too.
426 ts.immediateTLS = false
427 ts.clientCert = &clientCert0
428 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
429 return sasl.NewClientExternal(user)
431 ts.immediateTLS = true
434 // Add a client session cache, so our connections will be resumed. We are testing
435 // that the credentials are applied to resumed connections too.
436 ts.clientConfig.ClientSessionCache = tls.NewLRUClientSessionCache(10)
437 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
439 panic("tls connection was resumed")
441 return sasl.NewClientExternal(user)
443 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
445 panic("tls connection was not resumed")
447 return sasl.NewClientExternal(user)
450 // Unknown client certificate should fail the connection.
451 serverConn, clientConn := net.Pipe()
452 serverdone := make(chan struct{})
453 defer func() { <-serverdone }()
456 defer serverConn.Close()
457 tlsConfig := &tls.Config{
458 Certificates: []tls.Certificate{fakeCert(ts.t, false)},
460 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)
464 defer clientConn.Close()
466 // Authentication with an unknown/untrusted certificate should fail.
467 clientCert1 := fakeCert(ts.t, true)
468 ts.clientConfig.ClientSessionCache = nil
469 ts.clientConfig.Certificates = []tls.Certificate{
472 clientConn = tls.Client(clientConn, ts.clientConfig)
473 // note: It's not enough to do a handshake and check if that was successful. If the
474 // client cert is not acceptable, we only learn after the handshake, when the first
475 // data messages are exchanged.
476 buf := make([]byte, 100)
477 _, err = clientConn.Read(buf)
479 t.Fatalf("tls handshake with unknown client certificate succeeded")
481 if alert, ok := mox.AsTLSAlert(err); !ok || alert != 42 {
482 t.Fatalf("got err %#v, expected tls 'bad certificate' alert", err)
486func TestDomainDisabled(t *testing.T) {
487 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
491 ts.user = "mjl@mox.example"
494 // Submission with SMTP MAIL FROM of disabled domain must fail.
495 ts.run(func(client *smtpclient.Client) {
496 mailFrom := "mjl@disabled.example" // Disabled.
497 rcptTo := "remote@example.org"
498 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
499 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
500 checkEvaluationCount(t, 0)
503 // Message From-address has disabled domain, must fail.
504 var submitMessage2 = strings.ReplaceAll(`From: <mjl@disabled.example>
505To: <remote@example.org>
507Message-Id: <test@mox.example>
511 ts.run(func(client *smtpclient.Client) {
512 mailFrom := "mjl@mox.example"
513 rcptTo := "remote@example.org"
514 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage2)), strings.NewReader(submitMessage2), false, false, false)
515 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
516 checkEvaluationCount(t, 0)
520// Test delivery from external MTA.
521func TestDelivery(t *testing.T) {
522 resolver := dns.MockResolver{
523 A: map[string][]string{
524 "example.org.": {"127.0.0.10"}, // For mx check.
526 PTR: map[string][]string{},
528 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
531 ts.run(func(client *smtpclient.Client) {
532 mailFrom := "remote@example.org"
533 rcptTo := "mjl@127.0.0.10"
534 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
535 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
538 ts.run(func(client *smtpclient.Client) {
539 mailFrom := "remote@example.org"
540 rcptTo := "mjl@test.example" // Not configured as destination.
541 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
542 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
545 ts.run(func(client *smtpclient.Client) {
546 mailFrom := "remote@example.org"
547 rcptTo := "unknown@mox.example" // User unknown.
548 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
549 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
552 ts.run(func(client *smtpclient.Client) {
553 mailFrom := "remote@example.org"
554 rcptTo := "mjl@mox.example"
555 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
556 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
559 // Set up iprev to get delivery from unknown user to be accepted.
560 resolver.PTR["127.0.0.10"] = []string{"example.org."}
562 // Only ascii o@ is configured, not the greek and cyrillic lookalikes.
563 ts.run(func(client *smtpclient.Client) {
564 mailFrom := "remote@example.org"
565 rcptTo := "ο@mox.example" // omicron \u03bf, looks like the configured o@
566 msg := strings.ReplaceAll(deliverMessage, "mjl@mox.example", rcptTo)
567 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, true, false)
568 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
571 // Deliveries to disabled domain are rejected with temporary error.
572 ts.run(func(client *smtpclient.Client) {
573 mailFrom := "remote@example.org"
574 rcptTo := "mjl@disabled.example"
575 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
576 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C450MailboxUnavail, Secode: smtp.SeMailbox2Disabled1})
579 ts.run(func(client *smtpclient.Client) {
580 recipients := []string{
582 "o@mox.example", // ascii o, as configured
583 "\u2126@mox.example", // ohm sign, as configured
584 "ω@mox.example", // lower-case omega, we match case-insensitively and this is the lowercase of ohm (!)
585 "\u03a9@mox.example", // capital omega, also lowercased to omega.
586 "móx@mox.example", // NFC
587 "mo\u0301x@mox.example", // not NFC, but normalized as móx@, see https://go.dev/blog/normalization
590 for _, rcptTo := range recipients {
591 // Ensure SMTP RCPT TO and message address headers are the same, otherwise the junk
592 // filter treats us more strictly.
593 msg := strings.ReplaceAll(deliverMessage, "mjl@mox.example", rcptTo)
595 mailFrom := "remote@example.org"
596 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, true, false)
597 tcheck(t, err, "deliver to remote")
599 changes := make(chan []store.Change)
601 changes <- ts.comm.Get()
604 timer := time.NewTimer(time.Second)
609 t.Fatalf("no delivery in 1s")
614 checkEvaluationCount(t, 0)
617func tinsertmsg(t *testing.T, acc *store.Account, mailbox string, m *store.Message, msg string) {
618 mf, err := store.CreateMessageTemp(pkglog, "queue-dsn")
619 tcheck(t, err, "temp message")
620 defer os.Remove(mf.Name())
622 _, err = mf.Write([]byte(msg))
623 tcheck(t, err, "write message")
624 err = acc.DeliverMailbox(pkglog, mailbox, m, mf)
625 tcheck(t, err, "deliver message")
627 tcheck(t, err, "close message")
630func tretrain(t *testing.T, acc *store.Account) {
633 // Fresh empty junkfilter.
634 basePath := mox.DataDirPath("accounts")
635 dbPath := filepath.Join(basePath, acc.Name, "junkfilter.db")
636 bloomPath := filepath.Join(basePath, acc.Name, "junkfilter.bloom")
639 jf, _, err := acc.OpenJunkFilter(ctxbg, pkglog)
640 tcheck(t, err, "open junk filter")
643 // Fetch messags to retrain on.
644 q := bstore.QueryDB[store.Message](ctxbg, acc.DB)
645 q.FilterEqual("Expunged", false)
646 q.FilterFn(func(m store.Message) bool {
647 return m.Flags.Junk || m.Flags.Notjunk
649 msgs, err := q.List()
650 tcheck(t, err, "fetch messages")
652 // Retrain the messages.
653 for _, m := range msgs {
654 ham := m.Flags.Notjunk
656 f, err := os.Open(acc.MessagePath(m.ID))
657 tcheck(t, err, "open message")
658 r := store.FileMsgReader(m.MsgPrefix, f)
660 jf.TrainMessage(ctxbg, r, m.Size, ham)
663 tcheck(t, err, "close message")
667 tcheck(t, err, "save junkfilter")
670// Test accept/reject with DMARC reputation and with spammy content.
671func TestSpam(t *testing.T) {
672 resolver := &dns.MockResolver{
673 A: map[string][]string{
674 "example.org.": {"127.0.0.1"}, // For mx check.
676 TXT: map[string][]string{
677 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
678 "_dmarc.example.org.": {"v=DMARC1;p=reject; rua=mailto:dmarcrpt@example.org"},
681 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
684 // Insert spammy messages. No junkfilter training yet.
686 RemoteIP: "127.0.0.10",
687 RemoteIPMasked1: "127.0.0.10",
688 RemoteIPMasked2: "127.0.0.0",
689 RemoteIPMasked3: "127.0.0.0",
690 MailFrom: "remote@example.org",
691 MailFromLocalpart: smtp.Localpart("remote"),
692 MailFromDomain: "example.org",
693 RcptToLocalpart: smtp.Localpart("mjl"),
694 RcptToDomain: "mox.example",
695 MsgFromLocalpart: smtp.Localpart("remote"),
696 MsgFromDomain: "example.org",
697 MsgFromOrgDomain: "example.org",
698 MsgFromValidated: true,
699 MsgFromValidation: store.ValidationStrict,
700 Flags: store.Flags{Seen: true, Junk: true},
701 Size: int64(len(deliverMessage)),
703 for i := 0; i < 3; i++ {
705 tinsertmsg(t, ts.acc, "Inbox", &nm, deliverMessage)
707 tinsertmsg(t, ts.acc, "mjl2", &nm, deliverMessage)
710 // Delivery from sender with bad reputation should fail.
711 ts.run(func(client *smtpclient.Client) {
712 mailFrom := "remote@example.org"
713 rcptTo := "mjl@mox.example"
714 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
715 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
717 ts.checkCount("Rejects", 1)
718 checkEvaluationCount(t, 0) // No positive interactions yet.
721 // Delivery from sender with bad reputation matching AcceptRejectsToMailbox should
722 // result in accepted delivery to the mailbox.
723 ts.run(func(client *smtpclient.Client) {
724 mailFrom := "remote@example.org"
725 rcptTo := "mjl2@mox.example"
726 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage2)), strings.NewReader(deliverMessage2), false, false, false)
727 tcheck(t, err, "deliver")
729 ts.checkCount("mjl2junk", 1) // In ruleset rejects mailbox.
730 ts.checkCount("Rejects", 1) // Same as before.
731 checkEvaluationCount(t, 0) // This is not an actual accept.
734 // Mark the messages as having good reputation.
735 q := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB)
736 q.FilterEqual("Expunged", false)
737 _, err := q.UpdateFields(map[string]any{"Junk": false, "Notjunk": true})
738 tcheck(t, err, "update junkiness")
740 // Message should now be accepted.
741 ts.run(func(client *smtpclient.Client) {
742 tcheck(t, err, "hello")
743 mailFrom := "remote@example.org"
744 rcptTo := "mjl@mox.example"
745 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
746 tcheck(t, err, "deliver")
748 // Message should now be removed from Rejects mailboxes.
749 ts.checkCount("Rejects", 0)
750 ts.checkCount("mjl2junk", 1)
751 checkEvaluationCount(t, 1)
754 // Undo dmarc pass, mark messages as junk, and train the filter.
756 q = bstore.QueryDB[store.Message](ctxbg, ts.acc.DB)
757 q.FilterEqual("Expunged", false)
758 _, err = q.UpdateFields(map[string]any{"Junk": true, "Notjunk": false})
759 tcheck(t, err, "update junkiness")
762 // Message should be refused for spammy content.
763 ts.run(func(client *smtpclient.Client) {
764 tcheck(t, err, "hello")
765 mailFrom := "remote@example.org"
766 rcptTo := "mjl@mox.example"
767 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
768 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
769 checkEvaluationCount(t, 1) // No new evaluation, this isn't a DMARC reject.
773// Test accept/reject with forwarded messages, DMARC ignored, no IP/EHLO/MAIL
774// FROM-based reputation.
775func TestForward(t *testing.T) {
776 // Do a run without forwarding, and with.
777 check := func(forward bool) {
779 resolver := &dns.MockResolver{
780 A: map[string][]string{
781 "bad.example.": {"127.0.0.1"}, // For mx check.
782 "good.example.": {"127.0.0.1"}, // For mx check.
783 "forward.example.": {"127.0.0.10"}, // For mx check.
785 TXT: map[string][]string{
786 "bad.example.": {"v=spf1 ip4:127.0.0.1 -all"},
787 "good.example.": {"v=spf1 ip4:127.0.0.1 -all"},
788 "forward.example.": {"v=spf1 ip4:127.0.0.10 -all"},
789 "_dmarc.bad.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@bad.example"},
790 "_dmarc.good.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@good.example"},
791 "_dmarc.forward.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@forward.example"},
793 PTR: map[string][]string{
794 "127.0.0.10": {"forward.example."}, // For iprev check.
797 rcptTo := "mjl3@mox.example"
799 // For SPF and DMARC pass, otherwise the test ends quickly.
800 resolver.TXT["bad.example."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
801 resolver.TXT["good.example."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
802 rcptTo = "mjl@mox.example" // Without IsForward rule.
805 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
808 totalEvaluations := 0
810 var msgBad = strings.ReplaceAll(`From: <remote@bad.example>
813Message-Id: <bad@example.org>
817 var msgOK = strings.ReplaceAll(`From: <remote@good.example>
820Message-Id: <good@example.org>
824 var msgOK2 = strings.ReplaceAll(`From: <other@forward.example>
827Message-Id: <regular@example.org>
829happens to come from forwarding mail server.
832 // Deliver forwarded messages, then classify as junk. Normally enough to treat
833 // other unrelated messages from IP as junk, but not for forwarded messages.
834 ts.run(func(client *smtpclient.Client) {
835 mailFrom := "remote@forward.example"
837 mailFrom = "remote@bad.example"
840 for i := 0; i < 10; i++ {
841 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgBad)), strings.NewReader(msgBad), false, false, false)
842 tcheck(t, err, "deliver message")
844 totalEvaluations += 10
846 n, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).UpdateFields(map[string]any{"Junk": true, "MsgFromValidated": true})
847 tcheck(t, err, "marking messages as junk")
850 // Next delivery will fail, with negative "message From" signal.
851 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgBad)), strings.NewReader(msgBad), false, false, false)
852 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
854 checkEvaluationCount(t, totalEvaluations)
857 // Delivery from different "message From" without reputation, but from same
858 // forwarding email server, should succeed under forwarding, not as regular sending
860 ts.run(func(client *smtpclient.Client) {
861 mailFrom := "remote@forward.example"
863 mailFrom = "remote@good.example"
866 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgOK)), strings.NewReader(msgOK), false, false, false)
868 tcheck(t, err, "deliver")
869 totalEvaluations += 1
871 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
873 checkEvaluationCount(t, totalEvaluations)
876 // Delivery from forwarding server that isn't a forward should get same treatment.
877 ts.run(func(client *smtpclient.Client) {
878 mailFrom := "other@forward.example"
880 // Ensure To header matches.
883 msg = strings.ReplaceAll(msg, "<mjl@mox.example>", "<mjl3@mox.example>")
886 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
888 tcheck(t, err, "deliver")
889 totalEvaluations += 1
891 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
893 checkEvaluationCount(t, totalEvaluations)
901// Messages that we sent to, that have passing DMARC, but that are otherwise spammy, should be accepted.
902func TestDMARCSent(t *testing.T) {
903 resolver := &dns.MockResolver{
904 A: map[string][]string{
905 "example.org.": {"127.0.0.1"}, // For mx check.
907 TXT: map[string][]string{
908 "example.org.": {"v=spf1 ip4:127.0.0.1 -all"},
909 "_dmarc.example.org.": {"v=DMARC1;p=reject;rua=mailto:dmarcrpt@example.org"},
912 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
915 // First check that DMARC policy rejects message and results in optional evaluation.
916 ts.run(func(client *smtpclient.Client) {
917 mailFrom := "remote@example.org"
918 rcptTo := "mjl@mox.example"
919 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
920 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7MultiAuthFails26})
921 l := checkEvaluationCount(t, 1)
922 tcompare(t, l[0].Optional, true)
925 // Update DNS for an SPF pass, and DMARC pass.
926 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
928 // Insert hammy & spammy messages not related to the test message.
930 MailFrom: "remote@test.example",
931 RcptToLocalpart: smtp.Localpart("mjl"),
932 RcptToDomain: "mox.example",
933 Flags: store.Flags{Seen: true},
934 Size: int64(len(deliverMessage)),
936 // We need at least 50 ham messages for the junk filter to become significant. We
937 // offset it with negative messages for mediocre score.
938 for i := 0; i < 50; i++ {
941 tinsertmsg(t, ts.acc, "Archive", &nm, deliverMessage)
944 tinsertmsg(t, ts.acc, "Archive", &nm, deliverMessage)
948 // Baseline, message should be refused for spammy content.
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: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
954 checkEvaluationCount(t, 1) // No new evaluation.
957 // Insert a message that we sent to the address that is about to send to us.
958 sentMsg := store.Message{Size: int64(len(deliverMessage))}
959 tinsertmsg(t, ts.acc, "Sent", &sentMsg, deliverMessage)
960 err := ts.acc.DB.Insert(ctxbg, &store.Recipient{MessageID: sentMsg.ID, Localpart: "remote", Domain: "example.org", OrgDomain: "example.org", Sent: time.Now()})
961 tcheck(t, err, "inserting message recipient")
963 // Reject a message due to DMARC again. Since we sent a message to the domain, it
964 // is no longer unknown and we should see a non-optional evaluation that will
965 // result in a DMARC report.
966 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.1 -all"}
967 ts.run(func(client *smtpclient.Client) {
968 mailFrom := "remote@example.org"
969 rcptTo := "mjl@mox.example"
970 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
971 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7MultiAuthFails26})
972 l := checkEvaluationCount(t, 2) // New evaluation.
973 tcompare(t, l[1].Optional, false)
976 // We should now be accepting the message because we recently sent a message.
977 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
978 ts.run(func(client *smtpclient.Client) {
979 mailFrom := "remote@example.org"
980 rcptTo := "mjl@mox.example"
981 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
982 tcheck(t, err, "deliver")
983 l := checkEvaluationCount(t, 3) // New evaluation.
984 tcompare(t, l[2].Optional, false)
988// Test DNSBL, then getting through with subjectpass.
989func TestBlocklistedSubjectpass(t *testing.T) {
990 // Set up a DNSBL on dnsbl.example, and get DMARC pass.
991 resolver := &dns.MockResolver{
992 A: map[string][]string{
993 "example.org.": {"127.0.0.10"}, // For mx check.
994 "2.0.0.127.dnsbl.example.": {"127.0.0.2"}, // For healthcheck.
995 "10.0.0.127.dnsbl.example.": {"127.0.0.10"}, // Where our connection pretends to come from.
997 TXT: map[string][]string{
998 "10.0.0.127.dnsbl.example.": {"blocklisted"},
999 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
1000 "_dmarc.example.org.": {"v=DMARC1;p=reject"},
1002 PTR: map[string][]string{
1003 "127.0.0.10": {"example.org."}, // For iprev check.
1006 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1007 ts.dnsbls = []dns.Domain{{ASCII: "dnsbl.example"}}
1010 // Message should be refused softly (temporary error) due to DNSBL.
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 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
1018 // Set up subjectpass on account.
1019 acc := mox.Conf.Dynamic.Accounts[ts.acc.Name]
1020 acc.SubjectPass.Period = time.Hour
1021 mox.Conf.Dynamic.Accounts[ts.acc.Name] = acc
1023 // Message should be refused quickly (permanent error) due to DNSBL and Subjectkey.
1025 ts.run(func(client *smtpclient.Client) {
1026 mailFrom := "remote@example.org"
1027 rcptTo := "mjl@mox.example"
1028 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1029 cerr := ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7DeliveryUnauth1})
1030 i := strings.Index(cerr.Line, subjectpass.Explanation)
1032 t.Fatalf("got error line %q, expected error line with subjectpass", cerr.Line)
1034 pass = cerr.Line[i+len(subjectpass.Explanation):]
1037 ts.run(func(client *smtpclient.Client) {
1038 mailFrom := "remote@example.org"
1039 rcptTo := "mjl@mox.example"
1040 passMessage := strings.Replace(deliverMessage, "Subject: test", "Subject: test "+pass, 1)
1041 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(passMessage)), strings.NewReader(passMessage), false, false, false)
1042 tcheck(t, err, "deliver with subjectpass")
1046// Test accepting a DMARC report.
1047func TestDMARCReport(t *testing.T) {
1048 resolver := &dns.MockResolver{
1049 A: map[string][]string{
1050 "example.org.": {"127.0.0.10"}, // For mx check.
1052 TXT: map[string][]string{
1053 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
1054 "_dmarc.example.org.": {"v=DMARC1;p=reject; rua=mailto:dmarcrpt@example.org"},
1056 PTR: map[string][]string{
1057 "127.0.0.10": {"example.org."}, // For iprev check.
1060 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/dmarcreport/mox.conf"), resolver)
1063 run := func(report string, n int) {
1065 ts.run(func(client *smtpclient.Client) {
1068 mailFrom := "remote@example.org"
1069 rcptTo := "mjl@mox.example"
1071 msgb := &bytes.Buffer{}
1072 _, 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)
1073 tcheck(t, xerr, "write msg headers")
1074 w := quotedprintable.NewWriter(msgb)
1075 _, xerr = w.Write([]byte(strings.ReplaceAll(report, "\n", "\r\n")))
1076 tcheck(t, xerr, "write message")
1077 msg := msgb.String()
1079 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1080 tcheck(t, err, "deliver")
1082 records, err := dmarcdb.Records(ctxbg)
1083 tcheck(t, err, "dmarcdb records")
1084 if len(records) != n {
1085 t.Fatalf("got %d dmarcdb records, expected %d or more", len(records), n)
1091 run(strings.ReplaceAll(dmarcReport, "xmox.nl", "mox.example"), 1)
1093 // We always store as an evaluation, but as optional for reports.
1094 evals := checkEvaluationCount(t, 2)
1095 tcompare(t, evals[0].Optional, true)
1096 tcompare(t, evals[1].Optional, true)
1099const dmarcReport = `<?xml version="1.0" encoding="UTF-8" ?>
1102 <org_name>example.org</org_name>
1103 <email>postmaster@example.org</email>
1104 <report_id>1</report_id>
1106 <begin>1596412800</begin>
1107 <end>1596499199</end>
1111 <domain>xmox.nl</domain>
1120 <source_ip>127.0.0.10</source_ip>
1123 <disposition>none</disposition>
1129 <header_from>xmox.nl</header_from>
1133 <domain>xmox.nl</domain>
1134 <result>pass</result>
1135 <selector>testsel</selector>
1138 <domain>xmox.nl</domain>
1139 <result>pass</result>
1146// Test accepting a TLS report.
1147func TestTLSReport(t *testing.T) {
1148 // Requires setting up DKIM.
1149 privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
1150 dkimRecord := dkim.Record{
1152 Hashes: []string{"sha256"},
1153 Flags: []string{"s"},
1154 PublicKey: privKey.Public(),
1157 dkimTxt, err := dkimRecord.Record()
1158 tcheck(t, err, "dkim record")
1160 sel := config.Selector{
1161 HashEffective: "sha256",
1162 HeadersEffective: []string{"From", "To", "Subject", "Date"},
1164 Domain: dns.Domain{ASCII: "testsel"},
1166 dkimConf := config.DKIM{
1167 Selectors: map[string]config.Selector{"testsel": sel},
1168 Sign: []string{"testsel"},
1171 resolver := &dns.MockResolver{
1172 A: map[string][]string{
1173 "example.org.": {"127.0.0.10"}, // For mx check.
1175 TXT: map[string][]string{
1176 "testsel._domainkey.example.org.": {dkimTxt},
1177 "_dmarc.example.org.": {"v=DMARC1;p=reject;rua=mailto:dmarcrpt@example.org"},
1179 PTR: map[string][]string{
1180 "127.0.0.10": {"example.org."}, // For iprev check.
1183 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/tlsrpt/mox.conf"), resolver)
1186 run := func(rcptTo, tlsrpt string, n int) {
1188 ts.run(func(client *smtpclient.Client) {
1191 mailFrom := "remote@example.org"
1193 msgb := &bytes.Buffer{}
1194 _, 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)
1195 tcheck(t, xerr, "write msg")
1196 msg := msgb.String()
1198 selectors := mox.DKIMSelectors(dkimConf)
1199 headers, xerr := dkim.Sign(ctxbg, pkglog.Logger, "remote", dns.Domain{ASCII: "example.org"}, selectors, false, strings.NewReader(msg))
1200 tcheck(t, xerr, "dkim sign")
1203 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1204 tcheck(t, err, "deliver")
1206 records, err := tlsrptdb.Records(ctxbg)
1207 tcheck(t, err, "tlsrptdb records")
1208 if len(records) != n {
1209 t.Fatalf("got %d tlsrptdb records, expected %d", len(records), n)
1214 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}}]}`
1216 run("mjl@mox.example", tlsrpt, 0)
1217 run("mjl@mox.example", strings.ReplaceAll(tlsrpt, "xmox.nl", "mox.example"), 1)
1218 run("mjl@mailhost.mox.example", strings.ReplaceAll(tlsrpt, "xmox.nl", "mailhost.mox.example"), 2)
1220 // We always store as an evaluation, but as optional for reports.
1221 evals := checkEvaluationCount(t, 3)
1222 tcompare(t, evals[0].Optional, true)
1223 tcompare(t, evals[1].Optional, true)
1224 tcompare(t, evals[2].Optional, true)
1227func TestRatelimitConnectionrate(t *testing.T) {
1228 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1231 // We'll be creating 300 connections, no TLS and reduce noise.
1232 ts.tlsmode = smtpclient.TLSSkip
1233 mlog.SetConfig(map[string]slog.Level{"": mlog.LevelInfo})
1234 defer mlog.SetConfig(map[string]slog.Level{"": mlog.LevelDebug})
1236 // We may be passing a window boundary during this tests. The limit is 300/minute.
1237 // So make twice that many connections and hope the tests don't take too long.
1238 for i := 0; i <= 2*300; i++ {
1239 ts.runx(func(err error, client *smtpclient.Client) {
1241 if err != nil && i < 300 {
1242 t.Fatalf("expected smtp connection, got %v", err)
1244 if err == nil && i == 600 {
1245 t.Fatalf("expected no smtp connection due to connection rate limit, got connection")
1254func TestRatelimitAuth(t *testing.T) {
1255 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1258 ts.submission = true
1259 ts.tlsmode = smtpclient.TLSSkip
1263 // We may be passing a window boundary during this tests. The limit is 10 auth
1264 // failures/minute. So make twice that many connections and hope the tests don't
1266 for i := 0; i <= 2*10; i++ {
1267 ts.runx(func(err error, client *smtpclient.Client) {
1270 t.Fatalf("got auth success with bad credentials")
1272 var cerr smtpclient.Error
1273 badauth := errors.As(err, &cerr) && cerr.Code == smtp.C535AuthBadCreds
1274 if !badauth && i < 10 {
1275 t.Fatalf("expected auth failure, got %v", err)
1277 if badauth && i == 20 {
1278 t.Fatalf("expected no smtp connection due to failed auth rate limit, got other error %v", err)
1287func TestRatelimitDelivery(t *testing.T) {
1288 resolver := dns.MockResolver{
1289 A: map[string][]string{
1290 "example.org.": {"127.0.0.10"}, // For mx check.
1292 PTR: map[string][]string{
1293 "127.0.0.10": {"example.org."},
1296 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1299 orig := limitIPMasked1MessagesPerMinute
1300 limitIPMasked1MessagesPerMinute = 1
1302 limitIPMasked1MessagesPerMinute = orig
1305 ts.run(func(client *smtpclient.Client) {
1306 mailFrom := "remote@example.org"
1307 rcptTo := "mjl@mox.example"
1308 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1309 tcheck(t, err, "deliver to remote")
1311 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1312 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C452StorageFull, Secode: smtp.SeMailbox2Full2})
1315 limitIPMasked1MessagesPerMinute = orig
1317 origSize := limitIPMasked1SizePerMinute
1318 // Message was already delivered once. We'll do another one. But the 3rd will fail.
1319 // We need the actual size with prepended headers, since that is used in the
1321 msg, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).Get()
1323 t.Fatalf("getting delivered message for its size: %v", err)
1325 limitIPMasked1SizePerMinute = 2*msg.Size + int64(len(deliverMessage)/2)
1327 limitIPMasked1SizePerMinute = origSize
1329 ts.run(func(client *smtpclient.Client) {
1330 mailFrom := "remote@example.org"
1331 rcptTo := "mjl@mox.example"
1332 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1333 tcheck(t, err, "deliver to remote")
1335 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1336 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C452StorageFull, Secode: smtp.SeMailbox2Full2})
1340func TestNonSMTP(t *testing.T) {
1341 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1345 serverConn, clientConn := net.Pipe()
1346 defer serverConn.Close()
1347 serverdone := make(chan struct{})
1348 defer func() { <-serverdone }()
1351 tlsConfig := &tls.Config{
1352 Certificates: []tls.Certificate{fakeCert(ts.t, false)},
1354 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)
1358 defer clientConn.Close()
1360 buf := make([]byte, 128)
1362 // Read and ignore hello.
1363 if _, err := clientConn.Read(buf); err != nil {
1364 t.Fatalf("reading hello: %v", err)
1367 if _, err := fmt.Fprintf(clientConn, "bogus\r\n"); err != nil {
1368 t.Fatalf("write command: %v", err)
1370 n, err := clientConn.Read(buf)
1372 t.Fatalf("read response line: %v", err)
1374 s := string(buf[:n])
1375 if !strings.HasPrefix(s, "500 5.5.2 ") {
1376 t.Fatalf(`got %q, expected "500 5.5.2 ...`, s)
1378 if _, err := clientConn.Read(buf); err == nil {
1379 t.Fatalf("connection not closed after bogus command")
1383// Test limits on outgoing messages.
1384func TestLimitOutgoing(t *testing.T) {
1385 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpserversendlimit/mox.conf"), dns.MockResolver{})
1388 ts.user = "mjl@mox.example"
1390 ts.submission = true
1392 err := ts.acc.DB.Insert(ctxbg, &store.Outgoing{Recipient: "a@other.example", Submitted: time.Now().Add(-24*time.Hour - time.Minute)})
1393 tcheck(t, err, "inserting outgoing/recipient past 24h window")
1395 testSubmit := func(rcptTo string, expErr *smtpclient.Error) {
1397 ts.run(func(client *smtpclient.Client) {
1399 mailFrom := "mjl@mox.example"
1400 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
1401 ts.smtpErr(err, expErr)
1405 // Limits are set to 4 messages a day, 2 first-time recipients.
1406 testSubmit("b@other.example", nil)
1407 testSubmit("c@other.example", nil)
1408 testSubmit("d@other.example", &smtpclient.Error{Code: smtp.C451LocalErr, Secode: smtp.SePol7DeliveryUnauth1}) // Would be 3rd recipient.
1409 testSubmit("b@other.example", nil)
1410 testSubmit("b@other.example", nil)
1411 testSubmit("b@other.example", &smtpclient.Error{Code: smtp.C451LocalErr, Secode: smtp.SePol7DeliveryUnauth1}) // Would be 5th message.
1414// Test account size limit enforcement.
1415func TestQuota(t *testing.T) {
1416 resolver := dns.MockResolver{
1417 A: map[string][]string{
1418 "other.example.": {"127.0.0.10"}, // For mx check.
1420 PTR: map[string][]string{
1421 "127.0.0.10": {"other.example."},
1424 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpserverquota/mox.conf"), resolver)
1427 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1428 ts.run(func(client *smtpclient.Client) {
1429 mailFrom := "mjl@other.example"
1430 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1431 ts.smtpErr(err, expErr)
1435 testDeliver("mjl@mox.example", &smtpclient.Error{Code: smtp.C452StorageFull, Secode: smtp.SeMailbox2Full2})
1438// Test with catchall destination address.
1439func TestCatchall(t *testing.T) {
1440 resolver := dns.MockResolver{
1441 A: map[string][]string{
1442 "other.example.": {"127.0.0.10"}, // For mx check.
1444 PTR: map[string][]string{
1445 "127.0.0.10": {"other.example."},
1448 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpservercatchall/mox.conf"), resolver)
1451 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1453 ts.run(func(client *smtpclient.Client) {
1455 mailFrom := "mjl@other.example"
1456 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
1457 ts.smtpErr(err, expErr)
1461 testDeliver("mjl@mox.example", nil) // Exact match.
1462 testDeliver("mjl+test@mox.example", nil) // Domain localpart catchall separator.
1463 testDeliver("MJL+TEST@mox.example", nil) // Again, and case insensitive.
1464 testDeliver("unknown@mox.example", nil) // Catchall address, to account catchall.
1466 n, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).Count()
1467 tcheck(t, err, "checking delivered messages")
1470 acc, err := store.OpenAccount(pkglog, "catchall", false)
1471 tcheck(t, err, "open account")
1476 n, err = bstore.QueryDB[store.Message](ctxbg, acc.DB).Count()
1477 tcheck(t, err, "checking delivered messages to catchall account")
1481// Test DKIM signing for outgoing messages.
1482func TestDKIMSign(t *testing.T) {
1483 resolver := dns.MockResolver{
1484 A: map[string][]string{
1485 "mox.example.": {"127.0.0.10"}, // For mx check.
1487 PTR: map[string][]string{
1488 "127.0.0.10": {"mox.example."},
1492 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1495 // Set DKIM signing config.
1497 genDKIM := func(domain string) string {
1498 dom, _ := mox.Conf.Domain(dns.Domain{ASCII: domain})
1500 privkey := make([]byte, ed25519.SeedSize) // Fake key, don't use for real.
1502 privkey[0] = byte(gen)
1504 sel := config.Selector{
1505 HashEffective: "sha256",
1506 HeadersEffective: []string{"From", "To", "Subject"},
1507 Key: ed25519.NewKeyFromSeed(privkey),
1508 Domain: dns.Domain{ASCII: "testsel"},
1510 dom.DKIM = config.DKIM{
1511 Selectors: map[string]config.Selector{"testsel": sel},
1512 Sign: []string{"testsel"},
1514 mox.Conf.Dynamic.Domains[domain] = dom
1515 pubkey := sel.Key.Public().(ed25519.PublicKey)
1516 return "v=DKIM1;k=ed25519;p=" + base64.StdEncoding.EncodeToString(pubkey)
1519 dkimtxt := genDKIM("mox.example")
1520 dkimtxt2 := genDKIM("mox2.example")
1522 // DKIM verify needs to find the key.
1523 resolver.TXT = map[string][]string{
1524 "testsel._domainkey.mox.example.": {dkimtxt},
1525 "testsel._domainkey.mox2.example.": {dkimtxt2},
1528 ts.submission = true
1529 ts.user = "mjl@mox.example"
1533 testSubmit := func(mailFrom, msgFrom string) {
1535 ts.run(func(client *smtpclient.Client) {
1538 msg := strings.ReplaceAll(fmt.Sprintf(`From: <%s>
1539To: <remote@example.org>
1541Message-Id: <test@mox.example>
1544`, msgFrom), "\n", "\r\n")
1546 rcptTo := "remote@example.org"
1547 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1548 tcheck(t, err, "deliver")
1550 msgs, err := queue.List(ctxbg, queue.Filter{}, queue.Sort{})
1551 tcheck(t, err, "listing queue")
1553 tcompare(t, len(msgs), n)
1554 sort.Slice(msgs, func(i, j int) bool {
1555 return msgs[i].ID > msgs[j].ID
1557 f, err := queue.OpenMessage(ctxbg, msgs[0].ID)
1558 tcheck(t, err, "open message in queue")
1560 results, err := dkim.Verify(ctxbg, pkglog.Logger, resolver, false, dkim.DefaultPolicy, f, false)
1561 tcheck(t, err, "verifying dkim message")
1562 tcompare(t, len(results), 1)
1563 tcompare(t, results[0].Status, dkim.StatusPass)
1564 tcompare(t, results[0].Sig.Domain.ASCII, strings.Split(msgFrom, "@")[1])
1568 testSubmit("mjl@mox.example", "mjl@mox.example")
1569 testSubmit("mjl@mox.example", "mjl@mox2.example") // DKIM signature will be for mox2.example.
1572// Test to postmaster addresses.
1573func TestPostmaster(t *testing.T) {
1574 resolver := dns.MockResolver{
1575 A: map[string][]string{
1576 "other.example.": {"127.0.0.10"}, // For mx check.
1578 PTR: map[string][]string{
1579 "127.0.0.10": {"other.example."},
1582 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/postmaster/mox.conf"), resolver)
1585 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1587 ts.run(func(client *smtpclient.Client) {
1589 mailFrom := "mjl@other.example"
1590 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1591 ts.smtpErr(err, expErr)
1595 testDeliver("postmaster", nil) // Plain postmaster address without domain.
1596 testDeliver("postmaster@host.mox.example", nil) // Postmaster address with configured mail server hostname.
1597 testDeliver("postmaster@mox.example", nil) // Postmaster address without explicitly configured destination.
1598 testDeliver("postmaster@unknown.example", &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
1601// Test to address with empty localpart.
1602func TestEmptylocalpart(t *testing.T) {
1603 resolver := dns.MockResolver{
1604 A: map[string][]string{
1605 "other.example.": {"127.0.0.10"}, // For mx check.
1607 PTR: map[string][]string{
1608 "127.0.0.10": {"other.example."},
1611 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1614 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1615 ts.run(func(client *smtpclient.Client) {
1616 mailFrom := `""@other.example`
1617 msg := strings.ReplaceAll(deliverMessage, "To: <mjl@mox.example>", `To: <""@mox.example>`)
1618 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1619 ts.smtpErr(err, expErr)
1623 testDeliver(`""@mox.example`, nil)
1626// Test handling REQUIRETLS and TLS-Required: No.
1627func TestRequireTLS(t *testing.T) {
1628 resolver := dns.MockResolver{
1629 A: map[string][]string{
1630 "mox.example.": {"127.0.0.10"}, // For mx check.
1632 PTR: map[string][]string{
1633 "127.0.0.10": {"mox.example."},
1637 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1640 ts.submission = true
1641 ts.requiretls = true
1642 ts.user = "mjl@mox.example"
1648 msg0 := strings.ReplaceAll(`From: <mjl@mox.example>
1649To: <remote@example.org>
1651Message-Id: <test@mox.example>
1657 msg1 := strings.ReplaceAll(`From: <mjl@mox.example>
1658To: <remote@example.org>
1660Message-Id: <test@mox.example>
1667 msg2 := strings.ReplaceAll(`From: <mjl@mox.example>
1668To: <remote@example.org>
1670Message-Id: <test@mox.example>
1675 testSubmit := func(msg string, requiretls bool, expRequireTLS *bool) {
1677 ts.run(func(client *smtpclient.Client) {
1680 rcptTo := "remote@example.org"
1681 err := client.Deliver(ctxbg, "mjl@mox.example", rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, requiretls)
1682 tcheck(t, err, "deliver")
1684 msgs, err := queue.List(ctxbg, queue.Filter{}, queue.Sort{})
1685 tcheck(t, err, "listing queue")
1686 tcompare(t, len(msgs), 1)
1687 tcompare(t, msgs[0].RequireTLS, expRequireTLS)
1688 _, err = queue.Drop(ctxbg, pkglog, queue.Filter{IDs: []int64{msgs[0].ID}})
1689 tcheck(t, err, "deleting message from queue")
1693 testSubmit(msg0, true, &yes) // Header ignored, requiretls applied.
1694 testSubmit(msg0, false, &no) // TLS-Required header applied.
1695 testSubmit(msg1, true, &yes) // Bad headers ignored, requiretls applied.
1696 testSubmit(msg1, false, nil) // Inconsistent multiple headers ignored.
1697 testSubmit(msg2, false, nil) // Regular message, no RequireTLS setting.
1698 testSubmit(msg2, true, &yes) // Requiretls applied.
1700 // Check that we get an error if remote SMTP server does not support the requiretls
1702 ts.requiretls = false
1703 ts.run(func(client *smtpclient.Client) {
1704 rcptTo := "remote@example.org"
1705 err := client.Deliver(ctxbg, "mjl@mox.example", rcptTo, int64(len(msg0)), strings.NewReader(msg0), false, false, true)
1707 t.Fatalf("delivered with requiretls to server without requiretls")
1709 if !errors.Is(err, smtpclient.ErrRequireTLSUnsupported) {
1710 t.Fatalf("got err %v, expected ErrRequireTLSUnsupported", err)
1715func TestSmuggle(t *testing.T) {
1716 resolver := dns.MockResolver{
1717 A: map[string][]string{
1718 "example.org.": {"127.0.0.10"}, // For mx check.
1720 PTR: map[string][]string{
1721 "127.0.0.10": {"example.org."}, // For iprev check.
1724 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpsmuggle/mox.conf"), resolver)
1725 ts.tlsmode = smtpclient.TLSSkip
1728 test := func(data string) {
1731 ts.runRaw(func(conn net.Conn) {
1734 ourHostname := mox.Conf.Static.HostnameDomain
1735 remoteHostname := dns.Domain{ASCII: "mox.example"}
1736 opts := smtpclient.Opts{
1737 RootCAs: mox.Conf.Static.TLS.CertPool,
1739 log := pkglog.WithCid(ts.cid - 1)
1740 _, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, ts.tlspkix, ourHostname, remoteHostname, opts)
1741 tcheck(t, err, "smtpclient")
1744 write := func(s string) {
1745 _, err := conn.Write([]byte(s))
1746 tcheck(t, err, "write")
1749 readPrefixLine := func(prefix string) string {
1751 buf := make([]byte, 512)
1752 n, err := conn.Read(buf)
1753 tcheck(t, err, "read")
1754 s := strings.TrimRight(string(buf[:n]), "\r\n")
1755 if !strings.HasPrefix(s, prefix) {
1756 t.Fatalf("got smtp response %q, expected line with prefix %q", s, prefix)
1761 write("MAIL FROM:<remote@example.org>\r\n")
1763 write("RCPT TO:<mjl@mox.example>\r\n")
1768 write("\r\n") // Empty header.
1770 write("\r\n.\r\n") // End of message.
1771 line := readPrefixLine("5")
1772 if !strings.Contains(line, "smug") {
1773 t.Errorf("got 5xx error with message %q, expected error text containing smug", line)
1784func TestFutureRelease(t *testing.T) {
1785 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1786 ts.tlsmode = smtpclient.TLSSkip
1787 ts.user = "mjl@mox.example"
1789 ts.submission = true
1792 ts.auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
1793 return sasl.NewClientPlain(ts.user, ts.pass), nil
1796 test := func(mailtoMore, expResponsePrefix string) {
1799 ts.runRaw(func(conn net.Conn) {
1802 ourHostname := mox.Conf.Static.HostnameDomain
1803 remoteHostname := dns.Domain{ASCII: "mox.example"}
1804 opts := smtpclient.Opts{Auth: ts.auth}
1805 log := pkglog.WithCid(ts.cid - 1)
1806 _, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, false, ourHostname, remoteHostname, opts)
1807 tcheck(t, err, "smtpclient")
1810 write := func(s string) {
1811 _, err := conn.Write([]byte(s))
1812 tcheck(t, err, "write")
1815 readPrefixLine := func(prefix string) string {
1817 buf := make([]byte, 512)
1818 n, err := conn.Read(buf)
1819 tcheck(t, err, "read")
1820 s := strings.TrimRight(string(buf[:n]), "\r\n")
1821 if !strings.HasPrefix(s, prefix) {
1822 t.Fatalf("got smtp response %q, expected line with prefix %q", s, prefix)
1827 write(fmt.Sprintf("MAIL FROM:<mjl@mox.example>%s\r\n", mailtoMore))
1828 readPrefixLine(expResponsePrefix)
1829 if expResponsePrefix != "2" {
1832 write("RCPT TO:<mjl@mox.example>\r\n")
1837 write("From: <mjl@mox.example>\r\n\r\nbody\r\n\r\n.\r\n")
1842 test(" HOLDFOR=1", "2")
1843 test(" HOLDUNTIL="+time.Now().Add(time.Minute).UTC().Format(time.RFC3339), "2")
1844 test(" HOLDUNTIL="+time.Now().Add(time.Minute).UTC().Format(time.RFC3339Nano), "2")
1846 test(" HOLDFOR=0", "501") // 0 is invalid syntax.
1847 test(fmt.Sprintf(" HOLDFOR=%d", int64((queue.FutureReleaseIntervalMax+time.Minute)/time.Second)), "554") // Too far in the future.
1848 test(" HOLDUNTIL="+time.Now().Add(-time.Minute).UTC().Format(time.RFC3339), "554") // In the past.
1849 test(" HOLDUNTIL="+time.Now().Add(queue.FutureReleaseIntervalMax+time.Minute).UTC().Format(time.RFC3339), "554") // Too far in the future.
1850 test(" HOLDUNTIL=2024-02-10T17:28:00+00:00", "501") // "Z" required.
1851 test(" HOLDUNTIL=24-02-10T17:28:00Z", "501") // Invalid.
1852 test(" HOLDFOR=1 HOLDFOR=1", "501") // Duplicate.
1853 test(" HOLDFOR=1 HOLDUNTIL="+time.Now().Add(time.Hour).UTC().Format(time.RFC3339), "501") // Duplicate.
1857func TestSMTPUTF8(t *testing.T) {
1858 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1861 ts.user = "mjl@mox.example"
1863 ts.submission = true
1865 test := func(mailFrom string, rcptTo string, headerValue string, filename string, clientSmtputf8 bool, expectedSmtputf8 bool, expErr *smtpclient.Error) {
1868 ts.run(func(client *smtpclient.Client) {
1870 msg := strings.ReplaceAll(fmt.Sprintf(`From: <%s>
1873X-Custom-Test-Header: %s
1875Content-type: multipart/mixed; boundary="simple boundary"
1878Content-Type: text/plain; charset=UTF-8;
1879Content-Disposition: attachment; filename="%s"
1880Content-Transfer-Encoding: base64
1882QW4gYXR0YWNoZWQgdGV4dCBmaWxlLg==
1885`, mailFrom, rcptTo, headerValue, filename), "\n", "\r\n")
1887 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), true, clientSmtputf8, false)
1888 ts.smtpErr(err, expErr)
1893 msgs, _ := queue.List(ctxbg, queue.Filter{}, queue.Sort{Field: "Queued", Asc: false})
1894 queuedMsg := msgs[0]
1895 if queuedMsg.SMTPUTF8 != expectedSmtputf8 {
1896 t.Fatalf("[%s / %s / %s / %s] got SMTPUTF8 %t, expected %t", mailFrom, rcptTo, headerValue, filename, queuedMsg.SMTPUTF8, expectedSmtputf8)
1901 test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", false, false, nil)
1902 test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", true, false, nil)
1903 test(`mjl@mox.example`, `🙂@example.org`, "header-ascii", "ascii.txt", true, true, nil)
1904 test(`mjl@mox.example`, `🙂@example.org`, "header-ascii", "ascii.txt", false, true, &smtpclient.Error{Permanent: true, Code: smtp.C553BadMailbox, Secode: smtp.SeMsg6NonASCIIAddrNotPermitted7})
1905 test(`Ω@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", true, true, nil)
1906 test(`Ω@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", false, true, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeMsg6NonASCIIAddrNotPermitted7})
1907 test(`mjl@mox.example`, `remote@example.org`, "header-utf8-😍", "ascii.txt", true, true, nil)
1908 test(`mjl@mox.example`, `remote@example.org`, "header-utf8-😍", "ascii.txt", false, true, nil)
1909 test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "utf8-🫠️.txt", true, true, nil)
1910 test(`Ω@mox.example`, `🙂@example.org`, "header-utf8-😍", "utf8-🫠️.txt", true, true, nil)
1911 test(`mjl@mox.example`, `remote@xn--vg8h.example.org`, "header-ascii", "ascii.txt", true, false, nil)
1914// TestExtra checks whether submission of messages with "X-Mox-Extra-<key>: value"
1915// headers cause those those key/value pairs to be added to the Extra field in the
1917func TestExtra(t *testing.T) {
1918 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1921 ts.user = "mjl@mox.example"
1923 ts.submission = true
1925 extraMsg := strings.ReplaceAll(`From: <mjl@mox.example>
1926To: <remote@example.org>
1928X-Mox-Extra-Test: testvalue
1931X-Mox-Extra-x-cANONICAL-z: ok
1932Message-Id: <test@mox.example>
1937 ts.run(func(client *smtpclient.Client) {
1938 mailFrom := "mjl@mox.example"
1939 rcptTo := "mjl@mox.example"
1940 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
1941 tcheck(t, err, "deliver")
1943 msgs, err := queue.List(ctxbg, queue.Filter{}, queue.Sort{})
1944 tcheck(t, err, "queue list")
1945 tcompare(t, len(msgs), 1)
1946 tcompare(t, msgs[0].Extra, map[string]string{
1947 "Test": "testvalue",
1950 "X-Canonical-Z": "ok",
1952 // note: these headers currently stay in the message.
1955// TestExtraDup checks for an error for duplicate x-mox-extra-* keys.
1956func TestExtraDup(t *testing.T) {
1957 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1960 ts.user = "mjl@mox.example"
1962 ts.submission = true
1964 extraMsg := strings.ReplaceAll(`From: <mjl@mox.example>
1965To: <remote@example.org>
1967X-Mox-Extra-Test: testvalue
1968X-Mox-Extra-Test: testvalue
1969Message-Id: <test@mox.example>
1974 ts.run(func(client *smtpclient.Client) {
1975 mailFrom := "mjl@mox.example"
1976 rcptTo := "mjl@mox.example"
1977 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
1978 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C554TransactionFailed, Secode: smtp.SeMsg6Other0})
1982// FromID can be specified during submission, but must be unique, with single recipient.
1983func TestUniqueFromID(t *testing.T) {
1984 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpfromid/mox.conf"), dns.MockResolver{})
1987 ts.user = "mjl+fromid@mox.example"
1989 ts.submission = true
1991 extraMsg := strings.ReplaceAll(`From: <mjl@mox.example>
1992To: <remote@example.org>
1998 // Specify our own unique id when queueing.
1999 ts.run(func(client *smtpclient.Client) {
2000 mailFrom := "mjl+unique@mox.example"
2001 rcptTo := "mjl@mox.example"
2002 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2003 ts.smtpErr(err, nil)
2006 // But we can only use it once.
2007 ts.run(func(client *smtpclient.Client) {
2008 mailFrom := "mjl+unique@mox.example"
2009 rcptTo := "mjl@mox.example"
2010 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2011 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C554TransactionFailed, Secode: smtp.SeAddr1SenderSyntax7})
2014 // We cannot use our own fromid with multiple recipients.
2015 ts.run(func(client *smtpclient.Client) {
2016 mailFrom := "mjl+unique2@mox.example"
2017 rcptTo := []string{"mjl@mox.example", "mjl@mox.example"}
2018 _, err := client.DeliverMultiple(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.SeProto5TooManyRcpts3})
2023// TestDestinationSMTPError checks delivery to a destination with an SMTPError is rejected as configured.
2024func TestDestinationSMTPError(t *testing.T) {
2025 resolver := dns.MockResolver{
2026 A: map[string][]string{
2027 "example.org.": {"127.0.0.10"}, // For mx check.
2031 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
2034 ts.run(func(client *smtpclient.Client) {
2035 mailFrom := "mjl@example.org"
2036 rcptTo := "blocked@mox.example"
2037 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
2038 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
2042// TestDestinationMessageAuthRequiredSMTPError checks delivery to a destination
2043// with an MessageAuthRequiredSMTPError is accepted/rejected as configured.
2044func TestDestinationMessageAuthRequiredSMTPError(t *testing.T) {
2045 resolver := dns.MockResolver{
2046 A: map[string][]string{
2047 "example.org.": {"127.0.0.10"}, // For mx check.
2049 PTR: map[string][]string{
2050 "127.0.0.10": {"example.org."},
2052 TXT: map[string][]string{},
2055 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
2058 ts.run(func(client *smtpclient.Client) {
2059 mailFrom := "mjl@example.org"
2060 rcptTo := "msgauthrequired@mox.example"
2061 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
2062 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7MultiAuthFails26})
2065 // Ensure SPF pass, message should now be accepted.
2066 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
2067 ts.run(func(client *smtpclient.Client) {
2068 mailFrom := "mjl@example.org"
2069 rcptTo := "msgauthrequired@mox.example"
2070 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
2071 ts.smtpErr(err, nil)