New Upstream Release - golang-github-emersion-go-smtp

Ready changes

Summary

Merged new upstream version: 0.16.0 (was: 0.15.0).

Resulting package

Built on 2023-05-16T11:55 (took 4m29s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases golang-github-emersion-go-smtp-dev

Diff

diff --git a/.build.yml b/.build.yml
index f9dfa06..46854b9 100644
--- a/.build.yml
+++ b/.build.yml
@@ -1,11 +1,10 @@
 image: alpine/edge
 packages:
   - go
-  # Required by codecov
-  - bash
-  - findutils
 sources:
   - https://github.com/emersion/go-smtp
+artifacts:
+  - coverage.html
 tasks:
   - build: |
       cd go-smtp
@@ -13,7 +12,6 @@ tasks:
   - test: |
       cd go-smtp
       go test -coverprofile=coverage.txt -covermode=atomic ./...
-  - upload-coverage: |
+  - coverage: |
       cd go-smtp
-      export CODECOV_TOKEN=3f257f71-a128-4834-8f68-2b534e9f4cb1
-      curl -s https://codecov.io/bash | bash
+      go tool cover -html=coverage.txt -o ~/coverage.html
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..271b7cf
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+  - name: Question
+    url: "https://web.libera.chat/gamja/#emersion"
+    about: "Please ask questions in #emersion on Libera Chat"
diff --git a/.github/ISSUE_TEMPLATE/issue_template.md b/.github/ISSUE_TEMPLATE/issue_template.md
new file mode 100644
index 0000000..5044dbf
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/issue_template.md
@@ -0,0 +1,12 @@
+---
+name: Bug report or feature request
+about: Report a bug or request a new feature
+---
+
+<!--
+
+Please read the following before submitting a new issue:
+
+Do NOT create GitHub issues if you have a question about go-smtp or about the SMTP protocol in general. Ask questions on IRC in #emersion on Libera Chat.
+
+-->
diff --git a/README.md b/README.md
old mode 100755
new mode 100644
index 7195af6..6992498
--- a/README.md
+++ b/README.md
@@ -1,8 +1,7 @@
 # go-smtp
 
-[![GoDoc](https://godoc.org/github.com/emersion/go-smtp?status.svg)](https://godoc.org/github.com/emersion/go-smtp)
-[![builds.sr.ht status](https://builds.sr.ht/~emersion/go-smtp.svg)](https://builds.sr.ht/~emersion/go-smtp?)
-[![codecov](https://codecov.io/gh/emersion/go-smtp/branch/master/graph/badge.svg)](https://codecov.io/gh/emersion/go-smtp)
+[![godocs.io](https://godocs.io/github.com/emersion/go-smtp?status.svg)](https://godocs.io/github.com/emersion/go-smtp)
+[![builds.sr.ht status](https://builds.sr.ht/~emersion/go-smtp/commits.svg)](https://builds.sr.ht/~emersion/go-smtp/commits?)
 
 An ESMTP client and server library written in Go.
 
@@ -29,7 +28,7 @@ import (
 )
 
 func main() {
-	// Set up authentication information.
+	// Setup authentication information.
 	auth := sasl.NewPlainClient("", "user@example.com", "password")
 
 	// Connect to the server, authenticate, set the sender and recipient,
@@ -46,7 +45,40 @@ func main() {
 }
 ```
 
-If you need more control, you can use `Client` instead.
+If you need more control, you can use `Client` instead. For example, if you
+want to send an email via a server without TLS or auth support, you can do
+something like this:
+
+```go
+package main
+
+import (
+	"log"
+	"strings"
+
+	"github.com/emersion/go-smtp"
+)
+
+func main() {
+	// Setup an unencrypted connection to a local mail server.
+	c, err := smtp.Dial("localhost:25")
+	if err != nil {
+		return err
+	}
+	defer c.Close()
+
+	// Set the sender and recipient, and send the email all in one step.
+	to := []string{"recipient@example.net"}
+	msg := strings.NewReader("To: recipient@example.net\r\n" +
+		"Subject: discount Gophers!\r\n" +
+		"\r\n" +
+		"This is the email body.\r\n")
+	err := c.SendMail("sender@example.org", to, msg)
+	if err != nil {
+		log.Fatal(err)
+	}
+}
+```
 
 ### Server
 
@@ -66,23 +98,21 @@ import (
 // The Backend implements SMTP server methods.
 type Backend struct{}
 
-// Login handles a login command with username and password.
-func (bkd *Backend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) {
-	if username != "username" || password != "password" {
-		return nil, errors.New("Invalid username or password")
-	}
+func (bkd *Backend) NewSession(_ *smtp.Conn) (smtp.Session, error) {
 	return &Session{}, nil
 }
 
-// AnonymousLogin requires clients to authenticate using SMTP AUTH before sending emails
-func (bkd *Backend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
-	return nil, smtp.ErrAuthRequired
-}
-
-// A Session is returned after successful login.
+// A Session is returned after EHLO.
 type Session struct{}
 
-func (s *Session) Mail(from string) error {
+func (s *Session) AuthPlain(username, password string) error {
+	if username != "username" || password != "password" {
+		return errors.New("Invalid username or password")
+	}
+	return nil
+}
+
+func (s *Session) Mail(from string, opts *smtp.MailOptions) error {
 	log.Println("Mail from:", from)
 	return nil
 }
@@ -140,6 +170,12 @@ Hey <3
 .
 ```
 
+## Relationship with net/smtp
+
+The Go standard library provides a SMTP client implementation in `net/smtp`.
+However `net/smtp` is frozen: it's not getting any new features. go-smtp
+provides a server implementation and a number of client improvements.
+
 ## Licence
 
 MIT
diff --git a/backend.go b/backend.go
index 75cce17..59cea3a 100644
--- a/backend.go
+++ b/backend.go
@@ -1,29 +1,41 @@
 package smtp
 
 import (
-	"errors"
 	"io"
 )
 
 var (
-	ErrAuthRequired    = errors.New("Please authenticate first")
-	ErrAuthUnsupported = errors.New("Authentication not supported")
+	ErrAuthRequired = &SMTPError{
+		Code:         502,
+		EnhancedCode: EnhancedCode{5, 7, 0},
+		Message:      "Please authenticate first",
+	}
+	ErrAuthUnsupported = &SMTPError{
+		Code:         502,
+		EnhancedCode: EnhancedCode{5, 7, 0},
+		Message:      "Authentication not supported",
+	}
 )
 
 // A SMTP server backend.
 type Backend interface {
-	// Authenticate a user. Return smtp.ErrAuthUnsupported if you don't want to
-	// support this.
-	Login(state *ConnectionState, username, password string) (Session, error)
-
-	// Called if the client attempts to send mail without logging in first.
-	// Return smtp.ErrAuthRequired if you don't want to support this.
-	AnonymousLogin(state *ConnectionState) (Session, error)
+	NewSession(c *Conn) (Session, error)
 }
 
+type BodyType string
+
+const (
+	Body7Bit       BodyType = "7BIT"
+	Body8BitMIME   BodyType = "8BITMIME"
+	BodyBinaryMIME BodyType = "BINARYMIME"
+)
+
 // MailOptions contains custom arguments that were
 // passed as an argument to the MAIL command.
 type MailOptions struct {
+	// Value of BODY= argument, 7BIT, 8BITMIME or BINARYMIME.
+	Body BodyType
+
 	// Size of the body. Can be 0 if not specified by client.
 	Size int
 
@@ -36,8 +48,20 @@ type MailOptions struct {
 	// The message envelope or message header contains UTF-8-encoded strings.
 	// This flag is set by SMTPUTF8-aware (RFC 6531) client.
 	UTF8 bool
+
+	// The authorization identity asserted by the message sender in decoded
+	// form with angle brackets stripped.
+	//
+	// nil value indicates missing AUTH, non-nil empty string indicates
+	// AUTH=<>.
+	//
+	// Defined in RFC 4954.
+	Auth *string
 }
 
+// Session is used by servers to respond to an SMTP client.
+//
+// The methods are called when the remote client issues the matching command.
 type Session interface {
 	// Discard currently processed message.
 	Reset()
@@ -45,14 +69,21 @@ type Session interface {
 	// Free all resources associated with session.
 	Logout() error
 
+	// Authenticate the user using SASL PLAIN.
+	AuthPlain(username, password string) error
+
 	// Set return path for currently processed message.
-	Mail(from string, opts MailOptions) error
+	Mail(from string, opts *MailOptions) error
 	// Add recipient for currently processed message.
 	Rcpt(to string) error
 	// Set currently processed message contents and send it.
+	//
+	// r must be consumed before Data returns.
 	Data(r io.Reader) error
 }
 
+// LMTPSession is an add-on interface for Session. It can be implemented by
+// LMTP servers to provide extra functionality.
 type LMTPSession interface {
 	// LMTPData is the LMTP-specific version of Data method.
 	// It can be optionally implemented by the backend to provide
@@ -70,6 +101,8 @@ type LMTPSession interface {
 	LMTPData(r io.Reader, status StatusCollector) error
 }
 
+// StatusCollector allows a backend to provide per-recipient status
+// information.
 type StatusCollector interface {
 	SetStatus(rcptTo string, err error)
 }
diff --git a/backendutil/transform.go b/backendutil/transform.go
index a5ff666..2a2fe9f 100755
--- a/backendutil/transform.go
+++ b/backendutil/transform.go
@@ -15,22 +15,12 @@ type TransformBackend struct {
 	TransformData func(r io.Reader) (io.Reader, error)
 }
 
-// Login implements the smtp.Backend interface.
-func (be *TransformBackend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) {
-	s, err := be.Backend.Login(state, username, password)
+func (be *TransformBackend) NewSession(c *smtp.Conn) (smtp.Session, error) {
+	sess, err := be.Backend.NewSession(c)
 	if err != nil {
 		return nil, err
 	}
-	return &transformSession{s, be}, nil
-}
-
-// AnonymousLogin implements the smtp.Backend interface.
-func (be *TransformBackend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
-	s, err := be.Backend.AnonymousLogin(state)
-	if err != nil {
-		return nil, err
-	}
-	return &transformSession{s, be}, nil
+	return &transformSession{Session: sess, be: be}, nil
 }
 
 type transformSession struct {
@@ -43,7 +33,11 @@ func (s *transformSession) Reset() {
 	s.Session.Reset()
 }
 
-func (s *transformSession) Mail(from string, opts smtp.MailOptions) error {
+func (s *transformSession) AuthPlain(username, password string) error {
+	return s.Session.AuthPlain(username, password)
+}
+
+func (s *transformSession) Mail(from string, opts *smtp.MailOptions) error {
 	if s.be.TransformMail != nil {
 		var err error
 		from, err = s.be.TransformMail(from)
diff --git a/backendutil/transform_test.go b/backendutil/transform_test.go
index 4505d0d..50961cf 100755
--- a/backendutil/transform_test.go
+++ b/backendutil/transform_test.go
@@ -29,22 +29,7 @@ type backend struct {
 	userErr error
 }
 
-func (be *backend) Login(_ *smtp.ConnectionState, username, password string) (smtp.Session, error) {
-	if be.userErr != nil {
-		return &session{}, be.userErr
-	}
-
-	if username != "username" || password != "password" {
-		return nil, errors.New("Invalid username or password")
-	}
-	return &session{backend: be}, nil
-}
-
-func (be *backend) AnonymousLogin(_ *smtp.ConnectionState) (smtp.Session, error) {
-	if be.userErr != nil {
-		return &session{}, be.userErr
-	}
-
+func (be *backend) NewSession(c *smtp.Conn) (smtp.Session, error) {
 	return &session{backend: be, anonymous: true}, nil
 }
 
@@ -63,7 +48,18 @@ func (s *session) Logout() error {
 	return nil
 }
 
-func (s *session) Mail(from string, opts smtp.MailOptions) error {
+func (s *session) AuthPlain(username, password string) error {
+	if username != "username" || password != "password" {
+		return errors.New("Invalid username or password")
+	}
+	s.anonymous = false
+	return nil
+}
+
+func (s *session) Mail(from string, opts *smtp.MailOptions) error {
+	if s.backend.userErr != nil {
+		return s.backend.userErr
+	}
 	s.Reset()
 	s.msg.From = from
 	return nil
@@ -252,7 +248,7 @@ func TestServer(t *testing.T) {
 		t.Fatal("Invalid mail recipients:", msg.To)
 	}
 	// base64 of "Hey <3\n" (with actual newline)
-	if string(msg.Data) != "SGV5IDwzCg==" {
+	if string(msg.Data) != "SGV5IDwzDQo=" {
 		t.Fatal("Invalid mail data:", string(msg.Data))
 	}
 }
@@ -316,8 +312,8 @@ func TestServer_anonymousUserOK(t *testing.T) {
 	if len(msg.To) != 1 || msg.To[0] != "cm9vdEBnY2hxLmdvdi51aw==" {
 		t.Fatal("Invalid mail recipients:", msg.To)
 	}
-	// base64 of "Hey <3\n" (with actual newline)
-	if string(msg.Data) != "SGV5IDwzCg==" {
+	// base64 of "Hey <3\r\n" (with actual newline)
+	if string(msg.Data) != "SGV5IDwzDQo=" {
 		t.Fatal("Invalid mail data:", string(msg.Data))
 	}
 }
diff --git a/client.go b/client.go
index 9a7accf..2b455ba 100644
--- a/client.go
+++ b/client.go
@@ -14,6 +14,7 @@ import (
 	"net/textproto"
 	"strconv"
 	"strings"
+	"time"
 
 	"github.com/emersion/go-sasl"
 )
@@ -23,6 +24,7 @@ type Client struct {
 	// Text is the textproto.Conn used by the Client. It is exported to allow for
 	// clients to add extensions.
 	Text *textproto.Conn
+
 	// keep a reference to the connection so it can be used to create a TLS
 	// connection later
 	conn net.Conn
@@ -33,17 +35,29 @@ type Client struct {
 	// map of supported extensions
 	ext map[string]string
 	// supported auth mechanisms
-	auth        []string
-	localName   string // the name to use in HELO/EHLO/LHLO
-	didHello    bool   // whether we've said HELO/EHLO/LHLO
-	helloError  error  // the error from the hello
-	rcptToCount int    // number of recipients
+	auth       []string
+	localName  string   // the name to use in HELO/EHLO/LHLO
+	didHello   bool     // whether we've said HELO/EHLO/LHLO
+	helloError error    // the error from the hello
+	rcpts      []string // recipients accumulated for the current session
+
+	// Time to wait for command responses (this includes 3xx reply to DATA).
+	CommandTimeout time.Duration
+	// Time to wait for responses after final dot.
+	SubmissionTimeout time.Duration
+
+	// Logger for all network activity.
+	DebugWriter io.Writer
 }
 
+// 30 seconds was chosen as it's the
+// same duration as http.DefaultTransport's timeout.
+var defaultTimeout = 30 * time.Second
+
 // Dial returns a new Client connected to an SMTP server at addr.
 // The addr must include a port, as in "mail.example.com:smtp".
 func Dial(addr string) (*Client, error) {
-	conn, err := net.Dial("tcp", addr)
+	conn, err := net.DialTimeout("tcp", addr, defaultTimeout)
 	if err != nil {
 		return nil, err
 	}
@@ -53,8 +67,16 @@ func Dial(addr string) (*Client, error) {
 
 // DialTLS returns a new Client connected to an SMTP server via TLS at addr.
 // The addr must include a port, as in "mail.example.com:smtps".
+//
+// A nil tlsConfig is equivalent to a zero tls.Config.
 func DialTLS(addr string, tlsConfig *tls.Config) (*Client, error) {
-	conn, err := tls.Dial("tcp", addr, tlsConfig)
+	tlsDialer := tls.Dialer{
+		NetDialer: &net.Dialer{
+			Timeout: defaultTimeout,
+		},
+		Config: tlsConfig,
+	}
+	conn, err := tlsDialer.Dial("tcp", addr)
 	if err != nil {
 		return nil, err
 	}
@@ -65,36 +87,38 @@ func DialTLS(addr string, tlsConfig *tls.Config) (*Client, error) {
 // NewClient returns a new Client using an existing connection and host as a
 // server name to be used when authenticating.
 func NewClient(conn net.Conn, host string) (*Client, error) {
-	rwc := struct {
-		io.Reader
-		io.Writer
-		io.Closer
-	}{
-		Reader: lineLimitReader{
-			R: conn,
-			// Doubled maximum line length per RFC 5321 (Section 4.5.3.1.6)
-			LineLimit: 2000,
-		},
-		Writer: conn,
-		Closer: conn,
+	c := &Client{
+		serverName: host,
+		localName:  "localhost",
+		// As recommended by RFC 5321. For DATA command reply (3xx one) RFC
+		// recommends a slightly shorter timeout but we do not bother
+		// differentiating these.
+		CommandTimeout: 5 * time.Minute,
+		// 10 minutes + 2 minute buffer in case the server is doing transparent
+		// forwarding and also follows recommended timeouts.
+		SubmissionTimeout: 12 * time.Minute,
 	}
 
-	text := textproto.NewConn(rwc)
-	_, _, err := text.ReadResponse(220)
+	c.setConn(conn)
+
+	// Initial greeting timeout. RFC 5321 recommends 5 minutes.
+	c.conn.SetDeadline(time.Now().Add(5 * time.Minute))
+	defer c.conn.SetDeadline(time.Time{})
+
+	_, _, err := c.Text.ReadResponse(220)
 	if err != nil {
-		text.Close()
+		c.Text.Close()
 		if protoErr, ok := err.(*textproto.Error); ok {
 			return nil, toSMTPErr(protoErr)
 		}
 		return nil, err
 	}
-	_, isTLS := conn.(*tls.Conn)
-	c := &Client{Text: text, conn: conn, serverName: host, localName: "localhost", tls: isTLS}
+
 	return c, nil
 }
 
 // NewClientLMTP returns a new LMTP Client (as defined in RFC 2033) using an
-// existing connector and host as a server name to be used when authenticating.
+// existing connection and host as a server name to be used when authenticating.
 func NewClientLMTP(conn net.Conn, host string) (*Client, error) {
 	c, err := NewClient(conn, host)
 	if err != nil {
@@ -104,6 +128,37 @@ func NewClientLMTP(conn net.Conn, host string) (*Client, error) {
 	return c, nil
 }
 
+// setConn sets the underlying network connection for the client.
+func (c *Client) setConn(conn net.Conn) {
+	c.conn = conn
+
+	var r io.Reader = conn
+	var w io.Writer = conn
+
+	r = &lineLimitReader{
+		R: conn,
+		// Doubled maximum line length per RFC 5321 (Section 4.5.3.1.6)
+		LineLimit: 2000,
+	}
+
+	r = io.TeeReader(r, clientDebugWriter{c})
+	w = io.MultiWriter(w, clientDebugWriter{c})
+
+	rwc := struct {
+		io.Reader
+		io.Writer
+		io.Closer
+	}{
+		Reader: r,
+		Writer: w,
+		Closer: conn,
+	}
+	c.Text = textproto.NewConn(rwc)
+
+	_, isTLS := conn.(*tls.Conn)
+	c.tls = isTLS
+}
+
 // Close closes the connection.
 func (c *Client) Close() error {
 	return c.Text.Close()
@@ -142,6 +197,9 @@ func (c *Client) Hello(localName string) error {
 // cmd is a convenience function that sends a command and returns the response
 // textproto.Error returned by c.Text.ReadResponse is converted into SMTPError.
 func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
+	c.conn.SetDeadline(time.Now().Add(c.CommandTimeout))
+	defer c.conn.SetDeadline(time.Time{})
+
 	id, err := c.Text.Cmd(format, args...)
 	if err != nil {
 		return 0, "", err
@@ -174,6 +232,7 @@ func (c *Client) ehlo() error {
 	if c.lmtp {
 		cmd = "LHLO"
 	}
+
 	_, msg, err := c.cmd(250, "%s %s", cmd, c.localName)
 	if err != nil {
 		return err
@@ -201,6 +260,8 @@ func (c *Client) ehlo() error {
 // StartTLS sends the STARTTLS command and encrypts all further communication.
 // Only servers that advertise the STARTTLS extension support this function.
 //
+// A nil config is equivalent to a zero tls.Config.
+//
 // If server returns an error, it will be of type *SMTPError.
 func (c *Client) StartTLS(config *tls.Config) error {
 	if err := c.hello(); err != nil {
@@ -221,9 +282,7 @@ func (c *Client) StartTLS(config *tls.Config) error {
 	if testHookStartTLS != nil {
 		testHookStartTLS(config)
 	}
-	c.conn = tls.Client(c.conn, config)
-	c.Text = textproto.NewConn(c.conn)
-	c.tls = true
+	c.setConn(tls.Client(c.conn, config))
 	return c.ehlo()
 }
 
@@ -341,7 +400,12 @@ func (c *Client) Mail(from string, opts *MailOptions) error {
 			return errors.New("smtp: server does not support SMTPUTF8")
 		}
 	}
-
+	if opts != nil && opts.Auth != nil {
+		if _, ok := c.ext["AUTH"]; ok {
+			cmdStr += " AUTH=" + encodeXtext(*opts.Auth)
+		}
+		// We can safely discard parameter if server does not support AUTH.
+	}
 	_, _, err := c.cmd(250, cmdStr, from)
 	return err
 }
@@ -358,28 +422,46 @@ func (c *Client) Rcpt(to string) error {
 	if _, _, err := c.cmd(25, "RCPT TO:<%s>", to); err != nil {
 		return err
 	}
-	c.rcptToCount++
+	c.rcpts = append(c.rcpts, to)
 	return nil
 }
 
 type dataCloser struct {
 	c *Client
 	io.WriteCloser
+	statusCb func(rcpt string, status *SMTPError)
+	closed   bool
 }
 
 func (d *dataCloser) Close() error {
-	d.WriteCloser.Close()
+	if d.closed {
+		return fmt.Errorf("smtp: data writer closed twice")
+	}
+
+	if err := d.WriteCloser.Close(); err != nil {
+		return err
+	}
+
+	d.c.conn.SetDeadline(time.Now().Add(d.c.SubmissionTimeout))
+	defer d.c.conn.SetDeadline(time.Time{})
+
+	expectedResponses := len(d.c.rcpts)
 	if d.c.lmtp {
-		for d.c.rcptToCount > 0 {
+		for expectedResponses > 0 {
+			rcpt := d.c.rcpts[len(d.c.rcpts)-expectedResponses]
 			if _, _, err := d.c.Text.ReadResponse(250); err != nil {
 				if protoErr, ok := err.(*textproto.Error); ok {
-					return toSMTPErr(protoErr)
+					if d.statusCb != nil {
+						d.statusCb(rcpt, toSMTPErr(protoErr))
+					}
+				} else {
+					return err
 				}
-				return err
+			} else if d.statusCb != nil {
+				d.statusCb(rcpt, nil)
 			}
-			d.c.rcptToCount--
+			expectedResponses--
 		}
-		return nil
 	} else {
 		_, _, err := d.c.Text.ReadResponse(250)
 		if err != nil {
@@ -388,8 +470,10 @@ func (d *dataCloser) Close() error {
 			}
 			return err
 		}
-		return nil
 	}
+
+	d.closed = true
+	return nil
 }
 
 // Data issues a DATA command to the server and returns a writer that
@@ -403,16 +487,75 @@ func (c *Client) Data() (io.WriteCloser, error) {
 	if err != nil {
 		return nil, err
 	}
-	return &dataCloser{c, c.Text.DotWriter()}, nil
+	return &dataCloser{c: c, WriteCloser: c.Text.DotWriter()}, nil
+}
+
+// LMTPData is the LMTP-specific version of the Data method. It accepts a callback
+// that will be called for each status response received from the server.
+//
+// Status callback will receive a SMTPError argument for each negative server
+// reply and nil for each positive reply. I/O errors will not be reported using
+// callback and instead will be returned by the Close method of io.WriteCloser.
+// Callback will be called for each successfull Rcpt call done before in the
+// same order.
+func (c *Client) LMTPData(statusCb func(rcpt string, status *SMTPError)) (io.WriteCloser, error) {
+	if !c.lmtp {
+		return nil, errors.New("smtp: not a LMTP client")
+	}
+
+	_, _, err := c.cmd(354, "DATA")
+	if err != nil {
+		return nil, err
+	}
+	return &dataCloser{c: c, WriteCloser: c.Text.DotWriter(), statusCb: statusCb}, nil
+}
+
+// SendMail will use an existing connection to send an email from
+// address from, to addresses to, with message r.
+//
+// This function does not start TLS, nor does it perform authentication. Use
+// StartTLS and Auth before-hand if desirable.
+//
+// The addresses in the to parameter are the SMTP RCPT addresses.
+//
+// The r parameter should be an RFC 822-style email with headers
+// first, a blank line, and then the message body. The lines of r
+// should be CRLF terminated. The r headers should usually include
+// fields such as "From", "To", "Subject", and "Cc".  Sending "Bcc"
+// messages is accomplished by including an email address in the to
+// parameter but not including it in the r headers.
+func (c *Client) SendMail(from string, to []string, r io.Reader) error {
+	var err error
+
+	if err = c.Mail(from, nil); err != nil {
+		return err
+	}
+	for _, addr := range to {
+		if err = c.Rcpt(addr); err != nil {
+			return err
+		}
+	}
+	w, err := c.Data()
+	if err != nil {
+		return err
+	}
+	_, err = io.Copy(w, r)
+	if err != nil {
+		return err
+	}
+	err = w.Close()
+	if err != nil {
+		return err
+	}
+	return c.Quit()
 }
 
 var testHookStartTLS func(*tls.Config) // nil, except for tests
 
-// SendMail connects to the server at addr, switches to TLS if
-// possible, authenticates with the optional mechanism a if possible,
-// and then sends an email from address from, to addresses to, with
-// message r.
-// The addr must include a port, as in "mail.example.com:smtp".
+// SendMail connects to the server at addr, switches to TLS, authenticates with
+// the optional SASL client, and then sends an email from address from, to
+// addresses to, with message r. The addr must include a port, as in
+// "mail.example.com:smtp".
 //
 // The addresses in the to parameter are the SMTP RCPT addresses.
 //
@@ -423,11 +566,13 @@ var testHookStartTLS func(*tls.Config) // nil, except for tests
 // messages is accomplished by including an email address in the to
 // parameter but not including it in the r headers.
 //
-// The SendMail function and the net/smtp package are low-level
-// mechanisms and provide no support for DKIM signing, MIME
-// attachments (see the mime/multipart package), or other mail
-// functionality. Higher-level packages exist outside of the standard
-// library.
+// SendMail is intended to be used for very simple use-cases. If you want to
+// customize SendMail's behavior, use a Client instead.
+//
+// The SendMail function and the go-smtp package are low-level
+// mechanisms and provide no support for DKIM signing (see go-msgauth), MIME
+// attachments (see the mime/multipart package or the go-message package), or
+// other mail functionality.
 func SendMail(addr string, a sasl.Client, from string, to []string, r io.Reader) error {
 	if err := validateLine(from); err != nil {
 		return err
@@ -442,13 +587,15 @@ func SendMail(addr string, a sasl.Client, from string, to []string, r io.Reader)
 		return err
 	}
 	defer c.Close()
+
 	if err = c.hello(); err != nil {
 		return err
 	}
-	if ok, _ := c.Extension("STARTTLS"); ok {
-		if err = c.StartTLS(nil); err != nil {
-			return err
-		}
+	if ok, _ := c.Extension("STARTTLS"); !ok {
+		return errors.New("smtp: server doesn't support STARTTLS")
+	}
+	if err = c.StartTLS(nil); err != nil {
+		return err
 	}
 	if a != nil && c.ext != nil {
 		if _, ok := c.ext["AUTH"]; !ok {
@@ -458,27 +605,7 @@ func SendMail(addr string, a sasl.Client, from string, to []string, r io.Reader)
 			return err
 		}
 	}
-	if err = c.Mail(from, nil); err != nil {
-		return err
-	}
-	for _, addr := range to {
-		if err = c.Rcpt(addr); err != nil {
-			return err
-		}
-	}
-	w, err := c.Data()
-	if err != nil {
-		return err
-	}
-	_, err = io.Copy(w, r)
-	if err != nil {
-		return err
-	}
-	err = w.Close()
-	if err != nil {
-		return err
-	}
-	return c.Quit()
+	return c.SendMail(from, to, r)
 }
 
 // Extension reports whether an extension is support by the server.
@@ -506,7 +633,7 @@ func (c *Client) Reset() error {
 	if _, _, err := c.cmd(250, "RSET"); err != nil {
 		return err
 	}
-	c.rcptToCount = 0
+	c.rcpts = nil
 	return nil
 }
 
@@ -573,7 +700,23 @@ func toSMTPErr(protoErr *textproto.Error) *SMTPError {
 		return smtpErr
 	}
 
+	msg := parts[1]
+
+	// Per RFC 2034, enhanced code should be prepended to each line.
+	msg = strings.ReplaceAll(msg, "\n"+parts[0]+" ", "\n")
+
 	smtpErr.EnhancedCode = enchCode
-	smtpErr.Message = parts[1]
+	smtpErr.Message = msg
 	return smtpErr
 }
+
+type clientDebugWriter struct {
+	c *Client
+}
+
+func (cdw clientDebugWriter) Write(b []byte) (int, error) {
+	if cdw.c.DebugWriter == nil {
+		return len(b), nil
+	}
+	return cdw.c.DebugWriter.Write(b)
+}
diff --git a/client_test.go b/client_test.go
index 8734e76..4fbe2cd 100644
--- a/client_test.go
+++ b/client_test.go
@@ -12,8 +12,8 @@ import (
 	"io"
 	"net"
 	"net/textproto"
+	"reflect"
 	"strings"
-	"sync"
 	"testing"
 	"time"
 
@@ -79,7 +79,7 @@ func TestBasic(t *testing.T) {
 	bcmdbuf := bufio.NewWriter(&cmdbuf)
 	var fake faker
 	fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
-	c := &Client{Text: textproto.NewConn(fake), localName: "localhost"}
+	c := &Client{Text: textproto.NewConn(fake), conn: fake, localName: "localhost"}
 
 	if err := c.helo(); err != nil {
 		t.Fatalf("HELO failed: %s", err)
@@ -166,7 +166,10 @@ func TestBasic_SMTPError(t *testing.T) {
 250-mx.google.com at your service
 250 ENHANCEDSTATUSCODES
 500 5.0.0 Failing with enhanced code
-500 Failing without enhanced code`
+500 Failing without enhanced code
+500-5.0.0 Failing with multiline and enhanced code
+500 5.0.0 ... still failing
+`
 	// RFC 2034 says that enhanced codes *SHOULD* be included in errors,
 	// this means it can be violated hence we need to handle last
 	// case properly.
@@ -219,6 +222,21 @@ func TestBasic_SMTPError(t *testing.T) {
 	if smtpErr.Message != "Failing without enhanced code" {
 		t.Fatalf("Wrong message, got %s, want %s", smtpErr.Message, "Failing without enhanced code")
 	}
+
+	err = c.Mail("whatever", nil)
+	if err == nil {
+		t.Fatal("MAIL succeded")
+	}
+	smtpErr, ok = err.(*SMTPError)
+	if !ok {
+		t.Fatal("Returned error is not SMTPError")
+	}
+	if smtpErr.Code != 500 {
+		t.Fatalf("Wrong status code, got %d, want %d", smtpErr.Code, 500)
+	}
+	if want := "Failing with multiline and enhanced code\n... still failing"; smtpErr.Message != want {
+		t.Fatalf("Wrong message, got %s, want %s", smtpErr.Message, want)
+	}
 }
 
 func TestClient_TooLongLine(t *testing.T) {
@@ -503,84 +521,6 @@ var helloClient = []string{
 	"NOOP\n",
 }
 
-func TestSendMail(t *testing.T) {
-	server := strings.Join(strings.Split(sendMailServer, "\n"), "\r\n")
-	client := strings.Join(strings.Split(sendMailClient, "\n"), "\r\n")
-	var cmdbuf bytes.Buffer
-	bcmdbuf := bufio.NewWriter(&cmdbuf)
-	l, err := net.Listen("tcp", "127.0.0.1:0")
-	if err != nil {
-		t.Fatalf("Unable to create listener: %v", err)
-	}
-	defer l.Close()
-
-	// prevent data race on bcmdbuf
-	var done = make(chan struct{})
-	go func(data []string) {
-
-		defer close(done)
-
-		conn, err := l.Accept()
-		if err != nil {
-			t.Errorf("Accept error: %v", err)
-			return
-		}
-		defer conn.Close()
-
-		tc := textproto.NewConn(conn)
-		for i := 0; i < len(data) && data[i] != ""; i++ {
-			tc.PrintfLine(data[i])
-			for len(data[i]) >= 4 && data[i][3] == '-' {
-				i++
-				tc.PrintfLine(data[i])
-			}
-			if data[i] == "221 Goodbye" {
-				return
-			}
-			read := false
-			for !read || data[i] == "354 Go ahead" {
-				msg, err := tc.ReadLine()
-				bcmdbuf.Write([]byte(msg + "\r\n"))
-				read = true
-				if err != nil {
-					t.Errorf("Read error: %v", err)
-					return
-				}
-				if data[i] == "354 Go ahead" && msg == "." {
-					break
-				}
-			}
-		}
-	}(strings.Split(server, "\r\n"))
-
-	err = SendMail(l.Addr().String(), nil, "test@example.com", []string{"other@example.com>\n\rDATA\r\nInjected message body\r\n.\r\nQUIT\r\n"}, strings.NewReader(strings.Replace(`From: test@example.com
-To: other@example.com
-Subject: SendMail test
-SendMail is working for me.
-`, "\n", "\r\n", -1)))
-	if err == nil {
-		t.Errorf("Expected SendMail to be rejected due to a message injection attempt")
-	}
-
-	err = SendMail(l.Addr().String(), nil, "test@example.com", []string{"other@example.com"}, strings.NewReader(strings.Replace(`From: test@example.com
-To: other@example.com
-Subject: SendMail test
-
-SendMail is working for me.
-`, "\n", "\r\n", -1)))
-
-	if err != nil {
-		t.Errorf("%v", err)
-	}
-
-	<-done
-	bcmdbuf.Flush()
-	actualcmds := cmdbuf.String()
-	if client != actualcmds {
-		t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client)
-	}
-}
-
 var sendMailServer = `220 hello world
 502 EH?
 250 mx.google.com at your service
@@ -605,49 +545,6 @@ SendMail is working for me.
 QUIT
 `
 
-func TestSendMailWithAuth(t *testing.T) {
-	l, err := net.Listen("tcp", "127.0.0.1:0")
-	if err != nil {
-		t.Fatalf("Unable to create listener: %v", err)
-	}
-	defer l.Close()
-	wg := sync.WaitGroup{}
-	var done = make(chan struct{})
-	go func() {
-		defer wg.Done()
-		conn, err := l.Accept()
-		if err != nil {
-			t.Errorf("Accept error: %v", err)
-			return
-		}
-		defer conn.Close()
-
-		tc := textproto.NewConn(conn)
-		tc.PrintfLine("220 hello world")
-		msg, err := tc.ReadLine()
-		if msg == "EHLO localhost" {
-			tc.PrintfLine("250 mx.google.com at your service")
-		}
-		// for this test case, there should have no more traffic
-		<-done
-	}()
-	wg.Add(1)
-
-	err = SendMail(l.Addr().String(), sasl.NewPlainClient("", "user", "pass"), "test@example.com", []string{"other@example.com"}, strings.NewReader(strings.Replace(`From: test@example.com
-To: other@example.com
-Subject: SendMail test
-SendMail is working for me.
-`, "\n", "\r\n", -1)))
-	if err == nil {
-		t.Error("SendMail: Server doesn't support AUTH, expected to get an error, but got none ")
-	}
-	if err.Error() != "smtp: server doesn't support AUTH" {
-		t.Errorf("Expected: smtp: server doesn't support AUTH, got: %s", err)
-	}
-	close(done)
-	wg.Wait()
-}
-
 func TestAuthFailed(t *testing.T) {
 	server := strings.Join(strings.Split(authFailedServer, "\n"), "\r\n")
 	client := strings.Join(strings.Split(authFailedClient, "\n"), "\r\n")
@@ -845,8 +742,9 @@ func sendMail(hostPort string) error {
 }
 
 // localhostCert is a PEM-encoded TLS cert generated from src/crypto/tls:
-// go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com \
-// 		--ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
+//
+//	go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com \
+//			--ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
 var localhostCert = []byte(`
 -----BEGIN CERTIFICATE-----
 MIICFDCCAX2gAwIBAgIRAK0xjnaPuNDSreeXb+z+0u4wDQYJKoZIhvcNAQELBQAw
@@ -889,7 +787,7 @@ func TestLMTP(t *testing.T) {
 	bcmdbuf := bufio.NewWriter(&cmdbuf)
 	var fake faker
 	fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
-	c := &Client{Text: textproto.NewConn(fake), lmtp: true}
+	c := &Client{Text: textproto.NewConn(fake), conn: fake, lmtp: true}
 
 	if err := c.Hello("localhost"); err != nil {
 		t.Fatalf("LHLO failed: %s", err)
@@ -955,3 +853,81 @@ Goodbye.
 .
 QUIT
 `
+
+func TestLMTPData(t *testing.T) {
+	var lmtpServerPartial = `250-localhost at your service
+250-SIZE 35651584
+250 8BITMIME
+250 Sender OK
+250 Receiver OK
+250 Receiver OK
+354 Go ahead
+250 This recipient is fine
+500 But not this one
+221 OK
+`
+	server := strings.Join(strings.Split(lmtpServerPartial, "\n"), "\r\n")
+
+	var cmdbuf bytes.Buffer
+	bcmdbuf := bufio.NewWriter(&cmdbuf)
+	var fake faker
+	fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
+	c := &Client{Text: textproto.NewConn(fake), conn: fake, lmtp: true}
+
+	if err := c.Hello("localhost"); err != nil {
+		t.Fatalf("LHLO failed: %s", err)
+	}
+	c.didHello = true
+
+	if err := c.Mail("user@gmail.com", nil); err != nil {
+		t.Fatalf("MAIL failed: %s", err)
+	}
+	if err := c.Rcpt("golang-nuts@googlegroups.com"); err != nil {
+		t.Fatalf("RCPT failed: %s", err)
+	}
+	if err := c.Rcpt("golang-not-nuts@googlegroups.com"); err != nil {
+		t.Fatalf("RCPT failed: %s", err)
+	}
+	msg := `From: user@gmail.com
+To: golang-nuts@googlegroups.com
+Subject: Hooray for Go
+
+Line 1
+.Leading dot line .
+Goodbye.`
+
+	rcpts := []string{}
+	errors := []*SMTPError{}
+
+	w, err := c.LMTPData(func(rcpt string, status *SMTPError) {
+		rcpts = append(rcpts, rcpt)
+		errors = append(errors, status)
+	})
+	if err != nil {
+		t.Fatalf("DATA failed: %s", err)
+	}
+	if _, err := w.Write([]byte(msg)); err != nil {
+		t.Fatalf("Data write failed: %s", err)
+	}
+	if err := w.Close(); err != nil {
+		t.Fatalf("Bad data response: %s", err)
+	}
+
+	if !reflect.DeepEqual(rcpts, []string{"golang-nuts@googlegroups.com", "golang-not-nuts@googlegroups.com"}) {
+		t.Fatal("Status callbacks called for wrong recipients:", rcpts)
+	}
+
+	if len(errors) != 2 {
+		t.Fatalf("Wrong amount of status callback calls: %v", len(errors))
+	}
+	if errors[0] != nil {
+		t.Fatalf("Unexpected error status for the first recipient: %v", errors[0])
+	}
+	if errors[1] == nil {
+		t.Fatalf("Unexpected success status for the second recipient")
+	}
+
+	if err := c.Quit(); err != nil {
+		t.Fatalf("QUIT failed: %s", err)
+	}
+}
diff --git a/cmd/smtp-debug-server/main.go b/cmd/smtp-debug-server/main.go
new file mode 100644
index 0000000..271a7ef
--- /dev/null
+++ b/cmd/smtp-debug-server/main.go
@@ -0,0 +1,60 @@
+package main
+
+import (
+	"flag"
+	"io"
+	"log"
+	"os"
+
+	"github.com/emersion/go-smtp"
+)
+
+var addr = "127.0.0.1:1025"
+
+func init() {
+	flag.StringVar(&addr, "l", addr, "Listen address")
+}
+
+type backend struct{}
+
+func (bkd *backend) NewSession(c *smtp.Conn) (smtp.Session, error) {
+	return &session{}, nil
+}
+
+type session struct{}
+
+func (s *session) AuthPlain(username, password string) error {
+	return nil
+}
+
+func (s *session) Mail(from string, opts *smtp.MailOptions) error {
+	return nil
+}
+
+func (s *session) Rcpt(to string) error {
+	return nil
+}
+
+func (s *session) Data(r io.Reader) error {
+	return nil
+}
+
+func (s *session) Reset() {}
+
+func (s *session) Logout() error {
+	return nil
+}
+
+func main() {
+	flag.Parse()
+
+	s := smtp.NewServer(&backend{})
+
+	s.Addr = addr
+	s.Domain = "localhost"
+	s.AllowInsecureAuth = true
+	s.Debug = os.Stdout
+
+	log.Println("Starting SMTP server at", addr)
+	log.Fatal(s.ListenAndServe())
+}
diff --git a/conn.go b/conn.go
index 93e077d..72a67d8 100644
--- a/conn.go
+++ b/conn.go
@@ -3,11 +3,13 @@ package smtp
 import (
 	"crypto/tls"
 	"encoding/base64"
+	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
 	"net"
 	"net/textproto"
+	"regexp"
 	"runtime/debug"
 	"strconv"
 	"strings"
@@ -15,24 +17,31 @@ import (
 	"time"
 )
 
-type ConnectionState struct {
-	Hostname   string
-	LocalAddr  net.Addr
-	RemoteAddr net.Addr
-	TLS        tls.ConnectionState
-}
+// Number of errors we'll tolerate per connection before closing. Defaults to 3.
+const errThreshold = 3
 
 type Conn struct {
-	conn      net.Conn
-	text      *textproto.Conn
-	server    *Server
-	helo      string
-	nbrErrors int
-	session   Session
-	locker    sync.Mutex
+	conn   net.Conn
+	text   *textproto.Conn
+	server *Server
+	helo   string
+
+	// Number of errors witnessed on this connection
+	errCount int
+
+	session    Session
+	locker     sync.Mutex
+	binarymime bool
+
+	lineLimitReader *lineLimitReader
+	bdatPipe        *io.PipeWriter
+	bdatStatus      *statusCollector // used for BDAT on LMTP
+	dataResult      chan error
+	bytesReceived   int // counts total size of chunks when BDAT is used
 
 	fromReceived bool
 	recipients   []string
+	didAuth      bool
 }
 
 func newConn(c net.Conn, s *Server) *Conn {
@@ -46,15 +55,16 @@ func newConn(c net.Conn, s *Server) *Conn {
 }
 
 func (c *Conn) init() {
+	c.lineLimitReader = &lineLimitReader{
+		R:         c.conn,
+		LineLimit: c.server.MaxLineLength,
+	}
 	rwc := struct {
 		io.Reader
 		io.Writer
 		io.Closer
 	}{
-		Reader: lineLimitReader{
-			R:         c.conn,
-			LineLimit: c.server.MaxLineLength,
-		},
+		Reader: c.lineLimitReader,
 		Writer: c.conn,
 		Closer: c.conn,
 	}
@@ -74,32 +84,22 @@ func (c *Conn) init() {
 	c.text = textproto.NewConn(rwc)
 }
 
-func (c *Conn) unrecognizedCommand(cmd string) {
-	c.WriteResponse(500, EnhancedCode{5, 5, 2}, fmt.Sprintf("Syntax error, %v command unrecognized", cmd))
-
-	c.nbrErrors++
-	if c.nbrErrors > 3 {
-		c.WriteResponse(500, EnhancedCode{5, 5, 2}, "Too many unrecognized commands")
-		c.Close()
-	}
-}
-
 // Commands are dispatched to the appropriate handler functions.
 func (c *Conn) handle(cmd string, arg string) {
 	// If panic happens during command handling - send 421 response
 	// and close connection.
 	defer func() {
 		if err := recover(); err != nil {
-			c.WriteResponse(421, EnhancedCode{4, 0, 0}, "Internal server error")
+			c.writeResponse(421, EnhancedCode{4, 0, 0}, "Internal server error")
 			c.Close()
 
 			stack := debug.Stack()
-			c.server.ErrorLog.Printf("panic serving %v: %v\n%s", c.State().RemoteAddr, err, stack)
+			c.server.ErrorLog.Printf("panic serving %v: %v\n%s", c.conn.RemoteAddr(), err, stack)
 		}
 	}()
 
 	if cmd == "" {
-		c.WriteResponse(500, EnhancedCode{5, 5, 2}, "Speak up")
+		c.protocolError(500, EnhancedCode{5, 5, 2}, "Error: bad syntax")
 		return
 	}
 
@@ -107,16 +107,16 @@ func (c *Conn) handle(cmd string, arg string) {
 	switch cmd {
 	case "SEND", "SOML", "SAML", "EXPN", "HELP", "TURN":
 		// These commands are not implemented in any state
-		c.WriteResponse(502, EnhancedCode{5, 5, 1}, fmt.Sprintf("%v command not implemented", cmd))
+		c.writeResponse(502, EnhancedCode{5, 5, 1}, fmt.Sprintf("%v command not implemented", cmd))
 	case "HELO", "EHLO", "LHLO":
 		lmtp := cmd == "LHLO"
 		enhanced := lmtp || cmd == "EHLO"
 		if c.server.LMTP && !lmtp {
-			c.WriteResponse(500, EnhancedCode{5, 5, 1}, "This is a LMTP server, use LHLO")
+			c.writeResponse(500, EnhancedCode{5, 5, 1}, "This is a LMTP server, use LHLO")
 			return
 		}
 		if !c.server.LMTP && lmtp {
-			c.WriteResponse(500, EnhancedCode{5, 5, 1}, "This is not a LMTP server")
+			c.writeResponse(500, EnhancedCode{5, 5, 1}, "This is not a LMTP server")
 			return
 		}
 		c.handleGreet(enhanced, arg)
@@ -125,27 +125,30 @@ func (c *Conn) handle(cmd string, arg string) {
 	case "RCPT":
 		c.handleRcpt(arg)
 	case "VRFY":
-		c.WriteResponse(252, EnhancedCode{2, 5, 0}, "Cannot VRFY user, but will accept message")
+		c.writeResponse(252, EnhancedCode{2, 5, 0}, "Cannot VRFY user, but will accept message")
 	case "NOOP":
-		c.WriteResponse(250, EnhancedCode{2, 0, 0}, "I have sucessfully done nothing")
+		c.writeResponse(250, EnhancedCode{2, 0, 0}, "I have sucessfully done nothing")
 	case "RSET": // Reset session
 		c.reset()
-		c.WriteResponse(250, EnhancedCode{2, 0, 0}, "Session reset")
+		c.writeResponse(250, EnhancedCode{2, 0, 0}, "Session reset")
+	case "BDAT":
+		c.handleBdat(arg)
 	case "DATA":
 		c.handleData(arg)
 	case "QUIT":
-		c.WriteResponse(221, EnhancedCode{2, 0, 0}, "Goodnight and good luck")
+		c.writeResponse(221, EnhancedCode{2, 0, 0}, "Bye")
 		c.Close()
 	case "AUTH":
 		if c.server.AuthDisabled {
-			c.unrecognizedCommand(cmd)
+			c.protocolError(500, EnhancedCode{5, 5, 2}, "Syntax error, AUTH command unrecognized")
 		} else {
 			c.handleAuth(arg)
 		}
 	case "STARTTLS":
 		c.handleStartTLS()
 	default:
-		c.unrecognizedCommand(cmd)
+		msg := fmt.Sprintf("Syntax errors, %v command unrecognized", cmd)
+		c.protocolError(500, EnhancedCode{5, 5, 2}, msg)
 	}
 }
 
@@ -159,17 +162,24 @@ func (c *Conn) Session() Session {
 	return c.session
 }
 
-// Setting the user resets any message being generated
-func (c *Conn) SetSession(session Session) {
+func (c *Conn) setSession(session Session) {
 	c.locker.Lock()
 	defer c.locker.Unlock()
 	c.session = session
 }
 
 func (c *Conn) Close() error {
-	if session := c.Session(); session != nil {
-		session.Logout()
-		c.SetSession(nil)
+	c.locker.Lock()
+	defer c.locker.Unlock()
+
+	if c.bdatPipe != nil {
+		c.bdatPipe.CloseWithError(ErrDataReset)
+		c.bdatPipe = nil
+	}
+
+	if c.session != nil {
+		c.session.Logout()
+		c.session = nil
 	}
 
 	return c.conn.Close()
@@ -185,18 +195,12 @@ func (c *Conn) TLSConnectionState() (state tls.ConnectionState, ok bool) {
 	return tc.ConnectionState(), true
 }
 
-func (c *Conn) State() ConnectionState {
-	state := ConnectionState{}
-	tlsState, ok := c.TLSConnectionState()
-	if ok {
-		state.TLS = tlsState
-	}
-
-	state.Hostname = c.helo
-	state.LocalAddr = c.conn.LocalAddr()
-	state.RemoteAddr = c.conn.RemoteAddr()
+func (c *Conn) Hostname() string {
+	return c.helo
+}
 
-	return state
+func (c *Conn) Conn() net.Conn {
+	return c.conn
 }
 
 func (c *Conn) authAllowed() bool {
@@ -204,103 +208,114 @@ func (c *Conn) authAllowed() bool {
 	return !c.server.AuthDisabled && (isTLS || c.server.AllowInsecureAuth)
 }
 
+// protocolError writes errors responses and closes the connection once too many
+// have occurred.
+func (c *Conn) protocolError(code int, ec EnhancedCode, msg string) {
+	c.writeResponse(code, ec, msg)
+
+	c.errCount++
+	if c.errCount > errThreshold {
+		c.writeResponse(500, EnhancedCode{5, 5, 1}, "Too many errors. Quiting now")
+		c.Close()
+	}
+}
+
 // GREET state -> waiting for HELO
 func (c *Conn) handleGreet(enhanced bool, arg string) {
-	if !enhanced {
-		domain, err := parseHelloArgument(arg)
-		if err != nil {
-			c.WriteResponse(501, EnhancedCode{5, 5, 2}, "Domain/address argument required for HELO")
-			return
-		}
-		c.helo = domain
+	domain, err := parseHelloArgument(arg)
+	if err != nil {
+		c.writeResponse(501, EnhancedCode{5, 5, 2}, "Domain/address argument required for HELO")
+		return
+	}
+	c.helo = domain
 
-		c.WriteResponse(250, EnhancedCode{2, 0, 0}, fmt.Sprintf("Hello %s", domain))
-	} else {
-		domain, err := parseHelloArgument(arg)
-		if err != nil {
-			c.WriteResponse(501, EnhancedCode{5, 5, 2}, "Domain/address argument required for EHLO")
+	sess, err := c.server.Backend.NewSession(c)
+	if err != nil {
+		if smtpErr, ok := err.(*SMTPError); ok {
+			c.writeResponse(smtpErr.Code, smtpErr.EnhancedCode, smtpErr.Message)
 			return
 		}
+		c.writeResponse(451, EnhancedCode{4, 0, 0}, err.Error())
+		return
+	}
+	c.setSession(sess)
 
-		c.helo = domain
-
-		caps := []string{}
-		caps = append(caps, c.server.caps...)
-		if _, isTLS := c.TLSConnectionState(); c.server.TLSConfig != nil && !isTLS {
-			caps = append(caps, "STARTTLS")
-		}
-		if c.authAllowed() {
-			authCap := "AUTH"
-			for name := range c.server.auths {
-				authCap += " " + name
-			}
+	if !enhanced {
+		c.writeResponse(250, EnhancedCode{2, 0, 0}, fmt.Sprintf("Hello %s", domain))
+		return
+	}
 
-			caps = append(caps, authCap)
-		}
-		if c.server.EnableSMTPUTF8 {
-			caps = append(caps, "SMTPUTF8")
-		}
-		if _, isTLS := c.TLSConnectionState(); isTLS && c.server.EnableREQUIRETLS {
-			caps = append(caps, "REQUIRETLS")
-		}
-		if c.server.MaxMessageBytes > 0 {
-			caps = append(caps, fmt.Sprintf("SIZE %v", c.server.MaxMessageBytes))
+	caps := []string{}
+	caps = append(caps, c.server.caps...)
+	if _, isTLS := c.TLSConnectionState(); c.server.TLSConfig != nil && !isTLS {
+		caps = append(caps, "STARTTLS")
+	}
+	if c.authAllowed() {
+		authCap := "AUTH"
+		for name := range c.server.auths {
+			authCap += " " + name
 		}
 
-		args := []string{"Hello " + domain}
-		args = append(args, caps...)
-		c.WriteResponse(250, NoEnhancedCode, args...)
+		caps = append(caps, authCap)
 	}
+	if c.server.EnableSMTPUTF8 {
+		caps = append(caps, "SMTPUTF8")
+	}
+	if _, isTLS := c.TLSConnectionState(); isTLS && c.server.EnableREQUIRETLS {
+		caps = append(caps, "REQUIRETLS")
+	}
+	if c.server.EnableBINARYMIME {
+		caps = append(caps, "BINARYMIME")
+	}
+	if c.server.MaxMessageBytes > 0 {
+		caps = append(caps, fmt.Sprintf("SIZE %v", c.server.MaxMessageBytes))
+	} else {
+		caps = append(caps, "SIZE")
+	}
+
+	args := []string{"Hello " + domain}
+	args = append(args, caps...)
+	c.writeResponse(250, NoEnhancedCode, args...)
 }
 
 // READY state -> waiting for MAIL
 func (c *Conn) handleMail(arg string) {
 	if c.helo == "" {
-		c.WriteResponse(502, EnhancedCode{2, 5, 1}, "Please introduce yourself first.")
+		c.writeResponse(502, EnhancedCode{2, 5, 1}, "Please introduce yourself first.")
 		return
 	}
-
-	if c.Session() == nil {
-		state := c.State()
-		session, err := c.server.Backend.AnonymousLogin(&state)
-		if err != nil {
-			if smtpErr, ok := err.(*SMTPError); ok {
-				c.WriteResponse(smtpErr.Code, smtpErr.EnhancedCode, smtpErr.Message)
-			} else {
-				c.WriteResponse(502, EnhancedCode{5, 7, 0}, err.Error())
-			}
-			return
-		}
-
-		c.SetSession(session)
+	if c.bdatPipe != nil {
+		c.writeResponse(502, EnhancedCode{5, 5, 1}, "MAIL not allowed during message transfer")
+		return
 	}
 
 	if len(arg) < 6 || strings.ToUpper(arg[0:5]) != "FROM:" {
-		c.WriteResponse(501, EnhancedCode{5, 5, 2}, "Was expecting MAIL arg syntax of FROM:<address>")
+		c.writeResponse(501, EnhancedCode{5, 5, 2}, "Was expecting MAIL arg syntax of FROM:<address>")
 		return
 	}
 	fromArgs := strings.Split(strings.Trim(arg[5:], " "), " ")
 	if c.server.Strict {
 		if !strings.HasPrefix(fromArgs[0], "<") || !strings.HasSuffix(fromArgs[0], ">") {
-			c.WriteResponse(501, EnhancedCode{5, 5, 2}, "Was expecting MAIL arg syntax of FROM:<address>")
+			c.writeResponse(501, EnhancedCode{5, 5, 2}, "Was expecting MAIL arg syntax of FROM:<address>")
 			return
 		}
 	}
 	from := fromArgs[0]
 	if from == "" {
-		c.WriteResponse(501, EnhancedCode{5, 5, 2}, "Was expecting MAIL arg syntax of FROM:<address>")
+		c.writeResponse(501, EnhancedCode{5, 5, 2}, "Was expecting MAIL arg syntax of FROM:<address>")
 		return
 	}
 	from = strings.Trim(from, "<>")
 
-	opts := MailOptions{}
+	opts := &MailOptions{}
 
+	c.binarymime = false
 	// This is where the Conn may put BODY=8BITMIME, but we already
 	// read the DATA as bytes, so it does not effect our processing.
 	if len(fromArgs) > 1 {
 		args, err := parseArgs(fromArgs[1:])
 		if err != nil {
-			c.WriteResponse(501, EnhancedCode{5, 5, 4}, "Unable to parse MAIL ESMTP parameters")
+			c.writeResponse(501, EnhancedCode{5, 5, 4}, "Unable to parse MAIL ESMTP parameters")
 			return
 		}
 
@@ -309,37 +324,60 @@ func (c *Conn) handleMail(arg string) {
 			case "SIZE":
 				size, err := strconv.ParseInt(value, 10, 32)
 				if err != nil {
-					c.WriteResponse(501, EnhancedCode{5, 5, 4}, "Unable to parse SIZE as an integer")
+					c.writeResponse(501, EnhancedCode{5, 5, 4}, "Unable to parse SIZE as an integer")
 					return
 				}
 
 				if c.server.MaxMessageBytes > 0 && int(size) > c.server.MaxMessageBytes {
-					c.WriteResponse(552, EnhancedCode{5, 3, 4}, "Max message size exceeded")
+					c.writeResponse(552, EnhancedCode{5, 3, 4}, "Max message size exceeded")
 					return
 				}
 
 				opts.Size = int(size)
 			case "SMTPUTF8":
 				if !c.server.EnableSMTPUTF8 {
-					c.WriteResponse(504, EnhancedCode{5, 5, 4}, "SMTPUTF8 is not implemented")
+					c.writeResponse(504, EnhancedCode{5, 5, 4}, "SMTPUTF8 is not implemented")
 					return
 				}
 				opts.UTF8 = true
 			case "REQUIRETLS":
 				if !c.server.EnableREQUIRETLS {
-					c.WriteResponse(504, EnhancedCode{5, 5, 4}, "REQUIRETLS is not implemented")
+					c.writeResponse(504, EnhancedCode{5, 5, 4}, "REQUIRETLS is not implemented")
 					return
 				}
 				opts.RequireTLS = true
 			case "BODY":
 				switch value {
+				case "BINARYMIME":
+					if !c.server.EnableBINARYMIME {
+						c.writeResponse(504, EnhancedCode{5, 5, 4}, "BINARYMIME is not implemented")
+						return
+					}
+					c.binarymime = true
 				case "7BIT", "8BITMIME":
 				default:
-					c.WriteResponse(500, EnhancedCode{5, 5, 4}, "Unknown BODY value")
+					c.writeResponse(500, EnhancedCode{5, 5, 4}, "Unknown BODY value")
+					return
+				}
+				opts.Body = BodyType(value)
+			case "AUTH":
+				value, err := decodeXtext(value)
+				if err != nil {
+					c.writeResponse(500, EnhancedCode{5, 5, 4}, "Malformed AUTH parameter value")
+					return
+				}
+				if !strings.HasPrefix(value, "<") {
+					c.writeResponse(500, EnhancedCode{5, 5, 4}, "Missing opening angle bracket")
 					return
 				}
+				if !strings.HasSuffix(value, ">") {
+					c.writeResponse(500, EnhancedCode{5, 5, 4}, "Missing closing angle bracket")
+					return
+				}
+				decodedMbox := value[1 : len(value)-1]
+				opts.Auth = &decodedMbox
 			default:
-				c.WriteResponse(500, EnhancedCode{5, 5, 4}, "Unknown MAIL FROM argument")
+				c.writeResponse(500, EnhancedCode{5, 5, 4}, "Unknown MAIL FROM argument")
 				return
 			}
 		}
@@ -347,26 +385,81 @@ func (c *Conn) handleMail(arg string) {
 
 	if err := c.Session().Mail(from, opts); err != nil {
 		if smtpErr, ok := err.(*SMTPError); ok {
-			c.WriteResponse(smtpErr.Code, smtpErr.EnhancedCode, smtpErr.Message)
+			c.writeResponse(smtpErr.Code, smtpErr.EnhancedCode, smtpErr.Message)
 			return
 		}
-		c.WriteResponse(451, EnhancedCode{4, 0, 0}, err.Error())
+		c.writeResponse(451, EnhancedCode{4, 0, 0}, err.Error())
 		return
 	}
 
-	c.WriteResponse(250, EnhancedCode{2, 0, 0}, fmt.Sprintf("Roger, accepting mail from <%v>", from))
+	c.writeResponse(250, EnhancedCode{2, 0, 0}, fmt.Sprintf("Roger, accepting mail from <%v>", from))
 	c.fromReceived = true
 }
 
+// This regexp matches 'hexchar' token defined in
+// https://tools.ietf.org/html/rfc4954#section-8 however it is intentionally
+// relaxed by requiring only '+' to be present.  It allows us to detect
+// malformed values such as +A or +HH and report them appropriately.
+var hexcharRe = regexp.MustCompile(`\+[0-9A-F]?[0-9A-F]?`)
+
+func decodeXtext(val string) (string, error) {
+	if !strings.Contains(val, "+") {
+		return val, nil
+	}
+
+	var replaceErr error
+	decoded := hexcharRe.ReplaceAllStringFunc(val, func(match string) string {
+		if len(match) != 3 {
+			replaceErr = errors.New("incomplete hexchar")
+			return ""
+		}
+		char, err := strconv.ParseInt(match, 16, 8)
+		if err != nil {
+			replaceErr = err
+			return ""
+		}
+
+		return string(rune(char))
+	})
+	if replaceErr != nil {
+		return "", replaceErr
+	}
+
+	return decoded, nil
+}
+
+func encodeXtext(raw string) string {
+	var out strings.Builder
+	out.Grow(len(raw))
+
+	for _, ch := range raw {
+		if ch == '+' || ch == '=' {
+			out.WriteRune('+')
+			out.WriteString(strings.ToUpper(strconv.FormatInt(int64(ch), 16)))
+		}
+		if ch > '!' && ch < '~' { // printable non-space US-ASCII
+			out.WriteRune(ch)
+		}
+		// Non-ASCII.
+		out.WriteRune('+')
+		out.WriteString(strings.ToUpper(strconv.FormatInt(int64(ch), 16)))
+	}
+	return out.String()
+}
+
 // MAIL state -> waiting for RCPTs followed by DATA
 func (c *Conn) handleRcpt(arg string) {
 	if !c.fromReceived {
-		c.WriteResponse(502, EnhancedCode{5, 5, 1}, "Missing MAIL FROM command.")
+		c.writeResponse(502, EnhancedCode{5, 5, 1}, "Missing MAIL FROM command.")
+		return
+	}
+	if c.bdatPipe != nil {
+		c.writeResponse(502, EnhancedCode{5, 5, 1}, "RCPT not allowed during message transfer")
 		return
 	}
 
 	if (len(arg) < 4) || (strings.ToUpper(arg[0:3]) != "TO:") {
-		c.WriteResponse(501, EnhancedCode{5, 5, 2}, "Was expecting RCPT arg syntax of TO:<address>")
+		c.writeResponse(501, EnhancedCode{5, 5, 2}, "Was expecting RCPT arg syntax of TO:<address>")
 		return
 	}
 
@@ -374,36 +467,40 @@ func (c *Conn) handleRcpt(arg string) {
 	recipient := strings.Trim(arg[3:], "<> ")
 
 	if c.server.MaxRecipients > 0 && len(c.recipients) >= c.server.MaxRecipients {
-		c.WriteResponse(552, EnhancedCode{5, 5, 3}, fmt.Sprintf("Maximum limit of %v recipients reached", c.server.MaxRecipients))
+		c.writeResponse(552, EnhancedCode{5, 5, 3}, fmt.Sprintf("Maximum limit of %v recipients reached", c.server.MaxRecipients))
 		return
 	}
 
 	if err := c.Session().Rcpt(recipient); err != nil {
 		if smtpErr, ok := err.(*SMTPError); ok {
-			c.WriteResponse(smtpErr.Code, smtpErr.EnhancedCode, smtpErr.Message)
+			c.writeResponse(smtpErr.Code, smtpErr.EnhancedCode, smtpErr.Message)
 			return
 		}
-		c.WriteResponse(451, EnhancedCode{4, 0, 0}, err.Error())
+		c.writeResponse(451, EnhancedCode{4, 0, 0}, err.Error())
 		return
 	}
 	c.recipients = append(c.recipients, recipient)
-	c.WriteResponse(250, EnhancedCode{2, 0, 0}, fmt.Sprintf("I'll make sure <%v> gets this", recipient))
+	c.writeResponse(250, EnhancedCode{2, 0, 0}, fmt.Sprintf("I'll make sure <%v> gets this", recipient))
 }
 
 func (c *Conn) handleAuth(arg string) {
 	if c.helo == "" {
-		c.WriteResponse(502, EnhancedCode{5, 5, 1}, "Please introduce yourself first.")
+		c.writeResponse(502, EnhancedCode{5, 5, 1}, "Please introduce yourself first.")
+		return
+	}
+	if c.didAuth {
+		c.writeResponse(503, EnhancedCode{5, 5, 1}, "Already authenticated")
 		return
 	}
 
 	parts := strings.Fields(arg)
 	if len(parts) == 0 {
-		c.WriteResponse(502, EnhancedCode{5, 5, 4}, "Missing parameter")
+		c.writeResponse(502, EnhancedCode{5, 5, 4}, "Missing parameter")
 		return
 	}
 
 	if _, isTLS := c.TLSConnectionState(); !isTLS && !c.server.AllowInsecureAuth {
-		c.WriteResponse(523, EnhancedCode{5, 7, 10}, "TLS is required")
+		c.writeResponse(523, EnhancedCode{5, 7, 10}, "TLS is required")
 		return
 	}
 
@@ -421,7 +518,7 @@ func (c *Conn) handleAuth(arg string) {
 
 	newSasl, ok := c.server.auths[mechanism]
 	if !ok {
-		c.WriteResponse(504, EnhancedCode{5, 7, 4}, "Unsupported authentication mechanism")
+		c.writeResponse(504, EnhancedCode{5, 7, 4}, "Unsupported authentication mechanism")
 		return
 	}
 
@@ -432,10 +529,10 @@ func (c *Conn) handleAuth(arg string) {
 		challenge, done, err := sasl.Next(response)
 		if err != nil {
 			if smtpErr, ok := err.(*SMTPError); ok {
-				c.WriteResponse(smtpErr.Code, smtpErr.EnhancedCode, smtpErr.Message)
+				c.writeResponse(smtpErr.Code, smtpErr.EnhancedCode, smtpErr.Message)
 				return
 			}
-			c.WriteResponse(454, EnhancedCode{4, 7, 0}, err.Error())
+			c.writeResponse(454, EnhancedCode{4, 7, 0}, err.Error())
 			return
 		}
 
@@ -447,67 +544,89 @@ func (c *Conn) handleAuth(arg string) {
 		if len(challenge) > 0 {
 			encoded = base64.StdEncoding.EncodeToString(challenge)
 		}
-		c.WriteResponse(334, NoEnhancedCode, encoded)
+		c.writeResponse(334, NoEnhancedCode, encoded)
 
-		encoded, err = c.ReadLine()
+		encoded, err = c.readLine()
 		if err != nil {
 			return // TODO: error handling
 		}
 
+		if encoded == "*" {
+			// https://tools.ietf.org/html/rfc4954#page-4
+			c.writeResponse(501, EnhancedCode{5, 0, 0}, "Negotiation cancelled")
+			return
+		}
+
 		response, err = base64.StdEncoding.DecodeString(encoded)
 		if err != nil {
-			c.WriteResponse(454, EnhancedCode{4, 7, 0}, "Invalid base64 data")
+			c.writeResponse(454, EnhancedCode{4, 7, 0}, "Invalid base64 data")
 			return
 		}
 	}
 
-	if c.Session() != nil {
-		c.WriteResponse(235, EnhancedCode{2, 0, 0}, "Authentication succeeded")
-	}
+	c.writeResponse(235, EnhancedCode{2, 0, 0}, "Authentication succeeded")
+	c.didAuth = true
 }
 
 func (c *Conn) handleStartTLS() {
 	if _, isTLS := c.TLSConnectionState(); isTLS {
-		c.WriteResponse(502, EnhancedCode{5, 5, 1}, "Already running in TLS")
+		c.writeResponse(502, EnhancedCode{5, 5, 1}, "Already running in TLS")
 		return
 	}
 
 	if c.server.TLSConfig == nil {
-		c.WriteResponse(502, EnhancedCode{5, 5, 1}, "TLS not supported")
+		c.writeResponse(502, EnhancedCode{5, 5, 1}, "TLS not supported")
 		return
 	}
 
-	c.WriteResponse(220, EnhancedCode{2, 0, 0}, "Ready to start TLS")
+	c.writeResponse(220, EnhancedCode{2, 0, 0}, "Ready to start TLS")
 
 	// Upgrade to TLS
-	var tlsConn *tls.Conn
-	tlsConn = tls.Server(c.conn, c.server.TLSConfig)
+	tlsConn := tls.Server(c.conn, c.server.TLSConfig)
 
 	if err := tlsConn.Handshake(); err != nil {
-		c.WriteResponse(550, EnhancedCode{5, 0, 0}, "Handshake error")
+		c.writeResponse(550, EnhancedCode{5, 0, 0}, "Handshake error")
+		return
 	}
 
 	c.conn = tlsConn
 	c.init()
 
-	// Reset envelope as a new EHLO/HELO is required after STARTTLS
+	// Reset all state and close the previous Session.
+	// This is different from just calling reset() since we want the Backend to
+	// be able to see the information about TLS connection in the
+	// ConnectionState object passed to it.
+	if session := c.Session(); session != nil {
+		session.Logout()
+		c.setSession(nil)
+	}
+	c.helo = ""
+	c.didAuth = false
 	c.reset()
 }
 
 // DATA
 func (c *Conn) handleData(arg string) {
 	if arg != "" {
-		c.WriteResponse(501, EnhancedCode{5, 5, 4}, "DATA command should not have any arguments")
+		c.writeResponse(501, EnhancedCode{5, 5, 4}, "DATA command should not have any arguments")
+		return
+	}
+	if c.bdatPipe != nil {
+		c.writeResponse(502, EnhancedCode{5, 5, 1}, "DATA not allowed during message transfer")
+		return
+	}
+	if c.binarymime {
+		c.writeResponse(502, EnhancedCode{5, 5, 1}, "DATA not allowed for BINARYMIME messages")
 		return
 	}
 
 	if !c.fromReceived || len(c.recipients) == 0 {
-		c.WriteResponse(502, EnhancedCode{5, 5, 1}, "Missing RCPT TO command.")
+		c.writeResponse(502, EnhancedCode{5, 5, 1}, "Missing RCPT TO command.")
 		return
 	}
 
 	// We have recipients, go to accept data
-	c.WriteResponse(354, EnhancedCode{2, 0, 0}, "Go ahead. End your data with <CR><LF>.<CR><LF>")
+	c.writeResponse(354, EnhancedCode{2, 0, 0}, "Go ahead. End your data with <CR><LF>.<CR><LF>")
 
 	defer c.reset()
 
@@ -518,9 +637,182 @@ func (c *Conn) handleData(arg string) {
 
 	r := newDataReader(c)
 	code, enhancedCode, msg := toSMTPStatus(c.Session().Data(r))
+	r.limited = false
 	io.Copy(ioutil.Discard, r) // Make sure all the data has been consumed
-	c.WriteResponse(code, enhancedCode, msg)
+	c.writeResponse(code, enhancedCode, msg)
+}
+
+func (c *Conn) handleBdat(arg string) {
+	args := strings.Fields(arg)
+	if len(args) == 0 {
+		c.writeResponse(501, EnhancedCode{5, 5, 4}, "Missing chunk size argument")
+		return
+	}
+	if len(args) > 2 {
+		c.writeResponse(501, EnhancedCode{5, 5, 4}, "Too many arguments")
+		return
+	}
 
+	if !c.fromReceived || len(c.recipients) == 0 {
+		c.writeResponse(502, EnhancedCode{5, 5, 1}, "Missing RCPT TO command.")
+		return
+	}
+
+	last := false
+	if len(args) == 2 {
+		if !strings.EqualFold(args[1], "LAST") {
+			c.writeResponse(501, EnhancedCode{5, 5, 4}, "Unknown BDAT argument")
+			return
+		}
+		last = true
+	}
+
+	// ParseUint instead of Atoi so we will not accept negative values.
+	size, err := strconv.ParseUint(args[0], 10, 32)
+	if err != nil {
+		c.writeResponse(501, EnhancedCode{5, 5, 4}, "Malformed size argument")
+		return
+	}
+
+	if c.server.MaxMessageBytes != 0 && c.bytesReceived+int(size) > c.server.MaxMessageBytes {
+		c.writeResponse(552, EnhancedCode{5, 3, 4}, "Max message size exceeded")
+
+		// Discard chunk itself without passing it to backend.
+		io.Copy(ioutil.Discard, io.LimitReader(c.text.R, int64(size)))
+
+		c.reset()
+		return
+	}
+
+	if c.bdatStatus == nil && c.server.LMTP {
+		c.bdatStatus = c.createStatusCollector()
+	}
+
+	if c.bdatPipe == nil {
+		var r *io.PipeReader
+		r, c.bdatPipe = io.Pipe()
+
+		c.dataResult = make(chan error, 1)
+
+		go func() {
+			defer func() {
+				if err := recover(); err != nil {
+					c.handlePanic(err, c.bdatStatus)
+
+					c.dataResult <- errPanic
+					r.CloseWithError(errPanic)
+				}
+			}()
+
+			var err error
+			if !c.server.LMTP {
+				err = c.Session().Data(r)
+			} else {
+				lmtpSession, ok := c.Session().(LMTPSession)
+				if !ok {
+					err = c.Session().Data(r)
+					for _, rcpt := range c.recipients {
+						c.bdatStatus.SetStatus(rcpt, err)
+					}
+				} else {
+					err = lmtpSession.LMTPData(r, c.bdatStatus)
+				}
+			}
+
+			c.dataResult <- err
+			r.CloseWithError(err)
+		}()
+	}
+
+	c.lineLimitReader.LineLimit = 0
+
+	chunk := io.LimitReader(c.text.R, int64(size))
+	_, err = io.Copy(c.bdatPipe, chunk)
+	if err != nil {
+		// Backend might return an error early using CloseWithError without consuming
+		// the whole chunk.
+		io.Copy(ioutil.Discard, chunk)
+
+		c.writeResponse(toSMTPStatus(err))
+
+		if err == errPanic {
+			c.Close()
+		}
+
+		c.reset()
+		c.lineLimitReader.LineLimit = c.server.MaxLineLength
+		return
+	}
+
+	c.bytesReceived += int(size)
+
+	if last {
+		c.lineLimitReader.LineLimit = c.server.MaxLineLength
+
+		c.bdatPipe.Close()
+
+		err := <-c.dataResult
+
+		if c.server.LMTP {
+			c.bdatStatus.fillRemaining(err)
+			for i, rcpt := range c.recipients {
+				code, enchCode, msg := toSMTPStatus(<-c.bdatStatus.status[i])
+				c.writeResponse(code, enchCode, "<"+rcpt+"> "+msg)
+			}
+		} else {
+			c.writeResponse(toSMTPStatus(err))
+		}
+
+		if err == errPanic {
+			c.Close()
+			return
+		}
+
+		c.reset()
+	} else {
+		c.writeResponse(250, EnhancedCode{2, 0, 0}, "Continue")
+	}
+}
+
+// ErrDataReset is returned by Reader pased to Data function if client does not
+// send another BDAT command and instead closes connection or issues RSET command.
+var ErrDataReset = errors.New("smtp: message transmission aborted")
+
+var errPanic = &SMTPError{
+	Code:         421,
+	EnhancedCode: EnhancedCode{4, 0, 0},
+	Message:      "Internal server error",
+}
+
+func (c *Conn) handlePanic(err interface{}, status *statusCollector) {
+	if status != nil {
+		status.fillRemaining(errPanic)
+	}
+
+	stack := debug.Stack()
+	c.server.ErrorLog.Printf("panic serving %v: %v\n%s", c.conn.RemoteAddr(), err, stack)
+}
+
+func (c *Conn) createStatusCollector() *statusCollector {
+	rcptCounts := make(map[string]int, len(c.recipients))
+
+	status := &statusCollector{
+		statusMap: make(map[string]chan error, len(c.recipients)),
+		status:    make([]chan error, 0, len(c.recipients)),
+	}
+	for _, rcpt := range c.recipients {
+		rcptCounts[rcpt]++
+	}
+	// Create channels with buffer sizes necessary to fit all
+	// statuses for a single recipient to avoid deadlocks.
+	for rcpt, count := range rcptCounts {
+		status.statusMap[rcpt] = make(chan error, count)
+	}
+	for _, rcpt := range c.recipients {
+		status.status = append(status.status, status.statusMap[rcpt])
+	}
+
+	return status
 }
 
 type statusCollector struct {
@@ -567,24 +859,7 @@ func (s *statusCollector) SetStatus(rcptTo string, err error) {
 
 func (c *Conn) handleDataLMTP() {
 	r := newDataReader(c)
-
-	rcptCounts := make(map[string]int, len(c.recipients))
-
-	status := &statusCollector{
-		statusMap: make(map[string]chan error, len(c.recipients)),
-		status:    make([]chan error, 0, len(c.recipients)),
-	}
-	for _, rcpt := range c.recipients {
-		rcptCounts[rcpt]++
-	}
-	// Create channels with buffer sizes necessary to fit all
-	// statuses for a single recipient to avoid deadlocks.
-	for rcpt, count := range rcptCounts {
-		status.statusMap[rcpt] = make(chan error, count)
-	}
-	for _, rcpt := range c.recipients {
-		status.status = append(status.status, status.statusMap[rcpt])
-	}
+	status := c.createStatusCollector()
 
 	done := make(chan bool, 1)
 
@@ -608,7 +883,7 @@ func (c *Conn) handleDataLMTP() {
 					})
 
 					stack := debug.Stack()
-					c.server.ErrorLog.Printf("panic serving %v: %v\n%s", c.State().RemoteAddr, err, stack)
+					c.server.ErrorLog.Printf("panic serving %v: %v\n%s", c.conn.RemoteAddr(), err, stack)
 					done <- false
 				}
 			}()
@@ -621,7 +896,7 @@ func (c *Conn) handleDataLMTP() {
 
 	for i, rcpt := range c.recipients {
 		code, enchCode, msg := toSMTPStatus(<-status.status[i])
-		c.WriteResponse(code, enchCode, "<"+rcpt+"> "+msg)
+		c.writeResponse(code, enchCode, "<"+rcpt+"> "+msg)
 	}
 
 	// If done gets false, the panic occured in LMTPData and the connection
@@ -644,15 +919,15 @@ func toSMTPStatus(err error) (code int, enchCode EnhancedCode, msg string) {
 }
 
 func (c *Conn) Reject() {
-	c.WriteResponse(421, EnhancedCode{4, 4, 5}, "Too busy. Try again later.")
+	c.writeResponse(421, EnhancedCode{4, 4, 5}, "Too busy. Try again later.")
 	c.Close()
 }
 
 func (c *Conn) greet() {
-	c.WriteResponse(220, NoEnhancedCode, fmt.Sprintf("%v ESMTP Service Ready", c.server.Domain))
+	c.writeResponse(220, NoEnhancedCode, fmt.Sprintf("%v ESMTP Service Ready", c.server.Domain))
 }
 
-func (c *Conn) WriteResponse(code int, enhCode EnhancedCode, text ...string) {
+func (c *Conn) writeResponse(code int, enhCode EnhancedCode, text ...string) {
 	// TODO: error handling
 	if c.server.WriteTimeout != 0 {
 		c.conn.SetWriteDeadline(time.Now().Add(c.server.WriteTimeout))
@@ -671,17 +946,17 @@ func (c *Conn) WriteResponse(code int, enhCode EnhancedCode, text ...string) {
 	}
 
 	for i := 0; i < len(text)-1; i++ {
-		c.text.PrintfLine("%v-%v", code, text[i])
+		c.text.PrintfLine("%d-%v", code, text[i])
 	}
 	if enhCode == NoEnhancedCode {
-		c.text.PrintfLine("%v %v", code, text[len(text)-1])
+		c.text.PrintfLine("%d %v", code, text[len(text)-1])
 	} else {
-		c.text.PrintfLine("%v %v.%v.%v %v", code, enhCode[0], enhCode[1], enhCode[2], text[len(text)-1])
+		c.text.PrintfLine("%d %v.%v.%v %v", code, enhCode[0], enhCode[1], enhCode[2], text[len(text)-1])
 	}
 }
 
 // Reads a line of input
-func (c *Conn) ReadLine() (string, error) {
+func (c *Conn) readLine() (string, error) {
 	if c.server.ReadTimeout != 0 {
 		if err := c.conn.SetReadDeadline(time.Now().Add(c.server.ReadTimeout)); err != nil {
 			return "", err
@@ -695,9 +970,17 @@ func (c *Conn) reset() {
 	c.locker.Lock()
 	defer c.locker.Unlock()
 
+	if c.bdatPipe != nil {
+		c.bdatPipe.CloseWithError(ErrDataReset)
+		c.bdatPipe = nil
+	}
+	c.bdatStatus = nil
+	c.bytesReceived = 0
+
 	if c.session != nil {
 		c.session.Reset()
 	}
+
 	c.fromReceived = false
 	c.recipients = nil
 }
diff --git a/data.go b/data.go
index d65a91a..c338455 100644
--- a/data.go
+++ b/data.go
@@ -1,6 +1,7 @@
 package smtp
 
 import (
+	"bufio"
 	"io"
 )
 
@@ -42,15 +43,16 @@ var ErrDataTooLarge = &SMTPError{
 }
 
 type dataReader struct {
-	r io.Reader
+	r     *bufio.Reader
+	state int
 
 	limited bool
 	n       int64 // Maximum bytes remaining
 }
 
-func newDataReader(c *Conn) io.Reader {
+func newDataReader(c *Conn) *dataReader {
 	dr := &dataReader{
-		r: c.text.DotReader(),
+		r: c.text.R,
 	}
 
 	if c.server.MaxMessageBytes > 0 {
@@ -71,7 +73,72 @@ func (r *dataReader) Read(b []byte) (n int, err error) {
 		}
 	}
 
-	n, err = r.r.Read(b)
+	// Code below is taken from net/textproto with only one modification to
+	// not rewrite CRLF -> LF.
+
+	// Run data through a simple state machine to
+	// elide leading dots and detect ending .\r\n line.
+	const (
+		stateBeginLine = iota // beginning of line; initial state; must be zero
+		stateDot              // read . at beginning of line
+		stateDotCR            // read .\r at beginning of line
+		stateCR               // read \r (possibly at end of line)
+		stateData             // reading data in middle of line
+		stateEOF              // reached .\r\n end marker line
+	)
+	for n < len(b) && r.state != stateEOF {
+		var c byte
+		c, err = r.r.ReadByte()
+		if err != nil {
+			if err == io.EOF {
+				err = io.ErrUnexpectedEOF
+			}
+			break
+		}
+		switch r.state {
+		case stateBeginLine:
+			if c == '.' {
+				r.state = stateDot
+				continue
+			}
+			r.state = stateData
+		case stateDot:
+			if c == '\r' {
+				r.state = stateDotCR
+				continue
+			}
+			if c == '\n' {
+				r.state = stateEOF
+				continue
+			}
+
+			r.state = stateData
+		case stateDotCR:
+			if c == '\n' {
+				r.state = stateEOF
+				continue
+			}
+			r.state = stateData
+		case stateCR:
+			if c == '\n' {
+				r.state = stateBeginLine
+				break
+			}
+			r.state = stateData
+		case stateData:
+			if c == '\r' {
+				r.state = stateCR
+			}
+			if c == '\n' {
+				r.state = stateBeginLine
+			}
+		}
+		b[n] = c
+		n++
+	}
+	if err == nil && r.state == stateEOF {
+		err = io.EOF
+	}
 
 	if r.limited {
 		r.n -= int64(n)
diff --git a/debian/changelog b/debian/changelog
index f39ea13..6d41f4c 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,10 @@
+golang-github-emersion-go-smtp (0.16.0-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Tue, 16 May 2023 11:51:02 -0000
+
 golang-github-emersion-go-smtp (0.12.1-1) unstable; urgency=medium
 
   * Team Upload.
diff --git a/example_test.go b/example_test.go
index 607c971..d747688 100644
--- a/example_test.go
+++ b/example_test.go
@@ -92,23 +92,23 @@ func ExampleSendMail() {
 // The Backend implements SMTP server methods.
 type Backend struct{}
 
-// Login handles a login command with username and password.
-func (bkd *Backend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) {
-	if username != "username" || password != "password" {
-		return nil, errors.New("Invalid username or password")
-	}
+// NewSession is called after client greeting (EHLO, HELO).
+func (bkd *Backend) NewSession(c *smtp.Conn) (smtp.Session, error) {
 	return &Session{}, nil
 }
 
-// AnonymousLogin requires clients to authenticate using SMTP AUTH before sending emails
-func (bkd *Backend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
-	return nil, smtp.ErrAuthRequired
-}
-
 // A Session is returned after successful login.
 type Session struct{}
 
-func (s *Session) Mail(from string, opts smtp.MailOptions) error {
+// AuthPlain implements authentication using SASL PLAIN.
+func (s *Session) AuthPlain(username, password string) error {
+	if username != "username" || password != "password" {
+		return errors.New("Invalid username or password")
+	}
+	return nil
+}
+
+func (s *Session) Mail(from string, opts *smtp.MailOptions) error {
 	log.Println("Mail from:", from)
 	return nil
 }
diff --git a/go.mod b/go.mod
index d314e5f..88097d6 100644
--- a/go.mod
+++ b/go.mod
@@ -1,5 +1,5 @@
 module github.com/emersion/go-smtp
 
-require github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e
+require github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
 
 go 1.13
diff --git a/go.sum b/go.sum
index 1f5c09c..8e0463a 100644
--- a/go.sum
+++ b/go.sum
@@ -1,2 +1,2 @@
-github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e h1:ba7YsgX5OV8FjGi5ZWml8Jng6oBrJAb3ahqWMJ5Ce8Q=
-github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
+github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
+github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
diff --git a/lengthlimit_reader.go b/lengthlimit_reader.go
index 992c3c8..1513e56 100644
--- a/lengthlimit_reader.go
+++ b/lengthlimit_reader.go
@@ -5,7 +5,7 @@ import (
 	"io"
 )
 
-var ErrTooLongLine = errors.New("smtp: too longer line in input stream")
+var ErrTooLongLine = errors.New("smtp: too long a line in input stream")
 
 // lineLimitReader reads from the underlying Reader but restricts
 // line length of lines in input stream to a certain length.
@@ -18,8 +18,8 @@ type lineLimitReader struct {
 	curLineLength int
 }
 
-func (r lineLimitReader) Read(b []byte) (int, error) {
-	if r.curLineLength > r.LineLimit {
+func (r *lineLimitReader) Read(b []byte) (int, error) {
+	if r.curLineLength > r.LineLimit && r.LineLimit > 0 {
 		return 0, ErrTooLongLine
 	}
 
diff --git a/parse.go b/parse.go
index df73786..dc7e77f 100644
--- a/parse.go
+++ b/parse.go
@@ -37,7 +37,9 @@ func parseCmd(line string) (cmd string, arg string, err error) {
 // Takes the arguments proceeding a command and files them
 // into a map[string]string after uppercasing each key.  Sample arg
 // string:
-//		" BODY=8BITMIME SIZE=1024 SMTPUTF8"
+//
+//	" BODY=8BITMIME SIZE=1024 SMTPUTF8"
+//
 // The leading space is mandatory.
 func parseArgs(args []string) (map[string]string, error) {
 	argMap := map[string]string{}
diff --git a/server.go b/server.go
old mode 100755
new mode 100644
index c83ff0b..82cc422
--- a/server.go
+++ b/server.go
@@ -49,10 +49,14 @@ type Server struct {
 	// Should be used only if backend supports it.
 	EnableSMTPUTF8 bool
 
-	// Advertise REQUIRETLS (draft-ietf-uta-smtp-require-tls-09) capability.
+	// Advertise REQUIRETLS (RFC 8689) capability.
 	// Should be used only if backend supports it.
 	EnableREQUIRETLS bool
 
+	// Advertise BINARYMIME (RFC 3030) capability.
+	// Should be used only if backend supports it.
+	EnableBINARYMIME bool
+
 	// If set, the AUTH command will not be advertised and authentication
 	// attempts will be rejected. This setting overrides AllowInsecureAuth.
 	AuthDisabled bool
@@ -60,13 +64,13 @@ type Server struct {
 	// The server backend.
 	Backend Backend
 
-	listeners []net.Listener
-	caps      []string
-	auths     map[string]SaslServerFactory
-	done      chan struct{}
+	caps  []string
+	auths map[string]SaslServerFactory
+	done  chan struct{}
 
-	locker sync.Mutex
-	conns  map[*Conn]struct{}
+	locker    sync.Mutex
+	listeners []net.Listener
+	conns     map[*Conn]struct{}
 }
 
 // New creates a new SMTP server.
@@ -78,7 +82,7 @@ func NewServer(be Backend) *Server {
 		Backend:  be,
 		done:     make(chan struct{}, 1),
 		ErrorLog: log.New(os.Stderr, "smtp/server ", log.LstdFlags),
-		caps:     []string{"PIPELINING", "8BITMIME", "ENHANCEDSTATUSCODES"},
+		caps:     []string{"PIPELINING", "8BITMIME", "ENHANCEDSTATUSCODES", "CHUNKING"},
 		auths: map[string]SaslServerFactory{
 			sasl.Plain: func(conn *Conn) sasl.Server {
 				return sasl.NewPlainServer(func(identity, username, password string) error {
@@ -86,14 +90,12 @@ func NewServer(be Backend) *Server {
 						return errors.New("Identities not supported")
 					}
 
-					state := conn.State()
-					session, err := be.Login(&state, username, password)
-					if err != nil {
-						return err
+					sess := conn.Session()
+					if sess == nil {
+						panic("No session when AUTH is called")
 					}
 
-					conn.SetSession(session)
-					return nil
+					return sess.AuthPlain(username, password)
 				})
 			},
 		},
@@ -103,7 +105,11 @@ func NewServer(be Backend) *Server {
 
 // Serve accepts incoming connections on the Listener l.
 func (s *Server) Serve(l net.Listener) error {
+	s.locker.Lock()
 	s.listeners = append(s.listeners, l)
+	s.locker.Unlock()
+
+	var tempDelay time.Duration // how long to sleep on accept failure
 
 	for {
 		c, err := l.Accept()
@@ -113,11 +119,28 @@ func (s *Server) Serve(l net.Listener) error {
 				// we called Close()
 				return nil
 			default:
-				return err
 			}
+			if ne, ok := err.(net.Error); ok && ne.Temporary() {
+				if tempDelay == 0 {
+					tempDelay = 5 * time.Millisecond
+				} else {
+					tempDelay *= 2
+				}
+				if max := 1 * time.Second; tempDelay > max {
+					tempDelay = max
+				}
+				s.ErrorLog.Printf("accept error: %s; retrying in %s", err, tempDelay)
+				time.Sleep(tempDelay)
+				continue
+			}
+			return err
 		}
-
-		go s.handleConn(newConn(c, s))
+		go func() {
+			err := s.handleConn(newConn(c, s))
+			if err != nil {
+				s.ErrorLog.Printf("handler error: %s", err)
+			}
+		}()
 	}
 }
 
@@ -134,34 +157,45 @@ func (s *Server) handleConn(c *Conn) error {
 		s.locker.Unlock()
 	}()
 
+	if tlsConn, ok := c.conn.(*tls.Conn); ok {
+		if d := s.ReadTimeout; d != 0 {
+			c.conn.SetReadDeadline(time.Now().Add(d))
+		}
+		if d := s.WriteTimeout; d != 0 {
+			c.conn.SetWriteDeadline(time.Now().Add(d))
+		}
+		if err := tlsConn.Handshake(); err != nil {
+			return err
+		}
+	}
+
 	c.greet()
 
 	for {
-		line, err := c.ReadLine()
+		line, err := c.readLine()
 		if err == nil {
 			cmd, arg, err := parseCmd(line)
 			if err != nil {
-				c.nbrErrors++
-				c.WriteResponse(501, EnhancedCode{5, 5, 2}, "Bad command")
+				c.protocolError(501, EnhancedCode{5, 5, 2}, "Bad command")
 				continue
 			}
 
 			c.handle(cmd, arg)
 		} else {
-			if err == io.EOF {
+			if err == io.EOF || errors.Is(err, net.ErrClosed) {
 				return nil
 			}
 			if err == ErrTooLongLine {
-				c.WriteResponse(500, EnhancedCode{5, 4, 0}, "Too long line, closing connection")
+				c.writeResponse(500, EnhancedCode{5, 4, 0}, "Too long line, closing connection")
 				return nil
 			}
 
 			if neterr, ok := err.(net.Error); ok && neterr.Timeout() {
-				c.WriteResponse(221, EnhancedCode{2, 4, 2}, "Idle timeout, bye bye")
+				c.writeResponse(221, EnhancedCode{2, 4, 2}, "Idle timeout, bye bye")
 				return nil
 			}
 
-			c.WriteResponse(221, EnhancedCode{2, 4, 0}, "Connection error, sorry")
+			c.writeResponse(221, EnhancedCode{2, 4, 0}, "Connection error, sorry")
 			return err
 		}
 	}
@@ -212,19 +246,32 @@ func (s *Server) ListenAndServeTLS() error {
 	return s.Serve(l)
 }
 
-// Close stops the server.
-func (s *Server) Close() {
-	close(s.done)
-	for _, l := range s.listeners {
-		l.Close()
+// Close immediately closes all active listeners and connections.
+//
+// Close returns any error returned from closing the server's underlying
+// listener(s).
+func (s *Server) Close() error {
+	select {
+	case <-s.done:
+		return errors.New("smtp: server already closed")
+	default:
+		close(s.done)
 	}
 
+	var err error
 	s.locker.Lock()
-	defer s.locker.Unlock()
+	for _, l := range s.listeners {
+		if lerr := l.Close(); lerr != nil && err == nil {
+			err = lerr
+		}
+	}
 
 	for conn := range s.conns {
 		conn.Close()
 	}
+	s.locker.Unlock()
+
+	return err
 }
 
 // EnableAuth enables an authentication mechanism on this server.
diff --git a/server_test.go b/server_test.go
index b575c7d..88319e1 100644
--- a/server_test.go
+++ b/server_test.go
@@ -2,6 +2,7 @@ package smtp_test
 
 import (
 	"bufio"
+	"bytes"
 	"errors"
 	"io"
 	"io/ioutil"
@@ -17,6 +18,7 @@ type message struct {
 	From string
 	To   []string
 	Data []byte
+	Opts *smtp.MailOptions
 }
 
 type backend struct {
@@ -30,31 +32,20 @@ type backend struct {
 	}
 	lmtpStatusSync chan struct{}
 
-	panicOnMail bool
-	userErr     error
-}
+	// Errors returned by Data method.
+	dataErrors chan error
 
-func (be *backend) Login(_ *smtp.ConnectionState, username, password string) (smtp.Session, error) {
-	if be.userErr != nil {
-		return &session{}, be.userErr
-	}
+	// Error that will be returned by Data method.
+	dataErr error
 
-	if username != "username" || password != "password" {
-		return nil, errors.New("Invalid username or password")
-	}
+	// Read N bytes of message before returning dataErr.
+	dataErrOffset int64
 
-	if be.implementLMTPData {
-		return &lmtpSession{&session{backend: be}}, nil
-	}
-
-	return &session{backend: be}, nil
+	panicOnMail bool
+	userErr     error
 }
 
-func (be *backend) AnonymousLogin(_ *smtp.ConnectionState) (smtp.Session, error) {
-	if be.userErr != nil {
-		return &session{}, be.userErr
-	}
-
+func (be *backend) NewSession(_ *smtp.Conn) (smtp.Session, error) {
 	if be.implementLMTPData {
 		return &lmtpSession{&session{backend: be, anonymous: true}}, nil
 	}
@@ -73,6 +64,14 @@ type session struct {
 	msg *message
 }
 
+func (s *session) AuthPlain(username, password string) error {
+	if username != "username" || password != "password" {
+		return errors.New("Invalid username or password")
+	}
+	s.anonymous = false
+	return nil
+}
+
 func (s *session) Reset() {
 	s.msg = &message{}
 }
@@ -81,12 +80,16 @@ func (s *session) Logout() error {
 	return nil
 }
 
-func (s *session) Mail(from string, opts smtp.MailOptions) error {
+func (s *session) Mail(from string, opts *smtp.MailOptions) error {
+	if s.backend.userErr != nil {
+		return s.backend.userErr
+	}
 	if s.backend.panicOnMail {
 		panic("Everything is on fire!")
 	}
 	s.Reset()
 	s.msg.From = from
+	s.msg.Opts = opts
 	return nil
 }
 
@@ -96,7 +99,23 @@ func (s *session) Rcpt(to string) error {
 }
 
 func (s *session) Data(r io.Reader) error {
+	if s.backend.dataErr != nil {
+
+		if s.backend.dataErrOffset != 0 {
+			io.CopyN(ioutil.Discard, r, s.backend.dataErrOffset)
+		}
+
+		err := s.backend.dataErr
+		if s.backend.dataErrors != nil {
+			s.backend.dataErrors <- err
+		}
+		return err
+	}
+
 	if b, err := ioutil.ReadAll(r); err != nil {
+		if s.backend.dataErrors != nil {
+			s.backend.dataErrors <- err
+		}
 		return err
 	} else {
 		s.msg.Data = b
@@ -105,6 +124,9 @@ func (s *session) Data(r io.Reader) error {
 		} else {
 			s.backend.messages = append(s.backend.messages, s.msg)
 		}
+		if s.backend.dataErrors != nil {
+			s.backend.dataErrors <- nil
+		}
 	}
 	return nil
 }
@@ -125,6 +147,57 @@ func (s *session) LMTPData(r io.Reader, collector smtp.StatusCollector) error {
 	return nil
 }
 
+type failingListener struct {
+	c      chan error
+	closed bool
+}
+
+func newFailingListener() *failingListener {
+	return &failingListener{c: make(chan error)}
+}
+
+func (l *failingListener) Send(err error) {
+	if !l.closed {
+		l.c <- err
+	}
+}
+
+func (l *failingListener) Accept() (net.Conn, error) {
+	return nil, <-l.c
+}
+
+func (l *failingListener) Close() error {
+	if !l.closed {
+		close(l.c)
+		l.closed = true
+	}
+	return nil
+}
+
+func (l *failingListener) Addr() net.Addr {
+	return &net.TCPAddr{
+		IP:   net.ParseIP("127.0.0.1"),
+		Port: 12345,
+	}
+}
+
+type mockError struct {
+	msg       string
+	temporary bool
+}
+
+func newMockError(msg string, temporary bool) *mockError {
+	return &mockError{
+		msg:       msg,
+		temporary: temporary,
+	}
+}
+
+func (m *mockError) Error() string   { return m.msg }
+func (m *mockError) String() string  { return m.msg }
+func (m *mockError) Timeout() bool   { return false }
+func (m *mockError) Temporary() bool { return m.temporary }
+
 type serverConfigureFunc func(*smtp.Server)
 
 var (
@@ -205,6 +278,37 @@ func testServerEhlo(t *testing.T, fn ...serverConfigureFunc) (be *backend, s *sm
 	return
 }
 
+func TestServerAcceptErrorHandling(t *testing.T) {
+	errorLog := bytes.NewBuffer(nil)
+	be := new(backend)
+	s := smtp.NewServer(be)
+	s.Domain = "localhost"
+	s.AllowInsecureAuth = true
+	s.ErrorLog = log.New(errorLog, "", 0)
+
+	l := newFailingListener()
+	var serveError error
+	go func() {
+		serveError = s.Serve(l)
+		l.Close()
+	}()
+
+	temporaryError := newMockError("temporary mock error", true)
+	l.Send(temporaryError)
+	permanentError := newMockError("permanent mock error", false)
+	l.Send(permanentError)
+	s.Close()
+
+	if serveError == nil {
+		t.Fatal("Serve had exited without an expected error")
+	} else if serveError != permanentError {
+		t.Fatal("Unexpected error:", serveError)
+	}
+	if !strings.Contains(errorLog.String(), temporaryError.String()) {
+		t.Fatal("Missing temporary error in log output:", errorLog.String())
+	}
+}
+
 func TestServer_helo(t *testing.T) {
 	_, s, c, scanner := testServerGreeted(t)
 	defer s.Close()
@@ -239,6 +343,58 @@ func testServerAuthenticated(t *testing.T) (be *backend, s *smtp.Server, c net.C
 	return
 }
 
+func TestServerAuthTwice(t *testing.T) {
+	_, _, c, scanner, caps := testServerEhlo(t)
+
+	if _, ok := caps["AUTH PLAIN"]; !ok {
+		t.Fatal("AUTH PLAIN capability is missing when auth is enabled")
+	}
+
+	io.WriteString(c, "AUTH PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "235 ") {
+		t.Fatal("Invalid AUTH response:", scanner.Text())
+	}
+
+	io.WriteString(c, "AUTH PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "503 ") {
+		t.Fatal("Invalid AUTH response:", scanner.Text())
+	}
+
+	io.WriteString(c, "RSET\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid AUTH response:", scanner.Text())
+	}
+
+	io.WriteString(c, "AUTH PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "503 ") {
+		t.Fatal("Invalid AUTH response:", scanner.Text())
+	}
+}
+
+func TestServerCancelSASL(t *testing.T) {
+	_, _, c, scanner, caps := testServerEhlo(t)
+
+	if _, ok := caps["AUTH PLAIN"]; !ok {
+		t.Fatal("AUTH PLAIN capability is missing when auth is enabled")
+	}
+
+	io.WriteString(c, "AUTH PLAIN\r\n")
+	scanner.Scan()
+	if scanner.Text() != "334 " {
+		t.Fatal("Invalid AUTH response:", scanner.Text())
+	}
+
+	io.WriteString(c, "*\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "501 ") {
+		t.Fatal("Invalid AUTH response:", scanner.Text())
+	}
+}
+
 func TestServerEmptyFrom1(t *testing.T) {
 	_, s, c, scanner := testServerAuthenticated(t)
 	defer s.Close()
@@ -249,8 +405,6 @@ func TestServerEmptyFrom1(t *testing.T) {
 	if strings.HasPrefix(scanner.Text(), "250 ") {
 		t.Fatal("Invalid MAIL response:", scanner.Text())
 	}
-
-	return
 }
 
 func TestServerEmptyFrom2(t *testing.T) {
@@ -263,8 +417,6 @@ func TestServerEmptyFrom2(t *testing.T) {
 	if !strings.HasPrefix(scanner.Text(), "250 ") {
 		t.Fatal("Invalid MAIL response:", scanner.Text())
 	}
-
-	return
 }
 
 func TestServerPanicRecover(t *testing.T) {
@@ -281,8 +433,6 @@ func TestServerPanicRecover(t *testing.T) {
 	if !strings.HasPrefix(scanner.Text(), "421 ") {
 		t.Fatal("Invalid MAIL response:", scanner.Text())
 	}
-
-	return
 }
 
 func TestServerSMTPUTF8(t *testing.T) {
@@ -296,8 +446,6 @@ func TestServerSMTPUTF8(t *testing.T) {
 	if !strings.HasPrefix(scanner.Text(), "250 ") {
 		t.Fatal("Invalid MAIL response:", scanner.Text())
 	}
-
-	return
 }
 
 func TestServerSMTPUTF8_Disabled(t *testing.T) {
@@ -310,8 +458,6 @@ func TestServerSMTPUTF8_Disabled(t *testing.T) {
 	if strings.HasPrefix(scanner.Text(), "250 ") {
 		t.Fatal("Invalid MAIL response:", scanner.Text())
 	}
-
-	return
 }
 
 func TestServer8BITMIME(t *testing.T) {
@@ -324,8 +470,6 @@ func TestServer8BITMIME(t *testing.T) {
 	if !strings.HasPrefix(scanner.Text(), "250 ") {
 		t.Fatal("Invalid MAIL response:", scanner.Text())
 	}
-
-	return
 }
 
 func TestServer_BODYInvalidValue(t *testing.T) {
@@ -338,8 +482,6 @@ func TestServer_BODYInvalidValue(t *testing.T) {
 	if strings.HasPrefix(scanner.Text(), "250 ") {
 		t.Fatal("Invalid MAIL response:", scanner.Text())
 	}
-
-	return
 }
 
 func TestServerUnknownArg(t *testing.T) {
@@ -352,8 +494,6 @@ func TestServerUnknownArg(t *testing.T) {
 	if strings.HasPrefix(scanner.Text(), "250 ") {
 		t.Fatal("Invalid MAIL response:", scanner.Text())
 	}
-
-	return
 }
 
 func TestServerBadSize(t *testing.T) {
@@ -366,8 +506,6 @@ func TestServerBadSize(t *testing.T) {
 	if strings.HasPrefix(scanner.Text(), "250 ") {
 		t.Fatal("Invalid MAIL response:", scanner.Text())
 	}
-
-	return
 }
 
 func TestServerTooBig(t *testing.T) {
@@ -380,8 +518,6 @@ func TestServerTooBig(t *testing.T) {
 	if strings.HasPrefix(scanner.Text(), "250 ") {
 		t.Fatal("Invalid MAIL response:", scanner.Text())
 	}
-
-	return
 }
 
 func TestServerEmptyTo(t *testing.T) {
@@ -400,8 +536,6 @@ func TestServerEmptyTo(t *testing.T) {
 	if strings.HasPrefix(scanner.Text(), "250 ") {
 		t.Fatal("Invalid RCPT response:", scanner.Text())
 	}
-
-	return
 }
 
 func TestServer(t *testing.T) {
@@ -427,7 +561,10 @@ func TestServer(t *testing.T) {
 		t.Fatal("Invalid DATA response:", scanner.Text())
 	}
 
-	io.WriteString(c, "Hey <3\r\n")
+	io.WriteString(c, "From: root@nsa.gov\r\n")
+	io.WriteString(c, "\r\n")
+	io.WriteString(c, "Hey\r <3\r\n")
+	io.WriteString(c, "..this dot is fine\r\n")
 	io.WriteString(c, ".\r\n")
 	scanner.Scan()
 	if !strings.HasPrefix(scanner.Text(), "250 ") {
@@ -445,7 +582,51 @@ func TestServer(t *testing.T) {
 	if len(msg.To) != 1 || msg.To[0] != "root@gchq.gov.uk" {
 		t.Fatal("Invalid mail recipients:", msg.To)
 	}
-	if string(msg.Data) != "Hey <3\n" {
+	if string(msg.Data) != "From: root@nsa.gov\r\n\r\nHey\r <3\r\n.this dot is fine\r\n" {
+		t.Fatal("Invalid mail data:", string(msg.Data))
+	}
+}
+
+func TestServer_LFDotLF(t *testing.T) {
+	be, s, c, scanner := testServerAuthenticated(t)
+	defer s.Close()
+	defer c.Close()
+
+	io.WriteString(c, "MAIL FROM:<root@nsa.gov>\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid MAIL response:", scanner.Text())
+	}
+
+	io.WriteString(c, "RCPT TO:<root@gchq.gov.uk>\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid RCPT response:", scanner.Text())
+	}
+
+	io.WriteString(c, "DATA\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "354 ") {
+		t.Fatal("Invalid DATA response:", scanner.Text())
+	}
+
+	io.WriteString(c, "From: root@nsa.gov\r\n")
+	io.WriteString(c, "\r\n")
+	io.WriteString(c, "hey\r\n")
+	io.WriteString(c, "\n.\n")
+	io.WriteString(c, "this is going to break your server\r\n")
+	io.WriteString(c, ".\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid DATA response:", scanner.Text())
+	}
+
+	if len(be.messages) != 1 || len(be.anonmsgs) != 0 {
+		t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs)
+	}
+
+	msg := be.messages[0]
+	if string(msg.Data) != "From: root@nsa.gov\r\n\r\nhey\r\n\n.\nthis is going to break your server\r\n" {
 		t.Fatal("Invalid mail data:", string(msg.Data))
 	}
 }
@@ -596,6 +777,56 @@ func TestServer_anonymousUserOK(t *testing.T) {
 	}
 }
 
+func TestServer_authParam(t *testing.T) {
+	be, s, c, scanner, _ := testServerEhlo(t)
+	defer s.Close()
+	defer c.Close()
+
+	// Invalid HEXCHAR
+	io.WriteString(c, "MAIL FROM: root@nsa.gov AUTH=<hey+A>\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "500 ") {
+		t.Fatal("Invalid MAIL response:", scanner.Text())
+	}
+
+	// Invalid HEXCHAR
+	io.WriteString(c, "MAIL FROM: root@nsa.gov AUTH=<he+YYa>\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "500 ") {
+		t.Fatal("Invalid MAIL response:", scanner.Text())
+	}
+
+	// https://tools.ietf.org/html/rfc4954#section-4
+	// >servers that advertise support for this
+	// >extension MUST support the AUTH parameter to the MAIL FROM
+	// >command even when the client has not authenticated itself to the
+	// >server.
+	io.WriteString(c, "MAIL FROM: root@nsa.gov AUTH=<hey+3Da>\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid MAIL response:", scanner.Text())
+	}
+
+	// Go on as usual.
+	io.WriteString(c, "RCPT TO:<root@gchq.gov.uk>\r\n")
+	scanner.Scan()
+	io.WriteString(c, "DATA\r\n")
+	scanner.Scan()
+	io.WriteString(c, "Hey <3\r\n")
+	io.WriteString(c, ".\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid DATA response:", scanner.Text())
+	}
+
+	if len(be.messages) != 0 || len(be.anonmsgs) != 1 {
+		t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs)
+	}
+	if val := be.anonmsgs[0].Opts.Auth; val == nil || *val != "hey=a" {
+		t.Fatal("Invalid Auth value:", val)
+	}
+}
+
 func testStrictServer(t *testing.T) (s *smtp.Server, c net.Conn, scanner *bufio.Scanner) {
 	l, err := net.Listen("tcp", "127.0.0.1:0")
 	if err != nil {
@@ -678,3 +909,328 @@ func TestStrictServerBad(t *testing.T) {
 		t.Fatal("Invalid MAIL response:", scanner.Text())
 	}
 }
+
+func TestServer_Chunking(t *testing.T) {
+	be, s, c, scanner := testServerAuthenticated(t)
+	defer s.Close()
+	defer c.Close()
+
+	io.WriteString(c, "MAIL FROM:<root@nsa.gov>\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid MAIL response:", scanner.Text())
+	}
+
+	io.WriteString(c, "RCPT TO:<root@gchq.gov.uk>\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid RCPT response:", scanner.Text())
+	}
+
+	io.WriteString(c, "BDAT 8\r\n")
+	io.WriteString(c, "Hey <3\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid BDAT response:", scanner.Text())
+	}
+
+	io.WriteString(c, "BDAT 8 LAST\r\n")
+	io.WriteString(c, "Hey :3\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid BDAT response:", scanner.Text())
+	}
+
+	if len(be.messages) != 1 || len(be.anonmsgs) != 0 {
+		t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs)
+	}
+
+	msg := be.messages[0]
+	if msg.From != "root@nsa.gov" {
+		t.Fatal("Invalid mail sender:", msg.From)
+	}
+	if len(msg.To) != 1 || msg.To[0] != "root@gchq.gov.uk" {
+		t.Fatal("Invalid mail recipients:", msg.To)
+	}
+	if want := "Hey <3\r\nHey :3\r\n"; string(msg.Data) != want {
+		t.Fatal("Invalid mail data:", string(msg.Data), msg.Data)
+	}
+}
+
+func TestServer_Chunking_LMTP(t *testing.T) {
+	be, s, c, scanner := testServerAuthenticated(t)
+	s.LMTP = true
+	defer s.Close()
+	defer c.Close()
+
+	io.WriteString(c, "MAIL FROM:<root@nsa.gov>\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid MAIL response:", scanner.Text())
+	}
+
+	io.WriteString(c, "RCPT TO:<root@gchq.gov.uk>\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid RCPT response:", scanner.Text())
+	}
+	io.WriteString(c, "RCPT TO:<toor@gchq.gov.uk>\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid RCPT response:", scanner.Text())
+	}
+
+	io.WriteString(c, "BDAT 8\r\n")
+	io.WriteString(c, "Hey <3\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid BDAT response:", scanner.Text())
+	}
+
+	io.WriteString(c, "BDAT 8 LAST\r\n")
+	io.WriteString(c, "Hey :3\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid BDAT response:", scanner.Text())
+	}
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid BDAT response:", scanner.Text())
+	}
+
+	if len(be.messages) != 1 || len(be.anonmsgs) != 0 {
+		t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs)
+	}
+
+	msg := be.messages[0]
+	if msg.From != "root@nsa.gov" {
+		t.Fatal("Invalid mail sender:", msg.From)
+	}
+	if want := "Hey <3\r\nHey :3\r\n"; string(msg.Data) != want {
+		t.Fatal("Invalid mail data:", string(msg.Data), msg.Data)
+	}
+}
+
+func TestServer_Chunking_Reset(t *testing.T) {
+	be, s, c, scanner := testServerAuthenticated(t)
+	defer s.Close()
+	defer c.Close()
+	be.dataErrors = make(chan error, 10)
+
+	io.WriteString(c, "MAIL FROM:<root@nsa.gov>\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid MAIL response:", scanner.Text())
+	}
+
+	io.WriteString(c, "RCPT TO:<root@gchq.gov.uk>\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid RCPT response:", scanner.Text())
+	}
+
+	io.WriteString(c, "BDAT 8\r\n")
+	io.WriteString(c, "Hey <3\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid BDAT response:", scanner.Text())
+	}
+
+	// Client changed its mind... Note, in this case Data method error is discarded and not returned to the cilent.
+	io.WriteString(c, "RSET\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid BDAT response:", scanner.Text())
+	}
+
+	if err := <-be.dataErrors; err != smtp.ErrDataReset {
+		t.Fatal("Backend received a different error:", err)
+	}
+}
+
+func TestServer_Chunking_ClosedInTheMiddle(t *testing.T) {
+	be, s, c, scanner := testServerAuthenticated(t)
+	defer s.Close()
+	defer c.Close()
+	be.dataErrors = make(chan error, 10)
+
+	io.WriteString(c, "MAIL FROM:<root@nsa.gov>\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid MAIL response:", scanner.Text())
+	}
+
+	io.WriteString(c, "RCPT TO:<root@gchq.gov.uk>\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid RCPT response:", scanner.Text())
+	}
+
+	io.WriteString(c, "BDAT 8\r\n")
+	io.WriteString(c, "Hey <")
+
+	// Bye!
+	c.Close()
+
+	if err := <-be.dataErrors; err != smtp.ErrDataReset {
+		t.Fatal("Backend received a different error:", err)
+	}
+}
+
+func TestServer_Chunking_EarlyError(t *testing.T) {
+	be, s, c, scanner := testServerAuthenticated(t)
+	defer s.Close()
+	defer c.Close()
+
+	be.dataErr = &smtp.SMTPError{
+		Code:         555,
+		EnhancedCode: smtp.EnhancedCode{5, 0, 0},
+		Message:      "I failed",
+	}
+
+	io.WriteString(c, "MAIL FROM:<root@nsa.gov>\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid MAIL response:", scanner.Text())
+	}
+
+	io.WriteString(c, "RCPT TO:<root@gchq.gov.uk>\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid RCPT response:", scanner.Text())
+	}
+
+	io.WriteString(c, "BDAT 8\r\n")
+	io.WriteString(c, "Hey <3\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "555 5.0.0 I failed") {
+		t.Fatal("Invalid BDAT response:", scanner.Text())
+	}
+}
+
+func TestServer_Chunking_EarlyErrorDuringChunk(t *testing.T) {
+	be, s, c, scanner := testServerAuthenticated(t)
+	defer s.Close()
+	defer c.Close()
+
+	be.dataErr = &smtp.SMTPError{
+		Code:         555,
+		EnhancedCode: smtp.EnhancedCode{5, 0, 0},
+		Message:      "I failed",
+	}
+	be.dataErrOffset = 5
+
+	io.WriteString(c, "MAIL FROM:<root@nsa.gov>\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid MAIL response:", scanner.Text())
+	}
+
+	io.WriteString(c, "RCPT TO:<root@gchq.gov.uk>\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid RCPT response:", scanner.Text())
+	}
+
+	io.WriteString(c, "BDAT 8\r\n")
+	io.WriteString(c, "Hey <3\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "555 5.0.0 I failed") {
+		t.Fatal("Invalid BDAT response:", scanner.Text())
+	}
+
+	// See that command stream state is not corrupted e.g. server is still not
+	// waiting for remaining chunk octets.
+	io.WriteString(c, "NOOP\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid RCPT response:", scanner.Text())
+	}
+}
+
+func TestServer_Chunking_tooLongMessage(t *testing.T) {
+	be, s, c, scanner := testServerAuthenticated(t)
+	defer s.Close()
+
+	s.MaxMessageBytes = 50
+
+	io.WriteString(c, "MAIL FROM:<root@nsa.gov>\r\n")
+	scanner.Scan()
+	io.WriteString(c, "RCPT TO:<root@gchq.gov.uk>\r\n")
+	scanner.Scan()
+	io.WriteString(c, "BDAT 30\r\n")
+	io.WriteString(c, "This is a very long message.\r\n")
+	scanner.Scan()
+
+	io.WriteString(c, "BDAT 96 LAST\r\n")
+	io.WriteString(c, "Much longer than you can possibly imagine.\r\n")
+	io.WriteString(c, "And much longer than the server's MaxMessageBytes.\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "552 ") {
+		t.Fatal("Invalid DATA response, expected an error but got:", scanner.Text())
+	}
+
+	if len(be.messages) != 0 || len(be.anonmsgs) != 0 {
+		t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs)
+	}
+}
+
+func TestServer_Chunking_Binarymime(t *testing.T) {
+	be, s, c, scanner := testServerAuthenticated(t)
+	defer s.Close()
+	defer c.Close()
+	s.EnableBINARYMIME = true
+
+	io.WriteString(c, "MAIL FROM:<root@nsa.gov> BODY=BINARYMIME\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid MAIL response:", scanner.Text())
+	}
+
+	io.WriteString(c, "RCPT TO:<root@gchq.gov.uk>\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid RCPT response:", scanner.Text())
+	}
+
+	io.WriteString(c, "BDAT 8\r\n")
+	io.WriteString(c, "Hey <3\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid BDAT response:", scanner.Text())
+	}
+
+	io.WriteString(c, "BDAT 8 LAST\r\n")
+	io.WriteString(c, "Hey :3\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "250 ") {
+		t.Fatal("Invalid BDAT response:", scanner.Text())
+	}
+
+	if len(be.messages) != 1 || len(be.anonmsgs) != 0 {
+		t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs)
+	}
+
+	msg := be.messages[0]
+	if msg.From != "root@nsa.gov" {
+		t.Fatal("Invalid mail sender:", msg.From)
+	}
+	if len(msg.To) != 1 || msg.To[0] != "root@gchq.gov.uk" {
+		t.Fatal("Invalid mail recipients:", msg.To)
+	}
+	if want := "Hey <3\r\nHey :3\r\n"; string(msg.Data) != want {
+		t.Fatal("Invalid mail data:", string(msg.Data), msg.Data)
+	}
+}
+
+func TestServer_TooLongCommand(t *testing.T) {
+	_, s, c, scanner := testServerAuthenticated(t)
+	defer s.Close()
+	defer c.Close()
+
+	io.WriteString(c, "MAIL FROM:<"+strings.Repeat("a", s.MaxLineLength)+">\r\n")
+	scanner.Scan()
+	if !strings.HasPrefix(scanner.Text(), "500 5.4.0 ") {
+		t.Fatal("Invalid too long MAIL response:", scanner.Text())
+	}
+}
diff --git a/smtp.go b/smtp.go
index e5045a7..36963cb 100644
--- a/smtp.go
+++ b/smtp.go
@@ -2,12 +2,14 @@
 //
 // It also implements the following extensions:
 //
-//	8BITMIME		RFC 1652
-//	AUTH      		RFC 2554
-//	STARTTLS  		RFC 3207
-//	ENHANCEDSTATUSCODES	RFC 2034
-//  SMTPUTF8		RFC 6531
-//  REQUIRETLS		draft-ietf-uta-smtp-require-tls-09
+//	8BITMIME: RFC 1652
+//	AUTH: RFC 2554
+//	STARTTLS: RFC 3207
+//	ENHANCEDSTATUSCODES: RFC 2034
+//	SMTPUTF8: RFC 6531
+//	REQUIRETLS: RFC 8689
+//	CHUNKING: RFC 3030
+//	BINARYMIME: RFC 3030
 //
 // LMTP (RFC 2033) is also supported.
 //

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/share/gocode/src/github.com/emersion/go-smtp/cmd/smtp-debug-server/main.go
-rwxr-xr-x  root/root   /usr/bin/smtp-debug-server

No differences were encountered in the control files

More details

Full run details