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