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 changes <- ts.comm.Get()
628 }()
629
630 timer := time.NewTimer(time.Second)
631 defer timer.Stop()
632 select {
633 case <-changes:
634 case <-timer.C:
635 t.Fatalf("no delivery in 1s")
636 }
637 }
638 })
639
640 checkEvaluationCount(t, 0)
641}
642
643func tinsertmsg(t *testing.T, acc *store.Account, mailbox string, m *store.Message, msg string) {
644 mf, err := store.CreateMessageTemp(pkglog, "insertmsg")
645 tcheck(t, err, "temp message")
646 defer os.Remove(mf.Name())
647 defer mf.Close()
648 _, err = mf.Write([]byte(msg))
649 tcheck(t, err, "write message")
650
651 acc.WithWLock(func() {
652 err = acc.DeliverMailbox(pkglog, mailbox, m, mf)
653 tcheck(t, err, "deliver message")
654 })
655}
656
657func tretrain(t *testing.T, acc *store.Account) {
658 t.Helper()
659
660 // Fresh empty junkfilter.
661 basePath := mox.DataDirPath("accounts")
662 dbPath := filepath.Join(basePath, acc.Name, "junkfilter.db")
663 bloomPath := filepath.Join(basePath, acc.Name, "junkfilter.bloom")
664 os.Remove(dbPath)
665 os.Remove(bloomPath)
666 jf, _, err := acc.OpenJunkFilter(ctxbg, pkglog)
667 tcheck(t, err, "open junk filter")
668 defer jf.Close()
669
670 // Fetch messags to retrain on.
671 q := bstore.QueryDB[store.Message](ctxbg, acc.DB)
672 q.FilterEqual("Expunged", false)
673 q.FilterFn(func(m store.Message) bool {
674 return m.Flags.Junk != m.Flags.Notjunk
675 })
676 msgs, err := q.List()
677 tcheck(t, err, "fetch messages")
678
679 // Retrain the messages.
680 for _, m := range msgs {
681 ham := m.Flags.Notjunk
682
683 f, err := os.Open(acc.MessagePath(m.ID))
684 tcheck(t, err, "open message")
685 r := store.FileMsgReader(m.MsgPrefix, f)
686
687 jf.TrainMessage(ctxbg, r, m.Size, ham)
688
689 err = r.Close()
690 tcheck(t, err, "close message")
691 }
692
693 err = jf.Save()
694 tcheck(t, err, "save junkfilter")
695}
696
697// Test accept/reject with DMARC reputation and with spammy content.
698func TestSpam(t *testing.T) {
699 resolver := &dns.MockResolver{
700 A: map[string][]string{
701 "example.org.": {"127.0.0.1"}, // For mx check.
702 },
703 TXT: map[string][]string{
704 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
705 "_dmarc.example.org.": {"v=DMARC1;p=reject; rua=mailto:dmarcrpt@example.org"},
706 },
707 }
708 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
709 defer ts.close()
710
711 // Insert spammy messages. No junkfilter training yet.
712 m := store.Message{
713 RemoteIP: "127.0.0.10",
714 RemoteIPMasked1: "127.0.0.10",
715 RemoteIPMasked2: "127.0.0.0",
716 RemoteIPMasked3: "127.0.0.0",
717 MailFrom: "remote@example.org",
718 MailFromLocalpart: smtp.Localpart("remote"),
719 MailFromDomain: "example.org",
720 RcptToLocalpart: smtp.Localpart("mjl"),
721 RcptToDomain: "mox.example",
722 MsgFromLocalpart: smtp.Localpart("remote"),
723 MsgFromDomain: "example.org",
724 MsgFromOrgDomain: "example.org",
725 MsgFromValidated: true,
726 MsgFromValidation: store.ValidationStrict,
727 Flags: store.Flags{Seen: true, Junk: true},
728 Size: int64(len(deliverMessage)),
729 }
730 for range 3 {
731 nm := m
732 tinsertmsg(t, ts.acc, "Inbox", &nm, deliverMessage)
733 nm = m
734 tinsertmsg(t, ts.acc, "mjl2", &nm, deliverMessage)
735 }
736
737 // Delivery from sender with bad reputation should fail.
738 ts.run(func(client *smtpclient.Client) {
739 mailFrom := "remote@example.org"
740 rcptTo := "mjl@mox.example"
741 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
742 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
743
744 ts.checkCount("Rejects", 1)
745 checkEvaluationCount(t, 0) // No positive interactions yet.
746 })
747
748 // Delivery from sender with bad reputation matching AcceptRejectsToMailbox should
749 // result in accepted delivery to the mailbox.
750 ts.run(func(client *smtpclient.Client) {
751 mailFrom := "remote@example.org"
752 rcptTo := "mjl2@mox.example"
753 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage2)), strings.NewReader(deliverMessage2), false, false, false)
754 tcheck(t, err, "deliver")
755
756 ts.checkCount("mjl2junk", 1) // In ruleset rejects mailbox.
757 ts.checkCount("Rejects", 1) // Same as before.
758 checkEvaluationCount(t, 0) // This is not an actual accept.
759 })
760
761 // Mark the messages as having good reputation.
762 var ids []int64
763 err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).FilterEqual("Expunged", false).ForEach(func(m store.Message) error {
764 ids = append(ids, m.ID)
765 return nil
766 })
767 tcheck(t, err, "get message ids")
768 ts.xops.MessageFlagsClear(ctxbg, pkglog, ts.acc, ids, []string{"$Junk"})
769 ts.xops.MessageFlagsAdd(ctxbg, pkglog, ts.acc, ids, []string{"$NotJunk"})
770
771 // Message should now be accepted.
772 ts.run(func(client *smtpclient.Client) {
773 tcheck(t, err, "hello")
774 mailFrom := "remote@example.org"
775 rcptTo := "mjl@mox.example"
776 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
777 tcheck(t, err, "deliver")
778
779 // Message should now be removed from Rejects mailboxes.
780 ts.checkCount("Rejects", 0)
781 ts.checkCount("mjl2junk", 1)
782 checkEvaluationCount(t, 1)
783 })
784
785 // Undo dmarc pass, mark messages as junk, and train the filter.
786 resolver.TXT = nil
787 q := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB)
788 q.FilterEqual("Expunged", false)
789 _, err = q.UpdateFields(map[string]any{"Junk": true, "Notjunk": false})
790 tcheck(t, err, "update junkiness")
791 tretrain(t, ts.acc)
792
793 // Message should be refused for spammy content.
794 ts.run(func(client *smtpclient.Client) {
795 tcheck(t, err, "hello")
796 mailFrom := "remote@example.org"
797 rcptTo := "mjl@mox.example"
798 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
799 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
800 checkEvaluationCount(t, 1) // No new evaluation, this isn't a DMARC reject.
801 })
802}
803
804// Test accept/reject with forwarded messages, DMARC ignored, no IP/EHLO/MAIL
805// FROM-based reputation.
806func TestForward(t *testing.T) {
807 // Do a run without forwarding, and with.
808 check := func(forward bool) {
809
810 resolver := &dns.MockResolver{
811 A: map[string][]string{
812 "bad.example.": {"127.0.0.1"}, // For mx check.
813 "good.example.": {"127.0.0.1"}, // For mx check.
814 "forward.example.": {"127.0.0.10"}, // For mx check.
815 },
816 TXT: map[string][]string{
817 "bad.example.": {"v=spf1 ip4:127.0.0.1 -all"},
818 "good.example.": {"v=spf1 ip4:127.0.0.1 -all"},
819 "forward.example.": {"v=spf1 ip4:127.0.0.10 -all"},
820 "_dmarc.bad.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@bad.example"},
821 "_dmarc.good.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@good.example"},
822 "_dmarc.forward.example.": {"v=DMARC1;p=reject; rua=mailto:dmarc@forward.example"},
823 },
824 PTR: map[string][]string{
825 "127.0.0.10": {"forward.example."}, // For iprev check.
826 },
827 }
828 rcptTo := "mjl3@mox.example"
829 if !forward {
830 // For SPF and DMARC pass, otherwise the test ends quickly.
831 resolver.TXT["bad.example."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
832 resolver.TXT["good.example."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
833 rcptTo = "mjl@mox.example" // Without IsForward rule.
834 }
835
836 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
837 defer ts.close()
838
839 totalEvaluations := 0
840
841 var msgBad = strings.ReplaceAll(`From: <remote@bad.example>
842To: <mjl@mox.example>
843Subject: test
844Message-Id: <bad@example.org>
845
846test email
847`, "\n", "\r\n")
848 var msgOK = strings.ReplaceAll(`From: <remote@good.example>
849To: <mjl@mox.example>
850Subject: other
851Message-Id: <good@example.org>
852
853unrelated message.
854`, "\n", "\r\n")
855 var msgOK2 = strings.ReplaceAll(`From: <other@forward.example>
856To: <mjl@mox.example>
857Subject: non-forward
858Message-Id: <regular@example.org>
859
860happens to come from forwarding mail server.
861`, "\n", "\r\n")
862
863 // Deliver forwarded messages, then classify as junk. Normally enough to treat
864 // other unrelated messages from IP as junk, but not for forwarded messages.
865 ts.run(func(client *smtpclient.Client) {
866 mailFrom := "remote@forward.example"
867 if !forward {
868 mailFrom = "remote@bad.example"
869 }
870
871 for range 10 {
872 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgBad)), strings.NewReader(msgBad), false, false, false)
873 tcheck(t, err, "deliver message")
874 }
875 totalEvaluations += 10
876
877 n, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).UpdateFields(map[string]any{"Junk": true, "MsgFromValidated": true})
878 tcheck(t, err, "marking messages as junk")
879 tcompare(t, n, 10)
880 tretrain(t, ts.acc)
881
882 // Next delivery will fail, with negative "message From" signal.
883 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgBad)), strings.NewReader(msgBad), false, false, false)
884 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
885
886 checkEvaluationCount(t, totalEvaluations)
887 })
888
889 // Delivery from different "message From" without reputation, but from same
890 // forwarding email server, should succeed under forwarding, not as regular sending
891 // server.
892 ts.run(func(client *smtpclient.Client) {
893 mailFrom := "remote@forward.example"
894 if !forward {
895 mailFrom = "remote@good.example"
896 }
897
898 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgOK)), strings.NewReader(msgOK), false, false, false)
899 if forward {
900 tcheck(t, err, "deliver")
901 totalEvaluations += 1
902 } else {
903 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
904 }
905 checkEvaluationCount(t, totalEvaluations)
906 })
907
908 // Delivery from forwarding server that isn't a forward should get same treatment.
909 ts.run(func(client *smtpclient.Client) {
910 mailFrom := "other@forward.example"
911
912 // Ensure To header matches.
913 msg := msgOK2
914 if forward {
915 msg = strings.ReplaceAll(msg, "<mjl@mox.example>", "<mjl3@mox.example>")
916 }
917
918 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
919 if forward {
920 tcheck(t, err, "deliver")
921 totalEvaluations += 1
922 } else {
923 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
924 }
925 checkEvaluationCount(t, totalEvaluations)
926 })
927 }
928
929 check(true)
930 check(false)
931}
932
933// Messages that we sent to, that have passing DMARC, but that are otherwise spammy, should be accepted.
934func TestDMARCSent(t *testing.T) {
935 resolver := &dns.MockResolver{
936 A: map[string][]string{
937 "example.org.": {"127.0.0.1"}, // For mx check.
938 },
939 TXT: map[string][]string{
940 "example.org.": {"v=spf1 ip4:127.0.0.1 -all"},
941 "_dmarc.example.org.": {"v=DMARC1;p=reject;rua=mailto:dmarcrpt@example.org"},
942 },
943 }
944 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
945 defer ts.close()
946
947 // First check that DMARC policy rejects message and results in optional evaluation.
948 ts.run(func(client *smtpclient.Client) {
949 mailFrom := "remote@example.org"
950 rcptTo := "mjl@mox.example"
951 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
952 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7MultiAuthFails26})
953 l := checkEvaluationCount(t, 1)
954 tcompare(t, l[0].Optional, true)
955 })
956
957 // Update DNS for an SPF pass, and DMARC pass.
958 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
959
960 // Insert hammy & spammy messages not related to the test message.
961 m := store.Message{
962 MailFrom: "remote@test.example",
963 RcptToLocalpart: smtp.Localpart("mjl"),
964 RcptToDomain: "mox.example",
965 Flags: store.Flags{Seen: true},
966 Size: int64(len(deliverMessage)),
967 }
968 // We need at least 50 ham messages for the junk filter to become significant. We
969 // offset it with negative messages for mediocre score.
970 for range 50 {
971 nm := m
972 nm.Junk = true
973 tinsertmsg(t, ts.acc, "Archive", &nm, deliverMessage)
974
975 nm = m
976 nm.Notjunk = true
977 tinsertmsg(t, ts.acc, "Archive", &nm, deliverMessage)
978 }
979 tretrain(t, ts.acc)
980
981 // Baseline, message should be refused for spammy content.
982 ts.run(func(client *smtpclient.Client) {
983 mailFrom := "remote@example.org"
984 rcptTo := "mjl@mox.example"
985 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
986 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
987 checkEvaluationCount(t, 1) // No new evaluation.
988 })
989
990 // Insert a message that we sent to the address that is about to send to us.
991 sentMsg := store.Message{Size: int64(len(deliverMessage))}
992 tinsertmsg(t, ts.acc, "Sent", &sentMsg, deliverMessage)
993 err := ts.acc.DB.Insert(ctxbg, &store.Recipient{MessageID: sentMsg.ID, Localpart: "remote", Domain: "example.org", OrgDomain: "example.org", Sent: time.Now()})
994 tcheck(t, err, "inserting message recipient")
995
996 // Reject a message due to DMARC again. Since we sent a message to the domain, it
997 // is no longer unknown and we should see a non-optional evaluation that will
998 // result in a DMARC report.
999 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.1 -all"}
1000 ts.run(func(client *smtpclient.Client) {
1001 mailFrom := "remote@example.org"
1002 rcptTo := "mjl@mox.example"
1003 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1004 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7MultiAuthFails26})
1005 l := checkEvaluationCount(t, 2) // New evaluation.
1006 tcompare(t, l[1].Optional, false)
1007 })
1008
1009 // We should now be accepting the message because we recently sent a message.
1010 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
1011 ts.run(func(client *smtpclient.Client) {
1012 mailFrom := "remote@example.org"
1013 rcptTo := "mjl@mox.example"
1014 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1015 tcheck(t, err, "deliver")
1016 l := checkEvaluationCount(t, 3) // New evaluation.
1017 tcompare(t, l[2].Optional, false)
1018 })
1019}
1020
1021// Test DNSBL, then getting through with subjectpass.
1022func TestBlocklistedSubjectpass(t *testing.T) {
1023 // Set up a DNSBL on dnsbl.example, and get DMARC pass.
1024 resolver := &dns.MockResolver{
1025 A: map[string][]string{
1026 "example.org.": {"127.0.0.10"}, // For mx check.
1027 "2.0.0.127.dnsbl.example.": {"127.0.0.2"}, // For healthcheck.
1028 "10.0.0.127.dnsbl.example.": {"127.0.0.10"}, // Where our connection pretends to come from.
1029 },
1030 TXT: map[string][]string{
1031 "10.0.0.127.dnsbl.example.": {"blocklisted"},
1032 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
1033 "_dmarc.example.org.": {"v=DMARC1;p=reject"},
1034 },
1035 PTR: map[string][]string{
1036 "127.0.0.10": {"example.org."}, // For iprev check.
1037 },
1038 }
1039 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1040 ts.dnsbls = []dns.Domain{{ASCII: "dnsbl.example"}}
1041 defer ts.close()
1042
1043 // Message should be refused softly (temporary error) due to DNSBL.
1044 ts.run(func(client *smtpclient.Client) {
1045 mailFrom := "remote@example.org"
1046 rcptTo := "mjl@mox.example"
1047 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1048 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
1049 })
1050
1051 // Set up subjectpass on account.
1052 acc := mox.Conf.Dynamic.Accounts[ts.acc.Name]
1053 acc.SubjectPass.Period = time.Hour
1054 mox.Conf.Dynamic.Accounts[ts.acc.Name] = acc
1055
1056 // Message should be refused quickly (permanent error) due to DNSBL and Subjectkey.
1057 var pass string
1058 ts.run(func(client *smtpclient.Client) {
1059 mailFrom := "remote@example.org"
1060 rcptTo := "mjl@mox.example"
1061 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1062 cerr := ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7DeliveryUnauth1})
1063 i := strings.Index(cerr.Line, subjectpass.Explanation)
1064 if i < 0 {
1065 t.Fatalf("got error line %q, expected error line with subjectpass", cerr.Line)
1066 }
1067 pass = cerr.Line[i+len(subjectpass.Explanation):]
1068 })
1069
1070 ts.run(func(client *smtpclient.Client) {
1071 mailFrom := "remote@example.org"
1072 rcptTo := "mjl@mox.example"
1073 passMessage := strings.Replace(deliverMessage, "Subject: test", "Subject: test "+pass, 1)
1074 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(passMessage)), strings.NewReader(passMessage), false, false, false)
1075 tcheck(t, err, "deliver with subjectpass")
1076 })
1077}
1078
1079// Test accepting a DMARC report.
1080func TestDMARCReport(t *testing.T) {
1081 resolver := &dns.MockResolver{
1082 A: map[string][]string{
1083 "example.org.": {"127.0.0.10"}, // For mx check.
1084 },
1085 TXT: map[string][]string{
1086 "example.org.": {"v=spf1 ip4:127.0.0.10 -all"},
1087 "_dmarc.example.org.": {"v=DMARC1;p=reject; rua=mailto:dmarcrpt@example.org"},
1088 },
1089 PTR: map[string][]string{
1090 "127.0.0.10": {"example.org."}, // For iprev check.
1091 },
1092 }
1093 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/dmarcreport/mox.conf"), resolver)
1094 defer ts.close()
1095
1096 run := func(report string, n int) {
1097 t.Helper()
1098 ts.run(func(client *smtpclient.Client) {
1099 t.Helper()
1100
1101 mailFrom := "remote@example.org"
1102 rcptTo := "mjl@mox.example"
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 run(dmarcReport, 0)
1124 run(strings.ReplaceAll(dmarcReport, "xmox.nl", "mox.example"), 1)
1125
1126 // We always store as an evaluation, but as optional for reports.
1127 evals := checkEvaluationCount(t, 2)
1128 tcompare(t, evals[0].Optional, true)
1129 tcompare(t, evals[1].Optional, true)
1130}
1131
1132const dmarcReport = `<?xml version="1.0" encoding="UTF-8" ?>
1133<feedback>
1134 <report_metadata>
1135 <org_name>example.org</org_name>
1136 <email>postmaster@example.org</email>
1137 <report_id>1</report_id>
1138 <date_range>
1139 <begin>1596412800</begin>
1140 <end>1596499199</end>
1141 </date_range>
1142 </report_metadata>
1143 <policy_published>
1144 <domain>xmox.nl</domain>
1145 <adkim>r</adkim>
1146 <aspf>r</aspf>
1147 <p>reject</p>
1148 <sp>reject</sp>
1149 <pct>100</pct>
1150 </policy_published>
1151 <record>
1152 <row>
1153 <source_ip>127.0.0.10</source_ip>
1154 <count>1</count>
1155 <policy_evaluated>
1156 <disposition>none</disposition>
1157 <dkim>pass</dkim>
1158 <spf>pass</spf>
1159 </policy_evaluated>
1160 </row>
1161 <identifiers>
1162 <header_from>xmox.nl</header_from>
1163 </identifiers>
1164 <auth_results>
1165 <dkim>
1166 <domain>xmox.nl</domain>
1167 <result>pass</result>
1168 <selector>testsel</selector>
1169 </dkim>
1170 <spf>
1171 <domain>xmox.nl</domain>
1172 <result>pass</result>
1173 </spf>
1174 </auth_results>
1175 </record>
1176</feedback>
1177`
1178
1179// Test accepting a TLS report.
1180func TestTLSReport(t *testing.T) {
1181 // Requires setting up DKIM.
1182 privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
1183 dkimRecord := dkim.Record{
1184 Version: "DKIM1",
1185 Hashes: []string{"sha256"},
1186 Flags: []string{"s"},
1187 PublicKey: privKey.Public(),
1188 Key: "ed25519",
1189 }
1190 dkimTxt, err := dkimRecord.Record()
1191 tcheck(t, err, "dkim record")
1192
1193 sel := config.Selector{
1194 HashEffective: "sha256",
1195 HeadersEffective: []string{"From", "To", "Subject", "Date"},
1196 Key: privKey,
1197 Domain: dns.Domain{ASCII: "testsel"},
1198 }
1199 dkimConf := config.DKIM{
1200 Selectors: map[string]config.Selector{"testsel": sel},
1201 Sign: []string{"testsel"},
1202 }
1203
1204 resolver := &dns.MockResolver{
1205 A: map[string][]string{
1206 "example.org.": {"127.0.0.10"}, // For mx check.
1207 },
1208 TXT: map[string][]string{
1209 "testsel._domainkey.example.org.": {dkimTxt},
1210 "_dmarc.example.org.": {"v=DMARC1;p=reject;rua=mailto:dmarcrpt@example.org"},
1211 },
1212 PTR: map[string][]string{
1213 "127.0.0.10": {"example.org."}, // For iprev check.
1214 },
1215 }
1216 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/tlsrpt/mox.conf"), resolver)
1217 defer ts.close()
1218
1219 run := func(rcptTo, tlsrpt string, n int) {
1220 t.Helper()
1221 ts.run(func(client *smtpclient.Client) {
1222 t.Helper()
1223
1224 mailFrom := "remote@example.org"
1225
1226 msgb := &bytes.Buffer{}
1227 _, 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)
1228 tcheck(t, xerr, "write msg")
1229 msg := msgb.String()
1230
1231 selectors := mox.DKIMSelectors(dkimConf)
1232 headers, xerr := dkim.Sign(ctxbg, pkglog.Logger, "remote", dns.Domain{ASCII: "example.org"}, selectors, false, strings.NewReader(msg))
1233 tcheck(t, xerr, "dkim sign")
1234 msg = headers + msg
1235
1236 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1237 tcheck(t, err, "deliver")
1238
1239 records, err := tlsrptdb.Records(ctxbg)
1240 tcheck(t, err, "tlsrptdb records")
1241 if len(records) != n {
1242 t.Fatalf("got %d tlsrptdb records, expected %d", len(records), n)
1243 }
1244 })
1245 }
1246
1247 const tlsrpt = `{"organization-name":"Example.org","date-range":{"start-datetime":"2022-01-07T00:00:00Z","end-datetime":"2022-01-07T23:59:59Z"},"contact-info":"tlsrpt@example.org","report-id":"1","policies":[{"policy":{"policy-type":"no-policy-found","policy-domain":"xmox.nl"},"summary":{"total-successful-session-count":1,"total-failure-session-count":0}}]}`
1248
1249 run("mjl@mox.example", tlsrpt, 0)
1250 run("mjl@mox.example", strings.ReplaceAll(tlsrpt, "xmox.nl", "mox.example"), 1)
1251 run("mjl@mailhost.mox.example", strings.ReplaceAll(tlsrpt, "xmox.nl", "mailhost.mox.example"), 2)
1252
1253 // We always store as an evaluation, but as optional for reports.
1254 evals := checkEvaluationCount(t, 3)
1255 tcompare(t, evals[0].Optional, true)
1256 tcompare(t, evals[1].Optional, true)
1257 tcompare(t, evals[2].Optional, true)
1258}
1259
1260func TestRatelimitConnectionrate(t *testing.T) {
1261 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1262 defer ts.close()
1263
1264 // We'll be creating 300 connections, no TLS and reduce noise.
1265 ts.tlsmode = smtpclient.TLSSkip
1266 mlog.SetConfig(map[string]slog.Level{"": mlog.LevelInfo})
1267 defer mlog.SetConfig(map[string]slog.Level{"": mlog.LevelDebug})
1268
1269 // We may be passing a window boundary during this tests. The limit is 300/minute.
1270 // So make twice that many connections and hope the tests don't take too long.
1271 for i := 0; i <= 2*300; i++ {
1272 ts.runx(func(err error, client *smtpclient.Client) {
1273 t.Helper()
1274 if err != nil && i < 300 {
1275 t.Fatalf("expected smtp connection, got %v", err)
1276 }
1277 if err == nil && i == 600 {
1278 t.Fatalf("expected no smtp connection due to connection rate limit, got connection")
1279 }
1280 if client != nil {
1281 client.Close()
1282 }
1283 })
1284 }
1285}
1286
1287func TestRatelimitAuth(t *testing.T) {
1288 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1289 defer ts.close()
1290
1291 ts.submission = true
1292 ts.tlsmode = smtpclient.TLSSkip
1293 ts.user = "bad"
1294 ts.pass = "bad"
1295
1296 // We may be passing a window boundary during this tests. The limit is 10 auth
1297 // failures/minute. So make twice that many connections and hope the tests don't
1298 // take too long.
1299 for i := 0; i <= 2*10; i++ {
1300 ts.runx(func(err error, client *smtpclient.Client) {
1301 t.Helper()
1302 if err == nil {
1303 t.Fatalf("got auth success with bad credentials")
1304 }
1305 var cerr smtpclient.Error
1306 badauth := errors.As(err, &cerr) && cerr.Code == smtp.C535AuthBadCreds
1307 if !badauth && i < 10 {
1308 t.Fatalf("expected auth failure, got %v", err)
1309 }
1310 if badauth && i == 20 {
1311 t.Fatalf("expected no smtp connection due to failed auth rate limit, got other error %v", err)
1312 }
1313 if client != nil {
1314 client.Close()
1315 }
1316 })
1317 }
1318}
1319
1320func TestRatelimitDelivery(t *testing.T) {
1321 resolver := dns.MockResolver{
1322 A: map[string][]string{
1323 "example.org.": {"127.0.0.10"}, // For mx check.
1324 },
1325 PTR: map[string][]string{
1326 "127.0.0.10": {"example.org."},
1327 },
1328 }
1329 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1330 defer ts.close()
1331
1332 orig := limitIPMasked1MessagesPerMinute
1333 limitIPMasked1MessagesPerMinute = 1
1334 defer func() {
1335 limitIPMasked1MessagesPerMinute = orig
1336 }()
1337
1338 ts.run(func(client *smtpclient.Client) {
1339 mailFrom := "remote@example.org"
1340 rcptTo := "mjl@mox.example"
1341 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1342 tcheck(t, err, "deliver to remote")
1343
1344 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1345 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C452StorageFull, Secode: smtp.SeMailbox2Full2})
1346 })
1347
1348 limitIPMasked1MessagesPerMinute = orig
1349
1350 origSize := limitIPMasked1SizePerMinute
1351 // Message was already delivered once. We'll do another one. But the 3rd will fail.
1352 // We need the actual size with prepended headers, since that is used in the
1353 // calculations.
1354 msg, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).Get()
1355 if err != nil {
1356 t.Fatalf("getting delivered message for its size: %v", err)
1357 }
1358 limitIPMasked1SizePerMinute = 2*msg.Size + int64(len(deliverMessage)/2)
1359 defer func() {
1360 limitIPMasked1SizePerMinute = origSize
1361 }()
1362 ts.run(func(client *smtpclient.Client) {
1363 mailFrom := "remote@example.org"
1364 rcptTo := "mjl@mox.example"
1365 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1366 tcheck(t, err, "deliver to remote")
1367
1368 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1369 ts.smtpErr(err, &smtpclient.Error{Permanent: false, Code: smtp.C452StorageFull, Secode: smtp.SeMailbox2Full2})
1370 })
1371}
1372
1373func TestNonSMTP(t *testing.T) {
1374 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1375 defer ts.close()
1376 ts.cid += 2
1377
1378 serverConn, clientConn := net.Pipe()
1379 defer serverConn.Close()
1380 serverdone := make(chan struct{})
1381 defer func() { <-serverdone }()
1382
1383 go func() {
1384 tlsConfig := &tls.Config{
1385 Certificates: []tls.Certificate{fakeCert(ts.t, false)},
1386 }
1387 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)
1388 close(serverdone)
1389 }()
1390
1391 defer clientConn.Close()
1392
1393 buf := make([]byte, 128)
1394
1395 // Read and ignore hello.
1396 if _, err := clientConn.Read(buf); err != nil {
1397 t.Fatalf("reading hello: %v", err)
1398 }
1399
1400 if _, err := fmt.Fprintf(clientConn, "bogus\r\n"); err != nil {
1401 t.Fatalf("write command: %v", err)
1402 }
1403 n, err := clientConn.Read(buf)
1404 if err != nil {
1405 t.Fatalf("read response line: %v", err)
1406 }
1407 s := string(buf[:n])
1408 if !strings.HasPrefix(s, "500 5.5.2 ") {
1409 t.Fatalf(`got %q, expected "500 5.5.2 ...`, s)
1410 }
1411 if _, err := clientConn.Read(buf); err == nil {
1412 t.Fatalf("connection not closed after bogus command")
1413 }
1414}
1415
1416// Test limits on outgoing messages.
1417func TestLimitOutgoing(t *testing.T) {
1418 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpserversendlimit/mox.conf"), dns.MockResolver{})
1419 defer ts.close()
1420
1421 ts.user = "mjl@mox.example"
1422 ts.pass = password0
1423 ts.submission = true
1424
1425 err := ts.acc.DB.Insert(ctxbg, &store.Outgoing{Recipient: "a@other.example", Submitted: time.Now().Add(-24*time.Hour - time.Minute)})
1426 tcheck(t, err, "inserting outgoing/recipient past 24h window")
1427
1428 testSubmit := func(rcptTo string, expErr *smtpclient.Error) {
1429 t.Helper()
1430 ts.run(func(client *smtpclient.Client) {
1431 t.Helper()
1432 mailFrom := "mjl@mox.example"
1433 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
1434 ts.smtpErr(err, expErr)
1435 })
1436 }
1437
1438 // Limits are set to 4 messages a day, 2 first-time recipients.
1439 testSubmit("b@other.example", nil)
1440 testSubmit("c@other.example", nil)
1441 testSubmit("d@other.example", &smtpclient.Error{Code: smtp.C451LocalErr, Secode: smtp.SePol7DeliveryUnauth1}) // Would be 3rd recipient.
1442 testSubmit("b@other.example", nil)
1443 testSubmit("b@other.example", nil)
1444 testSubmit("b@other.example", &smtpclient.Error{Code: smtp.C451LocalErr, Secode: smtp.SePol7DeliveryUnauth1}) // Would be 5th message.
1445}
1446
1447// Test account size limit enforcement.
1448func TestQuota(t *testing.T) {
1449 resolver := dns.MockResolver{
1450 A: map[string][]string{
1451 "other.example.": {"127.0.0.10"}, // For mx check.
1452 },
1453 PTR: map[string][]string{
1454 "127.0.0.10": {"other.example."},
1455 },
1456 }
1457 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpserverquota/mox.conf"), resolver)
1458 defer ts.close()
1459
1460 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1461 ts.run(func(client *smtpclient.Client) {
1462 mailFrom := "mjl@other.example"
1463 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1464 ts.smtpErr(err, expErr)
1465 })
1466 }
1467
1468 testDeliver("mjl@mox.example", &smtpclient.Error{Code: smtp.C452StorageFull, Secode: smtp.SeMailbox2Full2})
1469}
1470
1471// Test with catchall destination address.
1472func TestCatchall(t *testing.T) {
1473 resolver := dns.MockResolver{
1474 A: map[string][]string{
1475 "other.example.": {"127.0.0.10"}, // For mx check.
1476 },
1477 PTR: map[string][]string{
1478 "127.0.0.10": {"other.example."},
1479 },
1480 }
1481 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpservercatchall/mox.conf"), resolver)
1482 defer ts.close()
1483
1484 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1485 t.Helper()
1486 ts.run(func(client *smtpclient.Client) {
1487 t.Helper()
1488 mailFrom := "mjl@other.example"
1489 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
1490 ts.smtpErr(err, expErr)
1491 })
1492 }
1493
1494 testDeliver("mjl@mox.example", nil) // Exact match.
1495 testDeliver("mjl+test@mox.example", nil) // Domain localpart catchall separator.
1496 testDeliver("MJL+TEST@mox.example", nil) // Again, and case insensitive.
1497
1498 n, err := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).Count()
1499 tcheck(t, err, "checking delivered messages")
1500 tcompare(t, n, 3)
1501
1502 testDeliver("unknown@mox.example", nil) // Catchall address, to account catchall.
1503
1504 acc, err := store.OpenAccount(pkglog, "catchall", false)
1505 tcheck(t, err, "open account")
1506 defer func() {
1507 acc.Close()
1508 acc.WaitClosed()
1509 }()
1510 n, err = bstore.QueryDB[store.Message](ctxbg, acc.DB).Count()
1511 tcheck(t, err, "checking delivered messages to catchall account")
1512 tcompare(t, n, 1)
1513
1514 testDeliver("mjl-test@mox2.example", nil) // Second catchall separator.
1515 testDeliver("mjl-test+test@mox2.example", nil) // Silly, both separators in address.
1516 testDeliver("mjl+test-test@mox2.example", nil)
1517 n, err = bstore.QueryDB[store.Message](ctxbg, ts.acc.DB).Count()
1518 tcheck(t, err, "checking delivered messages")
1519 tcompare(t, n, 6)
1520}
1521
1522// Test DKIM signing for outgoing messages.
1523func TestDKIMSign(t *testing.T) {
1524 resolver := dns.MockResolver{
1525 A: map[string][]string{
1526 "mox.example.": {"127.0.0.10"}, // For mx check.
1527 },
1528 PTR: map[string][]string{
1529 "127.0.0.10": {"mox.example."},
1530 },
1531 }
1532
1533 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1534 defer ts.close()
1535
1536 // Set DKIM signing config.
1537 var gen byte
1538 genDKIM := func(domain string) string {
1539 dom, _ := mox.Conf.Domain(dns.Domain{ASCII: domain})
1540
1541 privkey := make([]byte, ed25519.SeedSize) // Fake key, don't use for real.
1542 gen++
1543 privkey[0] = byte(gen)
1544
1545 sel := config.Selector{
1546 HashEffective: "sha256",
1547 HeadersEffective: []string{"From", "To", "Subject"},
1548 Key: ed25519.NewKeyFromSeed(privkey),
1549 Domain: dns.Domain{ASCII: "testsel"},
1550 }
1551 dom.DKIM = config.DKIM{
1552 Selectors: map[string]config.Selector{"testsel": sel},
1553 Sign: []string{"testsel"},
1554 }
1555 mox.Conf.Dynamic.Domains[domain] = dom
1556 pubkey := sel.Key.Public().(ed25519.PublicKey)
1557 return "v=DKIM1;k=ed25519;p=" + base64.StdEncoding.EncodeToString(pubkey)
1558 }
1559
1560 dkimtxt := genDKIM("mox.example")
1561 dkimtxt2 := genDKIM("mox2.example")
1562
1563 // DKIM verify needs to find the key.
1564 resolver.TXT = map[string][]string{
1565 "testsel._domainkey.mox.example.": {dkimtxt},
1566 "testsel._domainkey.mox2.example.": {dkimtxt2},
1567 }
1568
1569 ts.submission = true
1570 ts.user = "mjl@mox.example"
1571 ts.pass = password0
1572
1573 n := 0
1574 testSubmit := func(mailFrom, msgFrom string) {
1575 t.Helper()
1576 ts.run(func(client *smtpclient.Client) {
1577 t.Helper()
1578
1579 msg := strings.ReplaceAll(fmt.Sprintf(`From: <%s>
1580To: <remote@example.org>
1581Subject: test
1582Message-Id: <test@mox.example>
1583
1584test email
1585`, msgFrom), "\n", "\r\n")
1586
1587 rcptTo := "remote@example.org"
1588 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1589 tcheck(t, err, "deliver")
1590
1591 msgs, err := queue.List(ctxbg, queue.Filter{}, queue.Sort{})
1592 tcheck(t, err, "listing queue")
1593 n++
1594 tcompare(t, len(msgs), n)
1595 sort.Slice(msgs, func(i, j int) bool {
1596 return msgs[i].ID > msgs[j].ID
1597 })
1598 f, err := queue.OpenMessage(ctxbg, msgs[0].ID)
1599 tcheck(t, err, "open message in queue")
1600 defer f.Close()
1601 results, err := dkim.Verify(ctxbg, pkglog.Logger, resolver, false, dkim.DefaultPolicy, f, false)
1602 tcheck(t, err, "verifying dkim message")
1603 tcompare(t, len(results), 1)
1604 tcompare(t, results[0].Status, dkim.StatusPass)
1605 tcompare(t, results[0].Sig.Domain.ASCII, strings.Split(msgFrom, "@")[1])
1606 })
1607 }
1608
1609 testSubmit("mjl@mox.example", "mjl@mox.example")
1610 testSubmit("mjl@mox.example", "mjl@mox2.example") // DKIM signature will be for mox2.example.
1611}
1612
1613// Test to postmaster addresses.
1614func TestPostmaster(t *testing.T) {
1615 resolver := dns.MockResolver{
1616 A: map[string][]string{
1617 "other.example.": {"127.0.0.10"}, // For mx check.
1618 },
1619 PTR: map[string][]string{
1620 "127.0.0.10": {"other.example."},
1621 },
1622 }
1623 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/postmaster/mox.conf"), resolver)
1624 defer ts.close()
1625
1626 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1627 t.Helper()
1628 ts.run(func(client *smtpclient.Client) {
1629 t.Helper()
1630 mailFrom := "mjl@other.example"
1631 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
1632 ts.smtpErr(err, expErr)
1633 })
1634 }
1635
1636 testDeliver("postmaster", nil) // Plain postmaster address without domain.
1637 testDeliver("postmaster@host.mox.example", nil) // Postmaster address with configured mail server hostname.
1638 testDeliver("postmaster@mox.example", nil) // Postmaster address without explicitly configured destination.
1639 testDeliver("postmaster@unknown.example", &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
1640}
1641
1642// Test to address with empty localpart.
1643func TestEmptylocalpart(t *testing.T) {
1644 resolver := dns.MockResolver{
1645 A: map[string][]string{
1646 "other.example.": {"127.0.0.10"}, // For mx check.
1647 },
1648 PTR: map[string][]string{
1649 "127.0.0.10": {"other.example."},
1650 },
1651 }
1652 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1653 defer ts.close()
1654
1655 testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
1656 ts.run(func(client *smtpclient.Client) {
1657 mailFrom := `""@other.example`
1658 msg := strings.ReplaceAll(deliverMessage, "To: <mjl@mox.example>", `To: <""@mox.example>`)
1659 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
1660 ts.smtpErr(err, expErr)
1661 })
1662 }
1663
1664 testDeliver(`""@mox.example`, nil)
1665}
1666
1667// Test handling REQUIRETLS and TLS-Required: No.
1668func TestRequireTLS(t *testing.T) {
1669 resolver := dns.MockResolver{
1670 A: map[string][]string{
1671 "mox.example.": {"127.0.0.10"}, // For mx check.
1672 },
1673 PTR: map[string][]string{
1674 "127.0.0.10": {"mox.example."},
1675 },
1676 }
1677
1678 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
1679 defer ts.close()
1680
1681 ts.submission = true
1682 ts.requiretls = true
1683 ts.user = "mjl@mox.example"
1684 ts.pass = password0
1685
1686 no := false
1687 yes := true
1688
1689 msg0 := strings.ReplaceAll(`From: <mjl@mox.example>
1690To: <remote@example.org>
1691Subject: test
1692Message-Id: <test@mox.example>
1693TLS-Required: No
1694
1695test email
1696`, "\n", "\r\n")
1697
1698 msg1 := strings.ReplaceAll(`From: <mjl@mox.example>
1699To: <remote@example.org>
1700Subject: test
1701Message-Id: <test@mox.example>
1702TLS-Required: No
1703TLS-Required: bogus
1704
1705test email
1706`, "\n", "\r\n")
1707
1708 msg2 := strings.ReplaceAll(`From: <mjl@mox.example>
1709To: <remote@example.org>
1710Subject: test
1711Message-Id: <test@mox.example>
1712
1713test email
1714`, "\n", "\r\n")
1715
1716 testSubmit := func(msg string, requiretls bool, expRequireTLS *bool) {
1717 t.Helper()
1718 ts.run(func(client *smtpclient.Client) {
1719 t.Helper()
1720
1721 rcptTo := "remote@example.org"
1722 err := client.Deliver(ctxbg, "mjl@mox.example", rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, requiretls)
1723 tcheck(t, err, "deliver")
1724
1725 msgs, err := queue.List(ctxbg, queue.Filter{}, queue.Sort{})
1726 tcheck(t, err, "listing queue")
1727 tcompare(t, len(msgs), 1)
1728 tcompare(t, msgs[0].RequireTLS, expRequireTLS)
1729 _, err = queue.Drop(ctxbg, pkglog, queue.Filter{IDs: []int64{msgs[0].ID}})
1730 tcheck(t, err, "deleting message from queue")
1731 })
1732 }
1733
1734 testSubmit(msg0, true, &yes) // Header ignored, requiretls applied.
1735 testSubmit(msg0, false, &no) // TLS-Required header applied.
1736 testSubmit(msg1, true, &yes) // Bad headers ignored, requiretls applied.
1737 testSubmit(msg1, false, nil) // Inconsistent multiple headers ignored.
1738 testSubmit(msg2, false, nil) // Regular message, no RequireTLS setting.
1739 testSubmit(msg2, true, &yes) // Requiretls applied.
1740
1741 // Check that we get an error if remote SMTP server does not support the requiretls
1742 // extension.
1743 ts.requiretls = false
1744 ts.run(func(client *smtpclient.Client) {
1745 rcptTo := "remote@example.org"
1746 err := client.Deliver(ctxbg, "mjl@mox.example", rcptTo, int64(len(msg0)), strings.NewReader(msg0), false, false, true)
1747 if err == nil {
1748 t.Fatalf("delivered with requiretls to server without requiretls")
1749 }
1750 if !errors.Is(err, smtpclient.ErrRequireTLSUnsupported) {
1751 t.Fatalf("got err %v, expected ErrRequireTLSUnsupported", err)
1752 }
1753 })
1754}
1755
1756func TestSmuggle(t *testing.T) {
1757 resolver := dns.MockResolver{
1758 A: map[string][]string{
1759 "example.org.": {"127.0.0.10"}, // For mx check.
1760 },
1761 PTR: map[string][]string{
1762 "127.0.0.10": {"example.org."}, // For iprev check.
1763 },
1764 }
1765 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpsmuggle/mox.conf"), resolver)
1766 ts.tlsmode = smtpclient.TLSSkip
1767 defer ts.close()
1768
1769 test := func(data string) {
1770 t.Helper()
1771
1772 ts.runRaw(func(conn net.Conn) {
1773 t.Helper()
1774
1775 ourHostname := mox.Conf.Static.HostnameDomain
1776 remoteHostname := dns.Domain{ASCII: "mox.example"}
1777 opts := smtpclient.Opts{
1778 RootCAs: mox.Conf.Static.TLS.CertPool,
1779 }
1780 log := pkglog.WithCid(ts.cid - 1)
1781 _, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, ts.tlspkix, ourHostname, remoteHostname, opts)
1782 tcheck(t, err, "smtpclient")
1783 defer conn.Close()
1784
1785 write := func(s string) {
1786 _, err := conn.Write([]byte(s))
1787 tcheck(t, err, "write")
1788 }
1789
1790 readPrefixLine := func(prefix string) string {
1791 t.Helper()
1792 buf := make([]byte, 512)
1793 n, err := conn.Read(buf)
1794 tcheck(t, err, "read")
1795 s := strings.TrimRight(string(buf[:n]), "\r\n")
1796 if !strings.HasPrefix(s, prefix) {
1797 t.Fatalf("got smtp response %q, expected line with prefix %q", s, prefix)
1798 }
1799 return s
1800 }
1801
1802 write("MAIL FROM:<remote@example.org>\r\n")
1803 readPrefixLine("2")
1804 write("RCPT TO:<mjl@mox.example>\r\n")
1805 readPrefixLine("2")
1806
1807 write("DATA\r\n")
1808 readPrefixLine("3")
1809 write("\r\n") // Empty header.
1810 write(data)
1811 write("\r\n.\r\n") // End of message.
1812 line := readPrefixLine("5")
1813 if !strings.Contains(line, "smug") {
1814 t.Errorf("got 5xx error with message %q, expected error text containing smug", line)
1815 }
1816 })
1817 }
1818
1819 test("\r\n.\n")
1820 test("\n.\n")
1821 test("\r.\r")
1822 test("\n.\r\n")
1823}
1824
1825func TestFutureRelease(t *testing.T) {
1826 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1827 ts.tlsmode = smtpclient.TLSSkip
1828 ts.user = "mjl@mox.example"
1829 ts.pass = password0
1830 ts.submission = true
1831 defer ts.close()
1832
1833 ts.auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
1834 return sasl.NewClientPlain(ts.user, ts.pass), nil
1835 }
1836
1837 test := func(mailtoMore, expResponsePrefix string) {
1838 t.Helper()
1839
1840 ts.runRaw(func(conn net.Conn) {
1841 t.Helper()
1842
1843 ourHostname := mox.Conf.Static.HostnameDomain
1844 remoteHostname := dns.Domain{ASCII: "mox.example"}
1845 opts := smtpclient.Opts{Auth: ts.auth}
1846 log := pkglog.WithCid(ts.cid - 1)
1847 _, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, false, ourHostname, remoteHostname, opts)
1848 tcheck(t, err, "smtpclient")
1849 defer conn.Close()
1850
1851 write := func(s string) {
1852 _, err := conn.Write([]byte(s))
1853 tcheck(t, err, "write")
1854 }
1855
1856 readPrefixLine := func(prefix string) string {
1857 t.Helper()
1858 buf := make([]byte, 512)
1859 n, err := conn.Read(buf)
1860 tcheck(t, err, "read")
1861 s := strings.TrimRight(string(buf[:n]), "\r\n")
1862 if !strings.HasPrefix(s, prefix) {
1863 t.Fatalf("got smtp response %q, expected line with prefix %q", s, prefix)
1864 }
1865 return s
1866 }
1867
1868 write(fmt.Sprintf("MAIL FROM:<mjl@mox.example>%s\r\n", mailtoMore))
1869 readPrefixLine(expResponsePrefix)
1870 if expResponsePrefix != "2" {
1871 return
1872 }
1873 write("RCPT TO:<mjl@mox.example>\r\n")
1874 readPrefixLine("2")
1875
1876 write("DATA\r\n")
1877 readPrefixLine("3")
1878 write("From: <mjl@mox.example>\r\n\r\nbody\r\n\r\n.\r\n")
1879 readPrefixLine("2")
1880 })
1881 }
1882
1883 test(" HOLDFOR=1", "2")
1884 test(" HOLDUNTIL="+time.Now().Add(time.Minute).UTC().Format(time.RFC3339), "2")
1885 test(" HOLDUNTIL="+time.Now().Add(time.Minute).UTC().Format(time.RFC3339Nano), "2")
1886
1887 test(" HOLDFOR=0", "501") // 0 is invalid syntax.
1888 test(fmt.Sprintf(" HOLDFOR=%d", int64((queue.FutureReleaseIntervalMax+time.Minute)/time.Second)), "554") // Too far in the future.
1889 test(" HOLDUNTIL="+time.Now().Add(-time.Minute).UTC().Format(time.RFC3339), "554") // In the past.
1890 test(" HOLDUNTIL="+time.Now().Add(queue.FutureReleaseIntervalMax+time.Minute).UTC().Format(time.RFC3339), "554") // Too far in the future.
1891 test(" HOLDUNTIL=2024-02-10T17:28:00+00:00", "501") // "Z" required.
1892 test(" HOLDUNTIL=24-02-10T17:28:00Z", "501") // Invalid.
1893 test(" HOLDFOR=1 HOLDFOR=1", "501") // Duplicate.
1894 test(" HOLDFOR=1 HOLDUNTIL="+time.Now().Add(time.Hour).UTC().Format(time.RFC3339), "501") // Duplicate.
1895}
1896
1897// Test SMTPUTF8
1898func TestSMTPUTF8(t *testing.T) {
1899 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1900 defer ts.close()
1901
1902 ts.user = "mjl@mox.example"
1903 ts.pass = password0
1904 ts.submission = true
1905
1906 test := func(mailFrom string, rcptTo string, headerValue string, filename string, clientSmtputf8 bool, expectedSmtputf8 bool, expErr *smtpclient.Error) {
1907 t.Helper()
1908
1909 ts.run(func(client *smtpclient.Client) {
1910 t.Helper()
1911 msg := strings.ReplaceAll(fmt.Sprintf(`From: <%s>
1912To: <%s>
1913Subject: test
1914X-Custom-Test-Header: %s
1915MIME-Version: 1.0
1916Content-type: multipart/mixed; boundary="simple boundary"
1917
1918--simple boundary
1919Content-Type: text/plain; charset=UTF-8;
1920Content-Disposition: attachment; filename="%s"
1921Content-Transfer-Encoding: base64
1922
1923QW4gYXR0YWNoZWQgdGV4dCBmaWxlLg==
1924
1925--simple boundary--
1926`, mailFrom, rcptTo, headerValue, filename), "\n", "\r\n")
1927
1928 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), true, clientSmtputf8, false)
1929 ts.smtpErr(err, expErr)
1930 if err != nil {
1931 return
1932 }
1933
1934 msgs, _ := queue.List(ctxbg, queue.Filter{}, queue.Sort{Field: "Queued", Asc: false})
1935 queuedMsg := msgs[0]
1936 if queuedMsg.SMTPUTF8 != expectedSmtputf8 {
1937 t.Fatalf("[%s / %s / %s / %s] got SMTPUTF8 %t, expected %t", mailFrom, rcptTo, headerValue, filename, queuedMsg.SMTPUTF8, expectedSmtputf8)
1938 }
1939 })
1940 }
1941
1942 test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", false, false, nil)
1943 test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", true, false, nil)
1944 test(`mjl@mox.example`, `🙂@example.org`, "header-ascii", "ascii.txt", true, true, nil)
1945 test(`mjl@mox.example`, `🙂@example.org`, "header-ascii", "ascii.txt", false, true, &smtpclient.Error{Permanent: true, Code: smtp.C553BadMailbox, Secode: smtp.SeMsg6NonASCIIAddrNotPermitted7})
1946 test(`Ω@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", true, true, nil)
1947 test(`Ω@mox.example`, `remote@example.org`, "header-ascii", "ascii.txt", false, true, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeMsg6NonASCIIAddrNotPermitted7})
1948 test(`mjl@mox.example`, `remote@example.org`, "header-utf8-😍", "ascii.txt", true, true, nil)
1949 test(`mjl@mox.example`, `remote@example.org`, "header-utf8-😍", "ascii.txt", false, true, nil)
1950 test(`mjl@mox.example`, `remote@example.org`, "header-ascii", "utf8-🫠️.txt", true, true, nil)
1951 test(`Ω@mox.example`, `🙂@example.org`, "header-utf8-😍", "utf8-🫠️.txt", true, true, nil)
1952 test(`mjl@mox.example`, `remote@xn--vg8h.example.org`, "header-ascii", "ascii.txt", true, false, nil)
1953}
1954
1955// TestExtra checks whether submission of messages with "X-Mox-Extra-<key>: value"
1956// headers cause those those key/value pairs to be added to the Extra field in the
1957// queue.
1958func TestExtra(t *testing.T) {
1959 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1960 defer ts.close()
1961
1962 ts.user = "mjl@mox.example"
1963 ts.pass = password0
1964 ts.submission = true
1965
1966 extraMsg := strings.ReplaceAll(`From: <mjl@mox.example>
1967To: <remote@example.org>
1968Subject: test
1969X-Mox-Extra-Test: testvalue
1970X-Mox-Extra-a: 123
1971X-Mox-Extra-☺: ☹
1972X-Mox-Extra-x-cANONICAL-z: ok
1973Message-Id: <test@mox.example>
1974
1975test email
1976`, "\n", "\r\n")
1977
1978 ts.run(func(client *smtpclient.Client) {
1979 mailFrom := "mjl@mox.example"
1980 rcptTo := "mjl@mox.example"
1981 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
1982 tcheck(t, err, "deliver")
1983 })
1984 msgs, err := queue.List(ctxbg, queue.Filter{}, queue.Sort{})
1985 tcheck(t, err, "queue list")
1986 tcompare(t, len(msgs), 1)
1987 tcompare(t, msgs[0].Extra, map[string]string{
1988 "Test": "testvalue",
1989 "A": "123",
1990 "☺": "☹",
1991 "X-Canonical-Z": "ok",
1992 })
1993 // note: these headers currently stay in the message.
1994}
1995
1996// TestExtraDup checks for an error for duplicate x-mox-extra-* keys.
1997func TestExtraDup(t *testing.T) {
1998 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
1999 defer ts.close()
2000
2001 ts.user = "mjl@mox.example"
2002 ts.pass = password0
2003 ts.submission = true
2004
2005 extraMsg := strings.ReplaceAll(`From: <mjl@mox.example>
2006To: <remote@example.org>
2007Subject: test
2008X-Mox-Extra-Test: testvalue
2009X-Mox-Extra-Test: testvalue
2010Message-Id: <test@mox.example>
2011
2012test email
2013`, "\n", "\r\n")
2014
2015 ts.run(func(client *smtpclient.Client) {
2016 mailFrom := "mjl@mox.example"
2017 rcptTo := "mjl@mox.example"
2018 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2019 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C554TransactionFailed, Secode: smtp.SeMsg6Other0})
2020 })
2021}
2022
2023// FromID can be specified during submission, but must be unique, with single recipient.
2024func TestUniqueFromID(t *testing.T) {
2025 ts := newTestServer(t, filepath.FromSlash("../testdata/smtpfromid/mox.conf"), dns.MockResolver{})
2026 defer ts.close()
2027
2028 ts.user = "mjl+fromid@mox.example"
2029 ts.pass = password0
2030 ts.submission = true
2031
2032 extraMsg := strings.ReplaceAll(`From: <mjl@mox.example>
2033To: <remote@example.org>
2034Subject: test
2035
2036test email
2037`, "\n", "\r\n")
2038
2039 // Specify our own unique id when queueing.
2040 ts.run(func(client *smtpclient.Client) {
2041 mailFrom := "mjl+unique@mox.example"
2042 rcptTo := "mjl@mox.example"
2043 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2044 ts.smtpErr(err, nil)
2045 })
2046
2047 // But we can only use it once.
2048 ts.run(func(client *smtpclient.Client) {
2049 mailFrom := "mjl+unique@mox.example"
2050 rcptTo := "mjl@mox.example"
2051 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2052 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C554TransactionFailed, Secode: smtp.SeAddr1SenderSyntax7})
2053 })
2054
2055 // We cannot use our own fromid with multiple recipients.
2056 ts.run(func(client *smtpclient.Client) {
2057 mailFrom := "mjl+unique2@mox.example"
2058 rcptTo := []string{"mjl@mox.example", "mjl@mox.example"}
2059 _, err := client.DeliverMultiple(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
2060 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C554TransactionFailed, Secode: smtp.SeProto5TooManyRcpts3})
2061 })
2062}
2063
2064// TestDestinationSMTPError checks delivery to a destination with an SMTPError is rejected as configured.
2065func TestDestinationSMTPError(t *testing.T) {
2066 resolver := dns.MockResolver{
2067 A: map[string][]string{
2068 "example.org.": {"127.0.0.10"}, // For mx check.
2069 },
2070 }
2071
2072 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
2073 defer ts.close()
2074
2075 ts.run(func(client *smtpclient.Client) {
2076 mailFrom := "mjl@example.org"
2077 rcptTo := "blocked@mox.example"
2078 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
2079 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
2080 })
2081}
2082
2083// TestDestinationMessageAuthRequiredSMTPError checks delivery to a destination
2084// with an MessageAuthRequiredSMTPError is accepted/rejected as configured.
2085func TestDestinationMessageAuthRequiredSMTPError(t *testing.T) {
2086 resolver := dns.MockResolver{
2087 A: map[string][]string{
2088 "example.org.": {"127.0.0.10"}, // For mx check.
2089 },
2090 PTR: map[string][]string{
2091 "127.0.0.10": {"example.org."},
2092 },
2093 TXT: map[string][]string{},
2094 }
2095
2096 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
2097 defer ts.close()
2098
2099 ts.run(func(client *smtpclient.Client) {
2100 mailFrom := "mjl@example.org"
2101 rcptTo := "msgauthrequired@mox.example"
2102 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
2103 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7MultiAuthFails26})
2104 })
2105
2106 // Ensure SPF pass, message should now be accepted.
2107 resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
2108 ts.run(func(client *smtpclient.Client) {
2109 mailFrom := "mjl@example.org"
2110 rcptTo := "msgauthrequired@mox.example"
2111 err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
2112 ts.smtpErr(err, nil)
2113 })
2114}
2115