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