1package smtpclient
2
3import (
4 "bufio"
5 "context"
6 "crypto/ed25519"
7 cryptorand "crypto/rand"
8 "crypto/sha1"
9 "crypto/sha256"
10 "crypto/tls"
11 "crypto/x509"
12 "encoding/base64"
13 "errors"
14 "fmt"
15 "hash"
16 "io"
17 "log/slog"
18 "math/big"
19 "net"
20 "reflect"
21 "strings"
22 "testing"
23 "time"
24
25 "github.com/mjl-/mox/dns"
26 "github.com/mjl-/mox/mlog"
27 "github.com/mjl-/mox/sasl"
28 "github.com/mjl-/mox/scram"
29 "github.com/mjl-/mox/smtp"
30)
31
32var zerohost dns.Domain
33var localhost = dns.Domain{ASCII: "localhost"}
34
35func TestClient(t *testing.T) {
36 ctx := context.Background()
37 log := mlog.New("smtpclient", nil)
38
39 mlog.SetConfig(map[string]slog.Level{"": mlog.LevelTrace})
40 defer mlog.SetConfig(map[string]slog.Level{"": mlog.LevelDebug})
41
42 type options struct {
43 // Server behaviour.
44 pipelining bool
45 ecodes bool
46 maxSize int
47 starttls bool
48 eightbitmime bool
49 smtputf8 bool
50 requiretls bool
51 ehlo bool
52 auths []string // Allowed mechanisms.
53
54 nodeliver bool // For server, whether client will attempt a delivery.
55
56 // Client behaviour.
57 tlsMode TLSMode
58 tlsPKIX bool
59 roots *x509.CertPool
60 tlsHostname dns.Domain
61 need8bitmime bool
62 needsmtputf8 bool
63 needsrequiretls bool
64 recipients []string // If nil, mjl@mox.example is used.
65 resps []Response // Checked only if non-nil.
66 }
67
68 // Make fake cert, and make it trusted.
69 cert := fakeCert(t, false)
70 roots := x509.NewCertPool()
71 roots.AddCert(cert.Leaf)
72 tlsConfig := tls.Config{
73 Certificates: []tls.Certificate{cert},
74 }
75
76 cleanupResp := func(resps []Response) []Response {
77 for i, r := range resps {
78 resps[i] = Response{Code: r.Code, Secode: r.Secode}
79 }
80 return resps
81 }
82
83 test := func(msg string, opts options, auth func(l []string, cs *tls.ConnectionState) (sasl.Client, error), expClientErr, expDeliverErr, expServerErr error) {
84 t.Helper()
85
86 if opts.tlsMode == "" {
87 opts.tlsMode = TLSOpportunistic
88 }
89
90 clientConn, serverConn := net.Pipe()
91 defer serverConn.Close()
92
93 result := make(chan error, 2)
94
95 go func() {
96 defer func() {
97 x := recover()
98 if x != nil && x != "stop" {
99 panic(x)
100 }
101 }()
102 fail := func(format string, args ...any) {
103 err := fmt.Errorf("server: %w", fmt.Errorf(format, args...))
104 log.Errorx("failure", err)
105 if err != nil && expServerErr != nil && (errors.Is(err, expServerErr) || errors.As(err, reflect.New(reflect.ValueOf(expServerErr).Type()).Interface())) {
106 err = nil
107 }
108 result <- err
109 panic("stop")
110 }
111
112 br := bufio.NewReader(serverConn)
113 readline := func(prefix string) string {
114 s, err := br.ReadString('\n')
115 if err != nil {
116 fail("expected command: %v", err)
117 }
118 if !strings.HasPrefix(strings.ToLower(s), strings.ToLower(prefix)) {
119 fail("expected command %q, got: %s", prefix, s)
120 }
121 s = s[len(prefix):]
122 return strings.TrimSuffix(s, "\r\n")
123 }
124 writeline := func(s string) {
125 fmt.Fprintf(serverConn, "%s\r\n", s)
126 }
127
128 haveTLS := false
129
130 ehlo := true // Initially we expect EHLO.
131 var hello func()
132 hello = func() {
133 if !ehlo {
134 readline("HELO")
135 writeline("250 mox.example")
136 return
137 }
138
139 readline("EHLO")
140
141 if !opts.ehlo {
142 // Client will try again with HELO.
143 writeline("500 bad syntax")
144 ehlo = false
145 hello()
146 return
147 }
148
149 writeline("250-mox.example")
150 if opts.pipelining {
151 writeline("250-PIPELINING")
152 }
153 if opts.maxSize > 0 {
154 writeline(fmt.Sprintf("250-SIZE %d", opts.maxSize))
155 }
156 if opts.ecodes {
157 writeline("250-ENHANCEDSTATUSCODES")
158 }
159 if opts.starttls && !haveTLS {
160 writeline("250-STARTTLS")
161 }
162 if opts.eightbitmime {
163 writeline("250-8BITMIME")
164 }
165 if opts.smtputf8 {
166 writeline("250-SMTPUTF8")
167 }
168 if opts.requiretls && haveTLS {
169 writeline("250-REQUIRETLS")
170 }
171 if opts.auths != nil {
172 writeline("250-AUTH " + strings.Join(opts.auths, " "))
173 }
174 writeline("250-LIMITS MAILMAX=10 RCPTMAX=100 RCPTDOMAINMAX=1")
175 writeline("250 UNKNOWN") // To be ignored.
176 }
177
178 writeline("220 mox.example ESMTP test")
179
180 hello()
181
182 if opts.starttls {
183 readline("STARTTLS")
184 writeline("220 go")
185 tlsConn := tls.Server(serverConn, &tlsConfig)
186 nctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
187 defer cancel()
188 err := tlsConn.HandshakeContext(nctx)
189 if err != nil {
190 fail("tls handshake: %w", err)
191 }
192 serverConn = tlsConn
193 br = bufio.NewReader(serverConn)
194
195 haveTLS = true
196 hello()
197 }
198
199 if opts.auths != nil {
200 more := readline("AUTH ")
201 t := strings.SplitN(more, " ", 2)
202 switch t[0] {
203 case "PLAIN":
204 writeline("235 2.7.0 auth ok")
205 case "CRAM-MD5":
206 writeline("334 " + base64.StdEncoding.EncodeToString([]byte("<123.1234@host>")))
207 readline("") // Proof
208 writeline("235 2.7.0 auth ok")
209 case "SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1":
210 // Cannot fake/hardcode scram interactions.
211 var h func() hash.Hash
212 salt := scram.MakeRandom()
213 var iterations int
214 switch t[0] {
215 case "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1":
216 h = sha1.New
217 iterations = 2 * 4096
218 case "SCRAM-SHA-256-PLUS", "SCRAM-SHA-256":
219 h = sha256.New
220 iterations = 4096
221 default:
222 panic("missing case for scram")
223 }
224 var cs *tls.ConnectionState
225 if strings.HasSuffix(t[0], "-PLUS") {
226 if !haveTLS {
227 writeline("501 scram plus without tls not possible")
228 readline("QUIT")
229 writeline("221 ok")
230 result <- nil
231 return
232 }
233 xcs := serverConn.(*tls.Conn).ConnectionState()
234 cs = &xcs
235 }
236 saltedPassword := scram.SaltPassword(h, "test", salt, iterations)
237
238 clientFirst, err := base64.StdEncoding.DecodeString(t[1])
239 if err != nil {
240 fail("bad base64: %w", err)
241 }
242 s, err := scram.NewServer(h, clientFirst, cs, cs != nil)
243 if err != nil {
244 fail("scram new server: %w", err)
245 }
246 serverFirst, err := s.ServerFirst(iterations, salt)
247 if err != nil {
248 fail("scram server first: %w", err)
249 }
250 writeline("334 " + base64.StdEncoding.EncodeToString([]byte(serverFirst)))
251
252 xclientFinal := readline("")
253 clientFinal, err := base64.StdEncoding.DecodeString(xclientFinal)
254 if err != nil {
255 fail("bad base64: %w", err)
256 }
257 serverFinal, err := s.Finish([]byte(clientFinal), saltedPassword)
258 if err != nil {
259 fail("scram finish: %w", err)
260 }
261 writeline("334 " + base64.StdEncoding.EncodeToString([]byte(serverFinal)))
262 readline("")
263 writeline("235 2.7.0 auth ok")
264 default:
265 writeline("501 unknown mechanism")
266 }
267 }
268
269 if expClientErr == nil && !opts.nodeliver {
270 readline("MAIL FROM:")
271 writeline("250 ok")
272 n := len(opts.recipients)
273 if n == 0 {
274 n = 1
275 }
276 for i := 0; i < n; i++ {
277 readline("RCPT TO:")
278 resp := "250 ok"
279 if i < len(opts.resps) {
280 resp = fmt.Sprintf("%d maybe", opts.resps[i].Code)
281 }
282 writeline(resp)
283 }
284 readline("DATA")
285 writeline("354 continue")
286 reader := smtp.NewDataReader(br)
287 io.Copy(io.Discard, reader)
288 writeline("250 ok")
289
290 if expDeliverErr == nil {
291 readline("RSET")
292 writeline("250 ok")
293
294 readline("MAIL FROM:")
295 writeline("250 ok")
296 for i := 0; i < n; i++ {
297 readline("RCPT TO:")
298 resp := "250 ok"
299 if i < len(opts.resps) {
300 resp = fmt.Sprintf("%d maybe", opts.resps[i].Code)
301 }
302 writeline(resp)
303 }
304 readline("DATA")
305 writeline("354 continue")
306 reader = smtp.NewDataReader(br)
307 io.Copy(io.Discard, reader)
308 writeline("250 ok")
309 }
310 }
311
312 readline("QUIT")
313 writeline("221 ok")
314 result <- nil
315 }()
316
317 // todo: should abort tests more properly. on client failures, we may be left with hanging test.
318 go func() {
319 defer func() {
320 x := recover()
321 if x != nil && x != "stop" {
322 panic(x)
323 }
324 }()
325 fail := func(format string, args ...any) {
326 err := fmt.Errorf("client: %w", fmt.Errorf(format, args...))
327 log.Errorx("failure", err)
328 result <- err
329 panic("stop")
330 }
331 client, err := New(ctx, log.Logger, clientConn, opts.tlsMode, opts.tlsPKIX, localhost, opts.tlsHostname, Opts{Auth: auth, RootCAs: opts.roots})
332 if (err == nil) != (expClientErr == nil) || err != nil && !errors.As(err, reflect.New(reflect.ValueOf(expClientErr).Type()).Interface()) && !errors.Is(err, expClientErr) {
333 fail("new client: got err %v, expected %#v", err, expClientErr)
334 }
335 if err != nil {
336 result <- nil
337 return
338 }
339 rcptTo := opts.recipients
340 if len(rcptTo) == 0 {
341 rcptTo = []string{"mjl@mox.example"}
342 }
343 resps, err := client.DeliverMultiple(ctx, "postmaster@mox.example", rcptTo, int64(len(msg)), strings.NewReader(msg), opts.need8bitmime, opts.needsmtputf8, opts.needsrequiretls)
344 if (err == nil) != (expDeliverErr == nil) || err != nil && !errors.Is(err, expDeliverErr) && !reflect.DeepEqual(err, expDeliverErr) {
345 fail("first deliver: got err %#v (%s), expected %#v (%s)", err, err, expDeliverErr, expDeliverErr)
346 } else if opts.resps != nil && !reflect.DeepEqual(cleanupResp(resps), opts.resps) {
347 fail("first deliver: got resps %v, expected %v", resps, opts.resps)
348 }
349 if err == nil {
350 err = client.Reset()
351 if err != nil {
352 fail("reset: %v", err)
353 }
354 resps, err = client.DeliverMultiple(ctx, "postmaster@mox.example", rcptTo, int64(len(msg)), strings.NewReader(msg), opts.need8bitmime, opts.needsmtputf8, opts.needsrequiretls)
355 if (err == nil) != (expDeliverErr == nil) || err != nil && !errors.Is(err, expDeliverErr) && !reflect.DeepEqual(err, expDeliverErr) {
356 fail("second deliver: got err %#v (%s), expected %#v (%s)", err, err, expDeliverErr, expDeliverErr)
357 } else if opts.resps != nil && !reflect.DeepEqual(cleanupResp(resps), opts.resps) {
358 fail("second: got resps %v, expected %v", resps, opts.resps)
359 }
360 }
361 err = client.Close()
362 if err != nil {
363 fail("close client: %v", err)
364 }
365 result <- nil
366 }()
367
368 var errs []error
369 for i := 0; i < 2; i++ {
370 err := <-result
371 if err != nil {
372 errs = append(errs, err)
373 }
374 }
375 if errs != nil {
376 t.Fatalf("%v", errs)
377 }
378 }
379
380 msg := strings.ReplaceAll(`From: <postmaster@mox.example>
381To: <mjl@mox.example>
382Subject: test
383
384test
385`, "\n", "\r\n")
386
387 allopts := options{
388 pipelining: true,
389 ecodes: true,
390 maxSize: 512,
391 eightbitmime: true,
392 smtputf8: true,
393 starttls: true,
394 ehlo: true,
395 requiretls: true,
396
397 tlsMode: TLSRequiredStartTLS,
398 tlsPKIX: true,
399 roots: roots,
400 tlsHostname: dns.Domain{ASCII: "mox.example"},
401 need8bitmime: true,
402 needsmtputf8: true,
403 needsrequiretls: true,
404 }
405
406 test(msg, options{}, nil, nil, nil, nil)
407 test(msg, allopts, nil, nil, nil, nil)
408 test(msg, options{ehlo: true, eightbitmime: true}, nil, nil, nil, nil)
409 test(msg, options{ehlo: true, eightbitmime: false, need8bitmime: true, nodeliver: true}, nil, nil, Err8bitmimeUnsupported, nil)
410 test(msg, options{ehlo: true, smtputf8: false, needsmtputf8: true, nodeliver: true}, nil, nil, ErrSMTPUTF8Unsupported, nil)
411
412 // Server TLS handshake is a net.OpError with "remote error" as text.
413 test(msg, options{ehlo: true, starttls: true, tlsMode: TLSRequiredStartTLS, tlsPKIX: true, tlsHostname: dns.Domain{ASCII: "mismatch.example"}, nodeliver: true}, nil, ErrTLS, nil, &net.OpError{})
414
415 test(msg, options{ehlo: true, maxSize: len(msg) - 1, nodeliver: true}, nil, nil, ErrSize, nil)
416
417 // Multiple recipients, not pipelined.
418 multi1 := options{
419 ehlo: true,
420 pipelining: true,
421 ecodes: true,
422 recipients: []string{"mjl@mox.example", "mjl2@mox.example", "mjl3@mox.example"},
423 resps: []Response{
424 {Code: smtp.C250Completed},
425 {Code: smtp.C250Completed},
426 {Code: smtp.C250Completed},
427 },
428 }
429 test(msg, multi1, nil, nil, nil, nil)
430 multi1.pipelining = true
431 test(msg, multi1, nil, nil, nil, nil)
432
433 // Multiple recipients with 452 and other error, not pipelined
434 multi2 := options{
435 ehlo: true,
436 ecodes: true,
437 recipients: []string{"xmjl@mox.example", "xmjl2@mox.example", "xmjl3@mox.example"},
438 resps: []Response{
439 {Code: smtp.C250Completed},
440 {Code: smtp.C554TransactionFailed}, // Will continue when not pipelined.
441 {Code: smtp.C452StorageFull}, // Will stop sending further recipients.
442 },
443 }
444 test(msg, multi2, nil, nil, nil, nil)
445 multi2.pipelining = true
446 test(msg, multi2, nil, nil, nil, nil)
447 multi2.pipelining = false
448 multi2.resps[2].Code = smtp.C552MailboxFull
449 test(msg, multi2, nil, nil, nil, nil)
450 multi2.pipelining = true
451 test(msg, multi2, nil, nil, nil, nil)
452
453 // Single recipient with error and pipelining is an error.
454 multi3 := options{
455 ehlo: true,
456 pipelining: true,
457 ecodes: true,
458 recipients: []string{"xmjl@mox.example"},
459 resps: []Response{{Code: smtp.C452StorageFull}},
460 }
461 test(msg, multi3, nil, nil, Error{Code: smtp.C452StorageFull, Command: "rcptto", Line: "452 maybe"}, nil)
462
463 authPlain := func(l []string, cs *tls.ConnectionState) (sasl.Client, error) {
464 return sasl.NewClientPlain("test", "test"), nil
465 }
466 test(msg, options{ehlo: true, auths: []string{"PLAIN"}}, authPlain, nil, nil, nil)
467
468 authCRAMMD5 := func(l []string, cs *tls.ConnectionState) (sasl.Client, error) {
469 return sasl.NewClientCRAMMD5("test", "test"), nil
470 }
471 test(msg, options{ehlo: true, auths: []string{"CRAM-MD5"}}, authCRAMMD5, nil, nil, nil)
472
473 // todo: add tests for failing authentication, also at various stages in SCRAM
474
475 authSCRAMSHA1 := func(l []string, cs *tls.ConnectionState) (sasl.Client, error) {
476 return sasl.NewClientSCRAMSHA1("test", "test", false), nil
477 }
478 test(msg, options{ehlo: true, auths: []string{"SCRAM-SHA-1"}}, authSCRAMSHA1, nil, nil, nil)
479
480 authSCRAMSHA1PLUS := func(l []string, cs *tls.ConnectionState) (sasl.Client, error) {
481 return sasl.NewClientSCRAMSHA1PLUS("test", "test", *cs), nil
482 }
483 test(msg, options{ehlo: true, starttls: true, auths: []string{"SCRAM-SHA-1-PLUS"}}, authSCRAMSHA1PLUS, nil, nil, nil)
484
485 authSCRAMSHA256 := func(l []string, cs *tls.ConnectionState) (sasl.Client, error) {
486 return sasl.NewClientSCRAMSHA256("test", "test", false), nil
487 }
488 test(msg, options{ehlo: true, auths: []string{"SCRAM-SHA-256"}}, authSCRAMSHA256, nil, nil, nil)
489
490 authSCRAMSHA256PLUS := func(l []string, cs *tls.ConnectionState) (sasl.Client, error) {
491 return sasl.NewClientSCRAMSHA256PLUS("test", "test", *cs), nil
492 }
493 test(msg, options{ehlo: true, starttls: true, auths: []string{"SCRAM-SHA-256-PLUS"}}, authSCRAMSHA256PLUS, nil, nil, nil)
494
495 test(msg, options{ehlo: true, requiretls: false, needsrequiretls: true, nodeliver: true}, nil, nil, ErrRequireTLSUnsupported, nil)
496
497 // Set an expired certificate. For non-strict TLS, we should still accept it.
498 // ../rfc/7435:424
499 cert = fakeCert(t, true)
500 roots = x509.NewCertPool()
501 roots.AddCert(cert.Leaf)
502 tlsConfig = tls.Config{
503 Certificates: []tls.Certificate{cert},
504 }
505 test(msg, options{ehlo: true, starttls: true, roots: roots}, nil, nil, nil, nil)
506
507 // Again with empty cert pool so it isn't trusted in any way.
508 roots = x509.NewCertPool()
509 tlsConfig = tls.Config{
510 Certificates: []tls.Certificate{cert},
511 }
512 test(msg, options{ehlo: true, starttls: true, roots: roots}, nil, nil, nil, nil)
513}
514
515func TestErrors(t *testing.T) {
516 ctx := context.Background()
517 log := mlog.New("smtpclient", nil)
518
519 // Invalid greeting.
520 run(t, func(s xserver) {
521 s.writeline("bogus") // Invalid, should be "220 <hostname>".
522 }, func(conn net.Conn) {
523 _, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
524 var xerr Error
525 if err == nil || !errors.Is(err, ErrProtocol) || !errors.As(err, &xerr) || xerr.Permanent {
526 panic(fmt.Errorf("got %#v, expected ErrProtocol without Permanent", err))
527 }
528 })
529
530 // Server just closes connection.
531 run(t, func(s xserver) {
532 s.conn.Close()
533 }, func(conn net.Conn) {
534 _, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
535 var xerr Error
536 if err == nil || !errors.Is(err, io.ErrUnexpectedEOF) || !errors.As(err, &xerr) || xerr.Permanent {
537 panic(fmt.Errorf("got %#v (%v), expected ErrUnexpectedEOF without Permanent", err, err))
538 }
539 })
540
541 // Server does not want to speak SMTP.
542 run(t, func(s xserver) {
543 s.writeline("521 not accepting connections")
544 }, func(conn net.Conn) {
545 _, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
546 var xerr Error
547 if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || !xerr.Permanent {
548 panic(fmt.Errorf("got %#v, expected ErrStatus with Permanent", err))
549 }
550 })
551
552 // Server has invalid code in greeting.
553 run(t, func(s xserver) {
554 s.writeline("2200 mox.example") // Invalid, too many digits.
555 }, func(conn net.Conn) {
556 _, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
557 var xerr Error
558 if err == nil || !errors.Is(err, ErrProtocol) || !errors.As(err, &xerr) || xerr.Permanent {
559 panic(fmt.Errorf("got %#v, expected ErrProtocol without Permanent", err))
560 }
561 })
562
563 // Server sends multiline response, but with different codes.
564 run(t, func(s xserver) {
565 s.writeline("220 mox.example")
566 s.readline("EHLO")
567 s.writeline("250-mox.example")
568 s.writeline("500 different code") // Invalid.
569 }, func(conn net.Conn) {
570 _, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
571 var xerr Error
572 if err == nil || !errors.Is(err, ErrProtocol) || !errors.As(err, &xerr) || xerr.Permanent {
573 panic(fmt.Errorf("got %#v, expected ErrProtocol without Permanent", err))
574 }
575 })
576
577 // Server permanently refuses MAIL FROM.
578 run(t, func(s xserver) {
579 s.writeline("220 mox.example")
580 s.readline("EHLO")
581 s.writeline("250-mox.example")
582 s.writeline("250 ENHANCEDSTATUSCODES")
583 s.readline("MAIL FROM:")
584 s.writeline("550 5.7.0 not allowed")
585 }, func(conn net.Conn) {
586 c, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
587 if err != nil {
588 panic(err)
589 }
590 msg := ""
591 err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
592 var xerr Error
593 if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || !xerr.Permanent {
594 panic(fmt.Errorf("got %#v, expected ErrStatus with Permanent", err))
595 }
596 })
597
598 // Server temporarily refuses MAIL FROM.
599 run(t, func(s xserver) {
600 s.writeline("220 mox.example")
601 s.readline("EHLO")
602 s.writeline("250 mox.example")
603 s.readline("MAIL FROM:")
604 s.writeline("451 bad sender")
605 }, func(conn net.Conn) {
606 c, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
607 if err != nil {
608 panic(err)
609 }
610 msg := ""
611 err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
612 var xerr Error
613 if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || xerr.Permanent {
614 panic(fmt.Errorf("got %#v, expected ErrStatus with not-Permanent", err))
615 }
616 })
617
618 // Server temporarily refuses RCPT TO.
619 run(t, func(s xserver) {
620 s.writeline("220 mox.example")
621 s.readline("EHLO")
622 s.writeline("250 mox.example")
623 s.readline("MAIL FROM:")
624 s.writeline("250 ok")
625 s.readline("RCPT TO:")
626 s.writeline("451")
627 }, func(conn net.Conn) {
628 c, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
629 if err != nil {
630 panic(err)
631 }
632 msg := ""
633 err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
634 var xerr Error
635 if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || xerr.Permanent {
636 panic(fmt.Errorf("got %#v, expected ErrStatus with not-Permanent", err))
637 }
638 })
639
640 // Server permanently refuses DATA.
641 run(t, func(s xserver) {
642 s.writeline("220 mox.example")
643 s.readline("EHLO")
644 s.writeline("250 mox.example")
645 s.readline("MAIL FROM:")
646 s.writeline("250 ok")
647 s.readline("RCPT TO:")
648 s.writeline("250 ok")
649 s.readline("DATA")
650 s.writeline("550 no!")
651 }, func(conn net.Conn) {
652 c, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
653 if err != nil {
654 panic(err)
655 }
656 msg := ""
657 err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
658 var xerr Error
659 if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || !xerr.Permanent {
660 panic(fmt.Errorf("got %#v, expected ErrStatus with Permanent", err))
661 }
662 })
663
664 // TLS is required, so we attempt it regardless of whether it is advertised.
665 run(t, func(s xserver) {
666 s.writeline("220 mox.example")
667 s.readline("EHLO")
668 s.writeline("250 mox.example")
669 s.readline("STARTTLS")
670 s.writeline("502 command not implemented")
671 }, func(conn net.Conn) {
672 _, err := New(ctx, log.Logger, conn, TLSRequiredStartTLS, true, localhost, dns.Domain{ASCII: "mox.example"}, Opts{})
673 var xerr Error
674 if err == nil || !errors.Is(err, ErrTLS) || !errors.As(err, &xerr) || !xerr.Permanent {
675 panic(fmt.Errorf("got %#v, expected ErrTLS with Permanent", err))
676 }
677 })
678
679 // If TLS is available, but we don't want to use it, client should skip it.
680 run(t, func(s xserver) {
681 s.writeline("220 mox.example")
682 s.readline("EHLO")
683 s.writeline("250-mox.example")
684 s.writeline("250 STARTTLS")
685 s.readline("MAIL FROM:")
686 s.writeline("451 enough")
687 }, func(conn net.Conn) {
688 c, err := New(ctx, log.Logger, conn, TLSSkip, false, localhost, dns.Domain{ASCII: "mox.example"}, Opts{})
689 if err != nil {
690 panic(err)
691 }
692 msg := ""
693 err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
694 var xerr Error
695 if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || xerr.Permanent {
696 panic(fmt.Errorf("got %#v, expected ErrStatus with non-Permanent", err))
697 }
698 })
699
700 // A transaction is aborted. If we try another one, we should send a RSET.
701 run(t, func(s xserver) {
702 s.writeline("220 mox.example")
703 s.readline("EHLO")
704 s.writeline("250 mox.example")
705 s.readline("MAIL FROM:")
706 s.writeline("250 ok")
707 s.readline("RCPT TO:")
708 s.writeline("451 not now")
709 s.readline("RSET")
710 s.writeline("250 ok")
711 s.readline("MAIL FROM:")
712 s.writeline("250 ok")
713 s.readline("RCPT TO:")
714 s.writeline("250 ok")
715 s.readline("DATA")
716 s.writeline("550 not now")
717 }, func(conn net.Conn) {
718 c, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
719 if err != nil {
720 panic(err)
721 }
722
723 msg := ""
724 err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
725 var xerr Error
726 if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || xerr.Permanent {
727 panic(fmt.Errorf("got %#v, expected ErrStatus with non-Permanent", err))
728 }
729
730 // Another delivery.
731 err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
732 if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || !xerr.Permanent {
733 panic(fmt.Errorf("got %#v, expected ErrStatus with Permanent", err))
734 }
735 })
736
737 // Remote closes connection after 550 response to MAIL FROM in pipelined
738 // connection. Should result in permanent error, not temporary read error.
739 // E.g. outlook.com that has your IP blocklisted.
740 run(t, func(s xserver) {
741 s.writeline("220 mox.example")
742 s.readline("EHLO")
743 s.writeline("250-mox.example")
744 s.writeline("250 PIPELINING")
745 s.readline("MAIL FROM:")
746 s.writeline("550 ok")
747 }, func(conn net.Conn) {
748 c, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
749 if err != nil {
750 panic(err)
751 }
752
753 msg := ""
754 err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
755 var xerr Error
756 if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || !xerr.Permanent {
757 panic(fmt.Errorf("got %#v, expected ErrStatus with Permanent", err))
758 }
759 })
760
761 // Remote closes connection after 554 response to RCPT TO in pipelined
762 // connection. Should result in permanent error, not temporary read error.
763 // E.g. icloud.com that has your IP blocklisted.
764 run(t, func(s xserver) {
765 s.writeline("220 mox.example")
766 s.readline("EHLO")
767 s.writeline("250-mox.example")
768 s.writeline("250-ENHANCEDSTATUSCODES")
769 s.writeline("250 PIPELINING")
770 s.readline("MAIL FROM:")
771 s.writeline("250 2.1.0 ok")
772 s.readline("RCPT TO:")
773 s.writeline("554 5.7.0 Blocked")
774 }, func(conn net.Conn) {
775 c, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
776 if err != nil {
777 panic(err)
778 }
779
780 msg := ""
781 err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
782 var xerr Error
783 if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || !xerr.Permanent {
784 panic(fmt.Errorf("got %#v, expected ErrStatus with Permanent", err))
785 }
786 })
787
788 // If we try multiple recipients and first is 452, it is an error and a
789 // non-pipelined deliver will be aborted.
790 run(t, func(s xserver) {
791 s.writeline("220 mox.example")
792 s.readline("EHLO")
793 s.writeline("250 mox.example")
794 s.readline("MAIL FROM:")
795 s.writeline("250 ok")
796 s.readline("RCPT TO:")
797 s.writeline("451 not now")
798 s.readline("RCPT TO:")
799 s.writeline("451 not now")
800 s.readline("QUIT")
801 s.writeline("250 ok")
802 }, func(conn net.Conn) {
803 c, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
804 if err != nil {
805 panic(err)
806 }
807
808 msg := ""
809 _, err = c.DeliverMultiple(ctx, "postmaster@other.example", []string{"mjl@mox.example", "mjl@mox.example"}, int64(len(msg)), strings.NewReader(msg), false, false, false)
810 var xerr Error
811 if err == nil || !errors.Is(err, errNoRecipients) || !errors.As(err, &xerr) || xerr.Permanent {
812 panic(fmt.Errorf("got %#v (%s) expected errNoRecipients with non-Permanent", err, err))
813 }
814 c.Close()
815 })
816
817 // If we try multiple recipients and first is 452, it is an error and a pipelined
818 // deliver will abort an allowed DATA.
819 run(t, func(s xserver) {
820 s.writeline("220 mox.example")
821 s.readline("EHLO")
822 s.writeline("250-mox.example")
823 s.writeline("250 PIPELINING")
824 s.readline("MAIL FROM:")
825 s.writeline("250 ok")
826 s.readline("RCPT TO:")
827 s.writeline("451 not now")
828 s.readline("RCPT TO:")
829 s.writeline("451 not now")
830 s.readline("DATA")
831 s.writeline("354 ok")
832 s.readline(".")
833 s.writeline("503 no recipient")
834 s.readline("QUIT")
835 s.writeline("250 ok")
836 }, func(conn net.Conn) {
837 c, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
838 if err != nil {
839 panic(err)
840 }
841
842 msg := ""
843 _, err = c.DeliverMultiple(ctx, "postmaster@other.example", []string{"mjl@mox.example", "mjl@mox.example"}, int64(len(msg)), strings.NewReader(msg), false, false, false)
844 var xerr Error
845 if err == nil || !errors.Is(err, errNoRecipientsPipelined) || !errors.As(err, &xerr) || xerr.Permanent {
846 panic(fmt.Errorf("got %#v (%s), expected errNoRecipientsPipelined with non-Permanent", err, err))
847 }
848 c.Close()
849 })
850}
851
852type xserver struct {
853 conn net.Conn
854 br *bufio.Reader
855}
856
857func (s xserver) check(err error, msg string) {
858 if err != nil {
859 panic(fmt.Errorf("%s: %w", msg, err))
860 }
861}
862
863func (s xserver) errorf(format string, args ...any) {
864 panic(fmt.Errorf(format, args...))
865}
866
867func (s xserver) writeline(line string) {
868 _, err := fmt.Fprintf(s.conn, "%s\r\n", line)
869 s.check(err, "write")
870}
871
872func (s xserver) readline(prefix string) {
873 line, err := s.br.ReadString('\n')
874 s.check(err, "reading command")
875 if !strings.HasPrefix(strings.ToLower(line), strings.ToLower(prefix)) {
876 s.errorf("expected command %q, got: %s", prefix, line)
877 }
878}
879
880func run(t *testing.T, server func(s xserver), client func(conn net.Conn)) {
881 t.Helper()
882
883 result := make(chan error, 2)
884 clientConn, serverConn := net.Pipe()
885 go func() {
886 defer func() {
887 serverConn.Close()
888 x := recover()
889 if x != nil {
890 result <- fmt.Errorf("server: %v", x)
891 } else {
892 result <- nil
893 }
894 }()
895 server(xserver{serverConn, bufio.NewReader(serverConn)})
896 }()
897 go func() {
898 defer func() {
899 clientConn.Close()
900 x := recover()
901 if x != nil {
902 result <- fmt.Errorf("client: %v", x)
903 } else {
904 result <- nil
905 }
906 }()
907 client(clientConn)
908 }()
909 var errs []error
910 for i := 0; i < 2; i++ {
911 err := <-result
912 if err != nil {
913 errs = append(errs, err)
914 }
915 }
916 if errs != nil {
917 t.Fatalf("errors: %v", errs)
918 }
919}
920
921func TestLimits(t *testing.T) {
922 check := func(s string, expLimits map[string]string, expMailMax, expRcptMax, expRcptDomainMax int) {
923 t.Helper()
924 limits, mailmax, rcptMax, rcptDomainMax := parseLimits([]byte(s))
925 if !reflect.DeepEqual(limits, expLimits) || mailmax != expMailMax || rcptMax != expRcptMax || rcptDomainMax != expRcptDomainMax {
926 t.Errorf("bad limits, got %v %d %d %d, expected %v %d %d %d, for %q", limits, mailmax, rcptMax, rcptDomainMax, expLimits, expMailMax, expRcptMax, expRcptDomainMax, s)
927 }
928 }
929 check(" unknown=a=b -_1oK=xY", map[string]string{"UNKNOWN": "a=b", "-_1OK": "xY"}, 0, 0, 0)
930 check(" MAILMAX=123 OTHER=ignored RCPTDOMAINMAX=1 RCPTMAX=321", map[string]string{"MAILMAX": "123", "OTHER": "ignored", "RCPTDOMAINMAX": "1", "RCPTMAX": "321"}, 123, 321, 1)
931 check(" MAILMAX=invalid", map[string]string{"MAILMAX": "invalid"}, 0, 0, 0)
932 check(" invalid syntax", nil, 0, 0, 0)
933 check(" DUP=1 DUP=2", nil, 0, 0, 0)
934}
935
936// Just a cert that appears valid. SMTP client will not verify anything about it
937// (that is opportunistic TLS for you, "better some than none"). Let's enjoy this
938// one moment where it makes life easier.
939func fakeCert(t *testing.T, expired bool) tls.Certificate {
940 notAfter := time.Now()
941 if expired {
942 notAfter = notAfter.Add(-time.Hour)
943 } else {
944 notAfter = notAfter.Add(time.Hour)
945 }
946
947 privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
948 template := &x509.Certificate{
949 SerialNumber: big.NewInt(1), // Required field...
950 DNSNames: []string{"mox.example"},
951 NotBefore: time.Now().Add(-time.Hour),
952 NotAfter: notAfter,
953 }
954 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
955 if err != nil {
956 t.Fatalf("making certificate: %s", err)
957 }
958 cert, err := x509.ParseCertificate(localCertBuf)
959 if err != nil {
960 t.Fatalf("parsing generated certificate: %s", err)
961 }
962 c := tls.Certificate{
963 Certificate: [][]byte{localCertBuf},
964 PrivateKey: privKey,
965 Leaf: cert,
966 }
967 return c
968}
969