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