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)
96 tlsmode smtpclient.TLSMode
100const password0 = "te\u0301st \u00a0\u2002\u200a" // NFD and various unicode spaces.
101const password1 = "tést " // PRECIS normalized, with NFC.
103func newTestServer(t *testing.T, configPath string, resolver dns.Resolver) *testserver {
104 limitersInit() // Reset rate limiters.
106 ts := testserver{t: t, cid: 1, resolver: resolver, tlsmode: smtpclient.TLSOpportunistic}
108 log := mlog.New("smtpserver", nil)
110 mox.ConfigStaticPath = configPath
111 mox.MustLoadConfig(true, false)
112 dataDir := mox.ConfigDirPath(mox.Conf.Static.DataDir)
113 os.RemoveAll(dataDir)
115 err := dmarcdb.Init()
116 tcheck(t, err, "dmarcdb init")
117 err = tlsrptdb.Init()
118 tcheck(t, err, "tlsrptdb init")
120 ts.acc, err = store.OpenAccount(log, "mjl")
121 tcheck(t, err, "open account")
122 err = ts.acc.SetPassword(log, password0)
123 tcheck(t, err, "set password")
125 ts.switchStop = store.Switchboard()
127 tcheck(t, err, "queue init")
129 ts.comm = store.RegisterComm(ts.acc)
134func (ts *testserver) close() {
138 err := dmarcdb.Close()
139 tcheck(ts.t, err, "dmarcdb close")
140 err = tlsrptdb.Close()
141 tcheck(ts.t, err, "tlsrptdb close")
146 tcheck(ts.t, err, "closing account")
151func (ts *testserver) checkCount(mailboxName string, expect int) {
154 q := bstore.QueryDB[store.Mailbox](ctxbg, ts.acc.DB)
155 q.FilterNonzero(store.Mailbox{Name: mailboxName})
157 tcheck(t, err, "get mailbox")
158 qm := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB)
159 qm.FilterNonzero(store.Message{MailboxID: mb.ID})
160 qm.FilterEqual("Expunged", false)
162 tcheck(t, err, "count messages in mailbox")
164 t.Fatalf("messages in mailbox, found %d, expected %d", n, expect)
168func (ts *testserver) run(fn func(helloErr error, client *smtpclient.Client)) {
170 ts.runRaw(func(conn net.Conn) {
174 if auth == nil && ts.user != "" {
175 auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
176 return sasl.NewClientPlain(ts.user, ts.pass), nil
180 ourHostname := mox.Conf.Static.HostnameDomain
181 remoteHostname := dns.Domain{ASCII: "mox.example"}
182 opts := smtpclient.Opts{
184 RootCAs: mox.Conf.Static.TLS.CertPool,
186 log := pkglog.WithCid(ts.cid - 1)
187 client, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, ts.tlspkix, ourHostname, remoteHostname, opts)
197func (ts *testserver) runRaw(fn func(clientConn net.Conn)) {
202 serverConn, clientConn := net.Pipe()
203 defer serverConn.Close()
204 // clientConn is closed as part of closing client.
205 serverdone := make(chan struct{})
206 defer func() { <-serverdone }()
209 tlsConfig := &tls.Config{
210 Certificates: []tls.Certificate{fakeCert(ts.t)},
212 serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, tlsConfig, serverConn, ts.resolver, ts.submission, false, 100<<20, false, false, ts.requiretls, ts.dnsbls, 0)
219func (ts *testserver) smtpErr(err error, expErr *smtpclient.Error) {
222 var cerr smtpclient.Error
223 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) {
224 t.Fatalf("got err:\n%#v (%q)\nexpected:\n%#v", err, err, expErr)
228// Just a cert that appears valid. SMTP client will not verify anything about it
229// (that is opportunistic TLS for you, "better some than none"). Let's enjoy this
230// one moment where it makes life easier.
231func fakeCert(t *testing.T) tls.Certificate {
232 privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
233 template := &x509.Certificate{
234 SerialNumber: big.NewInt(1), // Required field...
236 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
238 t.Fatalf("making certificate: %s", err)
240 cert, err := x509.ParseCertificate(localCertBuf)
242 t.Fatalf("parsing generated certificate: %s", err)
244 c := tls.Certificate{
245 Certificate: [][]byte{localCertBuf},
252// check expected dmarc evaluations for outgoing aggregate reports.
253func checkEvaluationCount(t *testing.T, n int) []dmarcdb.Evaluation {
255 l, err := dmarcdb.Evaluations(ctxbg)
256 tcheck(t, err, "get dmarc evaluations")
257 tcompare(t, len(l), n)
261// Test submission from authenticated user.
262func TestSubmission(t *testing.T) {
263 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
266 // Set DKIM signing config.
267 dom, _ := mox.Conf.Domain(dns.Domain{ASCII: "mox.example"})
268 sel := config.Selector{
269 HashEffective: "sha256",
270 HeadersEffective: []string{"From", "To", "Subject"},
271 Key: ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)), // Fake key, don't use for real.
272 Domain: dns.Domain{ASCII: "mox.example"},
274 dom.DKIM = config.DKIM{
275 Selectors: map[string]config.Selector{"testsel": sel},
276 Sign: []string{"testsel"},
278 mox.Conf.Dynamic.Domains["mox.example"] = dom
280 testAuth := func(authfn func(user, pass string, cs *tls.ConnectionState) sasl.Client, user, pass string, expErr *smtpclient.Error) {
283 ts.auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
284 return authfn(user, pass, cs), nil
289 ts.run(func(err error, client *smtpclient.Client) {
291 mailFrom := "mjl@mox.example"
292 rcptTo := "remote@example.org"
294 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
296 var cerr smtpclient.Error
297 if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Code != expErr.Code || cerr.Secode != expErr.Secode) {
298 t.Fatalf("got err:\n%#v (%q)\nexpected:\n%#v", err, err, expErr)
300 checkEvaluationCount(t, 0)
305 testAuth(nil, "", "", &smtpclient.Error{Permanent: true, Code: smtp.C530SecurityRequired, Secode: smtp.SePol7Other0})
306 authfns := []func(user, pass string, cs *tls.ConnectionState) sasl.Client{
307 func(user, pass string, cs *tls.ConnectionState) sasl.Client { return sasl.NewClientPlain(user, pass) },
308 func(user, pass string, cs *tls.ConnectionState) sasl.Client { return sasl.NewClientLogin(user, pass) },
309 func(user, pass string, cs *tls.ConnectionState) sasl.Client { return sasl.NewClientCRAMMD5(user, pass) },
310 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
311 return sasl.NewClientSCRAMSHA1(user, pass, false)
313 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
314 return sasl.NewClientSCRAMSHA256(user, pass, false)
316 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
317 return sasl.NewClientSCRAMSHA1PLUS(user, pass, *cs)
319 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
320 return sasl.NewClientSCRAMSHA256PLUS(user, pass, *cs)
323 for _, fn := range authfns {
324 testAuth(fn, "mjl@mox.example", "test", &smtpclient.Error{Code: smtp.C535AuthBadCreds, Secode: smtp.SePol7AuthBadCreds8}) // Bad (short) password.
325 testAuth(fn, "mjl@mox.example", password0+"test", &smtpclient.Error{Code: smtp.C535AuthBadCreds, Secode: smtp.SePol7AuthBadCreds8}) // Bad password.
326 testAuth(fn, "mjl@mox.example", password0, nil)
327 testAuth(fn, "mjl@mox.example", password1, nil)
328 testAuth(fn, "móx@mox.example", password0, nil)
329 testAuth(fn, "móx@mox.example", password1, nil)
330 testAuth(fn, "mo\u0301x@mox.example", password0, nil)
331 testAuth(fn, "mo\u0301x@mox.example", password1, nil)
335// Test delivery from external MTA.
336func TestDelivery(t *testing.T) {
337 resolver := dns.MockResolver{
338 A: map[string][]string{
339 "example.org.": {"127.0.0.10"}, // For mx check.
341 PTR: map[string][]string{},
343 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
346 ts.run(func(err error, client *smtpclient.Client) {
347 mailFrom := "remote@example.org"
348 rcptTo := "mjl@127.0.0.10"
350 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
352 var cerr smtpclient.Error
353 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
354 t.Fatalf("deliver to ip address, got err %v, expected smtpclient.Error with code %d", err, smtp.C550MailboxUnavail)
358 ts.run(func(err error, client *smtpclient.Client) {
359 mailFrom := "remote@example.org"
360 rcptTo := "mjl@test.example" // Not configured as destination.
362 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
364 var cerr smtpclient.Error
365 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
366 t.Fatalf("deliver to unknown domain, got err %v, expected smtpclient.Error with code %d", err, smtp.C550MailboxUnavail)
370 ts.run(func(err error, client *smtpclient.Client) {
371 mailFrom := "remote@example.org"
372 rcptTo := "unknown@mox.example" // User unknown.
374 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
376 var cerr smtpclient.Error
377 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
378 t.Fatalf("deliver to unknown user for known domain, got err %v, expected smtpclient.Error with code %d", err, smtp.C550MailboxUnavail)
382 ts.run(func(err error, client *smtpclient.Client) {
383 mailFrom := "remote@example.org"
384 rcptTo := "mjl@mox.example"
386 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
388 var cerr smtpclient.Error
389 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
390 t.Fatalf("deliver from user without reputation, valid iprev required, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
394 // Set up iprev to get delivery from unknown user to be accepted.
395 resolver.PTR["127.0.0.10"] = []string{"example.org."}
397 // Only ascii o@ is configured, not the greek and cyrillic lookalikes.
398 ts.run(func(err error, client *smtpclient.Client) {
399 mailFrom := "remote@example.org"
400 rcptTo := "ο@mox.example" // omicron \u03bf, looks like the configured o@
401 msg := strings.ReplaceAll(deliverMessage, "mjl@mox.example", rcptTo)
403 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, true, false)
405 var cerr smtpclient.Error
406 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
407 t.Fatalf("deliver to omicron @ instead of ascii o @, got err %v, expected smtpclient.Error with code %d", err, smtp.C550MailboxUnavail)
411 ts.run(func(err error, client *smtpclient.Client) {
412 recipients := []string{
414 "o@mox.example", // ascii o, as configured
415 "\u2126@mox.example", // ohm sign, as configured
416 "ω@mox.example", // lower-case omega, we match case-insensitively and this is the lowercase of ohm (!)
417 "\u03a9@mox.example", // capital omega, also lowercased to omega.
418 "móx@mox.example", // NFC
419 "mo\u0301x@mox.example", // not NFC, but normalized as móx@, see https://go.dev/blog/normalization
422 for _, rcptTo := range recipients {
423 // Ensure SMTP RCPT TO and message address headers are the same, otherwise the junk
424 // filter treats us more strictly.
425 msg := strings.ReplaceAll(deliverMessage, "mjl@mox.example", rcptTo)
427 mailFrom := "remote@example.org"
429 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, true, false)
431 tcheck(t, err, "deliver to remote")
433 changes := make(chan []store.Change)
435 changes <- ts.comm.Get()
438 timer := time.NewTimer(time.Second)
443 t.Fatalf("no delivery in 1s")
448 checkEvaluationCount(t, 0)
451func tinsertmsg(t *testing.T, acc *store.Account, mailbox string, m *store.Message, msg string) {
452 mf, err := store.CreateMessageTemp(pkglog, "queue-dsn")
453 tcheck(t, err, "temp message")
454 defer os.Remove(mf.Name())
456 _, err = mf.Write([]byte(msg))
457 tcheck(t, err, "write message")
458 err = acc.DeliverMailbox(pkglog, mailbox, m, mf)
459 tcheck(t, err, "deliver message")
461 tcheck(t, err, "close message")
464func tretrain(t *testing.T, acc *store.Account) {
467 // Fresh empty junkfilter.
468 basePath := mox.DataDirPath("accounts")
469 dbPath := filepath.Join(basePath, acc.Name, "junkfilter.db")
470 bloomPath := filepath.Join(basePath, acc.Name, "junkfilter.bloom")
473 jf, _, err := acc.OpenJunkFilter(ctxbg, pkglog)
474 tcheck(t, err, "open junk filter")
477 // Fetch messags to retrain on.
478 q := bstore.QueryDB[store.Message](ctxbg, acc.DB)
479 q.FilterEqual("Expunged", false)
480 q.FilterFn(func(m store.Message) bool {
481 return m.Flags.Junk || m.Flags.Notjunk
483 msgs, err := q.List()
484 tcheck(t, err, "fetch messages")
486 // Retrain the messages.
487 for _, m := range msgs {
488 ham := m.Flags.Notjunk
490 f, err := os.Open(acc.MessagePath(m.ID))
491 tcheck(t, err, "open message")
492 r := store.FileMsgReader(m.MsgPrefix, f)
494 jf.TrainMessage(ctxbg, r, m.Size, ham)
497 tcheck(t, err, "close message")
501 tcheck(t, err, "save junkfilter")
504// Test accept/reject with DMARC reputation and with spammy content.
505func TestSpam(t *testing.T) {
506 resolver := &dns.MockResolver{
507 A: map[string][]string{
508 "example.org.": {"127.0.0.1"}, // For mx check.
510 TXT: map[string][]string{
511 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
512 "_dmarc.example.org.": {"v=DMARC1;p=reject; rua=mailto:dmarcrpt@example.org"},
515 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
518 // Insert spammy messages. No junkfilter training yet.
520 RemoteIP: "127.0.0.10",
521 RemoteIPMasked1: "127.0.0.10",
522 RemoteIPMasked2: "127.0.0.0",
523 RemoteIPMasked3: "127.0.0.0",
524 MailFrom: "remote@example.org",
525 MailFromLocalpart: smtp.Localpart("remote"),
526 MailFromDomain: "example.org",
527 RcptToLocalpart: smtp.Localpart("mjl"),
528 RcptToDomain: "mox.example",
529 MsgFromLocalpart: smtp.Localpart("remote"),
530 MsgFromDomain: "example.org",
531 MsgFromOrgDomain: "example.org",
532 MsgFromValidated: true,
533 MsgFromValidation: store.ValidationStrict,
534 Flags: store.Flags{Seen: true, Junk: true},
535 Size: int64(len(deliverMessage)),
537 for i := 0; i < 3; i++ {
539 tinsertmsg(t, ts.acc, "Inbox", &nm, deliverMessage)
542 // Delivery from sender with bad reputation should fail.
543 ts.run(func(err error, client *smtpclient.Client) {
544 mailFrom := "remote@example.org"
545 rcptTo := "mjl@mox.example"
547 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
549 var cerr smtpclient.Error
550 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
551 t.Fatalf("delivery by bad sender, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
554 ts.checkCount("Rejects", 1)
555 checkEvaluationCount(t, 0) // No positive interactions yet.
558 // Delivery from sender with bad reputation matching AcceptRejectsToMailbox should
559 // result in accepted delivery to the mailbox.
560 ts.run(func(err error, client *smtpclient.Client) {
561 mailFrom := "remote@example.org"
562 rcptTo := "mjl2@mox.example"
564 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage2)), strings.NewReader(deliverMessage2), false, false, false)
566 tcheck(t, err, "deliver")
568 ts.checkCount("mjl2junk", 1) // In ruleset rejects mailbox.
569 ts.checkCount("Rejects", 1) // Same as before.
570 checkEvaluationCount(t, 0) // This is not an actual accept.
573 // Mark the messages as having good reputation.
574 q := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB)
575 q.FilterEqual("Expunged", false)
576 _, err := q.UpdateFields(map[string]any{"Junk": false, "Notjunk": true})
577 tcheck(t, err, "update junkiness")
579 // Message should now be accepted.
580 ts.run(func(err error, client *smtpclient.Client) {
581 mailFrom := "remote@example.org"
582 rcptTo := "mjl@mox.example"
584 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
586 tcheck(t, err, "deliver")
588 // Message should now be removed from Rejects mailboxes.
589 ts.checkCount("Rejects", 0)
590 ts.checkCount("mjl2junk", 1)
591 checkEvaluationCount(t, 1)
594 // Undo dmarc pass, mark messages as junk, and train the filter.
596 q = bstore.QueryDB[store.Message](ctxbg, ts.acc.DB)
597 q.FilterEqual("Expunged", false)
598 _, err = q.UpdateFields(map[string]any{"Junk": true, "Notjunk": false})
599 tcheck(t, err, "update junkiness")
602 // Message should be refused for spammy content.
603 ts.run(func(err error, client *smtpclient.Client) {
604 mailFrom := "remote@example.org"
605 rcptTo := "mjl@mox.example"
607 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
609 var cerr smtpclient.Error
610 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
611 t.Fatalf("attempt to deliver spamy message, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
613 checkEvaluationCount(t, 1) // No new evaluation, this isn't a DMARC reject.
617// Test accept/reject with forwarded messages, DMARC ignored, no IP/EHLO/MAIL
618// FROM-based reputation.
619func TestForward(t *testing.T) {
620 // Do a run without forwarding, and with.
621 check := func(forward bool) {
623 resolver := &dns.MockResolver{
624 A: map[string][]string{
625 "bad.example.": {"127.0.0.1"}, // For mx check.
626 "good.example.": {"127.0.0.1"}, // For mx check.
627 "forward.example.": {"127.0.0.10"}, // For mx check.
629 TXT: map[string][]string{
630 "bad.example.": {"v=spf1 ip4:127.0.0.1 -all"},
631 "good.example.": {"v=spf1 ip4:127.0.0.1 -all"},
632 "forward.example.": {"v=spf1 ip4:127.0.0.10 -all"},
633 "_dmarc.bad.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@bad.example"},
634 "_dmarc.good.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@good.example"},
635 "_dmarc.forward.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@forward.example"},
637 PTR: map[string][]string{
638 "127.0.0.10": {"forward.example."}, // For iprev check.
641 rcptTo := "mjl3@mox.example"
643 // For SPF and DMARC pass, otherwise the test ends quickly.
644 resolver.TXT["bad.example."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
645 resolver.TXT["good.example."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
646 rcptTo = "mjl@mox.example" // Without IsForward rule.
649 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
652 totalEvaluations := 0
654 var msgBad = strings.ReplaceAll(`From: <remote@bad.example>
657Message-Id: <bad@example.org>
661 var msgOK = strings.ReplaceAll(`From: <remote@good.example>
664Message-Id: <good@example.org>
668 var msgOK2 = strings.ReplaceAll(`From: <other@forward.example>
671Message-Id: <regular@example.org>
673happens to come from forwarding mail server.
676 // Deliver forwarded messages, then classify as junk. Normally enough to treat
677 // other unrelated messages from IP as junk, but not for forwarded messages.
678 ts.run(func(err error, client *smtpclient.Client) {
679 tcheck(t, err, "connect")
681 mailFrom := "remote@forward.example"
683 mailFrom = "remote@bad.example"
686 for i := 0; i < 10; i++ {
687 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgBad)), strings.NewReader(msgBad), false, false, false)
688 tcheck(t, err, "deliver message")
690 totalEvaluations += 10
692 n, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).UpdateFields(map[string]any{"Junk": true, "MsgFromValidated": true})
693 tcheck(t, err, "marking messages as junk")
696 // Next delivery will fail, with negative "message From" signal.
697 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgBad)), strings.NewReader(msgBad), false, false, false)
698 var cerr smtpclient.Error
699 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
700 t.Fatalf("delivery by bad sender, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
703 checkEvaluationCount(t, totalEvaluations)
706 // Delivery from different "message From" without reputation, but from same
707 // forwarding email server, should succeed under forwarding, not as regular sending
709 ts.run(func(err error, client *smtpclient.Client) {
710 tcheck(t, err, "connect")
712 mailFrom := "remote@forward.example"
714 mailFrom = "remote@good.example"
717 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgOK)), strings.NewReader(msgOK), false, false, false)
719 tcheck(t, err, "deliver")
720 totalEvaluations += 1
722 var cerr smtpclient.Error
723 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
724 t.Fatalf("delivery by bad ip, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
727 checkEvaluationCount(t, totalEvaluations)
730 // Delivery from forwarding server that isn't a forward should get same treatment.
731 ts.run(func(err error, client *smtpclient.Client) {
732 tcheck(t, err, "connect")
734 mailFrom := "other@forward.example"
736 // Ensure To header matches.
739 msg = strings.ReplaceAll(msg, "<mjl@mox.example>", "<mjl3@mox.example>")
742 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
744 tcheck(t, err, "deliver")
745 totalEvaluations += 1
747 var cerr smtpclient.Error
748 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
749 t.Fatalf("delivery by bad ip, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
752 checkEvaluationCount(t, totalEvaluations)
760// Messages that we sent to, that have passing DMARC, but that are otherwise spammy, should be accepted.
761func TestDMARCSent(t *testing.T) {
762 resolver := &dns.MockResolver{
763 A: map[string][]string{
764 "example.org.": {"127.0.0.1"}, // For mx check.
766 TXT: map[string][]string{
767 "example.org.": {"v=spf1 ip4:127.0.0.1 -all"},
768 "_dmarc.example.org.": {"v=DMARC1;p=reject;rua=mailto:dmarcrpt@example.org"},
771 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
774 // First check that DMARC policy rejects message and results in optional evaluation.
775 ts.run(func(err error, client *smtpclient.Client) {
776 mailFrom := "remote@example.org"
777 rcptTo := "mjl@mox.example"
779 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
781 var cerr smtpclient.Error
782 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
783 t.Fatalf("attempt to deliver spamy message, got err %v, expected smtpclient.Error with code %d", err, smtp.C550MailboxUnavail)
785 l := checkEvaluationCount(t, 1)
786 tcompare(t, l[0].Optional, true)
789 // Update DNS for an SPF pass, and DMARC pass.
790 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
792 // Insert spammy messages not related to the test message.
794 MailFrom: "remote@test.example",
795 RcptToLocalpart: smtp.Localpart("mjl"),
796 RcptToDomain: "mox.example",
797 Flags: store.Flags{Seen: true, Junk: true},
798 Size: int64(len(deliverMessage)),
800 for i := 0; i < 3; i++ {
802 tinsertmsg(t, ts.acc, "Archive", &nm, deliverMessage)
806 // Baseline, message should be refused for spammy content.
807 ts.run(func(err error, client *smtpclient.Client) {
808 mailFrom := "remote@example.org"
809 rcptTo := "mjl@mox.example"
811 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
813 var cerr smtpclient.Error
814 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
815 t.Fatalf("attempt to deliver spamy message, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
817 checkEvaluationCount(t, 1) // No new evaluation.
820 // Insert a message that we sent to the address that is about to send to us.
821 sentMsg := store.Message{Size: int64(len(deliverMessage))}
822 tinsertmsg(t, ts.acc, "Sent", &sentMsg, deliverMessage)
823 err := ts.acc.DB.Insert(ctxbg, &store.Recipient{MessageID: sentMsg.ID, Localpart: "remote", Domain: "example.org", OrgDomain: "example.org", Sent: time.Now()})
824 tcheck(t, err, "inserting message recipient")
826 // Reject a message due to DMARC again. Since we sent a message to the domain, it
827 // is no longer unknown and we should see a non-optional evaluation that will
828 // result in a DMARC report.
829 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.1 -all"}
830 ts.run(func(err error, client *smtpclient.Client) {
831 mailFrom := "remote@example.org"
832 rcptTo := "mjl@mox.example"
834 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
836 var cerr smtpclient.Error
837 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
838 t.Fatalf("attempt to deliver spamy message, got err %v, expected smtpclient.Error with code %d", err, smtp.C550MailboxUnavail)
840 l := checkEvaluationCount(t, 2) // New evaluation.
841 tcompare(t, l[1].Optional, false)
844 // We should now be accepting the message because we recently sent a message.
845 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
846 ts.run(func(err error, client *smtpclient.Client) {
847 mailFrom := "remote@example.org"
848 rcptTo := "mjl@mox.example"
850 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
852 tcheck(t, err, "deliver")
853 l := checkEvaluationCount(t, 3) // New evaluation.
854 tcompare(t, l[2].Optional, false)
858// Test DNSBL, then getting through with subjectpass.
859func TestBlocklistedSubjectpass(t *testing.T) {
860 // Set up a DNSBL on dnsbl.example, and get DMARC pass.
861 resolver := &dns.MockResolver{
862 A: map[string][]string{
863 "example.org.": {"127.0.0.10"}, // For mx check.
864 "2.0.0.127.dnsbl.example.": {"127.0.0.2"}, // For healthcheck.
865 "10.0.0.127.dnsbl.example.": {"127.0.0.10"}, // Where our connection pretends to come from.
867 TXT: map[string][]string{
868 "10.0.0.127.dnsbl.example.": {"blocklisted"},
869 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
870 "_dmarc.example.org.": {"v=DMARC1;p=reject"},
872 PTR: map[string][]string{
873 "127.0.0.10": {"example.org."}, // For iprev check.
876 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
877 ts.dnsbls = []dns.Domain{{ASCII: "dnsbl.example"}}
880 // Message should be refused softly (temporary error) due to DNSBL.
881 ts.run(func(err error, client *smtpclient.Client) {
882 mailFrom := "remote@example.org"
883 rcptTo := "mjl@mox.example"
885 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
887 var cerr smtpclient.Error
888 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
889 t.Fatalf("attempted deliver from dnsblocklisted ip, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
893 // Set up subjectpass on account.
894 acc := mox.Conf.Dynamic.Accounts[ts.acc.Name]
895 acc.SubjectPass.Period = time.Hour
896 mox.Conf.Dynamic.Accounts[ts.acc.Name] = acc
898 // Message should be refused quickly (permanent error) due to DNSBL and Subjectkey.
900 ts.run(func(err error, client *smtpclient.Client) {
901 mailFrom := "remote@example.org"
902 rcptTo := "mjl@mox.example"
904 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
906 var cerr smtpclient.Error
907 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
908 t.Fatalf("attempted deliver from dnsblocklisted ip, got err %v, expected smtpclient.Error with code %d", err, smtp.C550MailboxUnavail)
910 i := strings.Index(cerr.Line, subjectpass.Explanation)
912 t.Fatalf("got error line %q, expected error line with subjectpass", cerr.Line)
914 pass = cerr.Line[i+len(subjectpass.Explanation):]
917 ts.run(func(err error, client *smtpclient.Client) {
918 mailFrom := "remote@example.org"
919 rcptTo := "mjl@mox.example"
920 passMessage := strings.Replace(deliverMessage, "Subject: test", "Subject: test "+pass, 1)
922 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(passMessage)), strings.NewReader(passMessage), false, false, false)
924 tcheck(t, err, "deliver with subjectpass")
928// Test accepting a DMARC report.
929func TestDMARCReport(t *testing.T) {
930 resolver := &dns.MockResolver{
931 A: map[string][]string{
932 "example.org.": {"127.0.0.10"}, // For mx check.
934 TXT: map[string][]string{
935 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
936 "_dmarc.example.org.": {"v=DMARC1;p=reject; rua=mailto:dmarcrpt@example.org"},
938 PTR: map[string][]string{
939 "127.0.0.10": {"example.org."}, // For iprev check.
942 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/dmarcreport/mox.conf"), resolver)
945 run := func(report string, n int) {
947 ts.run(func(err error, client *smtpclient.Client) {
950 tcheck(t, err, "run")
952 mailFrom := "remote@example.org"
953 rcptTo := "mjl@mox.example"
955 msgb := &bytes.Buffer{}
956 _, 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)
957 tcheck(t, xerr, "write msg headers")
958 w := quotedprintable.NewWriter(msgb)
959 _, xerr = w.Write([]byte(strings.ReplaceAll(report, "\n", "\r\n")))
960 tcheck(t, xerr, "write message")
964 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
966 tcheck(t, err, "deliver")
968 records, err := dmarcdb.Records(ctxbg)
969 tcheck(t, err, "dmarcdb records")
970 if len(records) != n {
971 t.Fatalf("got %d dmarcdb records, expected %d or more", len(records), n)
977 run(strings.ReplaceAll(dmarcReport, "xmox.nl", "mox.example"), 1)
979 // We always store as an evaluation, but as optional for reports.
980 evals := checkEvaluationCount(t, 2)
981 tcompare(t, evals[0].Optional, true)
982 tcompare(t, evals[1].Optional, true)
985const dmarcReport = `<?xml version="1.0" encoding="UTF-8" ?>
988 <org_name>example.org</org_name>
989 <email>postmaster@example.org</email>
990 <report_id>1</report_id>
992 <begin>1596412800</begin>
993 <end>1596499199</end>
997 <domain>xmox.nl</domain>
1006 <source_ip>127.0.0.10</source_ip>
1009 <disposition>none</disposition>
1015 <header_from>xmox.nl</header_from>
1019 <domain>xmox.nl</domain>
1020 <result>pass</result>
1021 <selector>testsel</selector>
1024 <domain>xmox.nl</domain>
1025 <result>pass</result>
1032// Test accepting a TLS report.
1033func TestTLSReport(t *testing.T) {
1034 // Requires setting up DKIM.
1035 privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
1036 dkimRecord := dkim.Record{
1038 Hashes: []string{"sha256"},
1039 Flags: []string{"s"},
1040 PublicKey: privKey.Public(),
1043 dkimTxt, err := dkimRecord.Record()
1044 tcheck(t, err, "dkim record")
1046 sel := config.Selector{
1047 HashEffective: "sha256",
1048 HeadersEffective: []string{"From", "To", "Subject", "Date"},
1050 Domain: dns.Domain{ASCII: "testsel"},
1052 dkimConf := config.DKIM{
1053 Selectors: map[string]config.Selector{"testsel": sel},
1054 Sign: []string{"testsel"},
1057 resolver := &dns.MockResolver{
1058 A: map[string][]string{
1059 "example.org.": {"127.0.0.10"}, // For mx check.
1061 TXT: map[string][]string{
1062 "testsel._domainkey.example.org.": {dkimTxt},
1063 "_dmarc.example.org.": {"v=DMARC1;p=reject;rua=mailto:dmarcrpt@example.org"},
1065 PTR: map[string][]string{
1066 "127.0.0.10": {"example.org."}, // For iprev check.
1069 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/tlsrpt/mox.conf"), resolver)
1072 run := func(rcptTo, tlsrpt string, n int) {
1074 ts.run(func(err error, client *smtpclient.Client) {
1077 mailFrom := "remote@example.org"
1079 msgb := &bytes.Buffer{}
1080 _, 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)
1081 tcheck(t, xerr, "write msg")
1082 msg := msgb.String()
1084 selectors := mox.DKIMSelectors(dkimConf)
1085 headers, xerr := dkim.Sign(ctxbg, pkglog.Logger, "remote", dns.Domain{ASCII: "example.org"}, selectors, false, strings.NewReader(msg))
1086 tcheck(t, xerr, "dkim sign")
1090 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1092 tcheck(t, err, "deliver")
1094 records, err := tlsrptdb.Records(ctxbg)
1095 tcheck(t, err, "tlsrptdb records")
1096 if len(records) != n {
1097 t.Fatalf("got %d tlsrptdb records, expected %d", len(records), n)
1102 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}}]}`
1104 run("mjl@mox.example", tlsrpt, 0)
1105 run("mjl@mox.example", strings.ReplaceAll(tlsrpt, "xmox.nl", "mox.example"), 1)
1106 run("mjl@mailhost.mox.example", strings.ReplaceAll(tlsrpt, "xmox.nl", "mailhost.mox.example"), 2)
1108 // We always store as an evaluation, but as optional for reports.
1109 evals := checkEvaluationCount(t, 3)
1110 tcompare(t, evals[0].Optional, true)
1111 tcompare(t, evals[1].Optional, true)
1112 tcompare(t, evals[2].Optional, true)
1115func TestRatelimitConnectionrate(t *testing.T) {
1116 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1119 // We'll be creating 300 connections, no TLS and reduce noise.
1120 ts.tlsmode = smtpclient.TLSSkip
1121 mlog.SetConfig(map[string]slog.Level{"": mlog.LevelInfo})
1122 defer mlog.SetConfig(map[string]slog.Level{"": mlog.LevelDebug})
1124 // We may be passing a window boundary during this tests. The limit is 300/minute.
1125 // So make twice that many connections and hope the tests don't take too long.
1126 for i := 0; i <= 2*300; i++ {
1127 ts.run(func(err error, client *smtpclient.Client) {
1129 if err != nil && i < 300 {
1130 t.Fatalf("expected smtp connection, got %v", err)
1132 if err == nil && i == 600 {
1133 t.Fatalf("expected no smtp connection due to connection rate limit, got connection")
1142func TestRatelimitAuth(t *testing.T) {
1143 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1146 ts.submission = true
1147 ts.tlsmode = smtpclient.TLSSkip
1151 // We may be passing a window boundary during this tests. The limit is 10 auth
1152 // failures/minute. So make twice that many connections and hope the tests don't
1154 for i := 0; i <= 2*10; i++ {
1155 ts.run(func(err error, client *smtpclient.Client) {
1158 t.Fatalf("got auth success with bad credentials")
1160 var cerr smtpclient.Error
1161 badauth := errors.As(err, &cerr) && cerr.Code == smtp.C535AuthBadCreds
1162 if !badauth && i < 10 {
1163 t.Fatalf("expected auth failure, got %v", err)
1165 if badauth && i == 20 {
1166 t.Fatalf("expected no smtp connection due to failed auth rate limit, got other error %v", err)
1175func TestRatelimitDelivery(t *testing.T) {
1176 resolver := dns.MockResolver{
1177 A: map[string][]string{
1178 "example.org.": {"127.0.0.10"}, // For mx check.
1180 PTR: map[string][]string{
1181 "127.0.0.10": {"example.org."},
1184 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1187 orig := limitIPMasked1MessagesPerMinute
1188 limitIPMasked1MessagesPerMinute = 1
1190 limitIPMasked1MessagesPerMinute = orig
1193 ts.run(func(err error, client *smtpclient.Client) {
1194 mailFrom := "remote@example.org"
1195 rcptTo := "mjl@mox.example"
1197 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1199 tcheck(t, err, "deliver to remote")
1201 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1202 var cerr smtpclient.Error
1203 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C452StorageFull {
1204 t.Fatalf("got err %v, expected smtpclient error with code 452 for storage full", err)
1208 limitIPMasked1MessagesPerMinute = orig
1210 origSize := limitIPMasked1SizePerMinute
1211 // Message was already delivered once. We'll do another one. But the 3rd will fail.
1212 // We need the actual size with prepended headers, since that is used in the
1214 msg, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).Get()
1216 t.Fatalf("getting delivered message for its size: %v", err)
1218 limitIPMasked1SizePerMinute = 2*msg.Size + int64(len(deliverMessage)/2)
1220 limitIPMasked1SizePerMinute = origSize
1222 ts.run(func(err error, client *smtpclient.Client) {
1223 mailFrom := "remote@example.org"
1224 rcptTo := "mjl@mox.example"
1226 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1228 tcheck(t, err, "deliver to remote")
1230 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1231 var cerr smtpclient.Error
1232 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C452StorageFull {
1233 t.Fatalf("got err %v, expected smtpclient error with code 452 for storage full", err)
1238func TestNonSMTP(t *testing.T) {
1239 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1243 serverConn, clientConn := net.Pipe()
1244 defer serverConn.Close()
1245 serverdone := make(chan struct{})
1246 defer func() { <-serverdone }()
1249 tlsConfig := &tls.Config{
1250 Certificates: []tls.Certificate{fakeCert(ts.t)},
1252 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)
1256 defer clientConn.Close()
1258 buf := make([]byte, 128)
1260 // Read and ignore hello.
1261 if _, err := clientConn.Read(buf); err != nil {
1262 t.Fatalf("reading hello: %v", err)
1265 if _, err := fmt.Fprintf(clientConn, "bogus\r\n"); err != nil {
1266 t.Fatalf("write command: %v", err)
1268 n, err := clientConn.Read(buf)
1270 t.Fatalf("read response line: %v", err)
1272 s := string(buf[:n])
1273 if !strings.HasPrefix(s, "500 5.5.2 ") {
1274 t.Fatalf(`got %q, expected "500 5.5.2 ...`, s)
1276 if _, err := clientConn.Read(buf); err == nil {
1277 t.Fatalf("connection not closed after bogus command")
1281// Test limits on outgoing messages.
1282func TestLimitOutgoing(t *testing.T) {
1283 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpserversendlimit/mox.conf"), dns.MockResolver{})
1286 ts.user = "mjl@mox.example"
1288 ts.submission = true
1290 err := ts.acc.DB.Insert(ctxbg, &store.Outgoing{Recipient: "a@other.example", Submitted: time.Now().Add(-24*time.Hour - time.Minute)})
1291 tcheck(t, err, "inserting outgoing/recipient past 24h window")
1293 testSubmit := func(rcptTo string, expErr *smtpclient.Error) {
1295 ts.run(func(err error, client *smtpclient.Client) {
1297 mailFrom := "mjl@mox.example"
1299 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
1301 ts.smtpErr(err, expErr)
1305 // Limits are set to 4 messages a day, 2 first-time recipients.
1306 testSubmit("b@other.example", nil)
1307 testSubmit("c@other.example", nil)
1308 testSubmit("d@other.example", &smtpclient.Error{Code: smtp.C451LocalErr, Secode: smtp.SePol7DeliveryUnauth1}) // Would be 3rd recipient.
1309 testSubmit("b@other.example", nil)
1310 testSubmit("b@other.example", nil)
1311 testSubmit("b@other.example", &smtpclient.Error{Code: smtp.C451LocalErr, Secode: smtp.SePol7DeliveryUnauth1}) // Would be 5th message.
1314// Test account size limit enforcement.
1315func TestQuota(t *testing.T) {
1316 resolver := dns.MockResolver{
1317 A: map[string][]string{
1318 "other.example.": {"127.0.0.10"}, // For mx check.
1320 PTR: map[string][]string{
1321 "127.0.0.10": {"other.example."},
1324 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpserverquota/mox.conf"), resolver)
1327 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1329 ts.run(func(err error, client *smtpclient.Client) {
1331 mailFrom := "mjl@other.example"
1333 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1335 ts.smtpErr(err, expErr)
1339 testDeliver("mjl@mox.example", &smtpclient.Error{Code: smtp.C452StorageFull, Secode: smtp.SeMailbox2Full2})
1342// Test with catchall destination address.
1343func TestCatchall(t *testing.T) {
1344 resolver := dns.MockResolver{
1345 A: map[string][]string{
1346 "other.example.": {"127.0.0.10"}, // For mx check.
1348 PTR: map[string][]string{
1349 "127.0.0.10": {"other.example."},
1352 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpservercatchall/mox.conf"), resolver)
1355 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1357 ts.run(func(err error, client *smtpclient.Client) {
1359 mailFrom := "mjl@other.example"
1361 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
1363 ts.smtpErr(err, expErr)
1367 testDeliver("mjl@mox.example", nil) // Exact match.
1368 testDeliver("mjl+test@mox.example", nil) // Domain localpart catchall separator.
1369 testDeliver("MJL+TEST@mox.example", nil) // Again, and case insensitive.
1370 testDeliver("unknown@mox.example", nil) // Catchall address, to account catchall.
1372 n, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).Count()
1373 tcheck(t, err, "checking delivered messages")
1376 acc, err := store.OpenAccount(pkglog, "catchall")
1377 tcheck(t, err, "open account")
1382 n, err = bstore.QueryDB[store.Message](ctxbg, acc.DB).Count()
1383 tcheck(t, err, "checking delivered messages to catchall account")
1387// Test DKIM signing for outgoing messages.
1388func TestDKIMSign(t *testing.T) {
1389 resolver := dns.MockResolver{
1390 A: map[string][]string{
1391 "mox.example.": {"127.0.0.10"}, // For mx check.
1393 PTR: map[string][]string{
1394 "127.0.0.10": {"mox.example."},
1398 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1401 // Set DKIM signing config.
1403 genDKIM := func(domain string) string {
1404 dom, _ := mox.Conf.Domain(dns.Domain{ASCII: domain})
1406 privkey := make([]byte, ed25519.SeedSize) // Fake key, don't use for real.
1408 privkey[0] = byte(gen)
1410 sel := config.Selector{
1411 HashEffective: "sha256",
1412 HeadersEffective: []string{"From", "To", "Subject"},
1413 Key: ed25519.NewKeyFromSeed(privkey),
1414 Domain: dns.Domain{ASCII: "testsel"},
1416 dom.DKIM = config.DKIM{
1417 Selectors: map[string]config.Selector{"testsel": sel},
1418 Sign: []string{"testsel"},
1420 mox.Conf.Dynamic.Domains[domain] = dom
1421 pubkey := sel.Key.Public().(ed25519.PublicKey)
1422 return "v=DKIM1;k=ed25519;p=" + base64.StdEncoding.EncodeToString(pubkey)
1425 dkimtxt := genDKIM("mox.example")
1426 dkimtxt2 := genDKIM("mox2.example")
1428 // DKIM verify needs to find the key.
1429 resolver.TXT = map[string][]string{
1430 "testsel._domainkey.mox.example.": {dkimtxt},
1431 "testsel._domainkey.mox2.example.": {dkimtxt2},
1434 ts.submission = true
1435 ts.user = "mjl@mox.example"
1439 testSubmit := func(mailFrom, msgFrom string) {
1441 ts.run(func(err error, client *smtpclient.Client) {
1444 msg := strings.ReplaceAll(fmt.Sprintf(`From: <%s>
1445To: <remote@example.org>
1447Message-Id: <test@mox.example>
1450`, msgFrom), "\n", "\r\n")
1452 rcptTo := "remote@example.org"
1454 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1456 tcheck(t, err, "deliver")
1458 msgs, err := queue.List(ctxbg, queue.Filter{}, queue.Sort{})
1459 tcheck(t, err, "listing queue")
1461 tcompare(t, len(msgs), n)
1462 sort.Slice(msgs, func(i, j int) bool {
1463 return msgs[i].ID > msgs[j].ID
1465 f, err := queue.OpenMessage(ctxbg, msgs[0].ID)
1466 tcheck(t, err, "open message in queue")
1468 results, err := dkim.Verify(ctxbg, pkglog.Logger, resolver, false, dkim.DefaultPolicy, f, false)
1469 tcheck(t, err, "verifying dkim message")
1470 tcompare(t, len(results), 1)
1471 tcompare(t, results[0].Status, dkim.StatusPass)
1472 tcompare(t, results[0].Sig.Domain.ASCII, strings.Split(msgFrom, "@")[1])
1476 testSubmit("mjl@mox.example", "mjl@mox.example")
1477 testSubmit("mjl@mox.example", "mjl@mox2.example") // DKIM signature will be for mox2.example.
1480// Test to postmaster addresses.
1481func TestPostmaster(t *testing.T) {
1482 resolver := dns.MockResolver{
1483 A: map[string][]string{
1484 "other.example.": {"127.0.0.10"}, // For mx check.
1486 PTR: map[string][]string{
1487 "127.0.0.10": {"other.example."},
1490 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/postmaster/mox.conf"), resolver)
1493 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1495 ts.run(func(err error, client *smtpclient.Client) {
1497 mailFrom := "mjl@other.example"
1499 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1501 ts.smtpErr(err, expErr)
1505 testDeliver("postmaster", nil) // Plain postmaster address without domain.
1506 testDeliver("postmaster@host.mox.example", nil) // Postmaster address with configured mail server hostname.
1507 testDeliver("postmaster@mox.example", nil) // Postmaster address without explicitly configured destination.
1508 testDeliver("postmaster@unknown.example", &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
1511// Test to address with empty localpart.
1512func TestEmptylocalpart(t *testing.T) {
1513 resolver := dns.MockResolver{
1514 A: map[string][]string{
1515 "other.example.": {"127.0.0.10"}, // For mx check.
1517 PTR: map[string][]string{
1518 "127.0.0.10": {"other.example."},
1521 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1524 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1526 ts.run(func(err error, client *smtpclient.Client) {
1529 mailFrom := `""@other.example`
1530 msg := strings.ReplaceAll(deliverMessage, "To: <mjl@mox.example>", `To: <""@mox.example>`)
1532 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1534 ts.smtpErr(err, expErr)
1538 testDeliver(`""@mox.example`, nil)
1541// Test handling REQUIRETLS and TLS-Required: No.
1542func TestRequireTLS(t *testing.T) {
1543 resolver := dns.MockResolver{
1544 A: map[string][]string{
1545 "mox.example.": {"127.0.0.10"}, // For mx check.
1547 PTR: map[string][]string{
1548 "127.0.0.10": {"mox.example."},
1552 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1555 ts.submission = true
1556 ts.requiretls = true
1557 ts.user = "mjl@mox.example"
1563 msg0 := strings.ReplaceAll(`From: <mjl@mox.example>
1564To: <remote@example.org>
1566Message-Id: <test@mox.example>
1572 msg1 := strings.ReplaceAll(`From: <mjl@mox.example>
1573To: <remote@example.org>
1575Message-Id: <test@mox.example>
1582 msg2 := strings.ReplaceAll(`From: <mjl@mox.example>
1583To: <remote@example.org>
1585Message-Id: <test@mox.example>
1590 testSubmit := func(msg string, requiretls bool, expRequireTLS *bool) {
1592 ts.run(func(err error, client *smtpclient.Client) {
1595 rcptTo := "remote@example.org"
1597 err = client.Deliver(ctxbg, "mjl@mox.example", rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, requiretls)
1599 tcheck(t, err, "deliver")
1601 msgs, err := queue.List(ctxbg, queue.Filter{}, queue.Sort{})
1602 tcheck(t, err, "listing queue")
1603 tcompare(t, len(msgs), 1)
1604 tcompare(t, msgs[0].RequireTLS, expRequireTLS)
1605 _, err = queue.Drop(ctxbg, pkglog, queue.Filter{IDs: []int64{msgs[0].ID}})
1606 tcheck(t, err, "deleting message from queue")
1610 testSubmit(msg0, true, &yes) // Header ignored, requiretls applied.
1611 testSubmit(msg0, false, &no) // TLS-Required header applied.
1612 testSubmit(msg1, true, &yes) // Bad headers ignored, requiretls applied.
1613 testSubmit(msg1, false, nil) // Inconsistent multiple headers ignored.
1614 testSubmit(msg2, false, nil) // Regular message, no RequireTLS setting.
1615 testSubmit(msg2, true, &yes) // Requiretls applied.
1617 // Check that we get an error if remote SMTP server does not support the requiretls
1619 ts.requiretls = false
1620 ts.run(func(err error, client *smtpclient.Client) {
1623 rcptTo := "remote@example.org"
1625 err = client.Deliver(ctxbg, "mjl@mox.example", rcptTo, int64(len(msg0)), strings.NewReader(msg0), false, false, true)
1628 t.Fatalf("delivered with requiretls to server without requiretls")
1630 if !errors.Is(err, smtpclient.ErrRequireTLSUnsupported) {
1631 t.Fatalf("got err %v, expected ErrRequireTLSUnsupported", err)
1636func TestSmuggle(t *testing.T) {
1637 resolver := dns.MockResolver{
1638 A: map[string][]string{
1639 "example.org.": {"127.0.0.10"}, // For mx check.
1641 PTR: map[string][]string{
1642 "127.0.0.10": {"example.org."}, // For iprev check.
1645 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpsmuggle/mox.conf"), resolver)
1646 ts.tlsmode = smtpclient.TLSSkip
1649 test := func(data string) {
1652 ts.runRaw(func(conn net.Conn) {
1655 ourHostname := mox.Conf.Static.HostnameDomain
1656 remoteHostname := dns.Domain{ASCII: "mox.example"}
1657 opts := smtpclient.Opts{
1658 RootCAs: mox.Conf.Static.TLS.CertPool,
1660 log := pkglog.WithCid(ts.cid - 1)
1661 _, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, ts.tlspkix, ourHostname, remoteHostname, opts)
1662 tcheck(t, err, "smtpclient")
1665 write := func(s string) {
1666 _, err := conn.Write([]byte(s))
1667 tcheck(t, err, "write")
1670 readPrefixLine := func(prefix string) string {
1672 buf := make([]byte, 512)
1673 n, err := conn.Read(buf)
1674 tcheck(t, err, "read")
1675 s := strings.TrimRight(string(buf[:n]), "\r\n")
1676 if !strings.HasPrefix(s, prefix) {
1677 t.Fatalf("got smtp response %q, expected line with prefix %q", s, prefix)
1682 write("MAIL FROM:<remote@example.org>\r\n")
1684 write("RCPT TO:<mjl@mox.example>\r\n")
1689 write("\r\n") // Empty header.
1691 write("\r\n.\r\n") // End of message.
1692 line := readPrefixLine("5")
1693 if !strings.Contains(line, "smug") {
1694 t.Errorf("got 5xx error with message %q, expected error text containing smug", line)
1705func TestFutureRelease(t *testing.T) {
1706 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1707 ts.tlsmode = smtpclient.TLSSkip
1708 ts.user = "mjl@mox.example"
1710 ts.submission = true
1713 ts.auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
1714 return sasl.NewClientPlain(ts.user, ts.pass), nil
1717 test := func(mailtoMore, expResponsePrefix string) {
1720 ts.runRaw(func(conn net.Conn) {
1723 ourHostname := mox.Conf.Static.HostnameDomain
1724 remoteHostname := dns.Domain{ASCII: "mox.example"}
1725 opts := smtpclient.Opts{Auth: ts.auth}
1726 log := pkglog.WithCid(ts.cid - 1)
1727 _, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, false, ourHostname, remoteHostname, opts)
1728 tcheck(t, err, "smtpclient")
1731 write := func(s string) {
1732 _, err := conn.Write([]byte(s))
1733 tcheck(t, err, "write")
1736 readPrefixLine := func(prefix string) string {
1738 buf := make([]byte, 512)
1739 n, err := conn.Read(buf)
1740 tcheck(t, err, "read")
1741 s := strings.TrimRight(string(buf[:n]), "\r\n")
1742 if !strings.HasPrefix(s, prefix) {
1743 t.Fatalf("got smtp response %q, expected line with prefix %q", s, prefix)
1748 write(fmt.Sprintf("MAIL FROM:<mjl@mox.example>%s\r\n", mailtoMore))
1749 readPrefixLine(expResponsePrefix)
1750 if expResponsePrefix != "2" {
1753 write("RCPT TO:<mjl@mox.example>\r\n")
1758 write("From: <mjl@mox.example>\r\n\r\nbody\r\n\r\n.\r\n")
1763 test(" HOLDFOR=1", "2")
1764 test(" HOLDUNTIL="+time.Now().Add(time.Minute).UTC().Format(time.RFC3339), "2")
1765 test(" HOLDUNTIL="+time.Now().Add(time.Minute).UTC().Format(time.RFC3339Nano), "2")
1767 test(" HOLDFOR=0", "501") // 0 is invalid syntax.
1768 test(fmt.Sprintf(" HOLDFOR=%d", int64((queue.FutureReleaseIntervalMax+time.Minute)/time.Second)), "554") // Too far in the future.
1769 test(" HOLDUNTIL="+time.Now().Add(-time.Minute).UTC().Format(time.RFC3339), "554") // In the past.
1770 test(" HOLDUNTIL="+time.Now().Add(queue.FutureReleaseIntervalMax+time.Minute).UTC().Format(time.RFC3339), "554") // Too far in the future.
1771 test(" HOLDUNTIL=2024-02-10T17:28:00+00:00", "501") // "Z" required.
1772 test(" HOLDUNTIL=24-02-10T17:28:00Z", "501") // Invalid.
1773 test(" HOLDFOR=1 HOLDFOR=1", "501") // Duplicate.
1774 test(" HOLDFOR=1 HOLDUNTIL="+time.Now().Add(time.Hour).UTC().Format(time.RFC3339), "501") // Duplicate.
1778func TestSMTPUTF8(t *testing.T) {
1779 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1782 ts.user = "mjl@mox.example"
1784 ts.submission = true
1786 test := func(mailFrom string, rcptTo string, headerValue string, filename string, clientSmtputf8 bool, expectedSmtputf8 bool, expErr *smtpclient.Error) {
1789 ts.run(func(_ error, client *smtpclient.Client) {
1791 msg := strings.ReplaceAll(fmt.Sprintf(`From: <%s>
1794X-Custom-Test-Header: %s
1796Content-type: multipart/mixed; boundary="simple boundary"
1799Content-Type: text/plain; charset=UTF-8;
1800Content-Disposition: attachment; filename="%s"
1801Content-Transfer-Encoding: base64
1803QW4gYXR0YWNoZWQgdGV4dCBmaWxlLg==
1806`, mailFrom, rcptTo, headerValue, filename), "\n", "\r\n")
1808 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), true, clientSmtputf8, false)
1809 ts.smtpErr(err, expErr)
1814 msgs, _ := queue.List(ctxbg, queue.Filter{}, queue.Sort{Field: "Queued", Asc: false})
1815 queuedMsg := msgs[0]
1816 if queuedMsg.SMTPUTF8 != expectedSmtputf8 {
1817 t.Fatalf("[%s / %s / %s / %s] got SMTPUTF8 %t, expected %t", mailFrom, rcptTo, headerValue, filename, queuedMsg.SMTPUTF8, expectedSmtputf8)
1822 test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", false, false, nil)
1823 test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", true, false, nil)
1824 test(`mjl@mox.example`, `🙂@example.org`, "header-ascii", "ascii.txt", true, true, nil)
1825 test(`mjl@mox.example`, `🙂@example.org`, "header-ascii", "ascii.txt", false, true, &smtpclient.Error{Permanent: true, Code: smtp.C553BadMailbox, Secode: smtp.SeMsg6NonASCIIAddrNotPermitted7})
1826 test(`Ω@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", true, true, nil)
1827 test(`Ω@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", false, true, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeMsg6NonASCIIAddrNotPermitted7})
1828 test(`mjl@mox.example`, `remote@example.org`, "header-utf8-😍", "ascii.txt", true, true, nil)
1829 test(`mjl@mox.example`, `remote@example.org`, "header-utf8-😍", "ascii.txt", false, true, nil)
1830 test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "utf8-🫠️.txt", true, true, nil)
1831 test(`Ω@mox.example`, `🙂@example.org`, "header-utf8-😍", "utf8-🫠️.txt", true, true, nil)
1832 test(`mjl@mox.example`, `remote@xn--vg8h.example.org`, "header-ascii", "ascii.txt", true, false, nil)
1835// TestExtra checks whether submission of messages with "X-Mox-Extra-<key>: value"
1836// headers cause those those key/value pairs to be added to the Extra field in the
1838func TestExtra(t *testing.T) {
1839 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1842 ts.user = "mjl@mox.example"
1844 ts.submission = true
1846 extraMsg := strings.ReplaceAll(`From: <mjl@mox.example>
1847To: <remote@example.org>
1849X-Mox-Extra-Test: testvalue
1852X-Mox-Extra-x-cANONICAL-z: ok
1853Message-Id: <test@mox.example>
1858 ts.run(func(err error, client *smtpclient.Client) {
1860 tcheck(t, err, "init client")
1861 mailFrom := "mjl@mox.example"
1862 rcptTo := "mjl@mox.example"
1863 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
1864 tcheck(t, err, "deliver")
1866 msgs, err := queue.List(ctxbg, queue.Filter{}, queue.Sort{})
1867 tcheck(t, err, "queue list")
1868 tcompare(t, len(msgs), 1)
1869 tcompare(t, msgs[0].Extra, map[string]string{
1870 "Test": "testvalue",
1873 "X-Canonical-Z": "ok",
1875 // note: these headers currently stay in the message.
1878// TestExtraDup checks for an error for duplicate x-mox-extra-* keys.
1879func TestExtraDup(t *testing.T) {
1880 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1883 ts.user = "mjl@mox.example"
1885 ts.submission = true
1887 extraMsg := strings.ReplaceAll(`From: <mjl@mox.example>
1888To: <remote@example.org>
1890X-Mox-Extra-Test: testvalue
1891X-Mox-Extra-Test: testvalue
1892Message-Id: <test@mox.example>
1897 ts.run(func(err error, client *smtpclient.Client) {
1898 tcheck(t, err, "init client")
1899 mailFrom := "mjl@mox.example"
1900 rcptTo := "mjl@mox.example"
1901 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
1902 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C554TransactionFailed, Secode: smtp.SeMsg6Other0})
1906// FromID can be specified during submission, but must be unique, with single recipient.
1907func TestUniqueFromID(t *testing.T) {
1908 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpfromid/mox.conf"), dns.MockResolver{})
1911 ts.user = "mjl+fromid@mox.example"
1913 ts.submission = true
1915 extraMsg := strings.ReplaceAll(`From: <mjl@mox.example>
1916To: <remote@example.org>
1922 // Specify our own unique id when queueing.
1923 ts.run(func(err error, client *smtpclient.Client) {
1924 tcheck(t, err, "init client")
1925 mailFrom := "mjl+unique@mox.example"
1926 rcptTo := "mjl@mox.example"
1927 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
1928 ts.smtpErr(err, nil)
1931 // But we can only use it once.
1932 ts.run(func(err error, client *smtpclient.Client) {
1933 tcheck(t, err, "init client")
1934 mailFrom := "mjl+unique@mox.example"
1935 rcptTo := "mjl@mox.example"
1936 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
1937 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C554TransactionFailed, Secode: smtp.SeAddr1SenderSyntax7})
1940 // We cannot use our own fromid with multiple recipients.
1941 ts.run(func(err error, client *smtpclient.Client) {
1942 tcheck(t, err, "init client")
1943 mailFrom := "mjl+unique2@mox.example"
1944 rcptTo := []string{"mjl@mox.example", "mjl@mox.example"}
1945 _, err = client.DeliverMultiple(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
1946 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C554TransactionFailed, Secode: smtp.SeProto5TooManyRcpts3})