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