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"
28 "github.com/mjl-/bstore"
30 "github.com/mjl-/mox/config"
31 "github.com/mjl-/mox/dkim"
32 "github.com/mjl-/mox/dmarcdb"
33 "github.com/mjl-/mox/dns"
34 "github.com/mjl-/mox/mlog"
35 "github.com/mjl-/mox/mox-"
36 "github.com/mjl-/mox/queue"
37 "github.com/mjl-/mox/sasl"
38 "github.com/mjl-/mox/smtp"
39 "github.com/mjl-/mox/smtpclient"
40 "github.com/mjl-/mox/store"
41 "github.com/mjl-/mox/subjectpass"
42 "github.com/mjl-/mox/tlsrptdb"
43 "github.com/mjl-/mox/webops"
46var ctxbg = context.Background()
49 // Don't make tests slow.
52 unknownRecipientsDelay = 0
55func tcheck(t *testing.T, err error, msg string) {
58 t.Fatalf("%s: %s", msg, err)
62var submitMessage = strings.ReplaceAll(`From: <mjl@mox.example>
63To: <remote@example.org>
65Message-Id: <test@mox.example>
70var deliverMessage = strings.ReplaceAll(`From: <remote@example.org>
73Message-Id: <test@example.org>
78var deliverMessage2 = strings.ReplaceAll(`From: <remote@example.org>
81Message-Id: <test2@example.org>
86var deliverMessage3 = strings.ReplaceAll(`From: <remote@example.org>
89Message-Id: <test3@example.org>
91test email, unique again.
94type testserver struct {
100 resolver dns.Resolver
101 auth func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error)
104 serverConfig *tls.Config
105 clientConfig *tls.Config
106 clientCert *tls.Certificate // Passed to smtpclient for starttls authentication.
110 tlsmode smtpclient.TLSMode
115const password0 = "te\u0301st \u00a0\u2002\u200a" // NFD and various unicode spaces.
116const password1 = "tést " // PRECIS normalized, with NFC.
118func newTestServer(t *testing.T, configPath string, resolver dns.Resolver) *testserver {
119 limitersInit() // Reset rate limiters.
121 log := mlog.New("smtpserver", nil)
123 checkf := func(ctx context.Context, err error, format string, args ...any) {
124 tcheck(t, err, fmt.Sprintf(format, args...))
127 DBWrite: func(ctx context.Context, acc *store.Account, fn func(tx *bstore.Tx)) {
128 err := acc.DB.Write(ctx, func(tx *bstore.Tx) error {
132 tcheck(t, err, "db write")
142 tlsmode: smtpclient.TLSOpportunistic,
143 serverConfig: &tls.Config{
144 Certificates: []tls.Certificate{fakeCert(t, false)},
149 // Ensure session keys, for tests that check resume and authentication.
150 ctx, cancel := context.WithCancel(ctxbg)
152 mox.StartTLSSessionTicketKeyRefresher(ctx, log, ts.serverConfig)
155 mox.ConfigStaticPath = configPath
156 mox.MustLoadConfig(true, false)
157 dataDir := mox.ConfigDirPath(mox.Conf.Static.DataDir)
158 os.RemoveAll(dataDir)
160 err := dmarcdb.Init()
161 tcheck(t, err, "dmarcdb init")
162 err = tlsrptdb.Init()
163 tcheck(t, err, "tlsrptdb init")
164 err = store.Init(ctxbg)
165 tcheck(t, err, "store init")
167 ts.switchStop = store.Switchboard()
169 tcheck(t, err, "queue init")
171 ts.acc, err = store.OpenAccount(log, "mjl", false)
172 tcheck(t, err, "open account")
173 err = ts.acc.SetPassword(log, password0)
174 tcheck(t, err, "set password")
176 ts.comm = store.RegisterComm(ts.acc)
181func (ts *testserver) close() {
185 err := dmarcdb.Close()
186 tcheck(ts.t, err, "dmarcdb close")
187 err = tlsrptdb.Close()
188 tcheck(ts.t, err, "tlsrptdb close")
192 tcheck(ts.t, err, "closing account")
197 tcheck(ts.t, err, "store close")
200func (ts *testserver) checkCount(mailboxName string, expect int) {
203 q := bstore.QueryDB[store.Mailbox](ctxbg, ts.acc.DB)
204 q.FilterNonzero(store.Mailbox{Name: mailboxName})
205 q.FilterEqual("Expunged", false)
207 tcheck(t, err, "get mailbox")
208 qm := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB)
209 qm.FilterNonzero(store.Message{MailboxID: mb.ID})
210 qm.FilterEqual("Expunged", false)
212 tcheck(t, err, "count messages in mailbox")
214 t.Fatalf("messages in mailbox, found %d, expected %d", n, expect)
218func (ts *testserver) run(fn func(client *smtpclient.Client)) {
220 ts.runx(func(helloErr error, client *smtpclient.Client) {
222 tcheck(ts.t, helloErr, "hello")
227func (ts *testserver) runx(fn func(helloErr error, client *smtpclient.Client)) {
229 ts.runRaw(func(conn net.Conn) {
233 if auth == nil && ts.user != "" {
234 auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
235 return sasl.NewClientPlain(ts.user, ts.pass), nil
239 ourHostname := mox.Conf.Static.HostnameDomain
240 remoteHostname := dns.Domain{ASCII: "mox.example"}
241 opts := smtpclient.Opts{
243 RootCAs: mox.Conf.Static.TLS.CertPool,
244 ClientCert: ts.clientCert,
246 log := pkglog.WithCid(ts.cid - 1)
247 client, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, ts.tlspkix, ourHostname, remoteHostname, opts)
257func (ts *testserver) runRaw(fn func(clientConn net.Conn)) {
262 serverConn, clientConn := net.Pipe()
263 defer serverConn.Close()
264 // clientConn is closed as part of closing client.
265 serverdone := make(chan struct{})
266 defer func() { <-serverdone }()
269 serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, ts.serverConfig, serverConn, ts.resolver, ts.submission, ts.immediateTLS, false, false, 100<<20, false, false, ts.requiretls, ts.dnsbls, 0)
274 clientConn = tls.Client(clientConn, ts.clientConfig)
280func (ts *testserver) smtpErr(err error, expErr *smtpclient.Error) *smtpclient.Error {
283 var cerr smtpclient.Error
284 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) {
285 t.Fatalf("got err:\n%#v (%q)\nexpected:\n%#v", err, err, expErr)
290// Just a cert that appears valid. SMTP client will not verify anything about it
291// (that is opportunistic TLS for you, "better some than none"). Let's enjoy this
292// one moment where it makes life easier.
293func fakeCert(t *testing.T, randomkey bool) tls.Certificate {
294 seed := make([]byte, ed25519.SeedSize)
296 cryptorand.Read(seed)
298 privKey := ed25519.NewKeyFromSeed(seed) // Fake key, don't use this for real!
299 template := &x509.Certificate{
300 SerialNumber: big.NewInt(1), // Required field...
301 // Valid period is needed to get session resumption enabled.
302 NotBefore: time.Now().Add(-time.Minute),
303 NotAfter: time.Now().Add(time.Hour),
305 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
307 t.Fatalf("making certificate: %s", err)
309 cert, err := x509.ParseCertificate(localCertBuf)
311 t.Fatalf("parsing generated certificate: %s", err)
313 c := tls.Certificate{
314 Certificate: [][]byte{localCertBuf},
321// check expected dmarc evaluations for outgoing aggregate reports.
322func checkEvaluationCount(t *testing.T, n int) []dmarcdb.Evaluation {
324 l, err := dmarcdb.Evaluations(ctxbg)
325 tcheck(t, err, "get dmarc evaluations")
326 tcompare(t, len(l), n)
330// Test submission from authenticated user.
331func TestSubmission(t *testing.T) {
332 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
335 // Set DKIM signing config.
336 dom, _ := mox.Conf.Domain(dns.Domain{ASCII: "mox.example"})
337 sel := config.Selector{
338 HashEffective: "sha256",
339 HeadersEffective: []string{"From", "To", "Subject"},
340 Key: ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)), // Fake key, don't use for real.
341 Domain: dns.Domain{ASCII: "mox.example"},
343 dom.DKIM = config.DKIM{
344 Selectors: map[string]config.Selector{"testsel": sel},
345 Sign: []string{"testsel"},
347 mox.Conf.Dynamic.Domains["mox.example"] = dom
349 testAuth := func(authfn func(user, pass string, cs *tls.ConnectionState) sasl.Client, user, pass string, expErr *smtpclient.Error) {
352 ts.auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
353 return authfn(user, pass, cs), nil
358 ts.runx(func(err error, client *smtpclient.Client) {
359 mailFrom := "mjl@mox.example"
360 rcptTo := "remote@example.org"
362 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
364 var cerr smtpclient.Error
365 if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Code != expErr.Code || cerr.Secode != expErr.Secode) {
366 t.Fatalf("got err:\n%#v (%q)\nexpected:\n%#v", err, err, expErr)
368 checkEvaluationCount(t, 0)
372 acc, err := store.OpenAccount(pkglog, "disabled", false)
373 tcheck(t, err, "open account")
374 err = acc.SetPassword(pkglog, "test1234")
375 tcheck(t, err, "set password")
377 tcheck(t, err, "close account")
380 testAuth(nil, "", "", &smtpclient.Error{Permanent: true, Code: smtp.C530SecurityRequired, Secode: smtp.SePol7Other0})
381 authfns := []func(user, pass string, cs *tls.ConnectionState) sasl.Client{
382 func(user, pass string, cs *tls.ConnectionState) sasl.Client { return sasl.NewClientPlain(user, pass) },
383 func(user, pass string, cs *tls.ConnectionState) sasl.Client { return sasl.NewClientLogin(user, pass) },
384 func(user, pass string, cs *tls.ConnectionState) sasl.Client { return sasl.NewClientCRAMMD5(user, pass) },
385 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
386 return sasl.NewClientSCRAMSHA1(user, pass, false)
388 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
389 return sasl.NewClientSCRAMSHA256(user, pass, false)
391 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
392 return sasl.NewClientSCRAMSHA1PLUS(user, pass, *cs)
394 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
395 return sasl.NewClientSCRAMSHA256PLUS(user, pass, *cs)
398 for _, fn := range authfns {
399 testAuth(fn, "mjl@mox.example", "test", &smtpclient.Error{Code: smtp.C535AuthBadCreds, Secode: smtp.SePol7AuthBadCreds8}) // Bad (short) password.
400 testAuth(fn, "mjl@mox.example", password0+"test", &smtpclient.Error{Code: smtp.C535AuthBadCreds, Secode: smtp.SePol7AuthBadCreds8}) // Bad password.
401 testAuth(fn, "mjl@mox.example", password0, nil)
402 testAuth(fn, "mjl@mox.example", password1, nil)
403 testAuth(fn, "móx@mox.example", password0, nil)
404 testAuth(fn, "móx@mox.example", password1, nil)
405 testAuth(fn, "mo\u0301x@mox.example", password0, nil)
406 testAuth(fn, "mo\u0301x@mox.example", password1, nil)
407 testAuth(fn, "disabled@mox.example", "test1234", &smtpclient.Error{Code: smtp.C525AccountDisabled, Secode: smtp.SePol7AccountDisabled13})
408 testAuth(fn, "disabled@mox.example", "bogus", &smtpclient.Error{Code: smtp.C535AuthBadCreds, Secode: smtp.SePol7AuthBadCreds8})
411 // Create a certificate, register its public key with account, and make a tls
412 // client config that sends the certificate.
413 clientCert0 := fakeCert(ts.t, true)
414 tlspubkey, err := store.ParseTLSPublicKeyCert(clientCert0.Certificate[0])
415 tcheck(t, err, "parse certificate")
416 tlspubkey.Account = "mjl"
417 tlspubkey.LoginAddress = "mjl@mox.example"
418 err = store.TLSPublicKeyAdd(ctxbg, &tlspubkey)
419 tcheck(t, err, "add tls public key to account")
420 ts.immediateTLS = true
421 ts.clientConfig = &tls.Config{
422 InsecureSkipVerify: true,
423 Certificates: []tls.Certificate{
428 // No explicit address in EXTERNAL.
429 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
430 return sasl.NewClientExternal(user)
433 // Same username in EXTERNAL as configured for key.
434 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
435 return sasl.NewClientExternal(user)
436 }, "mjl@mox.example", "", nil)
438 // Different username in EXTERNAL as configured for key, but same account.
439 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
440 return sasl.NewClientExternal(user)
441 }, "móx@mox.example", "", nil)
443 // Different username as configured for key, but same account, but not EXTERNAL auth.
444 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
445 return sasl.NewClientSCRAMSHA256PLUS(user, pass, *cs)
446 }, "móx@mox.example", password0, nil)
448 // Different account results in error.
449 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
450 return sasl.NewClientExternal(user)
451 }, "☺@mox.example", "", &smtpclient.Error{Code: smtp.C535AuthBadCreds, Secode: smtp.SePol7AuthBadCreds8})
453 // Starttls with client cert should authenticate too.
454 ts.immediateTLS = false
455 ts.clientCert = &clientCert0
456 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
457 return sasl.NewClientExternal(user)
459 ts.immediateTLS = true
462 // Add a client session cache, so our connections will be resumed. We are testing
463 // that the credentials are applied to resumed connections too.
464 ts.clientConfig.ClientSessionCache = tls.NewLRUClientSessionCache(10)
465 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
467 panic("tls connection was resumed")
469 return sasl.NewClientExternal(user)
471 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
473 panic("tls connection was not resumed")
475 return sasl.NewClientExternal(user)
478 // Unknown client certificate should fail the connection.
479 serverConn, clientConn := net.Pipe()
480 serverdone := make(chan struct{})
481 defer func() { <-serverdone }()
484 defer serverConn.Close()
485 tlsConfig := &tls.Config{
486 Certificates: []tls.Certificate{fakeCert(ts.t, false)},
488 serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, tlsConfig, serverConn, ts.resolver, ts.submission, ts.immediateTLS, false, false, 100<<20, false, false, false, ts.dnsbls, 0)
492 defer clientConn.Close()
494 // Authentication with an unknown/untrusted certificate should fail.
495 clientCert1 := fakeCert(ts.t, true)
496 ts.clientConfig.ClientSessionCache = nil
497 ts.clientConfig.Certificates = []tls.Certificate{
500 clientConn = tls.Client(clientConn, ts.clientConfig)
501 // note: It's not enough to do a handshake and check if that was successful. If the
502 // client cert is not acceptable, we only learn after the handshake, when the first
503 // data messages are exchanged.
504 buf := make([]byte, 100)
505 _, err = clientConn.Read(buf)
507 t.Fatalf("tls handshake with unknown client certificate succeeded")
509 if alert, ok := mox.AsTLSAlert(err); !ok || alert != 42 {
510 t.Fatalf("got err %#v, expected tls 'bad certificate' alert", err)
514func TestDomainDisabled(t *testing.T) {
515 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
519 ts.user = "mjl@mox.example"
522 // Submission with SMTP MAIL FROM of disabled domain must fail.
523 ts.run(func(client *smtpclient.Client) {
524 mailFrom := "mjl@disabled.example" // Disabled.
525 rcptTo := "remote@example.org"
526 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
527 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
528 checkEvaluationCount(t, 0)
531 // Message From-address has disabled domain, must fail.
532 var submitMessage2 = strings.ReplaceAll(`From: <mjl@disabled.example>
533To: <remote@example.org>
535Message-Id: <test@mox.example>
539 ts.run(func(client *smtpclient.Client) {
540 mailFrom := "mjl@mox.example"
541 rcptTo := "remote@example.org"
542 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage2)), strings.NewReader(submitMessage2), false, false, false)
543 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
544 checkEvaluationCount(t, 0)
548// Test delivery from external MTA.
549func TestDelivery(t *testing.T) {
550 resolver := dns.MockResolver{
551 A: map[string][]string{
552 "example.org.": {"127.0.0.10"}, // For mx check.
554 PTR: map[string][]string{},
556 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
559 ts.run(func(client *smtpclient.Client) {
560 mailFrom := "remote@example.org"
561 rcptTo := "mjl@[127.0.0.10]"
562 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
563 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
566 ts.run(func(client *smtpclient.Client) {
567 mailFrom := "remote@example.org"
568 rcptTo := "mjl@[IPv6:::1]"
569 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
570 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
573 ts.run(func(client *smtpclient.Client) {
574 mailFrom := "remote@example.org"
575 rcptTo := "mjl@test.example" // Not configured as destination.
576 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
577 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
580 ts.run(func(client *smtpclient.Client) {
581 mailFrom := "remote@example.org"
582 rcptTo := "unknown@mox.example" // User unknown.
583 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
584 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
587 ts.run(func(client *smtpclient.Client) {
588 mailFrom := "remote@example.org"
589 rcptTo := "mjl@mox.example"
590 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
591 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
594 // Set up iprev to get delivery from unknown user to be accepted.
595 resolver.PTR["127.0.0.10"] = []string{"example.org."}
597 // Only ascii o@ is configured, not the greek and cyrillic lookalikes.
598 ts.run(func(client *smtpclient.Client) {
599 mailFrom := "remote@example.org"
600 rcptTo := "ο@mox.example" // omicron \u03bf, looks like the configured o@
601 msg := strings.ReplaceAll(deliverMessage, "mjl@mox.example", rcptTo)
602 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, true, false)
603 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
606 // Deliveries to disabled domain are rejected with temporary error.
607 ts.run(func(client *smtpclient.Client) {
608 mailFrom := "remote@example.org"
609 rcptTo := "mjl@disabled.example"
610 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
611 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C450MailboxUnavail, Secode: smtp.SeMailbox2Disabled1})
614 ts.run(func(client *smtpclient.Client) {
615 recipients := []string{
617 "o@mox.example", // ascii o, as configured
618 "\u2126@mox.example", // ohm sign, as configured
619 "ω@mox.example", // lower-case omega, we match case-insensitively and this is the lowercase of ohm (!)
620 "\u03a9@mox.example", // capital omega, also lowercased to omega.
621 "móx@mox.example", // NFC
622 "mo\u0301x@mox.example", // not NFC, but normalized as móx@, see https://go.dev/blog/normalization
625 for _, rcptTo := range recipients {
626 // Ensure SMTP RCPT TO and message address headers are the same, otherwise the junk
627 // filter treats us more strictly.
628 msg := strings.ReplaceAll(deliverMessage, "mjl@mox.example", rcptTo)
630 mailFrom := "remote@example.org"
631 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, true, false)
632 tcheck(t, err, "deliver to remote")
634 changes := make(chan []store.Change)
636 _, l := ts.comm.Get()
640 timer := time.NewTimer(time.Second)
645 t.Fatalf("no delivery in 1s")
650 checkEvaluationCount(t, 0)
653func TestIntrobox(t *testing.T) {
654 resolver := dns.MockResolver{
655 A: map[string][]string{
656 "example.org.": {"127.0.0.10"}, // For mx check.
658 TXT: map[string][]string{
659 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
660 "_dmarc.example.org.": {"v=DMARC1;p=reject"},
662 PTR: map[string][]string{
663 "127.0.0.10": {"example.org."},
666 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
669 acc := mox.Conf.Dynamic.Accounts[ts.acc.Name]
670 dest := acc.Destinations["mjl@mox.example"]
671 dest.Mailbox = "Inbox"
672 acc.Destinations["mjl@mox.example"] = dest
673 accDest := mox.Conf.AccountDestinationsLocked["mjl@mox.example"]
674 accDest.Destination = dest
675 mox.Conf.AccountDestinationsLocked["mjl@mox.example"] = accDest
676 acc.Introbox = "Introbox"
677 acc.AutomaticJunkFlags.Enabled = true
678 acc.AutomaticJunkFlags.JunkMailboxRegexp = "^(junk|spam)"
679 acc.AutomaticJunkFlags.NeutralMailboxRegexp = "^(inbox|introbox|neutral|postmaster|dmarc|tlsrpt|rejects)"
680 acc.JunkMailbox = regexp.MustCompile(acc.AutomaticJunkFlags.JunkMailboxRegexp)
681 acc.NeutralMailbox = regexp.MustCompile(acc.AutomaticJunkFlags.NeutralMailboxRegexp)
682 mox.Conf.Dynamic.Accounts[ts.acc.Name] = acc
684 deliver := func(msg string) {
685 ts.run(func(client *smtpclient.Client) {
686 mailFrom := "remote@example.org"
687 rcptTo := "mjl@mox.example"
688 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
689 tcheck(t, err, "deliver")
693 deliver(deliverMessage)
694 ts.checkCount("Inbox", 0)
695 ts.checkCount("Introbox", 1)
697 deliver(deliverMessage2)
698 ts.checkCount("Inbox", 0)
699 ts.checkCount("Introbox", 2)
701 introbox, err := bstore.QueryDB[store.Mailbox](ctxbg, ts.acc.DB).FilterNonzero(store.Mailbox{Name: "Introbox"}).Get()
702 tcheck(t, err, "get introbox")
704 err = bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).FilterNonzero(store.Message{MailboxID: introbox.ID}).ForEach(func(m store.Message) error {
705 if m.Junk || m.Notjunk {
706 t.Fatalf("introbox message has junk flags: junk %v, notjunk %v", m.Junk, m.Notjunk)
708 ids = append(ids, m.ID)
711 tcheck(t, err, "get introbox message ids")
712 ts.xops.MessageMove(ctxbg, pkglog, ts.acc, ids, "Inbox", 0, false)
713 ts.checkCount("Inbox", 2)
714 ts.checkCount("Introbox", 0)
716 deliver(deliverMessage3)
717 ts.checkCount("Inbox", 3)
718 ts.checkCount("Introbox", 0)
721func tinsertmsg(t *testing.T, acc *store.Account, mailbox string, m *store.Message, msg string) {
722 mf, err := store.CreateMessageTemp(pkglog, "insertmsg")
723 tcheck(t, err, "temp message")
724 defer os.Remove(mf.Name())
726 _, err = mf.Write([]byte(msg))
727 tcheck(t, err, "write message")
729 acc.WithWLock(func() {
730 err = acc.DeliverMailbox(pkglog, mailbox, "", m, mf)
731 tcheck(t, err, "deliver message")
735func tretrain(t *testing.T, acc *store.Account) {
738 // Fresh empty junkfilter.
739 basePath := mox.DataDirPath("accounts")
740 dbPath := filepath.Join(basePath, acc.Name, "junkfilter.db")
741 bloomPath := filepath.Join(basePath, acc.Name, "junkfilter.bloom")
744 jf, _, err := acc.OpenJunkFilter(ctxbg, pkglog)
745 tcheck(t, err, "open junk filter")
748 // Fetch messags to retrain on.
749 q := bstore.QueryDB[store.Message](ctxbg, acc.DB)
750 q.FilterEqual("Expunged", false)
751 q.FilterFn(func(m store.Message) bool {
752 return m.Flags.Junk != m.Flags.Notjunk
754 msgs, err := q.List()
755 tcheck(t, err, "fetch messages")
757 // Retrain the messages.
758 for _, m := range msgs {
759 ham := m.Flags.Notjunk
761 f, err := os.Open(acc.MessagePath(m.ID))
762 tcheck(t, err, "open message")
763 r := store.FileMsgReader(m.MsgPrefix, f)
765 jf.TrainMessage(ctxbg, r, m.Size, ham)
768 tcheck(t, err, "close message")
772 tcheck(t, err, "save junkfilter")
775// Test accept/reject with DMARC reputation and with spammy content.
776func TestSpam(t *testing.T) {
777 resolver := &dns.MockResolver{
778 A: map[string][]string{
779 "example.org.": {"127.0.0.1"}, // For mx check.
781 TXT: map[string][]string{
782 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
783 "_dmarc.example.org.": {"v=DMARC1;p=reject; rua=mailto:dmarcrpt@example.org"},
786 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
789 // Insert spammy messages. No junkfilter training yet.
791 RemoteIP: "127.0.0.10",
792 RemoteIPMasked1: "127.0.0.10",
793 RemoteIPMasked2: "127.0.0.0",
794 RemoteIPMasked3: "127.0.0.0",
795 MailFrom: "remote@example.org",
796 MailFromLocalpart: smtp.Localpart("remote"),
797 MailFromDomain: "example.org",
798 RcptToLocalpart: smtp.Localpart("mjl"),
799 RcptToDomain: "mox.example",
800 MsgFromLocalpart: smtp.Localpart("remote"),
801 MsgFromDomain: "example.org",
802 MsgFromOrgDomain: "example.org",
803 MsgFromValidated: true,
804 MsgFromValidation: store.ValidationStrict,
805 Flags: store.Flags{Seen: true, Junk: true},
806 Size: int64(len(deliverMessage)),
810 tinsertmsg(t, ts.acc, "Inbox", &nm, deliverMessage)
812 tinsertmsg(t, ts.acc, "mjl2", &nm, deliverMessage)
815 // Delivery from sender with bad reputation should fail.
816 ts.run(func(client *smtpclient.Client) {
817 mailFrom := "remote@example.org"
818 rcptTo := "mjl@mox.example"
819 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
820 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
822 ts.checkCount("Rejects", 1)
823 checkEvaluationCount(t, 0) // No positive interactions yet.
826 // Delivery from sender with bad reputation matching AcceptRejectsToMailbox should
827 // result in accepted delivery to the mailbox.
828 ts.run(func(client *smtpclient.Client) {
829 mailFrom := "remote@example.org"
830 rcptTo := "mjl2@mox.example"
831 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage2)), strings.NewReader(deliverMessage2), false, false, false)
832 tcheck(t, err, "deliver")
834 ts.checkCount("mjl2junk", 1) // In ruleset rejects mailbox.
835 ts.checkCount("Rejects", 1) // Same as before.
836 checkEvaluationCount(t, 0) // This is not an actual accept.
839 // Mark the messages as having good reputation.
841 err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).FilterEqual("Expunged", false).ForEach(func(m store.Message) error {
842 ids = append(ids, m.ID)
845 tcheck(t, err, "get message ids")
846 ts.xops.MessageFlagsClear(ctxbg, pkglog, ts.acc, ids, []string{"$Junk"})
847 ts.xops.MessageFlagsAdd(ctxbg, pkglog, ts.acc, ids, []string{"$NotJunk"})
849 // Message should now be accepted.
850 ts.run(func(client *smtpclient.Client) {
851 tcheck(t, err, "hello")
852 mailFrom := "remote@example.org"
853 rcptTo := "mjl@mox.example"
854 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
855 tcheck(t, err, "deliver")
857 // Message should now be removed from Rejects mailboxes.
858 ts.checkCount("Rejects", 0)
859 ts.checkCount("mjl2junk", 1)
860 checkEvaluationCount(t, 1)
863 // Undo dmarc pass, mark messages as junk, and train the filter.
865 q := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB)
866 q.FilterEqual("Expunged", false)
867 _, err = q.UpdateFields(map[string]any{"Junk": true, "Notjunk": false})
868 tcheck(t, err, "update junkiness")
871 // Message should be refused for spammy content.
872 ts.run(func(client *smtpclient.Client) {
873 tcheck(t, err, "hello")
874 mailFrom := "remote@example.org"
875 rcptTo := "mjl@mox.example"
876 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
877 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
878 checkEvaluationCount(t, 1) // No new evaluation, this isn't a DMARC reject.
882// Test accept/reject with forwarded messages, DMARC ignored, no IP/EHLO/MAIL
883// FROM-based reputation.
884func TestForward(t *testing.T) {
885 // Do a run without forwarding, and with.
886 check := func(forward bool) {
888 resolver := &dns.MockResolver{
889 A: map[string][]string{
890 "bad.example.": {"127.0.0.1"}, // For mx check.
891 "good.example.": {"127.0.0.1"}, // For mx check.
892 "forward.example.": {"127.0.0.10"}, // For mx check.
894 TXT: map[string][]string{
895 "bad.example.": {"v=spf1 ip4:127.0.0.1 -all"},
896 "good.example.": {"v=spf1 ip4:127.0.0.1 -all"},
897 "forward.example.": {"v=spf1 ip4:127.0.0.10 -all"},
898 "_dmarc.bad.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@bad.example"},
899 "_dmarc.good.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@good.example"},
900 "_dmarc.forward.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@forward.example"},
902 PTR: map[string][]string{
903 "127.0.0.10": {"forward.example."}, // For iprev check.
906 rcptTo := "mjl3@mox.example"
908 // For SPF and DMARC pass, otherwise the test ends quickly.
909 resolver.TXT["bad.example."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
910 resolver.TXT["good.example."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
911 rcptTo = "mjl@mox.example" // Without IsForward rule.
914 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
917 totalEvaluations := 0
919 var msgBad = strings.ReplaceAll(`From: <remote@bad.example>
922Message-Id: <bad@example.org>
926 var msgOK = strings.ReplaceAll(`From: <remote@good.example>
929Message-Id: <good@example.org>
933 var msgOK2 = strings.ReplaceAll(`From: <other@forward.example>
936Message-Id: <regular@example.org>
938happens to come from forwarding mail server.
941 // Deliver forwarded messages, then classify as junk. Normally enough to treat
942 // other unrelated messages from IP as junk, but not for forwarded messages.
943 ts.run(func(client *smtpclient.Client) {
944 mailFrom := "remote@forward.example"
946 mailFrom = "remote@bad.example"
950 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgBad)), strings.NewReader(msgBad), false, false, false)
951 tcheck(t, err, "deliver message")
953 totalEvaluations += 10
955 n, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).UpdateFields(map[string]any{"Junk": true, "MsgFromValidated": true})
956 tcheck(t, err, "marking messages as junk")
960 // Next delivery will fail, with negative "message From" signal.
961 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgBad)), strings.NewReader(msgBad), false, false, false)
962 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
964 checkEvaluationCount(t, totalEvaluations)
967 // Delivery from different "message From" without reputation, but from same
968 // forwarding email server, should succeed under forwarding, not as regular sending
970 ts.run(func(client *smtpclient.Client) {
971 mailFrom := "remote@forward.example"
973 mailFrom = "remote@good.example"
976 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgOK)), strings.NewReader(msgOK), false, false, false)
978 tcheck(t, err, "deliver")
979 totalEvaluations += 1
981 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
983 checkEvaluationCount(t, totalEvaluations)
986 // Delivery from forwarding server that isn't a forward should get same treatment.
987 ts.run(func(client *smtpclient.Client) {
988 mailFrom := "other@forward.example"
990 // Ensure To header matches.
993 msg = strings.ReplaceAll(msg, "<mjl@mox.example>", "<mjl3@mox.example>")
996 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
998 tcheck(t, err, "deliver")
999 totalEvaluations += 1
1001 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
1003 checkEvaluationCount(t, totalEvaluations)
1011// Messages that we sent to, that have passing DMARC, but that are otherwise spammy, should be accepted.
1012func TestDMARCSent(t *testing.T) {
1013 resolver := &dns.MockResolver{
1014 A: map[string][]string{
1015 "example.org.": {"127.0.0.1"}, // For mx check.
1017 TXT: map[string][]string{
1018 "example.org.": {"v=spf1 ip4:127.0.0.1 -all"},
1019 "_dmarc.example.org.": {"v=DMARC1;p=reject;rua=mailto:dmarcrpt@example.org"},
1022 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
1025 // First check that DMARC policy rejects message and results in optional evaluation.
1026 ts.run(func(client *smtpclient.Client) {
1027 mailFrom := "remote@example.org"
1028 rcptTo := "mjl@mox.example"
1029 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1030 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7MultiAuthFails26})
1031 l := checkEvaluationCount(t, 1)
1032 tcompare(t, l[0].Optional, true)
1035 // Update DNS for an SPF pass, and DMARC pass.
1036 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
1038 // Insert hammy & spammy messages not related to the test message.
1040 MailFrom: "remote@test.example",
1041 RcptToLocalpart: smtp.Localpart("mjl"),
1042 RcptToDomain: "mox.example",
1043 Flags: store.Flags{Seen: true},
1044 Size: int64(len(deliverMessage)),
1046 // We need at least 50 ham messages for the junk filter to become significant. We
1047 // offset it with negative messages for mediocre score.
1051 tinsertmsg(t, ts.acc, "Archive", &nm, deliverMessage)
1055 tinsertmsg(t, ts.acc, "Archive", &nm, deliverMessage)
1059 // Baseline, message should be refused for spammy content.
1060 ts.run(func(client *smtpclient.Client) {
1061 mailFrom := "remote@example.org"
1062 rcptTo := "mjl@mox.example"
1063 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1064 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
1065 checkEvaluationCount(t, 1) // No new evaluation.
1068 // Insert a message that we sent to the address that is about to send to us.
1069 sentMsg := store.Message{Size: int64(len(deliverMessage))}
1070 tinsertmsg(t, ts.acc, "Sent", &sentMsg, deliverMessage)
1071 err := ts.acc.DB.Insert(ctxbg, &store.Recipient{MessageID: sentMsg.ID, Localpart: "remote", Domain: "example.org", OrgDomain: "example.org", Sent: time.Now()})
1072 tcheck(t, err, "inserting message recipient")
1074 // Reject a message due to DMARC again. Since we sent a message to the domain, it
1075 // is no longer unknown and we should see a non-optional evaluation that will
1076 // result in a DMARC report.
1077 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.1 -all"}
1078 ts.run(func(client *smtpclient.Client) {
1079 mailFrom := "remote@example.org"
1080 rcptTo := "mjl@mox.example"
1081 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1082 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7MultiAuthFails26})
1083 l := checkEvaluationCount(t, 2) // New evaluation.
1084 tcompare(t, l[1].Optional, false)
1087 // We should now be accepting the message because we recently sent a message.
1088 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
1089 ts.run(func(client *smtpclient.Client) {
1090 mailFrom := "remote@example.org"
1091 rcptTo := "mjl@mox.example"
1092 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1093 tcheck(t, err, "deliver")
1094 l := checkEvaluationCount(t, 3) // New evaluation.
1095 tcompare(t, l[2].Optional, false)
1099// Test DNSBL, then getting through with subjectpass.
1100func TestBlocklistedSubjectpass(t *testing.T) {
1101 // Set up a DNSBL on dnsbl.example, and get DMARC pass.
1102 resolver := &dns.MockResolver{
1103 A: map[string][]string{
1104 "example.org.": {"127.0.0.10"}, // For mx check.
1105 "2.0.0.127.dnsbl.example.": {"127.0.0.2"}, // For healthcheck.
1106 "10.0.0.127.dnsbl.example.": {"127.0.0.10"}, // Where our connection pretends to come from.
1108 TXT: map[string][]string{
1109 "10.0.0.127.dnsbl.example.": {"blocklisted"},
1110 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
1111 "_dmarc.example.org.": {"v=DMARC1;p=reject"},
1113 PTR: map[string][]string{
1114 "127.0.0.10": {"example.org."}, // For iprev check.
1117 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1118 ts.dnsbls = []dns.Domain{{ASCII: "dnsbl.example"}}
1121 // Message should be refused softly (temporary error) due to DNSBL.
1122 ts.run(func(client *smtpclient.Client) {
1123 mailFrom := "remote@example.org"
1124 rcptTo := "mjl@mox.example"
1125 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1126 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
1129 // Set up subjectpass on account.
1130 acc := mox.Conf.Dynamic.Accounts[ts.acc.Name]
1131 acc.SubjectPass.Period = time.Hour
1132 mox.Conf.Dynamic.Accounts[ts.acc.Name] = acc
1134 // Message should be refused quickly (permanent error) due to DNSBL and Subjectkey.
1136 ts.run(func(client *smtpclient.Client) {
1137 mailFrom := "remote@example.org"
1138 rcptTo := "mjl@mox.example"
1139 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1140 cerr := ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7DeliveryUnauth1})
1141 i := strings.Index(cerr.Line, subjectpass.Explanation)
1143 t.Fatalf("got error line %q, expected error line with subjectpass", cerr.Line)
1145 pass = cerr.Line[i+len(subjectpass.Explanation):]
1148 ts.run(func(client *smtpclient.Client) {
1149 mailFrom := "remote@example.org"
1150 rcptTo := "mjl@mox.example"
1151 passMessage := strings.Replace(deliverMessage, "Subject: test", "Subject: test "+pass, 1)
1152 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(passMessage)), strings.NewReader(passMessage), false, false, false)
1153 tcheck(t, err, "deliver with subjectpass")
1157// Test accepting a DMARC report.
1158func TestDMARCReport(t *testing.T) {
1159 resolver := &dns.MockResolver{
1160 A: map[string][]string{
1161 "example.org.": {"127.0.0.10"}, // For mx check.
1163 TXT: map[string][]string{
1164 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
1165 "_dmarc.example.org.": {"v=DMARC1;p=reject; rua=mailto:dmarcrpt@example.org"},
1167 PTR: map[string][]string{
1168 "127.0.0.10": {"example.org."}, // For iprev check.
1171 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/dmarcreport/mox.conf"), resolver)
1174 run := func(rcptTo, report string, n int) {
1176 ts.run(func(client *smtpclient.Client) {
1179 mailFrom := "remote@example.org"
1181 msgb := &bytes.Buffer{}
1182 _, 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)
1183 tcheck(t, xerr, "write msg headers")
1184 w := quotedprintable.NewWriter(msgb)
1185 _, xerr = w.Write([]byte(strings.ReplaceAll(report, "\n", "\r\n")))
1186 tcheck(t, xerr, "write message")
1187 msg := msgb.String()
1189 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1190 tcheck(t, err, "deliver")
1192 records, err := dmarcdb.Records(ctxbg)
1193 tcheck(t, err, "dmarcdb records")
1194 if len(records) != n {
1195 t.Fatalf("got %d dmarcdb records, expected %d or more", len(records), n)
1201 run("dmarc-reports@mox.example", dmarcReport, 0) // Wrong domain in report.
1203 report := strings.ReplaceAll(dmarcReport, "xmox.nl", "mox.example")
1205 run("dmarc-reports@mox.example", report, n)
1207 // We always store as an evaluation, but as optional for reports.
1208 evals := checkEvaluationCount(t, 2)
1209 tcompare(t, evals[0].Optional, true)
1210 tcompare(t, evals[1].Optional, true)
1212 // Not a dmarc recipient, delivery should succeed.
1213 run("mjl@mox.example", report, n)
1214 run("mjl-test@mox.example", report, n)
1215 run("mjl+test@mox.example", report, n)
1216 // Likewise, address that is prefix of reporting address.
1217 run("dmarc@mox.example", report, n)
1218 run("Dmarc-test@mox.example", report, n)
1219 run("dmarc+test@mox.example", report, n)
1221 // Localpart catchall separators work for dmarc reporting addresses too.
1223 run("Dmarc-reports-test@mox.example", report, n)
1226 run("dmarc-Reports+test@mox.example", report, n)
1229const dmarcReport = `<?xml version="1.0" encoding="UTF-8" ?>
1232 <org_name>example.org</org_name>
1233 <email>postmaster@example.org</email>
1234 <report_id>1</report_id>
1236 <begin>1596412800</begin>
1237 <end>1596499199</end>
1241 <domain>xmox.nl</domain>
1250 <source_ip>127.0.0.10</source_ip>
1253 <disposition>none</disposition>
1259 <header_from>xmox.nl</header_from>
1263 <domain>xmox.nl</domain>
1264 <result>pass</result>
1265 <selector>testsel</selector>
1268 <domain>xmox.nl</domain>
1269 <result>pass</result>
1276// Test accepting a TLS report.
1277func TestTLSReport(t *testing.T) {
1278 // Requires setting up DKIM.
1279 privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
1280 dkimRecord := dkim.Record{
1282 Hashes: []string{"sha256"},
1283 Flags: []string{"s"},
1284 PublicKey: privKey.Public(),
1287 dkimTxt, err := dkimRecord.Record()
1288 tcheck(t, err, "dkim record")
1290 sel := config.Selector{
1291 HashEffective: "sha256",
1292 HeadersEffective: []string{"From", "To", "Subject", "Date"},
1294 Domain: dns.Domain{ASCII: "testsel"},
1296 dkimConf := config.DKIM{
1297 Selectors: map[string]config.Selector{"testsel": sel},
1298 Sign: []string{"testsel"},
1301 resolver := &dns.MockResolver{
1302 A: map[string][]string{
1303 "example.org.": {"127.0.0.10"}, // For mx check.
1305 TXT: map[string][]string{
1306 "testsel._domainkey.example.org.": {dkimTxt},
1307 "_dmarc.example.org.": {"v=DMARC1;p=reject;rua=mailto:dmarcrpt@example.org"},
1309 PTR: map[string][]string{
1310 "127.0.0.10": {"example.org."}, // For iprev check.
1313 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/tlsrpt/mox.conf"), resolver)
1316 run := func(rcptTo, tlsrpt string, n int) {
1318 ts.run(func(client *smtpclient.Client) {
1321 mailFrom := "remote@example.org"
1323 msgb := &bytes.Buffer{}
1324 _, 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)
1325 tcheck(t, xerr, "write msg")
1326 msg := msgb.String()
1328 selectors := mox.DKIMSelectors(dkimConf)
1329 headers, xerr := dkim.Sign(ctxbg, pkglog.Logger, "remote", dns.Domain{ASCII: "example.org"}, selectors, false, strings.NewReader(msg))
1330 tcheck(t, xerr, "dkim sign")
1333 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1334 tcheck(t, err, "deliver")
1336 records, err := tlsrptdb.Records(ctxbg)
1337 tcheck(t, err, "tlsrptdb records")
1338 if len(records) != n {
1339 t.Fatalf("got %d tlsrptdb records, expected %d", len(records), n)
1344 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}}]}`
1347 run("tls-reports@mox.example", tlsrpt, n) // Wrong domain in report.
1349 tlsrptdom := strings.ReplaceAll(tlsrpt, "xmox.nl", "mox.example")
1351 run("tls-reports@mox.example", tlsrptdom, n)
1353 tlsrpthost := strings.ReplaceAll(tlsrpt, "xmox.nl", "mailhost.mox.example")
1355 run("tls-reports@mailhost.mox.example", tlsrpthost, n)
1357 // We always store as an evaluation, but as optional for reports.
1358 evals := checkEvaluationCount(t, 3)
1359 tcompare(t, evals[0].Optional, true)
1360 tcompare(t, evals[1].Optional, true)
1361 tcompare(t, evals[2].Optional, true)
1363 // Catchall separators work for reporting address too.
1365 run("Tls-reports+more@mox.example", tlsrptdom, n)
1367 run("tls-Reports-more@mox.example", tlsrptdom, n)
1369 // Non-reporting addresses, mail accepted, but not as report.
1370 run("mjl@mox.example", tlsrptdom, n)
1371 run("Mjl-other@mox.example", tlsrptdom, n)
1372 run("mjl+other@mox.example", tlsrptdom, n)
1373 // Likewise, address that is prefix of reporting address.
1374 run("tls@mox.example", tlsrptdom, n)
1375 run("Tls-other@mox.example", tlsrptdom, n)
1376 run("tls+other@mox.example", tlsrptdom, n)
1379func TestRatelimitConnectionrate(t *testing.T) {
1380 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1383 // We'll be creating 300 connections, no TLS and reduce noise.
1384 ts.tlsmode = smtpclient.TLSSkip
1385 mlog.SetConfig(map[string]slog.Level{"": mlog.LevelInfo})
1386 defer mlog.SetConfig(map[string]slog.Level{"": mlog.LevelDebug})
1388 // We may be passing a window boundary during this tests. The limit is 300/minute.
1389 // So make twice that many connections and hope the tests don't take too long.
1390 for i := 0; i <= 2*300; i++ {
1391 ts.runx(func(err error, client *smtpclient.Client) {
1393 if err != nil && i < 300 {
1394 t.Fatalf("expected smtp connection, got %v", err)
1396 if err == nil && i == 600 {
1397 t.Fatalf("expected no smtp connection due to connection rate limit, got connection")
1406func TestRatelimitAuth(t *testing.T) {
1407 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1410 ts.submission = true
1411 ts.tlsmode = smtpclient.TLSSkip
1415 // We may be passing a window boundary during this tests. The limit is 10 auth
1416 // failures/minute. So make twice that many connections and hope the tests don't
1418 for i := 0; i <= 2*10; i++ {
1419 ts.runx(func(err error, client *smtpclient.Client) {
1422 t.Fatalf("got auth success with bad credentials")
1424 var cerr smtpclient.Error
1425 badauth := errors.As(err, &cerr) && cerr.Code == smtp.C535AuthBadCreds
1426 if !badauth && i < 10 {
1427 t.Fatalf("expected auth failure, got %v", err)
1429 if badauth && i == 20 {
1430 t.Fatalf("expected no smtp connection due to failed auth rate limit, got other error %v", err)
1439func TestRatelimitDelivery(t *testing.T) {
1440 resolver := dns.MockResolver{
1441 A: map[string][]string{
1442 "example.org.": {"127.0.0.10"}, // For mx check.
1444 PTR: map[string][]string{
1445 "127.0.0.10": {"example.org."},
1448 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1451 orig := limitIPMasked1MessagesPerMinute
1452 limitIPMasked1MessagesPerMinute = 1
1454 limitIPMasked1MessagesPerMinute = orig
1457 ts.run(func(client *smtpclient.Client) {
1458 mailFrom := "remote@example.org"
1459 rcptTo := "mjl@mox.example"
1460 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1461 tcheck(t, err, "deliver to remote")
1463 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1464 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C452StorageFull, Secode: smtp.SeMailbox2Full2})
1467 limitIPMasked1MessagesPerMinute = orig
1469 origSize := limitIPMasked1SizePerMinute
1470 // Message was already delivered once. We'll do another one. But the 3rd will fail.
1471 // We need the actual size with prepended headers, since that is used in the
1473 msg, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).Get()
1475 t.Fatalf("getting delivered message for its size: %v", err)
1477 limitIPMasked1SizePerMinute = 2*msg.Size + int64(len(deliverMessage)/2)
1479 limitIPMasked1SizePerMinute = origSize
1481 ts.run(func(client *smtpclient.Client) {
1482 mailFrom := "remote@example.org"
1483 rcptTo := "mjl@mox.example"
1484 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1485 tcheck(t, err, "deliver to remote")
1487 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1488 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C452StorageFull, Secode: smtp.SeMailbox2Full2})
1492func TestNonSMTP(t *testing.T) {
1493 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1497 serverConn, clientConn := net.Pipe()
1498 defer serverConn.Close()
1499 serverdone := make(chan struct{})
1500 defer func() { <-serverdone }()
1503 tlsConfig := &tls.Config{
1504 Certificates: []tls.Certificate{fakeCert(ts.t, false)},
1506 serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, tlsConfig, serverConn, ts.resolver, ts.submission, false, false, false, 100<<20, false, false, false, ts.dnsbls, 0)
1510 defer clientConn.Close()
1512 buf := make([]byte, 128)
1514 // Read and ignore hello.
1515 if _, err := clientConn.Read(buf); err != nil {
1516 t.Fatalf("reading hello: %v", err)
1519 if _, err := fmt.Fprintf(clientConn, "bogus\r\n"); err != nil {
1520 t.Fatalf("write command: %v", err)
1522 n, err := clientConn.Read(buf)
1524 t.Fatalf("read response line: %v", err)
1526 s := string(buf[:n])
1527 if !strings.HasPrefix(s, "500 5.5.2 ") {
1528 t.Fatalf(`got %q, expected "500 5.5.2 ...`, s)
1530 if _, err := clientConn.Read(buf); err == nil {
1531 t.Fatalf("connection not closed after bogus command")
1535// Test limits on outgoing messages.
1536func TestLimitOutgoing(t *testing.T) {
1537 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpserversendlimit/mox.conf"), dns.MockResolver{})
1540 ts.user = "mjl@mox.example"
1542 ts.submission = true
1544 err := ts.acc.DB.Insert(ctxbg, &store.Outgoing{Recipient: "a@other.example", Submitted: time.Now().Add(-24*time.Hour - time.Minute)})
1545 tcheck(t, err, "inserting outgoing/recipient past 24h window")
1547 testSubmit := func(rcptTo string, expErr *smtpclient.Error) {
1549 ts.run(func(client *smtpclient.Client) {
1551 mailFrom := "mjl@mox.example"
1552 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
1553 ts.smtpErr(err, expErr)
1557 // Limits are set to 4 messages a day, 2 first-time recipients.
1558 testSubmit("b@other.example", nil)
1559 testSubmit("c@other.example", nil)
1560 testSubmit("d@other.example", &smtpclient.Error{Code: smtp.C451LocalErr, Secode: smtp.SePol7DeliveryUnauth1}) // Would be 3rd recipient.
1561 testSubmit("b@other.example", nil)
1562 testSubmit("b@other.example", nil)
1563 testSubmit("b@other.example", &smtpclient.Error{Code: smtp.C451LocalErr, Secode: smtp.SePol7DeliveryUnauth1}) // Would be 5th message.
1566// Test account size limit enforcement.
1567func TestQuota(t *testing.T) {
1568 resolver := dns.MockResolver{
1569 A: map[string][]string{
1570 "other.example.": {"127.0.0.10"}, // For mx check.
1572 PTR: map[string][]string{
1573 "127.0.0.10": {"other.example."},
1576 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpserverquota/mox.conf"), resolver)
1579 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1580 ts.run(func(client *smtpclient.Client) {
1581 mailFrom := "mjl@other.example"
1582 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1583 ts.smtpErr(err, expErr)
1587 testDeliver("mjl@mox.example", &smtpclient.Error{Code: smtp.C452StorageFull, Secode: smtp.SeMailbox2Full2})
1590// Test with catchall destination address.
1591func TestCatchall(t *testing.T) {
1592 resolver := dns.MockResolver{
1593 A: map[string][]string{
1594 "other.example.": {"127.0.0.10"}, // For mx check.
1596 PTR: map[string][]string{
1597 "127.0.0.10": {"other.example."},
1600 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpservercatchall/mox.conf"), resolver)
1603 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1605 ts.run(func(client *smtpclient.Client) {
1607 mailFrom := "mjl@other.example"
1608 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
1609 ts.smtpErr(err, expErr)
1613 testDeliver("mjl@mox.example", nil) // Exact match.
1614 testDeliver("mjl+test@mox.example", nil) // Domain localpart catchall separator.
1615 testDeliver("MJL+TEST@mox.example", nil) // Again, and case insensitive.
1617 n, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).Count()
1618 tcheck(t, err, "checking delivered messages")
1621 testDeliver("unknown@mox.example", nil) // Catchall address, to account catchall.
1623 acc, err := store.OpenAccount(pkglog, "catchall", false)
1624 tcheck(t, err, "open account")
1629 n, err = bstore.QueryDB[store.Message](ctxbg, acc.DB).Count()
1630 tcheck(t, err, "checking delivered messages to catchall account")
1633 testDeliver("mjl-test@mox2.example", nil) // Second catchall separator.
1634 testDeliver("mjl-test+test@mox2.example", nil) // Silly, both separators in address.
1635 testDeliver("mjl+test-test@mox2.example", nil)
1636 n, err = bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).Count()
1637 tcheck(t, err, "checking delivered messages")
1641// Test DKIM signing for outgoing messages.
1642func TestDKIMSign(t *testing.T) {
1643 resolver := dns.MockResolver{
1644 A: map[string][]string{
1645 "mox.example.": {"127.0.0.10"}, // For mx check.
1647 PTR: map[string][]string{
1648 "127.0.0.10": {"mox.example."},
1652 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1655 // Set DKIM signing config.
1657 genDKIM := func(domain string) string {
1658 dom, _ := mox.Conf.Domain(dns.Domain{ASCII: domain})
1660 privkey := make([]byte, ed25519.SeedSize) // Fake key, don't use for real.
1662 privkey[0] = byte(gen)
1664 sel := config.Selector{
1665 HashEffective: "sha256",
1666 HeadersEffective: []string{"From", "To", "Subject"},
1667 Key: ed25519.NewKeyFromSeed(privkey),
1668 Domain: dns.Domain{ASCII: "testsel"},
1670 dom.DKIM = config.DKIM{
1671 Selectors: map[string]config.Selector{"testsel": sel},
1672 Sign: []string{"testsel"},
1674 mox.Conf.Dynamic.Domains[domain] = dom
1675 pubkey := sel.Key.Public().(ed25519.PublicKey)
1676 return "v=DKIM1;k=ed25519;p=" + base64.StdEncoding.EncodeToString(pubkey)
1679 dkimtxt := genDKIM("mox.example")
1680 dkimtxt2 := genDKIM("mox2.example")
1682 // DKIM verify needs to find the key.
1683 resolver.TXT = map[string][]string{
1684 "testsel._domainkey.mox.example.": {dkimtxt},
1685 "testsel._domainkey.mox2.example.": {dkimtxt2},
1688 ts.submission = true
1689 ts.user = "mjl@mox.example"
1693 testSubmit := func(mailFrom, msgFrom string) {
1695 ts.run(func(client *smtpclient.Client) {
1698 msg := strings.ReplaceAll(fmt.Sprintf(`From: <%s>
1699To: <remote@example.org>
1701Message-Id: <test@mox.example>
1704`, msgFrom), "\n", "\r\n")
1706 rcptTo := "remote@example.org"
1707 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1708 tcheck(t, err, "deliver")
1710 msgs, err := queue.List(ctxbg, queue.Filter{}, queue.Sort{})
1711 tcheck(t, err, "listing queue")
1713 tcompare(t, len(msgs), n)
1714 sort.Slice(msgs, func(i, j int) bool {
1715 return msgs[i].ID > msgs[j].ID
1717 f, err := queue.OpenMessage(ctxbg, msgs[0].ID)
1718 tcheck(t, err, "open message in queue")
1720 results, err := dkim.Verify(ctxbg, pkglog.Logger, resolver, false, dkim.DefaultPolicy, f, false)
1721 tcheck(t, err, "verifying dkim message")
1722 tcompare(t, len(results), 1)
1723 tcompare(t, results[0].Status, dkim.StatusPass)
1724 tcompare(t, results[0].Sig.Domain.ASCII, strings.Split(msgFrom, "@")[1])
1728 testSubmit("mjl@mox.example", "mjl@mox.example")
1729 testSubmit("mjl@mox.example", "mjl@mox2.example") // DKIM signature will be for mox2.example.
1732// Test to postmaster addresses.
1733func TestPostmaster(t *testing.T) {
1734 resolver := dns.MockResolver{
1735 A: map[string][]string{
1736 "other.example.": {"127.0.0.10"}, // For mx check.
1738 PTR: map[string][]string{
1739 "127.0.0.10": {"other.example."},
1742 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/postmaster/mox.conf"), resolver)
1745 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1747 ts.run(func(client *smtpclient.Client) {
1749 mailFrom := "mjl@other.example"
1750 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1751 ts.smtpErr(err, expErr)
1755 testDeliver("postmaster", nil) // Plain postmaster address without domain.
1756 testDeliver("postmaster@host.mox.example", nil) // Postmaster address with configured mail server hostname.
1757 testDeliver("postmaster@mox.example", nil) // Postmaster address without explicitly configured destination.
1758 testDeliver("postmaster@unknown.example", &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
1761// Test to address with empty localpart.
1762func TestEmptylocalpart(t *testing.T) {
1763 resolver := dns.MockResolver{
1764 A: map[string][]string{
1765 "other.example.": {"127.0.0.10"}, // For mx check.
1767 PTR: map[string][]string{
1768 "127.0.0.10": {"other.example."},
1771 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1774 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1775 ts.run(func(client *smtpclient.Client) {
1776 mailFrom := `""@other.example`
1777 msg := strings.ReplaceAll(deliverMessage, "To: <mjl@mox.example>", `To: <""@mox.example>`)
1778 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1779 ts.smtpErr(err, expErr)
1783 testDeliver(`""@mox.example`, nil)
1786// Test handling REQUIRETLS and TLS-Required: No.
1787func TestRequireTLS(t *testing.T) {
1788 resolver := dns.MockResolver{
1789 A: map[string][]string{
1790 "mox.example.": {"127.0.0.10"}, // For mx check.
1792 PTR: map[string][]string{
1793 "127.0.0.10": {"mox.example."},
1797 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1800 ts.submission = true
1801 ts.requiretls = true
1802 ts.user = "mjl@mox.example"
1808 msg0 := strings.ReplaceAll(`From: <mjl@mox.example>
1809To: <remote@example.org>
1811Message-Id: <test@mox.example>
1817 msg1 := strings.ReplaceAll(`From: <mjl@mox.example>
1818To: <remote@example.org>
1820Message-Id: <test@mox.example>
1827 msg2 := strings.ReplaceAll(`From: <mjl@mox.example>
1828To: <remote@example.org>
1830Message-Id: <test@mox.example>
1835 testSubmit := func(msg string, requiretls bool, expRequireTLS *bool) {
1837 ts.run(func(client *smtpclient.Client) {
1840 rcptTo := "remote@example.org"
1841 err := client.Deliver(ctxbg, "mjl@mox.example", rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, requiretls)
1842 tcheck(t, err, "deliver")
1844 msgs, err := queue.List(ctxbg, queue.Filter{}, queue.Sort{})
1845 tcheck(t, err, "listing queue")
1846 tcompare(t, len(msgs), 1)
1847 tcompare(t, msgs[0].RequireTLS, expRequireTLS)
1848 _, err = queue.Drop(ctxbg, pkglog, queue.Filter{IDs: []int64{msgs[0].ID}})
1849 tcheck(t, err, "deleting message from queue")
1853 testSubmit(msg0, true, &yes) // Header ignored, requiretls applied.
1854 testSubmit(msg0, false, &no) // TLS-Required header applied.
1855 testSubmit(msg1, true, &yes) // Bad headers ignored, requiretls applied.
1856 testSubmit(msg1, false, nil) // Inconsistent multiple headers ignored.
1857 testSubmit(msg2, false, nil) // Regular message, no RequireTLS setting.
1858 testSubmit(msg2, true, &yes) // Requiretls applied.
1860 // Check that we get an error if remote SMTP server does not support the requiretls
1862 ts.requiretls = false
1863 ts.run(func(client *smtpclient.Client) {
1864 rcptTo := "remote@example.org"
1865 err := client.Deliver(ctxbg, "mjl@mox.example", rcptTo, int64(len(msg0)), strings.NewReader(msg0), false, false, true)
1867 t.Fatalf("delivered with requiretls to server without requiretls")
1869 if !errors.Is(err, smtpclient.ErrRequireTLSUnsupported) {
1870 t.Fatalf("got err %v, expected ErrRequireTLSUnsupported", err)
1875func TestSmuggle(t *testing.T) {
1876 resolver := dns.MockResolver{
1877 A: map[string][]string{
1878 "example.org.": {"127.0.0.10"}, // For mx check.
1880 PTR: map[string][]string{
1881 "127.0.0.10": {"example.org."}, // For iprev check.
1884 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpsmuggle/mox.conf"), resolver)
1885 ts.tlsmode = smtpclient.TLSSkip
1888 test := func(data string) {
1891 ts.runRaw(func(conn net.Conn) {
1894 ourHostname := mox.Conf.Static.HostnameDomain
1895 remoteHostname := dns.Domain{ASCII: "mox.example"}
1896 opts := smtpclient.Opts{
1897 RootCAs: mox.Conf.Static.TLS.CertPool,
1899 log := pkglog.WithCid(ts.cid - 1)
1900 _, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, ts.tlspkix, ourHostname, remoteHostname, opts)
1901 tcheck(t, err, "smtpclient")
1904 write := func(s string) {
1905 _, err := conn.Write([]byte(s))
1906 tcheck(t, err, "write")
1909 readPrefixLine := func(prefix string) string {
1911 buf := make([]byte, 512)
1912 n, err := conn.Read(buf)
1913 tcheck(t, err, "read")
1914 s := strings.TrimRight(string(buf[:n]), "\r\n")
1915 if !strings.HasPrefix(s, prefix) {
1916 t.Fatalf("got smtp response %q, expected line with prefix %q", s, prefix)
1921 write("MAIL FROM:<remote@example.org>\r\n")
1923 write("RCPT TO:<mjl@mox.example>\r\n")
1928 write("\r\n") // Empty header.
1930 write("\r\n.\r\n") // End of message.
1931 line := readPrefixLine("5")
1932 if !strings.Contains(line, "smug") {
1933 t.Errorf("got 5xx error with message %q, expected error text containing smug", line)
1944func TestFutureRelease(t *testing.T) {
1945 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1946 ts.tlsmode = smtpclient.TLSSkip
1947 ts.user = "mjl@mox.example"
1949 ts.submission = true
1952 ts.auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
1953 return sasl.NewClientPlain(ts.user, ts.pass), nil
1956 test := func(mailtoMore, expResponsePrefix string) {
1959 ts.runRaw(func(conn net.Conn) {
1962 ourHostname := mox.Conf.Static.HostnameDomain
1963 remoteHostname := dns.Domain{ASCII: "mox.example"}
1964 opts := smtpclient.Opts{Auth: ts.auth}
1965 log := pkglog.WithCid(ts.cid - 1)
1966 _, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, false, ourHostname, remoteHostname, opts)
1967 tcheck(t, err, "smtpclient")
1970 write := func(s string) {
1971 _, err := conn.Write([]byte(s))
1972 tcheck(t, err, "write")
1975 readPrefixLine := func(prefix string) string {
1977 buf := make([]byte, 512)
1978 n, err := conn.Read(buf)
1979 tcheck(t, err, "read")
1980 s := strings.TrimRight(string(buf[:n]), "\r\n")
1981 if !strings.HasPrefix(s, prefix) {
1982 t.Fatalf("got smtp response %q, expected line with prefix %q", s, prefix)
1987 write(fmt.Sprintf("MAIL FROM:<mjl@mox.example>%s\r\n", mailtoMore))
1988 readPrefixLine(expResponsePrefix)
1989 if expResponsePrefix != "2" {
1992 write("RCPT TO:<mjl@mox.example>\r\n")
1997 write("From: <mjl@mox.example>\r\n\r\nbody\r\n\r\n.\r\n")
2002 test(" HOLDFOR=1", "2")
2003 test(" HOLDUNTIL="+time.Now().Add(time.Minute).UTC().Format(time.RFC3339), "2")
2004 test(" HOLDUNTIL="+time.Now().Add(time.Minute).UTC().Format(time.RFC3339Nano), "2")
2006 test(" HOLDFOR=0", "501") // 0 is invalid syntax.
2007 test(fmt.Sprintf(" HOLDFOR=%d", int64((queue.FutureReleaseIntervalMax+time.Minute)/time.Second)), "554") // Too far in the future.
2008 test(" HOLDUNTIL="+time.Now().Add(-time.Minute).UTC().Format(time.RFC3339), "554") // In the past.
2009 test(" HOLDUNTIL="+time.Now().Add(queue.FutureReleaseIntervalMax+time.Minute).UTC().Format(time.RFC3339), "554") // Too far in the future.
2010 test(" HOLDUNTIL=2024-02-10T17:28:00+00:00", "501") // "Z" required.
2011 test(" HOLDUNTIL=24-02-10T17:28:00Z", "501") // Invalid.
2012 test(" HOLDFOR=1 HOLDFOR=1", "501") // Duplicate.
2013 test(" HOLDFOR=1 HOLDUNTIL="+time.Now().Add(time.Hour).UTC().Format(time.RFC3339), "501") // Duplicate.
2017func TestSMTPUTF8(t *testing.T) {
2018 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
2021 ts.user = "mjl@mox.example"
2023 ts.submission = true
2025 test := func(mailFrom string, rcptTo string, headerValue string, filename string, clientSmtputf8 bool, expectedSmtputf8 bool, expErr *smtpclient.Error) {
2028 ts.run(func(client *smtpclient.Client) {
2030 msg := strings.ReplaceAll(fmt.Sprintf(`From: <%s>
2033X-Custom-Test-Header: %s
2035Content-type: multipart/mixed; boundary="simple boundary"
2038Content-Type: text/plain; charset=UTF-8;
2039Content-Disposition: attachment; filename="%s"
2040Content-Transfer-Encoding: base64
2042QW4gYXR0YWNoZWQgdGV4dCBmaWxlLg==
2045`, mailFrom, rcptTo, headerValue, filename), "\n", "\r\n")
2047 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), true, clientSmtputf8, false)
2048 ts.smtpErr(err, expErr)
2053 msgs, _ := queue.List(ctxbg, queue.Filter{}, queue.Sort{Field: "Queued", Asc: false})
2054 queuedMsg := msgs[0]
2055 if queuedMsg.SMTPUTF8 != expectedSmtputf8 {
2056 t.Fatalf("[%s / %s / %s / %s] got SMTPUTF8 %t, expected %t", mailFrom, rcptTo, headerValue, filename, queuedMsg.SMTPUTF8, expectedSmtputf8)
2061 test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", false, false, nil)
2062 test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", true, false, nil)
2063 test(`mjl@mox.example`, `🙂@example.org`, "header-ascii", "ascii.txt", true, true, nil)
2064 test(`mjl@mox.example`, `🙂@example.org`, "header-ascii", "ascii.txt", false, true, &smtpclient.Error{Permanent: true, Code: smtp.C553BadMailbox, Secode: smtp.SeMsg6NonASCIIAddrNotPermitted7})
2065 test(`Ω@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", true, true, nil)
2066 test(`Ω@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", false, true, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeMsg6NonASCIIAddrNotPermitted7})
2067 test(`mjl@mox.example`, `remote@example.org`, "header-utf8-😍", "ascii.txt", true, true, nil)
2068 test(`mjl@mox.example`, `remote@example.org`, "header-utf8-😍", "ascii.txt", false, true, nil)
2069 test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "utf8-🫠️.txt", true, true, nil)
2070 test(`Ω@mox.example`, `🙂@example.org`, "header-utf8-😍", "utf8-🫠️.txt", true, true, nil)
2071 test(`mjl@mox.example`, `remote@xn--vg8h.example.org`, "header-ascii", "ascii.txt", true, false, nil)
2074// TestExtra checks whether submission of messages with "X-Mox-Extra-<key>: value"
2075// headers cause those those key/value pairs to be added to the Extra field in the
2077func TestExtra(t *testing.T) {
2078 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
2081 ts.user = "mjl@mox.example"
2083 ts.submission = true
2085 extraMsg := strings.ReplaceAll(`From: <mjl@mox.example>
2086To: <remote@example.org>
2088X-Mox-Extra-Test: testvalue
2091X-Mox-Extra-x-cANONICAL-z: ok
2092Message-Id: <test@mox.example>
2097 ts.run(func(client *smtpclient.Client) {
2098 mailFrom := "mjl@mox.example"
2099 rcptTo := "mjl@mox.example"
2100 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2101 tcheck(t, err, "deliver")
2103 msgs, err := queue.List(ctxbg, queue.Filter{}, queue.Sort{})
2104 tcheck(t, err, "queue list")
2105 tcompare(t, len(msgs), 1)
2106 tcompare(t, msgs[0].Extra, map[string]string{
2107 "Test": "testvalue",
2110 "X-Canonical-Z": "ok",
2112 // note: these headers currently stay in the message.
2115// TestExtraDup checks for an error for duplicate x-mox-extra-* keys.
2116func TestExtraDup(t *testing.T) {
2117 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
2120 ts.user = "mjl@mox.example"
2122 ts.submission = true
2124 extraMsg := strings.ReplaceAll(`From: <mjl@mox.example>
2125To: <remote@example.org>
2127X-Mox-Extra-Test: testvalue
2128X-Mox-Extra-Test: testvalue
2129Message-Id: <test@mox.example>
2134 ts.run(func(client *smtpclient.Client) {
2135 mailFrom := "mjl@mox.example"
2136 rcptTo := "mjl@mox.example"
2137 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2138 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C554TransactionFailed, Secode: smtp.SeMsg6Other0})
2142// FromID can be specified during submission, but must be unique, with single recipient.
2143func TestUniqueFromID(t *testing.T) {
2144 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpfromid/mox.conf"), dns.MockResolver{})
2147 ts.user = "mjl+fromid@mox.example"
2149 ts.submission = true
2151 extraMsg := strings.ReplaceAll(`From: <mjl@mox.example>
2152To: <remote@example.org>
2158 // Specify our own unique id when queueing.
2159 ts.run(func(client *smtpclient.Client) {
2160 mailFrom := "mjl+unique@mox.example"
2161 rcptTo := "mjl@mox.example"
2162 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2163 ts.smtpErr(err, nil)
2166 // But we can only use it once.
2167 ts.run(func(client *smtpclient.Client) {
2168 mailFrom := "mjl+unique@mox.example"
2169 rcptTo := "mjl@mox.example"
2170 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2171 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C554TransactionFailed, Secode: smtp.SeAddr1SenderSyntax7})
2174 // We cannot use our own fromid with multiple recipients.
2175 ts.run(func(client *smtpclient.Client) {
2176 mailFrom := "mjl+unique2@mox.example"
2177 rcptTo := []string{"mjl@mox.example", "mjl@mox.example"}
2178 _, err := client.DeliverMultiple(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2179 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C554TransactionFailed, Secode: smtp.SeProto5TooManyRcpts3})
2183// TestDestinationSMTPError checks delivery to a destination with an SMTPError is rejected as configured.
2184func TestDestinationSMTPError(t *testing.T) {
2185 resolver := dns.MockResolver{
2186 A: map[string][]string{
2187 "example.org.": {"127.0.0.10"}, // For mx check.
2191 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
2194 ts.run(func(client *smtpclient.Client) {
2195 mailFrom := "mjl@example.org"
2196 rcptTo := "blocked@mox.example"
2197 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
2198 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
2202// TestDestinationMessageAuthRequiredSMTPError checks delivery to a destination
2203// with an MessageAuthRequiredSMTPError is accepted/rejected as configured.
2204func TestDestinationMessageAuthRequiredSMTPError(t *testing.T) {
2205 resolver := dns.MockResolver{
2206 A: map[string][]string{
2207 "example.org.": {"127.0.0.10"}, // For mx check.
2209 PTR: map[string][]string{
2210 "127.0.0.10": {"example.org."},
2212 TXT: map[string][]string{},
2215 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
2218 ts.run(func(client *smtpclient.Client) {
2219 mailFrom := "mjl@example.org"
2220 rcptTo := "msgauthrequired@mox.example"
2221 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
2222 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7MultiAuthFails26})
2225 // Ensure SPF pass, message should now be accepted.
2226 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
2227 ts.run(func(client *smtpclient.Client) {
2228 mailFrom := "mjl@example.org"
2229 rcptTo := "msgauthrequired@mox.example"
2230 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
2231 ts.smtpErr(err, nil)