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