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