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