1package smtpserver
2
3// todo: test delivery with failing spf/dkim/dmarc
4// todo: test delivering a message to multiple recipients, and with some of them failing.
5
6import (
7 "bytes"
8 "context"
9 "crypto/ed25519"
10 cryptorand "crypto/rand"
11 "crypto/tls"
12 "crypto/x509"
13 "encoding/base64"
14 "errors"
15 "fmt"
16 "log/slog"
17 "math/big"
18 "mime/quotedprintable"
19 "net"
20 "os"
21 "path/filepath"
22 "regexp"
23 "sort"
24 "strings"
25 "testing"
26 "time"
27
28 "github.com/mjl-/bstore"
29
30 "github.com/mjl-/mox/config"
31 "github.com/mjl-/mox/dkim"
32 "github.com/mjl-/mox/dmarcdb"
33 "github.com/mjl-/mox/dns"
34 "github.com/mjl-/mox/mlog"
35 "github.com/mjl-/mox/mox-"
36 "github.com/mjl-/mox/queue"
37 "github.com/mjl-/mox/sasl"
38 "github.com/mjl-/mox/smtp"
39 "github.com/mjl-/mox/smtpclient"
40 "github.com/mjl-/mox/store"
41 "github.com/mjl-/mox/subjectpass"
42 "github.com/mjl-/mox/tlsrptdb"
43 "github.com/mjl-/mox/webops"
44)
45
46var ctxbg = context.Background()
47
48func init() {
49 // Don't make tests slow.
50 badClientDelay = 0
51 authFailDelay = 0
52 unknownRecipientsDelay = 0
53}
54
55func tcheck(t *testing.T, err error, msg string) {
56 if err != nil {
57 t.Helper()
58 t.Fatalf("%s: %s", msg, err)
59 }
60}
61
62var submitMessage = strings.ReplaceAll(`From: <mjl@mox.example>
63To: <remote@example.org>
64Subject: test
65Message-Id: <test@mox.example>
66
67test email
68`, "\n", "\r\n")
69
70var deliverMessage = strings.ReplaceAll(`From: <remote@example.org>
71To: <mjl@mox.example>
72Subject: test
73Message-Id: <test@example.org>
74
75test email
76`, "\n", "\r\n")
77
78var deliverMessage2 = strings.ReplaceAll(`From: <remote@example.org>
79To: <mjl@mox.example>
80Subject: test
81Message-Id: <test2@example.org>
82
83test email, unique.
84`, "\n", "\r\n")
85
86var deliverMessage3 = strings.ReplaceAll(`From: <remote@example.org>
87To: <mjl@mox.example>
88Subject: test
89Message-Id: <test3@example.org>
90
91test email, unique again.
92`, "\n", "\r\n")
93
94type testserver struct {
95 t *testing.T
96 acc *store.Account
97 switchStop func()
98 comm *store.Comm
99 cid int64
100 resolver dns.Resolver
101 auth func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error)
102 user, pass string
103 immediateTLS bool
104 serverConfig *tls.Config
105 clientConfig *tls.Config
106 clientCert *tls.Certificate // Passed to smtpclient for starttls authentication.
107 submission bool
108 requiretls bool
109 dnsbls []dns.Domain
110 tlsmode smtpclient.TLSMode
111 tlspkix bool
112 xops webops.XOps
113}
114
115const password0 = "te\u0301st \u00a0\u2002\u200a" // NFD and various unicode spaces.
116const password1 = "tést " // PRECIS normalized, with NFC.
117
118func newTestServer(t *testing.T, configPath string, resolver dns.Resolver) *testserver {
119 limitersInit() // Reset rate limiters.
120
121 log := mlog.New("smtpserver", nil)
122
123 checkf := func(ctx context.Context, err error, format string, args ...any) {
124 tcheck(t, err, fmt.Sprintf(format, args...))
125 }
126 xops := webops.XOps{
127 DBWrite: func(ctx context.Context, acc *store.Account, fn func(tx *bstore.Tx)) {
128 err := acc.DB.Write(ctx, func(tx *bstore.Tx) error {
129 fn(tx)
130 return nil
131 })
132 tcheck(t, err, "db write")
133 },
134 Checkf: checkf,
135 Checkuserf: checkf,
136 }
137
138 ts := testserver{
139 t: t,
140 cid: 1,
141 resolver: resolver,
142 tlsmode: smtpclient.TLSOpportunistic,
143 serverConfig: &tls.Config{
144 Certificates: []tls.Certificate{fakeCert(t, false)},
145 },
146 xops: xops,
147 }
148
149 // Ensure session keys, for tests that check resume and authentication.
150 ctx, cancel := context.WithCancel(ctxbg)
151 defer cancel()
152 mox.StartTLSSessionTicketKeyRefresher(ctx, log, ts.serverConfig)
153
154 mox.Context = ctxbg
155 mox.ConfigStaticPath = configPath
156 mox.MustLoadConfig(true, false)
157 dataDir := mox.ConfigDirPath(mox.Conf.Static.DataDir)
158 os.RemoveAll(dataDir)
159
160 err := dmarcdb.Init()
161 tcheck(t, err, "dmarcdb init")
162 err = tlsrptdb.Init()
163 tcheck(t, err, "tlsrptdb init")
164 err = store.Init(ctxbg)
165 tcheck(t, err, "store init")
166
167 ts.switchStop = store.Switchboard()
168 err = queue.Init()
169 tcheck(t, err, "queue init")
170
171 ts.acc, err = store.OpenAccount(log, "mjl", false)
172 tcheck(t, err, "open account")
173 err = ts.acc.SetPassword(log, password0)
174 tcheck(t, err, "set password")
175
176 ts.comm = store.RegisterComm(ts.acc)
177
178 return &ts
179}
180
181func (ts *testserver) close() {
182 if ts.acc == nil {
183 return
184 }
185 err := dmarcdb.Close()
186 tcheck(ts.t, err, "dmarcdb close")
187 err = tlsrptdb.Close()
188 tcheck(ts.t, err, "tlsrptdb close")
189 ts.comm.Unregister()
190 queue.Shutdown()
191 err = ts.acc.Close()
192 tcheck(ts.t, err, "closing account")
193 ts.acc.WaitClosed()
194 ts.acc = nil
195 ts.switchStop()
196 err = store.Close()
197 tcheck(ts.t, err, "store close")
198}
199
200func (ts *testserver) checkCount(mailboxName string, expect int) {
201 t := ts.t
202 t.Helper()
203 q := bstore.QueryDB[store.Mailbox](ctxbg, ts.acc.DB)
204 q.FilterNonzero(store.Mailbox{Name: mailboxName})
205 q.FilterEqual("Expunged", false)
206 mb, err := q.Get()
207 tcheck(t, err, "get mailbox")
208 qm := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB)
209 qm.FilterNonzero(store.Message{MailboxID: mb.ID})
210 qm.FilterEqual("Expunged", false)
211 n, err := qm.Count()
212 tcheck(t, err, "count messages in mailbox")
213 if n != expect {
214 t.Fatalf("messages in mailbox, found %d, expected %d", n, expect)
215 }
216}
217
218func (ts *testserver) run(fn func(client *smtpclient.Client)) {
219 ts.t.Helper()
220 ts.runx(func(helloErr error, client *smtpclient.Client) {
221 ts.t.Helper()
222 tcheck(ts.t, helloErr, "hello")
223 fn(client)
224 })
225}
226
227func (ts *testserver) runx(fn func(helloErr error, client *smtpclient.Client)) {
228 ts.t.Helper()
229 ts.runRaw(func(conn net.Conn) {
230 ts.t.Helper()
231
232 auth := ts.auth
233 if auth == nil && ts.user != "" {
234 auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
235 return sasl.NewClientPlain(ts.user, ts.pass), nil
236 }
237 }
238
239 ourHostname := mox.Conf.Static.HostnameDomain
240 remoteHostname := dns.Domain{ASCII: "mox.example"}
241 opts := smtpclient.Opts{
242 Auth: auth,
243 RootCAs: mox.Conf.Static.TLS.CertPool,
244 ClientCert: ts.clientCert,
245 }
246 log := pkglog.WithCid(ts.cid - 1)
247 client, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, ts.tlspkix, ourHostname, remoteHostname, opts)
248 if err != nil {
249 conn.Close()
250 } else {
251 defer client.Close()
252 }
253 fn(err, client)
254 })
255}
256
257func (ts *testserver) runRaw(fn func(clientConn net.Conn)) {
258 ts.t.Helper()
259
260 ts.cid += 2
261
262 serverConn, clientConn := net.Pipe()
263 defer serverConn.Close()
264 // clientConn is closed as part of closing client.
265 serverdone := make(chan struct{})
266 defer func() { <-serverdone }()
267
268 go func() {
269 serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, ts.serverConfig, serverConn, ts.resolver, ts.submission, ts.immediateTLS, false, false, 100<<20, false, false, ts.requiretls, ts.dnsbls, 0)
270 close(serverdone)
271 }()
272
273 if ts.immediateTLS {
274 clientConn = tls.Client(clientConn, ts.clientConfig)
275 }
276
277 fn(clientConn)
278}
279
280func (ts *testserver) smtpErr(err error, expErr *smtpclient.Error) *smtpclient.Error {
281 t := ts.t
282 t.Helper()
283 var cerr smtpclient.Error
284 if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Permanent != expErr.Permanent || cerr.Code != expErr.Code || cerr.Secode != expErr.Secode) {
285 t.Fatalf("got err:\n%#v (%q)\nexpected:\n%#v", err, err, expErr)
286 }
287 return &cerr
288}
289
290// Just a cert that appears valid. SMTP client will not verify anything about it
291// (that is opportunistic TLS for you, "better some than none"). Let's enjoy this
292// one moment where it makes life easier.
293func fakeCert(t *testing.T, randomkey bool) tls.Certificate {
294 seed := make([]byte, ed25519.SeedSize)
295 if randomkey {
296 cryptorand.Read(seed)
297 }
298 privKey := ed25519.NewKeyFromSeed(seed) // Fake key, don't use this for real!
299 template := &x509.Certificate{
300 SerialNumber: big.NewInt(1), // Required field...
301 // Valid period is needed to get session resumption enabled.
302 NotBefore: time.Now().Add(-time.Minute),
303 NotAfter: time.Now().Add(time.Hour),
304 }
305 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
306 if err != nil {
307 t.Fatalf("making certificate: %s", err)
308 }
309 cert, err := x509.ParseCertificate(localCertBuf)
310 if err != nil {
311 t.Fatalf("parsing generated certificate: %s", err)
312 }
313 c := tls.Certificate{
314 Certificate: [][]byte{localCertBuf},
315 PrivateKey: privKey,
316 Leaf: cert,
317 }
318 return c
319}
320
321// check expected dmarc evaluations for outgoing aggregate reports.
322func checkEvaluationCount(t *testing.T, n int) []dmarcdb.Evaluation {
323 t.Helper()
324 l, err := dmarcdb.Evaluations(ctxbg)
325 tcheck(t, err, "get dmarc evaluations")
326 tcompare(t, len(l), n)
327 return l
328}
329
330// Test submission from authenticated user.
331func TestSubmission(t *testing.T) {
332 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
333 defer ts.close()
334
335 // Set DKIM signing config.
336 dom, _ := mox.Conf.Domain(dns.Domain{ASCII: "mox.example"})
337 sel := config.Selector{
338 HashEffective: "sha256",
339 HeadersEffective: []string{"From", "To", "Subject"},
340 Key: ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)), // Fake key, don't use for real.
341 Domain: dns.Domain{ASCII: "mox.example"},
342 }
343 dom.DKIM = config.DKIM{
344 Selectors: map[string]config.Selector{"testsel": sel},
345 Sign: []string{"testsel"},
346 }
347 mox.Conf.Dynamic.Domains["mox.example"] = dom
348
349 testAuth := func(authfn func(user, pass string, cs *tls.ConnectionState) sasl.Client, user, pass string, expErr *smtpclient.Error) {
350 t.Helper()
351 if authfn != nil {
352 ts.auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
353 return authfn(user, pass, cs), nil
354 }
355 } else {
356 ts.auth = nil
357 }
358 ts.runx(func(err error, client *smtpclient.Client) {
359 mailFrom := "mjl@mox.example"
360 rcptTo := "remote@example.org"
361 if err == nil {
362 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
363 }
364 var cerr smtpclient.Error
365 if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Code != expErr.Code || cerr.Secode != expErr.Secode) {
366 t.Fatalf("got err:\n%#v (%q)\nexpected:\n%#v", err, err, expErr)
367 }
368 checkEvaluationCount(t, 0)
369 })
370 }
371
372 acc, err := store.OpenAccount(pkglog, "disabled", false)
373 tcheck(t, err, "open account")
374 err = acc.SetPassword(pkglog, "test1234")
375 tcheck(t, err, "set password")
376 err = acc.Close()
377 tcheck(t, err, "close account")
378
379 ts.submission = true
380 testAuth(nil, "", "", &smtpclient.Error{Permanent: true, Code: smtp.C530SecurityRequired, Secode: smtp.SePol7Other0})
381 authfns := []func(user, pass string, cs *tls.ConnectionState) sasl.Client{
382 func(user, pass string, cs *tls.ConnectionState) sasl.Client { return sasl.NewClientPlain(user, pass) },
383 func(user, pass string, cs *tls.ConnectionState) sasl.Client { return sasl.NewClientLogin(user, pass) },
384 func(user, pass string, cs *tls.ConnectionState) sasl.Client { return sasl.NewClientCRAMMD5(user, pass) },
385 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
386 return sasl.NewClientSCRAMSHA1(user, pass, false)
387 },
388 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
389 return sasl.NewClientSCRAMSHA256(user, pass, false)
390 },
391 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
392 return sasl.NewClientSCRAMSHA1PLUS(user, pass, *cs)
393 },
394 func(user, pass string, cs *tls.ConnectionState) sasl.Client {
395 return sasl.NewClientSCRAMSHA256PLUS(user, pass, *cs)
396 },
397 }
398 for _, fn := range authfns {
399 testAuth(fn, "mjl@mox.example", "test", &smtpclient.Error{Code: smtp.C535AuthBadCreds, Secode: smtp.SePol7AuthBadCreds8}) // Bad (short) password.
400 testAuth(fn, "mjl@mox.example", password0+"test", &smtpclient.Error{Code: smtp.C535AuthBadCreds, Secode: smtp.SePol7AuthBadCreds8}) // Bad password.
401 testAuth(fn, "mjl@mox.example", password0, nil)
402 testAuth(fn, "mjl@mox.example", password1, nil)
403 testAuth(fn, "móx@mox.example", password0, nil)
404 testAuth(fn, "móx@mox.example", password1, nil)
405 testAuth(fn, "mo\u0301x@mox.example", password0, nil)
406 testAuth(fn, "mo\u0301x@mox.example", password1, nil)
407 testAuth(fn, "disabled@mox.example", "test1234", &smtpclient.Error{Code: smtp.C525AccountDisabled, Secode: smtp.SePol7AccountDisabled13})
408 testAuth(fn, "disabled@mox.example", "bogus", &smtpclient.Error{Code: smtp.C535AuthBadCreds, Secode: smtp.SePol7AuthBadCreds8})
409 }
410
411 // Create a certificate, register its public key with account, and make a tls
412 // client config that sends the certificate.
413 clientCert0 := fakeCert(ts.t, true)
414 tlspubkey, err := store.ParseTLSPublicKeyCert(clientCert0.Certificate[0])
415 tcheck(t, err, "parse certificate")
416 tlspubkey.Account = "mjl"
417 tlspubkey.LoginAddress = "mjl@mox.example"
418 err = store.TLSPublicKeyAdd(ctxbg, &tlspubkey)
419 tcheck(t, err, "add tls public key to account")
420 ts.immediateTLS = true
421 ts.clientConfig = &tls.Config{
422 InsecureSkipVerify: true,
423 Certificates: []tls.Certificate{
424 clientCert0,
425 },
426 }
427
428 // No explicit address in EXTERNAL.
429 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
430 return sasl.NewClientExternal(user)
431 }, "", "", nil)
432
433 // Same username in EXTERNAL as configured for key.
434 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
435 return sasl.NewClientExternal(user)
436 }, "mjl@mox.example", "", nil)
437
438 // Different username in EXTERNAL as configured for key, but same account.
439 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
440 return sasl.NewClientExternal(user)
441 }, "móx@mox.example", "", nil)
442
443 // Different username as configured for key, but same account, but not EXTERNAL auth.
444 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
445 return sasl.NewClientSCRAMSHA256PLUS(user, pass, *cs)
446 }, "móx@mox.example", password0, nil)
447
448 // Different account results in error.
449 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
450 return sasl.NewClientExternal(user)
451 }, "☺@mox.example", "", &smtpclient.Error{Code: smtp.C535AuthBadCreds, Secode: smtp.SePol7AuthBadCreds8})
452
453 // Starttls with client cert should authenticate too.
454 ts.immediateTLS = false
455 ts.clientCert = &clientCert0
456 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
457 return sasl.NewClientExternal(user)
458 }, "", "", nil)
459 ts.immediateTLS = true
460 ts.clientCert = nil
461
462 // Add a client session cache, so our connections will be resumed. We are testing
463 // that the credentials are applied to resumed connections too.
464 ts.clientConfig.ClientSessionCache = tls.NewLRUClientSessionCache(10)
465 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
466 if cs.DidResume {
467 panic("tls connection was resumed")
468 }
469 return sasl.NewClientExternal(user)
470 }, "", "", nil)
471 testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
472 if !cs.DidResume {
473 panic("tls connection was not resumed")
474 }
475 return sasl.NewClientExternal(user)
476 }, "", "", nil)
477
478 // Unknown client certificate should fail the connection.
479 serverConn, clientConn := net.Pipe()
480 serverdone := make(chan struct{})
481 defer func() { <-serverdone }()
482
483 go func() {
484 defer serverConn.Close()
485 tlsConfig := &tls.Config{
486 Certificates: []tls.Certificate{fakeCert(ts.t, false)},
487 }
488 serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, tlsConfig, serverConn, ts.resolver, ts.submission, ts.immediateTLS, false, false, 100<<20, false, false, false, ts.dnsbls, 0)
489 close(serverdone)
490 }()
491
492 defer clientConn.Close()
493
494 // Authentication with an unknown/untrusted certificate should fail.
495 clientCert1 := fakeCert(ts.t, true)
496 ts.clientConfig.ClientSessionCache = nil
497 ts.clientConfig.Certificates = []tls.Certificate{
498 clientCert1,
499 }
500 clientConn = tls.Client(clientConn, ts.clientConfig)
501 // note: It's not enough to do a handshake and check if that was successful. If the
502 // client cert is not acceptable, we only learn after the handshake, when the first
503 // data messages are exchanged.
504 buf := make([]byte, 100)
505 _, err = clientConn.Read(buf)
506 if err == nil {
507 t.Fatalf("tls handshake with unknown client certificate succeeded")
508 }
509 if alert, ok := mox.AsTLSAlert(err); !ok || alert != 42 {
510 t.Fatalf("got err %#v, expected tls 'bad certificate' alert", err)
511 }
512}
513
514func TestDomainDisabled(t *testing.T) {
515 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
516 defer ts.close()
517
518 ts.submission = true
519 ts.user = "mjl@mox.example"
520 ts.pass = password0
521
522 // Submission with SMTP MAIL FROM of disabled domain must fail.
523 ts.run(func(client *smtpclient.Client) {
524 mailFrom := "mjl@disabled.example" // Disabled.
525 rcptTo := "remote@example.org"
526 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
527 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
528 checkEvaluationCount(t, 0)
529 })
530
531 // Message From-address has disabled domain, must fail.
532 var submitMessage2 = strings.ReplaceAll(`From: <mjl@disabled.example>
533To: <remote@example.org>
534Subject: test
535Message-Id: <test@mox.example>
536
537test email
538`, "\n", "\r\n")
539 ts.run(func(client *smtpclient.Client) {
540 mailFrom := "mjl@mox.example"
541 rcptTo := "remote@example.org"
542 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage2)), strings.NewReader(submitMessage2), false, false, false)
543 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
544 checkEvaluationCount(t, 0)
545 })
546}
547
548// Test delivery from external MTA.
549func TestDelivery(t *testing.T) {
550 resolver := dns.MockResolver{
551 A: map[string][]string{
552 "example.org.": {"127.0.0.10"}, // For mx check.
553 },
554 PTR: map[string][]string{},
555 }
556 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
557 defer ts.close()
558
559 ts.run(func(client *smtpclient.Client) {
560 mailFrom := "remote@example.org"
561 rcptTo := "mjl@[127.0.0.10]"
562 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
563 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
564 })
565
566 ts.run(func(client *smtpclient.Client) {
567 mailFrom := "remote@example.org"
568 rcptTo := "mjl@[IPv6:::1]"
569 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
570 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
571 })
572
573 ts.run(func(client *smtpclient.Client) {
574 mailFrom := "remote@example.org"
575 rcptTo := "mjl@test.example" // Not configured as destination.
576 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
577 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
578 })
579
580 ts.run(func(client *smtpclient.Client) {
581 mailFrom := "remote@example.org"
582 rcptTo := "unknown@mox.example" // User unknown.
583 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
584 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
585 })
586
587 ts.run(func(client *smtpclient.Client) {
588 mailFrom := "remote@example.org"
589 rcptTo := "mjl@mox.example"
590 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
591 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
592 })
593
594 // Set up iprev to get delivery from unknown user to be accepted.
595 resolver.PTR["127.0.0.10"] = []string{"example.org."}
596
597 // Only ascii o@ is configured, not the greek and cyrillic lookalikes.
598 ts.run(func(client *smtpclient.Client) {
599 mailFrom := "remote@example.org"
600 rcptTo := "ο@mox.example" // omicron \u03bf, looks like the configured o@
601 msg := strings.ReplaceAll(deliverMessage, "mjl@mox.example", rcptTo)
602 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, true, false)
603 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
604 })
605
606 // Deliveries to disabled domain are rejected with temporary error.
607 ts.run(func(client *smtpclient.Client) {
608 mailFrom := "remote@example.org"
609 rcptTo := "mjl@disabled.example"
610 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
611 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C450MailboxUnavail, Secode: smtp.SeMailbox2Disabled1})
612 })
613
614 ts.run(func(client *smtpclient.Client) {
615 recipients := []string{
616 "mjl@mox.example",
617 "o@mox.example", // ascii o, as configured
618 "\u2126@mox.example", // ohm sign, as configured
619 "ω@mox.example", // lower-case omega, we match case-insensitively and this is the lowercase of ohm (!)
620 "\u03a9@mox.example", // capital omega, also lowercased to omega.
621 "móx@mox.example", // NFC
622 "mo\u0301x@mox.example", // not NFC, but normalized as móx@, see https://go.dev/blog/normalization
623 }
624
625 for _, rcptTo := range recipients {
626 // Ensure SMTP RCPT TO and message address headers are the same, otherwise the junk
627 // filter treats us more strictly.
628 msg := strings.ReplaceAll(deliverMessage, "mjl@mox.example", rcptTo)
629
630 mailFrom := "remote@example.org"
631 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, true, false)
632 tcheck(t, err, "deliver to remote")
633
634 changes := make(chan []store.Change)
635 go func() {
636 _, l := ts.comm.Get()
637 changes <- l
638 }()
639
640 timer := time.NewTimer(time.Second)
641 defer timer.Stop()
642 select {
643 case <-changes:
644 case <-timer.C:
645 t.Fatalf("no delivery in 1s")
646 }
647 }
648 })
649
650 checkEvaluationCount(t, 0)
651}
652
653func TestIntrobox(t *testing.T) {
654 resolver := dns.MockResolver{
655 A: map[string][]string{
656 "example.org.": {"127.0.0.10"}, // For mx check.
657 },
658 TXT: map[string][]string{
659 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
660 "_dmarc.example.org.": {"v=DMARC1;p=reject"},
661 },
662 PTR: map[string][]string{
663 "127.0.0.10": {"example.org."},
664 },
665 }
666 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
667 defer ts.close()
668
669 acc := mox.Conf.Dynamic.Accounts[ts.acc.Name]
670 dest := acc.Destinations["mjl@mox.example"]
671 dest.Mailbox = "Inbox"
672 acc.Destinations["mjl@mox.example"] = dest
673 accDest := mox.Conf.AccountDestinationsLocked["mjl@mox.example"]
674 accDest.Destination = dest
675 mox.Conf.AccountDestinationsLocked["mjl@mox.example"] = accDest
676 acc.Introbox = "Introbox"
677 acc.AutomaticJunkFlags.Enabled = true
678 acc.AutomaticJunkFlags.JunkMailboxRegexp = "^(junk|spam)"
679 acc.AutomaticJunkFlags.NeutralMailboxRegexp = "^(inbox|introbox|neutral|postmaster|dmarc|tlsrpt|rejects)"
680 acc.JunkMailbox = regexp.MustCompile(acc.AutomaticJunkFlags.JunkMailboxRegexp)
681 acc.NeutralMailbox = regexp.MustCompile(acc.AutomaticJunkFlags.NeutralMailboxRegexp)
682 mox.Conf.Dynamic.Accounts[ts.acc.Name] = acc
683
684 deliver := func(msg string) {
685 ts.run(func(client *smtpclient.Client) {
686 mailFrom := "remote@example.org"
687 rcptTo := "mjl@mox.example"
688 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
689 tcheck(t, err, "deliver")
690 })
691 }
692
693 deliver(deliverMessage)
694 ts.checkCount("Inbox", 0)
695 ts.checkCount("Introbox", 1)
696
697 deliver(deliverMessage2)
698 ts.checkCount("Inbox", 0)
699 ts.checkCount("Introbox", 2)
700
701 introbox, err := bstore.QueryDB[store.Mailbox](ctxbg, ts.acc.DB).FilterNonzero(store.Mailbox{Name: "Introbox"}).Get()
702 tcheck(t, err, "get introbox")
703 var ids []int64
704 err = bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).FilterNonzero(store.Message{MailboxID: introbox.ID}).ForEach(func(m store.Message) error {
705 if m.Junk || m.Notjunk {
706 t.Fatalf("introbox message has junk flags: junk %v, notjunk %v", m.Junk, m.Notjunk)
707 }
708 ids = append(ids, m.ID)
709 return nil
710 })
711 tcheck(t, err, "get introbox message ids")
712 ts.xops.MessageMove(ctxbg, pkglog, ts.acc, ids, "Inbox", 0, false)
713 ts.checkCount("Inbox", 2)
714 ts.checkCount("Introbox", 0)
715
716 deliver(deliverMessage3)
717 ts.checkCount("Inbox", 3)
718 ts.checkCount("Introbox", 0)
719}
720
721func tinsertmsg(t *testing.T, acc *store.Account, mailbox string, m *store.Message, msg string) {
722 mf, err := store.CreateMessageTemp(pkglog, "insertmsg")
723 tcheck(t, err, "temp message")
724 defer os.Remove(mf.Name())
725 defer mf.Close()
726 _, err = mf.Write([]byte(msg))
727 tcheck(t, err, "write message")
728
729 acc.WithWLock(func() {
730 err = acc.DeliverMailbox(pkglog, mailbox, "", m, mf)
731 tcheck(t, err, "deliver message")
732 })
733}
734
735func tretrain(t *testing.T, acc *store.Account) {
736 t.Helper()
737
738 // Fresh empty junkfilter.
739 basePath := mox.DataDirPath("accounts")
740 dbPath := filepath.Join(basePath, acc.Name, "junkfilter.db")
741 bloomPath := filepath.Join(basePath, acc.Name, "junkfilter.bloom")
742 os.Remove(dbPath)
743 os.Remove(bloomPath)
744 jf, _, err := acc.OpenJunkFilter(ctxbg, pkglog)
745 tcheck(t, err, "open junk filter")
746 defer jf.Close()
747
748 // Fetch messags to retrain on.
749 q := bstore.QueryDB[store.Message](ctxbg, acc.DB)
750 q.FilterEqual("Expunged", false)
751 q.FilterFn(func(m store.Message) bool {
752 return m.Flags.Junk != m.Flags.Notjunk
753 })
754 msgs, err := q.List()
755 tcheck(t, err, "fetch messages")
756
757 // Retrain the messages.
758 for _, m := range msgs {
759 ham := m.Flags.Notjunk
760
761 f, err := os.Open(acc.MessagePath(m.ID))
762 tcheck(t, err, "open message")
763 r := store.FileMsgReader(m.MsgPrefix, f)
764
765 jf.TrainMessage(ctxbg, r, m.Size, ham)
766
767 err = r.Close()
768 tcheck(t, err, "close message")
769 }
770
771 err = jf.Save()
772 tcheck(t, err, "save junkfilter")
773}
774
775// Test accept/reject with DMARC reputation and with spammy content.
776func TestSpam(t *testing.T) {
777 resolver := &dns.MockResolver{
778 A: map[string][]string{
779 "example.org.": {"127.0.0.1"}, // For mx check.
780 },
781 TXT: map[string][]string{
782 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
783 "_dmarc.example.org.": {"v=DMARC1;p=reject; rua=mailto:dmarcrpt@example.org"},
784 },
785 }
786 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
787 defer ts.close()
788
789 // Insert spammy messages. No junkfilter training yet.
790 m := store.Message{
791 RemoteIP: "127.0.0.10",
792 RemoteIPMasked1: "127.0.0.10",
793 RemoteIPMasked2: "127.0.0.0",
794 RemoteIPMasked3: "127.0.0.0",
795 MailFrom: "remote@example.org",
796 MailFromLocalpart: smtp.Localpart("remote"),
797 MailFromDomain: "example.org",
798 RcptToLocalpart: smtp.Localpart("mjl"),
799 RcptToDomain: "mox.example",
800 MsgFromLocalpart: smtp.Localpart("remote"),
801 MsgFromDomain: "example.org",
802 MsgFromOrgDomain: "example.org",
803 MsgFromValidated: true,
804 MsgFromValidation: store.ValidationStrict,
805 Flags: store.Flags{Seen: true, Junk: true},
806 Size: int64(len(deliverMessage)),
807 }
808 for range 3 {
809 nm := m
810 tinsertmsg(t, ts.acc, "Inbox", &nm, deliverMessage)
811 nm = m
812 tinsertmsg(t, ts.acc, "mjl2", &nm, deliverMessage)
813 }
814
815 // Delivery from sender with bad reputation should fail.
816 ts.run(func(client *smtpclient.Client) {
817 mailFrom := "remote@example.org"
818 rcptTo := "mjl@mox.example"
819 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
820 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
821
822 ts.checkCount("Rejects", 1)
823 checkEvaluationCount(t, 0) // No positive interactions yet.
824 })
825
826 // Delivery from sender with bad reputation matching AcceptRejectsToMailbox should
827 // result in accepted delivery to the mailbox.
828 ts.run(func(client *smtpclient.Client) {
829 mailFrom := "remote@example.org"
830 rcptTo := "mjl2@mox.example"
831 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage2)), strings.NewReader(deliverMessage2), false, false, false)
832 tcheck(t, err, "deliver")
833
834 ts.checkCount("mjl2junk", 1) // In ruleset rejects mailbox.
835 ts.checkCount("Rejects", 1) // Same as before.
836 checkEvaluationCount(t, 0) // This is not an actual accept.
837 })
838
839 // Mark the messages as having good reputation.
840 var ids []int64
841 err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).FilterEqual("Expunged", false).ForEach(func(m store.Message) error {
842 ids = append(ids, m.ID)
843 return nil
844 })
845 tcheck(t, err, "get message ids")
846 ts.xops.MessageFlagsClear(ctxbg, pkglog, ts.acc, ids, []string{"$Junk"})
847 ts.xops.MessageFlagsAdd(ctxbg, pkglog, ts.acc, ids, []string{"$NotJunk"})
848
849 // Message should now be accepted.
850 ts.run(func(client *smtpclient.Client) {
851 tcheck(t, err, "hello")
852 mailFrom := "remote@example.org"
853 rcptTo := "mjl@mox.example"
854 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
855 tcheck(t, err, "deliver")
856
857 // Message should now be removed from Rejects mailboxes.
858 ts.checkCount("Rejects", 0)
859 ts.checkCount("mjl2junk", 1)
860 checkEvaluationCount(t, 1)
861 })
862
863 // Undo dmarc pass, mark messages as junk, and train the filter.
864 resolver.TXT = nil
865 q := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB)
866 q.FilterEqual("Expunged", false)
867 _, err = q.UpdateFields(map[string]any{"Junk": true, "Notjunk": false})
868 tcheck(t, err, "update junkiness")
869 tretrain(t, ts.acc)
870
871 // Message should be refused for spammy content.
872 ts.run(func(client *smtpclient.Client) {
873 tcheck(t, err, "hello")
874 mailFrom := "remote@example.org"
875 rcptTo := "mjl@mox.example"
876 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
877 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
878 checkEvaluationCount(t, 1) // No new evaluation, this isn't a DMARC reject.
879 })
880}
881
882// Test accept/reject with forwarded messages, DMARC ignored, no IP/EHLO/MAIL
883// FROM-based reputation.
884func TestForward(t *testing.T) {
885 // Do a run without forwarding, and with.
886 check := func(forward bool) {
887
888 resolver := &dns.MockResolver{
889 A: map[string][]string{
890 "bad.example.": {"127.0.0.1"}, // For mx check.
891 "good.example.": {"127.0.0.1"}, // For mx check.
892 "forward.example.": {"127.0.0.10"}, // For mx check.
893 },
894 TXT: map[string][]string{
895 "bad.example.": {"v=spf1 ip4:127.0.0.1 -all"},
896 "good.example.": {"v=spf1 ip4:127.0.0.1 -all"},
897 "forward.example.": {"v=spf1 ip4:127.0.0.10 -all"},
898 "_dmarc.bad.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@bad.example"},
899 "_dmarc.good.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@good.example"},
900 "_dmarc.forward.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@forward.example"},
901 },
902 PTR: map[string][]string{
903 "127.0.0.10": {"forward.example."}, // For iprev check.
904 },
905 }
906 rcptTo := "mjl3@mox.example"
907 if !forward {
908 // For SPF and DMARC pass, otherwise the test ends quickly.
909 resolver.TXT["bad.example."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
910 resolver.TXT["good.example."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
911 rcptTo = "mjl@mox.example" // Without IsForward rule.
912 }
913
914 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
915 defer ts.close()
916
917 totalEvaluations := 0
918
919 var msgBad = strings.ReplaceAll(`From: <remote@bad.example>
920To: <mjl@mox.example>
921Subject: test
922Message-Id: <bad@example.org>
923
924test email
925`, "\n", "\r\n")
926 var msgOK = strings.ReplaceAll(`From: <remote@good.example>
927To: <mjl@mox.example>
928Subject: other
929Message-Id: <good@example.org>
930
931unrelated message.
932`, "\n", "\r\n")
933 var msgOK2 = strings.ReplaceAll(`From: <other@forward.example>
934To: <mjl@mox.example>
935Subject: non-forward
936Message-Id: <regular@example.org>
937
938happens to come from forwarding mail server.
939`, "\n", "\r\n")
940
941 // Deliver forwarded messages, then classify as junk. Normally enough to treat
942 // other unrelated messages from IP as junk, but not for forwarded messages.
943 ts.run(func(client *smtpclient.Client) {
944 mailFrom := "remote@forward.example"
945 if !forward {
946 mailFrom = "remote@bad.example"
947 }
948
949 for range 10 {
950 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgBad)), strings.NewReader(msgBad), false, false, false)
951 tcheck(t, err, "deliver message")
952 }
953 totalEvaluations += 10
954
955 n, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).UpdateFields(map[string]any{"Junk": true, "MsgFromValidated": true})
956 tcheck(t, err, "marking messages as junk")
957 tcompare(t, n, 10)
958 tretrain(t, ts.acc)
959
960 // Next delivery will fail, with negative "message From" signal.
961 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgBad)), strings.NewReader(msgBad), false, false, false)
962 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
963
964 checkEvaluationCount(t, totalEvaluations)
965 })
966
967 // Delivery from different "message From" without reputation, but from same
968 // forwarding email server, should succeed under forwarding, not as regular sending
969 // server.
970 ts.run(func(client *smtpclient.Client) {
971 mailFrom := "remote@forward.example"
972 if !forward {
973 mailFrom = "remote@good.example"
974 }
975
976 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgOK)), strings.NewReader(msgOK), false, false, false)
977 if forward {
978 tcheck(t, err, "deliver")
979 totalEvaluations += 1
980 } else {
981 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
982 }
983 checkEvaluationCount(t, totalEvaluations)
984 })
985
986 // Delivery from forwarding server that isn't a forward should get same treatment.
987 ts.run(func(client *smtpclient.Client) {
988 mailFrom := "other@forward.example"
989
990 // Ensure To header matches.
991 msg := msgOK2
992 if forward {
993 msg = strings.ReplaceAll(msg, "<mjl@mox.example>", "<mjl3@mox.example>")
994 }
995
996 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
997 if forward {
998 tcheck(t, err, "deliver")
999 totalEvaluations += 1
1000 } else {
1001 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
1002 }
1003 checkEvaluationCount(t, totalEvaluations)
1004 })
1005 }
1006
1007 check(true)
1008 check(false)
1009}
1010
1011// Messages that we sent to, that have passing DMARC, but that are otherwise spammy, should be accepted.
1012func TestDMARCSent(t *testing.T) {
1013 resolver := &dns.MockResolver{
1014 A: map[string][]string{
1015 "example.org.": {"127.0.0.1"}, // For mx check.
1016 },
1017 TXT: map[string][]string{
1018 "example.org.": {"v=spf1 ip4:127.0.0.1 -all"},
1019 "_dmarc.example.org.": {"v=DMARC1;p=reject;rua=mailto:dmarcrpt@example.org"},
1020 },
1021 }
1022 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
1023 defer ts.close()
1024
1025 // First check that DMARC policy rejects message and results in optional evaluation.
1026 ts.run(func(client *smtpclient.Client) {
1027 mailFrom := "remote@example.org"
1028 rcptTo := "mjl@mox.example"
1029 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1030 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7MultiAuthFails26})
1031 l := checkEvaluationCount(t, 1)
1032 tcompare(t, l[0].Optional, true)
1033 })
1034
1035 // Update DNS for an SPF pass, and DMARC pass.
1036 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
1037
1038 // Insert hammy & spammy messages not related to the test message.
1039 m := store.Message{
1040 MailFrom: "remote@test.example",
1041 RcptToLocalpart: smtp.Localpart("mjl"),
1042 RcptToDomain: "mox.example",
1043 Flags: store.Flags{Seen: true},
1044 Size: int64(len(deliverMessage)),
1045 }
1046 // We need at least 50 ham messages for the junk filter to become significant. We
1047 // offset it with negative messages for mediocre score.
1048 for range 50 {
1049 nm := m
1050 nm.Junk = true
1051 tinsertmsg(t, ts.acc, "Archive", &nm, deliverMessage)
1052
1053 nm = m
1054 nm.Notjunk = true
1055 tinsertmsg(t, ts.acc, "Archive", &nm, deliverMessage)
1056 }
1057 tretrain(t, ts.acc)
1058
1059 // Baseline, message should be refused for spammy content.
1060 ts.run(func(client *smtpclient.Client) {
1061 mailFrom := "remote@example.org"
1062 rcptTo := "mjl@mox.example"
1063 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1064 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
1065 checkEvaluationCount(t, 1) // No new evaluation.
1066 })
1067
1068 // Insert a message that we sent to the address that is about to send to us.
1069 sentMsg := store.Message{Size: int64(len(deliverMessage))}
1070 tinsertmsg(t, ts.acc, "Sent", &sentMsg, deliverMessage)
1071 err := ts.acc.DB.Insert(ctxbg, &store.Recipient{MessageID: sentMsg.ID, Localpart: "remote", Domain: "example.org", OrgDomain: "example.org", Sent: time.Now()})
1072 tcheck(t, err, "inserting message recipient")
1073
1074 // Reject a message due to DMARC again. Since we sent a message to the domain, it
1075 // is no longer unknown and we should see a non-optional evaluation that will
1076 // result in a DMARC report.
1077 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.1 -all"}
1078 ts.run(func(client *smtpclient.Client) {
1079 mailFrom := "remote@example.org"
1080 rcptTo := "mjl@mox.example"
1081 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1082 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7MultiAuthFails26})
1083 l := checkEvaluationCount(t, 2) // New evaluation.
1084 tcompare(t, l[1].Optional, false)
1085 })
1086
1087 // We should now be accepting the message because we recently sent a message.
1088 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
1089 ts.run(func(client *smtpclient.Client) {
1090 mailFrom := "remote@example.org"
1091 rcptTo := "mjl@mox.example"
1092 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1093 tcheck(t, err, "deliver")
1094 l := checkEvaluationCount(t, 3) // New evaluation.
1095 tcompare(t, l[2].Optional, false)
1096 })
1097}
1098
1099// Test DNSBL, then getting through with subjectpass.
1100func TestBlocklistedSubjectpass(t *testing.T) {
1101 // Set up a DNSBL on dnsbl.example, and get DMARC pass.
1102 resolver := &dns.MockResolver{
1103 A: map[string][]string{
1104 "example.org.": {"127.0.0.10"}, // For mx check.
1105 "2.0.0.127.dnsbl.example.": {"127.0.0.2"}, // For healthcheck.
1106 "10.0.0.127.dnsbl.example.": {"127.0.0.10"}, // Where our connection pretends to come from.
1107 },
1108 TXT: map[string][]string{
1109 "10.0.0.127.dnsbl.example.": {"blocklisted"},
1110 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
1111 "_dmarc.example.org.": {"v=DMARC1;p=reject"},
1112 },
1113 PTR: map[string][]string{
1114 "127.0.0.10": {"example.org."}, // For iprev check.
1115 },
1116 }
1117 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1118 ts.dnsbls = []dns.Domain{{ASCII: "dnsbl.example"}}
1119 defer ts.close()
1120
1121 // Message should be refused softly (temporary error) due to DNSBL.
1122 ts.run(func(client *smtpclient.Client) {
1123 mailFrom := "remote@example.org"
1124 rcptTo := "mjl@mox.example"
1125 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1126 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
1127 })
1128
1129 // Set up subjectpass on account.
1130 acc := mox.Conf.Dynamic.Accounts[ts.acc.Name]
1131 acc.SubjectPass.Period = time.Hour
1132 mox.Conf.Dynamic.Accounts[ts.acc.Name] = acc
1133
1134 // Message should be refused quickly (permanent error) due to DNSBL and Subjectkey.
1135 var pass string
1136 ts.run(func(client *smtpclient.Client) {
1137 mailFrom := "remote@example.org"
1138 rcptTo := "mjl@mox.example"
1139 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1140 cerr := ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7DeliveryUnauth1})
1141 i := strings.Index(cerr.Line, subjectpass.Explanation)
1142 if i < 0 {
1143 t.Fatalf("got error line %q, expected error line with subjectpass", cerr.Line)
1144 }
1145 pass = cerr.Line[i+len(subjectpass.Explanation):]
1146 })
1147
1148 ts.run(func(client *smtpclient.Client) {
1149 mailFrom := "remote@example.org"
1150 rcptTo := "mjl@mox.example"
1151 passMessage := strings.Replace(deliverMessage, "Subject: test", "Subject: test "+pass, 1)
1152 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(passMessage)), strings.NewReader(passMessage), false, false, false)
1153 tcheck(t, err, "deliver with subjectpass")
1154 })
1155}
1156
1157// Test accepting a DMARC report.
1158func TestDMARCReport(t *testing.T) {
1159 resolver := &dns.MockResolver{
1160 A: map[string][]string{
1161 "example.org.": {"127.0.0.10"}, // For mx check.
1162 },
1163 TXT: map[string][]string{
1164 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
1165 "_dmarc.example.org.": {"v=DMARC1;p=reject; rua=mailto:dmarcrpt@example.org"},
1166 },
1167 PTR: map[string][]string{
1168 "127.0.0.10": {"example.org."}, // For iprev check.
1169 },
1170 }
1171 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/dmarcreport/mox.conf"), resolver)
1172 defer ts.close()
1173
1174 run := func(rcptTo, report string, n int) {
1175 t.Helper()
1176 ts.run(func(client *smtpclient.Client) {
1177 t.Helper()
1178
1179 mailFrom := "remote@example.org"
1180
1181 msgb := &bytes.Buffer{}
1182 _, xerr := fmt.Fprintf(msgb, "From: %s\r\nTo: %s\r\nSubject: dmarc report\r\nMIME-Version: 1.0\r\nContent-Type: text/xml\r\n\r\n", mailFrom, rcptTo)
1183 tcheck(t, xerr, "write msg headers")
1184 w := quotedprintable.NewWriter(msgb)
1185 _, xerr = w.Write([]byte(strings.ReplaceAll(report, "\n", "\r\n")))
1186 tcheck(t, xerr, "write message")
1187 msg := msgb.String()
1188
1189 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1190 tcheck(t, err, "deliver")
1191
1192 records, err := dmarcdb.Records(ctxbg)
1193 tcheck(t, err, "dmarcdb records")
1194 if len(records) != n {
1195 t.Fatalf("got %d dmarcdb records, expected %d or more", len(records), n)
1196 }
1197 })
1198 }
1199
1200 n := 0
1201 run("dmarc-reports@mox.example", dmarcReport, 0) // Wrong domain in report.
1202
1203 report := strings.ReplaceAll(dmarcReport, "xmox.nl", "mox.example")
1204 n++
1205 run("dmarc-reports@mox.example", report, n)
1206
1207 // We always store as an evaluation, but as optional for reports.
1208 evals := checkEvaluationCount(t, 2)
1209 tcompare(t, evals[0].Optional, true)
1210 tcompare(t, evals[1].Optional, true)
1211
1212 // Not a dmarc recipient, delivery should succeed.
1213 run("mjl@mox.example", report, n)
1214 run("mjl-test@mox.example", report, n)
1215 run("mjl+test@mox.example", report, n)
1216 // Likewise, address that is prefix of reporting address.
1217 run("dmarc@mox.example", report, n)
1218 run("Dmarc-test@mox.example", report, n)
1219 run("dmarc+test@mox.example", report, n)
1220
1221 // Localpart catchall separators work for dmarc reporting addresses too.
1222 n++
1223 run("Dmarc-reports-test@mox.example", report, n)
1224
1225 n++
1226 run("dmarc-Reports+test@mox.example", report, n)
1227}
1228
1229const dmarcReport = `<?xml version="1.0" encoding="UTF-8" ?>
1230<feedback>
1231 <report_metadata>
1232 <org_name>example.org</org_name>
1233 <email>postmaster@example.org</email>
1234 <report_id>1</report_id>
1235 <date_range>
1236 <begin>1596412800</begin>
1237 <end>1596499199</end>
1238 </date_range>
1239 </report_metadata>
1240 <policy_published>
1241 <domain>xmox.nl</domain>
1242 <adkim>r</adkim>
1243 <aspf>r</aspf>
1244 <p>reject</p>
1245 <sp>reject</sp>
1246 <pct>100</pct>
1247 </policy_published>
1248 <record>
1249 <row>
1250 <source_ip>127.0.0.10</source_ip>
1251 <count>1</count>
1252 <policy_evaluated>
1253 <disposition>none</disposition>
1254 <dkim>pass</dkim>
1255 <spf>pass</spf>
1256 </policy_evaluated>
1257 </row>
1258 <identifiers>
1259 <header_from>xmox.nl</header_from>
1260 </identifiers>
1261 <auth_results>
1262 <dkim>
1263 <domain>xmox.nl</domain>
1264 <result>pass</result>
1265 <selector>testsel</selector>
1266 </dkim>
1267 <spf>
1268 <domain>xmox.nl</domain>
1269 <result>pass</result>
1270 </spf>
1271 </auth_results>
1272 </record>
1273</feedback>
1274`
1275
1276// Test accepting a TLS report.
1277func TestTLSReport(t *testing.T) {
1278 // Requires setting up DKIM.
1279 privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
1280 dkimRecord := dkim.Record{
1281 Version: "DKIM1",
1282 Hashes: []string{"sha256"},
1283 Flags: []string{"s"},
1284 PublicKey: privKey.Public(),
1285 Key: "ed25519",
1286 }
1287 dkimTxt, err := dkimRecord.Record()
1288 tcheck(t, err, "dkim record")
1289
1290 sel := config.Selector{
1291 HashEffective: "sha256",
1292 HeadersEffective: []string{"From", "To", "Subject", "Date"},
1293 Key: privKey,
1294 Domain: dns.Domain{ASCII: "testsel"},
1295 }
1296 dkimConf := config.DKIM{
1297 Selectors: map[string]config.Selector{"testsel": sel},
1298 Sign: []string{"testsel"},
1299 }
1300
1301 resolver := &dns.MockResolver{
1302 A: map[string][]string{
1303 "example.org.": {"127.0.0.10"}, // For mx check.
1304 },
1305 TXT: map[string][]string{
1306 "testsel._domainkey.example.org.": {dkimTxt},
1307 "_dmarc.example.org.": {"v=DMARC1;p=reject;rua=mailto:dmarcrpt@example.org"},
1308 },
1309 PTR: map[string][]string{
1310 "127.0.0.10": {"example.org."}, // For iprev check.
1311 },
1312 }
1313 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/tlsrpt/mox.conf"), resolver)
1314 defer ts.close()
1315
1316 run := func(rcptTo, tlsrpt string, n int) {
1317 t.Helper()
1318 ts.run(func(client *smtpclient.Client) {
1319 t.Helper()
1320
1321 mailFrom := "remote@example.org"
1322
1323 msgb := &bytes.Buffer{}
1324 _, xerr := fmt.Fprintf(msgb, "From: %s\r\nTo: %s\r\nSubject: tlsrpt report\r\nMIME-Version: 1.0\r\nContent-Type: application/tlsrpt+json\r\n\r\n%s\r\n", mailFrom, rcptTo, tlsrpt)
1325 tcheck(t, xerr, "write msg")
1326 msg := msgb.String()
1327
1328 selectors := mox.DKIMSelectors(dkimConf)
1329 headers, xerr := dkim.Sign(ctxbg, pkglog.Logger, "remote", dns.Domain{ASCII: "example.org"}, selectors, false, strings.NewReader(msg))
1330 tcheck(t, xerr, "dkim sign")
1331 msg = headers + msg
1332
1333 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1334 tcheck(t, err, "deliver")
1335
1336 records, err := tlsrptdb.Records(ctxbg)
1337 tcheck(t, err, "tlsrptdb records")
1338 if len(records) != n {
1339 t.Fatalf("got %d tlsrptdb records, expected %d", len(records), n)
1340 }
1341 })
1342 }
1343
1344 tlsrpt := `{"organization-name":"Example.org","date-range":{"start-datetime":"2022-01-07T00:00:00Z","end-datetime":"2022-01-07T23:59:59Z"},"contact-info":"tlsrpt@example.org","report-id":"1","policies":[{"policy":{"policy-type":"no-policy-found","policy-domain":"xmox.nl"},"summary":{"total-successful-session-count":1,"total-failure-session-count":0}}]}`
1345
1346 n := 0
1347 run("tls-reports@mox.example", tlsrpt, n) // Wrong domain in report.
1348
1349 tlsrptdom := strings.ReplaceAll(tlsrpt, "xmox.nl", "mox.example")
1350 n++
1351 run("tls-reports@mox.example", tlsrptdom, n)
1352
1353 tlsrpthost := strings.ReplaceAll(tlsrpt, "xmox.nl", "mailhost.mox.example")
1354 n++
1355 run("tls-reports@mailhost.mox.example", tlsrpthost, n)
1356
1357 // We always store as an evaluation, but as optional for reports.
1358 evals := checkEvaluationCount(t, 3)
1359 tcompare(t, evals[0].Optional, true)
1360 tcompare(t, evals[1].Optional, true)
1361 tcompare(t, evals[2].Optional, true)
1362
1363 // Catchall separators work for reporting address too.
1364 n++
1365 run("Tls-reports+more@mox.example", tlsrptdom, n)
1366 n++
1367 run("tls-Reports-more@mox.example", tlsrptdom, n)
1368
1369 // Non-reporting addresses, mail accepted, but not as report.
1370 run("mjl@mox.example", tlsrptdom, n)
1371 run("Mjl-other@mox.example", tlsrptdom, n)
1372 run("mjl+other@mox.example", tlsrptdom, n)
1373 // Likewise, address that is prefix of reporting address.
1374 run("tls@mox.example", tlsrptdom, n)
1375 run("Tls-other@mox.example", tlsrptdom, n)
1376 run("tls+other@mox.example", tlsrptdom, n)
1377}
1378
1379func TestRatelimitConnectionrate(t *testing.T) {
1380 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1381 defer ts.close()
1382
1383 // We'll be creating 300 connections, no TLS and reduce noise.
1384 ts.tlsmode = smtpclient.TLSSkip
1385 mlog.SetConfig(map[string]slog.Level{"": mlog.LevelInfo})
1386 defer mlog.SetConfig(map[string]slog.Level{"": mlog.LevelDebug})
1387
1388 // We may be passing a window boundary during this tests. The limit is 300/minute.
1389 // So make twice that many connections and hope the tests don't take too long.
1390 for i := 0; i <= 2*300; i++ {
1391 ts.runx(func(err error, client *smtpclient.Client) {
1392 t.Helper()
1393 if err != nil && i < 300 {
1394 t.Fatalf("expected smtp connection, got %v", err)
1395 }
1396 if err == nil && i == 600 {
1397 t.Fatalf("expected no smtp connection due to connection rate limit, got connection")
1398 }
1399 if client != nil {
1400 client.Close()
1401 }
1402 })
1403 }
1404}
1405
1406func TestRatelimitAuth(t *testing.T) {
1407 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1408 defer ts.close()
1409
1410 ts.submission = true
1411 ts.tlsmode = smtpclient.TLSSkip
1412 ts.user = "bad"
1413 ts.pass = "bad"
1414
1415 // We may be passing a window boundary during this tests. The limit is 10 auth
1416 // failures/minute. So make twice that many connections and hope the tests don't
1417 // take too long.
1418 for i := 0; i <= 2*10; i++ {
1419 ts.runx(func(err error, client *smtpclient.Client) {
1420 t.Helper()
1421 if err == nil {
1422 t.Fatalf("got auth success with bad credentials")
1423 }
1424 var cerr smtpclient.Error
1425 badauth := errors.As(err, &cerr) && cerr.Code == smtp.C535AuthBadCreds
1426 if !badauth && i < 10 {
1427 t.Fatalf("expected auth failure, got %v", err)
1428 }
1429 if badauth && i == 20 {
1430 t.Fatalf("expected no smtp connection due to failed auth rate limit, got other error %v", err)
1431 }
1432 if client != nil {
1433 client.Close()
1434 }
1435 })
1436 }
1437}
1438
1439func TestRatelimitDelivery(t *testing.T) {
1440 resolver := dns.MockResolver{
1441 A: map[string][]string{
1442 "example.org.": {"127.0.0.10"}, // For mx check.
1443 },
1444 PTR: map[string][]string{
1445 "127.0.0.10": {"example.org."},
1446 },
1447 }
1448 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1449 defer ts.close()
1450
1451 orig := limitIPMasked1MessagesPerMinute
1452 limitIPMasked1MessagesPerMinute = 1
1453 defer func() {
1454 limitIPMasked1MessagesPerMinute = orig
1455 }()
1456
1457 ts.run(func(client *smtpclient.Client) {
1458 mailFrom := "remote@example.org"
1459 rcptTo := "mjl@mox.example"
1460 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1461 tcheck(t, err, "deliver to remote")
1462
1463 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1464 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C452StorageFull, Secode: smtp.SeMailbox2Full2})
1465 })
1466
1467 limitIPMasked1MessagesPerMinute = orig
1468
1469 origSize := limitIPMasked1SizePerMinute
1470 // Message was already delivered once. We'll do another one. But the 3rd will fail.
1471 // We need the actual size with prepended headers, since that is used in the
1472 // calculations.
1473 msg, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).Get()
1474 if err != nil {
1475 t.Fatalf("getting delivered message for its size: %v", err)
1476 }
1477 limitIPMasked1SizePerMinute = 2*msg.Size + int64(len(deliverMessage)/2)
1478 defer func() {
1479 limitIPMasked1SizePerMinute = origSize
1480 }()
1481 ts.run(func(client *smtpclient.Client) {
1482 mailFrom := "remote@example.org"
1483 rcptTo := "mjl@mox.example"
1484 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1485 tcheck(t, err, "deliver to remote")
1486
1487 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1488 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C452StorageFull, Secode: smtp.SeMailbox2Full2})
1489 })
1490}
1491
1492func TestNonSMTP(t *testing.T) {
1493 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1494 defer ts.close()
1495 ts.cid += 2
1496
1497 serverConn, clientConn := net.Pipe()
1498 defer serverConn.Close()
1499 serverdone := make(chan struct{})
1500 defer func() { <-serverdone }()
1501
1502 go func() {
1503 tlsConfig := &tls.Config{
1504 Certificates: []tls.Certificate{fakeCert(ts.t, false)},
1505 }
1506 serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, tlsConfig, serverConn, ts.resolver, ts.submission, false, false, false, 100<<20, false, false, false, ts.dnsbls, 0)
1507 close(serverdone)
1508 }()
1509
1510 defer clientConn.Close()
1511
1512 buf := make([]byte, 128)
1513
1514 // Read and ignore hello.
1515 if _, err := clientConn.Read(buf); err != nil {
1516 t.Fatalf("reading hello: %v", err)
1517 }
1518
1519 if _, err := fmt.Fprintf(clientConn, "bogus\r\n"); err != nil {
1520 t.Fatalf("write command: %v", err)
1521 }
1522 n, err := clientConn.Read(buf)
1523 if err != nil {
1524 t.Fatalf("read response line: %v", err)
1525 }
1526 s := string(buf[:n])
1527 if !strings.HasPrefix(s, "500 5.5.2 ") {
1528 t.Fatalf(`got %q, expected "500 5.5.2 ...`, s)
1529 }
1530 if _, err := clientConn.Read(buf); err == nil {
1531 t.Fatalf("connection not closed after bogus command")
1532 }
1533}
1534
1535// Test limits on outgoing messages.
1536func TestLimitOutgoing(t *testing.T) {
1537 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpserversendlimit/mox.conf"), dns.MockResolver{})
1538 defer ts.close()
1539
1540 ts.user = "mjl@mox.example"
1541 ts.pass = password0
1542 ts.submission = true
1543
1544 err := ts.acc.DB.Insert(ctxbg, &store.Outgoing{Recipient: "a@other.example", Submitted: time.Now().Add(-24*time.Hour - time.Minute)})
1545 tcheck(t, err, "inserting outgoing/recipient past 24h window")
1546
1547 testSubmit := func(rcptTo string, expErr *smtpclient.Error) {
1548 t.Helper()
1549 ts.run(func(client *smtpclient.Client) {
1550 t.Helper()
1551 mailFrom := "mjl@mox.example"
1552 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
1553 ts.smtpErr(err, expErr)
1554 })
1555 }
1556
1557 // Limits are set to 4 messages a day, 2 first-time recipients.
1558 testSubmit("b@other.example", nil)
1559 testSubmit("c@other.example", nil)
1560 testSubmit("d@other.example", &smtpclient.Error{Code: smtp.C451LocalErr, Secode: smtp.SePol7DeliveryUnauth1}) // Would be 3rd recipient.
1561 testSubmit("b@other.example", nil)
1562 testSubmit("b@other.example", nil)
1563 testSubmit("b@other.example", &smtpclient.Error{Code: smtp.C451LocalErr, Secode: smtp.SePol7DeliveryUnauth1}) // Would be 5th message.
1564}
1565
1566// Test account size limit enforcement.
1567func TestQuota(t *testing.T) {
1568 resolver := dns.MockResolver{
1569 A: map[string][]string{
1570 "other.example.": {"127.0.0.10"}, // For mx check.
1571 },
1572 PTR: map[string][]string{
1573 "127.0.0.10": {"other.example."},
1574 },
1575 }
1576 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpserverquota/mox.conf"), resolver)
1577 defer ts.close()
1578
1579 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1580 ts.run(func(client *smtpclient.Client) {
1581 mailFrom := "mjl@other.example"
1582 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1583 ts.smtpErr(err, expErr)
1584 })
1585 }
1586
1587 testDeliver("mjl@mox.example", &smtpclient.Error{Code: smtp.C452StorageFull, Secode: smtp.SeMailbox2Full2})
1588}
1589
1590// Test with catchall destination address.
1591func TestCatchall(t *testing.T) {
1592 resolver := dns.MockResolver{
1593 A: map[string][]string{
1594 "other.example.": {"127.0.0.10"}, // For mx check.
1595 },
1596 PTR: map[string][]string{
1597 "127.0.0.10": {"other.example."},
1598 },
1599 }
1600 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpservercatchall/mox.conf"), resolver)
1601 defer ts.close()
1602
1603 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1604 t.Helper()
1605 ts.run(func(client *smtpclient.Client) {
1606 t.Helper()
1607 mailFrom := "mjl@other.example"
1608 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
1609 ts.smtpErr(err, expErr)
1610 })
1611 }
1612
1613 testDeliver("mjl@mox.example", nil) // Exact match.
1614 testDeliver("mjl+test@mox.example", nil) // Domain localpart catchall separator.
1615 testDeliver("MJL+TEST@mox.example", nil) // Again, and case insensitive.
1616
1617 n, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).Count()
1618 tcheck(t, err, "checking delivered messages")
1619 tcompare(t, n, 3)
1620
1621 testDeliver("unknown@mox.example", nil) // Catchall address, to account catchall.
1622
1623 acc, err := store.OpenAccount(pkglog, "catchall", false)
1624 tcheck(t, err, "open account")
1625 defer func() {
1626 acc.Close()
1627 acc.WaitClosed()
1628 }()
1629 n, err = bstore.QueryDB[store.Message](ctxbg, acc.DB).Count()
1630 tcheck(t, err, "checking delivered messages to catchall account")
1631 tcompare(t, n, 1)
1632
1633 testDeliver("mjl-test@mox2.example", nil) // Second catchall separator.
1634 testDeliver("mjl-test+test@mox2.example", nil) // Silly, both separators in address.
1635 testDeliver("mjl+test-test@mox2.example", nil)
1636 n, err = bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).Count()
1637 tcheck(t, err, "checking delivered messages")
1638 tcompare(t, n, 6)
1639}
1640
1641// Test DKIM signing for outgoing messages.
1642func TestDKIMSign(t *testing.T) {
1643 resolver := dns.MockResolver{
1644 A: map[string][]string{
1645 "mox.example.": {"127.0.0.10"}, // For mx check.
1646 },
1647 PTR: map[string][]string{
1648 "127.0.0.10": {"mox.example."},
1649 },
1650 }
1651
1652 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1653 defer ts.close()
1654
1655 // Set DKIM signing config.
1656 var gen byte
1657 genDKIM := func(domain string) string {
1658 dom, _ := mox.Conf.Domain(dns.Domain{ASCII: domain})
1659
1660 privkey := make([]byte, ed25519.SeedSize) // Fake key, don't use for real.
1661 gen++
1662 privkey[0] = byte(gen)
1663
1664 sel := config.Selector{
1665 HashEffective: "sha256",
1666 HeadersEffective: []string{"From", "To", "Subject"},
1667 Key: ed25519.NewKeyFromSeed(privkey),
1668 Domain: dns.Domain{ASCII: "testsel"},
1669 }
1670 dom.DKIM = config.DKIM{
1671 Selectors: map[string]config.Selector{"testsel": sel},
1672 Sign: []string{"testsel"},
1673 }
1674 mox.Conf.Dynamic.Domains[domain] = dom
1675 pubkey := sel.Key.Public().(ed25519.PublicKey)
1676 return "v=DKIM1;k=ed25519;p=" + base64.StdEncoding.EncodeToString(pubkey)
1677 }
1678
1679 dkimtxt := genDKIM("mox.example")
1680 dkimtxt2 := genDKIM("mox2.example")
1681
1682 // DKIM verify needs to find the key.
1683 resolver.TXT = map[string][]string{
1684 "testsel._domainkey.mox.example.": {dkimtxt},
1685 "testsel._domainkey.mox2.example.": {dkimtxt2},
1686 }
1687
1688 ts.submission = true
1689 ts.user = "mjl@mox.example"
1690 ts.pass = password0
1691
1692 n := 0
1693 testSubmit := func(mailFrom, msgFrom string) {
1694 t.Helper()
1695 ts.run(func(client *smtpclient.Client) {
1696 t.Helper()
1697
1698 msg := strings.ReplaceAll(fmt.Sprintf(`From: <%s>
1699To: <remote@example.org>
1700Subject: test
1701Message-Id: <test@mox.example>
1702
1703test email
1704`, msgFrom), "\n", "\r\n")
1705
1706 rcptTo := "remote@example.org"
1707 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1708 tcheck(t, err, "deliver")
1709
1710 msgs, err := queue.List(ctxbg, queue.Filter{}, queue.Sort{})
1711 tcheck(t, err, "listing queue")
1712 n++
1713 tcompare(t, len(msgs), n)
1714 sort.Slice(msgs, func(i, j int) bool {
1715 return msgs[i].ID > msgs[j].ID
1716 })
1717 f, err := queue.OpenMessage(ctxbg, msgs[0].ID)
1718 tcheck(t, err, "open message in queue")
1719 defer f.Close()
1720 results, err := dkim.Verify(ctxbg, pkglog.Logger, resolver, false, dkim.DefaultPolicy, f, false)
1721 tcheck(t, err, "verifying dkim message")
1722 tcompare(t, len(results), 1)
1723 tcompare(t, results[0].Status, dkim.StatusPass)
1724 tcompare(t, results[0].Sig.Domain.ASCII, strings.Split(msgFrom, "@")[1])
1725 })
1726 }
1727
1728 testSubmit("mjl@mox.example", "mjl@mox.example")
1729 testSubmit("mjl@mox.example", "mjl@mox2.example") // DKIM signature will be for mox2.example.
1730}
1731
1732// Test to postmaster addresses.
1733func TestPostmaster(t *testing.T) {
1734 resolver := dns.MockResolver{
1735 A: map[string][]string{
1736 "other.example.": {"127.0.0.10"}, // For mx check.
1737 },
1738 PTR: map[string][]string{
1739 "127.0.0.10": {"other.example."},
1740 },
1741 }
1742 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/postmaster/mox.conf"), resolver)
1743 defer ts.close()
1744
1745 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1746 t.Helper()
1747 ts.run(func(client *smtpclient.Client) {
1748 t.Helper()
1749 mailFrom := "mjl@other.example"
1750 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1751 ts.smtpErr(err, expErr)
1752 })
1753 }
1754
1755 testDeliver("postmaster", nil) // Plain postmaster address without domain.
1756 testDeliver("postmaster@host.mox.example", nil) // Postmaster address with configured mail server hostname.
1757 testDeliver("postmaster@mox.example", nil) // Postmaster address without explicitly configured destination.
1758 testDeliver("postmaster@unknown.example", &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
1759}
1760
1761// Test to address with empty localpart.
1762func TestEmptylocalpart(t *testing.T) {
1763 resolver := dns.MockResolver{
1764 A: map[string][]string{
1765 "other.example.": {"127.0.0.10"}, // For mx check.
1766 },
1767 PTR: map[string][]string{
1768 "127.0.0.10": {"other.example."},
1769 },
1770 }
1771 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1772 defer ts.close()
1773
1774 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1775 ts.run(func(client *smtpclient.Client) {
1776 mailFrom := `""@other.example`
1777 msg := strings.ReplaceAll(deliverMessage, "To: <mjl@mox.example>", `To: <""@mox.example>`)
1778 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1779 ts.smtpErr(err, expErr)
1780 })
1781 }
1782
1783 testDeliver(`""@mox.example`, nil)
1784}
1785
1786// Test handling REQUIRETLS and TLS-Required: No.
1787func TestRequireTLS(t *testing.T) {
1788 resolver := dns.MockResolver{
1789 A: map[string][]string{
1790 "mox.example.": {"127.0.0.10"}, // For mx check.
1791 },
1792 PTR: map[string][]string{
1793 "127.0.0.10": {"mox.example."},
1794 },
1795 }
1796
1797 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1798 defer ts.close()
1799
1800 ts.submission = true
1801 ts.requiretls = true
1802 ts.user = "mjl@mox.example"
1803 ts.pass = password0
1804
1805 no := false
1806 yes := true
1807
1808 msg0 := strings.ReplaceAll(`From: <mjl@mox.example>
1809To: <remote@example.org>
1810Subject: test
1811Message-Id: <test@mox.example>
1812TLS-Required: No
1813
1814test email
1815`, "\n", "\r\n")
1816
1817 msg1 := strings.ReplaceAll(`From: <mjl@mox.example>
1818To: <remote@example.org>
1819Subject: test
1820Message-Id: <test@mox.example>
1821TLS-Required: No
1822TLS-Required: bogus
1823
1824test email
1825`, "\n", "\r\n")
1826
1827 msg2 := strings.ReplaceAll(`From: <mjl@mox.example>
1828To: <remote@example.org>
1829Subject: test
1830Message-Id: <test@mox.example>
1831
1832test email
1833`, "\n", "\r\n")
1834
1835 testSubmit := func(msg string, requiretls bool, expRequireTLS *bool) {
1836 t.Helper()
1837 ts.run(func(client *smtpclient.Client) {
1838 t.Helper()
1839
1840 rcptTo := "remote@example.org"
1841 err := client.Deliver(ctxbg, "mjl@mox.example", rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, requiretls)
1842 tcheck(t, err, "deliver")
1843
1844 msgs, err := queue.List(ctxbg, queue.Filter{}, queue.Sort{})
1845 tcheck(t, err, "listing queue")
1846 tcompare(t, len(msgs), 1)
1847 tcompare(t, msgs[0].RequireTLS, expRequireTLS)
1848 _, err = queue.Drop(ctxbg, pkglog, queue.Filter{IDs: []int64{msgs[0].ID}})
1849 tcheck(t, err, "deleting message from queue")
1850 })
1851 }
1852
1853 testSubmit(msg0, true, &yes) // Header ignored, requiretls applied.
1854 testSubmit(msg0, false, &no) // TLS-Required header applied.
1855 testSubmit(msg1, true, &yes) // Bad headers ignored, requiretls applied.
1856 testSubmit(msg1, false, nil) // Inconsistent multiple headers ignored.
1857 testSubmit(msg2, false, nil) // Regular message, no RequireTLS setting.
1858 testSubmit(msg2, true, &yes) // Requiretls applied.
1859
1860 // Check that we get an error if remote SMTP server does not support the requiretls
1861 // extension.
1862 ts.requiretls = false
1863 ts.run(func(client *smtpclient.Client) {
1864 rcptTo := "remote@example.org"
1865 err := client.Deliver(ctxbg, "mjl@mox.example", rcptTo, int64(len(msg0)), strings.NewReader(msg0), false, false, true)
1866 if err == nil {
1867 t.Fatalf("delivered with requiretls to server without requiretls")
1868 }
1869 if !errors.Is(err, smtpclient.ErrRequireTLSUnsupported) {
1870 t.Fatalf("got err %v, expected ErrRequireTLSUnsupported", err)
1871 }
1872 })
1873}
1874
1875func TestSmuggle(t *testing.T) {
1876 resolver := dns.MockResolver{
1877 A: map[string][]string{
1878 "example.org.": {"127.0.0.10"}, // For mx check.
1879 },
1880 PTR: map[string][]string{
1881 "127.0.0.10": {"example.org."}, // For iprev check.
1882 },
1883 }
1884 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpsmuggle/mox.conf"), resolver)
1885 ts.tlsmode = smtpclient.TLSSkip
1886 defer ts.close()
1887
1888 test := func(data string) {
1889 t.Helper()
1890
1891 ts.runRaw(func(conn net.Conn) {
1892 t.Helper()
1893
1894 ourHostname := mox.Conf.Static.HostnameDomain
1895 remoteHostname := dns.Domain{ASCII: "mox.example"}
1896 opts := smtpclient.Opts{
1897 RootCAs: mox.Conf.Static.TLS.CertPool,
1898 }
1899 log := pkglog.WithCid(ts.cid - 1)
1900 _, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, ts.tlspkix, ourHostname, remoteHostname, opts)
1901 tcheck(t, err, "smtpclient")
1902 defer conn.Close()
1903
1904 write := func(s string) {
1905 _, err := conn.Write([]byte(s))
1906 tcheck(t, err, "write")
1907 }
1908
1909 readPrefixLine := func(prefix string) string {
1910 t.Helper()
1911 buf := make([]byte, 512)
1912 n, err := conn.Read(buf)
1913 tcheck(t, err, "read")
1914 s := strings.TrimRight(string(buf[:n]), "\r\n")
1915 if !strings.HasPrefix(s, prefix) {
1916 t.Fatalf("got smtp response %q, expected line with prefix %q", s, prefix)
1917 }
1918 return s
1919 }
1920
1921 write("MAIL FROM:<remote@example.org>\r\n")
1922 readPrefixLine("2")
1923 write("RCPT TO:<mjl@mox.example>\r\n")
1924 readPrefixLine("2")
1925
1926 write("DATA\r\n")
1927 readPrefixLine("3")
1928 write("\r\n") // Empty header.
1929 write(data)
1930 write("\r\n.\r\n") // End of message.
1931 line := readPrefixLine("5")
1932 if !strings.Contains(line, "smug") {
1933 t.Errorf("got 5xx error with message %q, expected error text containing smug", line)
1934 }
1935 })
1936 }
1937
1938 test("\r\n.\n")
1939 test("\n.\n")
1940 test("\r.\r")
1941 test("\n.\r\n")
1942}
1943
1944func TestFutureRelease(t *testing.T) {
1945 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1946 ts.tlsmode = smtpclient.TLSSkip
1947 ts.user = "mjl@mox.example"
1948 ts.pass = password0
1949 ts.submission = true
1950 defer ts.close()
1951
1952 ts.auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
1953 return sasl.NewClientPlain(ts.user, ts.pass), nil
1954 }
1955
1956 test := func(mailtoMore, expResponsePrefix string) {
1957 t.Helper()
1958
1959 ts.runRaw(func(conn net.Conn) {
1960 t.Helper()
1961
1962 ourHostname := mox.Conf.Static.HostnameDomain
1963 remoteHostname := dns.Domain{ASCII: "mox.example"}
1964 opts := smtpclient.Opts{Auth: ts.auth}
1965 log := pkglog.WithCid(ts.cid - 1)
1966 _, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, false, ourHostname, remoteHostname, opts)
1967 tcheck(t, err, "smtpclient")
1968 defer conn.Close()
1969
1970 write := func(s string) {
1971 _, err := conn.Write([]byte(s))
1972 tcheck(t, err, "write")
1973 }
1974
1975 readPrefixLine := func(prefix string) string {
1976 t.Helper()
1977 buf := make([]byte, 512)
1978 n, err := conn.Read(buf)
1979 tcheck(t, err, "read")
1980 s := strings.TrimRight(string(buf[:n]), "\r\n")
1981 if !strings.HasPrefix(s, prefix) {
1982 t.Fatalf("got smtp response %q, expected line with prefix %q", s, prefix)
1983 }
1984 return s
1985 }
1986
1987 write(fmt.Sprintf("MAIL FROM:<mjl@mox.example>%s\r\n", mailtoMore))
1988 readPrefixLine(expResponsePrefix)
1989 if expResponsePrefix != "2" {
1990 return
1991 }
1992 write("RCPT TO:<mjl@mox.example>\r\n")
1993 readPrefixLine("2")
1994
1995 write("DATA\r\n")
1996 readPrefixLine("3")
1997 write("From: <mjl@mox.example>\r\n\r\nbody\r\n\r\n.\r\n")
1998 readPrefixLine("2")
1999 })
2000 }
2001
2002 test(" HOLDFOR=1", "2")
2003 test(" HOLDUNTIL="+time.Now().Add(time.Minute).UTC().Format(time.RFC3339), "2")
2004 test(" HOLDUNTIL="+time.Now().Add(time.Minute).UTC().Format(time.RFC3339Nano), "2")
2005
2006 test(" HOLDFOR=0", "501") // 0 is invalid syntax.
2007 test(fmt.Sprintf(" HOLDFOR=%d", int64((queue.FutureReleaseIntervalMax+time.Minute)/time.Second)), "554") // Too far in the future.
2008 test(" HOLDUNTIL="+time.Now().Add(-time.Minute).UTC().Format(time.RFC3339), "554") // In the past.
2009 test(" HOLDUNTIL="+time.Now().Add(queue.FutureReleaseIntervalMax+time.Minute).UTC().Format(time.RFC3339), "554") // Too far in the future.
2010 test(" HOLDUNTIL=2024-02-10T17:28:00+00:00", "501") // "Z" required.
2011 test(" HOLDUNTIL=24-02-10T17:28:00Z", "501") // Invalid.
2012 test(" HOLDFOR=1 HOLDFOR=1", "501") // Duplicate.
2013 test(" HOLDFOR=1 HOLDUNTIL="+time.Now().Add(time.Hour).UTC().Format(time.RFC3339), "501") // Duplicate.
2014}
2015
2016// Test SMTPUTF8
2017func TestSMTPUTF8(t *testing.T) {
2018 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
2019 defer ts.close()
2020
2021 ts.user = "mjl@mox.example"
2022 ts.pass = password0
2023 ts.submission = true
2024
2025 test := func(mailFrom string, rcptTo string, headerValue string, filename string, clientSmtputf8 bool, expectedSmtputf8 bool, expErr *smtpclient.Error) {
2026 t.Helper()
2027
2028 ts.run(func(client *smtpclient.Client) {
2029 t.Helper()
2030 msg := strings.ReplaceAll(fmt.Sprintf(`From: <%s>
2031To: <%s>
2032Subject: test
2033X-Custom-Test-Header: %s
2034MIME-Version: 1.0
2035Content-type: multipart/mixed; boundary="simple boundary"
2036
2037--simple boundary
2038Content-Type: text/plain; charset=UTF-8;
2039Content-Disposition: attachment; filename="%s"
2040Content-Transfer-Encoding: base64
2041
2042QW4gYXR0YWNoZWQgdGV4dCBmaWxlLg==
2043
2044--simple boundary--
2045`, mailFrom, rcptTo, headerValue, filename), "\n", "\r\n")
2046
2047 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), true, clientSmtputf8, false)
2048 ts.smtpErr(err, expErr)
2049 if err != nil {
2050 return
2051 }
2052
2053 msgs, _ := queue.List(ctxbg, queue.Filter{}, queue.Sort{Field: "Queued", Asc: false})
2054 queuedMsg := msgs[0]
2055 if queuedMsg.SMTPUTF8 != expectedSmtputf8 {
2056 t.Fatalf("[%s / %s / %s / %s] got SMTPUTF8 %t, expected %t", mailFrom, rcptTo, headerValue, filename, queuedMsg.SMTPUTF8, expectedSmtputf8)
2057 }
2058 })
2059 }
2060
2061 test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", false, false, nil)
2062 test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", true, false, nil)
2063 test(`mjl@mox.example`, `🙂@example.org`, "header-ascii", "ascii.txt", true, true, nil)
2064 test(`mjl@mox.example`, `🙂@example.org`, "header-ascii", "ascii.txt", false, true, &smtpclient.Error{Permanent: true, Code: smtp.C553BadMailbox, Secode: smtp.SeMsg6NonASCIIAddrNotPermitted7})
2065 test(`Ω@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", true, true, nil)
2066 test(`Ω@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", false, true, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeMsg6NonASCIIAddrNotPermitted7})
2067 test(`mjl@mox.example`, `remote@example.org`, "header-utf8-😍", "ascii.txt", true, true, nil)
2068 test(`mjl@mox.example`, `remote@example.org`, "header-utf8-😍", "ascii.txt", false, true, nil)
2069 test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "utf8-🫠️.txt", true, true, nil)
2070 test(`Ω@mox.example`, `🙂@example.org`, "header-utf8-😍", "utf8-🫠️.txt", true, true, nil)
2071 test(`mjl@mox.example`, `remote@xn--vg8h.example.org`, "header-ascii", "ascii.txt", true, false, nil)
2072}
2073
2074// TestExtra checks whether submission of messages with "X-Mox-Extra-<key>: value"
2075// headers cause those those key/value pairs to be added to the Extra field in the
2076// queue.
2077func TestExtra(t *testing.T) {
2078 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
2079 defer ts.close()
2080
2081 ts.user = "mjl@mox.example"
2082 ts.pass = password0
2083 ts.submission = true
2084
2085 extraMsg := strings.ReplaceAll(`From: <mjl@mox.example>
2086To: <remote@example.org>
2087Subject: test
2088X-Mox-Extra-Test: testvalue
2089X-Mox-Extra-a: 123
2090X-Mox-Extra-☺: ☹
2091X-Mox-Extra-x-cANONICAL-z: ok
2092Message-Id: <test@mox.example>
2093
2094test email
2095`, "\n", "\r\n")
2096
2097 ts.run(func(client *smtpclient.Client) {
2098 mailFrom := "mjl@mox.example"
2099 rcptTo := "mjl@mox.example"
2100 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2101 tcheck(t, err, "deliver")
2102 })
2103 msgs, err := queue.List(ctxbg, queue.Filter{}, queue.Sort{})
2104 tcheck(t, err, "queue list")
2105 tcompare(t, len(msgs), 1)
2106 tcompare(t, msgs[0].Extra, map[string]string{
2107 "Test": "testvalue",
2108 "A": "123",
2109 "☺": "☹",
2110 "X-Canonical-Z": "ok",
2111 })
2112 // note: these headers currently stay in the message.
2113}
2114
2115// TestExtraDup checks for an error for duplicate x-mox-extra-* keys.
2116func TestExtraDup(t *testing.T) {
2117 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
2118 defer ts.close()
2119
2120 ts.user = "mjl@mox.example"
2121 ts.pass = password0
2122 ts.submission = true
2123
2124 extraMsg := strings.ReplaceAll(`From: <mjl@mox.example>
2125To: <remote@example.org>
2126Subject: test
2127X-Mox-Extra-Test: testvalue
2128X-Mox-Extra-Test: testvalue
2129Message-Id: <test@mox.example>
2130
2131test email
2132`, "\n", "\r\n")
2133
2134 ts.run(func(client *smtpclient.Client) {
2135 mailFrom := "mjl@mox.example"
2136 rcptTo := "mjl@mox.example"
2137 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2138 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C554TransactionFailed, Secode: smtp.SeMsg6Other0})
2139 })
2140}
2141
2142// FromID can be specified during submission, but must be unique, with single recipient.
2143func TestUniqueFromID(t *testing.T) {
2144 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpfromid/mox.conf"), dns.MockResolver{})
2145 defer ts.close()
2146
2147 ts.user = "mjl+fromid@mox.example"
2148 ts.pass = password0
2149 ts.submission = true
2150
2151 extraMsg := strings.ReplaceAll(`From: <mjl@mox.example>
2152To: <remote@example.org>
2153Subject: test
2154
2155test email
2156`, "\n", "\r\n")
2157
2158 // Specify our own unique id when queueing.
2159 ts.run(func(client *smtpclient.Client) {
2160 mailFrom := "mjl+unique@mox.example"
2161 rcptTo := "mjl@mox.example"
2162 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2163 ts.smtpErr(err, nil)
2164 })
2165
2166 // But we can only use it once.
2167 ts.run(func(client *smtpclient.Client) {
2168 mailFrom := "mjl+unique@mox.example"
2169 rcptTo := "mjl@mox.example"
2170 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2171 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C554TransactionFailed, Secode: smtp.SeAddr1SenderSyntax7})
2172 })
2173
2174 // We cannot use our own fromid with multiple recipients.
2175 ts.run(func(client *smtpclient.Client) {
2176 mailFrom := "mjl+unique2@mox.example"
2177 rcptTo := []string{"mjl@mox.example", "mjl@mox.example"}
2178 _, err := client.DeliverMultiple(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2179 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C554TransactionFailed, Secode: smtp.SeProto5TooManyRcpts3})
2180 })
2181}
2182
2183// TestDestinationSMTPError checks delivery to a destination with an SMTPError is rejected as configured.
2184func TestDestinationSMTPError(t *testing.T) {
2185 resolver := dns.MockResolver{
2186 A: map[string][]string{
2187 "example.org.": {"127.0.0.10"}, // For mx check.
2188 },
2189 }
2190
2191 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
2192 defer ts.close()
2193
2194 ts.run(func(client *smtpclient.Client) {
2195 mailFrom := "mjl@example.org"
2196 rcptTo := "blocked@mox.example"
2197 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
2198 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
2199 })
2200}
2201
2202// TestDestinationMessageAuthRequiredSMTPError checks delivery to a destination
2203// with an MessageAuthRequiredSMTPError is accepted/rejected as configured.
2204func TestDestinationMessageAuthRequiredSMTPError(t *testing.T) {
2205 resolver := dns.MockResolver{
2206 A: map[string][]string{
2207 "example.org.": {"127.0.0.10"}, // For mx check.
2208 },
2209 PTR: map[string][]string{
2210 "127.0.0.10": {"example.org."},
2211 },
2212 TXT: map[string][]string{},
2213 }
2214
2215 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
2216 defer ts.close()
2217
2218 ts.run(func(client *smtpclient.Client) {
2219 mailFrom := "mjl@example.org"
2220 rcptTo := "msgauthrequired@mox.example"
2221 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
2222 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7MultiAuthFails26})
2223 })
2224
2225 // Ensure SPF pass, message should now be accepted.
2226 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
2227 ts.run(func(client *smtpclient.Client) {
2228 mailFrom := "mjl@example.org"
2229 rcptTo := "msgauthrequired@mox.example"
2230 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
2231 ts.smtpErr(err, nil)
2232 })
2233}
2234