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 if dmarcdb.EvalDB != nil {
109 dmarcdb.EvalDB.Close()
113 log := mlog.New("smtpserver", nil)
115 mox.ConfigStaticPath = configPath
116 mox.MustLoadConfig(true, false)
117 dataDir := mox.ConfigDirPath(mox.Conf.Static.DataDir)
118 os.RemoveAll(dataDir)
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")
124 ts.switchStop = store.Switchboard()
126 tcheck(t, err, "queue init")
128 ts.comm = store.RegisterComm(ts.acc)
133func (ts *testserver) close() {
140 err := ts.acc.Close()
141 tcheck(ts.t, err, "closing account")
145func (ts *testserver) run(fn func(helloErr error, client *smtpclient.Client)) {
146 ts.runRaw(func(conn net.Conn) {
150 if auth == nil && ts.user != "" {
151 auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
152 return sasl.NewClientPlain(ts.user, ts.pass), nil
156 ourHostname := mox.Conf.Static.HostnameDomain
157 remoteHostname := dns.Domain{ASCII: "mox.example"}
158 opts := smtpclient.Opts{
160 RootCAs: mox.Conf.Static.TLS.CertPool,
162 log := pkglog.WithCid(ts.cid - 1)
163 client, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, ts.tlspkix, ourHostname, remoteHostname, opts)
173func (ts *testserver) runRaw(fn func(clientConn net.Conn)) {
178 serverConn, clientConn := net.Pipe()
179 defer serverConn.Close()
180 // clientConn is closed as part of closing client.
181 serverdone := make(chan struct{})
182 defer func() { <-serverdone }()
185 tlsConfig := &tls.Config{
186 Certificates: []tls.Certificate{fakeCert(ts.t)},
188 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)
195// Just a cert that appears valid. SMTP client will not verify anything about it
196// (that is opportunistic TLS for you, "better some than none"). Let's enjoy this
197// one moment where it makes life easier.
198func fakeCert(t *testing.T) tls.Certificate {
199 privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
200 template := &x509.Certificate{
201 SerialNumber: big.NewInt(1), // Required field...
203 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
205 t.Fatalf("making certificate: %s", err)
207 cert, err := x509.ParseCertificate(localCertBuf)
209 t.Fatalf("parsing generated certificate: %s", err)
211 c := tls.Certificate{
212 Certificate: [][]byte{localCertBuf},
219// check expected dmarc evaluations for outgoing aggregate reports.
220func checkEvaluationCount(t *testing.T, n int) []dmarcdb.Evaluation {
222 l, err := dmarcdb.Evaluations(ctxbg)
223 tcheck(t, err, "get dmarc evaluations")
224 tcompare(t, len(l), n)
228// Test submission from authenticated user.
229func TestSubmission(t *testing.T) {
230 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
233 // Set DKIM signing config.
234 dom, _ := mox.Conf.Domain(dns.Domain{ASCII: "mox.example"})
235 sel := config.Selector{
236 HashEffective: "sha256",
237 HeadersEffective: []string{"From", "To", "Subject"},
238 Key: ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)), // Fake key, don't use for real.
239 Domain: dns.Domain{ASCII: "mox.example"},
241 dom.DKIM = config.DKIM{
242 Selectors: map[string]config.Selector{"testsel": sel},
243 Sign: []string{"testsel"},
245 mox.Conf.Dynamic.Domains["mox.example"] = dom
247 testAuth := func(authfn func(user, pass string, cs *tls.ConnectionState) sasl.Client, user, pass string, expErr *smtpclient.Error) {
250 ts.auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
251 return authfn(user, pass, cs), nil
256 ts.run(func(err error, client *smtpclient.Client) {
258 mailFrom := "mjl@mox.example"
259 rcptTo := "remote@example.org"
261 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
263 var cerr smtpclient.Error
264 if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Secode != expErr.Secode) {
265 t.Fatalf("got err %#v (%q), expected %#v", err, err, expErr)
267 checkEvaluationCount(t, 0)
272 testAuth(nil, "", "", &smtpclient.Error{Permanent: true, Code: smtp.C530SecurityRequired, Secode: smtp.SePol7Other0})
273 authfns := []func(user, pass string, cs *tls.ConnectionState) sasl.Client{
274 func(user, pass string, cs *tls.ConnectionState) sasl.Client { return sasl.NewClientPlain(user, pass) },
275 func(user, pass string, cs *tls.ConnectionState) sasl.Client { return sasl.NewClientLogin(user, pass) },
276 func(user, pass string, cs *tls.ConnectionState) sasl.Client { return sasl.NewClientCRAMMD5(user, pass) },
277 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
278 return sasl.NewClientSCRAMSHA1(user, pass, false)
280 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
281 return sasl.NewClientSCRAMSHA256(user, pass, false)
283 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
284 return sasl.NewClientSCRAMSHA1PLUS(user, pass, *cs)
286 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
287 return sasl.NewClientSCRAMSHA256PLUS(user, pass, *cs)
290 for _, fn := range authfns {
291 testAuth(fn, "mjl@mox.example", "test", &smtpclient.Error{Secode: smtp.SePol7AuthBadCreds8}) // Bad (short) password.
292 testAuth(fn, "mjl@mox.example", password0+"test", &smtpclient.Error{Secode: smtp.SePol7AuthBadCreds8}) // Bad password.
293 testAuth(fn, "mjl@mox.example", password0, nil)
294 testAuth(fn, "mjl@mox.example", password1, nil)
295 testAuth(fn, "móx@mox.example", password0, nil)
296 testAuth(fn, "móx@mox.example", password1, nil)
297 testAuth(fn, "mo\u0301x@mox.example", password0, nil)
298 testAuth(fn, "mo\u0301x@mox.example", password1, nil)
302// Test delivery from external MTA.
303func TestDelivery(t *testing.T) {
304 resolver := dns.MockResolver{
305 A: map[string][]string{
306 "example.org.": {"127.0.0.10"}, // For mx check.
308 PTR: map[string][]string{},
310 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
313 ts.run(func(err error, client *smtpclient.Client) {
314 mailFrom := "remote@example.org"
315 rcptTo := "mjl@127.0.0.10"
317 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
319 var cerr smtpclient.Error
320 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
321 t.Fatalf("deliver to ip address, got err %v, expected smtpclient.Error with code %d", err, smtp.C550MailboxUnavail)
325 ts.run(func(err error, client *smtpclient.Client) {
326 mailFrom := "remote@example.org"
327 rcptTo := "mjl@test.example" // Not configured as destination.
329 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
331 var cerr smtpclient.Error
332 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
333 t.Fatalf("deliver to unknown domain, got err %v, expected smtpclient.Error with code %d", err, smtp.C550MailboxUnavail)
337 ts.run(func(err error, client *smtpclient.Client) {
338 mailFrom := "remote@example.org"
339 rcptTo := "unknown@mox.example" // User unknown.
341 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
343 var cerr smtpclient.Error
344 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
345 t.Fatalf("deliver to unknown user for known domain, got err %v, expected smtpclient.Error with code %d", err, smtp.C550MailboxUnavail)
349 ts.run(func(err error, client *smtpclient.Client) {
350 mailFrom := "remote@example.org"
351 rcptTo := "mjl@mox.example"
353 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
355 var cerr smtpclient.Error
356 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
357 t.Fatalf("deliver from user without reputation, valid iprev required, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
361 // Set up iprev to get delivery from unknown user to be accepted.
362 resolver.PTR["127.0.0.10"] = []string{"example.org."}
364 // Only ascii o@ is configured, not the greek and cyrillic lookalikes.
365 ts.run(func(err error, client *smtpclient.Client) {
366 mailFrom := "remote@example.org"
367 rcptTo := "ο@mox.example" // omicron \u03bf, looks like the configured o@
368 msg := strings.ReplaceAll(deliverMessage, "mjl@mox.example", rcptTo)
370 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, true, false)
372 var cerr smtpclient.Error
373 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
374 t.Fatalf("deliver to omicron @ instead of ascii o @, got err %v, expected smtpclient.Error with code %d", err, smtp.C550MailboxUnavail)
378 ts.run(func(err error, client *smtpclient.Client) {
379 recipients := []string{
381 "o@mox.example", // ascii o, as configured
382 "\u2126@mox.example", // ohm sign, as configured
383 "ω@mox.example", // lower-case omega, we match case-insensitively and this is the lowercase of ohm (!)
384 "\u03a9@mox.example", // capital omega, also lowercased to omega.
385 "móx@mox.example", // NFC
386 "mo\u0301x@mox.example", // not NFC, but normalized as móx@, see https://go.dev/blog/normalization
389 for _, rcptTo := range recipients {
390 // Ensure SMTP RCPT TO and message address headers are the same, otherwise the junk
391 // filter treats us more strictly.
392 msg := strings.ReplaceAll(deliverMessage, "mjl@mox.example", rcptTo)
394 mailFrom := "remote@example.org"
396 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, true, false)
398 tcheck(t, err, "deliver to remote")
400 changes := make(chan []store.Change)
402 changes <- ts.comm.Get()
405 timer := time.NewTimer(time.Second)
410 t.Fatalf("no delivery in 1s")
415 checkEvaluationCount(t, 0)
418func tinsertmsg(t *testing.T, acc *store.Account, mailbox string, m *store.Message, msg string) {
419 mf, err := store.CreateMessageTemp(pkglog, "queue-dsn")
420 tcheck(t, err, "temp message")
421 defer os.Remove(mf.Name())
423 _, err = mf.Write([]byte(msg))
424 tcheck(t, err, "write message")
425 err = acc.DeliverMailbox(pkglog, mailbox, m, mf)
426 tcheck(t, err, "deliver message")
428 tcheck(t, err, "close message")
431func tretrain(t *testing.T, acc *store.Account) {
434 // Fresh empty junkfilter.
435 basePath := mox.DataDirPath("accounts")
436 dbPath := filepath.Join(basePath, acc.Name, "junkfilter.db")
437 bloomPath := filepath.Join(basePath, acc.Name, "junkfilter.bloom")
440 jf, _, err := acc.OpenJunkFilter(ctxbg, pkglog)
441 tcheck(t, err, "open junk filter")
444 // Fetch messags to retrain on.
445 q := bstore.QueryDB[store.Message](ctxbg, acc.DB)
446 q.FilterEqual("Expunged", false)
447 q.FilterFn(func(m store.Message) bool {
448 return m.Flags.Junk || m.Flags.Notjunk
450 msgs, err := q.List()
451 tcheck(t, err, "fetch messages")
453 // Retrain the messages.
454 for _, m := range msgs {
455 ham := m.Flags.Notjunk
457 f, err := os.Open(acc.MessagePath(m.ID))
458 tcheck(t, err, "open message")
459 r := store.FileMsgReader(m.MsgPrefix, f)
461 jf.TrainMessage(ctxbg, r, m.Size, ham)
464 tcheck(t, err, "close message")
468 tcheck(t, err, "save junkfilter")
471// Test accept/reject with DMARC reputation and with spammy content.
472func TestSpam(t *testing.T) {
473 resolver := &dns.MockResolver{
474 A: map[string][]string{
475 "example.org.": {"127.0.0.1"}, // For mx check.
477 TXT: map[string][]string{
478 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
479 "_dmarc.example.org.": {"v=DMARC1;p=reject; rua=mailto:dmarcrpt@example.org"},
482 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
485 // Insert spammy messages. No junkfilter training yet.
487 RemoteIP: "127.0.0.10",
488 RemoteIPMasked1: "127.0.0.10",
489 RemoteIPMasked2: "127.0.0.0",
490 RemoteIPMasked3: "127.0.0.0",
491 MailFrom: "remote@example.org",
492 MailFromLocalpart: smtp.Localpart("remote"),
493 MailFromDomain: "example.org",
494 RcptToLocalpart: smtp.Localpart("mjl"),
495 RcptToDomain: "mox.example",
496 MsgFromLocalpart: smtp.Localpart("remote"),
497 MsgFromDomain: "example.org",
498 MsgFromOrgDomain: "example.org",
499 MsgFromValidated: true,
500 MsgFromValidation: store.ValidationStrict,
501 Flags: store.Flags{Seen: true, Junk: true},
502 Size: int64(len(deliverMessage)),
504 for i := 0; i < 3; i++ {
506 tinsertmsg(t, ts.acc, "Inbox", &nm, deliverMessage)
509 checkCount := func(mailboxName string, expect int) {
511 q := bstore.QueryDB[store.Mailbox](ctxbg, ts.acc.DB)
512 q.FilterNonzero(store.Mailbox{Name: mailboxName})
514 tcheck(t, err, "get rejects mailbox")
515 qm := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB)
516 qm.FilterNonzero(store.Message{MailboxID: mb.ID})
517 qm.FilterEqual("Expunged", false)
519 tcheck(t, err, "count messages in rejects mailbox")
521 t.Fatalf("messages in rejects mailbox, found %d, expected %d", n, expect)
525 // Delivery from sender with bad reputation should fail.
526 ts.run(func(err error, client *smtpclient.Client) {
527 mailFrom := "remote@example.org"
528 rcptTo := "mjl@mox.example"
530 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
532 var cerr smtpclient.Error
533 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
534 t.Fatalf("delivery by bad sender, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
537 checkCount("Rejects", 1)
538 checkEvaluationCount(t, 0) // No positive interactions yet.
541 // Delivery from sender with bad reputation matching AcceptRejectsToMailbox should
542 // result in accepted delivery to the mailbox.
543 ts.run(func(err error, client *smtpclient.Client) {
544 mailFrom := "remote@example.org"
545 rcptTo := "mjl2@mox.example"
547 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage2)), strings.NewReader(deliverMessage2), false, false, false)
549 tcheck(t, err, "deliver")
551 checkCount("mjl2junk", 1) // In ruleset rejects mailbox.
552 checkCount("Rejects", 1) // Same as before.
553 checkEvaluationCount(t, 0) // This is not an actual accept.
556 // Mark the messages as having good reputation.
557 q := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB)
558 q.FilterEqual("Expunged", false)
559 _, err := q.UpdateFields(map[string]any{"Junk": false, "Notjunk": true})
560 tcheck(t, err, "update junkiness")
562 // Message should now be accepted.
563 ts.run(func(err error, client *smtpclient.Client) {
564 mailFrom := "remote@example.org"
565 rcptTo := "mjl@mox.example"
567 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
569 tcheck(t, err, "deliver")
571 // Message should now be removed from Rejects mailboxes.
572 checkCount("Rejects", 0)
573 checkCount("mjl2junk", 1)
574 checkEvaluationCount(t, 1)
577 // Undo dmarc pass, mark messages as junk, and train the filter.
579 q = bstore.QueryDB[store.Message](ctxbg, ts.acc.DB)
580 q.FilterEqual("Expunged", false)
581 _, err = q.UpdateFields(map[string]any{"Junk": true, "Notjunk": false})
582 tcheck(t, err, "update junkiness")
585 // Message should be refused for spammy content.
586 ts.run(func(err error, client *smtpclient.Client) {
587 mailFrom := "remote@example.org"
588 rcptTo := "mjl@mox.example"
590 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
592 var cerr smtpclient.Error
593 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
594 t.Fatalf("attempt to deliver spamy message, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
596 checkEvaluationCount(t, 1) // No new evaluation, this isn't a DMARC reject.
600// Test accept/reject with forwarded messages, DMARC ignored, no IP/EHLO/MAIL
601// FROM-based reputation.
602func TestForward(t *testing.T) {
603 // Do a run without forwarding, and with.
604 check := func(forward bool) {
606 resolver := &dns.MockResolver{
607 A: map[string][]string{
608 "bad.example.": {"127.0.0.1"}, // For mx check.
609 "good.example.": {"127.0.0.1"}, // For mx check.
610 "forward.example.": {"127.0.0.10"}, // For mx check.
612 TXT: map[string][]string{
613 "bad.example.": {"v=spf1 ip4:127.0.0.1 -all"},
614 "good.example.": {"v=spf1 ip4:127.0.0.1 -all"},
615 "forward.example.": {"v=spf1 ip4:127.0.0.10 -all"},
616 "_dmarc.bad.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@bad.example"},
617 "_dmarc.good.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@good.example"},
618 "_dmarc.forward.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@forward.example"},
620 PTR: map[string][]string{
621 "127.0.0.10": {"forward.example."}, // For iprev check.
624 rcptTo := "mjl3@mox.example"
626 // For SPF and DMARC pass, otherwise the test ends quickly.
627 resolver.TXT["bad.example."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
628 resolver.TXT["good.example."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
629 rcptTo = "mjl@mox.example" // Without IsForward rule.
632 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
635 totalEvaluations := 0
637 var msgBad = strings.ReplaceAll(`From: <remote@bad.example>
640Message-Id: <bad@example.org>
644 var msgOK = strings.ReplaceAll(`From: <remote@good.example>
647Message-Id: <good@example.org>
651 var msgOK2 = strings.ReplaceAll(`From: <other@forward.example>
654Message-Id: <regular@example.org>
656happens to come from forwarding mail server.
659 // Deliver forwarded messages, then classify as junk. Normally enough to treat
660 // other unrelated messages from IP as junk, but not for forwarded messages.
661 ts.run(func(err error, client *smtpclient.Client) {
662 tcheck(t, err, "connect")
664 mailFrom := "remote@forward.example"
666 mailFrom = "remote@bad.example"
669 for i := 0; i < 10; i++ {
670 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgBad)), strings.NewReader(msgBad), false, false, false)
671 tcheck(t, err, "deliver message")
673 totalEvaluations += 10
675 n, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).UpdateFields(map[string]any{"Junk": true, "MsgFromValidated": true})
676 tcheck(t, err, "marking messages as junk")
679 // Next delivery will fail, with negative "message From" signal.
680 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgBad)), strings.NewReader(msgBad), false, false, false)
681 var cerr smtpclient.Error
682 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
683 t.Fatalf("delivery by bad sender, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
686 checkEvaluationCount(t, totalEvaluations)
689 // Delivery from different "message From" without reputation, but from same
690 // forwarding email server, should succeed under forwarding, not as regular sending
692 ts.run(func(err error, client *smtpclient.Client) {
693 tcheck(t, err, "connect")
695 mailFrom := "remote@forward.example"
697 mailFrom = "remote@good.example"
700 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgOK)), strings.NewReader(msgOK), false, false, false)
702 tcheck(t, err, "deliver")
703 totalEvaluations += 1
705 var cerr smtpclient.Error
706 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
707 t.Fatalf("delivery by bad ip, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
710 checkEvaluationCount(t, totalEvaluations)
713 // Delivery from forwarding server that isn't a forward should get same treatment.
714 ts.run(func(err error, client *smtpclient.Client) {
715 tcheck(t, err, "connect")
717 mailFrom := "other@forward.example"
719 // Ensure To header matches.
722 msg = strings.ReplaceAll(msg, "<mjl@mox.example>", "<mjl3@mox.example>")
725 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
727 tcheck(t, err, "deliver")
728 totalEvaluations += 1
730 var cerr smtpclient.Error
731 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
732 t.Fatalf("delivery by bad ip, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
735 checkEvaluationCount(t, totalEvaluations)
743// Messages that we sent to, that have passing DMARC, but that are otherwise spammy, should be accepted.
744func TestDMARCSent(t *testing.T) {
745 resolver := &dns.MockResolver{
746 A: map[string][]string{
747 "example.org.": {"127.0.0.1"}, // For mx check.
749 TXT: map[string][]string{
750 "example.org.": {"v=spf1 ip4:127.0.0.1 -all"},
751 "_dmarc.example.org.": {"v=DMARC1;p=reject;rua=mailto:dmarcrpt@example.org"},
754 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
757 // First check that DMARC policy rejects message and results in optional evaluation.
758 ts.run(func(err error, client *smtpclient.Client) {
759 mailFrom := "remote@example.org"
760 rcptTo := "mjl@mox.example"
762 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
764 var cerr smtpclient.Error
765 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
766 t.Fatalf("attempt to deliver spamy message, got err %v, expected smtpclient.Error with code %d", err, smtp.C550MailboxUnavail)
768 l := checkEvaluationCount(t, 1)
769 tcompare(t, l[0].Optional, true)
772 // Update DNS for an SPF pass, and DMARC pass.
773 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
775 // Insert spammy messages not related to the test message.
777 MailFrom: "remote@test.example",
778 RcptToLocalpart: smtp.Localpart("mjl"),
779 RcptToDomain: "mox.example",
780 Flags: store.Flags{Seen: true, Junk: true},
781 Size: int64(len(deliverMessage)),
783 for i := 0; i < 3; i++ {
785 tinsertmsg(t, ts.acc, "Archive", &nm, deliverMessage)
789 // Baseline, message should be refused for spammy content.
790 ts.run(func(err error, client *smtpclient.Client) {
791 mailFrom := "remote@example.org"
792 rcptTo := "mjl@mox.example"
794 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
796 var cerr smtpclient.Error
797 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
798 t.Fatalf("attempt to deliver spamy message, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
800 checkEvaluationCount(t, 1) // No new evaluation.
803 // Insert a message that we sent to the address that is about to send to us.
804 sentMsg := store.Message{Size: int64(len(deliverMessage))}
805 tinsertmsg(t, ts.acc, "Sent", &sentMsg, deliverMessage)
806 err := ts.acc.DB.Insert(ctxbg, &store.Recipient{MessageID: sentMsg.ID, Localpart: "remote", Domain: "example.org", OrgDomain: "example.org", Sent: time.Now()})
807 tcheck(t, err, "inserting message recipient")
809 // Reject a message due to DMARC again. Since we sent a message to the domain, it
810 // is no longer unknown and we should see a non-optional evaluation that will
811 // result in a DMARC report.
812 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.1 -all"}
813 ts.run(func(err error, client *smtpclient.Client) {
814 mailFrom := "remote@example.org"
815 rcptTo := "mjl@mox.example"
817 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
819 var cerr smtpclient.Error
820 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
821 t.Fatalf("attempt to deliver spamy message, got err %v, expected smtpclient.Error with code %d", err, smtp.C550MailboxUnavail)
823 l := checkEvaluationCount(t, 2) // New evaluation.
824 tcompare(t, l[1].Optional, false)
827 // We should now be accepting the message because we recently sent a message.
828 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
829 ts.run(func(err error, client *smtpclient.Client) {
830 mailFrom := "remote@example.org"
831 rcptTo := "mjl@mox.example"
833 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
835 tcheck(t, err, "deliver")
836 l := checkEvaluationCount(t, 3) // New evaluation.
837 tcompare(t, l[2].Optional, false)
841// Test DNSBL, then getting through with subjectpass.
842func TestBlocklistedSubjectpass(t *testing.T) {
843 // Set up a DNSBL on dnsbl.example, and get DMARC pass.
844 resolver := &dns.MockResolver{
845 A: map[string][]string{
846 "example.org.": {"127.0.0.10"}, // For mx check.
847 "2.0.0.127.dnsbl.example.": {"127.0.0.2"}, // For healthcheck.
848 "10.0.0.127.dnsbl.example.": {"127.0.0.10"}, // Where our connection pretends to come from.
850 TXT: map[string][]string{
851 "10.0.0.127.dnsbl.example.": {"blocklisted"},
852 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
853 "_dmarc.example.org.": {"v=DMARC1;p=reject"},
855 PTR: map[string][]string{
856 "127.0.0.10": {"example.org."}, // For iprev check.
859 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
860 ts.dnsbls = []dns.Domain{{ASCII: "dnsbl.example"}}
863 // Message should be refused softly (temporary error) due to DNSBL.
864 ts.run(func(err error, client *smtpclient.Client) {
865 mailFrom := "remote@example.org"
866 rcptTo := "mjl@mox.example"
868 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
870 var cerr smtpclient.Error
871 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
872 t.Fatalf("attempted deliver from dnsblocklisted ip, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
876 // Set up subjectpass on account.
877 acc := mox.Conf.Dynamic.Accounts[ts.acc.Name]
878 acc.SubjectPass.Period = time.Hour
879 mox.Conf.Dynamic.Accounts[ts.acc.Name] = acc
881 // Message should be refused quickly (permanent error) due to DNSBL and Subjectkey.
883 ts.run(func(err error, client *smtpclient.Client) {
884 mailFrom := "remote@example.org"
885 rcptTo := "mjl@mox.example"
887 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
889 var cerr smtpclient.Error
890 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
891 t.Fatalf("attempted deliver from dnsblocklisted ip, got err %v, expected smtpclient.Error with code %d", err, smtp.C550MailboxUnavail)
893 i := strings.Index(cerr.Line, subjectpass.Explanation)
895 t.Fatalf("got error line %q, expected error line with subjectpass", cerr.Line)
897 pass = cerr.Line[i+len(subjectpass.Explanation):]
900 ts.run(func(err error, client *smtpclient.Client) {
901 mailFrom := "remote@example.org"
902 rcptTo := "mjl@mox.example"
903 passMessage := strings.Replace(deliverMessage, "Subject: test", "Subject: test "+pass, 1)
905 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(passMessage)), strings.NewReader(passMessage), false, false, false)
907 tcheck(t, err, "deliver with subjectpass")
911// Test accepting a DMARC report.
912func TestDMARCReport(t *testing.T) {
913 resolver := &dns.MockResolver{
914 A: map[string][]string{
915 "example.org.": {"127.0.0.10"}, // For mx check.
917 TXT: map[string][]string{
918 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
919 "_dmarc.example.org.": {"v=DMARC1;p=reject; rua=mailto:dmarcrpt@example.org"},
921 PTR: map[string][]string{
922 "127.0.0.10": {"example.org."}, // For iprev check.
925 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/dmarcreport/mox.conf"), resolver)
928 run := func(report string, n int) {
930 ts.run(func(err error, client *smtpclient.Client) {
933 tcheck(t, err, "run")
935 mailFrom := "remote@example.org"
936 rcptTo := "mjl@mox.example"
938 msgb := &bytes.Buffer{}
939 _, 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)
940 tcheck(t, xerr, "write msg headers")
941 w := quotedprintable.NewWriter(msgb)
942 _, xerr = w.Write([]byte(strings.ReplaceAll(report, "\n", "\r\n")))
943 tcheck(t, xerr, "write message")
947 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
949 tcheck(t, err, "deliver")
951 records, err := dmarcdb.Records(ctxbg)
952 tcheck(t, err, "dmarcdb records")
953 if len(records) != n {
954 t.Fatalf("got %d dmarcdb records, expected %d or more", len(records), n)
960 run(strings.ReplaceAll(dmarcReport, "xmox.nl", "mox.example"), 1)
962 // We always store as an evaluation, but as optional for reports.
963 evals := checkEvaluationCount(t, 2)
964 tcompare(t, evals[0].Optional, true)
965 tcompare(t, evals[1].Optional, true)
968const dmarcReport = `<?xml version="1.0" encoding="UTF-8" ?>
971 <org_name>example.org</org_name>
972 <email>postmaster@example.org</email>
973 <report_id>1</report_id>
975 <begin>1596412800</begin>
976 <end>1596499199</end>
980 <domain>xmox.nl</domain>
989 <source_ip>127.0.0.10</source_ip>
992 <disposition>none</disposition>
998 <header_from>xmox.nl</header_from>
1002 <domain>xmox.nl</domain>
1003 <result>pass</result>
1004 <selector>testsel</selector>
1007 <domain>xmox.nl</domain>
1008 <result>pass</result>
1015// Test accepting a TLS report.
1016func TestTLSReport(t *testing.T) {
1017 // Requires setting up DKIM.
1018 privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
1019 dkimRecord := dkim.Record{
1021 Hashes: []string{"sha256"},
1022 Flags: []string{"s"},
1023 PublicKey: privKey.Public(),
1026 dkimTxt, err := dkimRecord.Record()
1027 tcheck(t, err, "dkim record")
1029 sel := config.Selector{
1030 HashEffective: "sha256",
1031 HeadersEffective: []string{"From", "To", "Subject", "Date"},
1033 Domain: dns.Domain{ASCII: "testsel"},
1035 dkimConf := config.DKIM{
1036 Selectors: map[string]config.Selector{"testsel": sel},
1037 Sign: []string{"testsel"},
1040 resolver := &dns.MockResolver{
1041 A: map[string][]string{
1042 "example.org.": {"127.0.0.10"}, // For mx check.
1044 TXT: map[string][]string{
1045 "testsel._domainkey.example.org.": {dkimTxt},
1046 "_dmarc.example.org.": {"v=DMARC1;p=reject;rua=mailto:dmarcrpt@example.org"},
1048 PTR: map[string][]string{
1049 "127.0.0.10": {"example.org."}, // For iprev check.
1052 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/tlsrpt/mox.conf"), resolver)
1055 run := func(rcptTo, tlsrpt string, n int) {
1057 ts.run(func(err error, client *smtpclient.Client) {
1060 mailFrom := "remote@example.org"
1062 msgb := &bytes.Buffer{}
1063 _, 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)
1064 tcheck(t, xerr, "write msg")
1065 msg := msgb.String()
1067 selectors := mox.DKIMSelectors(dkimConf)
1068 headers, xerr := dkim.Sign(ctxbg, pkglog.Logger, "remote", dns.Domain{ASCII: "example.org"}, selectors, false, strings.NewReader(msg))
1069 tcheck(t, xerr, "dkim sign")
1073 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1075 tcheck(t, err, "deliver")
1077 records, err := tlsrptdb.Records(ctxbg)
1078 tcheck(t, err, "tlsrptdb records")
1079 if len(records) != n {
1080 t.Fatalf("got %d tlsrptdb records, expected %d", len(records), n)
1085 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}}]}`
1087 run("mjl@mox.example", tlsrpt, 0)
1088 run("mjl@mox.example", strings.ReplaceAll(tlsrpt, "xmox.nl", "mox.example"), 1)
1089 run("mjl@mailhost.mox.example", strings.ReplaceAll(tlsrpt, "xmox.nl", "mailhost.mox.example"), 2)
1091 // We always store as an evaluation, but as optional for reports.
1092 evals := checkEvaluationCount(t, 3)
1093 tcompare(t, evals[0].Optional, true)
1094 tcompare(t, evals[1].Optional, true)
1095 tcompare(t, evals[2].Optional, true)
1098func TestRatelimitConnectionrate(t *testing.T) {
1099 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1102 // We'll be creating 300 connections, no TLS and reduce noise.
1103 ts.tlsmode = smtpclient.TLSSkip
1104 mlog.SetConfig(map[string]slog.Level{"": mlog.LevelInfo})
1106 // We may be passing a window boundary during this tests. The limit is 300/minute.
1107 // So make twice that many connections and hope the tests don't take too long.
1108 for i := 0; i <= 2*300; i++ {
1109 ts.run(func(err error, client *smtpclient.Client) {
1111 if err != nil && i < 300 {
1112 t.Fatalf("expected smtp connection, got %v", err)
1114 if err == nil && i == 600 {
1115 t.Fatalf("expected no smtp connection due to connection rate limit, got connection")
1124func TestRatelimitAuth(t *testing.T) {
1125 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1128 ts.submission = true
1129 ts.tlsmode = smtpclient.TLSSkip
1133 // We may be passing a window boundary during this tests. The limit is 10 auth
1134 // failures/minute. So make twice that many connections and hope the tests don't
1136 for i := 0; i <= 2*10; i++ {
1137 ts.run(func(err error, client *smtpclient.Client) {
1140 t.Fatalf("got auth success with bad credentials")
1142 var cerr smtpclient.Error
1143 badauth := errors.As(err, &cerr) && cerr.Code == smtp.C535AuthBadCreds
1144 if !badauth && i < 10 {
1145 t.Fatalf("expected auth failure, got %v", err)
1147 if badauth && i == 20 {
1148 t.Fatalf("expected no smtp connection due to failed auth rate limit, got other error %v", err)
1157func TestRatelimitDelivery(t *testing.T) {
1158 resolver := dns.MockResolver{
1159 A: map[string][]string{
1160 "example.org.": {"127.0.0.10"}, // For mx check.
1162 PTR: map[string][]string{
1163 "127.0.0.10": {"example.org."},
1166 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1169 orig := limitIPMasked1MessagesPerMinute
1170 limitIPMasked1MessagesPerMinute = 1
1172 limitIPMasked1MessagesPerMinute = orig
1175 ts.run(func(err error, client *smtpclient.Client) {
1176 mailFrom := "remote@example.org"
1177 rcptTo := "mjl@mox.example"
1179 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1181 tcheck(t, err, "deliver to remote")
1183 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1184 var cerr smtpclient.Error
1185 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C452StorageFull {
1186 t.Fatalf("got err %v, expected smtpclient error with code 452 for storage full", err)
1190 limitIPMasked1MessagesPerMinute = orig
1192 origSize := limitIPMasked1SizePerMinute
1193 // Message was already delivered once. We'll do another one. But the 3rd will fail.
1194 // We need the actual size with prepended headers, since that is used in the
1196 msg, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).Get()
1198 t.Fatalf("getting delivered message for its size: %v", err)
1200 limitIPMasked1SizePerMinute = 2*msg.Size + int64(len(deliverMessage)/2)
1202 limitIPMasked1SizePerMinute = origSize
1204 ts.run(func(err error, client *smtpclient.Client) {
1205 mailFrom := "remote@example.org"
1206 rcptTo := "mjl@mox.example"
1208 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1210 tcheck(t, err, "deliver to remote")
1212 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1213 var cerr smtpclient.Error
1214 if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C452StorageFull {
1215 t.Fatalf("got err %v, expected smtpclient error with code 452 for storage full", err)
1220func TestNonSMTP(t *testing.T) {
1221 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1225 serverConn, clientConn := net.Pipe()
1226 defer serverConn.Close()
1227 serverdone := make(chan struct{})
1228 defer func() { <-serverdone }()
1231 tlsConfig := &tls.Config{
1232 Certificates: []tls.Certificate{fakeCert(ts.t)},
1234 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)
1238 defer clientConn.Close()
1240 buf := make([]byte, 128)
1242 // Read and ignore hello.
1243 if _, err := clientConn.Read(buf); err != nil {
1244 t.Fatalf("reading hello: %v", err)
1247 if _, err := fmt.Fprintf(clientConn, "bogus\r\n"); err != nil {
1248 t.Fatalf("write command: %v", err)
1250 n, err := clientConn.Read(buf)
1252 t.Fatalf("read response line: %v", err)
1254 s := string(buf[:n])
1255 if !strings.HasPrefix(s, "500 5.5.2 ") {
1256 t.Fatalf(`got %q, expected "500 5.5.2 ...`, s)
1258 if _, err := clientConn.Read(buf); err == nil {
1259 t.Fatalf("connection not closed after bogus command")
1263// Test limits on outgoing messages.
1264func TestLimitOutgoing(t *testing.T) {
1265 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpserversendlimit/mox.conf"), dns.MockResolver{})
1268 ts.user = "mjl@mox.example"
1270 ts.submission = true
1272 err := ts.acc.DB.Insert(ctxbg, &store.Outgoing{Recipient: "a@other.example", Submitted: time.Now().Add(-24*time.Hour - time.Minute)})
1273 tcheck(t, err, "inserting outgoing/recipient past 24h window")
1275 testSubmit := func(rcptTo string, expErr *smtpclient.Error) {
1277 ts.run(func(err error, client *smtpclient.Client) {
1279 mailFrom := "mjl@mox.example"
1281 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
1283 var cerr smtpclient.Error
1284 if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Secode != expErr.Secode) {
1285 t.Fatalf("got err %#v, expected %#v", err, expErr)
1290 // Limits are set to 4 messages a day, 2 first-time recipients.
1291 testSubmit("b@other.example", nil)
1292 testSubmit("c@other.example", nil)
1293 testSubmit("d@other.example", &smtpclient.Error{Code: smtp.C451LocalErr, Secode: smtp.SePol7DeliveryUnauth1}) // Would be 3rd recipient.
1294 testSubmit("b@other.example", nil)
1295 testSubmit("b@other.example", nil)
1296 testSubmit("b@other.example", &smtpclient.Error{Code: smtp.C451LocalErr, Secode: smtp.SePol7DeliveryUnauth1}) // Would be 5th message.
1299// Test account size limit enforcement.
1300func TestQuota(t *testing.T) {
1301 resolver := dns.MockResolver{
1302 A: map[string][]string{
1303 "other.example.": {"127.0.0.10"}, // For mx check.
1305 PTR: map[string][]string{
1306 "127.0.0.10": {"other.example."},
1309 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpserverquota/mox.conf"), resolver)
1312 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1314 ts.run(func(err error, client *smtpclient.Client) {
1316 mailFrom := "mjl@other.example"
1318 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1320 var cerr smtpclient.Error
1321 if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Secode != expErr.Secode) {
1322 t.Fatalf("got err %#v, expected %#v", err, expErr)
1327 testDeliver("mjl@mox.example", &smtpclient.Error{Code: smtp.C452StorageFull, Secode: smtp.SeMailbox2Full2})
1330// Test with catchall destination address.
1331func TestCatchall(t *testing.T) {
1332 resolver := dns.MockResolver{
1333 A: map[string][]string{
1334 "other.example.": {"127.0.0.10"}, // For mx check.
1336 PTR: map[string][]string{
1337 "127.0.0.10": {"other.example."},
1340 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpservercatchall/mox.conf"), resolver)
1343 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1345 ts.run(func(err error, client *smtpclient.Client) {
1347 mailFrom := "mjl@other.example"
1349 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
1351 var cerr smtpclient.Error
1352 if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Secode != expErr.Secode) {
1353 t.Fatalf("got err %#v, expected %#v", err, expErr)
1358 testDeliver("mjl@mox.example", nil) // Exact match.
1359 testDeliver("mjl+test@mox.example", nil) // Domain localpart catchall separator.
1360 testDeliver("MJL+TEST@mox.example", nil) // Again, and case insensitive.
1361 testDeliver("unknown@mox.example", nil) // Catchall address, to account catchall.
1363 n, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).Count()
1364 tcheck(t, err, "checking delivered messages")
1367 acc, err := store.OpenAccount(pkglog, "catchall")
1368 tcheck(t, err, "open account")
1370 n, err = bstore.QueryDB[store.Message](ctxbg, acc.DB).Count()
1371 tcheck(t, err, "checking delivered messages to catchall account")
1375// Test DKIM signing for outgoing messages.
1376func TestDKIMSign(t *testing.T) {
1377 resolver := dns.MockResolver{
1378 A: map[string][]string{
1379 "mox.example.": {"127.0.0.10"}, // For mx check.
1381 PTR: map[string][]string{
1382 "127.0.0.10": {"mox.example."},
1386 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1389 // Set DKIM signing config.
1391 genDKIM := func(domain string) string {
1392 dom, _ := mox.Conf.Domain(dns.Domain{ASCII: domain})
1394 privkey := make([]byte, ed25519.SeedSize) // Fake key, don't use for real.
1396 privkey[0] = byte(gen)
1398 sel := config.Selector{
1399 HashEffective: "sha256",
1400 HeadersEffective: []string{"From", "To", "Subject"},
1401 Key: ed25519.NewKeyFromSeed(privkey),
1402 Domain: dns.Domain{ASCII: "testsel"},
1404 dom.DKIM = config.DKIM{
1405 Selectors: map[string]config.Selector{"testsel": sel},
1406 Sign: []string{"testsel"},
1408 mox.Conf.Dynamic.Domains[domain] = dom
1409 pubkey := sel.Key.Public().(ed25519.PublicKey)
1410 return "v=DKIM1;k=ed25519;p=" + base64.StdEncoding.EncodeToString(pubkey)
1413 dkimtxt := genDKIM("mox.example")
1414 dkimtxt2 := genDKIM("mox2.example")
1416 // DKIM verify needs to find the key.
1417 resolver.TXT = map[string][]string{
1418 "testsel._domainkey.mox.example.": {dkimtxt},
1419 "testsel._domainkey.mox2.example.": {dkimtxt2},
1422 ts.submission = true
1423 ts.user = "mjl@mox.example"
1427 testSubmit := func(mailFrom, msgFrom string) {
1429 ts.run(func(err error, client *smtpclient.Client) {
1432 msg := strings.ReplaceAll(fmt.Sprintf(`From: <%s>
1433To: <remote@example.org>
1435Message-Id: <test@mox.example>
1438`, msgFrom), "\n", "\r\n")
1440 rcptTo := "remote@example.org"
1442 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1444 tcheck(t, err, "deliver")
1446 msgs, err := queue.List(ctxbg)
1447 tcheck(t, err, "listing queue")
1449 tcompare(t, len(msgs), n)
1450 sort.Slice(msgs, func(i, j int) bool {
1451 return msgs[i].ID > msgs[j].ID
1453 f, err := queue.OpenMessage(ctxbg, msgs[0].ID)
1454 tcheck(t, err, "open message in queue")
1456 results, err := dkim.Verify(ctxbg, pkglog.Logger, resolver, false, dkim.DefaultPolicy, f, false)
1457 tcheck(t, err, "verifying dkim message")
1458 tcompare(t, len(results), 1)
1459 tcompare(t, results[0].Status, dkim.StatusPass)
1460 tcompare(t, results[0].Sig.Domain.ASCII, strings.Split(msgFrom, "@")[1])
1464 testSubmit("mjl@mox.example", "mjl@mox.example")
1465 testSubmit("mjl@mox.example", "mjl@mox2.example") // DKIM signature will be for mox2.example.
1468// Test to postmaster addresses.
1469func TestPostmaster(t *testing.T) {
1470 resolver := dns.MockResolver{
1471 A: map[string][]string{
1472 "other.example.": {"127.0.0.10"}, // For mx check.
1474 PTR: map[string][]string{
1475 "127.0.0.10": {"other.example."},
1478 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/postmaster/mox.conf"), resolver)
1481 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1483 ts.run(func(err error, client *smtpclient.Client) {
1485 mailFrom := "mjl@other.example"
1487 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1489 var cerr smtpclient.Error
1490 if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Code != expErr.Code || cerr.Secode != expErr.Secode) {
1491 t.Fatalf("got err %#v, expected %#v", err, expErr)
1496 testDeliver("postmaster", nil) // Plain postmaster address without domain.
1497 testDeliver("postmaster@host.mox.example", nil) // Postmaster address with configured mail server hostname.
1498 testDeliver("postmaster@mox.example", nil) // Postmaster address without explicitly configured destination.
1499 testDeliver("postmaster@unknown.example", &smtpclient.Error{Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
1502// Test to address with empty localpart.
1503func TestEmptylocalpart(t *testing.T) {
1504 resolver := dns.MockResolver{
1505 A: map[string][]string{
1506 "other.example.": {"127.0.0.10"}, // For mx check.
1508 PTR: map[string][]string{
1509 "127.0.0.10": {"other.example."},
1512 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1515 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1517 ts.run(func(err error, client *smtpclient.Client) {
1520 mailFrom := `""@other.example`
1521 msg := strings.ReplaceAll(deliverMessage, "To: <mjl@mox.example>", `To: <""@mox.example>`)
1523 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1525 var cerr smtpclient.Error
1526 if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Code != expErr.Code || cerr.Secode != expErr.Secode) {
1527 t.Fatalf("got err %#v, expected %#v", err, expErr)
1532 testDeliver(`""@mox.example`, nil)
1535// Test handling REQUIRETLS and TLS-Required: No.
1536func TestRequireTLS(t *testing.T) {
1537 resolver := dns.MockResolver{
1538 A: map[string][]string{
1539 "mox.example.": {"127.0.0.10"}, // For mx check.
1541 PTR: map[string][]string{
1542 "127.0.0.10": {"mox.example."},
1546 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1549 ts.submission = true
1550 ts.requiretls = true
1551 ts.user = "mjl@mox.example"
1557 msg0 := strings.ReplaceAll(`From: <mjl@mox.example>
1558To: <remote@example.org>
1560Message-Id: <test@mox.example>
1566 msg1 := strings.ReplaceAll(`From: <mjl@mox.example>
1567To: <remote@example.org>
1569Message-Id: <test@mox.example>
1576 msg2 := strings.ReplaceAll(`From: <mjl@mox.example>
1577To: <remote@example.org>
1579Message-Id: <test@mox.example>
1584 testSubmit := func(msg string, requiretls bool, expRequireTLS *bool) {
1586 ts.run(func(err error, client *smtpclient.Client) {
1589 rcptTo := "remote@example.org"
1591 err = client.Deliver(ctxbg, "mjl@mox.example", rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, requiretls)
1593 tcheck(t, err, "deliver")
1595 msgs, err := queue.List(ctxbg)
1596 tcheck(t, err, "listing queue")
1597 tcompare(t, len(msgs), 1)
1598 tcompare(t, msgs[0].RequireTLS, expRequireTLS)
1599 _, err = queue.Drop(ctxbg, pkglog, msgs[0].ID, "", "")
1600 tcheck(t, err, "deleting message from queue")
1604 testSubmit(msg0, true, &yes) // Header ignored, requiretls applied.
1605 testSubmit(msg0, false, &no) // TLS-Required header applied.
1606 testSubmit(msg1, true, &yes) // Bad headers ignored, requiretls applied.
1607 testSubmit(msg1, false, nil) // Inconsistent multiple headers ignored.
1608 testSubmit(msg2, false, nil) // Regular message, no RequireTLS setting.
1609 testSubmit(msg2, true, &yes) // Requiretls applied.
1611 // Check that we get an error if remote SMTP server does not support the requiretls
1613 ts.requiretls = false
1614 ts.run(func(err error, client *smtpclient.Client) {
1617 rcptTo := "remote@example.org"
1619 err = client.Deliver(ctxbg, "mjl@mox.example", rcptTo, int64(len(msg0)), strings.NewReader(msg0), false, false, true)
1622 t.Fatalf("delivered with requiretls to server without requiretls")
1624 if !errors.Is(err, smtpclient.ErrRequireTLSUnsupported) {
1625 t.Fatalf("got err %v, expected ErrRequireTLSUnsupported", err)
1630func TestSmuggle(t *testing.T) {
1631 resolver := dns.MockResolver{
1632 A: map[string][]string{
1633 "example.org.": {"127.0.0.10"}, // For mx check.
1635 PTR: map[string][]string{
1636 "127.0.0.10": {"example.org."}, // For iprev check.
1639 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpsmuggle/mox.conf"), resolver)
1640 ts.tlsmode = smtpclient.TLSSkip
1643 test := func(data string) {
1646 ts.runRaw(func(conn net.Conn) {
1649 ourHostname := mox.Conf.Static.HostnameDomain
1650 remoteHostname := dns.Domain{ASCII: "mox.example"}
1651 opts := smtpclient.Opts{
1652 RootCAs: mox.Conf.Static.TLS.CertPool,
1654 log := pkglog.WithCid(ts.cid - 1)
1655 _, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, ts.tlspkix, ourHostname, remoteHostname, opts)
1656 tcheck(t, err, "smtpclient")
1659 write := func(s string) {
1660 _, err := conn.Write([]byte(s))
1661 tcheck(t, err, "write")
1664 readPrefixLine := func(prefix string) string {
1666 buf := make([]byte, 512)
1667 n, err := conn.Read(buf)
1668 tcheck(t, err, "read")
1669 s := strings.TrimRight(string(buf[:n]), "\r\n")
1670 if !strings.HasPrefix(s, prefix) {
1671 t.Fatalf("got smtp response %q, expected line with prefix %q", s, prefix)
1676 write("MAIL FROM:<remote@example.org>\r\n")
1678 write("RCPT TO:<mjl@mox.example>\r\n")
1683 write("\r\n") // Empty header.
1685 write("\r\n.\r\n") // End of message.
1686 line := readPrefixLine("5")
1687 if !strings.Contains(line, "smug") {
1688 t.Errorf("got 5xx error with message %q, expected error text containing smug", line)
1699func TestFutureRelease(t *testing.T) {
1700 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1701 ts.tlsmode = smtpclient.TLSSkip
1702 ts.user = "mjl@mox.example"
1704 ts.submission = true
1707 ts.auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
1708 return sasl.NewClientPlain(ts.user, ts.pass), nil
1711 test := func(mailtoMore, expResponsePrefix string) {
1714 ts.runRaw(func(conn net.Conn) {
1717 ourHostname := mox.Conf.Static.HostnameDomain
1718 remoteHostname := dns.Domain{ASCII: "mox.example"}
1719 opts := smtpclient.Opts{Auth: ts.auth}
1720 log := pkglog.WithCid(ts.cid - 1)
1721 _, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, false, ourHostname, remoteHostname, opts)
1722 tcheck(t, err, "smtpclient")
1725 write := func(s string) {
1726 _, err := conn.Write([]byte(s))
1727 tcheck(t, err, "write")
1730 readPrefixLine := func(prefix string) string {
1732 buf := make([]byte, 512)
1733 n, err := conn.Read(buf)
1734 tcheck(t, err, "read")
1735 s := strings.TrimRight(string(buf[:n]), "\r\n")
1736 if !strings.HasPrefix(s, prefix) {
1737 t.Fatalf("got smtp response %q, expected line with prefix %q", s, prefix)
1742 write(fmt.Sprintf("MAIL FROM:<mjl@mox.example>%s\r\n", mailtoMore))
1743 readPrefixLine(expResponsePrefix)
1744 if expResponsePrefix != "2" {
1747 write("RCPT TO:<mjl@mox.example>\r\n")
1752 write("From: <mjl@mox.example>\r\n\r\nbody\r\n\r\n.\r\n")
1757 test(" HOLDFOR=1", "2")
1758 test(" HOLDUNTIL="+time.Now().Add(time.Minute).UTC().Format(time.RFC3339), "2")
1759 test(" HOLDUNTIL="+time.Now().Add(time.Minute).UTC().Format(time.RFC3339Nano), "2")
1761 test(" HOLDFOR=0", "501") // 0 is invalid syntax.
1762 test(fmt.Sprintf(" HOLDFOR=%d", int64((queue.FutureReleaseIntervalMax+time.Minute)/time.Second)), "554") // Too far in the future.
1763 test(" HOLDUNTIL="+time.Now().Add(-time.Minute).UTC().Format(time.RFC3339), "554") // In the past.
1764 test(" HOLDUNTIL="+time.Now().Add(queue.FutureReleaseIntervalMax+time.Minute).UTC().Format(time.RFC3339), "554") // Too far in the future.
1765 test(" HOLDUNTIL=2024-02-10T17:28:00+00:00", "501") // "Z" required.
1766 test(" HOLDUNTIL=24-02-10T17:28:00Z", "501") // Invalid.
1767 test(" HOLDFOR=1 HOLDFOR=1", "501") // Duplicate.
1768 test(" HOLDFOR=1 HOLDUNTIL="+time.Now().Add(time.Hour).UTC().Format(time.RFC3339), "501") // Duplicate.