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