pax_global_header00006660000000000000000000000064146124340200014506gustar00rootroot0000000000000052 comment=e74d8b31c0f4ddec7143be166155f1c95abf096d go-smtp-0.21.2/000077500000000000000000000000001461243402000131565ustar00rootroot00000000000000go-smtp-0.21.2/.build.yml000066400000000000000000000006601461243402000150600ustar00rootroot00000000000000image: alpine/latest packages: - go sources: - https://github.com/emersion/go-smtp artifacts: - coverage.html tasks: - build: | cd go-smtp go build -race -v ./... - test: | cd go-smtp go test -race -coverprofile=coverage.txt -covermode=atomic ./... - coverage: | cd go-smtp go tool cover -html=coverage.txt -o ~/coverage.html - gofmt: | cd go-smtp test -z $(gofmt -l .) go-smtp-0.21.2/.github/000077500000000000000000000000001461243402000145165ustar00rootroot00000000000000go-smtp-0.21.2/.github/ISSUE_TEMPLATE/000077500000000000000000000000001461243402000167015ustar00rootroot00000000000000go-smtp-0.21.2/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000002571461243402000206750ustar00rootroot00000000000000blank_issues_enabled: false contact_links: - name: Question url: "https://web.libera.chat/gamja/#emersion" about: "Please ask questions in #emersion on Libera Chat" go-smtp-0.21.2/.github/ISSUE_TEMPLATE/issue_template.md000066400000000000000000000004711461243402000222500ustar00rootroot00000000000000--- name: Bug report or feature request about: Report a bug or request a new feature --- go-smtp-0.21.2/.gitignore000066400000000000000000000004241461243402000151460ustar00rootroot00000000000000# 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 go-smtp-0.21.2/LICENSE000066400000000000000000000022451461243402000141660ustar00rootroot00000000000000The 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. go-smtp-0.21.2/README.md000066400000000000000000000017001461243402000144330ustar00rootroot00000000000000# go-smtp [![Go Reference](https://pkg.go.dev/badge/github.com/emersion/go-smtp.svg)](https://pkg.go.dev/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] * Support for additional SMTP extensions such as [AUTH] and [PIPELINING] * UTF-8 support for subject and message * [LMTP] support ## 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 [RFC 5321]: https://tools.ietf.org/html/rfc5321 [AUTH]: https://tools.ietf.org/html/rfc4954 [PIPELINING]: https://tools.ietf.org/html/rfc2920 [LMTP]: https://tools.ietf.org/html/rfc2033 go-smtp-0.21.2/backend.go000066400000000000000000000053611461243402000151010ustar00rootroot00000000000000package smtp import ( "io" "github.com/emersion/go-sasl" ) var ( ErrAuthFailed = &SMTPError{ Code: 535, EnhancedCode: EnhancedCode{5, 7, 8}, Message: "Authentication failed", } 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", } ErrAuthUnknownMechanism = &SMTPError{ Code: 504, EnhancedCode: EnhancedCode{5, 7, 4}, Message: "Unsupported authentication mechanism", } ) // A SMTP server backend. type Backend interface { NewSession(c *Conn) (Session, error) } // BackendFunc is an adapter to allow the use of an ordinary function as a // Backend. type BackendFunc func(c *Conn) (Session, error) var _ Backend = (BackendFunc)(nil) // NewSession calls f(c). func (f BackendFunc) NewSession(c *Conn) (Session, error) { return f(c) } // 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 // Set return path for currently processed message. Mail(from string, opts *MailOptions) error // Add recipient for currently processed message. Rcpt(to string, opts *RcptOptions) 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 { Session // 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) } // AuthSession is an add-on interface for Session. It provides support for the // AUTH extension. type AuthSession interface { Session AuthMechanisms() []string Auth(mech string) (sasl.Server, error) } go-smtp-0.21.2/backendutil/000077500000000000000000000000001461243402000154435ustar00rootroot00000000000000go-smtp-0.21.2/backendutil/backendutil.go000077500000000000000000000001311461243402000202550ustar00rootroot00000000000000// Package backendutil provide utilities to implement SMTP backends. package backendutil go-smtp-0.21.2/backendutil/transform.go000077500000000000000000000034521461243402000200140ustar00rootroot00000000000000package backendutil import ( "io" "github.com/emersion/go-sasl" "github.com/emersion/go-smtp" ) // TransformBackend is a backend that transforms messages. type TransformBackend struct { Backend smtp.Backend TransformMail func(from string) (string, error) TransformRcpt func(to string) (string, error) TransformData func(r io.Reader) (io.Reader, error) } func (be *TransformBackend) NewSession(c *smtp.Conn) (smtp.Session, error) { sess, err := be.Backend.NewSession(c) if err != nil { return nil, err } return &transformSession{Session: sess, be: be}, nil } type transformSession struct { Session smtp.Session be *TransformBackend } func (s *transformSession) Reset() { s.Session.Reset() } func (s *transformSession) AuthMechanisms() []string { if authSession, ok := s.Session.(smtp.AuthSession); ok { return authSession.AuthMechanisms() } return nil } func (s *transformSession) Auth(mech string) (sasl.Server, error) { if authSession, ok := s.Session.(smtp.AuthSession); ok { return authSession.Auth(mech) } return nil, smtp.ErrAuthUnsupported } func (s *transformSession) Mail(from string, opts *smtp.MailOptions) error { if s.be.TransformMail != nil { var err error from, err = s.be.TransformMail(from) if err != nil { return err } } return s.Session.Mail(from, opts) } func (s *transformSession) Rcpt(to string, opts *smtp.RcptOptions) error { if s.be.TransformRcpt != nil { var err error to, err = s.be.TransformRcpt(to) if err != nil { return err } } return s.Session.Rcpt(to, opts) } func (s *transformSession) Data(r io.Reader) error { if s.be.TransformData != nil { var err error r, err = s.be.TransformData(r) if err != nil { return err } } return s.Session.Data(r) } func (s *transformSession) Logout() error { return s.Session.Logout() } go-smtp-0.21.2/backendutil/transform_test.go000077500000000000000000000173641461243402000210620ustar00rootroot00000000000000package backendutil_test import ( "bufio" "encoding/base64" "errors" "io" "io/ioutil" "net" "strings" "testing" "github.com/emersion/go-sasl" "github.com/emersion/go-smtp" "github.com/emersion/go-smtp/backendutil" ) var _ smtp.Backend = &backendutil.TransformBackend{} type message struct { From string To []string Data []byte } type backend struct { messages []*message anonmsgs []*message userErr error } func (be *backend) NewSession(c *smtp.Conn) (smtp.Session, error) { return &session{backend: be, anonymous: true}, nil } type session struct { backend *backend anonymous bool msg *message } func (s *session) Reset() { s.msg = &message{} } func (s *session) Logout() error { return nil } func (s *session) AuthMechanisms() []string { return []string{sasl.Plain} } func (s *session) Auth(mech string) (sasl.Server, error) { return sasl.NewPlainServer(func(identity, username, password string) error { if identity != "" && identity != username { return errors.New("Invalid identity") } if username != "username" || password != "password" { return errors.New("Invalid username or password") } s.anonymous = false return nil }), nil } func (s *session) Mail(from string, opts *smtp.MailOptions) error { if s.backend.userErr != nil { return s.backend.userErr } s.Reset() s.msg.From = from return nil } func (s *session) Rcpt(to string, opts *smtp.RcptOptions) error { s.msg.To = append(s.msg.To, to) return nil } func (s *session) Data(r io.Reader) error { if b, err := ioutil.ReadAll(r); err != nil { return err } else { s.msg.Data = b if s.anonymous { s.backend.anonmsgs = append(s.backend.anonmsgs, s.msg) } else { s.backend.messages = append(s.backend.messages, s.msg) } } return nil } type serverConfigureFunc func(*smtp.Server) func transformMailString(s string) (string, error) { s = base64.StdEncoding.EncodeToString([]byte(s)) return s, nil } func transformMailReader(r io.Reader) (io.Reader, error) { pr, pw := io.Pipe() w := base64.NewEncoder(base64.StdEncoding, pw) go copyAndClose(w, r, func(err error) { pw.CloseWithError(err) }) return pr, nil } func copyAndClose(w io.WriteCloser, r io.Reader, done func(err error)) { _, err := io.Copy(w, r) w.Close() done(err) } func testServer(t *testing.T, fn ...serverConfigureFunc) (be *backend, s *smtp.Server, c net.Conn, scanner *bufio.Scanner) { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } be = new(backend) tbe := &backendutil.TransformBackend{ Backend: be, TransformMail: transformMailString, TransformRcpt: transformMailString, TransformData: transformMailReader, } s = smtp.NewServer(tbe) s.Domain = "localhost" s.AllowInsecureAuth = true for _, f := range fn { f(s) } go s.Serve(l) c, err = net.Dial("tcp", l.Addr().String()) if err != nil { t.Fatal(err) } scanner = bufio.NewScanner(c) return } func testServerGreeted(t *testing.T, fn ...serverConfigureFunc) (be *backend, s *smtp.Server, c net.Conn, scanner *bufio.Scanner) { be, s, c, scanner = testServer(t, fn...) scanner.Scan() if scanner.Text() != "220 localhost ESMTP Service Ready" { t.Fatal("Invalid greeting:", scanner.Text()) } return } func testServerEhlo(t *testing.T, fn ...serverConfigureFunc) (be *backend, s *smtp.Server, c net.Conn, scanner *bufio.Scanner, caps map[string]bool) { be, s, c, scanner = testServerGreeted(t, fn...) io.WriteString(c, "EHLO localhost\r\n") scanner.Scan() if scanner.Text() != "250-Hello localhost" { t.Fatal("Invalid EHLO response:", scanner.Text()) } expectedCaps := []string{"PIPELINING", "8BITMIME"} caps = make(map[string]bool) for scanner.Scan() { s := scanner.Text() if strings.HasPrefix(s, "250 ") { caps[strings.TrimPrefix(s, "250 ")] = true break } else { if !strings.HasPrefix(s, "250-") { t.Fatal("Invalid capability response:", s) } caps[strings.TrimPrefix(s, "250-")] = true } } for _, cap := range expectedCaps { if !caps[cap] { t.Fatal("Missing capability:", cap) } } return } func testServerAuthenticated(t *testing.T) (be *backend, s *smtp.Server, c net.Conn, scanner *bufio.Scanner) { be, s, c, scanner, caps := testServerEhlo(t) if _, ok := caps["AUTH PLAIN"]; !ok { t.Fatal("AUTH PLAIN capability is missing when auth is enabled") } io.WriteString(c, "AUTH PLAIN\r\n") scanner.Scan() if scanner.Text() != "334 " { t.Fatal("Invalid AUTH response:", scanner.Text()) } io.WriteString(c, "AHVzZXJuYW1lAHBhc3N3b3Jk\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "235 ") { t.Fatal("Invalid AUTH response:", scanner.Text()) } return } func TestServer(t *testing.T) { be, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid RCPT response:", scanner.Text()) } io.WriteString(c, "DATA\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "354 ") { t.Fatal("Invalid DATA response:", scanner.Text()) } io.WriteString(c, "Hey <3\r\n") io.WriteString(c, ".\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid DATA response:", scanner.Text()) } if len(be.messages) != 1 || len(be.anonmsgs) != 0 { t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs) } msg := be.messages[0] // base64 of "root@nsa.gov" if msg.From != "cm9vdEBuc2EuZ292" { t.Fatal("Invalid mail sender:", msg.From) } // base64 of "root@gchq.gov.uk" if len(msg.To) != 1 || msg.To[0] != "cm9vdEBnY2hxLmdvdi51aw==" { t.Fatal("Invalid mail recipients:", msg.To) } // base64 of "Hey <3\n" (with actual newline) if string(msg.Data) != "SGV5IDwzDQo=" { t.Fatal("Invalid mail data:", string(msg.Data)) } } func TestServer_tooLongMessage(t *testing.T) { be, s, c, scanner := testServerAuthenticated(t) defer s.Close() s.MaxMessageBytes = 50 io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() io.WriteString(c, "DATA\r\n") scanner.Scan() io.WriteString(c, "This is a very long message.\r\n") io.WriteString(c, "Much longer than you can possibly imagine.\r\n") io.WriteString(c, "And much longer than the server's MaxMessageBytes.\r\n") io.WriteString(c, ".\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "552 ") { t.Fatal("Invalid DATA response, expected an error but got:", scanner.Text()) } if len(be.messages) != 0 || len(be.anonmsgs) != 0 { t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs) } } func TestServer_anonymousUserOK(t *testing.T) { be, s, c, scanner, _ := testServerEhlo(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM: root@nsa.gov\r\n") scanner.Scan() io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() io.WriteString(c, "DATA\r\n") scanner.Scan() io.WriteString(c, "Hey <3\r\n") io.WriteString(c, ".\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid DATA response:", scanner.Text()) } if len(be.messages) != 0 || len(be.anonmsgs) != 1 { t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs) } msg := be.anonmsgs[0] // base64 of "root@nsa.gov" if msg.From != "cm9vdEBuc2EuZ292" { t.Fatal("Invalid mail sender:", msg.From) } // base64 of "root@gchq.gov.uk" if len(msg.To) != 1 || msg.To[0] != "cm9vdEBnY2hxLmdvdi51aw==" { t.Fatal("Invalid mail recipients:", msg.To) } // base64 of "Hey <3\r\n" (with actual newline) if string(msg.Data) != "SGV5IDwzDQo=" { t.Fatal("Invalid mail data:", string(msg.Data)) } } go-smtp-0.21.2/client.go000066400000000000000000000561141461243402000147720ustar00rootroot00000000000000// 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 { // keep a reference to the connection so it can be used to create a TLS // connection later conn net.Conn text *textproto.Conn serverName string lmtp bool ext map[string]string // supported extensions 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 defaultDialer = net.Dialer{Timeout: 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". // // This function returns a plaintext connection. To enable TLS, use // DialStartTLS. func Dial(addr string) (*Client, error) { conn, err := defaultDialer.Dial("tcp", addr) if err != nil { return nil, err } client := NewClient(conn) client.serverName, _, _ = net.SplitHostPort(addr) return client, nil } // 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: &defaultDialer, Config: tlsConfig, } conn, err := tlsDialer.Dial("tcp", addr) if err != nil { return nil, err } client := NewClient(conn) client.serverName, _, _ = net.SplitHostPort(addr) return client, nil } // DialStartTLS retruns a new Client connected to an SMTP server via STARTTLS // at addr. The addr must include a port, as in "mail.example.com:smtp". // // A nil tlsConfig is equivalent to a zero tls.Config. func DialStartTLS(addr string, tlsConfig *tls.Config) (*Client, error) { c, err := Dial(addr) if err != nil { return nil, err } if err := initStartTLS(c, tlsConfig); err != nil { c.Close() return nil, err } return c, nil } // 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) *Client { c := &Client{ 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) return c } // NewClientStartTLS creates a new Client and performs a STARTTLS command. func NewClientStartTLS(conn net.Conn, tlsConfig *tls.Config) (*Client, error) { c := NewClient(conn) if err := initStartTLS(c, tlsConfig); err != nil { c.Close() return nil, err } return c, nil } func initStartTLS(c *Client, tlsConfig *tls.Config) error { 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(tlsConfig); err != nil { return err } return 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) *Client { c := NewClient(conn) c.lmtp = true return c } // 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) } // Close closes the connection. func (c *Client) Close() error { return c.text.Close() } func (c *Client) greet() error { // Initial greeting timeout. RFC 5321 recommends 5 minutes. c.conn.SetDeadline(time.Now().Add(c.CommandTimeout)) defer c.conn.SetDeadline(time.Time{}) _, _, err := c.readResponse(220) if err != nil { c.text.Close() return err } return nil } // hello runs a hello exchange if needed. func (c *Client) hello() error { if c.didHello { return c.helloError } c.didHello = true if err := c.greet(); err != nil { c.helloError = err return c.helloError } if err := c.ehlo(); err != nil { var smtpError *SMTPError if errors.As(err, &smtpError) && (smtpError.Code == 500 || smtpError.Code == 502) { // The server doesn't support EHLO, fallback to HELO c.helloError = c.helo() } else { c.helloError = err } } 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() } func (c *Client) readResponse(expectCode int) (int, string, error) { code, msg, err := c.text.ReadResponse(expectCode) if protoErr, ok := err.(*textproto.Error); ok { err = toSMTPErr(protoErr) } return code, msg, err } // 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) return c.readResponse(expectCode) } // 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]] = "" } } } 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 == "" && c.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 } var resp64 []byte if len(resp) > 0 { resp64 = make([]byte, encoding.EncodedLen(len(resp))) encoding.Encode(resp64, resp) } else if resp != nil { resp64 = []byte{'='} } 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 } var sb strings.Builder // A high enough power of 2 than 510+14+26+11+9+9+39+500 sb.Grow(2048) fmt.Fprintf(&sb, "MAIL FROM:<%s>", from) if _, ok := c.ext["8BITMIME"]; ok { sb.WriteString(" BODY=8BITMIME") } if _, ok := c.ext["SIZE"]; ok && opts != nil && opts.Size != 0 { fmt.Fprintf(&sb, " SIZE=%v", opts.Size) } if opts != nil && opts.RequireTLS { if _, ok := c.ext["REQUIRETLS"]; ok { sb.WriteString(" REQUIRETLS") } else { return errors.New("smtp: server does not support REQUIRETLS") } } if opts != nil && opts.UTF8 { if _, ok := c.ext["SMTPUTF8"]; ok { sb.WriteString(" SMTPUTF8") } else { return errors.New("smtp: server does not support SMTPUTF8") } } if _, ok := c.ext["DSN"]; ok && opts != nil { switch opts.Return { case DSNReturnFull, DSNReturnHeaders: fmt.Fprintf(&sb, " RET=%s", string(opts.Return)) case "": // This space is intentionally left blank default: return errors.New("smtp: Unknown RET parameter value") } if opts.EnvelopeID != "" { if !isPrintableASCII(opts.EnvelopeID) { return errors.New("smtp: Malformed ENVID parameter value") } fmt.Fprintf(&sb, " ENVID=%s", encodeXtext(opts.EnvelopeID)) } } if opts != nil && opts.Auth != nil { if _, ok := c.ext["AUTH"]; ok { fmt.Fprintf(&sb, " AUTH=%s", encodeXtext(*opts.Auth)) } // We can safely discard parameter if server does not support AUTH. } _, _, err := c.cmd(250, "%s", sb.String()) 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 opts is not nil, RCPT 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) Rcpt(to string, opts *RcptOptions) error { if err := validateLine(to); err != nil { return err } var sb strings.Builder // A high enough power of 2 than 510+29+501 sb.Grow(2048) fmt.Fprintf(&sb, "RCPT TO:<%s>", to) if _, ok := c.ext["DSN"]; ok && opts != nil { if opts.Notify != nil && len(opts.Notify) != 0 { sb.WriteString(" NOTIFY=") if err := checkNotifySet(opts.Notify); err != nil { return errors.New("smtp: Malformed NOTIFY parameter value") } for i, v := range opts.Notify { if i != 0 { sb.WriteString(",") } sb.WriteString(string(v)) } } if opts.OriginalRecipient != "" { var enc string switch opts.OriginalRecipientType { case DSNAddressTypeRFC822: if !isPrintableASCII(opts.OriginalRecipient) { return errors.New("smtp: Illegal address") } enc = encodeXtext(opts.OriginalRecipient) case DSNAddressTypeUTF8: if _, ok := c.ext["SMTPUTF8"]; ok { enc = encodeUTF8AddrUnitext(opts.OriginalRecipient) } else { enc = encodeUTF8AddrXtext(opts.OriginalRecipient) } default: return errors.New("smtp: Unknown address type") } fmt.Fprintf(&sb, " ORCPT=%s;%s", string(opts.OriginalRecipientType), enc) } } if _, _, err := c.cmd(25, "%s", sb.String()); 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.readResponse(250); err != nil { if smtpErr, ok := err.(*SMTPError); ok { if d.statusCb != nil { d.statusCb(rcpt, smtpErr) } } else { return err } } else if d.statusCb != nil { d.statusCb(rcpt, nil) } expectedResponses-- } } else { _, _, err := d.c.readResponse(250) if err != nil { 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 // DialStartTLS 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, nil); err != nil { return err } } w, err := c.Data() if err != nil { return err } _, err = io.Copy(w, r) if err != nil { return err } return w.Close() } var testHookStartTLS func(*tls.Config) // nil, except for tests func sendMail(addr string, implicitTLS bool, 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 } } var ( c *Client err error ) if implicitTLS { c, err = DialTLS(addr, nil) } else { c, err = DialStartTLS(addr, nil) } if err != nil { return err } defer c.Close() if a != nil { if ok, _ := c.Extension("AUTH"); !ok { return errors.New("smtp: server doesn't support AUTH") } if err = c.Auth(a); err != nil { return err } } if err := c.SendMail(from, to, r); err != nil { return err } return c.Quit() } // 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 { return sendMail(addr, false, a, from, to, r) } // SendMailTLS works like SendMail, but with implicit TLS. func SendMailTLS(addr string, a sasl.Client, from string, to []string, r io.Reader) error { return sendMail(addr, true, a, 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, "" } ext = strings.ToUpper(ext) param, ok := c.ext[ext] return ok, param } // SupportsAuth checks whether an authentication mechanism is supported. func (c *Client) SupportsAuth(mech string) bool { if err := c.hello(); err != nil { return false } mechs, ok := c.ext["AUTH"] if !ok { return false } for _, m := range strings.Split(mechs, " ") { if strings.EqualFold(m, mech) { return true } } return false } // MaxMessageSize returns the maximum message size accepted by the server. // 0 means unlimited. // // If the server doesn't convey this information, ok = false is returned. func (c *Client) MaxMessageSize() (size int, ok bool) { if err := c.hello(); err != nil { return 0, false } v := c.ext["SIZE"] if v == "" { return 0, false } size, err := strconv.Atoi(v) if err != nil || size < 0 { return 0, false } return size, true } // 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.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 { 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) } // validateLine checks to see if a line has CR or LF. 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 } go-smtp-0.21.2/client_test.go000066400000000000000000000707341461243402000160350ustar00rootroot00000000000000// 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 ( "bufio" "bytes" "crypto/tls" "crypto/x509" "errors" "io" "net" "net/textproto" "reflect" "strings" "testing" "time" "github.com/emersion/go-sasl" ) // Don't send a trailing space on AUTH command when there's no initial response: // https://github.com/golang/go/issues/17794 func TestClientAuthTrimSpace(t *testing.T) { server := "220 hello world\r\n" + "200 some more" var wrote bytes.Buffer var fake faker fake.ReadWriter = struct { io.Reader io.Writer }{ strings.NewReader(server), &wrote, } c := NewClient(fake) c.didHello = true c.Auth(toServerNoRespAuth{}) c.Close() if got, want := wrote.String(), "AUTH FOOAUTH\r\n*\r\n"; got != want { t.Errorf("wrote %q; want %q", got, want) } } // toServerNoRespAuth is an implementation of Auth that only implements // the Start method, and returns "FOOAUTH", nil, nil. Notably, it returns // nil for "toServer" so we can test that we don't send spaces at the end of // the line. See TestClientAuthTrimSpace. type toServerNoRespAuth struct{} func (toServerNoRespAuth) Start() (proto string, toServer []byte, err error) { return "FOOAUTH", nil, nil } func (toServerNoRespAuth) Next(fromServer []byte) (toServer []byte, err error) { panic("unexpected call") } type faker struct { io.ReadWriter } func (f faker) Close() error { return nil } func (f faker) LocalAddr() net.Addr { return nil } func (f faker) RemoteAddr() net.Addr { return nil } func (f faker) SetDeadline(time.Time) error { return nil } func (f faker) SetReadDeadline(time.Time) error { return nil } func (f faker) SetWriteDeadline(time.Time) error { return nil } func TestBasic(t *testing.T) { server := strings.Join(strings.Split(basicServer, "\n"), "\r\n") client := strings.Join(strings.Split(basicClient, "\n"), "\r\n") var cmdbuf bytes.Buffer bcmdbuf := bufio.NewWriter(&cmdbuf) var fake faker fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) c := &Client{text: textproto.NewConn(fake), conn: fake, localName: "localhost"} if err := c.helo(); err != nil { t.Fatalf("HELO failed: %s", err) } if err := c.ehlo(); err == nil { t.Fatalf("Expected first EHLO to fail") } if err := c.ehlo(); err != nil { t.Fatalf("Second EHLO failed: %s", err) } c.didHello = true if ok, args := c.Extension("aUtH"); !ok || args != "LOGIN PLAIN" { t.Fatalf("Expected AUTH supported") } if ok, _ := c.Extension("DSN"); ok { t.Fatalf("Shouldn't support DSN") } if !c.SupportsAuth("PLAIN") { t.Errorf("Expected AUTH PLAIN supported") } if size, ok := c.MaxMessageSize(); !ok { t.Errorf("Expected SIZE supported") } else if size != 35651584 { t.Errorf("Expected SIZE=35651584, got %v", size) } if err := c.Mail("user@gmail.com", nil); err == nil { t.Fatalf("MAIL should require authentication") } if err := c.Verify("user1@gmail.com"); err == nil { t.Fatalf("First VRFY: expected no verification") } if err := c.Verify("user2@gmail.com>\r\nDATA\r\nAnother injected message body\r\n.\r\nQUIT\r\n"); err == nil { t.Fatalf("VRFY should have failed due to a message injection attempt") } if err := c.Verify("user2@gmail.com"); err != nil { t.Fatalf("Second VRFY: expected verification, got %s", err) } c.serverName = "smtp.google.com" if err := c.Auth(sasl.NewPlainClient("", "user", "pass")); err != nil { t.Fatalf("AUTH failed: %s", err) } if err := c.Rcpt("golang-nuts@googlegroups.com>\r\nDATA\r\nInjected message body\r\n.\r\nQUIT\r\n", nil); err == nil { t.Fatalf("RCPT should have failed due to a message injection attempt") } if err := c.Mail("user@gmail.com>\r\nDATA\r\nAnother injected message body\r\n.\r\nQUIT\r\n", nil); err == nil { t.Fatalf("MAIL should have failed due to a message injection attempt") } if err := c.Mail("user@gmail.com", nil); err != nil { t.Fatalf("MAIL failed: %s", err) } if err := c.Rcpt("golang-nuts@googlegroups.com", nil); err != nil { t.Fatalf("RCPT failed: %s", err) } msg := `From: user@gmail.com To: golang-nuts@googlegroups.com Subject: Hooray for Go Line 1 .Leading dot line . Goodbye.` w, err := c.Data() if err != nil { t.Fatalf("DATA failed: %s", err) } if _, err := w.Write([]byte(msg)); err != nil { t.Fatalf("Data write failed: %s", err) } if err := w.Close(); err != nil { t.Fatalf("Bad data response: %s", err) } if err := c.Quit(); err != nil { t.Fatalf("QUIT failed: %s", err) } bcmdbuf.Flush() actualcmds := cmdbuf.String() if client != actualcmds { t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client) } } func TestBasic_SMTPError(t *testing.T) { faultyServer := `220 mx.google.com at your service 250-mx.google.com at your service 250 ENHANCEDSTATUSCODES 500 5.0.0 Failing with enhanced code 500 Failing without enhanced code 500-5.0.0 Failing with multiline and enhanced code 500 5.0.0 ... still failing ` // RFC 2034 says that enhanced codes *SHOULD* be included in errors, // this means it can be violated hence we need to handle last // case properly. faultyServer = strings.Join(strings.Split(faultyServer, "\n"), "\r\n") var wrote bytes.Buffer var fake faker fake.ReadWriter = struct { io.Reader io.Writer }{ strings.NewReader(faultyServer), &wrote, } c := NewClient(fake) err := c.Mail("whatever", nil) if err == nil { t.Fatal("MAIL succeeded") } smtpErr, ok := err.(*SMTPError) if !ok { t.Fatal("Returned error is not SMTPError") } if smtpErr.Code != 500 { t.Fatalf("Wrong status code, got %d, want %d", smtpErr.Code, 500) } if smtpErr.EnhancedCode != (EnhancedCode{5, 0, 0}) { t.Fatalf("Wrong enhanced code, got %v, want %v", smtpErr.EnhancedCode, EnhancedCode{5, 0, 0}) } if smtpErr.Message != "Failing with enhanced code" { t.Fatalf("Wrong message, got %s, want %s", smtpErr.Message, "Failing with enhanced code") } err = c.Mail("whatever", nil) if err == nil { t.Fatal("MAIL succeeded") } smtpErr, ok = err.(*SMTPError) if !ok { t.Fatal("Returned error is not SMTPError") } if smtpErr.Code != 500 { t.Fatalf("Wrong status code, got %d, want %d", smtpErr.Code, 500) } if smtpErr.Message != "Failing without enhanced code" { t.Fatalf("Wrong message, got %s, want %s", smtpErr.Message, "Failing without enhanced code") } err = c.Mail("whatever", nil) if err == nil { t.Fatal("MAIL succeeded") } smtpErr, ok = err.(*SMTPError) if !ok { t.Fatal("Returned error is not SMTPError") } if smtpErr.Code != 500 { t.Fatalf("Wrong status code, got %d, want %d", smtpErr.Code, 500) } if want := "Failing with multiline and enhanced code\n... still failing"; smtpErr.Message != want { t.Fatalf("Wrong message, got %s, want %s", smtpErr.Message, want) } } func TestClient_TooLongLine(t *testing.T) { faultyServer := []string{ "220 mx.google.com at your service\r\n", "250 2.0.0 Kk\r\n", "500 5.0.0 nU6XC5JJUfiuIkC7NhrxZz36Rl/rXpkfx9QdeZJ+rno6W5J9k9HvniyWXBBi1gOZ/CUXEI6K7Uony70eiVGGGkdFhP1rEvMGny1dqIRo3NM2NifrvvLIKGeX6HrYmkc7NMn9BwHyAnt5oLe5eNVDI+grwIikVPNVFZi0Dg4Xatdg5Cs8rH1x9BWhqyDoxosJst4wRoX4AymYygUcftM3y16nVg/qcb1GJwxSNbah7VjOiSrk6MlTdGR/2AwIIcSw7pZVJjGbCorniOTvKBcyut1YdbrX/4a/dBhvLfZtdSccqyMZAdZno+tGrnu+N2ghFvz6cx6bBab9Z4JJQMlkK/g1y7xjEPr6nKwruAf71NzOclPK5wzs2hY3Ku9xEjU0Cd+g/OjAzVsmeJk2U0q+vmACZsFAiOlRynXKFPLqMAg8skM5lioRTm05K/u3aBaUq0RKloeBHZ/zNp/kfHNp6TmJKAzvsXD3Xdo+PRAgCZRTRAl3ydGdrOOjxTULCVlgOL6xSAJdj9zGkzQoEW4tRmp1OiIab4GSxCtkIo7XnAowJ7EPUfDGTV3hhl5Qn7jvZjPCPlruRTtzVTho7D3HBEouWv1qDsqdED23myw0Ma9ZlobSf9eHqsSv1MxjKG2D5DdFBACu6pXGz3ceGreOHYWnI74TkoHtQ5oNuF6VUkGjGN+f4fOaiypQ54GJ8skTNoSCHLK4XF8ZutSxWzMR+LKoJBWMb6bdAiFNt+vXZOUiTgmTqs6Sw79JXqDX9YFxryJMKjHMiFkm+RZbaK5sIOXqyq+RNmOJ+G0unrQHQMCES476c7uvOlYrNoJtq+uox1qFdisIE/8vfSoKBlTtw+r2m87djIQh4ip/hVmalvtiF5fnVTxigbtwLWv8rAOCXKoktU0c2ie0a5hGtvZT0SXxwX8K2CeYXb81AFD2IaLt/p8Q4WuZ82eOCeXP72qP9yWYj6mIZdgyimm8wjrDowt2yPJU28ZD6k3Ei6C31OKgMpCf8+MW504/VCwld7czAIwjJiZe3DxtUdfM7Q565OzLiWQgI8fxjsvlCKMiOY7q42IGGsVxXJAFMtDKdchgqQA1PJR1vrw+SbI3Mh4AGnn8vKn+WTsieB3qkloo7MZlpMz/bwPXg7XadOVkUaVeHrZ5OsqDWhsWOLtPZLi5XdNazPzn9uxWbpelXEBKAjZzfoawSUgGT5vCYACNfz/yIw1DB067N+HN1KvVddI6TNBA32lpqkQ6VwdWztq6pREE51sNl9p7MUzr+ef0331N5DqQsy+epmRDwebosCx15l/rpvBc91OnxmMMXDNtmxSzVxaZjyGDmJ7RDdTy/Su76AlaMP1zxivxg2MU/9zyTzM16coIAMOd/6Uo9ezKgbZEPeMROKTzAld9BhK9BBPWofoQ0mBkVc7btnahQe3u8HoD6SKCkr9xcTcC9ZKpLkc4svrmxT9e0858pjhis9BbWD/owa6552n2+KwUMRyB8ys7rPL86hh9lBTS+05cVL+BmJfNHOA6ZizdGc3lpwIVbFmzMR5BM0HRf3OCntkWojgsdsP8BGZWHiCGGqA7YGa5AOleR887r8Zhyp47DT3Cn3Rg/icYurIx7Yh0p696gxfANo4jEkE2BOroIscDnhauwck5CCJMcabpTrGwzK8NJ+xZnCUplXnZiIaj85Uh9+yI670B4bybWlZoVmALUxxuQ8bSMAp7CAzMcMWbYJHwBqLF8V2qMj3/g81S3KOptn8b7Idh7IMzAkV8VxE3qAguzwS0zEu8l894sOFUPiJq2/llFeiHNOcEQUGJ+8ATJSAFOMDXAeQS2FoIDOYdesO6yacL0zUkvDydWbA84VXHW8DvdHPli/8hmc++dn5CXSDeBJfC/yypvrpLgkSilZMuHEYHEYHEYEHYEHEYEHEYEHEYEYEYEYEYEYEYEYEYEYEYEYEYEYEYEYEYEYEYYEYEYEYEYEYEYEYYEYEYEYEYEYEYEYEY\r\n", "250 2.0.0 Kk\r\n", } // The pipe is used to avoid bufio.Reader reading the too long line ahead // of time (in NewClient) and failing eariler than we expect. pr, pw := io.Pipe() go func() { for _, l := range faultyServer { pw.Write([]byte(l)) } pw.Close() }() var wrote bytes.Buffer var fake faker fake.ReadWriter = struct { io.Reader io.Writer }{ pr, &wrote, } c := NewClient(fake) err := c.Mail("whatever", nil) if err != ErrTooLongLine { t.Fatal("MAIL succeeded or returned a different error:", err) } // ErrTooLongLine is "sticky" since the connection is in broken state and // the only reasonable way to recover is to close it. err = c.Mail("whatever", nil) if err != ErrTooLongLine { t.Fatal("Second MAIL succeeded or returned a different error:", err) } } var basicServer = `250 mx.google.com at your service 502 Unrecognized command. 250-mx.google.com at your service 250-SIZE 35651584 250-AUTH LOGIN PLAIN 250 8BITMIME 530 Authentication required 252 Send some mail, I'll try my best 250 User is valid 235 Accepted 250 Sender OK 250 Receiver OK 354 Go ahead 250 Data OK 221 OK ` var basicClient = `HELO localhost EHLO localhost EHLO localhost MAIL FROM: BODY=8BITMIME VRFY user1@gmail.com VRFY user2@gmail.com AUTH PLAIN AHVzZXIAcGFzcw== MAIL FROM: BODY=8BITMIME RCPT TO: DATA From: user@gmail.com To: golang-nuts@googlegroups.com Subject: Hooray for Go Line 1 ..Leading dot line . Goodbye. . QUIT ` func TestNewClient(t *testing.T) { server := strings.Join(strings.Split(newClientServer, "\n"), "\r\n") client := strings.Join(strings.Split(newClientClient, "\n"), "\r\n") var cmdbuf bytes.Buffer bcmdbuf := bufio.NewWriter(&cmdbuf) out := func() string { bcmdbuf.Flush() return cmdbuf.String() } var fake faker fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) c := NewClient(fake) defer c.Close() if ok, args := c.Extension("aUtH"); !ok || args != "LOGIN PLAIN" { t.Fatalf("Expected AUTH supported") } if ok, _ := c.Extension("DSN"); ok { t.Fatalf("Shouldn't support DSN") } if err := c.Quit(); err != nil { t.Fatalf("QUIT failed: %s", err) } actualcmds := out() if client != actualcmds { t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client) } } var newClientServer = `220 hello world 250-mx.google.com at your service 250-SIZE 35651584 250-AUTH LOGIN PLAIN 250 8BITMIME 221 OK ` var newClientClient = `EHLO localhost QUIT ` func TestNewClient2(t *testing.T) { server := strings.Join(strings.Split(newClient2Server, "\n"), "\r\n") client := strings.Join(strings.Split(newClient2Client, "\n"), "\r\n") var cmdbuf bytes.Buffer bcmdbuf := bufio.NewWriter(&cmdbuf) var fake faker fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) c := NewClient(fake) defer c.Close() if ok, _ := c.Extension("DSN"); ok { t.Fatalf("Shouldn't support DSN") } if err := c.Quit(); err != nil { t.Fatalf("QUIT failed: %s", err) } bcmdbuf.Flush() actualcmds := cmdbuf.String() if client != actualcmds { t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client) } } var newClient2Server = `220 hello world 502 EH? 250-mx.google.com at your service 250-SIZE 35651584 250-AUTH LOGIN PLAIN 250 8BITMIME 221 OK ` var newClient2Client = `EHLO localhost HELO localhost QUIT ` func TestHello(t *testing.T) { if len(helloServer) != len(helloClient) { t.Fatalf("Hello server and client size mismatch") } for i := 0; i < len(helloServer); i++ { server := strings.Join(strings.Split(baseHelloServer+helloServer[i], "\n"), "\r\n") client := strings.Join(strings.Split(baseHelloClient+helloClient[i], "\n"), "\r\n") var cmdbuf bytes.Buffer bcmdbuf := bufio.NewWriter(&cmdbuf) var fake faker fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) c := NewClient(fake) defer c.Close() c.serverName = "fake.host" c.localName = "customhost" var err error switch i { case 0: err = c.Hello("hostinjection>\n\rDATA\r\nInjected message body\r\n.\r\nQUIT\r\n") if err == nil { t.Errorf("Expected Hello to be rejected due to a message injection attempt") } err = c.Hello("customhost") case 1: err = c.startTLS(nil) if err.Error() == "SMTP error 502: Not implemented" { err = nil } case 2: err = c.Verify("test@example.com") case 3: c.serverName = "smtp.google.com" err = c.Auth(sasl.NewPlainClient("", "user", "pass")) case 4: err = c.Mail("test@example.com", nil) case 5: ok, _ := c.Extension("feature") if ok { t.Errorf("Expected FEATURE not to be supported") } case 6: err = c.Reset() case 7: err = c.Quit() case 8: err = c.Verify("test@example.com") if err != nil { err = c.Hello("customhost") if err != nil { t.Errorf("Want error, got none") } } case 9: err = c.Noop() default: t.Fatalf("Unhandled command") } if err != nil { t.Errorf("Command %d failed: %v", i, err) } bcmdbuf.Flush() actualcmds := cmdbuf.String() if client != actualcmds { t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client) } } } var baseHelloServer = `220 hello world 502 EH? 250-mx.google.com at your service 250 FEATURE ` var helloServer = []string{ "", "502 Not implemented\n", "250 User is valid\n", "235 Accepted\n", "250 Sender ok\n", "", "250 Reset ok\n", "221 Goodbye\n", "250 Sender ok\n", "250 ok\n", } var baseHelloClient = `EHLO customhost HELO customhost ` var helloClient = []string{ "", "STARTTLS\n", "VRFY test@example.com\n", "AUTH PLAIN AHVzZXIAcGFzcw==\n", "MAIL FROM:\n", "", "RSET\n", "QUIT\n", "VRFY test@example.com\n", "NOOP\n", } var shuttingDownServerHello = `220 hello world 421 Service not available, closing transmission channel ` func TestHello_421Response(t *testing.T) { server := strings.Join(strings.Split(shuttingDownServerHello, "\n"), "\r\n") client := "EHLO customhost\r\n" var cmdbuf bytes.Buffer bcmdbuf := bufio.NewWriter(&cmdbuf) var fake faker fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) c := NewClient(fake) defer c.Close() c.serverName = "fake.host" c.localName = "customhost" err := c.Hello("customhost") if err == nil { t.Errorf("Expected Hello to fail") } var smtpError *SMTPError if !errors.As(err, &smtpError) || smtpError.Code != 421 || smtpError.Message != "Service not available, closing transmission channel" { t.Errorf("Expected error 421, got %v", err) } bcmdbuf.Flush() actualcmds := cmdbuf.String() if client != actualcmds { t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client) } } var sendMailServer = `220 hello world 502 EH? 250 mx.google.com at your service 250 Sender ok 250 Receiver ok 354 Go ahead 250 Data ok 221 Goodbye ` var sendMailClient = `EHLO localhost HELO localhost MAIL FROM: RCPT TO: DATA From: test@example.com To: other@example.com Subject: SendMail test SendMail is working for me. . QUIT ` func TestAuthFailed(t *testing.T) { server := strings.Join(strings.Split(authFailedServer, "\n"), "\r\n") client := strings.Join(strings.Split(authFailedClient, "\n"), "\r\n") var cmdbuf bytes.Buffer bcmdbuf := bufio.NewWriter(&cmdbuf) var fake faker fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) c := NewClient(fake) defer c.Close() c.serverName = "smtp.google.com" err := c.Auth(sasl.NewPlainClient("", "user", "pass")) if err == nil { t.Error("Auth: expected error; got none") } else if err.Error() != "SMTP error 535: Invalid credentials\nplease see www.example.com" { t.Errorf("Auth: got error: %v, want: %s", err, "Invalid credentials\nplease see www.example.com") } bcmdbuf.Flush() actualcmds := cmdbuf.String() if client != actualcmds { t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client) } } var authFailedServer = `220 hello world 250-mx.google.com at your service 250 AUTH LOGIN PLAIN 535-Invalid credentials 535 please see www.example.com 221 Goodbye ` var authFailedClient = `EHLO localhost AUTH PLAIN AHVzZXIAcGFzcw== * ` func TestTLSClient(t *testing.T) { ln := newLocalListener(t) defer ln.Close() errc := make(chan error) go func() { errc <- doSendMail(ln.Addr().String()) }() conn, err := ln.Accept() if err != nil { t.Fatalf("failed to accept connection: %v", err) } defer conn.Close() if err := serverHandle(conn, t); err != nil { t.Fatalf("failed to handle connection: %v", err) } if err := <-errc; err != nil { t.Fatalf("client error: %v", err) } } func TestTLSConnState(t *testing.T) { ln := newLocalListener(t) defer ln.Close() clientDone := make(chan bool) serverDone := make(chan bool) go func() { defer close(serverDone) c, err := ln.Accept() if err != nil { t.Errorf("Server accept: %v", err) return } defer c.Close() if err := serverHandle(c, t); err != nil { t.Errorf("server error: %v", err) } }() go func() { defer close(clientDone) cfg := &tls.Config{ServerName: "example.com"} testHookStartTLS(cfg) // set the RootCAs c, err := DialStartTLS(ln.Addr().String(), cfg) if err != nil { t.Errorf("Client dial: %v", err) return } defer c.Quit() cs, ok := c.TLSConnectionState() if !ok { t.Errorf("TLSConnectionState returned ok == false; want true") return } if cs.Version == 0 || !cs.HandshakeComplete { t.Errorf("ConnectionState = %#v; expect non-zero Version and HandshakeComplete", cs) } }() <-clientDone <-serverDone } func newLocalListener(t *testing.T) net.Listener { ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { ln, err = net.Listen("tcp6", "[::1]:0") } if err != nil { t.Fatal(err) } return ln } type smtpSender struct { w io.Writer } func (s smtpSender) send(f string) { s.w.Write([]byte(f + "\r\n")) } // smtp server, finely tailored to deal with our own client only! func serverHandle(c net.Conn, t *testing.T) error { send := smtpSender{c}.send send("220 127.0.0.1 ESMTP service ready") s := bufio.NewScanner(c) for s.Scan() { switch s.Text() { case "EHLO localhost": send("250-127.0.0.1 ESMTP offers a warm hug of welcome") send("250-STARTTLS") send("250 Ok") case "STARTTLS": send("220 Go ahead") keypair, err := tls.X509KeyPair(localhostCert, localhostKey) if err != nil { return err } config := &tls.Config{Certificates: []tls.Certificate{keypair}} c = tls.Server(c, config) defer c.Close() return serverHandleTLS(c, t) default: t.Fatalf("unrecognized command: %q", s.Text()) } } return s.Err() } func serverHandleTLS(c net.Conn, t *testing.T) error { send := smtpSender{c}.send s := bufio.NewScanner(c) for s.Scan() { switch s.Text() { case "EHLO localhost": send("250 Ok") case "MAIL FROM:": send("250 Ok") case "RCPT TO:": send("250 Ok") case "DATA": send("354 send the mail data, end with .") send("250 Ok") case "Subject: test": case "": case "howdy!": case ".": case "QUIT": send("221 127.0.0.1 Service closing transmission channel") return nil default: t.Fatalf("unrecognized command during TLS: %q", s.Text()) } } return s.Err() } func init() { testRootCAs := x509.NewCertPool() testRootCAs.AppendCertsFromPEM(localhostCert) testHookStartTLS = func(config *tls.Config) { config.RootCAs = testRootCAs } } func doSendMail(hostPort string) error { from := "joe1@example.com" to := []string{"joe2@example.com"} return SendMail(hostPort, nil, from, to, strings.NewReader("Subject: test\n\nhowdy!")) } // localhostCert is a PEM-encoded TLS cert generated from src/crypto/tls: // // go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com \ // --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h var localhostCert = []byte(` -----BEGIN CERTIFICATE----- MIICFDCCAX2gAwIBAgIRAK0xjnaPuNDSreeXb+z+0u4wDQYJKoZIhvcNAQELBQAw EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2 MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw gYkCgYEA0nFbQQuOWsjbGtejcpWz153OlziZM4bVjJ9jYruNw5n2Ry6uYQAffhqa JOInCmmcVe2siJglsyH9aRh6vKiobBbIUXXUU1ABd56ebAzlt0LobLlx7pZEMy30 LqIi9E6zmL3YvdGzpYlkFRnRrqwEtWYbGBf3znO250S56CCWH2UCAwEAAaNoMGYw DgYDVR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQF MAMBAf8wLgYDVR0RBCcwJYILZXhhbXBsZS5jb22HBH8AAAGHEAAAAAAAAAAAAAAA AAAAAAEwDQYJKoZIhvcNAQELBQADgYEAbZtDS2dVuBYvb+MnolWnCNqvw1w5Gtgi NmvQQPOMgM3m+oQSCPRTNGSg25e1Qbo7bgQDv8ZTnq8FgOJ/rbkyERw2JckkHpD4 n4qcK27WkEDBtQFlPihIM8hLIuzWoi/9wygiElTy/tVL3y7fGCvY2/k1KBthtZGF tN8URjVmyEo= -----END CERTIFICATE-----`) // localhostKey is the private key for localhostCert. var localhostKey = []byte(` -----BEGIN RSA PRIVATE KEY----- MIICXgIBAAKBgQDScVtBC45ayNsa16NylbPXnc6XOJkzhtWMn2Niu43DmfZHLq5h AB9+Gpok4icKaZxV7ayImCWzIf1pGHq8qKhsFshRddRTUAF3np5sDOW3QuhsuXHu lkQzLfQuoiL0TrOYvdi90bOliWQVGdGurAS1ZhsYF/fOc7bnRLnoIJYfZQIDAQAB AoGBAMst7OgpKyFV6c3JwyI/jWqxDySL3caU+RuTTBaodKAUx2ZEmNJIlx9eudLA kucHvoxsM/eRxlxkhdFxdBcwU6J+zqooTnhu/FE3jhrT1lPrbhfGhyKnUrB0KKMM VY3IQZyiehpxaeXAwoAou6TbWoTpl9t8ImAqAMY8hlULCUqlAkEA+9+Ry5FSYK/m 542LujIcCaIGoG1/Te6Sxr3hsPagKC2rH20rDLqXwEedSFOpSS0vpzlPAzy/6Rbb PHTJUhNdwwJBANXkA+TkMdbJI5do9/mn//U0LfrCR9NkcoYohxfKz8JuhgRQxzF2 6jpo3q7CdTuuRixLWVfeJzcrAyNrVcBq87cCQFkTCtOMNC7fZnCTPUv+9q1tcJyB vNjJu3yvoEZeIeuzouX9TJE21/33FaeDdsXbRhQEj23cqR38qFHsF1qAYNMCQQDP QXLEiJoClkR2orAmqjPLVhR3t2oB3INcnEjLNSq8LHyQEfXyaFfu4U9l5+fRPL2i jiC0k/9L5dHUsF0XZothAkEA23ddgRs+Id/HxtojqqUT27B8MT/IGNrYsp4DvS/c qgkeluku4GjxRlDMBuXk94xOBEinUs+p/hwP1Alll80Tpg== -----END RSA PRIVATE KEY-----`) func TestLMTP(t *testing.T) { server := strings.Join(strings.Split(lmtpServer, "\n"), "\r\n") client := strings.Join(strings.Split(lmtpClient, "\n"), "\r\n") var cmdbuf bytes.Buffer bcmdbuf := bufio.NewWriter(&cmdbuf) var fake faker fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) c := &Client{text: textproto.NewConn(fake), conn: fake, lmtp: true} if err := c.Hello("localhost"); err != nil { t.Fatalf("LHLO failed: %s", err) } c.didHello = true if err := c.Mail("user@gmail.com", nil); err != nil { t.Fatalf("MAIL failed: %s", err) } if err := c.Rcpt("golang-nuts@googlegroups.com", nil); err != nil { t.Fatalf("RCPT failed: %s", err) } msg := `From: user@gmail.com To: golang-nuts@googlegroups.com Subject: Hooray for Go Line 1 .Leading dot line . Goodbye.` w, err := c.Data() if err != nil { t.Fatalf("DATA failed: %s", err) } if _, err := w.Write([]byte(msg)); err != nil { t.Fatalf("Data write failed: %s", err) } if err := w.Close(); err != nil { t.Fatalf("Bad data response: %s", err) } if err := c.Quit(); err != nil { t.Fatalf("QUIT failed: %s", err) } bcmdbuf.Flush() actualcmds := cmdbuf.String() if client != actualcmds { t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client) } } var lmtpServer = `220 localhost Simple Mail Transfer Service Ready 250-localhost at your service 250-SIZE 35651584 250 8BITMIME 250 Sender OK 250 Receiver OK 354 Go ahead 250 Data OK 221 OK ` var lmtpClient = `LHLO localhost MAIL FROM: BODY=8BITMIME RCPT TO: DATA From: user@gmail.com To: golang-nuts@googlegroups.com Subject: Hooray for Go Line 1 ..Leading dot line . Goodbye. . QUIT ` func TestLMTPData(t *testing.T) { var lmtpServerPartial = `220 localhost Simple Mail Transfer Service Ready 250-localhost at your service 250-SIZE 35651584 250 8BITMIME 250 Sender OK 250 Receiver OK 250 Receiver OK 354 Go ahead 250 This recipient is fine 500 But not this one 221 OK ` server := strings.Join(strings.Split(lmtpServerPartial, "\n"), "\r\n") var cmdbuf bytes.Buffer bcmdbuf := bufio.NewWriter(&cmdbuf) var fake faker fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) c := &Client{text: textproto.NewConn(fake), conn: fake, lmtp: true} if err := c.Hello("localhost"); err != nil { t.Fatalf("LHLO failed: %s", err) } c.didHello = true if err := c.Mail("user@gmail.com", nil); err != nil { t.Fatalf("MAIL failed: %s", err) } if err := c.Rcpt("golang-nuts@googlegroups.com", nil); err != nil { t.Fatalf("RCPT failed: %s", err) } if err := c.Rcpt("golang-not-nuts@googlegroups.com", nil); err != nil { t.Fatalf("RCPT failed: %s", err) } msg := `From: user@gmail.com To: golang-nuts@googlegroups.com Subject: Hooray for Go Line 1 .Leading dot line . Goodbye.` rcpts := []string{} errors := []*SMTPError{} w, err := c.LMTPData(func(rcpt string, status *SMTPError) { rcpts = append(rcpts, rcpt) errors = append(errors, status) }) if err != nil { t.Fatalf("DATA failed: %s", err) } if _, err := w.Write([]byte(msg)); err != nil { t.Fatalf("Data write failed: %s", err) } if err := w.Close(); err != nil { t.Fatalf("Bad data response: %s", err) } if !reflect.DeepEqual(rcpts, []string{"golang-nuts@googlegroups.com", "golang-not-nuts@googlegroups.com"}) { t.Fatal("Status callbacks called for wrong recipients:", rcpts) } if len(errors) != 2 { t.Fatalf("Wrong amount of status callback calls: %v", len(errors)) } if errors[0] != nil { t.Fatalf("Unexpected error status for the first recipient: %v", errors[0]) } if errors[1] == nil { t.Fatalf("Unexpected success status for the second recipient") } if err := c.Quit(); err != nil { t.Fatalf("QUIT failed: %s", err) } } var xtextClient = `MAIL FROM: AUTH=e+3Dmc2@example.com RCPT TO: ORCPT=UTF-8;e\x{3D}mc2@example.com ` func TestClientXtext(t *testing.T) { server := "220 hello world\r\n" + "250 ok\r\n" + "250 ok" client := strings.Join(strings.Split(xtextClient, "\n"), "\r\n") var wrote bytes.Buffer var fake faker fake.ReadWriter = struct { io.Reader io.Writer }{ strings.NewReader(server), &wrote, } c := NewClient(fake) c.didHello = true c.ext = map[string]string{"AUTH": "PLAIN", "DSN": ""} email := "e=mc2@example.com" c.Mail(email, &MailOptions{Auth: &email}) c.Rcpt(email, &RcptOptions{ OriginalRecipientType: DSNAddressTypeUTF8, OriginalRecipient: email, }) c.Close() if got := wrote.String(); got != client { t.Errorf("wrote %q; want %q", got, client) } } const ( dsnEnvelopeID = "e=mc2" dsnEmailRFC822 = "e=mc2@example.com" dsnEmailUTF8 = "e=mc2@ドメイン名例.jp" ) var dsnServer = `220 hello world 250 ok 250 ok 250 ok 250 ok ` var dsnClient = `MAIL FROM: RET=HDRS ENVID=e+3Dmc2 RCPT TO: NOTIFY=NEVER ORCPT=RFC822;e+3Dmc2@example.com RCPT TO: NOTIFY=FAILURE,DELAY ORCPT=UTF-8;e\x{3D}mc2@\x{30C9}\x{30E1}\x{30A4}\x{30F3}\x{540D}\x{4F8B}.jp RCPT TO: ORCPT=UTF-8;e\x{3D}mc2@ドメイン名例.jp ` func TestClientDSN(t *testing.T) { server := strings.Join(strings.Split(dsnServer, "\n"), "\r\n") client := strings.Join(strings.Split(dsnClient, "\n"), "\r\n") var wrote bytes.Buffer var fake faker fake.ReadWriter = struct { io.Reader io.Writer }{ strings.NewReader(server), &wrote, } c := NewClient(fake) c.didHello = true c.ext = map[string]string{"DSN": ""} c.Mail(dsnEmailRFC822, &MailOptions{ Return: DSNReturnHeaders, EnvelopeID: dsnEnvelopeID, }) c.Rcpt(dsnEmailRFC822, &RcptOptions{ OriginalRecipientType: DSNAddressTypeRFC822, OriginalRecipient: dsnEmailRFC822, Notify: []DSNNotify{DSNNotifyNever}, }) c.Rcpt(dsnEmailRFC822, &RcptOptions{ OriginalRecipientType: DSNAddressTypeUTF8, OriginalRecipient: dsnEmailUTF8, Notify: []DSNNotify{DSNNotifyFailure, DSNNotifyDelayed}, }) c.ext["SMTPUTF8"] = "" c.Rcpt(dsnEmailUTF8, &RcptOptions{ OriginalRecipientType: DSNAddressTypeUTF8, OriginalRecipient: dsnEmailUTF8, }) c.Close() if actualcmds := wrote.String(); client != actualcmds { t.Errorf("wrote %q; want %q", actualcmds, client) } } go-smtp-0.21.2/cmd/000077500000000000000000000000001461243402000137215ustar00rootroot00000000000000go-smtp-0.21.2/cmd/smtp-debug-server/000077500000000000000000000000001461243402000172745ustar00rootroot00000000000000go-smtp-0.21.2/cmd/smtp-debug-server/main.go000066400000000000000000000016511461243402000205520ustar00rootroot00000000000000package main import ( "flag" "io" "log" "os" "github.com/emersion/go-smtp" ) var addr = "127.0.0.1:1025" func init() { flag.StringVar(&addr, "l", addr, "Listen address") } type backend struct{} func (bkd *backend) NewSession(c *smtp.Conn) (smtp.Session, error) { return &session{}, nil } type session struct{} func (s *session) AuthPlain(username, password string) error { return nil } func (s *session) Mail(from string, opts *smtp.MailOptions) error { return nil } func (s *session) Rcpt(to string, opts *smtp.RcptOptions) error { return nil } func (s *session) Data(r io.Reader) error { return nil } func (s *session) Reset() {} func (s *session) Logout() error { return nil } func main() { flag.Parse() s := smtp.NewServer(&backend{}) s.Addr = addr s.Domain = "localhost" s.AllowInsecureAuth = true s.Debug = os.Stdout log.Println("Starting SMTP server at", addr) log.Fatal(s.ListenAndServe()) } go-smtp-0.21.2/conn.go000066400000000000000000000762031461243402000144520ustar00rootroot00000000000000package smtp import ( "crypto/tls" "encoding/base64" "errors" "fmt" "io" "io/ioutil" "net" "net/textproto" "regexp" "runtime/debug" "strconv" "strings" "sync" "time" "github.com/emersion/go-sasl" ) // 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 int64 // 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 successfully 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": 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 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 is populated before NewSession so // NewSession can access it via Conn.Hostname. c.helo = domain sess, err := c.server.Backend.NewSession(c) if err != nil { c.helo = "" c.writeError(451, EnhancedCode{4, 0, 0}, err) return } c.setSession(sess) if !enhanced { c.writeResponse(250, EnhancedCode{2, 0, 0}, fmt.Sprintf("Hello %s", domain)) return } caps := []string{ "PIPELINING", "8BITMIME", "ENHANCEDSTATUSCODES", "CHUNKING", } if _, isTLS := c.TLSConnectionState(); c.server.TLSConfig != nil && !isTLS { caps = append(caps, "STARTTLS") } if c.authAllowed() { mechs := c.authMechanisms() authCap := "AUTH" for _, name := range mechs { authCap += " " + name } if len(mechs) > 0 { 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.EnableDSN { caps = append(caps, "DSN") } if c.server.MaxMessageBytes > 0 { caps = append(caps, fmt.Sprintf("SIZE %v", c.server.MaxMessageBytes)) } else { caps = append(caps, "SIZE") } if c.server.MaxRecipients > 0 { caps = append(caps, fmt.Sprintf("LIMITS RCPTMAX=%v", c.server.MaxRecipients)) } 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{5, 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 } arg, ok := cutPrefixFold(arg, "FROM:") if !ok { c.writeResponse(501, EnhancedCode{5, 5, 2}, "Was expecting MAIL arg syntax of FROM:
") return } p := parser{s: strings.TrimSpace(arg)} from, err := p.parseReversePath() if err != nil { c.writeResponse(501, EnhancedCode{5, 5, 2}, "Was expecting MAIL arg syntax of FROM:
") return } args, err := parseArgs(p.s) if err != nil { c.writeResponse(501, EnhancedCode{5, 5, 4}, "Unable to parse MAIL ESMTP parameters") return } 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. for key, value := range args { switch key { case "SIZE": size, err := strconv.ParseUint(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 && int64(size) > c.server.MaxMessageBytes { c.writeResponse(552, EnhancedCode{5, 3, 4}, "Max message size exceeded") return } opts.Size = int64(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": value = strings.ToUpper(value) switch BodyType(value) { case BodyBinaryMIME: if !c.server.EnableBINARYMIME { c.writeResponse(504, EnhancedCode{5, 5, 4}, "BINARYMIME is not implemented") return } c.binarymime = true case Body7Bit, Body8BitMIME: // This space is intentionally left blank default: c.writeResponse(501, EnhancedCode{5, 5, 4}, "Unknown BODY value") return } opts.Body = BodyType(value) case "RET": if !c.server.EnableDSN { c.writeResponse(504, EnhancedCode{5, 5, 4}, "RET is not implemented") return } value = strings.ToUpper(value) switch DSNReturn(value) { case DSNReturnFull, DSNReturnHeaders: // This space is intentionally left blank default: c.writeResponse(501, EnhancedCode{5, 5, 4}, "Unknown RET value") return } opts.Return = DSNReturn(value) case "ENVID": if !c.server.EnableDSN { c.writeResponse(504, EnhancedCode{5, 5, 4}, "ENVID is not implemented") return } value, err := decodeXtext(value) if err != nil || value == "" || !isPrintableASCII(value) { c.writeResponse(501, EnhancedCode{5, 5, 4}, "Malformed ENVID parameter value") return } opts.EnvelopeID = value case "AUTH": value, err := decodeXtext(value) if err != nil || value == "" { c.writeResponse(500, EnhancedCode{5, 5, 4}, "Malformed AUTH parameter value") return } if value == "<>" { value = "" } else { p := parser{s: value} value, err = p.parseMailbox() if err != nil || p.s != "" { c.writeResponse(500, EnhancedCode{5, 5, 4}, "Malformed AUTH parameter mailbox") return } } opts.Auth = &value default: c.writeResponse(500, EnhancedCode{5, 5, 4}, "Unknown MAIL FROM argument") return } } if err := c.Session().Mail(from, opts); err != nil { c.writeError(451, EnhancedCode{4, 0, 0}, err) 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 } // This regexp matches 'EmbeddedUnicodeChar' token defined in // https://datatracker.ietf.org/doc/html/rfc6533.html#section-3 // however it is intentionally relaxed by requiring only '\x{HEX}' to be // present. It also matches disallowed characters in QCHAR and QUCHAR defined // in above. // So it allows us to detect malformed values and report them appropriately. var eUOrDCharRe = regexp.MustCompile(`\\x[{][0-9A-F]+[}]|[[:cntrl:] \\+=]`) // Decodes the utf-8-addr-xtext or the utf-8-addr-unitext form. func decodeUTF8AddrXtext(val string) (string, error) { var replaceErr error decoded := eUOrDCharRe.ReplaceAllStringFunc(val, func(match string) string { if len(match) == 1 { replaceErr = errors.New("disallowed character:" + match) return "" } hexpoint := match[3 : len(match)-1] char, err := strconv.ParseUint(hexpoint, 16, 21) if err != nil { replaceErr = err return "" } switch len(hexpoint) { case 2: switch { // all xtext-specials case 0x01 <= char && char <= 0x09 || 0x11 <= char && char <= 0x19 || char == 0x10 || char == 0x20 || char == 0x2B || char == 0x3D || char == 0x7F: // 2-digit forms case char == 0x5C || 0x80 <= char && char <= 0xFF: // This space is intentionally left blank default: replaceErr = errors.New("illegal hexpoint:" + hexpoint) return "" } // 3-digit forms case 3: switch { case 0x100 <= char && char <= 0xFFF: // This space is intentionally left blank default: replaceErr = errors.New("illegal hexpoint:" + hexpoint) return "" } // 4-digit forms excluding surrogate case 4: switch { case 0x1000 <= char && char <= 0xD7FF: case 0xE000 <= char && char <= 0xFFFF: // This space is intentionally left blank default: replaceErr = errors.New("illegal hexpoint:" + hexpoint) return "" } // 5-digit forms case 5: switch { case 0x1_0000 <= char && char <= 0xF_FFFF: // This space is intentionally left blank default: replaceErr = errors.New("illegal hexpoint:" + hexpoint) return "" } // 6-digit forms case 6: switch { case 0x10_0000 <= char && char <= 0x10_FFFF: // This space is intentionally left blank default: replaceErr = errors.New("illegal hexpoint:" + hexpoint) return "" } // the other invalid forms default: replaceErr = errors.New("illegal hexpoint:" + hexpoint) return "" } return string(rune(char)) }) if replaceErr != nil { return "", replaceErr } return decoded, nil } func decodeTypedAddress(val string) (DSNAddressType, string, error) { tv := strings.SplitN(val, ";", 2) if len(tv) != 2 || tv[0] == "" || tv[1] == "" { return "", "", errors.New("bad address") } aType, aAddr := strings.ToUpper(tv[0]), tv[1] var err error switch DSNAddressType(aType) { case DSNAddressTypeRFC822: aAddr, err = decodeXtext(aAddr) if err == nil && !isPrintableASCII(aAddr) { err = errors.New("illegal address:" + aAddr) } case DSNAddressTypeUTF8: aAddr, err = decodeUTF8AddrXtext(aAddr) default: err = errors.New("unknown address type:" + aType) } if err != nil { return "", "", err } return DSNAddressType(aType), aAddr, nil } func encodeXtext(raw string) string { var out strings.Builder out.Grow(len(raw)) for _, ch := range raw { switch { case ch >= '!' && ch <= '~' && ch != '+' && ch != '=': // printable non-space US-ASCII except '+' and '=' out.WriteRune(ch) default: out.WriteRune('+') out.WriteString(strings.ToUpper(strconv.FormatInt(int64(ch), 16))) } } return out.String() } // Encodes raw string to the utf-8-addr-xtext form in RFC 6533. func encodeUTF8AddrXtext(raw string) string { var out strings.Builder out.Grow(len(raw)) for _, ch := range raw { switch { case ch >= '!' && ch <= '~' && ch != '+' && ch != '=': // printable non-space US-ASCII except '+' and '=' out.WriteRune(ch) default: out.WriteRune('\\') out.WriteRune('x') out.WriteRune('{') out.WriteString(strings.ToUpper(strconv.FormatInt(int64(ch), 16))) out.WriteRune('}') } } return out.String() } // Encodes raw string to the utf-8-addr-unitext form in RFC 6533. func encodeUTF8AddrUnitext(raw string) string { var out strings.Builder out.Grow(len(raw)) for _, ch := range raw { switch { case ch >= '!' && ch <= '~' && ch != '+' && ch != '=': // printable non-space US-ASCII except '+' and '=' out.WriteRune(ch) case ch <= '\x7F': // other ASCII: CTLs, space and specials out.WriteRune('\\') out.WriteRune('x') out.WriteRune('{') out.WriteString(strings.ToUpper(strconv.FormatInt(int64(ch), 16))) out.WriteRune('}') default: // UTF-8 non-ASCII out.WriteRune(ch) } } return out.String() } func isPrintableASCII(val string) bool { for _, ch := range val { if ch < ' ' || '~' < ch { return false } } return true } // 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 } arg, ok := cutPrefixFold(arg, "TO:") if !ok { c.writeResponse(501, EnhancedCode{5, 5, 2}, "Was expecting RCPT arg syntax of TO:
") return } p := parser{s: strings.TrimSpace(arg)} recipient, err := p.parsePath() if err != nil { c.writeResponse(501, EnhancedCode{5, 5, 2}, "Was expecting RCPT arg syntax of TO:
") return } if c.server.MaxRecipients > 0 && len(c.recipients) >= c.server.MaxRecipients { c.writeResponse(452, EnhancedCode{4, 5, 3}, fmt.Sprintf("Maximum limit of %v recipients reached", c.server.MaxRecipients)) return } args, err := parseArgs(p.s) if err != nil { c.writeResponse(501, EnhancedCode{5, 5, 4}, "Unable to parse RCPT ESMTP parameters") return } opts := &RcptOptions{} for key, value := range args { switch key { case "NOTIFY": if !c.server.EnableDSN { c.writeResponse(504, EnhancedCode{5, 5, 4}, "NOTIFY is not implemented") return } notify := []DSNNotify{} for _, val := range strings.Split(value, ",") { notify = append(notify, DSNNotify(strings.ToUpper(val))) } if err := checkNotifySet(notify); err != nil { c.writeResponse(501, EnhancedCode{5, 5, 4}, "Malformed NOTIFY parameter value") return } opts.Notify = notify case "ORCPT": if !c.server.EnableDSN { c.writeResponse(504, EnhancedCode{5, 5, 4}, "ORCPT is not implemented") return } aType, aAddr, err := decodeTypedAddress(value) if err != nil || aAddr == "" { c.writeResponse(501, EnhancedCode{5, 5, 4}, "Malformed ORCPT parameter value") return } opts.OriginalRecipientType = aType opts.OriginalRecipient = aAddr default: c.writeResponse(500, EnhancedCode{5, 5, 4}, "Unknown RCPT TO argument") return } } if err := c.Session().Rcpt(recipient, opts); err != nil { c.writeError(451, EnhancedCode{4, 0, 0}, err) 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 checkNotifySet(values []DSNNotify) error { if len(values) == 0 { return errors.New("Malformed NOTIFY parameter value") } seen := map[DSNNotify]struct{}{} for _, val := range values { switch val { case DSNNotifyNever, DSNNotifyDelayed, DSNNotifyFailure, DSNNotifySuccess: if _, ok := seen[val]; ok { return errors.New("Malformed NOTIFY parameter value") } default: return errors.New("Malformed NOTIFY parameter value") } seen[val] = struct{}{} } if _, ok := seen[DSNNotifyNever]; ok && len(seen) > 1 { return errors.New("Malformed NOTIFY parameter value") } return nil } 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 !c.authAllowed() { 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 = decodeSASLResponse(parts[1]) if err != nil { c.writeResponse(454, EnhancedCode{4, 7, 0}, "Invalid base64 data") return } } sasl, err := c.auth(mechanism) if err != nil { c.writeError(454, EnhancedCode{4, 7, 0}, err) return } response := ir for { challenge, done, err := sasl.Next(response) if err != nil { c.writeError(454, EnhancedCode{4, 7, 0}, err) 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 = decodeSASLResponse(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 decodeSASLResponse(s string) ([]byte, error) { if s == "=" { return []byte{}, nil } return base64.StdEncoding.DecodeString(s) } func (c *Conn) authMechanisms() []string { if authSession, ok := c.Session().(AuthSession); ok { return authSession.AuthMechanisms() } return nil } func (c *Conn) auth(mech string) (sasl.Server, error) { if authSession, ok := c.Session().(AuthSession); ok { return authSession.Auth(mech) } return nil, ErrAuthUnknownMechanism } 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, NoEnhancedCode, "Go ahead. End your data with .") defer c.reset() if c.server.LMTP { c.handleDataLMTP() return } r := newDataReader(c) code, enhancedCode, msg := dataErrorToStatus(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+int64(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(dataErrorToStatus(err)) if err == errPanic { c.Close() } c.reset() c.lineLimitReader.LineLimit = c.server.MaxLineLength return } c.bytesReceived += int64(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 := dataErrorToStatus(<-c.bdatStatus.status[i]) c.writeResponse(code, enchCode, "<"+rcpt+"> "+msg) } } else { c.writeResponse(dataErrorToStatus(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 := dataErrorToStatus(<-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 dataErrorToStatus(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: " + 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() { protocol := "ESMTP" if c.server.LMTP { protocol = "LMTP" } c.writeResponse(220, NoEnhancedCode, fmt.Sprintf("%v %s Service Ready", c.server.Domain, protocol)) } 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]) } } func (c *Conn) writeError(code int, enhCode EnhancedCode, err error) { if smtpErr, ok := err.(*SMTPError); ok { c.writeResponse(smtpErr.Code, smtpErr.EnhancedCode, smtpErr.Message) } else { c.writeResponse(code, enhCode, err.Error()) } } // 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 } go-smtp-0.21.2/data.go000066400000000000000000000061361461243402000144240ustar00rootroot00000000000000package smtp import ( "bufio" "fmt" "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 { s := fmt.Sprintf("SMTP error %03d", err.Code) if err.Message != "" { s += ": " + err.Message } return s } 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 End-of-Data (.) 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 } if c == '\r' { r.state = stateCR break } r.state = stateData case stateDot: if c == '\r' { r.state = stateDotCR 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 } } b[n] = c n++ } if err == nil && r.state == stateEOF { err = io.EOF } if r.limited { r.n -= int64(n) } return } go-smtp-0.21.2/example_server_test.go000066400000000000000000000040071461243402000175660ustar00rootroot00000000000000package smtp_test import ( "errors" "io" "log" "time" "github.com/emersion/go-sasl" "github.com/emersion/go-smtp" ) // The Backend implements SMTP server methods. type Backend struct{} // NewSession is called after client greeting (EHLO, HELO). func (bkd *Backend) NewSession(c *smtp.Conn) (smtp.Session, error) { return &Session{}, nil } // A Session is returned after successful login. type Session struct{} // AuthMechanisms returns a slice of available auth mechanisms; only PLAIN is // supported in this example. func (s *Session) AuthMechanisms() []string { return []string{sasl.Plain} } // Auth is the handler for supported authenticators. func (s *Session) Auth(mech string) (sasl.Server, error) { return sasl.NewPlainServer(func(identity, username, password string) error { if username != "username" || password != "password" { return errors.New("Invalid username or password") } return nil }), nil } func (s *Session) Mail(from string, opts *smtp.MailOptions) error { log.Println("Mail from:", from) return nil } func (s *Session) Rcpt(to string, opts *smtp.RcptOptions) error { log.Println("Rcpt to:", to) return nil } func (s *Session) Data(r io.Reader) error { if b, err := io.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 } // ExampleServer runs an example SMTP server. // // It can be tested manually with e.g. netcat: // // > netcat -C localhost 1025 // EHLO localhost // AUTH PLAIN // AHVzZXJuYW1lAHBhc3N3b3Jk // MAIL FROM: // RCPT TO: // DATA // Hey <3 // . func ExampleServer() { be := &Backend{} s := smtp.NewServer(be) s.Addr = "localhost:1025" s.Domain = "localhost" s.WriteTimeout = 10 * time.Second s.ReadTimeout = 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) } } go-smtp-0.21.2/example_test.go000066400000000000000000000037621461243402000162070ustar00rootroot00000000000000// Copyright 2013 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_test import ( "fmt" "log" "strings" "github.com/emersion/go-sasl" "github.com/emersion/go-smtp" ) func ExampleDial() { // Connect to the remote SMTP server. c, err := smtp.Dial("mail.example.com:25") if err != nil { log.Fatal(err) } // Set the sender and recipient first if err := c.Mail("sender@example.org", nil); err != nil { log.Fatal(err) } if err := c.Rcpt("recipient@example.net", nil); err != nil { log.Fatal(err) } // Send the email body. wc, err := c.Data() if err != nil { log.Fatal(err) } _, err = fmt.Fprintf(wc, "This is the email body") if err != nil { log.Fatal(err) } err = wc.Close() if err != nil { log.Fatal(err) } // Send the QUIT command and close the connection. err = c.Quit() if err != nil { log.Fatal(err) } } // variables to make ExamplePlainAuth compile, without adding // unnecessary noise there. var ( from = "gopher@example.net" msg = strings.NewReader("dummy message") recipients = []string{"foo@example.com"} ) func ExampleSendMail_plainAuth() { // hostname is used by PlainAuth to validate the TLS certificate. hostname := "mail.example.com" auth := sasl.NewPlainClient("", "user@example.com", "password") err := smtp.SendMail(hostname+":25", auth, from, recipients, msg) if err != nil { log.Fatal(err) } } func ExampleSendMail() { // Set up 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) } } go-smtp-0.21.2/go.mod000066400000000000000000000001641461243402000142650ustar00rootroot00000000000000module github.com/emersion/go-smtp require github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 go 1.13 go-smtp-0.21.2/go.sum000066400000000000000000000003451461243402000143130ustar00rootroot00000000000000github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= go-smtp-0.21.2/lengthlimit_reader.go000066400000000000000000000015001461243402000173430ustar00rootroot00000000000000package 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 } go-smtp-0.21.2/lmtp_server_test.go000066400000000000000000000122641461243402000171130ustar00rootroot00000000000000package smtp_test import ( "bufio" "errors" "io" "net" "strings" "testing" "github.com/emersion/go-smtp" ) func testServerGreetedLMTP(t *testing.T, fn ...serverConfigureFunc) (be *backend, s *smtp.Server, c net.Conn, scanner *bufio.Scanner) { be, s, c, scanner = testServer(t, fn...) scanner.Scan() if scanner.Text() != "220 localhost LMTP Service Ready" { t.Fatal("Invalid greeting:", scanner.Text()) } return } func sendDeliveryCmdsLMTP(t *testing.T, scanner *bufio.Scanner, c io.Writer) { sendLHLO(t, scanner, c) io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() io.WriteString(c, "DATA\r\n") scanner.Scan() io.WriteString(c, "Hey <3\r\n") io.WriteString(c, ".\r\n") } func sendLHLO(t *testing.T, scanner *bufio.Scanner, c io.Writer) { io.WriteString(c, "LHLO localhost\r\n") scanner.Scan() if scanner.Text() != "250-Hello localhost" { t.Fatal("Invalid LHLO response:", scanner.Text()) } for scanner.Scan() { s := scanner.Text() if strings.HasPrefix(s, "250 ") { break } else if !strings.HasPrefix(s, "250-") { t.Fatal("Invalid capability response:", s) } } } func TestServer_LMTP(t *testing.T) { be, s, c, scanner := testServerGreetedLMTP(t, func(s *smtp.Server) { s.LMTP = true be := s.Backend.(*backend) be.implementLMTPData = true be.lmtpStatus = []struct { addr string err error }{ {"root@gchq.gov.uk", errors.New("nah")}, {"root@bnd.bund.de", nil}, } }) defer s.Close() defer c.Close() sendDeliveryCmdsLMTP(t, scanner, c) scanner.Scan() if !strings.HasPrefix(scanner.Text(), "554 5.0.0 ") { t.Fatal("Invalid DATA first response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid DATA second response:", scanner.Text()) } if len(be.messages) != 0 || len(be.anonmsgs) != 1 { t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs) } } func TestServer_LMTP_Early(t *testing.T) { // This test confirms responses are sent as early as possible // e.g. right after SetStatus is called. lmtpStatusSync := make(chan struct{}) be, s, c, scanner := testServerGreetedLMTP(t, func(s *smtp.Server) { s.LMTP = true be := s.Backend.(*backend) be.implementLMTPData = true be.lmtpStatusSync = lmtpStatusSync be.lmtpStatus = []struct { addr string err error }{ {"root@gchq.gov.uk", errors.New("nah")}, {"root@bnd.bund.de", nil}, } }) defer s.Close() defer c.Close() sendDeliveryCmdsLMTP(t, scanner, c) // Test backend sends to sync channel after calling SetStatus. scanner.Scan() if !strings.HasPrefix(scanner.Text(), "554 5.0.0 ") { t.Fatal("Invalid DATA first response:", scanner.Text()) } <-be.lmtpStatusSync scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid DATA second response:", scanner.Text()) } <-be.lmtpStatusSync if len(be.messages) != 0 || len(be.anonmsgs) != 1 { t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs) } } func TestServer_LMTP_Expand(t *testing.T) { // This checks whether handleDataLMTP // correctly expands results if backend doesn't // implement LMTPSession. be, s, c, scanner := testServerGreetedLMTP(t, func(s *smtp.Server) { s.LMTP = true }) defer s.Close() defer c.Close() sendDeliveryCmdsLMTP(t, scanner, c) scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid DATA first response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid DATA second response:", scanner.Text()) } if len(be.messages) != 0 || len(be.anonmsgs) != 1 { t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs) } } func TestServer_LMTP_DuplicatedRcpt(t *testing.T) { be, s, c, scanner := testServerGreetedLMTP(t, func(s *smtp.Server) { s.LMTP = true be := s.Backend.(*backend) be.implementLMTPData = true be.lmtpStatus = []struct { addr string err error }{ {"root@gchq.gov.uk", &smtp.SMTPError{Code: 555}}, {"root@bnd.bund.de", nil}, {"root@gchq.gov.uk", &smtp.SMTPError{Code: 556}}, } }) defer s.Close() defer c.Close() sendLHLO(t, scanner, c) io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() io.WriteString(c, "DATA\r\n") scanner.Scan() io.WriteString(c, "Hey <3\r\n") io.WriteString(c, ".\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "555 5.0.0 ") { t.Fatal("Invalid DATA first response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid DATA second response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "556 5.0.0 ") { t.Fatal("Invalid DATA first response:", scanner.Text()) } if len(be.messages) != 0 || len(be.anonmsgs) != 1 { t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs) } } go-smtp-0.21.2/parse.go000066400000000000000000000107021461243402000146170ustar00rootroot00000000000000package smtp import ( "fmt" "strings" ) // cutPrefixFold is a version of strings.CutPrefix which is case-insensitive. func cutPrefixFold(s, prefix string) (string, bool) { if len(s) < len(prefix) || !strings.EqualFold(s[:len(prefix)], prefix) { return "", false } return s[len(prefix):], true } 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) } return strings.ToUpper(line[0:4]), strings.TrimSpace(line[5:]), 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(s string) (map[string]string, error) { argMap := map[string]string{} for _, arg := range strings.Fields(s) { 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 } // parser parses command arguments defined in RFC 5321 section 4.1.2. type parser struct { s string } func (p *parser) peekByte() (byte, bool) { if len(p.s) == 0 { return 0, false } return p.s[0], true } func (p *parser) readByte() (byte, bool) { ch, ok := p.peekByte() if ok { p.s = p.s[1:] } return ch, ok } func (p *parser) acceptByte(ch byte) bool { got, ok := p.peekByte() if !ok || got != ch { return false } p.readByte() return true } func (p *parser) expectByte(ch byte) error { if !p.acceptByte(ch) { if len(p.s) == 0 { return fmt.Errorf("expected '%v', got EOF", string(ch)) } else { return fmt.Errorf("expected '%v', got '%v'", string(ch), string(p.s[0])) } } return nil } func (p *parser) parseReversePath() (string, error) { if strings.HasPrefix(p.s, "<>") { p.s = strings.TrimPrefix(p.s, "<>") return "", nil } return p.parsePath() } func (p *parser) parsePath() (string, error) { hasBracket := p.acceptByte('<') if p.acceptByte('@') { i := strings.IndexByte(p.s, ':') if i < 0 { return "", fmt.Errorf("malformed a-d-l") } p.s = p.s[i+1:] } mbox, err := p.parseMailbox() if err != nil { return "", fmt.Errorf("in mailbox: %v", err) } if hasBracket { if err := p.expectByte('>'); err != nil { return "", err } } return mbox, nil } func (p *parser) parseMailbox() (string, error) { localPart, err := p.parseLocalPart() if err != nil { return "", fmt.Errorf("in local-part: %v", err) } else if localPart == "" { return "", fmt.Errorf("local-part is empty") } if err := p.expectByte('@'); err != nil { return "", err } var sb strings.Builder sb.WriteString(localPart) sb.WriteByte('@') for { ch, ok := p.peekByte() if !ok { break } if ch == ' ' || ch == '\t' || ch == '>' { break } p.readByte() sb.WriteByte(ch) } if strings.HasSuffix(sb.String(), "@") { return "", fmt.Errorf("domain is empty") } return sb.String(), nil } func (p *parser) parseLocalPart() (string, error) { var sb strings.Builder if p.acceptByte('"') { // quoted-string for { ch, ok := p.readByte() switch ch { case '\\': ch, ok = p.readByte() case '"': return sb.String(), nil } if !ok { return "", fmt.Errorf("malformed quoted-string") } sb.WriteByte(ch) } } else { // dot-string for { ch, ok := p.peekByte() if !ok { return sb.String(), nil } switch ch { case '@': return sb.String(), nil case '(', ')', '<', '>', '[', ']', ':', ';', '\\', ',', '"', ' ', '\t': return "", fmt.Errorf("malformed dot-string") } p.readByte() sb.WriteByte(ch) } } } go-smtp-0.21.2/parse_test.go000066400000000000000000000022301461243402000156530ustar00rootroot00000000000000package smtp import ( "testing" ) func TestParser(t *testing.T) { validReversePaths := []struct { raw, path, after string }{ {"<>", "", ""}, {"", "root@nsa.gov", ""}, {"root@nsa.gov", "root@nsa.gov", ""}, {" AUTH=asdf@example.org", "root@nsa.gov", " AUTH=asdf@example.org"}, {"root@nsa.gov AUTH=asdf@example.org", "root@nsa.gov", " AUTH=asdf@example.org"}, } for _, tc := range validReversePaths { p := parser{tc.raw} path, err := p.parseReversePath() if err != nil { t.Errorf("parser.parseReversePath(%q) = %v", tc.raw, err) } else if path != tc.path { t.Errorf("parser.parseReversePath(%q) = %q, want %q", tc.raw, path, tc.path) } else if p.s != tc.after { t.Errorf("parser.parseReversePath(%q): got after = %q, want %q", tc.raw, p.s, tc.after) } } invalidReversePaths := []string{ "", " ", "asdf", ">", " BODY=8BITMIME SIZE=12345", "a:b:c@example.org", " max { tempDelay = max } s.ErrorLog.Printf("accept error: %s; retrying in %s", err, tempDelay) time.Sleep(tempDelay) continue } return err } s.wg.Add(1) go func() { defer s.wg.Done() err := s.handleConn(newConn(c, s)) if err != nil { s.ErrorLog.Printf("error handling %v: %s", c.RemoteAddr(), 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(421, EnhancedCode{4, 4, 2}, "Idle timeout, bye bye") return nil } c.writeResponse(421, EnhancedCode{4, 4, 0}, "Connection error, sorry") return err } } } func (s *Server) network() string { if s.Network != "" { return s.Network } if s.LMTP { return "unix" } return "tcp" } // 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 := s.network() 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 and LMTP is disabled, ":smtps" is used. func (s *Server) ListenAndServeTLS() error { network := s.network() addr := s.Addr if !s.LMTP && addr == "" { addr = ":smtps" } l, err := tls.Listen(network, 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 ErrServerClosed 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 } // Shutdown gracefully shuts down the server without interrupting any // active connections. Shutdown works by first closing all open // listeners and then waiting indefinitely for connections to return to // idle and then shut down. // If the provided context expires before the shutdown is complete, // Shutdown returns the context's error, otherwise it returns any // error returned from closing the Server's underlying Listener(s). func (s *Server) Shutdown(ctx context.Context) error { select { case <-s.done: return ErrServerClosed 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 } } s.locker.Unlock() connDone := make(chan struct{}) go func() { defer close(connDone) s.wg.Wait() }() select { case <-ctx.Done(): return ctx.Err() case <-connDone: return err } } go-smtp-0.21.2/server_test.go000066400000000000000000001160071461243402000160570ustar00rootroot00000000000000package smtp_test import ( "bufio" "bytes" "context" "errors" "io" "io/ioutil" "log" "net" "strings" "sync" "testing" "github.com/emersion/go-sasl" "github.com/emersion/go-smtp" ) type message struct { From string To []string RcptOpts []*smtp.RcptOptions Data []byte Opts *smtp.MailOptions } type backend struct { authDisabled bool messages []*message anonmsgs []*message implementLMTPData bool lmtpStatus []struct { addr string err error } lmtpStatusSync chan struct{} // Errors returned by Data method. dataErrors chan error // Error that will be returned by Data method. dataErr error // Read N bytes of message before returning dataErr. dataErrOffset int64 panicOnMail bool userErr error } func (be *backend) NewSession(_ *smtp.Conn) (smtp.Session, error) { if be.implementLMTPData { return &lmtpSession{&session{backend: be, anonymous: true}}, nil } return &session{backend: be, anonymous: true}, nil } type lmtpSession struct { *session } type session struct { backend *backend anonymous bool msg *message } var _ smtp.AuthSession = (*session)(nil) func (s *session) AuthMechanisms() []string { if s.backend.authDisabled { return nil } return []string{sasl.Plain} } func (s *session) Auth(mech string) (sasl.Server, error) { if s.backend.authDisabled { return nil, smtp.ErrAuthUnsupported } return sasl.NewPlainServer(func(identity, username, password string) error { if identity != "" && identity != username { return errors.New("Invalid identity") } if username != "username" || password != "password" { return errors.New("Invalid username or password") } s.anonymous = false return nil }), nil } func (s *session) Reset() { s.msg = &message{} } func (s *session) Logout() error { return nil } func (s *session) Mail(from string, opts *smtp.MailOptions) error { if s.backend.userErr != nil { return s.backend.userErr } if s.backend.panicOnMail { panic("Everything is on fire!") } s.Reset() s.msg.From = from s.msg.Opts = opts return nil } func (s *session) Rcpt(to string, opts *smtp.RcptOptions) error { s.msg.To = append(s.msg.To, to) s.msg.RcptOpts = append(s.msg.RcptOpts, opts) return nil } func (s *session) Data(r io.Reader) error { if s.backend.dataErr != nil { if s.backend.dataErrOffset != 0 { io.CopyN(ioutil.Discard, r, s.backend.dataErrOffset) } err := s.backend.dataErr if s.backend.dataErrors != nil { s.backend.dataErrors <- err } return err } if b, err := ioutil.ReadAll(r); err != nil { if s.backend.dataErrors != nil { s.backend.dataErrors <- err } return err } else { s.msg.Data = b if s.anonymous { s.backend.anonmsgs = append(s.backend.anonmsgs, s.msg) } else { s.backend.messages = append(s.backend.messages, s.msg) } if s.backend.dataErrors != nil { s.backend.dataErrors <- nil } } return nil } func (s *session) LMTPData(r io.Reader, collector smtp.StatusCollector) error { if err := s.Data(r); err != nil { return err } for _, val := range s.backend.lmtpStatus { collector.SetStatus(val.addr, val.err) if s.backend.lmtpStatusSync != nil { s.backend.lmtpStatusSync <- struct{}{} } } return nil } type failingListener struct { c chan error closed bool mu sync.Mutex } func newFailingListener() *failingListener { return &failingListener{c: make(chan error)} } func (l *failingListener) Send(err error) { l.mu.Lock() defer l.mu.Unlock() if !l.closed { l.c <- err } } func (l *failingListener) Accept() (net.Conn, error) { return nil, <-l.c } func (l *failingListener) Close() error { l.mu.Lock() defer l.mu.Unlock() if !l.closed { close(l.c) l.closed = true } return nil } func (l *failingListener) Addr() net.Addr { return &net.TCPAddr{ IP: net.ParseIP("127.0.0.1"), Port: 12345, } } type mockError struct { msg string temporary bool } func newMockError(msg string, temporary bool) *mockError { return &mockError{ msg: msg, temporary: temporary, } } func (m *mockError) Error() string { return m.msg } func (m *mockError) String() string { return m.msg } func (m *mockError) Timeout() bool { return false } func (m *mockError) Temporary() bool { return m.temporary } type serverConfigureFunc func(*smtp.Server) var ( authDisabled = func(s *smtp.Server) { s.Backend.(*backend).authDisabled = true } ) func testServer(t *testing.T, fn ...serverConfigureFunc) (be *backend, s *smtp.Server, c net.Conn, scanner *bufio.Scanner) { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } be = new(backend) s = smtp.NewServer(be) s.Domain = "localhost" s.AllowInsecureAuth = true for _, f := range fn { f(s) } go s.Serve(l) c, err = net.Dial("tcp", l.Addr().String()) if err != nil { t.Fatal(err) } scanner = bufio.NewScanner(c) return } func testServerGreeted(t *testing.T, fn ...serverConfigureFunc) (be *backend, s *smtp.Server, c net.Conn, scanner *bufio.Scanner) { be, s, c, scanner = testServer(t, fn...) scanner.Scan() if scanner.Text() != "220 localhost ESMTP Service Ready" { t.Fatal("Invalid greeting:", scanner.Text()) } return } func testServerEhlo(t *testing.T, fn ...serverConfigureFunc) (be *backend, s *smtp.Server, c net.Conn, scanner *bufio.Scanner, caps map[string]bool) { be, s, c, scanner = testServerGreeted(t, fn...) io.WriteString(c, "EHLO localhost\r\n") scanner.Scan() if scanner.Text() != "250-Hello localhost" { t.Fatal("Invalid EHLO response:", scanner.Text()) } expectedCaps := []string{"PIPELINING", "8BITMIME"} caps = make(map[string]bool) for scanner.Scan() { s := scanner.Text() if strings.HasPrefix(s, "250 ") { caps[strings.TrimPrefix(s, "250 ")] = true break } else { if !strings.HasPrefix(s, "250-") { t.Fatal("Invalid capability response:", s) } caps[strings.TrimPrefix(s, "250-")] = true } } for _, cap := range expectedCaps { if !caps[cap] { t.Fatal("Missing capability:", cap) } } return } func TestServerAcceptErrorHandling(t *testing.T) { errorLog := bytes.NewBuffer(nil) be := new(backend) s := smtp.NewServer(be) s.Domain = "localhost" s.AllowInsecureAuth = true s.ErrorLog = log.New(errorLog, "", 0) l := newFailingListener() done := make(chan error, 1) go func() { done <- s.Serve(l) l.Close() }() temporaryError := newMockError("temporary mock error", true) l.Send(temporaryError) permanentError := newMockError("permanent mock error", false) l.Send(permanentError) s.Close() serveError := <-done if serveError == nil { t.Fatal("Serve had exited without an expected error") } else if serveError != permanentError { t.Fatal("Unexpected error:", serveError) } if !strings.Contains(errorLog.String(), temporaryError.String()) { t.Fatal("Missing temporary error in log output:", errorLog.String()) } } func TestServer_helo(t *testing.T) { _, s, c, scanner := testServerGreeted(t) defer s.Close() io.WriteString(c, "HELO localhost\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid HELO response:", scanner.Text()) } } func testServerAuthenticated(t *testing.T) (be *backend, s *smtp.Server, c net.Conn, scanner *bufio.Scanner) { be, s, c, scanner, caps := testServerEhlo(t) if _, ok := caps["AUTH PLAIN"]; !ok { t.Fatal("AUTH PLAIN capability is missing when auth is enabled") } io.WriteString(c, "AUTH PLAIN\r\n") scanner.Scan() if scanner.Text() != "334 " { t.Fatal("Invalid AUTH response:", scanner.Text()) } io.WriteString(c, "AHVzZXJuYW1lAHBhc3N3b3Jk\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "235 ") { t.Fatal("Invalid AUTH response:", scanner.Text()) } return } func TestServerAuthTwice(t *testing.T) { _, _, c, scanner, caps := testServerEhlo(t) if _, ok := caps["AUTH PLAIN"]; !ok { t.Fatal("AUTH PLAIN capability is missing when auth is enabled") } io.WriteString(c, "AUTH PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "235 ") { t.Fatal("Invalid AUTH response:", scanner.Text()) } io.WriteString(c, "AUTH PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "503 ") { t.Fatal("Invalid AUTH response:", scanner.Text()) } io.WriteString(c, "RSET\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid AUTH response:", scanner.Text()) } io.WriteString(c, "AUTH PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "503 ") { t.Fatal("Invalid AUTH response:", scanner.Text()) } } func TestServerCancelSASL(t *testing.T) { _, _, c, scanner, caps := testServerEhlo(t) if _, ok := caps["AUTH PLAIN"]; !ok { t.Fatal("AUTH PLAIN capability is missing when auth is enabled") } io.WriteString(c, "AUTH PLAIN\r\n") scanner.Scan() if scanner.Text() != "334 " { t.Fatal("Invalid AUTH response:", scanner.Text()) } io.WriteString(c, "*\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "501 ") { t.Fatal("Invalid AUTH response:", scanner.Text()) } } func TestServerEmptyFrom1(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() if strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } } func TestServerEmptyFrom2(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM:<>\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } } func TestServerPanicRecover(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() s.Backend.(*backend).panicOnMail = true // Don't log panic in tests to not confuse people who run 'go test'. s.ErrorLog = log.New(ioutil.Discard, "", 0) io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "421 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } } func TestServerSMTPUTF8(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) s.EnableSMTPUTF8 = true defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM: SMTPUTF8\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } } func TestServerSMTPUTF8_Disabled(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM: SMTPUTF8\r\n") scanner.Scan() if strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } } func TestServer8BITMIME(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM: BODY=8bitMIME\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } } func TestServer_BODYInvalidValue(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM: BODY=RABIIT\r\n") scanner.Scan() if strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } } func TestServerUnknownArg(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM: RABIIT\r\n") scanner.Scan() if strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } } func TestServerBadSize(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM: SIZE=rabbit\r\n") scanner.Scan() if strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } } func TestServerTooBig(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() s.MaxMessageBytes = 4294967294 io.WriteString(c, "MAIL FROM: SIZE=4294967295\r\n") scanner.Scan() if strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } } func TestServerEmptyTo(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() if strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid RCPT response:", scanner.Text()) } } func TestServer(t *testing.T) { be, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid RCPT response:", scanner.Text()) } io.WriteString(c, "DATA\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "354 ") { t.Fatal("Invalid DATA response:", scanner.Text()) } io.WriteString(c, "From: root@nsa.gov\r\n") io.WriteString(c, "\r\n") io.WriteString(c, "Hey\r <3\r\n") io.WriteString(c, "..this dot is fine\r\n") io.WriteString(c, ".\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid DATA response:", scanner.Text()) } if len(be.messages) != 1 || len(be.anonmsgs) != 0 { t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs) } msg := be.messages[0] if msg.From != "root@nsa.gov" { t.Fatal("Invalid mail sender:", msg.From) } if len(msg.To) != 1 || msg.To[0] != "root@gchq.gov.uk" { t.Fatal("Invalid mail recipients:", msg.To) } if string(msg.Data) != "From: root@nsa.gov\r\n\r\nHey\r <3\r\n.this dot is fine\r\n" { t.Fatal("Invalid mail data:", string(msg.Data)) } } func TestServer_LFDotLF(t *testing.T) { be, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid RCPT response:", scanner.Text()) } io.WriteString(c, "DATA\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "354 ") { t.Fatal("Invalid DATA response:", scanner.Text()) } io.WriteString(c, "From: root@nsa.gov\r\n") io.WriteString(c, "\r\n") io.WriteString(c, "hey\r\n") io.WriteString(c, "\n.\n") io.WriteString(c, "this is going to break your server\r\n") io.WriteString(c, ".\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid DATA response:", scanner.Text()) } if len(be.messages) != 1 || len(be.anonmsgs) != 0 { t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs) } msg := be.messages[0] if string(msg.Data) != "From: root@nsa.gov\r\n\r\nhey\r\n\n.\nthis is going to break your server\r\n" { t.Fatal("Invalid mail data:", string(msg.Data)) } } func TestServer_EmptyMessage(t *testing.T) { be, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid RCPT response:", scanner.Text()) } io.WriteString(c, "DATA\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "354 ") { t.Fatal("Invalid DATA response:", scanner.Text()) } io.WriteString(c, "\r\n\r\n.\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid DATA response:", scanner.Text()) } if len(be.messages) != 1 || len(be.anonmsgs) != 0 { t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs) } msg := be.messages[0] if string(msg.Data) != "\r\n\r\n" { t.Fatal("Invalid mail data:", string(msg.Data), msg.Data) } } func TestServer_authDisabled(t *testing.T) { _, s, c, scanner, caps := testServerEhlo(t, authDisabled) defer s.Close() defer c.Close() if _, ok := caps["AUTH PLAIN"]; ok { t.Fatal("AUTH PLAIN capability is present when auth is disabled") } io.WriteString(c, "AUTH PLAIN\r\n") scanner.Scan() if scanner.Text() != "502 5.7.0 Authentication not supported" { t.Fatal("Invalid AUTH response with auth disabled:", scanner.Text()) } } func TestServer_otherCommands(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) defer s.Close() io.WriteString(c, "HELP\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "502 ") { t.Fatal("Invalid HELP response:", scanner.Text()) } io.WriteString(c, "VRFY\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "252 ") { t.Fatal("Invalid VRFY response:", scanner.Text()) } io.WriteString(c, "NOOP\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid NOOP response:", scanner.Text()) } io.WriteString(c, "RSET\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid RSET response:", scanner.Text()) } io.WriteString(c, "QUIT\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "221 ") { t.Fatal("Invalid QUIT response:", scanner.Text()) } } func TestServer_tooManyInvalidCommands(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) defer s.Close() // Let's assume XXXX is a non-existing command for i := 0; i < 4; i++ { io.WriteString(c, "XXXX\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "500 ") { t.Fatal("Invalid invalid command response:", scanner.Text()) } } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "500 ") { t.Fatal("Invalid invalid command response:", scanner.Text()) } } func TestServer_tooLongMessage(t *testing.T) { be, s, c, scanner := testServerAuthenticated(t) defer s.Close() s.MaxMessageBytes = 50 io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() io.WriteString(c, "DATA\r\n") scanner.Scan() io.WriteString(c, "This is a very long message.\r\n") io.WriteString(c, "Much longer than you can possibly imagine.\r\n") io.WriteString(c, "And much longer than the server's MaxMessageBytes.\r\n") io.WriteString(c, ".\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "552 ") { t.Fatal("Invalid DATA response, expected an error but got:", scanner.Text()) } if len(be.messages) != 0 || len(be.anonmsgs) != 0 { t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs) } } // See https://www.postfix.org/smtp-smuggling.html func TestServer_smtpSmuggling(t *testing.T) { cases := []struct { name string lines []string expected string }{ { name: ".", lines: []string{ "This is a message with an SMTP smuggling dot:\r\n", ".\n", "Final dot comes after.\r\n", ".\r\n", }, expected: "This is a message with an SMTP smuggling dot:\r\n\nFinal dot comes after.\r\n", }, { name: ".", lines: []string{ "This is a message with an SMTP smuggling dot:\n", // not a line on its own ".\r\n", "Final dot comes after.\r\n", ".\r\n", }, expected: "This is a message with an SMTP smuggling dot:\n.\r\nFinal dot comes after.\r\n", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { be, s, c, scanner := testServerAuthenticated(t) defer s.Close() io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() io.WriteString(c, "DATA\r\n") scanner.Scan() for _, line := range tc.lines { io.WriteString(c, line) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid DATA response, expected an error but got:", scanner.Text()) } if len(be.messages) != 1 { t.Fatal("Invalid number of sent messages:", len(be.messages)) } msg := be.messages[0] if string(msg.Data) != tc.expected { t.Fatalf("Invalid mail data: %q", string(msg.Data)) } }) } } func TestServer_tooLongLine(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) defer s.Close() io.WriteString(c, "MAIL FROM: "+strings.Repeat("A", 2000)) scanner.Scan() if !strings.HasPrefix(scanner.Text(), "500 ") { t.Fatal("Invalid response, expected an error but got:", scanner.Text()) } } func TestServer_anonymousUserError(t *testing.T) { be, s, c, scanner, _ := testServerEhlo(t) defer s.Close() defer c.Close() be.userErr = smtp.ErrAuthRequired io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() if scanner.Text() != "502 5.7.0 Please authenticate first" { t.Fatal("Backend refused anonymous mail but client was permitted:", scanner.Text()) } } func TestServer_anonymousUserOK(t *testing.T) { be, s, c, scanner, _ := testServerEhlo(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM: root@nsa.gov\r\n") scanner.Scan() io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() io.WriteString(c, "DATA\r\n") scanner.Scan() io.WriteString(c, "Hey <3\r\n") io.WriteString(c, ".\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid DATA response:", scanner.Text()) } if len(be.messages) != 0 || len(be.anonmsgs) != 1 { t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs) } } func TestServer_authParam(t *testing.T) { be, s, c, scanner, _ := testServerEhlo(t) defer s.Close() defer c.Close() // Invalid HEXCHAR io.WriteString(c, "MAIL FROM: root@nsa.gov AUTH=\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "500 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } // Invalid HEXCHAR io.WriteString(c, "MAIL FROM: root@nsa.gov AUTH=\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "500 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } // https://tools.ietf.org/html/rfc4954#section-4 // >servers that advertise support for this // >extension MUST support the AUTH parameter to the MAIL FROM // >command even when the client has not authenticated itself to the // >server. io.WriteString(c, "MAIL FROM: root@nsa.gov AUTH=hey+3Da@example.com\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } // Go on as usual. io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() io.WriteString(c, "DATA\r\n") scanner.Scan() io.WriteString(c, "Hey <3\r\n") io.WriteString(c, ".\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid DATA response:", scanner.Text()) } if len(be.messages) != 0 || len(be.anonmsgs) != 1 { t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs) } if val := be.anonmsgs[0].Opts.Auth; val == nil || *val != "hey=a@example.com" { t.Fatal("Invalid Auth value:", val) } } func TestServer_Chunking(t *testing.T) { be, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid RCPT response:", scanner.Text()) } io.WriteString(c, "BDAT 8\r\n") io.WriteString(c, "Hey <3\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid BDAT response:", scanner.Text()) } io.WriteString(c, "BDAT 8 LAST\r\n") io.WriteString(c, "Hey :3\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid BDAT response:", scanner.Text()) } if len(be.messages) != 1 || len(be.anonmsgs) != 0 { t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs) } msg := be.messages[0] if msg.From != "root@nsa.gov" { t.Fatal("Invalid mail sender:", msg.From) } if len(msg.To) != 1 || msg.To[0] != "root@gchq.gov.uk" { t.Fatal("Invalid mail recipients:", msg.To) } if want := "Hey <3\r\nHey :3\r\n"; string(msg.Data) != want { t.Fatal("Invalid mail data:", string(msg.Data), msg.Data) } } func TestServer_Chunking_LMTP(t *testing.T) { be, s, c, scanner := testServerAuthenticated(t) s.LMTP = true defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid RCPT response:", scanner.Text()) } io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid RCPT response:", scanner.Text()) } io.WriteString(c, "BDAT 8\r\n") io.WriteString(c, "Hey <3\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid BDAT response:", scanner.Text()) } io.WriteString(c, "BDAT 8 LAST\r\n") io.WriteString(c, "Hey :3\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid BDAT response:", scanner.Text()) } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid BDAT response:", scanner.Text()) } if len(be.messages) != 1 || len(be.anonmsgs) != 0 { t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs) } msg := be.messages[0] if msg.From != "root@nsa.gov" { t.Fatal("Invalid mail sender:", msg.From) } if want := "Hey <3\r\nHey :3\r\n"; string(msg.Data) != want { t.Fatal("Invalid mail data:", string(msg.Data), msg.Data) } } func TestServer_Chunking_Reset(t *testing.T) { be, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() be.dataErrors = make(chan error, 10) io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid RCPT response:", scanner.Text()) } io.WriteString(c, "BDAT 8\r\n") io.WriteString(c, "Hey <3\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid BDAT response:", scanner.Text()) } // Client changed its mind... Note, in this case Data method error is discarded and not returned to the cilent. io.WriteString(c, "RSET\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid BDAT response:", scanner.Text()) } if err := <-be.dataErrors; err != smtp.ErrDataReset { t.Fatal("Backend received a different error:", err) } } func TestServer_Chunking_ClosedInTheMiddle(t *testing.T) { be, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() be.dataErrors = make(chan error, 10) io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid RCPT response:", scanner.Text()) } io.WriteString(c, "BDAT 8\r\n") io.WriteString(c, "Hey <") // Bye! c.Close() if err := <-be.dataErrors; err != smtp.ErrDataReset { t.Fatal("Backend received a different error:", err) } } func TestServer_Chunking_EarlyError(t *testing.T) { be, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() be.dataErr = &smtp.SMTPError{ Code: 555, EnhancedCode: smtp.EnhancedCode{5, 0, 0}, Message: "I failed", } io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid RCPT response:", scanner.Text()) } io.WriteString(c, "BDAT 8\r\n") io.WriteString(c, "Hey <3\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "555 5.0.0 I failed") { t.Fatal("Invalid BDAT response:", scanner.Text()) } } func TestServer_Chunking_EarlyErrorDuringChunk(t *testing.T) { be, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() be.dataErr = &smtp.SMTPError{ Code: 555, EnhancedCode: smtp.EnhancedCode{5, 0, 0}, Message: "I failed", } be.dataErrOffset = 5 io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid RCPT response:", scanner.Text()) } io.WriteString(c, "BDAT 8\r\n") io.WriteString(c, "Hey <3\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "555 5.0.0 I failed") { t.Fatal("Invalid BDAT response:", scanner.Text()) } // See that command stream state is not corrupted e.g. server is still not // waiting for remaining chunk octets. io.WriteString(c, "NOOP\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid RCPT response:", scanner.Text()) } } func TestServer_Chunking_tooLongMessage(t *testing.T) { be, s, c, scanner := testServerAuthenticated(t) defer s.Close() s.MaxMessageBytes = 50 io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() io.WriteString(c, "BDAT 30\r\n") io.WriteString(c, "This is a very long message.\r\n") scanner.Scan() io.WriteString(c, "BDAT 96 LAST\r\n") io.WriteString(c, "Much longer than you can possibly imagine.\r\n") io.WriteString(c, "And much longer than the server's MaxMessageBytes.\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "552 ") { t.Fatal("Invalid DATA response, expected an error but got:", scanner.Text()) } if len(be.messages) != 0 || len(be.anonmsgs) != 0 { t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs) } } func TestServer_Chunking_Binarymime(t *testing.T) { be, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() s.EnableBINARYMIME = true io.WriteString(c, "MAIL FROM: BODY=BINARYMIME\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid RCPT response:", scanner.Text()) } io.WriteString(c, "BDAT 8\r\n") io.WriteString(c, "Hey <3\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid BDAT response:", scanner.Text()) } io.WriteString(c, "BDAT 8 LAST\r\n") io.WriteString(c, "Hey :3\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid BDAT response:", scanner.Text()) } if len(be.messages) != 1 || len(be.anonmsgs) != 0 { t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs) } msg := be.messages[0] if msg.From != "root@nsa.gov" { t.Fatal("Invalid mail sender:", msg.From) } if len(msg.To) != 1 || msg.To[0] != "root@gchq.gov.uk" { t.Fatal("Invalid mail recipients:", msg.To) } if want := "Hey <3\r\nHey :3\r\n"; string(msg.Data) != want { t.Fatal("Invalid mail data:", string(msg.Data), msg.Data) } } func TestServer_TooLongCommand(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM:<"+strings.Repeat("a", s.MaxLineLength)+">\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "500 5.4.0 ") { t.Fatal("Invalid too long MAIL response:", scanner.Text()) } } func TestServerShutdown(t *testing.T) { _, s, c, _ := testServerGreeted(t) ctx := context.Background() errChan := make(chan error) go func() { defer close(errChan) errChan <- s.Shutdown(ctx) errChan <- s.Shutdown(ctx) }() select { case err := <-errChan: t.Fatal("Expected no err because conn is open:", err) default: c.Close() } errOne := <-errChan if errOne != nil { t.Fatal("Expected err to be nil:", errOne) } errTwo := <-errChan if errTwo != smtp.ErrServerClosed { t.Fatal("Expected err to be ErrServerClosed:", errTwo) } } const ( dsnEnvelopeID = "e=mc2" dsnEmailRFC822 = "e=mc2@example.com" dsnEmailUTF8 = "e=mc2@ドメイン名例.jp" ) func TestServerDSN(t *testing.T) { be, s, c, scanner, caps := testServerEhlo(t, func(s *smtp.Server) { s.EnableDSN = true }) defer s.Close() defer c.Close() if _, ok := caps["DSN"]; !ok { t.Fatal("Missing capability: DSN") } io.WriteString(c, "MAIL FROM: envID=e+3Dmc2 Ret=hdrs\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } io.WriteString(c, "RCPT TO: ORcpt=Rfc822;e+3Dmc2@example.com Notify=Never\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid RCPT response:", scanner.Text()) } io.WriteString(c, "RCPT TO: orcpt=Utf-8;e\\x{3D}mc2@\\x{30C9}\\x{30E1}\\x{30A4}\\x{30F3}\\x{540D}\\x{4F8B}.jp notify=failure,delay\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid RCPT response:", scanner.Text()) } // go on as usual io.WriteString(c, "DATA\r\n") scanner.Scan() io.WriteString(c, "Hey <3\r\n") io.WriteString(c, ".\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid DATA response:", scanner.Text()) } if len(be.messages) != 0 || len(be.anonmsgs) != 1 { t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs) } if val := be.anonmsgs[0].Opts.Return; val != smtp.DSNReturnHeaders { t.Fatal("Invalid RET parameter value:", val) } if val := be.anonmsgs[0].Opts.EnvelopeID; val != dsnEnvelopeID { t.Fatal("Invalid ENVID parameter value:", val) } to := be.anonmsgs[0].To if to == nil || len(to) != 2 { t.Fatal("Invalid number of recipients:", to) } if val := to[0]; val != dsnEmailRFC822 { t.Fatal("Invalid recipient:", val) } if val := to[1]; val != dsnEmailRFC822 { t.Fatal("Invalid recipient:", val) } opts := be.anonmsgs[0].RcptOpts if opts == nil || len(opts) != 2 { t.Fatal("Invalid number of recipients:", opts) } if val := opts[0].Notify; val == nil || len(val) != 1 || val[0] != smtp.DSNNotifyNever { t.Fatal("Invalid NOTIFY parameter value:", val) } if val := opts[0].OriginalRecipientType; val != smtp.DSNAddressTypeRFC822 { t.Fatal("Invalid ORCPT address type:", val) } if val := opts[0].OriginalRecipient; val != dsnEmailRFC822 { t.Fatal("Invalid ORCPT address:", val) } if val := opts[1].Notify; val == nil || len(val) != 2 || val[0] != smtp.DSNNotifyFailure || val[1] != smtp.DSNNotifyDelayed { t.Fatal("Invalid NOTIFY parameter value:", val) } if val := opts[1].OriginalRecipientType; val != smtp.DSNAddressTypeUTF8 { t.Fatal("Invalid ORCPT address type:", val) } if val := opts[1].OriginalRecipient; val != dsnEmailUTF8 { t.Fatal("Invalid ORCPT address:", val) } } func TestServerDSNwithSMTPUTF8(t *testing.T) { be, s, c, scanner, caps := testServerEhlo(t, func(s *smtp.Server) { s.EnableDSN = true s.EnableSMTPUTF8 = true }) defer s.Close() defer c.Close() for _, cap := range []string{"DSN", "SMTPUTF8"} { if _, ok := caps[cap]; !ok { t.Fatal("Missing capability:", cap) } } io.WriteString(c, "MAIL FROM: ENVID=e+3Dmc2 RET=HDRS\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } io.WriteString(c, "RCPT TO: ORCPT=RFC822;e+3Dmc2@example.com NOTIFY=NEVER\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid RCPT response:", scanner.Text()) } io.WriteString(c, "RCPT TO: ORCPT=UTF-8;e\\x{3D}mc2@\\x{30C9}\\x{30E1}\\x{30A4}\\x{30F3}\\x{540D}\\x{4F8B}.jp NOTIFY=FAILURE,DELAY\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid RCPT response:", scanner.Text()) } io.WriteString(c, "RCPT TO: ORCPT=utf-8;e\\x{3D}mc2@ドメイン名例.jp NOTIFY=SUCCESS\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid RCPT response:", scanner.Text()) } // go on as usual io.WriteString(c, "DATA\r\n") scanner.Scan() io.WriteString(c, "Hey <3\r\n") io.WriteString(c, ".\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid DATA response:", scanner.Text()) } if len(be.messages) != 0 || len(be.anonmsgs) != 1 { t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs) } if val := be.anonmsgs[0].Opts.Return; val != smtp.DSNReturnHeaders { t.Fatal("Invalid RET parameter value:", val) } if val := be.anonmsgs[0].Opts.EnvelopeID; val != dsnEnvelopeID { t.Fatal("Invalid ENVID parameter value:", val) } to := be.anonmsgs[0].To if to == nil || len(to) != 3 { t.Fatal("Invalid number of recipients:", to) } if val := to[0]; val != dsnEmailRFC822 { t.Fatal("Invalid recipient:", val) } // Non-ASCII UTF-8 is allowed in TO parameter value if val := to[1]; val != dsnEmailUTF8 { t.Fatal("Invalid recipient:", val) } if val := to[2]; val != dsnEmailUTF8 { t.Fatal("Invalid recipient:", val) } opts := be.anonmsgs[0].RcptOpts if opts == nil || len(opts) != 3 { t.Fatal("Invalid number of recipients:", opts) } if val := opts[0].Notify; val == nil || len(val) != 1 || val[0] != smtp.DSNNotifyNever { t.Fatal("Invalid NOTIFY parameter value:", val) } if val := opts[0].OriginalRecipientType; val != smtp.DSNAddressTypeRFC822 { t.Fatal("Invalid ORCPT address type:", val) } if val := opts[0].OriginalRecipient; val != dsnEmailRFC822 { t.Fatal("Invalid ORCPT address:", val) } if val := opts[1].Notify; val == nil || len(val) != 2 || val[0] != smtp.DSNNotifyFailure || val[1] != smtp.DSNNotifyDelayed { t.Fatal("Invalid NOTIFY parameter value:", val) } if val := opts[1].OriginalRecipientType; val != smtp.DSNAddressTypeUTF8 { t.Fatal("Invalid ORCPT address type:", val) } if val := opts[1].OriginalRecipient; val != dsnEmailUTF8 { t.Fatal("Invalid ORCPT address:", val) } // utf-8-addr-unitext form is allowed in ORCPT parameter value if val := opts[2].Notify; val == nil || len(val) != 1 || val[0] != smtp.DSNNotifySuccess { t.Fatal("Invalid NOTIFY parameter value:", val) } if val := opts[2].OriginalRecipientType; val != smtp.DSNAddressTypeUTF8 { t.Fatal("Invalid ORCPT address type:", val) } if val := opts[2].OriginalRecipient; val != dsnEmailUTF8 { t.Fatal("Invalid ORCPT address:", val) } } go-smtp-0.21.2/smtp.go000066400000000000000000000043631461243402000144760ustar00rootroot00000000000000// 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) // - DSN (RFC 3461, RFC 6533) // // LMTP (RFC 2033) is also supported. // // Additional extensions may be handled by other packages. package smtp type BodyType string const ( Body7Bit BodyType = "7BIT" Body8BitMIME BodyType = "8BITMIME" BodyBinaryMIME BodyType = "BINARYMIME" ) type DSNReturn string const ( DSNReturnFull DSNReturn = "FULL" DSNReturnHeaders DSNReturn = "HDRS" ) // MailOptions contains parameters for 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 int64 // 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 // Value of RET= argument, FULL or HDRS. Return DSNReturn // Envelope identifier set by the client. EnvelopeID string // 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 } type DSNNotify string const ( DSNNotifyNever DSNNotify = "NEVER" DSNNotifyDelayed DSNNotify = "DELAY" DSNNotifyFailure DSNNotify = "FAILURE" DSNNotifySuccess DSNNotify = "SUCCESS" ) type DSNAddressType string const ( DSNAddressTypeRFC822 DSNAddressType = "RFC822" DSNAddressTypeUTF8 DSNAddressType = "UTF-8" ) // RcptOptions contains parameters for the RCPT command. type RcptOptions struct { // Value of NOTIFY= argument, NEVER or a combination of either of // DELAY, FAILURE, SUCCESS. Notify []DSNNotify // Original recipient set by client. OriginalRecipientType DSNAddressType OriginalRecipient string }