diff options
Diffstat (limited to 'vendor/github.com/emersion/go-smtp')
-rw-r--r-- | vendor/github.com/emersion/go-smtp/.build.yml | 17 | ||||
-rw-r--r-- | vendor/github.com/emersion/go-smtp/.gitignore | 26 | ||||
-rw-r--r-- | vendor/github.com/emersion/go-smtp/LICENSE | 24 | ||||
-rw-r--r-- | vendor/github.com/emersion/go-smtp/README.md | 181 | ||||
-rw-r--r-- | vendor/github.com/emersion/go-smtp/backend.go | 108 | ||||
-rw-r--r-- | vendor/github.com/emersion/go-smtp/client.go | 722 | ||||
-rw-r--r-- | vendor/github.com/emersion/go-smtp/conn.go | 986 | ||||
-rw-r--r-- | vendor/github.com/emersion/go-smtp/data.go | 147 | ||||
-rw-r--r-- | vendor/github.com/emersion/go-smtp/lengthlimit_reader.go | 47 | ||||
-rw-r--r-- | vendor/github.com/emersion/go-smtp/parse.go | 72 | ||||
-rw-r--r-- | vendor/github.com/emersion/go-smtp/server.go | 292 | ||||
-rw-r--r-- | vendor/github.com/emersion/go-smtp/smtp.go | 30 |
12 files changed, 2652 insertions, 0 deletions
diff --git a/vendor/github.com/emersion/go-smtp/.build.yml b/vendor/github.com/emersion/go-smtp/.build.yml new file mode 100644 index 0000000..46854b9 --- /dev/null +++ b/vendor/github.com/emersion/go-smtp/.build.yml @@ -0,0 +1,17 @@ +image: alpine/edge +packages: + - go +sources: + - https://github.com/emersion/go-smtp +artifacts: + - coverage.html +tasks: + - build: | + cd go-smtp + go build -v ./... + - test: | + cd go-smtp + go test -coverprofile=coverage.txt -covermode=atomic ./... + - coverage: | + cd go-smtp + go tool cover -html=coverage.txt -o ~/coverage.html diff --git a/vendor/github.com/emersion/go-smtp/.gitignore b/vendor/github.com/emersion/go-smtp/.gitignore new file mode 100644 index 0000000..dc3e55d --- /dev/null +++ b/vendor/github.com/emersion/go-smtp/.gitignore @@ -0,0 +1,26 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +/main.go diff --git a/vendor/github.com/emersion/go-smtp/LICENSE b/vendor/github.com/emersion/go-smtp/LICENSE new file mode 100644 index 0000000..92ce700 --- /dev/null +++ b/vendor/github.com/emersion/go-smtp/LICENSE @@ -0,0 +1,24 @@ +The MIT License (MIT) + +Copyright (c) 2010 The Go Authors +Copyright (c) 2014 Gleez Technologies +Copyright (c) 2016 emersion +Copyright (c) 2016 Proton Technologies AG + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/emersion/go-smtp/README.md b/vendor/github.com/emersion/go-smtp/README.md new file mode 100644 index 0000000..6992498 --- /dev/null +++ b/vendor/github.com/emersion/go-smtp/README.md @@ -0,0 +1,181 @@ +# 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. + +## Features + +* ESMTP client & server implementing [RFC 5321](https://tools.ietf.org/html/rfc5321) +* Support for SMTP [AUTH](https://tools.ietf.org/html/rfc4954) and [PIPELINING](https://tools.ietf.org/html/rfc2920) +* UTF-8 support for subject and message +* [LMTP](https://tools.ietf.org/html/rfc2033) support + +## Usage + +### Client + +```go +package main + +import ( + "log" + "strings" + + "github.com/emersion/go-sasl" + "github.com/emersion/go-smtp" +) + +func main() { + // Setup authentication information. + auth := sasl.NewPlainClient("", "user@example.com", "password") + + // Connect to the server, authenticate, 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 := smtp.SendMail("mail.example.com:25", auth, "sender@example.org", to, msg) + if err != nil { + log.Fatal(err) + } +} +``` + +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 + +```go +package main + +import ( + "errors" + "io" + "io/ioutil" + "log" + "time" + + "github.com/emersion/go-smtp" +) + +// The Backend implements SMTP server methods. +type Backend struct{} + +func (bkd *Backend) NewSession(_ *smtp.Conn) (smtp.Session, error) { + return &Session{}, nil +} + +// A Session is returned after EHLO. +type Session struct{} + +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 +} + +func (s *Session) Rcpt(to string) error { + log.Println("Rcpt to:", to) + return nil +} + +func (s *Session) Data(r io.Reader) error { + if b, err := ioutil.ReadAll(r); err != nil { + return err + } else { + log.Println("Data:", string(b)) + } + return nil +} + +func (s *Session) Reset() {} + +func (s *Session) Logout() error { + return nil +} + +func main() { + be := &Backend{} + + s := smtp.NewServer(be) + + s.Addr = ":1025" + s.Domain = "localhost" + s.ReadTimeout = 10 * time.Second + s.WriteTimeout = 10 * time.Second + s.MaxMessageBytes = 1024 * 1024 + s.MaxRecipients = 50 + s.AllowInsecureAuth = true + + log.Println("Starting server at", s.Addr) + if err := s.ListenAndServe(); err != nil { + log.Fatal(err) + } +} +``` + +You can use the server manually with `telnet`: +``` +$ telnet localhost 1025 +EHLO localhost +AUTH PLAIN +AHVzZXJuYW1lAHBhc3N3b3Jk +MAIL FROM:<root@nsa.gov> +RCPT TO:<root@gchq.gov.uk> +DATA +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/vendor/github.com/emersion/go-smtp/backend.go b/vendor/github.com/emersion/go-smtp/backend.go new file mode 100644 index 0000000..59cea3a --- /dev/null +++ b/vendor/github.com/emersion/go-smtp/backend.go @@ -0,0 +1,108 @@ +package smtp + +import ( + "io" +) + +var ( + 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 { + 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 + + // TLS is required for the message transmission. + // + // The message should be rejected if it can't be transmitted + // with TLS. + RequireTLS bool + + // 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() + + // 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 + // 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 + // per-recipient status information when it is used over LMTP + // protocol. + // + // LMTPData implementation sets status information using passed + // StatusCollector by calling SetStatus once per each AddRcpt + // call, even if AddRcpt was called multiple times with + // the same argument. SetStatus must not be called after + // LMTPData returns. + // + // Return value of LMTPData itself is used as a status for + // recipients that got no status set before using StatusCollector. + 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/vendor/github.com/emersion/go-smtp/client.go b/vendor/github.com/emersion/go-smtp/client.go new file mode 100644 index 0000000..2b455ba --- /dev/null +++ b/vendor/github.com/emersion/go-smtp/client.go @@ -0,0 +1,722 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package smtp + +import ( + "crypto/tls" + "encoding/base64" + "errors" + "fmt" + "io" + "net" + "net/textproto" + "strconv" + "strings" + "time" + + "github.com/emersion/go-sasl" +) + +// A Client represents a client connection to an SMTP server. +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 + // whether the Client is using TLS + tls bool + serverName string + lmtp bool + // 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 + 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.DialTimeout("tcp", addr, defaultTimeout) + if err != nil { + return nil, err + } + host, _, _ := net.SplitHostPort(addr) + return NewClient(conn, host) +} + +// 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) { + tlsDialer := tls.Dialer{ + NetDialer: &net.Dialer{ + Timeout: defaultTimeout, + }, + Config: tlsConfig, + } + conn, err := tlsDialer.Dial("tcp", addr) + if err != nil { + return nil, err + } + host, _, _ := net.SplitHostPort(addr) + return NewClient(conn, host) +} + +// 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) { + 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, + } + + 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 { + c.Text.Close() + if protoErr, ok := err.(*textproto.Error); ok { + return nil, toSMTPErr(protoErr) + } + return nil, err + } + + return c, nil +} + +// NewClientLMTP returns a new LMTP Client (as defined in RFC 2033) using an +// 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 { + return nil, err + } + c.lmtp = true + 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() +} + +// hello runs a hello exchange if needed. +func (c *Client) hello() error { + if !c.didHello { + c.didHello = true + err := c.ehlo() + if err != nil { + c.helloError = c.helo() + } + } + return c.helloError +} + +// Hello sends a HELO or EHLO to the server as the given host name. +// Calling this method is only necessary if the client needs control +// over the host name used. The client will introduce itself as "localhost" +// automatically otherwise. If Hello is called, it must be called before +// any of the other methods. +// +// If server returns an error, it will be of type *SMTPError. +func (c *Client) Hello(localName string) error { + if err := validateLine(localName); err != nil { + return err + } + if c.didHello { + return errors.New("smtp: Hello called after other methods") + } + c.localName = localName + return c.hello() +} + +// 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 + } + c.Text.StartResponse(id) + defer c.Text.EndResponse(id) + code, msg, err := c.Text.ReadResponse(expectCode) + if err != nil { + if protoErr, ok := err.(*textproto.Error); ok { + smtpErr := toSMTPErr(protoErr) + return code, smtpErr.Message, smtpErr + } + return code, msg, err + } + return code, msg, nil +} + +// helo sends the HELO greeting to the server. It should be used only when the +// server does not support ehlo. +func (c *Client) helo() error { + c.ext = nil + _, _, err := c.cmd(250, "HELO %s", c.localName) + return err +} + +// ehlo sends the EHLO (extended hello) greeting to the server. It +// should be the preferred greeting for servers that support it. +func (c *Client) ehlo() error { + cmd := "EHLO" + if c.lmtp { + cmd = "LHLO" + } + + _, msg, err := c.cmd(250, "%s %s", cmd, c.localName) + if err != nil { + return err + } + ext := make(map[string]string) + extList := strings.Split(msg, "\n") + if len(extList) > 1 { + extList = extList[1:] + for _, line := range extList { + args := strings.SplitN(line, " ", 2) + if len(args) > 1 { + ext[args[0]] = args[1] + } else { + ext[args[0]] = "" + } + } + } + if mechs, ok := ext["AUTH"]; ok { + c.auth = strings.Split(mechs, " ") + } + c.ext = ext + return err +} + +// 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 { + return err + } + _, _, err := c.cmd(220, "STARTTLS") + if err != nil { + return err + } + if config == nil { + config = &tls.Config{} + } + if config.ServerName == "" { + // Make a copy to avoid polluting argument + config = config.Clone() + config.ServerName = c.serverName + } + if testHookStartTLS != nil { + testHookStartTLS(config) + } + c.setConn(tls.Client(c.conn, config)) + return c.ehlo() +} + +// TLSConnectionState returns the client's TLS connection state. +// The return values are their zero values if StartTLS did +// not succeed. +func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) { + tc, ok := c.conn.(*tls.Conn) + if !ok { + return + } + return tc.ConnectionState(), true +} + +// Verify checks the validity of an email address on the server. +// If Verify returns nil, the address is valid. A non-nil return +// does not necessarily indicate an invalid address. Many servers +// will not verify addresses for security reasons. +// +// If server returns an error, it will be of type *SMTPError. +func (c *Client) Verify(addr string) error { + if err := validateLine(addr); err != nil { + return err + } + if err := c.hello(); err != nil { + return err + } + _, _, err := c.cmd(250, "VRFY %s", addr) + return err +} + +// Auth authenticates a client using the provided authentication mechanism. +// Only servers that advertise the AUTH extension support this function. +// +// If server returns an error, it will be of type *SMTPError. +func (c *Client) Auth(a sasl.Client) error { + if err := c.hello(); err != nil { + return err + } + encoding := base64.StdEncoding + mech, resp, err := a.Start() + if err != nil { + return err + } + resp64 := make([]byte, encoding.EncodedLen(len(resp))) + encoding.Encode(resp64, resp) + code, msg64, err := c.cmd(0, strings.TrimSpace(fmt.Sprintf("AUTH %s %s", mech, resp64))) + for err == nil { + var msg []byte + switch code { + case 334: + msg, err = encoding.DecodeString(msg64) + case 235: + // the last message isn't base64 because it isn't a challenge + msg = []byte(msg64) + default: + err = toSMTPErr(&textproto.Error{Code: code, Msg: msg64}) + } + if err == nil { + if code == 334 { + resp, err = a.Next(msg) + } else { + resp = nil + } + } + if err != nil { + // abort the AUTH + c.cmd(501, "*") + break + } + if resp == nil { + break + } + resp64 = make([]byte, encoding.EncodedLen(len(resp))) + encoding.Encode(resp64, resp) + code, msg64, err = c.cmd(0, string(resp64)) + } + return err +} + +// Mail issues a MAIL command to the server using the provided email address. +// If the server supports the 8BITMIME extension, Mail adds the BODY=8BITMIME +// parameter. +// This initiates a mail transaction and is followed by one or more Rcpt calls. +// +// If opts is not nil, MAIL arguments provided in the structure will be added +// to the command. Handling of unsupported options depends on the extension. +// +// If server returns an error, it will be of type *SMTPError. +func (c *Client) Mail(from string, opts *MailOptions) error { + if err := validateLine(from); err != nil { + return err + } + if err := c.hello(); err != nil { + return err + } + cmdStr := "MAIL FROM:<%s>" + if _, ok := c.ext["8BITMIME"]; ok { + cmdStr += " BODY=8BITMIME" + } + if _, ok := c.ext["SIZE"]; ok && opts != nil && opts.Size != 0 { + cmdStr += " SIZE=" + strconv.Itoa(opts.Size) + } + if opts != nil && opts.RequireTLS { + if _, ok := c.ext["REQUIRETLS"]; ok { + cmdStr += " REQUIRETLS" + } else { + return errors.New("smtp: server does not support REQUIRETLS") + } + } + if opts != nil && opts.UTF8 { + if _, ok := c.ext["SMTPUTF8"]; ok { + cmdStr += " SMTPUTF8" + } else { + 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 +} + +// Rcpt issues a RCPT command to the server using the provided email address. +// A call to Rcpt must be preceded by a call to Mail and may be followed by +// a Data call or another Rcpt call. +// +// If server returns an error, it will be of type *SMTPError. +func (c *Client) Rcpt(to string) error { + if err := validateLine(to); err != nil { + return err + } + if _, _, err := c.cmd(25, "RCPT TO:<%s>", to); err != nil { + return err + } + 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 { + 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 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 { + if d.statusCb != nil { + d.statusCb(rcpt, toSMTPErr(protoErr)) + } + } else { + return err + } + } else if d.statusCb != nil { + d.statusCb(rcpt, nil) + } + expectedResponses-- + } + } else { + _, _, err := d.c.Text.ReadResponse(250) + if err != nil { + if protoErr, ok := err.(*textproto.Error); ok { + return toSMTPErr(protoErr) + } + return err + } + } + + d.closed = true + return nil +} + +// Data issues a DATA command to the server and returns a writer that +// can be used to write the mail headers and body. The caller should +// close the writer before calling any more methods on c. A call to +// Data must be preceded by one or more calls to Rcpt. +// +// If server returns an error, it will be of type *SMTPError. +func (c *Client) Data() (io.WriteCloser, error) { + _, _, err := c.cmd(354, "DATA") + if err != nil { + return nil, err + } + 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, 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. +// +// 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. +// +// 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 + } + for _, recp := range to { + if err := validateLine(recp); err != nil { + return err + } + } + c, err := Dial(addr) + if err != nil { + return err + } + defer c.Close() + + if err = c.hello(); 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 { + return errors.New("smtp: server doesn't support AUTH") + } + if err = c.Auth(a); err != nil { + return err + } + } + return c.SendMail(from, to, r) +} + +// Extension reports whether an extension is support by the server. +// The extension name is case-insensitive. If the extension is supported, +// Extension also returns a string that contains any parameters the +// server specifies for the extension. +func (c *Client) Extension(ext string) (bool, string) { + if err := c.hello(); err != nil { + return false, "" + } + if c.ext == nil { + return false, "" + } + ext = strings.ToUpper(ext) + param, ok := c.ext[ext] + return ok, param +} + +// Reset sends the RSET command to the server, aborting the current mail +// transaction. +func (c *Client) Reset() error { + if err := c.hello(); err != nil { + return err + } + if _, _, err := c.cmd(250, "RSET"); err != nil { + return err + } + c.rcpts = nil + return nil +} + +// Noop sends the NOOP command to the server. It does nothing but check +// that the connection to the server is okay. +func (c *Client) Noop() error { + if err := c.hello(); err != nil { + return err + } + _, _, err := c.cmd(250, "NOOP") + return err +} + +// Quit sends the QUIT command and closes the connection to the server. +// +// If Quit fails the connection is not closed, Close should be used +// in this case. +func (c *Client) Quit() error { + if err := c.hello(); err != nil { + return err + } + _, _, err := c.cmd(221, "QUIT") + if err != nil { + return err + } + return c.Text.Close() +} + +func parseEnhancedCode(s string) (EnhancedCode, error) { + parts := strings.Split(s, ".") + if len(parts) != 3 { + return EnhancedCode{}, fmt.Errorf("wrong amount of enhanced code parts") + } + + code := EnhancedCode{} + for i, part := range parts { + num, err := strconv.Atoi(part) + if err != nil { + return code, err + } + code[i] = num + } + return code, nil +} + +// toSMTPErr converts textproto.Error into SMTPError, parsing +// enhanced status code if it is present. +func toSMTPErr(protoErr *textproto.Error) *SMTPError { + if protoErr == nil { + return nil + } + smtpErr := &SMTPError{ + Code: protoErr.Code, + Message: protoErr.Msg, + } + + parts := strings.SplitN(protoErr.Msg, " ", 2) + if len(parts) != 2 { + return smtpErr + } + + enchCode, err := parseEnhancedCode(parts[0]) + if err != nil { + 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 = 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/vendor/github.com/emersion/go-smtp/conn.go b/vendor/github.com/emersion/go-smtp/conn.go new file mode 100644 index 0000000..72a67d8 --- /dev/null +++ b/vendor/github.com/emersion/go-smtp/conn.go @@ -0,0 +1,986 @@ +package smtp + +import ( + "crypto/tls" + "encoding/base64" + "errors" + "fmt" + "io" + "io/ioutil" + "net" + "net/textproto" + "regexp" + "runtime/debug" + "strconv" + "strings" + "sync" + "time" +) + +// 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 + + // 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 { + sc := &Conn{ + server: s, + conn: c, + } + + sc.init() + return sc +} + +func (c *Conn) init() { + c.lineLimitReader = &lineLimitReader{ + R: c.conn, + LineLimit: c.server.MaxLineLength, + } + rwc := struct { + io.Reader + io.Writer + io.Closer + }{ + Reader: c.lineLimitReader, + Writer: c.conn, + Closer: c.conn, + } + + if c.server.Debug != nil { + rwc = struct { + io.Reader + io.Writer + io.Closer + }{ + io.TeeReader(rwc.Reader, c.server.Debug), + io.MultiWriter(rwc.Writer, c.server.Debug), + rwc.Closer, + } + } + + c.text = textproto.NewConn(rwc) +} + +// 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.Close() + + stack := debug.Stack() + c.server.ErrorLog.Printf("panic serving %v: %v\n%s", c.conn.RemoteAddr(), err, stack) + } + }() + + if cmd == "" { + c.protocolError(500, EnhancedCode{5, 5, 2}, "Error: bad syntax") + return + } + + cmd = strings.ToUpper(cmd) + 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)) + 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") + return + } + if !c.server.LMTP && lmtp { + c.writeResponse(500, EnhancedCode{5, 5, 1}, "This is not a LMTP server") + return + } + c.handleGreet(enhanced, arg) + case "MAIL": + c.handleMail(arg) + case "RCPT": + c.handleRcpt(arg) + case "VRFY": + 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") + case "RSET": // Reset session + c.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}, "Bye") + c.Close() + case "AUTH": + if c.server.AuthDisabled { + c.protocolError(500, EnhancedCode{5, 5, 2}, "Syntax error, AUTH command unrecognized") + } else { + c.handleAuth(arg) + } + case "STARTTLS": + c.handleStartTLS() + default: + msg := fmt.Sprintf("Syntax errors, %v command unrecognized", cmd) + c.protocolError(500, EnhancedCode{5, 5, 2}, msg) + } +} + +func (c *Conn) Server() *Server { + return c.server +} + +func (c *Conn) Session() Session { + c.locker.Lock() + defer c.locker.Unlock() + return c.session +} + +func (c *Conn) setSession(session Session) { + c.locker.Lock() + defer c.locker.Unlock() + c.session = session +} + +func (c *Conn) Close() error { + 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() +} + +// TLSConnectionState returns the connection's TLS connection state. +// Zero values are returned if the connection doesn't use TLS. +func (c *Conn) TLSConnectionState() (state tls.ConnectionState, ok bool) { + tc, ok := c.conn.(*tls.Conn) + if !ok { + return + } + return tc.ConnectionState(), true +} + +func (c *Conn) Hostname() string { + return c.helo +} + +func (c *Conn) Conn() net.Conn { + return c.conn +} + +func (c *Conn) authAllowed() bool { + _, isTLS := c.TLSConnectionState() + 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) { + domain, err := parseHelloArgument(arg) + if err != nil { + c.writeResponse(501, EnhancedCode{5, 5, 2}, "Domain/address argument required for HELO") + return + } + c.helo = domain + + 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) + + if !enhanced { + c.writeResponse(250, EnhancedCode{2, 0, 0}, fmt.Sprintf("Hello %s", domain)) + return + } + + 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 + } + + 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.") + return + } + 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>") + 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>") + return + } + } + from := fromArgs[0] + if from == "" { + c.writeResponse(501, EnhancedCode{5, 5, 2}, "Was expecting MAIL arg syntax of FROM:<address>") + return + } + from = strings.Trim(from, "<>") + + 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") + return + } + + for key, value := range args { + switch key { + 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") + return + } + + if c.server.MaxMessageBytes > 0 && int(size) > c.server.MaxMessageBytes { + 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") + return + } + opts.UTF8 = true + case "REQUIRETLS": + if !c.server.EnableREQUIRETLS { + 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") + 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") + return + } + } + } + + if err := c.Session().Mail(from, opts); 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.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.") + 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>") + return + } + + // TODO: This trim is probably too forgiving + 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)) + return + } + + if err := c.Session().Rcpt(recipient); 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.recipients = append(c.recipients, 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.") + 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") + return + } + + if _, isTLS := c.TLSConnectionState(); !isTLS && !c.server.AllowInsecureAuth { + c.writeResponse(523, EnhancedCode{5, 7, 10}, "TLS is required") + return + } + + mechanism := strings.ToUpper(parts[0]) + + // Parse client initial response if there is one + var ir []byte + if len(parts) > 1 { + var err error + ir, err = base64.StdEncoding.DecodeString(parts[1]) + if err != nil { + return + } + } + + newSasl, ok := c.server.auths[mechanism] + if !ok { + c.writeResponse(504, EnhancedCode{5, 7, 4}, "Unsupported authentication mechanism") + return + } + + sasl := newSasl(c) + + response := ir + for { + challenge, done, err := sasl.Next(response) + if err != nil { + if smtpErr, ok := err.(*SMTPError); ok { + c.writeResponse(smtpErr.Code, smtpErr.EnhancedCode, smtpErr.Message) + return + } + c.writeResponse(454, EnhancedCode{4, 7, 0}, err.Error()) + return + } + + if done { + break + } + + encoded := "" + if len(challenge) > 0 { + encoded = base64.StdEncoding.EncodeToString(challenge) + } + c.writeResponse(334, NoEnhancedCode, encoded) + + 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") + return + } + } + + 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") + return + } + + if c.server.TLSConfig == nil { + c.writeResponse(502, EnhancedCode{5, 5, 1}, "TLS not supported") + return + } + + c.writeResponse(220, EnhancedCode{2, 0, 0}, "Ready to start TLS") + + // Upgrade to TLS + tlsConn := tls.Server(c.conn, c.server.TLSConfig) + + if err := tlsConn.Handshake(); err != nil { + c.writeResponse(550, EnhancedCode{5, 0, 0}, "Handshake error") + return + } + + c.conn = tlsConn + c.init() + + // 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") + 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.") + 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>") + + defer c.reset() + + if c.server.LMTP { + c.handleDataLMTP() + return + } + + 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) +} + +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 { + // Contains map from recipient to list of channels that are used for that + // recipient. + statusMap map[string]chan error + + // Contains channels from statusMap, in the same + // order as Conn.recipients. + status []chan error +} + +// fillRemaining sets status for all recipients SetStatus was not called for before. +func (s *statusCollector) fillRemaining(err error) { + // Amount of times certain recipient was specified is indicated by the channel + // buffer size, so once we fill it, we can be confident that we sent + // at least as much statuses as needed. Extra statuses will be ignored anyway. +chLoop: + for _, ch := range s.statusMap { + for { + select { + case ch <- err: + default: + continue chLoop + } + } + } +} + +func (s *statusCollector) SetStatus(rcptTo string, err error) { + ch := s.statusMap[rcptTo] + if ch == nil { + panic("SetStatus is called for recipient that was not specified before") + } + + select { + case ch <- err: + default: + // There enough buffer space to fit all statuses at once, if this is + // not the case - backend is doing something wrong. + panic("SetStatus is called more times than particular recipient was specified") + } +} + +func (c *Conn) handleDataLMTP() { + r := newDataReader(c) + status := c.createStatusCollector() + + done := make(chan bool, 1) + + lmtpSession, ok := c.Session().(LMTPSession) + if !ok { + // Fallback to using a single status for all recipients. + err := c.Session().Data(r) + io.Copy(ioutil.Discard, r) // Make sure all the data has been consumed + for _, rcpt := range c.recipients { + status.SetStatus(rcpt, err) + } + done <- true + } else { + go func() { + defer func() { + if err := recover(); err != nil { + status.fillRemaining(&SMTPError{ + Code: 421, + EnhancedCode: EnhancedCode{4, 0, 0}, + Message: "Internal server error", + }) + + stack := debug.Stack() + c.server.ErrorLog.Printf("panic serving %v: %v\n%s", c.conn.RemoteAddr(), err, stack) + done <- false + } + }() + + status.fillRemaining(lmtpSession.LMTPData(r, status)) + io.Copy(ioutil.Discard, r) // Make sure all the data has been consumed + done <- true + }() + } + + for i, rcpt := range c.recipients { + code, enchCode, msg := toSMTPStatus(<-status.status[i]) + c.writeResponse(code, enchCode, "<"+rcpt+"> "+msg) + } + + // If done gets false, the panic occured in LMTPData and the connection + // should be closed. + if !<-done { + c.Close() + } +} + +func toSMTPStatus(err error) (code int, enchCode EnhancedCode, msg string) { + if err != nil { + if smtperr, ok := err.(*SMTPError); ok { + return smtperr.Code, smtperr.EnhancedCode, smtperr.Message + } else { + return 554, EnhancedCode{5, 0, 0}, "Error: transaction failed, blame it on the weather: " + err.Error() + } + } + + return 250, EnhancedCode{2, 0, 0}, "OK: queued" +} + +func (c *Conn) Reject() { + 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)) +} + +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)) + } + + // All responses must include an enhanced code, if it is missing - use + // a generic code X.0.0. + if enhCode == EnhancedCodeNotSet { + cat := code / 100 + switch cat { + case 2, 4, 5: + enhCode = EnhancedCode{cat, 0, 0} + default: + enhCode = NoEnhancedCode + } + } + + for i := 0; i < len(text)-1; i++ { + c.text.PrintfLine("%d-%v", code, text[i]) + } + if enhCode == NoEnhancedCode { + c.text.PrintfLine("%d %v", code, text[len(text)-1]) + } else { + 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) { + if c.server.ReadTimeout != 0 { + if err := c.conn.SetReadDeadline(time.Now().Add(c.server.ReadTimeout)); err != nil { + return "", err + } + } + + return c.text.ReadLine() +} + +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/vendor/github.com/emersion/go-smtp/data.go b/vendor/github.com/emersion/go-smtp/data.go new file mode 100644 index 0000000..c338455 --- /dev/null +++ b/vendor/github.com/emersion/go-smtp/data.go @@ -0,0 +1,147 @@ +package smtp + +import ( + "bufio" + "io" +) + +type EnhancedCode [3]int + +// SMTPError specifies the error code, enhanced error code (if any) and +// message returned by the server. +type SMTPError struct { + Code int + EnhancedCode EnhancedCode + Message string +} + +// NoEnhancedCode is used to indicate that enhanced error code should not be +// included in response. +// +// Note that RFC 2034 requires an enhanced code to be included in all 2xx, 4xx +// and 5xx responses. This constant is exported for use by extensions, you +// should probably use EnhancedCodeNotSet instead. +var NoEnhancedCode = EnhancedCode{-1, -1, -1} + +// EnhancedCodeNotSet is a nil value of EnhancedCode field in SMTPError, used +// to indicate that backend failed to provide enhanced status code. X.0.0 will +// be used (X is derived from error code). +var EnhancedCodeNotSet = EnhancedCode{0, 0, 0} + +func (err *SMTPError) Error() string { + return err.Message +} + +func (err *SMTPError) Temporary() bool { + return err.Code/100 == 4 +} + +var ErrDataTooLarge = &SMTPError{ + Code: 552, + EnhancedCode: EnhancedCode{5, 3, 4}, + Message: "Maximum message size exceeded", +} + +type dataReader struct { + r *bufio.Reader + state int + + limited bool + n int64 // Maximum bytes remaining +} + +func newDataReader(c *Conn) *dataReader { + dr := &dataReader{ + r: c.text.R, + } + + if c.server.MaxMessageBytes > 0 { + dr.limited = true + dr.n = int64(c.server.MaxMessageBytes) + } + + return dr +} + +func (r *dataReader) Read(b []byte) (n int, err error) { + if r.limited { + if r.n <= 0 { + return 0, ErrDataTooLarge + } + if int64(len(b)) > r.n { + b = b[0:r.n] + } + } + + // 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) + } + return +} diff --git a/vendor/github.com/emersion/go-smtp/lengthlimit_reader.go b/vendor/github.com/emersion/go-smtp/lengthlimit_reader.go new file mode 100644 index 0000000..1513e56 --- /dev/null +++ b/vendor/github.com/emersion/go-smtp/lengthlimit_reader.go @@ -0,0 +1,47 @@ +package smtp + +import ( + "errors" + "io" +) + +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. +// +// If line length exceeds the limit - Read returns ErrTooLongLine +type lineLimitReader struct { + R io.Reader + LineLimit int + + curLineLength int +} + +func (r *lineLimitReader) Read(b []byte) (int, error) { + if r.curLineLength > r.LineLimit && r.LineLimit > 0 { + return 0, ErrTooLongLine + } + + n, err := r.R.Read(b) + if err != nil { + return n, err + } + + if r.LineLimit == 0 { + return n, nil + } + + for _, chr := range b[:n] { + if chr == '\n' { + r.curLineLength = 0 + } + r.curLineLength++ + + if r.curLineLength > r.LineLimit { + return 0, ErrTooLongLine + } + } + + return n, nil +} diff --git a/vendor/github.com/emersion/go-smtp/parse.go b/vendor/github.com/emersion/go-smtp/parse.go new file mode 100644 index 0000000..dc7e77f --- /dev/null +++ b/vendor/github.com/emersion/go-smtp/parse.go @@ -0,0 +1,72 @@ +package smtp + +import ( + "fmt" + "strings" +) + +func parseCmd(line string) (cmd string, arg string, err error) { + line = strings.TrimRight(line, "\r\n") + + l := len(line) + switch { + case strings.HasPrefix(strings.ToUpper(line), "STARTTLS"): + return "STARTTLS", "", nil + case l == 0: + return "", "", nil + case l < 4: + return "", "", fmt.Errorf("Command too short: %q", line) + case l == 4: + return strings.ToUpper(line), "", nil + case l == 5: + // Too long to be only command, too short to have args + return "", "", fmt.Errorf("Mangled command: %q", line) + } + + // If we made it here, command is long enough to have args + if line[4] != ' ' { + // There wasn't a space after the command? + return "", "", fmt.Errorf("Mangled command: %q", line) + } + + // I'm not sure if we should trim the args or not, but we will for now + //return strings.ToUpper(line[0:4]), strings.Trim(line[5:], " "), nil + return strings.ToUpper(line[0:4]), strings.Trim(line[5:], " \n\r"), nil +} + +// 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" +// +// The leading space is mandatory. +func parseArgs(args []string) (map[string]string, error) { + argMap := map[string]string{} + for _, arg := range args { + if arg == "" { + continue + } + m := strings.Split(arg, "=") + switch len(m) { + case 2: + argMap[strings.ToUpper(m[0])] = m[1] + case 1: + argMap[strings.ToUpper(m[0])] = "" + default: + return nil, fmt.Errorf("Failed to parse arg string: %q", arg) + } + } + return argMap, nil +} + +func parseHelloArgument(arg string) (string, error) { + domain := arg + if idx := strings.IndexRune(arg, ' '); idx >= 0 { + domain = arg[:idx] + } + if domain == "" { + return "", fmt.Errorf("Invalid domain") + } + return domain, nil +} diff --git a/vendor/github.com/emersion/go-smtp/server.go b/vendor/github.com/emersion/go-smtp/server.go new file mode 100644 index 0000000..82cc422 --- /dev/null +++ b/vendor/github.com/emersion/go-smtp/server.go @@ -0,0 +1,292 @@ +package smtp + +import ( + "crypto/tls" + "errors" + "io" + "log" + "net" + "os" + "sync" + "time" + + "github.com/emersion/go-sasl" +) + +var errTCPAndLMTP = errors.New("smtp: cannot start LMTP server listening on a TCP socket") + +// A function that creates SASL servers. +type SaslServerFactory func(conn *Conn) sasl.Server + +// Logger interface is used by Server to report unexpected internal errors. +type Logger interface { + Printf(format string, v ...interface{}) + Println(v ...interface{}) +} + +// A SMTP server. +type Server struct { + // TCP or Unix address to listen on. + Addr string + // The server TLS configuration. + TLSConfig *tls.Config + // Enable LMTP mode, as defined in RFC 2033. LMTP mode cannot be used with a + // TCP listener. + LMTP bool + + Domain string + MaxRecipients int + MaxMessageBytes int + MaxLineLength int + AllowInsecureAuth bool + Strict bool + Debug io.Writer + ErrorLog Logger + ReadTimeout time.Duration + WriteTimeout time.Duration + + // Advertise SMTPUTF8 (RFC 6531) capability. + // Should be used only if backend supports it. + EnableSMTPUTF8 bool + + // 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 + + // The server backend. + Backend Backend + + caps []string + auths map[string]SaslServerFactory + done chan struct{} + + locker sync.Mutex + listeners []net.Listener + conns map[*Conn]struct{} +} + +// New creates a new SMTP server. +func NewServer(be Backend) *Server { + return &Server{ + // Doubled maximum line length per RFC 5321 (Section 4.5.3.1.6) + MaxLineLength: 2000, + + Backend: be, + done: make(chan struct{}, 1), + ErrorLog: log.New(os.Stderr, "smtp/server ", log.LstdFlags), + 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 { + if identity != "" && identity != username { + return errors.New("Identities not supported") + } + + sess := conn.Session() + if sess == nil { + panic("No session when AUTH is called") + } + + return sess.AuthPlain(username, password) + }) + }, + }, + conns: make(map[*Conn]struct{}), + } +} + +// 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() + if err != nil { + select { + case <-s.done: + // we called Close() + return nil + default: + } + 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 func() { + err := s.handleConn(newConn(c, s)) + if err != nil { + s.ErrorLog.Printf("handler error: %s", err) + } + }() + } +} + +func (s *Server) handleConn(c *Conn) error { + s.locker.Lock() + s.conns[c] = struct{}{} + s.locker.Unlock() + + defer func() { + c.Close() + + s.locker.Lock() + delete(s.conns, c) + 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() + if err == nil { + cmd, arg, err := parseCmd(line) + if err != nil { + c.protocolError(501, EnhancedCode{5, 5, 2}, "Bad command") + continue + } + + c.handle(cmd, arg) + } else { + 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") + return nil + } + + if neterr, ok := err.(net.Error); ok && neterr.Timeout() { + c.writeResponse(221, EnhancedCode{2, 4, 2}, "Idle timeout, bye bye") + return nil + } + + c.writeResponse(221, EnhancedCode{2, 4, 0}, "Connection error, sorry") + return err + } + } +} + +// ListenAndServe listens on the network address s.Addr and then calls Serve +// to handle requests on incoming connections. +// +// If s.Addr is blank and LMTP is disabled, ":smtp" is used. +func (s *Server) ListenAndServe() error { + network := "tcp" + if s.LMTP { + network = "unix" + } + + addr := s.Addr + if !s.LMTP && addr == "" { + addr = ":smtp" + } + + l, err := net.Listen(network, addr) + if err != nil { + return err + } + + return s.Serve(l) +} + +// ListenAndServeTLS listens on the TCP network address s.Addr and then calls +// Serve to handle requests on incoming TLS connections. +// +// If s.Addr is blank, ":smtps" is used. +func (s *Server) ListenAndServeTLS() error { + if s.LMTP { + return errTCPAndLMTP + } + + addr := s.Addr + if addr == "" { + addr = ":smtps" + } + + l, err := tls.Listen("tcp", addr, s.TLSConfig) + if err != nil { + return err + } + + return s.Serve(l) +} + +// 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() + 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. +// +// This function should not be called directly, it must only be used by +// libraries implementing extensions of the SMTP protocol. +func (s *Server) EnableAuth(name string, f SaslServerFactory) { + s.auths[name] = f +} + +// ForEachConn iterates through all opened connections. +func (s *Server) ForEachConn(f func(*Conn)) { + s.locker.Lock() + defer s.locker.Unlock() + for conn := range s.conns { + f(conn) + } +} diff --git a/vendor/github.com/emersion/go-smtp/smtp.go b/vendor/github.com/emersion/go-smtp/smtp.go new file mode 100644 index 0000000..36963cb --- /dev/null +++ b/vendor/github.com/emersion/go-smtp/smtp.go @@ -0,0 +1,30 @@ +// Package smtp implements the Simple Mail Transfer Protocol as defined in RFC 5321. +// +// It also implements the following extensions: +// +// 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. +// +// Additional extensions may be handled by other packages. +package smtp + +import ( + "errors" + "strings" +) + +// validateLine checks to see if a line has CR or LF as per RFC 5321 +func validateLine(line string) error { + if strings.ContainsAny(line, "\n\r") { + return errors.New("smtp: A line must not contain CR or LF") + } + return nil +} |