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