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")
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(helloErr error, client *smtpclient.Client)) {
192 ts.runRaw(func(conn net.Conn) {
196 if auth == nil && ts.user != "" {
197 auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
198 return sasl.NewClientPlain(ts.user, ts.pass), nil
202 ourHostname := mox.Conf.Static.HostnameDomain
203 remoteHostname := dns.Domain{ASCII: "mox.example"}
204 opts := smtpclient.Opts{
206 RootCAs: mox.Conf.Static.TLS.CertPool,
207 ClientCert: ts.clientCert,
209 log := pkglog.WithCid(ts.cid - 1)
210 client, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, ts.tlspkix, ourHostname, remoteHostname, opts)
220func (ts *testserver) runRaw(fn func(clientConn net.Conn)) {
225 serverConn, clientConn := net.Pipe()
226 defer serverConn.Close()
227 // clientConn is closed as part of closing client.
228 serverdone := make(chan struct{})
229 defer func() { <-serverdone }()
232 serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, ts.serverConfig, serverConn, ts.resolver, ts.submission, ts.immediateTLS, 100<<20, false, false, ts.requiretls, ts.dnsbls, 0)
237 clientConn = tls.Client(clientConn, ts.clientConfig)
243func (ts *testserver) smtpErr(err error, expErr *smtpclient.Error) {
246 var cerr smtpclient.Error
247 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) {
248 t.Fatalf("got err:\n%#v (%q)\nexpected:\n%#v", err, err, expErr)
252// Just a cert that appears valid. SMTP client will not verify anything about it
253// (that is opportunistic TLS for you, "better some than none"). Let's enjoy this
254// one moment where it makes life easier.
255func fakeCert(t *testing.T, randomkey bool) tls.Certificate {
256 seed := make([]byte, ed25519.SeedSize)
258 cryptorand.Read(seed)
260 privKey := ed25519.NewKeyFromSeed(seed) // Fake key, don't use this for real!
261 template := &x509.Certificate{
262 SerialNumber: big.NewInt(1), // Required field...
263 // Valid period is needed to get session resumption enabled.
264 NotBefore: time.Now().Add(-time.Minute),
265 NotAfter: time.Now().Add(time.Hour),
267 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
269 t.Fatalf("making certificate: %s", err)
271 cert, err := x509.ParseCertificate(localCertBuf)
273 t.Fatalf("parsing generated certificate: %s", err)
275 c := tls.Certificate{
276 Certificate: [][]byte{localCertBuf},
283// check expected dmarc evaluations for outgoing aggregate reports.
284func checkEvaluationCount(t *testing.T, n int) []dmarcdb.Evaluation {
286 l, err := dmarcdb.Evaluations(ctxbg)
287 tcheck(t, err, "get dmarc evaluations")
288 tcompare(t, len(l), n)
292// Test submission from authenticated user.
293func TestSubmission(t *testing.T) {
294 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
297 // Set DKIM signing config.
298 dom, _ := mox.Conf.Domain(dns.Domain{ASCII: "mox.example"})
299 sel := config.Selector{
300 HashEffective: "sha256",
301 HeadersEffective: []string{"From", "To", "Subject"},
302 Key: ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)), // Fake key, don't use for real.
303 Domain: dns.Domain{ASCII: "mox.example"},
305 dom.DKIM = config.DKIM{
306 Selectors: map[string]config.Selector{"testsel": sel},
307 Sign: []string{"testsel"},
309 mox.Conf.Dynamic.Domains["mox.example"] = dom
311 testAuth := func(authfn func(user, pass string, cs *tls.ConnectionState) sasl.Client, user, pass string, expErr *smtpclient.Error) {
314 ts.auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
315 return authfn(user, pass, cs), nil
320 ts.run(func(err error, client *smtpclient.Client) {
322 mailFrom := "mjl@mox.example"
323 rcptTo := "remote@example.org"
325 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
327 var cerr smtpclient.Error
328 if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Code != expErr.Code || cerr.Secode != expErr.Secode) {
329 t.Fatalf("got err:\n%#v (%q)\nexpected:\n%#v", err, err, expErr)
331 checkEvaluationCount(t, 0)
336 testAuth(nil, "", "", &smtpclient.Error{Permanent: true, Code: smtp.C530SecurityRequired, Secode: smtp.SePol7Other0})
337 authfns := []func(user, pass string, cs *tls.ConnectionState) sasl.Client{
338 func(user, pass string, cs *tls.ConnectionState) sasl.Client { return sasl.NewClientPlain(user, pass) },
339 func(user, pass string, cs *tls.ConnectionState) sasl.Client { return sasl.NewClientLogin(user, pass) },
340 func(user, pass string, cs *tls.ConnectionState) sasl.Client { return sasl.NewClientCRAMMD5(user, pass) },
341 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
342 return sasl.NewClientSCRAMSHA1(user, pass, false)
344 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
345 return sasl.NewClientSCRAMSHA256(user, pass, false)
347 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
348 return sasl.NewClientSCRAMSHA1PLUS(user, pass, *cs)
350 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
351 return sasl.NewClientSCRAMSHA256PLUS(user, pass, *cs)
354 for _, fn := range authfns {
355 testAuth(fn, "mjl@mox.example", "test", &smtpclient.Error{Code: smtp.C535AuthBadCreds, Secode: smtp.SePol7AuthBadCreds8}) // Bad (short) password.
356 testAuth(fn, "mjl@mox.example", password0+"test", &smtpclient.Error{Code: smtp.C535AuthBadCreds, Secode: smtp.SePol7AuthBadCreds8}) // Bad password.
357 testAuth(fn, "mjl@mox.example", password0, nil)
358 testAuth(fn, "mjl@mox.example", password1, nil)
359 testAuth(fn, "móx@mox.example", password0, nil)
360 testAuth(fn, "móx@mox.example", password1, nil)
361 testAuth(fn, "mo\u0301x@mox.example", password0, nil)
362 testAuth(fn, "mo\u0301x@mox.example", password1, nil)
365 // Create a certificate, register its public key with account, and make a tls
366 // client config that sends the certificate.
367 clientCert0 := fakeCert(ts.t, true)
368 tlspubkey, err := store.ParseTLSPublicKeyCert(clientCert0.Certificate[0])
369 tcheck(t, err, "parse certificate")
370 tlspubkey.Account = "mjl"
371 tlspubkey.LoginAddress = "mjl@mox.example"
372 err = store.TLSPublicKeyAdd(ctxbg, &tlspubkey)
373 tcheck(t, err, "add tls public key to account")
374 ts.immediateTLS = true
375 ts.clientConfig = &tls.Config{
376 InsecureSkipVerify: true,
377 Certificates: []tls.Certificate{
382 // No explicit address in EXTERNAL.
383 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
384 return sasl.NewClientExternal(user)
387 // Same username in EXTERNAL as configured for key.
388 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
389 return sasl.NewClientExternal(user)
390 }, "mjl@mox.example", "", nil)
392 // Different username in EXTERNAL as configured for key, but same account.
393 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
394 return sasl.NewClientExternal(user)
395 }, "móx@mox.example", "", nil)
397 // Different username as configured for key, but same account, but not EXTERNAL auth.
398 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
399 return sasl.NewClientSCRAMSHA256PLUS(user, pass, *cs)
400 }, "móx@mox.example", password0, nil)
402 // Different account results in error.
403 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
404 return sasl.NewClientExternal(user)
405 }, "☺@mox.example", "", &smtpclient.Error{Code: smtp.C535AuthBadCreds, Secode: smtp.SePol7AuthBadCreds8})
407 // Starttls with client cert should authenticate too.
408 ts.immediateTLS = false
409 ts.clientCert = &clientCert0
410 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
411 return sasl.NewClientExternal(user)
413 ts.immediateTLS = true
416 // Add a client session cache, so our connections will be resumed. We are testing
417 // that the credentials are applied to resumed connections too.
418 ts.clientConfig.ClientSessionCache = tls.NewLRUClientSessionCache(10)
419 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
421 panic("tls connection was resumed")
423 return sasl.NewClientExternal(user)
425 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
427 panic("tls connection was not resumed")
429 return sasl.NewClientExternal(user)
432 // Unknown client certificate should fail the connection.
433 serverConn, clientConn := net.Pipe()
434 serverdone := make(chan struct{})
435 defer func() { <-serverdone }()
438 defer serverConn.Close()
439 tlsConfig := &tls.Config{
440 Certificates: []tls.Certificate{fakeCert(ts.t, false)},
442 serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, tlsConfig, serverConn, ts.resolver, ts.submission, ts.immediateTLS, 100<<20, false, false, false, ts.dnsbls, 0)
446 defer clientConn.Close()
448 // Authentication with an unknown/untrusted certificate should fail.
449 clientCert1 := fakeCert(ts.t, true)
450 ts.clientConfig.ClientSessionCache = nil
451 ts.clientConfig.Certificates = []tls.Certificate{
454 clientConn = tls.Client(clientConn, ts.clientConfig)
455 // note: It's not enough to do a handshake and check if that was successful. If the
456 // client cert is not acceptable, we only learn after the handshake, when the first
457 // data messages are exchanged.
458 buf := make([]byte, 100)
459 _, err = clientConn.Read(buf)
461 t.Fatalf("tls handshake with unknown client certificate succeeded")
463 if alert, ok := mox.AsTLSAlert(err); !ok || alert != 42 {
464 t.Fatalf("got err %#v, expected tls 'bad certificate' alert", err)
468// Test delivery from external MTA.
469func TestDelivery(t *testing.T) {
470 resolver := dns.MockResolver{
471 A: map[string][]string{
472 "example.org.": {"127.0.0.10"}, // For mx check.
474 PTR: map[string][]string{},
476 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
479 ts.run(func(err error, client *smtpclient.Client) {
480 mailFrom := "remote@example.org"
481 rcptTo := "mjl@127.0.0.10"
483 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
485 var cerr smtpclient.Error
486 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
487 t.Fatalf("deliver to ip address, got err %v, expected smtpclient.Error with code %d", err, smtp.C550MailboxUnavail)
491 ts.run(func(err error, client *smtpclient.Client) {
492 mailFrom := "remote@example.org"
493 rcptTo := "mjl@test.example" // Not configured as destination.
495 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
497 var cerr smtpclient.Error
498 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
499 t.Fatalf("deliver to unknown domain, got err %v, expected smtpclient.Error with code %d", err, smtp.C550MailboxUnavail)
503 ts.run(func(err error, client *smtpclient.Client) {
504 mailFrom := "remote@example.org"
505 rcptTo := "unknown@mox.example" // User unknown.
507 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
509 var cerr smtpclient.Error
510 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
511 t.Fatalf("deliver to unknown user for known domain, got err %v, expected smtpclient.Error with code %d", err, smtp.C550MailboxUnavail)
515 ts.run(func(err error, client *smtpclient.Client) {
516 mailFrom := "remote@example.org"
517 rcptTo := "mjl@mox.example"
519 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
521 var cerr smtpclient.Error
522 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
523 t.Fatalf("deliver from user without reputation, valid iprev required, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
527 // Set up iprev to get delivery from unknown user to be accepted.
528 resolver.PTR["127.0.0.10"] = []string{"example.org."}
530 // Only ascii o@ is configured, not the greek and cyrillic lookalikes.
531 ts.run(func(err error, client *smtpclient.Client) {
532 mailFrom := "remote@example.org"
533 rcptTo := "ο@mox.example" // omicron \u03bf, looks like the configured o@
534 msg := strings.ReplaceAll(deliverMessage, "mjl@mox.example", rcptTo)
536 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, true, false)
538 var cerr smtpclient.Error
539 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
540 t.Fatalf("deliver to omicron @ instead of ascii o @, got err %v, expected smtpclient.Error with code %d", err, smtp.C550MailboxUnavail)
544 ts.run(func(err error, client *smtpclient.Client) {
545 recipients := []string{
547 "o@mox.example", // ascii o, as configured
548 "\u2126@mox.example", // ohm sign, as configured
549 "ω@mox.example", // lower-case omega, we match case-insensitively and this is the lowercase of ohm (!)
550 "\u03a9@mox.example", // capital omega, also lowercased to omega.
551 "móx@mox.example", // NFC
552 "mo\u0301x@mox.example", // not NFC, but normalized as móx@, see https://go.dev/blog/normalization
555 for _, rcptTo := range recipients {
556 // Ensure SMTP RCPT TO and message address headers are the same, otherwise the junk
557 // filter treats us more strictly.
558 msg := strings.ReplaceAll(deliverMessage, "mjl@mox.example", rcptTo)
560 mailFrom := "remote@example.org"
562 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, true, false)
564 tcheck(t, err, "deliver to remote")
566 changes := make(chan []store.Change)
568 changes <- ts.comm.Get()
571 timer := time.NewTimer(time.Second)
576 t.Fatalf("no delivery in 1s")
581 checkEvaluationCount(t, 0)
584func tinsertmsg(t *testing.T, acc *store.Account, mailbox string, m *store.Message, msg string) {
585 mf, err := store.CreateMessageTemp(pkglog, "queue-dsn")
586 tcheck(t, err, "temp message")
587 defer os.Remove(mf.Name())
589 _, err = mf.Write([]byte(msg))
590 tcheck(t, err, "write message")
591 err = acc.DeliverMailbox(pkglog, mailbox, m, mf)
592 tcheck(t, err, "deliver message")
594 tcheck(t, err, "close message")
597func tretrain(t *testing.T, acc *store.Account) {
600 // Fresh empty junkfilter.
601 basePath := mox.DataDirPath("accounts")
602 dbPath := filepath.Join(basePath, acc.Name, "junkfilter.db")
603 bloomPath := filepath.Join(basePath, acc.Name, "junkfilter.bloom")
606 jf, _, err := acc.OpenJunkFilter(ctxbg, pkglog)
607 tcheck(t, err, "open junk filter")
610 // Fetch messags to retrain on.
611 q := bstore.QueryDB[store.Message](ctxbg, acc.DB)
612 q.FilterEqual("Expunged", false)
613 q.FilterFn(func(m store.Message) bool {
614 return m.Flags.Junk || m.Flags.Notjunk
616 msgs, err := q.List()
617 tcheck(t, err, "fetch messages")
619 // Retrain the messages.
620 for _, m := range msgs {
621 ham := m.Flags.Notjunk
623 f, err := os.Open(acc.MessagePath(m.ID))
624 tcheck(t, err, "open message")
625 r := store.FileMsgReader(m.MsgPrefix, f)
627 jf.TrainMessage(ctxbg, r, m.Size, ham)
630 tcheck(t, err, "close message")
634 tcheck(t, err, "save junkfilter")
637// Test accept/reject with DMARC reputation and with spammy content.
638func TestSpam(t *testing.T) {
639 resolver := &dns.MockResolver{
640 A: map[string][]string{
641 "example.org.": {"127.0.0.1"}, // For mx check.
643 TXT: map[string][]string{
644 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
645 "_dmarc.example.org.": {"v=DMARC1;p=reject; rua=mailto:dmarcrpt@example.org"},
648 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
651 // Insert spammy messages. No junkfilter training yet.
653 RemoteIP: "127.0.0.10",
654 RemoteIPMasked1: "127.0.0.10",
655 RemoteIPMasked2: "127.0.0.0",
656 RemoteIPMasked3: "127.0.0.0",
657 MailFrom: "remote@example.org",
658 MailFromLocalpart: smtp.Localpart("remote"),
659 MailFromDomain: "example.org",
660 RcptToLocalpart: smtp.Localpart("mjl"),
661 RcptToDomain: "mox.example",
662 MsgFromLocalpart: smtp.Localpart("remote"),
663 MsgFromDomain: "example.org",
664 MsgFromOrgDomain: "example.org",
665 MsgFromValidated: true,
666 MsgFromValidation: store.ValidationStrict,
667 Flags: store.Flags{Seen: true, Junk: true},
668 Size: int64(len(deliverMessage)),
670 for i := 0; i < 3; i++ {
672 tinsertmsg(t, ts.acc, "Inbox", &nm, deliverMessage)
675 // Delivery from sender with bad reputation should fail.
676 ts.run(func(err error, client *smtpclient.Client) {
677 mailFrom := "remote@example.org"
678 rcptTo := "mjl@mox.example"
680 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
682 var cerr smtpclient.Error
683 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
684 t.Fatalf("delivery by bad sender, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
687 ts.checkCount("Rejects", 1)
688 checkEvaluationCount(t, 0) // No positive interactions yet.
691 // Delivery from sender with bad reputation matching AcceptRejectsToMailbox should
692 // result in accepted delivery to the mailbox.
693 ts.run(func(err error, client *smtpclient.Client) {
694 mailFrom := "remote@example.org"
695 rcptTo := "mjl2@mox.example"
697 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage2)), strings.NewReader(deliverMessage2), false, false, false)
699 tcheck(t, err, "deliver")
701 ts.checkCount("mjl2junk", 1) // In ruleset rejects mailbox.
702 ts.checkCount("Rejects", 1) // Same as before.
703 checkEvaluationCount(t, 0) // This is not an actual accept.
706 // Mark the messages as having good reputation.
707 q := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB)
708 q.FilterEqual("Expunged", false)
709 _, err := q.UpdateFields(map[string]any{"Junk": false, "Notjunk": true})
710 tcheck(t, err, "update junkiness")
712 // Message should now be accepted.
713 ts.run(func(err error, client *smtpclient.Client) {
714 mailFrom := "remote@example.org"
715 rcptTo := "mjl@mox.example"
717 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
719 tcheck(t, err, "deliver")
721 // Message should now be removed from Rejects mailboxes.
722 ts.checkCount("Rejects", 0)
723 ts.checkCount("mjl2junk", 1)
724 checkEvaluationCount(t, 1)
727 // Undo dmarc pass, mark messages as junk, and train the filter.
729 q = bstore.QueryDB[store.Message](ctxbg, ts.acc.DB)
730 q.FilterEqual("Expunged", false)
731 _, err = q.UpdateFields(map[string]any{"Junk": true, "Notjunk": false})
732 tcheck(t, err, "update junkiness")
735 // Message should be refused for spammy content.
736 ts.run(func(err error, client *smtpclient.Client) {
737 mailFrom := "remote@example.org"
738 rcptTo := "mjl@mox.example"
740 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
742 var cerr smtpclient.Error
743 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
744 t.Fatalf("attempt to deliver spamy message, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
746 checkEvaluationCount(t, 1) // No new evaluation, this isn't a DMARC reject.
750// Test accept/reject with forwarded messages, DMARC ignored, no IP/EHLO/MAIL
751// FROM-based reputation.
752func TestForward(t *testing.T) {
753 // Do a run without forwarding, and with.
754 check := func(forward bool) {
756 resolver := &dns.MockResolver{
757 A: map[string][]string{
758 "bad.example.": {"127.0.0.1"}, // For mx check.
759 "good.example.": {"127.0.0.1"}, // For mx check.
760 "forward.example.": {"127.0.0.10"}, // For mx check.
762 TXT: map[string][]string{
763 "bad.example.": {"v=spf1 ip4:127.0.0.1 -all"},
764 "good.example.": {"v=spf1 ip4:127.0.0.1 -all"},
765 "forward.example.": {"v=spf1 ip4:127.0.0.10 -all"},
766 "_dmarc.bad.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@bad.example"},
767 "_dmarc.good.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@good.example"},
768 "_dmarc.forward.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@forward.example"},
770 PTR: map[string][]string{
771 "127.0.0.10": {"forward.example."}, // For iprev check.
774 rcptTo := "mjl3@mox.example"
776 // For SPF and DMARC pass, otherwise the test ends quickly.
777 resolver.TXT["bad.example."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
778 resolver.TXT["good.example."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
779 rcptTo = "mjl@mox.example" // Without IsForward rule.
782 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
785 totalEvaluations := 0
787 var msgBad = strings.ReplaceAll(`From: <remote@bad.example>
790Message-Id: <bad@example.org>
794 var msgOK = strings.ReplaceAll(`From: <remote@good.example>
797Message-Id: <good@example.org>
801 var msgOK2 = strings.ReplaceAll(`From: <other@forward.example>
804Message-Id: <regular@example.org>
806happens to come from forwarding mail server.
809 // Deliver forwarded messages, then classify as junk. Normally enough to treat
810 // other unrelated messages from IP as junk, but not for forwarded messages.
811 ts.run(func(err error, client *smtpclient.Client) {
812 tcheck(t, err, "connect")
814 mailFrom := "remote@forward.example"
816 mailFrom = "remote@bad.example"
819 for i := 0; i < 10; i++ {
820 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgBad)), strings.NewReader(msgBad), false, false, false)
821 tcheck(t, err, "deliver message")
823 totalEvaluations += 10
825 n, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).UpdateFields(map[string]any{"Junk": true, "MsgFromValidated": true})
826 tcheck(t, err, "marking messages as junk")
829 // Next delivery will fail, with negative "message From" signal.
830 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgBad)), strings.NewReader(msgBad), false, false, false)
831 var cerr smtpclient.Error
832 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
833 t.Fatalf("delivery by bad sender, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
836 checkEvaluationCount(t, totalEvaluations)
839 // Delivery from different "message From" without reputation, but from same
840 // forwarding email server, should succeed under forwarding, not as regular sending
842 ts.run(func(err error, client *smtpclient.Client) {
843 tcheck(t, err, "connect")
845 mailFrom := "remote@forward.example"
847 mailFrom = "remote@good.example"
850 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgOK)), strings.NewReader(msgOK), false, false, false)
852 tcheck(t, err, "deliver")
853 totalEvaluations += 1
855 var cerr smtpclient.Error
856 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
857 t.Fatalf("delivery by bad ip, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
860 checkEvaluationCount(t, totalEvaluations)
863 // Delivery from forwarding server that isn't a forward should get same treatment.
864 ts.run(func(err error, client *smtpclient.Client) {
865 tcheck(t, err, "connect")
867 mailFrom := "other@forward.example"
869 // Ensure To header matches.
872 msg = strings.ReplaceAll(msg, "<mjl@mox.example>", "<mjl3@mox.example>")
875 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
877 tcheck(t, err, "deliver")
878 totalEvaluations += 1
880 var cerr smtpclient.Error
881 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
882 t.Fatalf("delivery by bad ip, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
885 checkEvaluationCount(t, totalEvaluations)
893// Messages that we sent to, that have passing DMARC, but that are otherwise spammy, should be accepted.
894func TestDMARCSent(t *testing.T) {
895 resolver := &dns.MockResolver{
896 A: map[string][]string{
897 "example.org.": {"127.0.0.1"}, // For mx check.
899 TXT: map[string][]string{
900 "example.org.": {"v=spf1 ip4:127.0.0.1 -all"},
901 "_dmarc.example.org.": {"v=DMARC1;p=reject;rua=mailto:dmarcrpt@example.org"},
904 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
907 // First check that DMARC policy rejects message and results in optional evaluation.
908 ts.run(func(err error, client *smtpclient.Client) {
909 mailFrom := "remote@example.org"
910 rcptTo := "mjl@mox.example"
912 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
914 var cerr smtpclient.Error
915 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
916 t.Fatalf("attempt to deliver spamy message, got err %v, expected smtpclient.Error with code %d", err, smtp.C550MailboxUnavail)
918 l := checkEvaluationCount(t, 1)
919 tcompare(t, l[0].Optional, true)
922 // Update DNS for an SPF pass, and DMARC pass.
923 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
925 // Insert spammy messages not related to the test message.
927 MailFrom: "remote@test.example",
928 RcptToLocalpart: smtp.Localpart("mjl"),
929 RcptToDomain: "mox.example",
930 Flags: store.Flags{Seen: true, Junk: true},
931 Size: int64(len(deliverMessage)),
933 for i := 0; i < 3; i++ {
935 tinsertmsg(t, ts.acc, "Archive", &nm, deliverMessage)
939 // Baseline, message should be refused for spammy content.
940 ts.run(func(err error, client *smtpclient.Client) {
941 mailFrom := "remote@example.org"
942 rcptTo := "mjl@mox.example"
944 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
946 var cerr smtpclient.Error
947 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
948 t.Fatalf("attempt to deliver spamy message, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
950 checkEvaluationCount(t, 1) // No new evaluation.
953 // Insert a message that we sent to the address that is about to send to us.
954 sentMsg := store.Message{Size: int64(len(deliverMessage))}
955 tinsertmsg(t, ts.acc, "Sent", &sentMsg, deliverMessage)
956 err := ts.acc.DB.Insert(ctxbg, &store.Recipient{MessageID: sentMsg.ID, Localpart: "remote", Domain: "example.org", OrgDomain: "example.org", Sent: time.Now()})
957 tcheck(t, err, "inserting message recipient")
959 // Reject a message due to DMARC again. Since we sent a message to the domain, it
960 // is no longer unknown and we should see a non-optional evaluation that will
961 // result in a DMARC report.
962 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.1 -all"}
963 ts.run(func(err error, client *smtpclient.Client) {
964 mailFrom := "remote@example.org"
965 rcptTo := "mjl@mox.example"
967 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
969 var cerr smtpclient.Error
970 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
971 t.Fatalf("attempt to deliver spamy message, got err %v, expected smtpclient.Error with code %d", err, smtp.C550MailboxUnavail)
973 l := checkEvaluationCount(t, 2) // New evaluation.
974 tcompare(t, l[1].Optional, false)
977 // We should now be accepting the message because we recently sent a message.
978 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
979 ts.run(func(err error, client *smtpclient.Client) {
980 mailFrom := "remote@example.org"
981 rcptTo := "mjl@mox.example"
983 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
985 tcheck(t, err, "deliver")
986 l := checkEvaluationCount(t, 3) // New evaluation.
987 tcompare(t, l[2].Optional, false)
991// Test DNSBL, then getting through with subjectpass.
992func TestBlocklistedSubjectpass(t *testing.T) {
993 // Set up a DNSBL on dnsbl.example, and get DMARC pass.
994 resolver := &dns.MockResolver{
995 A: map[string][]string{
996 "example.org.": {"127.0.0.10"}, // For mx check.
997 "2.0.0.127.dnsbl.example.": {"127.0.0.2"}, // For healthcheck.
998 "10.0.0.127.dnsbl.example.": {"127.0.0.10"}, // Where our connection pretends to come from.
1000 TXT: map[string][]string{
1001 "10.0.0.127.dnsbl.example.": {"blocklisted"},
1002 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
1003 "_dmarc.example.org.": {"v=DMARC1;p=reject"},
1005 PTR: map[string][]string{
1006 "127.0.0.10": {"example.org."}, // For iprev check.
1009 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1010 ts.dnsbls = []dns.Domain{{ASCII: "dnsbl.example"}}
1013 // Message should be refused softly (temporary error) due to DNSBL.
1014 ts.run(func(err error, client *smtpclient.Client) {
1015 mailFrom := "remote@example.org"
1016 rcptTo := "mjl@mox.example"
1018 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1020 var cerr smtpclient.Error
1021 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
1022 t.Fatalf("attempted deliver from dnsblocklisted ip, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
1026 // Set up subjectpass on account.
1027 acc := mox.Conf.Dynamic.Accounts[ts.acc.Name]
1028 acc.SubjectPass.Period = time.Hour
1029 mox.Conf.Dynamic.Accounts[ts.acc.Name] = acc
1031 // Message should be refused quickly (permanent error) due to DNSBL and Subjectkey.
1033 ts.run(func(err error, client *smtpclient.Client) {
1034 mailFrom := "remote@example.org"
1035 rcptTo := "mjl@mox.example"
1037 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1039 var cerr smtpclient.Error
1040 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
1041 t.Fatalf("attempted deliver from dnsblocklisted ip, got err %v, expected smtpclient.Error with code %d", err, smtp.C550MailboxUnavail)
1043 i := strings.Index(cerr.Line, subjectpass.Explanation)
1045 t.Fatalf("got error line %q, expected error line with subjectpass", cerr.Line)
1047 pass = cerr.Line[i+len(subjectpass.Explanation):]
1050 ts.run(func(err error, client *smtpclient.Client) {
1051 mailFrom := "remote@example.org"
1052 rcptTo := "mjl@mox.example"
1053 passMessage := strings.Replace(deliverMessage, "Subject: test", "Subject: test "+pass, 1)
1055 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(passMessage)), strings.NewReader(passMessage), false, false, false)
1057 tcheck(t, err, "deliver with subjectpass")
1061// Test accepting a DMARC report.
1062func TestDMARCReport(t *testing.T) {
1063 resolver := &dns.MockResolver{
1064 A: map[string][]string{
1065 "example.org.": {"127.0.0.10"}, // For mx check.
1067 TXT: map[string][]string{
1068 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
1069 "_dmarc.example.org.": {"v=DMARC1;p=reject; rua=mailto:dmarcrpt@example.org"},
1071 PTR: map[string][]string{
1072 "127.0.0.10": {"example.org."}, // For iprev check.
1075 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/dmarcreport/mox.conf"), resolver)
1078 run := func(report string, n int) {
1080 ts.run(func(err error, client *smtpclient.Client) {
1083 tcheck(t, err, "run")
1085 mailFrom := "remote@example.org"
1086 rcptTo := "mjl@mox.example"
1088 msgb := &bytes.Buffer{}
1089 _, 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)
1090 tcheck(t, xerr, "write msg headers")
1091 w := quotedprintable.NewWriter(msgb)
1092 _, xerr = w.Write([]byte(strings.ReplaceAll(report, "\n", "\r\n")))
1093 tcheck(t, xerr, "write message")
1094 msg := msgb.String()
1097 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1099 tcheck(t, err, "deliver")
1101 records, err := dmarcdb.Records(ctxbg)
1102 tcheck(t, err, "dmarcdb records")
1103 if len(records) != n {
1104 t.Fatalf("got %d dmarcdb records, expected %d or more", len(records), n)
1110 run(strings.ReplaceAll(dmarcReport, "xmox.nl", "mox.example"), 1)
1112 // We always store as an evaluation, but as optional for reports.
1113 evals := checkEvaluationCount(t, 2)
1114 tcompare(t, evals[0].Optional, true)
1115 tcompare(t, evals[1].Optional, true)
1118const dmarcReport = `<?xml version="1.0" encoding="UTF-8" ?>
1121 <org_name>example.org</org_name>
1122 <email>postmaster@example.org</email>
1123 <report_id>1</report_id>
1125 <begin>1596412800</begin>
1126 <end>1596499199</end>
1130 <domain>xmox.nl</domain>
1139 <source_ip>127.0.0.10</source_ip>
1142 <disposition>none</disposition>
1148 <header_from>xmox.nl</header_from>
1152 <domain>xmox.nl</domain>
1153 <result>pass</result>
1154 <selector>testsel</selector>
1157 <domain>xmox.nl</domain>
1158 <result>pass</result>
1165// Test accepting a TLS report.
1166func TestTLSReport(t *testing.T) {
1167 // Requires setting up DKIM.
1168 privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
1169 dkimRecord := dkim.Record{
1171 Hashes: []string{"sha256"},
1172 Flags: []string{"s"},
1173 PublicKey: privKey.Public(),
1176 dkimTxt, err := dkimRecord.Record()
1177 tcheck(t, err, "dkim record")
1179 sel := config.Selector{
1180 HashEffective: "sha256",
1181 HeadersEffective: []string{"From", "To", "Subject", "Date"},
1183 Domain: dns.Domain{ASCII: "testsel"},
1185 dkimConf := config.DKIM{
1186 Selectors: map[string]config.Selector{"testsel": sel},
1187 Sign: []string{"testsel"},
1190 resolver := &dns.MockResolver{
1191 A: map[string][]string{
1192 "example.org.": {"127.0.0.10"}, // For mx check.
1194 TXT: map[string][]string{
1195 "testsel._domainkey.example.org.": {dkimTxt},
1196 "_dmarc.example.org.": {"v=DMARC1;p=reject;rua=mailto:dmarcrpt@example.org"},
1198 PTR: map[string][]string{
1199 "127.0.0.10": {"example.org."}, // For iprev check.
1202 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/tlsrpt/mox.conf"), resolver)
1205 run := func(rcptTo, tlsrpt string, n int) {
1207 ts.run(func(err error, client *smtpclient.Client) {
1210 mailFrom := "remote@example.org"
1212 msgb := &bytes.Buffer{}
1213 _, 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)
1214 tcheck(t, xerr, "write msg")
1215 msg := msgb.String()
1217 selectors := mox.DKIMSelectors(dkimConf)
1218 headers, xerr := dkim.Sign(ctxbg, pkglog.Logger, "remote", dns.Domain{ASCII: "example.org"}, selectors, false, strings.NewReader(msg))
1219 tcheck(t, xerr, "dkim sign")
1223 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1225 tcheck(t, err, "deliver")
1227 records, err := tlsrptdb.Records(ctxbg)
1228 tcheck(t, err, "tlsrptdb records")
1229 if len(records) != n {
1230 t.Fatalf("got %d tlsrptdb records, expected %d", len(records), n)
1235 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}}]}`
1237 run("mjl@mox.example", tlsrpt, 0)
1238 run("mjl@mox.example", strings.ReplaceAll(tlsrpt, "xmox.nl", "mox.example"), 1)
1239 run("mjl@mailhost.mox.example", strings.ReplaceAll(tlsrpt, "xmox.nl", "mailhost.mox.example"), 2)
1241 // We always store as an evaluation, but as optional for reports.
1242 evals := checkEvaluationCount(t, 3)
1243 tcompare(t, evals[0].Optional, true)
1244 tcompare(t, evals[1].Optional, true)
1245 tcompare(t, evals[2].Optional, true)
1248func TestRatelimitConnectionrate(t *testing.T) {
1249 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1252 // We'll be creating 300 connections, no TLS and reduce noise.
1253 ts.tlsmode = smtpclient.TLSSkip
1254 mlog.SetConfig(map[string]slog.Level{"": mlog.LevelInfo})
1255 defer mlog.SetConfig(map[string]slog.Level{"": mlog.LevelDebug})
1257 // We may be passing a window boundary during this tests. The limit is 300/minute.
1258 // So make twice that many connections and hope the tests don't take too long.
1259 for i := 0; i <= 2*300; i++ {
1260 ts.run(func(err error, client *smtpclient.Client) {
1262 if err != nil && i < 300 {
1263 t.Fatalf("expected smtp connection, got %v", err)
1265 if err == nil && i == 600 {
1266 t.Fatalf("expected no smtp connection due to connection rate limit, got connection")
1275func TestRatelimitAuth(t *testing.T) {
1276 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1279 ts.submission = true
1280 ts.tlsmode = smtpclient.TLSSkip
1284 // We may be passing a window boundary during this tests. The limit is 10 auth
1285 // failures/minute. So make twice that many connections and hope the tests don't
1287 for i := 0; i <= 2*10; i++ {
1288 ts.run(func(err error, client *smtpclient.Client) {
1291 t.Fatalf("got auth success with bad credentials")
1293 var cerr smtpclient.Error
1294 badauth := errors.As(err, &cerr) && cerr.Code == smtp.C535AuthBadCreds
1295 if !badauth && i < 10 {
1296 t.Fatalf("expected auth failure, got %v", err)
1298 if badauth && i == 20 {
1299 t.Fatalf("expected no smtp connection due to failed auth rate limit, got other error %v", err)
1308func TestRatelimitDelivery(t *testing.T) {
1309 resolver := dns.MockResolver{
1310 A: map[string][]string{
1311 "example.org.": {"127.0.0.10"}, // For mx check.
1313 PTR: map[string][]string{
1314 "127.0.0.10": {"example.org."},
1317 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1320 orig := limitIPMasked1MessagesPerMinute
1321 limitIPMasked1MessagesPerMinute = 1
1323 limitIPMasked1MessagesPerMinute = orig
1326 ts.run(func(err error, client *smtpclient.Client) {
1327 mailFrom := "remote@example.org"
1328 rcptTo := "mjl@mox.example"
1330 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1332 tcheck(t, err, "deliver to remote")
1334 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1335 var cerr smtpclient.Error
1336 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C452StorageFull {
1337 t.Fatalf("got err %v, expected smtpclient error with code 452 for storage full", err)
1341 limitIPMasked1MessagesPerMinute = orig
1343 origSize := limitIPMasked1SizePerMinute
1344 // Message was already delivered once. We'll do another one. But the 3rd will fail.
1345 // We need the actual size with prepended headers, since that is used in the
1347 msg, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).Get()
1349 t.Fatalf("getting delivered message for its size: %v", err)
1351 limitIPMasked1SizePerMinute = 2*msg.Size + int64(len(deliverMessage)/2)
1353 limitIPMasked1SizePerMinute = origSize
1355 ts.run(func(err error, client *smtpclient.Client) {
1356 mailFrom := "remote@example.org"
1357 rcptTo := "mjl@mox.example"
1359 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1361 tcheck(t, err, "deliver to remote")
1363 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1364 var cerr smtpclient.Error
1365 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C452StorageFull {
1366 t.Fatalf("got err %v, expected smtpclient error with code 452 for storage full", err)
1371func TestNonSMTP(t *testing.T) {
1372 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1376 serverConn, clientConn := net.Pipe()
1377 defer serverConn.Close()
1378 serverdone := make(chan struct{})
1379 defer func() { <-serverdone }()
1382 tlsConfig := &tls.Config{
1383 Certificates: []tls.Certificate{fakeCert(ts.t, false)},
1385 serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, tlsConfig, serverConn, ts.resolver, ts.submission, false, 100<<20, false, false, false, ts.dnsbls, 0)
1389 defer clientConn.Close()
1391 buf := make([]byte, 128)
1393 // Read and ignore hello.
1394 if _, err := clientConn.Read(buf); err != nil {
1395 t.Fatalf("reading hello: %v", err)
1398 if _, err := fmt.Fprintf(clientConn, "bogus\r\n"); err != nil {
1399 t.Fatalf("write command: %v", err)
1401 n, err := clientConn.Read(buf)
1403 t.Fatalf("read response line: %v", err)
1405 s := string(buf[:n])
1406 if !strings.HasPrefix(s, "500 5.5.2 ") {
1407 t.Fatalf(`got %q, expected "500 5.5.2 ...`, s)
1409 if _, err := clientConn.Read(buf); err == nil {
1410 t.Fatalf("connection not closed after bogus command")
1414// Test limits on outgoing messages.
1415func TestLimitOutgoing(t *testing.T) {
1416 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpserversendlimit/mox.conf"), dns.MockResolver{})
1419 ts.user = "mjl@mox.example"
1421 ts.submission = true
1423 err := ts.acc.DB.Insert(ctxbg, &store.Outgoing{Recipient: "a@other.example", Submitted: time.Now().Add(-24*time.Hour - time.Minute)})
1424 tcheck(t, err, "inserting outgoing/recipient past 24h window")
1426 testSubmit := func(rcptTo string, expErr *smtpclient.Error) {
1428 ts.run(func(err error, client *smtpclient.Client) {
1430 mailFrom := "mjl@mox.example"
1432 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
1434 ts.smtpErr(err, expErr)
1438 // Limits are set to 4 messages a day, 2 first-time recipients.
1439 testSubmit("b@other.example", nil)
1440 testSubmit("c@other.example", nil)
1441 testSubmit("d@other.example", &smtpclient.Error{Code: smtp.C451LocalErr, Secode: smtp.SePol7DeliveryUnauth1}) // Would be 3rd recipient.
1442 testSubmit("b@other.example", nil)
1443 testSubmit("b@other.example", nil)
1444 testSubmit("b@other.example", &smtpclient.Error{Code: smtp.C451LocalErr, Secode: smtp.SePol7DeliveryUnauth1}) // Would be 5th message.
1447// Test account size limit enforcement.
1448func TestQuota(t *testing.T) {
1449 resolver := dns.MockResolver{
1450 A: map[string][]string{
1451 "other.example.": {"127.0.0.10"}, // For mx check.
1453 PTR: map[string][]string{
1454 "127.0.0.10": {"other.example."},
1457 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpserverquota/mox.conf"), resolver)
1460 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1462 ts.run(func(err error, client *smtpclient.Client) {
1464 mailFrom := "mjl@other.example"
1466 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1468 ts.smtpErr(err, expErr)
1472 testDeliver("mjl@mox.example", &smtpclient.Error{Code: smtp.C452StorageFull, Secode: smtp.SeMailbox2Full2})
1475// Test with catchall destination address.
1476func TestCatchall(t *testing.T) {
1477 resolver := dns.MockResolver{
1478 A: map[string][]string{
1479 "other.example.": {"127.0.0.10"}, // For mx check.
1481 PTR: map[string][]string{
1482 "127.0.0.10": {"other.example."},
1485 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpservercatchall/mox.conf"), resolver)
1488 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1490 ts.run(func(err error, client *smtpclient.Client) {
1492 mailFrom := "mjl@other.example"
1494 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
1496 ts.smtpErr(err, expErr)
1500 testDeliver("mjl@mox.example", nil) // Exact match.
1501 testDeliver("mjl+test@mox.example", nil) // Domain localpart catchall separator.
1502 testDeliver("MJL+TEST@mox.example", nil) // Again, and case insensitive.
1503 testDeliver("unknown@mox.example", nil) // Catchall address, to account catchall.
1505 n, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).Count()
1506 tcheck(t, err, "checking delivered messages")
1509 acc, err := store.OpenAccount(pkglog, "catchall")
1510 tcheck(t, err, "open account")
1515 n, err = bstore.QueryDB[store.Message](ctxbg, acc.DB).Count()
1516 tcheck(t, err, "checking delivered messages to catchall account")
1520// Test DKIM signing for outgoing messages.
1521func TestDKIMSign(t *testing.T) {
1522 resolver := dns.MockResolver{
1523 A: map[string][]string{
1524 "mox.example.": {"127.0.0.10"}, // For mx check.
1526 PTR: map[string][]string{
1527 "127.0.0.10": {"mox.example."},
1531 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1534 // Set DKIM signing config.
1536 genDKIM := func(domain string) string {
1537 dom, _ := mox.Conf.Domain(dns.Domain{ASCII: domain})
1539 privkey := make([]byte, ed25519.SeedSize) // Fake key, don't use for real.
1541 privkey[0] = byte(gen)
1543 sel := config.Selector{
1544 HashEffective: "sha256",
1545 HeadersEffective: []string{"From", "To", "Subject"},
1546 Key: ed25519.NewKeyFromSeed(privkey),
1547 Domain: dns.Domain{ASCII: "testsel"},
1549 dom.DKIM = config.DKIM{
1550 Selectors: map[string]config.Selector{"testsel": sel},
1551 Sign: []string{"testsel"},
1553 mox.Conf.Dynamic.Domains[domain] = dom
1554 pubkey := sel.Key.Public().(ed25519.PublicKey)
1555 return "v=DKIM1;k=ed25519;p=" + base64.StdEncoding.EncodeToString(pubkey)
1558 dkimtxt := genDKIM("mox.example")
1559 dkimtxt2 := genDKIM("mox2.example")
1561 // DKIM verify needs to find the key.
1562 resolver.TXT = map[string][]string{
1563 "testsel._domainkey.mox.example.": {dkimtxt},
1564 "testsel._domainkey.mox2.example.": {dkimtxt2},
1567 ts.submission = true
1568 ts.user = "mjl@mox.example"
1572 testSubmit := func(mailFrom, msgFrom string) {
1574 ts.run(func(err error, client *smtpclient.Client) {
1577 msg := strings.ReplaceAll(fmt.Sprintf(`From: <%s>
1578To: <remote@example.org>
1580Message-Id: <test@mox.example>
1583`, msgFrom), "\n", "\r\n")
1585 rcptTo := "remote@example.org"
1587 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1589 tcheck(t, err, "deliver")
1591 msgs, err := queue.List(ctxbg, queue.Filter{}, queue.Sort{})
1592 tcheck(t, err, "listing queue")
1594 tcompare(t, len(msgs), n)
1595 sort.Slice(msgs, func(i, j int) bool {
1596 return msgs[i].ID > msgs[j].ID
1598 f, err := queue.OpenMessage(ctxbg, msgs[0].ID)
1599 tcheck(t, err, "open message in queue")
1601 results, err := dkim.Verify(ctxbg, pkglog.Logger, resolver, false, dkim.DefaultPolicy, f, false)
1602 tcheck(t, err, "verifying dkim message")
1603 tcompare(t, len(results), 1)
1604 tcompare(t, results[0].Status, dkim.StatusPass)
1605 tcompare(t, results[0].Sig.Domain.ASCII, strings.Split(msgFrom, "@")[1])
1609 testSubmit("mjl@mox.example", "mjl@mox.example")
1610 testSubmit("mjl@mox.example", "mjl@mox2.example") // DKIM signature will be for mox2.example.
1613// Test to postmaster addresses.
1614func TestPostmaster(t *testing.T) {
1615 resolver := dns.MockResolver{
1616 A: map[string][]string{
1617 "other.example.": {"127.0.0.10"}, // For mx check.
1619 PTR: map[string][]string{
1620 "127.0.0.10": {"other.example."},
1623 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/postmaster/mox.conf"), resolver)
1626 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1628 ts.run(func(err error, client *smtpclient.Client) {
1630 mailFrom := "mjl@other.example"
1632 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1634 ts.smtpErr(err, expErr)
1638 testDeliver("postmaster", nil) // Plain postmaster address without domain.
1639 testDeliver("postmaster@host.mox.example", nil) // Postmaster address with configured mail server hostname.
1640 testDeliver("postmaster@mox.example", nil) // Postmaster address without explicitly configured destination.
1641 testDeliver("postmaster@unknown.example", &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
1644// Test to address with empty localpart.
1645func TestEmptylocalpart(t *testing.T) {
1646 resolver := dns.MockResolver{
1647 A: map[string][]string{
1648 "other.example.": {"127.0.0.10"}, // For mx check.
1650 PTR: map[string][]string{
1651 "127.0.0.10": {"other.example."},
1654 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1657 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1659 ts.run(func(err error, client *smtpclient.Client) {
1662 mailFrom := `""@other.example`
1663 msg := strings.ReplaceAll(deliverMessage, "To: <mjl@mox.example>", `To: <""@mox.example>`)
1665 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1667 ts.smtpErr(err, expErr)
1671 testDeliver(`""@mox.example`, nil)
1674// Test handling REQUIRETLS and TLS-Required: No.
1675func TestRequireTLS(t *testing.T) {
1676 resolver := dns.MockResolver{
1677 A: map[string][]string{
1678 "mox.example.": {"127.0.0.10"}, // For mx check.
1680 PTR: map[string][]string{
1681 "127.0.0.10": {"mox.example."},
1685 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1688 ts.submission = true
1689 ts.requiretls = true
1690 ts.user = "mjl@mox.example"
1696 msg0 := strings.ReplaceAll(`From: <mjl@mox.example>
1697To: <remote@example.org>
1699Message-Id: <test@mox.example>
1705 msg1 := strings.ReplaceAll(`From: <mjl@mox.example>
1706To: <remote@example.org>
1708Message-Id: <test@mox.example>
1715 msg2 := strings.ReplaceAll(`From: <mjl@mox.example>
1716To: <remote@example.org>
1718Message-Id: <test@mox.example>
1723 testSubmit := func(msg string, requiretls bool, expRequireTLS *bool) {
1725 ts.run(func(err error, client *smtpclient.Client) {
1728 rcptTo := "remote@example.org"
1730 err = client.Deliver(ctxbg, "mjl@mox.example", rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, requiretls)
1732 tcheck(t, err, "deliver")
1734 msgs, err := queue.List(ctxbg, queue.Filter{}, queue.Sort{})
1735 tcheck(t, err, "listing queue")
1736 tcompare(t, len(msgs), 1)
1737 tcompare(t, msgs[0].RequireTLS, expRequireTLS)
1738 _, err = queue.Drop(ctxbg, pkglog, queue.Filter{IDs: []int64{msgs[0].ID}})
1739 tcheck(t, err, "deleting message from queue")
1743 testSubmit(msg0, true, &yes) // Header ignored, requiretls applied.
1744 testSubmit(msg0, false, &no) // TLS-Required header applied.
1745 testSubmit(msg1, true, &yes) // Bad headers ignored, requiretls applied.
1746 testSubmit(msg1, false, nil) // Inconsistent multiple headers ignored.
1747 testSubmit(msg2, false, nil) // Regular message, no RequireTLS setting.
1748 testSubmit(msg2, true, &yes) // Requiretls applied.
1750 // Check that we get an error if remote SMTP server does not support the requiretls
1752 ts.requiretls = false
1753 ts.run(func(err error, client *smtpclient.Client) {
1756 rcptTo := "remote@example.org"
1758 err = client.Deliver(ctxbg, "mjl@mox.example", rcptTo, int64(len(msg0)), strings.NewReader(msg0), false, false, true)
1761 t.Fatalf("delivered with requiretls to server without requiretls")
1763 if !errors.Is(err, smtpclient.ErrRequireTLSUnsupported) {
1764 t.Fatalf("got err %v, expected ErrRequireTLSUnsupported", err)
1769func TestSmuggle(t *testing.T) {
1770 resolver := dns.MockResolver{
1771 A: map[string][]string{
1772 "example.org.": {"127.0.0.10"}, // For mx check.
1774 PTR: map[string][]string{
1775 "127.0.0.10": {"example.org."}, // For iprev check.
1778 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpsmuggle/mox.conf"), resolver)
1779 ts.tlsmode = smtpclient.TLSSkip
1782 test := func(data string) {
1785 ts.runRaw(func(conn net.Conn) {
1788 ourHostname := mox.Conf.Static.HostnameDomain
1789 remoteHostname := dns.Domain{ASCII: "mox.example"}
1790 opts := smtpclient.Opts{
1791 RootCAs: mox.Conf.Static.TLS.CertPool,
1793 log := pkglog.WithCid(ts.cid - 1)
1794 _, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, ts.tlspkix, ourHostname, remoteHostname, opts)
1795 tcheck(t, err, "smtpclient")
1798 write := func(s string) {
1799 _, err := conn.Write([]byte(s))
1800 tcheck(t, err, "write")
1803 readPrefixLine := func(prefix string) string {
1805 buf := make([]byte, 512)
1806 n, err := conn.Read(buf)
1807 tcheck(t, err, "read")
1808 s := strings.TrimRight(string(buf[:n]), "\r\n")
1809 if !strings.HasPrefix(s, prefix) {
1810 t.Fatalf("got smtp response %q, expected line with prefix %q", s, prefix)
1815 write("MAIL FROM:<remote@example.org>\r\n")
1817 write("RCPT TO:<mjl@mox.example>\r\n")
1822 write("\r\n") // Empty header.
1824 write("\r\n.\r\n") // End of message.
1825 line := readPrefixLine("5")
1826 if !strings.Contains(line, "smug") {
1827 t.Errorf("got 5xx error with message %q, expected error text containing smug", line)
1838func TestFutureRelease(t *testing.T) {
1839 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1840 ts.tlsmode = smtpclient.TLSSkip
1841 ts.user = "mjl@mox.example"
1843 ts.submission = true
1846 ts.auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
1847 return sasl.NewClientPlain(ts.user, ts.pass), nil
1850 test := func(mailtoMore, expResponsePrefix string) {
1853 ts.runRaw(func(conn net.Conn) {
1856 ourHostname := mox.Conf.Static.HostnameDomain
1857 remoteHostname := dns.Domain{ASCII: "mox.example"}
1858 opts := smtpclient.Opts{Auth: ts.auth}
1859 log := pkglog.WithCid(ts.cid - 1)
1860 _, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, false, ourHostname, remoteHostname, opts)
1861 tcheck(t, err, "smtpclient")
1864 write := func(s string) {
1865 _, err := conn.Write([]byte(s))
1866 tcheck(t, err, "write")
1869 readPrefixLine := func(prefix string) string {
1871 buf := make([]byte, 512)
1872 n, err := conn.Read(buf)
1873 tcheck(t, err, "read")
1874 s := strings.TrimRight(string(buf[:n]), "\r\n")
1875 if !strings.HasPrefix(s, prefix) {
1876 t.Fatalf("got smtp response %q, expected line with prefix %q", s, prefix)
1881 write(fmt.Sprintf("MAIL FROM:<mjl@mox.example>%s\r\n", mailtoMore))
1882 readPrefixLine(expResponsePrefix)
1883 if expResponsePrefix != "2" {
1886 write("RCPT TO:<mjl@mox.example>\r\n")
1891 write("From: <mjl@mox.example>\r\n\r\nbody\r\n\r\n.\r\n")
1896 test(" HOLDFOR=1", "2")
1897 test(" HOLDUNTIL="+time.Now().Add(time.Minute).UTC().Format(time.RFC3339), "2")
1898 test(" HOLDUNTIL="+time.Now().Add(time.Minute).UTC().Format(time.RFC3339Nano), "2")
1900 test(" HOLDFOR=0", "501") // 0 is invalid syntax.
1901 test(fmt.Sprintf(" HOLDFOR=%d", int64((queue.FutureReleaseIntervalMax+time.Minute)/time.Second)), "554") // Too far in the future.
1902 test(" HOLDUNTIL="+time.Now().Add(-time.Minute).UTC().Format(time.RFC3339), "554") // In the past.
1903 test(" HOLDUNTIL="+time.Now().Add(queue.FutureReleaseIntervalMax+time.Minute).UTC().Format(time.RFC3339), "554") // Too far in the future.
1904 test(" HOLDUNTIL=2024-02-10T17:28:00+00:00", "501") // "Z" required.
1905 test(" HOLDUNTIL=24-02-10T17:28:00Z", "501") // Invalid.
1906 test(" HOLDFOR=1 HOLDFOR=1", "501") // Duplicate.
1907 test(" HOLDFOR=1 HOLDUNTIL="+time.Now().Add(time.Hour).UTC().Format(time.RFC3339), "501") // Duplicate.
1911func TestSMTPUTF8(t *testing.T) {
1912 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1915 ts.user = "mjl@mox.example"
1917 ts.submission = true
1919 test := func(mailFrom string, rcptTo string, headerValue string, filename string, clientSmtputf8 bool, expectedSmtputf8 bool, expErr *smtpclient.Error) {
1922 ts.run(func(_ error, client *smtpclient.Client) {
1924 msg := strings.ReplaceAll(fmt.Sprintf(`From: <%s>
1927X-Custom-Test-Header: %s
1929Content-type: multipart/mixed; boundary="simple boundary"
1932Content-Type: text/plain; charset=UTF-8;
1933Content-Disposition: attachment; filename="%s"
1934Content-Transfer-Encoding: base64
1936QW4gYXR0YWNoZWQgdGV4dCBmaWxlLg==
1939`, mailFrom, rcptTo, headerValue, filename), "\n", "\r\n")
1941 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), true, clientSmtputf8, false)
1942 ts.smtpErr(err, expErr)
1947 msgs, _ := queue.List(ctxbg, queue.Filter{}, queue.Sort{Field: "Queued", Asc: false})
1948 queuedMsg := msgs[0]
1949 if queuedMsg.SMTPUTF8 != expectedSmtputf8 {
1950 t.Fatalf("[%s / %s / %s / %s] got SMTPUTF8 %t, expected %t", mailFrom, rcptTo, headerValue, filename, queuedMsg.SMTPUTF8, expectedSmtputf8)
1955 test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", false, false, nil)
1956 test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", true, false, nil)
1957 test(`mjl@mox.example`, `🙂@example.org`, "header-ascii", "ascii.txt", true, true, nil)
1958 test(`mjl@mox.example`, `🙂@example.org`, "header-ascii", "ascii.txt", false, true, &smtpclient.Error{Permanent: true, Code: smtp.C553BadMailbox, Secode: smtp.SeMsg6NonASCIIAddrNotPermitted7})
1959 test(`Ω@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", true, true, nil)
1960 test(`Ω@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", false, true, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeMsg6NonASCIIAddrNotPermitted7})
1961 test(`mjl@mox.example`, `remote@example.org`, "header-utf8-😍", "ascii.txt", true, true, nil)
1962 test(`mjl@mox.example`, `remote@example.org`, "header-utf8-😍", "ascii.txt", false, true, nil)
1963 test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "utf8-🫠️.txt", true, true, nil)
1964 test(`Ω@mox.example`, `🙂@example.org`, "header-utf8-😍", "utf8-🫠️.txt", true, true, nil)
1965 test(`mjl@mox.example`, `remote@xn--vg8h.example.org`, "header-ascii", "ascii.txt", true, false, nil)
1968// TestExtra checks whether submission of messages with "X-Mox-Extra-<key>: value"
1969// headers cause those those key/value pairs to be added to the Extra field in the
1971func TestExtra(t *testing.T) {
1972 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1975 ts.user = "mjl@mox.example"
1977 ts.submission = true
1979 extraMsg := strings.ReplaceAll(`From: <mjl@mox.example>
1980To: <remote@example.org>
1982X-Mox-Extra-Test: testvalue
1985X-Mox-Extra-x-cANONICAL-z: ok
1986Message-Id: <test@mox.example>
1991 ts.run(func(err error, client *smtpclient.Client) {
1993 tcheck(t, err, "init client")
1994 mailFrom := "mjl@mox.example"
1995 rcptTo := "mjl@mox.example"
1996 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
1997 tcheck(t, err, "deliver")
1999 msgs, err := queue.List(ctxbg, queue.Filter{}, queue.Sort{})
2000 tcheck(t, err, "queue list")
2001 tcompare(t, len(msgs), 1)
2002 tcompare(t, msgs[0].Extra, map[string]string{
2003 "Test": "testvalue",
2006 "X-Canonical-Z": "ok",
2008 // note: these headers currently stay in the message.
2011// TestExtraDup checks for an error for duplicate x-mox-extra-* keys.
2012func TestExtraDup(t *testing.T) {
2013 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
2016 ts.user = "mjl@mox.example"
2018 ts.submission = true
2020 extraMsg := strings.ReplaceAll(`From: <mjl@mox.example>
2021To: <remote@example.org>
2023X-Mox-Extra-Test: testvalue
2024X-Mox-Extra-Test: testvalue
2025Message-Id: <test@mox.example>
2030 ts.run(func(err error, client *smtpclient.Client) {
2031 tcheck(t, err, "init client")
2032 mailFrom := "mjl@mox.example"
2033 rcptTo := "mjl@mox.example"
2034 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2035 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C554TransactionFailed, Secode: smtp.SeMsg6Other0})
2039// FromID can be specified during submission, but must be unique, with single recipient.
2040func TestUniqueFromID(t *testing.T) {
2041 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpfromid/mox.conf"), dns.MockResolver{})
2044 ts.user = "mjl+fromid@mox.example"
2046 ts.submission = true
2048 extraMsg := strings.ReplaceAll(`From: <mjl@mox.example>
2049To: <remote@example.org>
2055 // Specify our own unique id when queueing.
2056 ts.run(func(err error, client *smtpclient.Client) {
2057 tcheck(t, err, "init client")
2058 mailFrom := "mjl+unique@mox.example"
2059 rcptTo := "mjl@mox.example"
2060 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2061 ts.smtpErr(err, nil)
2064 // But we can only use it once.
2065 ts.run(func(err error, client *smtpclient.Client) {
2066 tcheck(t, err, "init client")
2067 mailFrom := "mjl+unique@mox.example"
2068 rcptTo := "mjl@mox.example"
2069 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2070 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C554TransactionFailed, Secode: smtp.SeAddr1SenderSyntax7})
2073 // We cannot use our own fromid with multiple recipients.
2074 ts.run(func(err error, client *smtpclient.Client) {
2075 tcheck(t, err, "init client")
2076 mailFrom := "mjl+unique2@mox.example"
2077 rcptTo := []string{"mjl@mox.example", "mjl@mox.example"}
2078 _, err = client.DeliverMultiple(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2079 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C554TransactionFailed, Secode: smtp.SeProto5TooManyRcpts3})