7	cryptorand "crypto/rand"
 
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"
 
32var zerohost dns.Domain
 
33var localhost = dns.Domain{ASCII: "localhost"}
 
35func TestClient(t *testing.T) {
 
36	ctx := context.Background()
 
37	log := mlog.New("smtpclient", nil)
 
39	mlog.SetConfig(map[string]slog.Level{"": mlog.LevelTrace})
 
40	defer mlog.SetConfig(map[string]slog.Level{"": mlog.LevelDebug})
 
52		auths        []string // Allowed mechanisms.
 
54		nodeliver bool // For server, whether client will attempt a delivery.
 
60		tlsHostname     dns.Domain
 
64		recipients      []string   // If nil, mjl@mox.example is used.
 
65		resps           []Response // Checked only if non-nil.
 
68	// Make fake cert, and make it trusted.
 
69	cert := fakeCert(t, false)
 
70	roots := x509.NewCertPool()
 
71	roots.AddCert(cert.Leaf)
 
72	tlsConfig := tls.Config{
 
73		Certificates: []tls.Certificate{cert},
 
76	cleanupResp := func(resps []Response) []Response {
 
77		for i, r := range resps {
 
78			resps[i] = Response{Code: r.Code, Secode: r.Secode}
 
83	test := func(msg string, opts options, auth func(l []string, cs *tls.ConnectionState) (sasl.Client, error), expClientErr, expDeliverErr, expServerErr error) {
 
86		if opts.tlsMode == "" {
 
87			opts.tlsMode = TLSOpportunistic
 
90		clientConn, serverConn := net.Pipe()
 
91		defer serverConn.Close()
 
93		result := make(chan error, 2)
 
98				if x != nil && x != "stop" {
 
102			fail := func(format string, args ...any) {
 
103				err := fmt.Errorf("server: %w", fmt.Errorf(format, args...))
 
104				log.Errorx("failure", err)
 
105				if err != nil && expServerErr != nil && (errors.Is(err, expServerErr) || errors.As(err, reflect.New(reflect.ValueOf(expServerErr).Type()).Interface())) {
 
112			br := bufio.NewReader(serverConn)
 
113			readline := func(prefix string) string {
 
114				s, err := br.ReadString('\n')
 
116					fail("expected command: %v", err)
 
118				if !strings.HasPrefix(strings.ToLower(s), strings.ToLower(prefix)) {
 
119					fail("expected command %q, got: %s", prefix, s)
 
122				return strings.TrimSuffix(s, "\r\n")
 
124			writeline := func(s string) {
 
125				fmt.Fprintf(serverConn, "%s\r\n", s)
 
130			ehlo := true // Initially we expect EHLO.
 
135					writeline("250 mox.example")
 
142					// Client will try again with HELO.
 
143					writeline("500 bad syntax")
 
149				writeline("250-mox.example")
 
151					writeline("250-PIPELINING")
 
153				if opts.maxSize > 0 {
 
154					writeline(fmt.Sprintf("250-SIZE %d", opts.maxSize))
 
157					writeline("250-ENHANCEDSTATUSCODES")
 
159				if opts.starttls && !haveTLS {
 
160					writeline("250-STARTTLS")
 
162				if opts.eightbitmime {
 
163					writeline("250-8BITMIME")
 
166					writeline("250-SMTPUTF8")
 
168				if opts.requiretls && haveTLS {
 
169					writeline("250-REQUIRETLS")
 
171				if opts.auths != nil {
 
172					writeline("250-AUTH " + strings.Join(opts.auths, " "))
 
174				writeline("250-LIMITS MAILMAX=10 RCPTMAX=100 RCPTDOMAINMAX=1")
 
175				writeline("250 UNKNOWN") // To be ignored.
 
178			writeline("220 mox.example ESMTP test")
 
185				tlsConn := tls.Server(serverConn, &tlsConfig)
 
186				nctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
 
188				err := tlsConn.HandshakeContext(nctx)
 
190					fail("tls handshake: %w", err)
 
193				br = bufio.NewReader(serverConn)
 
199			if opts.auths != nil {
 
200				more := readline("AUTH ")
 
201				t := strings.SplitN(more, " ", 2)
 
204					writeline("235 2.7.0 auth ok")
 
206					writeline("334 " + base64.StdEncoding.EncodeToString([]byte("<123.1234@host>")))
 
207					readline("") // Proof
 
208					writeline("235 2.7.0 auth ok")
 
209				case "SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1":
 
210					// Cannot fake/hardcode scram interactions.
 
211					var h func() hash.Hash
 
212					salt := scram.MakeRandom()
 
215					case "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1":
 
217						iterations = 2 * 4096
 
218					case "SCRAM-SHA-256-PLUS", "SCRAM-SHA-256":
 
222						panic("missing case for scram")
 
224					var cs *tls.ConnectionState
 
225					if strings.HasSuffix(t[0], "-PLUS") {
 
227							writeline("501 scram plus without tls not possible")
 
233						xcs := serverConn.(*tls.Conn).ConnectionState()
 
236					saltedPassword := scram.SaltPassword(h, "test", salt, iterations)
 
238					clientFirst, err := base64.StdEncoding.DecodeString(t[1])
 
240						fail("bad base64: %w", err)
 
242					s, err := scram.NewServer(h, clientFirst, cs, cs != nil)
 
244						fail("scram new server: %w", err)
 
246					serverFirst, err := s.ServerFirst(iterations, salt)
 
248						fail("scram server first: %w", err)
 
250					writeline("334 " + base64.StdEncoding.EncodeToString([]byte(serverFirst)))
 
252					xclientFinal := readline("")
 
253					clientFinal, err := base64.StdEncoding.DecodeString(xclientFinal)
 
255						fail("bad base64: %w", err)
 
257					serverFinal, err := s.Finish([]byte(clientFinal), saltedPassword)
 
259						fail("scram finish: %w", err)
 
261					writeline("334 " + base64.StdEncoding.EncodeToString([]byte(serverFinal)))
 
263					writeline("235 2.7.0 auth ok")
 
265					writeline("501 unknown mechanism")
 
269			if expClientErr == nil && !opts.nodeliver {
 
270				readline("MAIL FROM:")
 
272				n := len(opts.recipients)
 
279					if i < len(opts.resps) {
 
280						resp = fmt.Sprintf("%d maybe", opts.resps[i].Code)
 
285				writeline("354 continue")
 
286				reader := smtp.NewDataReader(br)
 
287				io.Copy(io.Discard, reader)
 
290				if expDeliverErr == nil {
 
294					readline("MAIL FROM:")
 
299						if i < len(opts.resps) {
 
300							resp = fmt.Sprintf("%d maybe", opts.resps[i].Code)
 
305					writeline("354 continue")
 
306					reader = smtp.NewDataReader(br)
 
307					io.Copy(io.Discard, reader)
 
317		// todo: should abort tests more properly. on client failures, we may be left with hanging test.
 
321				if x != nil && x != "stop" {
 
325			fail := func(format string, args ...any) {
 
326				err := fmt.Errorf("client: %w", fmt.Errorf(format, args...))
 
327				log.Errorx("failure", err)
 
331			client, err := New(ctx, log.Logger, clientConn, opts.tlsMode, opts.tlsPKIX, localhost, opts.tlsHostname, Opts{Auth: auth, RootCAs: opts.roots})
 
332			if (err == nil) != (expClientErr == nil) || err != nil && !errors.As(err, reflect.New(reflect.ValueOf(expClientErr).Type()).Interface()) && !errors.Is(err, expClientErr) {
 
333				fail("new client: got err %v, expected %#v", err, expClientErr)
 
339			rcptTo := opts.recipients
 
340			if len(rcptTo) == 0 {
 
341				rcptTo = []string{"mjl@mox.example"}
 
343			resps, err := client.DeliverMultiple(ctx, "postmaster@mox.example", rcptTo, int64(len(msg)), strings.NewReader(msg), opts.need8bitmime, opts.needsmtputf8, opts.needsrequiretls)
 
344			if (err == nil) != (expDeliverErr == nil) || err != nil && !errors.Is(err, expDeliverErr) && !reflect.DeepEqual(err, expDeliverErr) {
 
345				fail("first deliver: got err %#v (%s), expected %#v (%s)", err, err, expDeliverErr, expDeliverErr)
 
346			} else if opts.resps != nil && !reflect.DeepEqual(cleanupResp(resps), opts.resps) {
 
347				fail("first deliver: got resps %v, expected %v", resps, opts.resps)
 
352					fail("reset: %v", err)
 
354				resps, err = client.DeliverMultiple(ctx, "postmaster@mox.example", rcptTo, int64(len(msg)), strings.NewReader(msg), opts.need8bitmime, opts.needsmtputf8, opts.needsrequiretls)
 
355				if (err == nil) != (expDeliverErr == nil) || err != nil && !errors.Is(err, expDeliverErr) && !reflect.DeepEqual(err, expDeliverErr) {
 
356					fail("second deliver: got err %#v (%s), expected %#v (%s)", err, err, expDeliverErr, expDeliverErr)
 
357				} else if opts.resps != nil && !reflect.DeepEqual(cleanupResp(resps), opts.resps) {
 
358					fail("second: got resps %v, expected %v", resps, opts.resps)
 
363				fail("close client: %v", err)
 
372				errs = append(errs, err)
 
380	msg := strings.ReplaceAll(`From: <postmaster@mox.example>
 
397		tlsMode:         TLSRequiredStartTLS,
 
400		tlsHostname:     dns.Domain{ASCII: "mox.example"},
 
403		needsrequiretls: true,
 
406	test(msg, options{}, nil, nil, nil, nil)
 
407	test(msg, allopts, nil, nil, nil, nil)
 
408	test(msg, options{ehlo: true, eightbitmime: true}, nil, nil, nil, nil)
 
409	test(msg, options{ehlo: true, eightbitmime: false, need8bitmime: true, nodeliver: true}, nil, nil, Err8bitmimeUnsupported, nil)
 
410	test(msg, options{ehlo: true, smtputf8: false, needsmtputf8: true, nodeliver: true}, nil, nil, ErrSMTPUTF8Unsupported, nil)
 
412	// Server TLS handshake is a net.OpError with "remote error" as text.
 
413	test(msg, options{ehlo: true, starttls: true, tlsMode: TLSRequiredStartTLS, tlsPKIX: true, tlsHostname: dns.Domain{ASCII: "mismatch.example"}, nodeliver: true}, nil, ErrTLS, nil, &net.OpError{})
 
415	test(msg, options{ehlo: true, maxSize: len(msg) - 1, nodeliver: true}, nil, nil, ErrSize, nil)
 
417	// Multiple recipients, not pipelined.
 
422		recipients: []string{"mjl@mox.example", "mjl2@mox.example", "mjl3@mox.example"},
 
424			{Code: smtp.C250Completed},
 
425			{Code: smtp.C250Completed},
 
426			{Code: smtp.C250Completed},
 
429	test(msg, multi1, nil, nil, nil, nil)
 
430	multi1.pipelining = true
 
431	test(msg, multi1, nil, nil, nil, nil)
 
433	// Multiple recipients with 452 and other error, not pipelined
 
437		recipients: []string{"xmjl@mox.example", "xmjl2@mox.example", "xmjl3@mox.example"},
 
439			{Code: smtp.C250Completed},
 
440			{Code: smtp.C554TransactionFailed}, // Will continue when not pipelined.
 
441			{Code: smtp.C452StorageFull},       // Will stop sending further recipients.
 
444	test(msg, multi2, nil, nil, nil, nil)
 
445	multi2.pipelining = true
 
446	test(msg, multi2, nil, nil, nil, nil)
 
447	multi2.pipelining = false
 
448	multi2.resps[2].Code = smtp.C552MailboxFull
 
449	test(msg, multi2, nil, nil, nil, nil)
 
450	multi2.pipelining = true
 
451	test(msg, multi2, nil, nil, nil, nil)
 
453	// Single recipient with error and pipelining is an error.
 
458		recipients: []string{"xmjl@mox.example"},
 
459		resps:      []Response{{Code: smtp.C452StorageFull}},
 
461	test(msg, multi3, nil, nil, Error{Code: smtp.C452StorageFull, Command: "rcptto", Line: "452 maybe"}, nil)
 
463	authPlain := func(l []string, cs *tls.ConnectionState) (sasl.Client, error) {
 
464		return sasl.NewClientPlain("test", "test"), nil
 
466	test(msg, options{ehlo: true, auths: []string{"PLAIN"}}, authPlain, nil, nil, nil)
 
468	authCRAMMD5 := func(l []string, cs *tls.ConnectionState) (sasl.Client, error) {
 
469		return sasl.NewClientCRAMMD5("test", "test"), nil
 
471	test(msg, options{ehlo: true, auths: []string{"CRAM-MD5"}}, authCRAMMD5, nil, nil, nil)
 
473	// todo: add tests for failing authentication, also at various stages in SCRAM
 
475	authSCRAMSHA1 := func(l []string, cs *tls.ConnectionState) (sasl.Client, error) {
 
476		return sasl.NewClientSCRAMSHA1("test", "test", false), nil
 
478	test(msg, options{ehlo: true, auths: []string{"SCRAM-SHA-1"}}, authSCRAMSHA1, nil, nil, nil)
 
480	authSCRAMSHA1PLUS := func(l []string, cs *tls.ConnectionState) (sasl.Client, error) {
 
481		return sasl.NewClientSCRAMSHA1PLUS("test", "test", *cs), nil
 
483	test(msg, options{ehlo: true, starttls: true, auths: []string{"SCRAM-SHA-1-PLUS"}}, authSCRAMSHA1PLUS, nil, nil, nil)
 
485	authSCRAMSHA256 := func(l []string, cs *tls.ConnectionState) (sasl.Client, error) {
 
486		return sasl.NewClientSCRAMSHA256("test", "test", false), nil
 
488	test(msg, options{ehlo: true, auths: []string{"SCRAM-SHA-256"}}, authSCRAMSHA256, nil, nil, nil)
 
490	authSCRAMSHA256PLUS := func(l []string, cs *tls.ConnectionState) (sasl.Client, error) {
 
491		return sasl.NewClientSCRAMSHA256PLUS("test", "test", *cs), nil
 
493	test(msg, options{ehlo: true, starttls: true, auths: []string{"SCRAM-SHA-256-PLUS"}}, authSCRAMSHA256PLUS, nil, nil, nil)
 
495	test(msg, options{ehlo: true, requiretls: false, needsrequiretls: true, nodeliver: true}, nil, nil, ErrRequireTLSUnsupported, nil)
 
497	// Set an expired certificate. For non-strict TLS, we should still accept it.
 
499	cert = fakeCert(t, true)
 
500	roots = x509.NewCertPool()
 
501	roots.AddCert(cert.Leaf)
 
502	tlsConfig = tls.Config{
 
503		Certificates: []tls.Certificate{cert},
 
505	test(msg, options{ehlo: true, starttls: true, roots: roots}, nil, nil, nil, nil)
 
507	// Again with empty cert pool so it isn't trusted in any way.
 
508	roots = x509.NewCertPool()
 
509	tlsConfig = tls.Config{
 
510		Certificates: []tls.Certificate{cert},
 
512	test(msg, options{ehlo: true, starttls: true, roots: roots}, nil, nil, nil, nil)
 
515func TestErrors(t *testing.T) {
 
516	ctx := context.Background()
 
517	log := mlog.New("smtpclient", nil)
 
520	run(t, func(s xserver) {
 
521		s.writeline("bogus") // Invalid, should be "220 <hostname>".
 
522	}, func(conn net.Conn) {
 
523		_, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
 
525		if err == nil || !errors.Is(err, ErrProtocol) || !errors.As(err, &xerr) || xerr.Permanent {
 
526			panic(fmt.Errorf("got %#v, expected ErrProtocol without Permanent", err))
 
530	// Server just closes connection.
 
531	run(t, func(s xserver) {
 
533	}, func(conn net.Conn) {
 
534		_, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
 
536		if err == nil || !errors.Is(err, io.ErrUnexpectedEOF) || !errors.As(err, &xerr) || xerr.Permanent {
 
537			panic(fmt.Errorf("got %#v (%v), expected ErrUnexpectedEOF without Permanent", err, err))
 
541	// Server does not want to speak SMTP.
 
542	run(t, func(s xserver) {
 
543		s.writeline("521 not accepting connections")
 
544	}, func(conn net.Conn) {
 
545		_, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
 
547		if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || !xerr.Permanent {
 
548			panic(fmt.Errorf("got %#v, expected ErrStatus with Permanent", err))
 
552	// Server has invalid code in greeting.
 
553	run(t, func(s xserver) {
 
554		s.writeline("2200 mox.example") // Invalid, too many digits.
 
555	}, func(conn net.Conn) {
 
556		_, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
 
558		if err == nil || !errors.Is(err, ErrProtocol) || !errors.As(err, &xerr) || xerr.Permanent {
 
559			panic(fmt.Errorf("got %#v, expected ErrProtocol without Permanent", err))
 
563	// Server sends multiline response, but with different codes.
 
564	run(t, func(s xserver) {
 
565		s.writeline("220 mox.example")
 
567		s.writeline("250-mox.example")
 
568		s.writeline("500 different code") // Invalid.
 
569	}, func(conn net.Conn) {
 
570		_, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
 
572		if err == nil || !errors.Is(err, ErrProtocol) || !errors.As(err, &xerr) || xerr.Permanent {
 
573			panic(fmt.Errorf("got %#v, expected ErrProtocol without Permanent", err))
 
577	// Server permanently refuses MAIL FROM.
 
578	run(t, func(s xserver) {
 
579		s.writeline("220 mox.example")
 
581		s.writeline("250-mox.example")
 
582		s.writeline("250 ENHANCEDSTATUSCODES")
 
583		s.readline("MAIL FROM:")
 
584		s.writeline("550 5.7.0 not allowed")
 
585	}, func(conn net.Conn) {
 
586		c, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
 
591		err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
 
593		if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || !xerr.Permanent {
 
594			panic(fmt.Errorf("got %#v, expected ErrStatus with Permanent", err))
 
598	// Server temporarily refuses MAIL FROM.
 
599	run(t, func(s xserver) {
 
600		s.writeline("220 mox.example")
 
602		s.writeline("250 mox.example")
 
603		s.readline("MAIL FROM:")
 
604		s.writeline("451 bad sender")
 
605	}, func(conn net.Conn) {
 
606		c, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
 
611		err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
 
613		if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || xerr.Permanent {
 
614			panic(fmt.Errorf("got %#v, expected ErrStatus with not-Permanent", err))
 
618	// Server temporarily refuses RCPT TO.
 
619	run(t, func(s xserver) {
 
620		s.writeline("220 mox.example")
 
622		s.writeline("250 mox.example")
 
623		s.readline("MAIL FROM:")
 
624		s.writeline("250 ok")
 
625		s.readline("RCPT TO:")
 
627	}, func(conn net.Conn) {
 
628		c, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
 
633		err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
 
635		if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || xerr.Permanent {
 
636			panic(fmt.Errorf("got %#v, expected ErrStatus with not-Permanent", err))
 
640	// Server permanently refuses DATA.
 
641	run(t, func(s xserver) {
 
642		s.writeline("220 mox.example")
 
644		s.writeline("250 mox.example")
 
645		s.readline("MAIL FROM:")
 
646		s.writeline("250 ok")
 
647		s.readline("RCPT TO:")
 
648		s.writeline("250 ok")
 
650		s.writeline("550 no!")
 
651	}, func(conn net.Conn) {
 
652		c, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
 
657		err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
 
659		if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || !xerr.Permanent {
 
660			panic(fmt.Errorf("got %#v, expected ErrStatus with Permanent", err))
 
664	// TLS is required, so we attempt it regardless of whether it is advertised.
 
665	run(t, func(s xserver) {
 
666		s.writeline("220 mox.example")
 
668		s.writeline("250 mox.example")
 
669		s.readline("STARTTLS")
 
670		s.writeline("502 command not implemented")
 
671	}, func(conn net.Conn) {
 
672		_, err := New(ctx, log.Logger, conn, TLSRequiredStartTLS, true, localhost, dns.Domain{ASCII: "mox.example"}, Opts{})
 
674		if err == nil || !errors.Is(err, ErrTLS) || !errors.As(err, &xerr) || !xerr.Permanent {
 
675			panic(fmt.Errorf("got %#v, expected ErrTLS with Permanent", err))
 
679	// If TLS is available, but we don't want to use it, client should skip it.
 
680	run(t, func(s xserver) {
 
681		s.writeline("220 mox.example")
 
683		s.writeline("250-mox.example")
 
684		s.writeline("250 STARTTLS")
 
685		s.readline("MAIL FROM:")
 
686		s.writeline("451 enough")
 
687	}, func(conn net.Conn) {
 
688		c, err := New(ctx, log.Logger, conn, TLSSkip, false, localhost, dns.Domain{ASCII: "mox.example"}, Opts{})
 
693		err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
 
695		if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || xerr.Permanent {
 
696			panic(fmt.Errorf("got %#v, expected ErrStatus with non-Permanent", err))
 
700	// A transaction is aborted. If we try another one, we should send a RSET.
 
701	run(t, func(s xserver) {
 
702		s.writeline("220 mox.example")
 
704		s.writeline("250 mox.example")
 
705		s.readline("MAIL FROM:")
 
706		s.writeline("250 ok")
 
707		s.readline("RCPT TO:")
 
708		s.writeline("451 not now")
 
710		s.writeline("250 ok")
 
711		s.readline("MAIL FROM:")
 
712		s.writeline("250 ok")
 
713		s.readline("RCPT TO:")
 
714		s.writeline("250 ok")
 
716		s.writeline("550 not now")
 
717	}, func(conn net.Conn) {
 
718		c, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
 
724		err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
 
726		if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || xerr.Permanent {
 
727			panic(fmt.Errorf("got %#v, expected ErrStatus with non-Permanent", err))
 
731		err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
 
732		if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || !xerr.Permanent {
 
733			panic(fmt.Errorf("got %#v, expected ErrStatus with Permanent", err))
 
737	// Remote closes connection after 550 response to MAIL FROM in pipelined
 
738	// connection. Should result in permanent error, not temporary read error.
 
739	// E.g. outlook.com that has your IP blocklisted.
 
740	run(t, func(s xserver) {
 
741		s.writeline("220 mox.example")
 
743		s.writeline("250-mox.example")
 
744		s.writeline("250 PIPELINING")
 
745		s.readline("MAIL FROM:")
 
746		s.writeline("550 ok")
 
747	}, func(conn net.Conn) {
 
748		c, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
 
754		err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
 
756		if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || !xerr.Permanent {
 
757			panic(fmt.Errorf("got %#v, expected ErrStatus with Permanent", err))
 
761	// Remote closes connection after 554 response to RCPT TO in pipelined
 
762	// connection. Should result in permanent error, not temporary read error.
 
763	// E.g. icloud.com that has your IP blocklisted.
 
764	run(t, func(s xserver) {
 
765		s.writeline("220 mox.example")
 
767		s.writeline("250-mox.example")
 
768		s.writeline("250-ENHANCEDSTATUSCODES")
 
769		s.writeline("250 PIPELINING")
 
770		s.readline("MAIL FROM:")
 
771		s.writeline("250 2.1.0 ok")
 
772		s.readline("RCPT TO:")
 
773		s.writeline("554 5.7.0 Blocked")
 
774	}, func(conn net.Conn) {
 
775		c, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
 
781		err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
 
783		if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || !xerr.Permanent {
 
784			panic(fmt.Errorf("got %#v, expected ErrStatus with Permanent", err))
 
788	// If we try multiple recipients and first is 452, it is an error and a
 
789	// non-pipelined deliver will be aborted.
 
790	run(t, func(s xserver) {
 
791		s.writeline("220 mox.example")
 
793		s.writeline("250 mox.example")
 
794		s.readline("MAIL FROM:")
 
795		s.writeline("250 ok")
 
796		s.readline("RCPT TO:")
 
797		s.writeline("451 not now")
 
798		s.readline("RCPT TO:")
 
799		s.writeline("451 not now")
 
801		s.writeline("250 ok")
 
802	}, func(conn net.Conn) {
 
803		c, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
 
809		_, err = c.DeliverMultiple(ctx, "postmaster@other.example", []string{"mjl@mox.example", "mjl@mox.example"}, int64(len(msg)), strings.NewReader(msg), false, false, false)
 
811		if err == nil || !errors.Is(err, errNoRecipients) || !errors.As(err, &xerr) || xerr.Permanent {
 
812			panic(fmt.Errorf("got %#v (%s) expected errNoRecipients with non-Permanent", err, err))
 
817	// If we try multiple recipients and first is 452, it is an error and a pipelined
 
818	// deliver will abort an allowed DATA.
 
819	run(t, func(s xserver) {
 
820		s.writeline("220 mox.example")
 
822		s.writeline("250-mox.example")
 
823		s.writeline("250 PIPELINING")
 
824		s.readline("MAIL FROM:")
 
825		s.writeline("250 ok")
 
826		s.readline("RCPT TO:")
 
827		s.writeline("451 not now")
 
828		s.readline("RCPT TO:")
 
829		s.writeline("451 not now")
 
831		s.writeline("354 ok")
 
833		s.writeline("503 no recipient")
 
835		s.writeline("250 ok")
 
836	}, func(conn net.Conn) {
 
837		c, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
 
843		_, err = c.DeliverMultiple(ctx, "postmaster@other.example", []string{"mjl@mox.example", "mjl@mox.example"}, int64(len(msg)), strings.NewReader(msg), false, false, false)
 
845		if err == nil || !errors.Is(err, errNoRecipientsPipelined) || !errors.As(err, &xerr) || xerr.Permanent {
 
846			panic(fmt.Errorf("got %#v (%s), expected errNoRecipientsPipelined with non-Permanent", err, err))
 
857func (s xserver) check(err error, msg string) {
 
859		panic(fmt.Errorf("%s: %w", msg, err))
 
863func (s xserver) errorf(format string, args ...any) {
 
864	panic(fmt.Errorf(format, args...))
 
867func (s xserver) writeline(line string) {
 
868	_, err := fmt.Fprintf(s.conn, "%s\r\n", line)
 
869	s.check(err, "write")
 
872func (s xserver) readline(prefix string) {
 
873	line, err := s.br.ReadString('\n')
 
874	s.check(err, "reading command")
 
875	if !strings.HasPrefix(strings.ToLower(line), strings.ToLower(prefix)) {
 
876		s.errorf("expected command %q, got: %s", prefix, line)
 
880func run(t *testing.T, server func(s xserver), client func(conn net.Conn)) {
 
883	result := make(chan error, 2)
 
884	clientConn, serverConn := net.Pipe()
 
890				result <- fmt.Errorf("server: %v", x)
 
895		server(xserver{serverConn, bufio.NewReader(serverConn)})
 
902				result <- fmt.Errorf("client: %v", x)
 
913			errs = append(errs, err)
 
917		t.Fatalf("errors: %v", errs)
 
921func TestLimits(t *testing.T) {
 
922	check := func(s string, expLimits map[string]string, expMailMax, expRcptMax, expRcptDomainMax int) {
 
924		limits, mailmax, rcptMax, rcptDomainMax := parseLimits([]byte(s))
 
925		if !reflect.DeepEqual(limits, expLimits) || mailmax != expMailMax || rcptMax != expRcptMax || rcptDomainMax != expRcptDomainMax {
 
926			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)
 
929	check(" unknown=a=b -_1oK=xY", map[string]string{"UNKNOWN": "a=b", "-_1OK": "xY"}, 0, 0, 0)
 
930	check(" MAILMAX=123 OTHER=ignored RCPTDOMAINMAX=1 RCPTMAX=321", map[string]string{"MAILMAX": "123", "OTHER": "ignored", "RCPTDOMAINMAX": "1", "RCPTMAX": "321"}, 123, 321, 1)
 
931	check(" MAILMAX=invalid", map[string]string{"MAILMAX": "invalid"}, 0, 0, 0)
 
932	check(" invalid syntax", nil, 0, 0, 0)
 
933	check(" DUP=1 DUP=2", nil, 0, 0, 0)
 
936// Just a cert that appears valid. SMTP client will not verify anything about it
 
937// (that is opportunistic TLS for you, "better some than none"). Let's enjoy this
 
938// one moment where it makes life easier.
 
939func fakeCert(t *testing.T, expired bool) tls.Certificate {
 
940	notAfter := time.Now()
 
942		notAfter = notAfter.Add(-time.Hour)
 
944		notAfter = notAfter.Add(time.Hour)
 
947	privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
 
948	template := &x509.Certificate{
 
949		SerialNumber: big.NewInt(1), // Required field...
 
950		DNSNames:     []string{"mox.example"},
 
951		NotBefore:    time.Now().Add(-time.Hour),
 
954	localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
 
956		t.Fatalf("making certificate: %s", err)
 
958	cert, err := x509.ParseCertificate(localCertBuf)
 
960		t.Fatalf("parsing generated certificate: %s", err)
 
962	c := tls.Certificate{
 
963		Certificate: [][]byte{localCertBuf},