7 cryptorand "crypto/rand"
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"
32var zerohost dns.Domain
33var localhost = dns.Domain{ASCII: "localhost"}
35func TestClient(t *testing.T) {
36 ctx := context.Background()
37 log := mlog.New("smtpclient")
49 tlsHostname dns.Domain
52 auths []string // Allowed mechanisms.
54 nodeliver bool // For server, whether client will attempt a delivery.
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},
65 test := func(msg string, opts options, auths []sasl.Client, expClientErr, expDeliverErr, expServerErr error) {
68 if opts.tlsMode == "" {
69 opts.tlsMode = TLSOpportunistic
72 clientConn, serverConn := net.Pipe()
73 defer serverConn.Close()
75 result := make(chan error, 2)
80 if x != nil && x != "stop" {
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())) {
93 br := bufio.NewReader(serverConn)
94 readline := func(prefix string) string {
95 s, err := br.ReadString('\n')
97 fail("expected command: %v", err)
99 if !strings.HasPrefix(strings.ToLower(s), strings.ToLower(prefix)) {
100 fail("expected command %q, got: %s", prefix, s)
103 return strings.TrimSuffix(s, "\r\n")
105 writeline := func(s string) {
106 fmt.Fprintf(serverConn, "%s\r\n", s)
111 ehlo := true // Initially we expect EHLO.
116 writeline("250 mox.example")
123 // Client will try again with HELO.
124 writeline("500 bad syntax")
130 writeline("250-mox.example")
132 writeline("250-PIPELINING")
134 if opts.maxSize > 0 {
135 writeline(fmt.Sprintf("250-SIZE %d", opts.maxSize))
138 writeline("250-ENHANCEDSTATUSCODES")
140 if opts.starttls && !haveTLS {
141 writeline("250-STARTTLS")
143 if opts.eightbitmime {
144 writeline("250-8BITMIME")
147 writeline("250-SMTPUTF8")
149 if opts.auths != nil {
150 writeline("250-AUTH " + strings.Join(opts.auths, " "))
152 writeline("250 UNKNOWN") // To be ignored.
155 writeline("220 mox.example ESMTP test")
162 tlsConn := tls.Server(serverConn, &tlsConfig)
163 nctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
165 err := tlsConn.HandshakeContext(nctx)
167 fail("tls handshake: %w", err)
170 br = bufio.NewReader(serverConn)
176 if opts.auths != nil {
177 more := readline("AUTH ")
178 t := strings.SplitN(more, " ", 2)
181 writeline("235 2.7.0 auth ok")
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()
191 if t[0] == "SCRAM-SHA-1" {
193 iterations = 2 * 4096
198 saltedPassword := scram.SaltPassword(h, "test", salt, iterations)
200 clientFirst, err := base64.StdEncoding.DecodeString(t[1])
202 fail("bad base64: %w", err)
204 s, err := scram.NewServer(h, clientFirst)
206 fail("scram new server: %w", err)
208 serverFirst, err := s.ServerFirst(iterations, salt)
210 fail("scram server first: %w", err)
212 writeline("334 " + base64.StdEncoding.EncodeToString([]byte(serverFirst)))
214 xclientFinal := readline("")
215 clientFinal, err := base64.StdEncoding.DecodeString(xclientFinal)
217 fail("bad base64: %w", err)
219 serverFinal, err := s.Finish([]byte(clientFinal), saltedPassword)
221 fail("scram finish: %w", err)
223 writeline("334 " + base64.StdEncoding.EncodeToString([]byte(serverFinal)))
225 writeline("235 2.7.0 auth ok")
227 writeline("501 unknown mechanism")
231 if expClientErr == nil && !opts.nodeliver {
232 readline("MAIL FROM:")
237 writeline("354 continue")
238 reader := smtp.NewDataReader(br)
239 io.Copy(io.Discard, reader)
242 if expDeliverErr == nil {
246 readline("MAIL FROM:")
251 writeline("354 continue")
252 reader = smtp.NewDataReader(br)
253 io.Copy(io.Discard, reader)
266 if x != nil && x != "stop" {
270 fail := func(format string, args ...any) {
271 result <- fmt.Errorf("client: %w", fmt.Errorf(format, args...))
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)
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)
289 fail("reset: %v", err)
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)
298 fail("close client: %v", err)
304 for i := 0; i < 2; i++ {
307 errs = append(errs, err)
315 msg := strings.ReplaceAll(`From: <postmaster@mox.example>
331 tlsMode: TLSStrictStartTLS,
332 tlsHostname: dns.Domain{ASCII: "mox.example"},
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
350 // Set an expired certificate. For non-strict TLS, we should still accept it.
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},
358 test(msg, options{ehlo: true, starttls: true}, nil, nil, nil, nil)
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},
365 test(msg, options{ehlo: true, starttls: true}, nil, nil, nil, nil)
368func TestErrors(t *testing.T) {
369 ctx := context.Background()
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)
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))
383 // Server just closes connection.
384 run(t, func(s xserver) {
386 }, func(conn net.Conn) {
387 _, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil)
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))
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)
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))
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)
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))
416 // Server sends multiline response, but with different codes.
417 run(t, func(s xserver) {
418 s.writeline("220 mox.example")
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)
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))
430 // Server permanently refuses MAIL FROM.
431 run(t, func(s xserver) {
432 s.writeline("220 mox.example")
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)
444 err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false)
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))
451 // Server temporarily refuses MAIL FROM.
452 run(t, func(s xserver) {
453 s.writeline("220 mox.example")
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)
464 err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false)
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))
471 // Server temporarily refuses RCPT TO.
472 run(t, func(s xserver) {
473 s.writeline("220 mox.example")
475 s.writeline("250 mox.example")
476 s.readline("MAIL FROM:")
477 s.writeline("250 ok")
478 s.readline("RCPT TO:")
480 }, func(conn net.Conn) {
481 c, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil)
486 err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false)
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))
493 // Server permanently refuses DATA.
494 run(t, func(s xserver) {
495 s.writeline("220 mox.example")
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")
503 s.writeline("550 no!")
504 }, func(conn net.Conn) {
505 c, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil)
510 err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false)
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))
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")
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)
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))
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")
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)
546 err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false)
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))
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")
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")
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")
569 s.writeline("550 not now")
570 }, func(conn net.Conn) {
571 c, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil)
577 err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false)
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))
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))
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")
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)
607 err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false)
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))
620func (s xserver) check(err error, msg string) {
622 panic(fmt.Errorf("%s: %w", msg, err))
626func (s xserver) errorf(format string, args ...any) {
627 panic(fmt.Errorf(format, args...))
630func (s xserver) writeline(line string) {
631 _, err := fmt.Fprintf(s.conn, "%s\r\n", line)
632 s.check(err, "write")
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)
643func run(t *testing.T, server func(s xserver), client func(conn net.Conn)) {
646 result := make(chan error, 2)
647 clientConn, serverConn := net.Pipe()
653 result <- fmt.Errorf("server: %v", x)
658 server(xserver{serverConn, bufio.NewReader(serverConn)})
665 result <- fmt.Errorf("client: %v", x)
673 for i := 0; i < 2; i++ {
676 errs = append(errs, err)
680 t.Fatalf("errors: %v", errs)
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()
690 notAfter = notAfter.Add(-time.Hour)
692 notAfter = notAfter.Add(time.Hour)
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),
702 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
704 t.Fatalf("making certificate: %s", err)
706 cert, err := x509.ParseCertificate(localCertBuf)
708 t.Fatalf("parsing generated certificate: %s", err)
710 c := tls.Certificate{
711 Certificate: [][]byte{localCertBuf},