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