aboutsummaryrefslogtreecommitdiff
path: root/vendor/github.com/emersion/go-smtp
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/emersion/go-smtp')
-rw-r--r--vendor/github.com/emersion/go-smtp/.build.yml17
-rw-r--r--vendor/github.com/emersion/go-smtp/.gitignore26
-rw-r--r--vendor/github.com/emersion/go-smtp/LICENSE24
-rw-r--r--vendor/github.com/emersion/go-smtp/README.md181
-rw-r--r--vendor/github.com/emersion/go-smtp/backend.go108
-rw-r--r--vendor/github.com/emersion/go-smtp/client.go722
-rw-r--r--vendor/github.com/emersion/go-smtp/conn.go986
-rw-r--r--vendor/github.com/emersion/go-smtp/data.go147
-rw-r--r--vendor/github.com/emersion/go-smtp/lengthlimit_reader.go47
-rw-r--r--vendor/github.com/emersion/go-smtp/parse.go72
-rw-r--r--vendor/github.com/emersion/go-smtp/server.go292
-rw-r--r--vendor/github.com/emersion/go-smtp/smtp.go30
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
+}