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