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