pax_global_header00006660000000000000000000000064135727067510014527gustar00rootroot0000000000000052 comment=2bb99f0c49444cddd79a9339455e5d73f157ae20 go-smtp-0.12.1/000077500000000000000000000000001357270675100131765ustar00rootroot00000000000000go-smtp-0.12.1/.build.yml000066400000000000000000000006641357270675100151040ustar00rootroot00000000000000image: alpine/edge packages: - go # Required by codecov - bash - findutils sources: - https://github.com/emersion/go-smtp tasks: - build: | cd go-smtp go build -v ./... - test: | cd go-smtp go test -coverprofile=coverage.txt -covermode=atomic ./... - upload-coverage: | cd go-smtp export CODECOV_TOKEN=3f257f71-a128-4834-8f68-2b534e9f4cb1 curl -s https://codecov.io/bash | bash go-smtp-0.12.1/.gitignore000066400000000000000000000004241357270675100151660ustar00rootroot00000000000000# 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.12.1/LICENSE000066400000000000000000000022451357270675100142060ustar00rootroot00000000000000The 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.12.1/README.md000077500000000000000000000062511357270675100144640ustar00rootroot00000000000000# go-smtp [![GoDoc](https://godoc.org/github.com/emersion/go-smtp?status.svg)](https://godoc.org/github.com/emersion/go-smtp) [![builds.sr.ht status](https://builds.sr.ht/~emersion/go-smtp.svg)](https://builds.sr.ht/~emersion/go-smtp?) [![codecov](https://codecov.io/gh/emersion/go-smtp/branch/master/graph/badge.svg)](https://codecov.io/gh/emersion/go-smtp) An ESMTP client and server library written in Go. ## Features * ESMTP client & server implementing [RFC 5321](https://tools.ietf.org/html/rfc5321) * Support for SMTP [AUTH](https://tools.ietf.org/html/rfc4954) and [PIPELINING](https://tools.ietf.org/html/rfc2920) * UTF-8 support for subject and message * [LMTP](https://tools.ietf.org/html/rfc2033) support ## Usage ### Client ```go package main import ( "log" "strings" "github.com/emersion/go-sasl" "github.com/emersion/go-smtp" ) func main() { // 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) } } ``` If you need more control, you can use `Client` instead. ### Server ```go package main import ( "errors" "io" "io/ioutil" "log" "time" "github.com/emersion/go-smtp" ) // The Backend implements SMTP server methods. type Backend struct{} // Login handles a login command with username and password. func (bkd *Backend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) { if username != "username" || password != "password" { return nil, errors.New("Invalid username or password") } return &Session{}, nil } // AnonymousLogin requires clients to authenticate using SMTP AUTH before sending emails func (bkd *Backend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) { return nil, smtp.ErrAuthRequired } // A Session is returned after successful login. type Session struct{} func (s *Session) Mail(from string) error { log.Println("Mail from:", from) return nil } func (s *Session) Rcpt(to string) error { log.Println("Rcpt to:", to) return nil } func (s *Session) Data(r io.Reader) error { if b, err := ioutil.ReadAll(r); err != nil { return err } else { log.Println("Data:", string(b)) } return nil } func (s *Session) Reset() {} func (s *Session) Logout() error { return nil } func main() { be := &Backend{} s := smtp.NewServer(be) s.Addr = ":1025" s.Domain = "localhost" s.ReadTimeout = 10 * time.Second s.WriteTimeout = 10 * time.Second s.MaxMessageBytes = 1024 * 1024 s.MaxRecipients = 50 s.AllowInsecureAuth = true log.Println("Starting server at", s.Addr) if err := s.ListenAndServe(); err != nil { log.Fatal(err) } } ``` You can use the server manually with `telnet`: ``` $ telnet localhost 1025 EHLO localhost AUTH PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk MAIL FROM: RCPT TO: DATA Hey <3 . ``` ## Licence MIT go-smtp-0.12.1/backend.go000066400000000000000000000043011357270675100151120ustar00rootroot00000000000000package smtp import ( "errors" "io" ) var ( ErrAuthRequired = errors.New("Please authenticate first") ErrAuthUnsupported = errors.New("Authentication not supported") ) // A SMTP server backend. type Backend interface { // Authenticate a user. Return smtp.ErrAuthUnsupported if you don't want to // support this. Login(state *ConnectionState, username, password string) (Session, error) // Called if the client attempts to send mail without logging in first. // Return smtp.ErrAuthRequired if you don't want to support this. AnonymousLogin(state *ConnectionState) (Session, error) } // MailOptions contains custom arguments that were // passed as an argument to the MAIL command. type MailOptions struct { // Size of the body. Can be 0 if not specified by client. Size int // TLS is required for the message transmission. // // The message should be rejected if it can't be transmitted // with TLS. RequireTLS bool // The message envelope or message header contains UTF-8-encoded strings. // This flag is set by SMTPUTF8-aware (RFC 6531) client. UTF8 bool } 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) error // Set currently processed message contents and send it. Data(r io.Reader) error } type LMTPSession interface { // LMTPData is the LMTP-specific version of Data method. // It can be optionally implemented by the backend to provide // per-recipient status information when it is used over LMTP // protocol. // // LMTPData implementation sets status information using passed // StatusCollector by calling SetStatus once per each AddRcpt // call, even if AddRcpt was called multiple times with // the same argument. SetStatus must not be called after // LMTPData returns. // // Return value of LMTPData itself is used as a status for // recipients that got no status set before using StatusCollector. LMTPData(r io.Reader, status StatusCollector) error } type StatusCollector interface { SetStatus(rcptTo string, err error) } go-smtp-0.12.1/backendutil/000077500000000000000000000000001357270675100154635ustar00rootroot00000000000000go-smtp-0.12.1/backendutil/backendutil.go000077500000000000000000000001311357270675100202750ustar00rootroot00000000000000// Package backendutil provide utilities to implement SMTP backends. package backendutil go-smtp-0.12.1/backendutil/transform.go000077500000000000000000000033431357270675100200330ustar00rootroot00000000000000package backendutil import ( "io" "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) } // Login implements the smtp.Backend interface. func (be *TransformBackend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) { s, err := be.Backend.Login(state, username, password) if err != nil { return nil, err } return &transformSession{s, be}, nil } // AnonymousLogin implements the smtp.Backend interface. func (be *TransformBackend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) { s, err := be.Backend.AnonymousLogin(state) if err != nil { return nil, err } return &transformSession{s, be}, nil } type transformSession struct { Session smtp.Session be *TransformBackend } func (s *transformSession) Reset() { s.Session.Reset() } 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) 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) } 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.12.1/backendutil/transform_test.go000077500000000000000000000170601357270675100210730ustar00rootroot00000000000000package backendutil_test import ( "bufio" "encoding/base64" "errors" "io" "io/ioutil" "net" "strings" "testing" "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) Login(_ *smtp.ConnectionState, username, password string) (smtp.Session, error) { if be.userErr != nil { return &session{}, be.userErr } if username != "username" || password != "password" { return nil, errors.New("Invalid username or password") } return &session{backend: be}, nil } func (be *backend) AnonymousLogin(_ *smtp.ConnectionState) (smtp.Session, error) { if be.userErr != nil { return &session{}, be.userErr } 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) Mail(from string, opts smtp.MailOptions) error { s.Reset() s.msg.From = from return nil } func (s *session) Rcpt(to string) 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) != "SGV5IDwzCg==" { 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\n" (with actual newline) if string(msg.Data) != "SGV5IDwzCg==" { t.Fatal("Invalid mail data:", string(msg.Data)) } } go-smtp-0.12.1/client.go000066400000000000000000000354731357270675100150170ustar00rootroot00000000000000// 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" "github.com/emersion/go-sasl" ) // A Client represents a client connection to an SMTP server. type Client struct { // Text is the textproto.Conn used by the Client. It is exported to allow for // clients to add extensions. Text *textproto.Conn // keep a reference to the connection so it can be used to create a TLS // connection later conn net.Conn // whether the Client is using TLS tls bool serverName string lmtp bool // map of supported extensions ext map[string]string // supported auth mechanisms auth []string localName string // the name to use in HELO/EHLO/LHLO didHello bool // whether we've said HELO/EHLO/LHLO helloError error // the error from the hello rcptToCount int // number of recipients } // Dial returns a new Client connected to an SMTP server at addr. // The addr must include a port, as in "mail.example.com:smtp". func Dial(addr string) (*Client, error) { conn, err := net.Dial("tcp", addr) if err != nil { return nil, err } host, _, _ := net.SplitHostPort(addr) return NewClient(conn, host) } // DialTLS returns a new Client connected to an SMTP server via TLS at addr. // The addr must include a port, as in "mail.example.com:smtps". func DialTLS(addr string, tlsConfig *tls.Config) (*Client, error) { conn, err := tls.Dial("tcp", addr, tlsConfig) if err != nil { return nil, err } host, _, _ := net.SplitHostPort(addr) return NewClient(conn, host) } // NewClient returns a new Client using an existing connection and host as a // server name to be used when authenticating. func NewClient(conn net.Conn, host string) (*Client, error) { rwc := struct { io.Reader io.Writer io.Closer }{ Reader: lineLimitReader{ R: conn, // Doubled maximum line length per RFC 5321 (Section 4.5.3.1.6) LineLimit: 2000, }, Writer: conn, Closer: conn, } text := textproto.NewConn(rwc) _, _, err := text.ReadResponse(220) if err != nil { text.Close() if protoErr, ok := err.(*textproto.Error); ok { return nil, toSMTPErr(protoErr) } return nil, err } _, isTLS := conn.(*tls.Conn) c := &Client{Text: text, conn: conn, serverName: host, localName: "localhost", tls: isTLS} return c, nil } // NewClientLMTP returns a new LMTP Client (as defined in RFC 2033) using an // existing connector and host as a server name to be used when authenticating. func NewClientLMTP(conn net.Conn, host string) (*Client, error) { c, err := NewClient(conn, host) if err != nil { return nil, err } c.lmtp = true return c, nil } // Close closes the connection. func (c *Client) Close() error { return c.Text.Close() } // hello runs a hello exchange if needed. func (c *Client) hello() error { if !c.didHello { c.didHello = true err := c.ehlo() if err != nil { c.helloError = c.helo() } } return c.helloError } // Hello sends a HELO or EHLO to the server as the given host name. // Calling this method is only necessary if the client needs control // over the host name used. The client will introduce itself as "localhost" // automatically otherwise. If Hello is called, it must be called before // any of the other methods. // // If server returns an error, it will be of type *SMTPError. func (c *Client) Hello(localName string) error { if err := validateLine(localName); err != nil { return err } if c.didHello { return errors.New("smtp: Hello called after other methods") } c.localName = localName return c.hello() } // cmd is a convenience function that sends a command and returns the response // textproto.Error returned by c.Text.ReadResponse is converted into SMTPError. func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) { id, err := c.Text.Cmd(format, args...) if err != nil { return 0, "", err } c.Text.StartResponse(id) defer c.Text.EndResponse(id) code, msg, err := c.Text.ReadResponse(expectCode) if err != nil { if protoErr, ok := err.(*textproto.Error); ok { smtpErr := toSMTPErr(protoErr) return code, smtpErr.Message, smtpErr } return code, msg, err } return code, msg, nil } // helo sends the HELO greeting to the server. It should be used only when the // server does not support ehlo. func (c *Client) helo() error { c.ext = nil _, _, err := c.cmd(250, "HELO %s", c.localName) return err } // ehlo sends the EHLO (extended hello) greeting to the server. It // should be the preferred greeting for servers that support it. func (c *Client) ehlo() error { cmd := "EHLO" if c.lmtp { cmd = "LHLO" } _, msg, err := c.cmd(250, "%s %s", cmd, c.localName) if err != nil { return err } ext := make(map[string]string) extList := strings.Split(msg, "\n") if len(extList) > 1 { extList = extList[1:] for _, line := range extList { args := strings.SplitN(line, " ", 2) if len(args) > 1 { ext[args[0]] = args[1] } else { ext[args[0]] = "" } } } if mechs, ok := ext["AUTH"]; ok { c.auth = strings.Split(mechs, " ") } c.ext = ext return err } // StartTLS sends the STARTTLS command and encrypts all further communication. // Only servers that advertise the STARTTLS extension support this function. // // If server returns an error, it will be of type *SMTPError. func (c *Client) StartTLS(config *tls.Config) error { if err := c.hello(); err != nil { return err } _, _, err := c.cmd(220, "STARTTLS") if err != nil { return err } if config == nil { config = &tls.Config{} } if config.ServerName == "" { // Make a copy to avoid polluting argument config = config.Clone() config.ServerName = c.serverName } if testHookStartTLS != nil { testHookStartTLS(config) } c.conn = tls.Client(c.conn, config) c.Text = textproto.NewConn(c.conn) c.tls = true return c.ehlo() } // TLSConnectionState returns the client's TLS connection state. // The return values are their zero values if StartTLS did // not succeed. func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) { tc, ok := c.conn.(*tls.Conn) if !ok { return } return tc.ConnectionState(), true } // Verify checks the validity of an email address on the server. // If Verify returns nil, the address is valid. A non-nil return // does not necessarily indicate an invalid address. Many servers // will not verify addresses for security reasons. // // If server returns an error, it will be of type *SMTPError. func (c *Client) Verify(addr string) error { if err := validateLine(addr); err != nil { return err } if err := c.hello(); err != nil { return err } _, _, err := c.cmd(250, "VRFY %s", addr) return err } // Auth authenticates a client using the provided authentication mechanism. // Only servers that advertise the AUTH extension support this function. // // If server returns an error, it will be of type *SMTPError. func (c *Client) Auth(a sasl.Client) error { if err := c.hello(); err != nil { return err } encoding := base64.StdEncoding mech, resp, err := a.Start() if err != nil { return err } resp64 := make([]byte, encoding.EncodedLen(len(resp))) encoding.Encode(resp64, resp) code, msg64, err := c.cmd(0, strings.TrimSpace(fmt.Sprintf("AUTH %s %s", mech, resp64))) for err == nil { var msg []byte switch code { case 334: msg, err = encoding.DecodeString(msg64) case 235: // the last message isn't base64 because it isn't a challenge msg = []byte(msg64) default: err = toSMTPErr(&textproto.Error{Code: code, Msg: msg64}) } if err == nil { if code == 334 { resp, err = a.Next(msg) } else { resp = nil } } if err != nil { // abort the AUTH c.cmd(501, "*") break } if resp == nil { break } resp64 = make([]byte, encoding.EncodedLen(len(resp))) encoding.Encode(resp64, resp) code, msg64, err = c.cmd(0, string(resp64)) } return err } // Mail issues a MAIL command to the server using the provided email address. // If the server supports the 8BITMIME extension, Mail adds the BODY=8BITMIME // parameter. // This initiates a mail transaction and is followed by one or more Rcpt calls. // // If opts is not nil, MAIL arguments provided in the structure will be added // to the command. Handling of unsupported options depends on the extension. // // If server returns an error, it will be of type *SMTPError. func (c *Client) Mail(from string, opts *MailOptions) error { if err := validateLine(from); err != nil { return err } if err := c.hello(); err != nil { return err } cmdStr := "MAIL FROM:<%s>" if _, ok := c.ext["8BITMIME"]; ok { cmdStr += " BODY=8BITMIME" } if _, ok := c.ext["SIZE"]; ok && opts != nil && opts.Size != 0 { cmdStr += " SIZE=" + strconv.Itoa(opts.Size) } if opts != nil && opts.RequireTLS { if _, ok := c.ext["REQUIRETLS"]; ok { cmdStr += " REQUIRETLS" } else { return errors.New("smtp: server does not support REQUIRETLS") } } if opts != nil && opts.UTF8 { if _, ok := c.ext["SMTPUTF8"]; ok { cmdStr += " SMTPUTF8" } else { return errors.New("smtp: server does not support SMTPUTF8") } } _, _, err := c.cmd(250, cmdStr, from) return err } // Rcpt issues a RCPT command to the server using the provided email address. // A call to Rcpt must be preceded by a call to Mail and may be followed by // a Data call or another Rcpt call. // // If server returns an error, it will be of type *SMTPError. func (c *Client) Rcpt(to string) error { if err := validateLine(to); err != nil { return err } if _, _, err := c.cmd(25, "RCPT TO:<%s>", to); err != nil { return err } c.rcptToCount++ return nil } type dataCloser struct { c *Client io.WriteCloser } func (d *dataCloser) Close() error { d.WriteCloser.Close() if d.c.lmtp { for d.c.rcptToCount > 0 { if _, _, err := d.c.Text.ReadResponse(250); err != nil { if protoErr, ok := err.(*textproto.Error); ok { return toSMTPErr(protoErr) } return err } d.c.rcptToCount-- } return nil } else { _, _, err := d.c.Text.ReadResponse(250) if err != nil { if protoErr, ok := err.(*textproto.Error); ok { return toSMTPErr(protoErr) } return err } 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.Text.DotWriter()}, nil } var testHookStartTLS func(*tls.Config) // nil, except for tests // SendMail connects to the server at addr, switches to TLS if // possible, authenticates with the optional mechanism a if possible, // and then sends an email from address from, to addresses to, with // message r. // The addr must include a port, as in "mail.example.com:smtp". // // 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. // // The SendMail function and the net/smtp package are low-level // mechanisms and provide no support for DKIM signing, MIME // attachments (see the mime/multipart package), or other mail // functionality. Higher-level packages exist outside of the standard // library. func SendMail(addr string, a sasl.Client, from string, to []string, r io.Reader) error { if err := validateLine(from); err != nil { return err } for _, recp := range to { if err := validateLine(recp); err != nil { return err } } c, err := Dial(addr) if err != nil { return err } defer c.Close() if err = c.hello(); err != nil { return err } if ok, _ := c.Extension("STARTTLS"); ok { if err = c.StartTLS(nil); err != nil { return err } } if a != nil && c.ext != nil { if _, ok := c.ext["AUTH"]; !ok { return errors.New("smtp: server doesn't support AUTH") } if err = c.Auth(a); err != nil { return err } } if err = c.Mail(from, nil); err != nil { return err } for _, addr := range to { if err = c.Rcpt(addr); err != nil { return err } } w, err := c.Data() if err != nil { return err } _, err = io.Copy(w, r) if err != nil { return err } err = w.Close() if err != nil { return err } return c.Quit() } // Extension reports whether an extension is support by the server. // The extension name is case-insensitive. If the extension is supported, // Extension also returns a string that contains any parameters the // server specifies for the extension. func (c *Client) Extension(ext string) (bool, string) { if err := c.hello(); err != nil { return false, "" } if c.ext == nil { return false, "" } ext = strings.ToUpper(ext) param, ok := c.ext[ext] return ok, param } // Reset sends the RSET command to the server, aborting the current mail // transaction. func (c *Client) Reset() error { if err := c.hello(); err != nil { return err } if _, _, err := c.cmd(250, "RSET"); err != nil { return err } c.rcptToCount = 0 return nil } // Noop sends the NOOP command to the server. It does nothing but check // that the connection to the server is okay. func (c *Client) Noop() error { if err := c.hello(); err != nil { return err } _, _, err := c.cmd(250, "NOOP") return err } // Quit sends the QUIT command and closes the connection to the server. // // If Quit fails the connection is not closed, Close should be used // in this case. func (c *Client) Quit() error { if err := c.hello(); err != nil { return err } _, _, err := c.cmd(221, "QUIT") if err != nil { return err } return c.Text.Close() } func parseEnhancedCode(s string) (EnhancedCode, error) { parts := strings.Split(s, ".") if len(parts) != 3 { return EnhancedCode{}, fmt.Errorf("wrong amount of enhanced code parts") } code := EnhancedCode{} for i, part := range parts { num, err := strconv.Atoi(part) if err != nil { return code, err } code[i] = num } return code, nil } // toSMTPErr converts textproto.Error into SMTPError, parsing // enhanced status code if it is present. func toSMTPErr(protoErr *textproto.Error) *SMTPError { if protoErr == nil { return nil } smtpErr := &SMTPError{ Code: protoErr.Code, Message: protoErr.Msg, } parts := strings.SplitN(protoErr.Msg, " ", 2) if len(parts) != 2 { return smtpErr } enchCode, err := parseEnhancedCode(parts[0]) if err != nil { return smtpErr } smtpErr.EnhancedCode = enchCode smtpErr.Message = parts[1] return smtpErr } go-smtp-0.12.1/client_test.go000066400000000000000000000636601357270675100160550ustar00rootroot00000000000000// 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" "io" "net" "net/textproto" "strings" "sync" "testing" "time" "github.com/emersion/go-sasl" ) // Issue 17794: don't send a trailing space on AUTH command when there's no password. 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, err := NewClient(fake, "fake.host") if err != nil { t.Fatalf("NewClient: %v", err) } c.tls = true c.didHello = true c.Auth(toServerEmptyAuth{}) c.Close() if got, want := wrote.String(), "AUTH FOOAUTH\r\n*\r\n"; got != want { t.Errorf("wrote %q; want %q", got, want) } } // toServerEmptyAuth is an implementation of Auth that only implements // the Start method, and returns "FOOAUTH", nil, nil. Notably, it returns // zero bytes for "toServer" so we can test that we don't send spaces at // the end of the line. See TestClientAuthTrimSpace. type toServerEmptyAuth struct{} func (toServerEmptyAuth) Start() (proto string, toServer []byte, err error) { return "FOOAUTH", nil, nil } func (toServerEmptyAuth) 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), 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 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) } // fake TLS so authentication won't complain c.tls = true 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"); 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"); 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` // 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, err := NewClient(fake, "fake.host") if err != nil { t.Fatalf("NewClient failed: %v", err) } err = c.Mail("whatever", nil) if err == nil { t.Fatal("MAIL succeded") } smtpErr, ok := err.(*SMTPError) if !ok { t.Fatal("Returned error is not SMTPError") } if smtpErr.Code != 500 { t.Fatalf("Wrong status code, got %d, want %d", smtpErr.Code, 500) } if 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 succeded") } smtpErr, ok = err.(*SMTPError) if !ok { t.Fatal("Returned error is not SMTPError") } if smtpErr.Code != 500 { t.Fatalf("Wrong status code, got %d, want %d", smtpErr.Code, 500) } if smtpErr.Message != "Failing without enhanced code" { t.Fatalf("Wrong message, got %s, want %s", smtpErr.Message, "Failing without enhanced code") } } func TestClient_TooLongLine(t *testing.T) { faultyServer := []string{ "220 mx.google.com at your service\r\n", "220 mx.google.com at your service\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", "220 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, err := NewClient(fake, "fake.host") if err != nil { t.Fatalf("NewClient failed: %v", err) } err = c.Mail("whatever", nil) if err != ErrTooLongLine { t.Fatal("MAIL succeded 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 succeded 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, err := NewClient(fake, "fake.host") if err != nil { t.Fatalf("NewClient: %v\n(after %v)", err, out()) } 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, err := NewClient(fake, "fake.host") if err != nil { t.Fatalf("NewClient: %v", err) } 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, err := NewClient(fake, "fake.host") if err != nil { t.Fatalf("NewClient: %v", err) } defer c.Close() c.localName = "customhost" err = nil 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() == "Not implemented" { err = nil } case 2: err = c.Verify("test@example.com") case 3: c.tls = true 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", } func TestSendMail(t *testing.T) { server := strings.Join(strings.Split(sendMailServer, "\n"), "\r\n") client := strings.Join(strings.Split(sendMailClient, "\n"), "\r\n") var cmdbuf bytes.Buffer bcmdbuf := bufio.NewWriter(&cmdbuf) l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("Unable to create listener: %v", err) } defer l.Close() // prevent data race on bcmdbuf var done = make(chan struct{}) go func(data []string) { defer close(done) conn, err := l.Accept() if err != nil { t.Errorf("Accept error: %v", err) return } defer conn.Close() tc := textproto.NewConn(conn) for i := 0; i < len(data) && data[i] != ""; i++ { tc.PrintfLine(data[i]) for len(data[i]) >= 4 && data[i][3] == '-' { i++ tc.PrintfLine(data[i]) } if data[i] == "221 Goodbye" { return } read := false for !read || data[i] == "354 Go ahead" { msg, err := tc.ReadLine() bcmdbuf.Write([]byte(msg + "\r\n")) read = true if err != nil { t.Errorf("Read error: %v", err) return } if data[i] == "354 Go ahead" && msg == "." { break } } } }(strings.Split(server, "\r\n")) err = SendMail(l.Addr().String(), nil, "test@example.com", []string{"other@example.com>\n\rDATA\r\nInjected message body\r\n.\r\nQUIT\r\n"}, strings.NewReader(strings.Replace(`From: test@example.com To: other@example.com Subject: SendMail test SendMail is working for me. `, "\n", "\r\n", -1))) if err == nil { t.Errorf("Expected SendMail to be rejected due to a message injection attempt") } err = SendMail(l.Addr().String(), nil, "test@example.com", []string{"other@example.com"}, strings.NewReader(strings.Replace(`From: test@example.com To: other@example.com Subject: SendMail test SendMail is working for me. `, "\n", "\r\n", -1))) if err != nil { t.Errorf("%v", err) } <-done bcmdbuf.Flush() actualcmds := cmdbuf.String() if client != actualcmds { t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client) } } var sendMailServer = `220 hello world 502 EH? 250 mx.google.com at your service 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 TestSendMailWithAuth(t *testing.T) { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("Unable to create listener: %v", err) } defer l.Close() wg := sync.WaitGroup{} var done = make(chan struct{}) go func() { defer wg.Done() conn, err := l.Accept() if err != nil { t.Errorf("Accept error: %v", err) return } defer conn.Close() tc := textproto.NewConn(conn) tc.PrintfLine("220 hello world") msg, err := tc.ReadLine() if msg == "EHLO localhost" { tc.PrintfLine("250 mx.google.com at your service") } // for this test case, there should have no more traffic <-done }() wg.Add(1) err = SendMail(l.Addr().String(), sasl.NewPlainClient("", "user", "pass"), "test@example.com", []string{"other@example.com"}, strings.NewReader(strings.Replace(`From: test@example.com To: other@example.com Subject: SendMail test SendMail is working for me. `, "\n", "\r\n", -1))) if err == nil { t.Error("SendMail: Server doesn't support AUTH, expected to get an error, but got none ") } if err.Error() != "smtp: server doesn't support AUTH" { t.Errorf("Expected: smtp: server doesn't support AUTH, got: %s", err) } close(done) wg.Wait() } func TestAuthFailed(t *testing.T) { server := strings.Join(strings.Split(authFailedServer, "\n"), "\r\n") client := strings.Join(strings.Split(authFailedClient, "\n"), "\r\n") var cmdbuf bytes.Buffer bcmdbuf := bufio.NewWriter(&cmdbuf) var fake faker fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) c, err := NewClient(fake, "fake.host") if err != nil { t.Fatalf("NewClient: %v", err) } defer c.Close() c.tls = true 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() != "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 <- sendMail(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) c, err := Dial(ln.Addr().String()) if err != nil { t.Errorf("Client dial: %v", err) return } defer c.Quit() cfg := &tls.Config{ServerName: "example.com"} testHookStartTLS(cfg) // set the RootCAs if err := c.StartTLS(cfg); err != nil { t.Errorf("StartTLS: %v", err) return } 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 sendMail(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), lmtp: true} if err := c.Hello("localhost"); err != nil { t.Fatalf("LHLO failed: %s", err) } c.didHello = true if err := c.Mail("user@gmail.com", nil); err != nil { t.Fatalf("MAIL failed: %s", err) } if err := c.Rcpt("golang-nuts@googlegroups.com"); err != nil { t.Fatalf("RCPT failed: %s", err) } 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 = `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 ` go-smtp-0.12.1/conn.go000066400000000000000000000422741357270675100144730ustar00rootroot00000000000000package smtp import ( "crypto/tls" "encoding/base64" "fmt" "io" "io/ioutil" "net" "net/textproto" "runtime/debug" "strconv" "strings" "sync" "time" ) type ConnectionState struct { Hostname string LocalAddr net.Addr RemoteAddr net.Addr TLS tls.ConnectionState } type Conn struct { conn net.Conn text *textproto.Conn server *Server helo string nbrErrors int session Session locker sync.Mutex fromReceived bool recipients []string } func newConn(c net.Conn, s *Server) *Conn { sc := &Conn{ server: s, conn: c, } sc.init() return sc } func (c *Conn) init() { rwc := struct { io.Reader io.Writer io.Closer }{ Reader: lineLimitReader{ R: c.conn, LineLimit: c.server.MaxLineLength, }, 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) } func (c *Conn) unrecognizedCommand(cmd string) { c.WriteResponse(500, EnhancedCode{5, 5, 2}, fmt.Sprintf("Syntax error, %v command unrecognized", cmd)) c.nbrErrors++ if c.nbrErrors > 3 { c.WriteResponse(500, EnhancedCode{5, 5, 2}, "Too many unrecognized commands") c.Close() } } // Commands are dispatched to the appropriate handler functions. func (c *Conn) handle(cmd string, arg string) { // If panic happens during command handling - send 421 response // and close connection. defer func() { if err := recover(); err != nil { c.WriteResponse(421, EnhancedCode{4, 0, 0}, "Internal server error") c.Close() stack := debug.Stack() c.server.ErrorLog.Printf("panic serving %v: %v\n%s", c.State().RemoteAddr, err, stack) } }() if cmd == "" { c.WriteResponse(500, EnhancedCode{5, 5, 2}, "Speak up") return } cmd = strings.ToUpper(cmd) switch cmd { case "SEND", "SOML", "SAML", "EXPN", "HELP", "TURN": // These commands are not implemented in any state c.WriteResponse(502, EnhancedCode{5, 5, 1}, fmt.Sprintf("%v command not implemented", cmd)) case "HELO", "EHLO", "LHLO": lmtp := cmd == "LHLO" enhanced := lmtp || cmd == "EHLO" if c.server.LMTP && !lmtp { c.WriteResponse(500, EnhancedCode{5, 5, 1}, "This is a LMTP server, use LHLO") return } if !c.server.LMTP && lmtp { c.WriteResponse(500, EnhancedCode{5, 5, 1}, "This is not a LMTP server") return } c.handleGreet(enhanced, arg) case "MAIL": c.handleMail(arg) case "RCPT": c.handleRcpt(arg) case "VRFY": c.WriteResponse(252, EnhancedCode{2, 5, 0}, "Cannot VRFY user, but will accept message") case "NOOP": c.WriteResponse(250, EnhancedCode{2, 0, 0}, "I have sucessfully done nothing") case "RSET": // Reset session c.reset() c.WriteResponse(250, EnhancedCode{2, 0, 0}, "Session reset") case "DATA": c.handleData(arg) case "QUIT": c.WriteResponse(221, EnhancedCode{2, 0, 0}, "Goodnight and good luck") c.Close() case "AUTH": if c.server.AuthDisabled { c.unrecognizedCommand(cmd) } else { c.handleAuth(arg) } case "STARTTLS": c.handleStartTLS() default: c.unrecognizedCommand(cmd) } } func (c *Conn) Server() *Server { return c.server } func (c *Conn) Session() Session { c.locker.Lock() defer c.locker.Unlock() return c.session } // Setting the user resets any message being generated func (c *Conn) SetSession(session Session) { c.locker.Lock() defer c.locker.Unlock() c.session = session } func (c *Conn) Close() error { if session := c.Session(); session != nil { session.Logout() c.SetSession(nil) } 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) State() ConnectionState { state := ConnectionState{} tlsState, ok := c.TLSConnectionState() if ok { state.TLS = tlsState } state.Hostname = c.helo state.LocalAddr = c.conn.LocalAddr() state.RemoteAddr = c.conn.RemoteAddr() return state } func (c *Conn) authAllowed() bool { _, isTLS := c.TLSConnectionState() return !c.server.AuthDisabled && (isTLS || c.server.AllowInsecureAuth) } // GREET state -> waiting for HELO func (c *Conn) handleGreet(enhanced bool, arg string) { if !enhanced { domain, err := parseHelloArgument(arg) if err != nil { c.WriteResponse(501, EnhancedCode{5, 5, 2}, "Domain/address argument required for HELO") return } c.helo = domain c.WriteResponse(250, EnhancedCode{2, 0, 0}, fmt.Sprintf("Hello %s", domain)) } else { domain, err := parseHelloArgument(arg) if err != nil { c.WriteResponse(501, EnhancedCode{5, 5, 2}, "Domain/address argument required for EHLO") return } c.helo = domain caps := []string{} caps = append(caps, c.server.caps...) if _, isTLS := c.TLSConnectionState(); c.server.TLSConfig != nil && !isTLS { caps = append(caps, "STARTTLS") } if c.authAllowed() { authCap := "AUTH" for name := range c.server.auths { authCap += " " + name } caps = append(caps, authCap) } if c.server.EnableSMTPUTF8 { caps = append(caps, "SMTPUTF8") } if _, isTLS := c.TLSConnectionState(); isTLS && c.server.EnableREQUIRETLS { caps = append(caps, "REQUIRETLS") } if c.server.MaxMessageBytes > 0 { caps = append(caps, fmt.Sprintf("SIZE %v", c.server.MaxMessageBytes)) } args := []string{"Hello " + domain} args = append(args, caps...) c.WriteResponse(250, NoEnhancedCode, args...) } } // READY state -> waiting for MAIL func (c *Conn) handleMail(arg string) { if c.helo == "" { c.WriteResponse(502, EnhancedCode{2, 5, 1}, "Please introduce yourself first.") return } if c.Session() == nil { state := c.State() session, err := c.server.Backend.AnonymousLogin(&state) if err != nil { if smtpErr, ok := err.(*SMTPError); ok { c.WriteResponse(smtpErr.Code, smtpErr.EnhancedCode, smtpErr.Message) } else { c.WriteResponse(502, EnhancedCode{5, 7, 0}, err.Error()) } return } c.SetSession(session) } if len(arg) < 6 || strings.ToUpper(arg[0:5]) != "FROM:" { c.WriteResponse(501, EnhancedCode{5, 5, 2}, "Was expecting MAIL arg syntax of FROM:
") return } fromArgs := strings.Split(strings.Trim(arg[5:], " "), " ") if c.server.Strict { if !strings.HasPrefix(fromArgs[0], "<") || !strings.HasSuffix(fromArgs[0], ">") { c.WriteResponse(501, EnhancedCode{5, 5, 2}, "Was expecting MAIL arg syntax of FROM:
") return } } from := fromArgs[0] if from == "" { c.WriteResponse(501, EnhancedCode{5, 5, 2}, "Was expecting MAIL arg syntax of FROM:
") return } from = strings.Trim(from, "<>") opts := MailOptions{} // This is where the Conn may put BODY=8BITMIME, but we already // read the DATA as bytes, so it does not effect our processing. if len(fromArgs) > 1 { args, err := parseArgs(fromArgs[1:]) if err != nil { c.WriteResponse(501, EnhancedCode{5, 5, 4}, "Unable to parse MAIL ESMTP parameters") return } for key, value := range args { switch key { case "SIZE": size, err := strconv.ParseInt(value, 10, 32) if err != nil { c.WriteResponse(501, EnhancedCode{5, 5, 4}, "Unable to parse SIZE as an integer") return } if c.server.MaxMessageBytes > 0 && int(size) > c.server.MaxMessageBytes { c.WriteResponse(552, EnhancedCode{5, 3, 4}, "Max message size exceeded") return } opts.Size = int(size) case "SMTPUTF8": if !c.server.EnableSMTPUTF8 { c.WriteResponse(504, EnhancedCode{5, 5, 4}, "SMTPUTF8 is not implemented") return } opts.UTF8 = true case "REQUIRETLS": if !c.server.EnableREQUIRETLS { c.WriteResponse(504, EnhancedCode{5, 5, 4}, "REQUIRETLS is not implemented") return } opts.RequireTLS = true case "BODY": switch value { case "7BIT", "8BITMIME": default: c.WriteResponse(500, EnhancedCode{5, 5, 4}, "Unknown BODY value") return } default: c.WriteResponse(500, EnhancedCode{5, 5, 4}, "Unknown MAIL FROM argument") return } } } if err := c.Session().Mail(from, opts); err != nil { if smtpErr, ok := err.(*SMTPError); ok { c.WriteResponse(smtpErr.Code, smtpErr.EnhancedCode, smtpErr.Message) return } c.WriteResponse(451, EnhancedCode{4, 0, 0}, err.Error()) return } c.WriteResponse(250, EnhancedCode{2, 0, 0}, fmt.Sprintf("Roger, accepting mail from <%v>", from)) c.fromReceived = true } // 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 (len(arg) < 4) || (strings.ToUpper(arg[0:3]) != "TO:") { c.WriteResponse(501, EnhancedCode{5, 5, 2}, "Was expecting RCPT arg syntax of TO:
") return } // TODO: This trim is probably too forgiving recipient := strings.Trim(arg[3:], "<> ") if c.server.MaxRecipients > 0 && len(c.recipients) >= c.server.MaxRecipients { c.WriteResponse(552, EnhancedCode{5, 5, 3}, fmt.Sprintf("Maximum limit of %v recipients reached", c.server.MaxRecipients)) return } if err := c.Session().Rcpt(recipient); err != nil { if smtpErr, ok := err.(*SMTPError); ok { c.WriteResponse(smtpErr.Code, smtpErr.EnhancedCode, smtpErr.Message) return } c.WriteResponse(451, EnhancedCode{4, 0, 0}, err.Error()) return } c.recipients = append(c.recipients, recipient) c.WriteResponse(250, EnhancedCode{2, 0, 0}, fmt.Sprintf("I'll make sure <%v> gets this", recipient)) } func (c *Conn) handleAuth(arg string) { if c.helo == "" { c.WriteResponse(502, EnhancedCode{5, 5, 1}, "Please introduce yourself first.") return } parts := strings.Fields(arg) if len(parts) == 0 { c.WriteResponse(502, EnhancedCode{5, 5, 4}, "Missing parameter") return } if _, isTLS := c.TLSConnectionState(); !isTLS && !c.server.AllowInsecureAuth { c.WriteResponse(523, EnhancedCode{5, 7, 10}, "TLS is required") return } mechanism := strings.ToUpper(parts[0]) // Parse client initial response if there is one var ir []byte if len(parts) > 1 { var err error ir, err = base64.StdEncoding.DecodeString(parts[1]) if err != nil { return } } newSasl, ok := c.server.auths[mechanism] if !ok { c.WriteResponse(504, EnhancedCode{5, 7, 4}, "Unsupported authentication mechanism") return } sasl := newSasl(c) response := ir for { challenge, done, err := sasl.Next(response) if err != nil { if smtpErr, ok := err.(*SMTPError); ok { c.WriteResponse(smtpErr.Code, smtpErr.EnhancedCode, smtpErr.Message) return } c.WriteResponse(454, EnhancedCode{4, 7, 0}, err.Error()) return } if done { break } encoded := "" if len(challenge) > 0 { encoded = base64.StdEncoding.EncodeToString(challenge) } c.WriteResponse(334, NoEnhancedCode, encoded) encoded, err = c.ReadLine() if err != nil { return // TODO: error handling } response, err = base64.StdEncoding.DecodeString(encoded) if err != nil { c.WriteResponse(454, EnhancedCode{4, 7, 0}, "Invalid base64 data") return } } if c.Session() != nil { c.WriteResponse(235, EnhancedCode{2, 0, 0}, "Authentication succeeded") } } 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 var tlsConn *tls.Conn tlsConn = tls.Server(c.conn, c.server.TLSConfig) if err := tlsConn.Handshake(); err != nil { c.WriteResponse(550, EnhancedCode{5, 0, 0}, "Handshake error") } c.conn = tlsConn c.init() // Reset envelope as a new EHLO/HELO is required after STARTTLS 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.fromReceived || len(c.recipients) == 0 { c.WriteResponse(502, EnhancedCode{5, 5, 1}, "Missing RCPT TO command.") return } // We have recipients, go to accept data c.WriteResponse(354, EnhancedCode{2, 0, 0}, "Go ahead. End your data with .") defer c.reset() if c.server.LMTP { c.handleDataLMTP() return } r := newDataReader(c) code, enhancedCode, msg := toSMTPStatus(c.Session().Data(r)) io.Copy(ioutil.Discard, r) // Make sure all the data has been consumed c.WriteResponse(code, enhancedCode, msg) } 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) 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]) } 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.State().RemoteAddr, err, stack) done <- false } }() status.fillRemaining(lmtpSession.LMTPData(r, status)) io.Copy(ioutil.Discard, r) // Make sure all the data has been consumed done <- true }() } for i, rcpt := range c.recipients { code, enchCode, msg := toSMTPStatus(<-status.status[i]) c.WriteResponse(code, enchCode, "<"+rcpt+"> "+msg) } // If done gets false, the panic occured in LMTPData and the connection // should be closed. if !<-done { c.Close() } } func toSMTPStatus(err error) (code int, enchCode EnhancedCode, msg string) { if err != nil { if smtperr, ok := err.(*SMTPError); ok { return smtperr.Code, smtperr.EnhancedCode, smtperr.Message } else { return 554, EnhancedCode{5, 0, 0}, "Error: transaction failed, blame it on the weather: " + err.Error() } } return 250, EnhancedCode{2, 0, 0}, "OK: queued" } func (c *Conn) Reject() { c.WriteResponse(421, EnhancedCode{4, 4, 5}, "Too busy. Try again later.") c.Close() } func (c *Conn) greet() { c.WriteResponse(220, NoEnhancedCode, fmt.Sprintf("%v ESMTP Service Ready", c.server.Domain)) } func (c *Conn) WriteResponse(code int, enhCode EnhancedCode, text ...string) { // TODO: error handling if c.server.WriteTimeout != 0 { c.conn.SetWriteDeadline(time.Now().Add(c.server.WriteTimeout)) } // All responses must include an enhanced code, if it is missing - use // a generic code X.0.0. if enhCode == EnhancedCodeNotSet { cat := code / 100 switch cat { case 2, 4, 5: enhCode = EnhancedCode{cat, 0, 0} default: enhCode = NoEnhancedCode } } for i := 0; i < len(text)-1; i++ { c.text.PrintfLine("%v-%v", code, text[i]) } if enhCode == NoEnhancedCode { c.text.PrintfLine("%v %v", code, text[len(text)-1]) } else { c.text.PrintfLine("%v %v.%v.%v %v", code, enhCode[0], enhCode[1], enhCode[2], text[len(text)-1]) } } // 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.session != nil { c.session.Reset() } c.fromReceived = false c.recipients = nil } go-smtp-0.12.1/data.go000066400000000000000000000032221357270675100144350ustar00rootroot00000000000000package smtp import ( "io" ) type EnhancedCode [3]int // SMTPError specifies the error code, enhanced error code (if any) and // message returned by the server. type SMTPError struct { Code int EnhancedCode EnhancedCode Message string } // NoEnhancedCode is used to indicate that enhanced error code should not be // included in response. // // Note that RFC 2034 requires an enhanced code to be included in all 2xx, 4xx // and 5xx responses. This constant is exported for use by extensions, you // should probably use EnhancedCodeNotSet instead. var NoEnhancedCode = EnhancedCode{-1, -1, -1} // EnhancedCodeNotSet is a nil value of EnhancedCode field in SMTPError, used // to indicate that backend failed to provide enhanced status code. X.0.0 will // be used (X is derived from error code). var EnhancedCodeNotSet = EnhancedCode{0, 0, 0} func (err *SMTPError) Error() string { return err.Message } func (err *SMTPError) Temporary() bool { return err.Code/100 == 4 } var ErrDataTooLarge = &SMTPError{ Code: 552, EnhancedCode: EnhancedCode{5, 3, 4}, Message: "Maximum message size exceeded", } type dataReader struct { r io.Reader limited bool n int64 // Maximum bytes remaining } func newDataReader(c *Conn) io.Reader { dr := &dataReader{ r: c.text.DotReader(), } 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] } } n, err = r.r.Read(b) if r.limited { r.n -= int64(n) } return } go-smtp-0.12.1/example_test.go000066400000000000000000000067411357270675100162270ustar00rootroot00000000000000// 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 ( "errors" "fmt" "io" "io/ioutil" "log" "strings" "time" "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"); 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) } } // The Backend implements SMTP server methods. type Backend struct{} // Login handles a login command with username and password. func (bkd *Backend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) { if username != "username" || password != "password" { return nil, errors.New("Invalid username or password") } return &Session{}, nil } // AnonymousLogin requires clients to authenticate using SMTP AUTH before sending emails func (bkd *Backend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) { return nil, smtp.ErrAuthRequired } // A Session is returned after successful login. type Session struct{} func (s *Session) Mail(from string, opts smtp.MailOptions) error { log.Println("Mail from:", from) return nil } func (s *Session) Rcpt(to string) error { log.Println("Rcpt to:", to) return nil } func (s *Session) Data(r io.Reader) error { if b, err := ioutil.ReadAll(r); err != nil { return err } else { log.Println("Data:", string(b)) } return nil } func (s *Session) Reset() {} func (s *Session) Logout() error { return nil } func ExampleNewServer() { be := &Backend{} s := smtp.NewServer(be) s.Addr = ":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.12.1/go.mod000066400000000000000000000001641357270675100143050ustar00rootroot00000000000000module github.com/emersion/go-smtp require github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e go 1.13 go-smtp-0.12.1/go.sum000066400000000000000000000003451357270675100143330ustar00rootroot00000000000000github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e h1:ba7YsgX5OV8FjGi5ZWml8Jng6oBrJAb3ahqWMJ5Ce8Q= github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k= go-smtp-0.12.1/lengthlimit_reader.go000066400000000000000000000014541357270675100173730ustar00rootroot00000000000000package smtp import ( "errors" "io" ) var ErrTooLongLine = errors.New("smtp: too longer 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 { 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.12.1/lmtp_server_test.go000066400000000000000000000115411357270675100171300ustar00rootroot00000000000000package smtp_test import ( "bufio" "errors" "io" "strings" "testing" "github.com/emersion/go-smtp" ) 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 := testServerGreeted(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 := testServerGreeted(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 := testServerGreeted(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 := testServerGreeted(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.12.1/parse.go000066400000000000000000000034231357270675100146410ustar00rootroot00000000000000package smtp import ( "fmt" "strings" ) func parseCmd(line string) (cmd string, arg string, err error) { line = strings.TrimRight(line, "\r\n") l := len(line) switch { case strings.HasPrefix(strings.ToUpper(line), "STARTTLS"): return "STARTTLS", "", nil case l == 0: return "", "", nil case l < 4: return "", "", fmt.Errorf("Command too short: %q", line) case l == 4: return strings.ToUpper(line), "", nil case l == 5: // Too long to be only command, too short to have args return "", "", fmt.Errorf("Mangled command: %q", line) } // If we made it here, command is long enough to have args if line[4] != ' ' { // There wasn't a space after the command? return "", "", fmt.Errorf("Mangled command: %q", line) } // I'm not sure if we should trim the args or not, but we will for now //return strings.ToUpper(line[0:4]), strings.Trim(line[5:], " "), nil return strings.ToUpper(line[0:4]), strings.Trim(line[5:], " \n\r"), nil } // Takes the arguments proceeding a command and files them // into a map[string]string after uppercasing each key. Sample arg // string: // " BODY=8BITMIME SIZE=1024 SMTPUTF8" // The leading space is mandatory. func parseArgs(args []string) (map[string]string, error) { argMap := map[string]string{} for _, arg := range args { if arg == "" { continue } m := strings.Split(arg, "=") switch len(m) { case 2: argMap[strings.ToUpper(m[0])] = m[1] case 1: argMap[strings.ToUpper(m[0])] = "" default: return nil, fmt.Errorf("Failed to parse arg string: %q", arg) } } return argMap, nil } func parseHelloArgument(arg string) (string, error) { domain := arg if idx := strings.IndexRune(arg, ' '); idx >= 0 { domain = arg[:idx] } if domain == "" { return "", fmt.Errorf("Invalid domain") } return domain, nil } go-smtp-0.12.1/server.go000077500000000000000000000122351357270675100150410ustar00rootroot00000000000000package smtp import ( "crypto/tls" "errors" "io" "log" "net" "os" "sync" "time" "github.com/emersion/go-sasl" ) var errTCPAndLMTP = errors.New("smtp: cannot start LMTP server listening on a TCP socket") // A function that creates SASL servers. type SaslServerFactory func(conn *Conn) sasl.Server // Logger interface is used by Server to report unexpected internal errors. type Logger interface { Printf(format string, v ...interface{}) Println(v ...interface{}) } // A SMTP server. type Server struct { // TCP or Unix address to listen on. Addr string // The server TLS configuration. TLSConfig *tls.Config // Enable LMTP mode, as defined in RFC 2033. LMTP mode cannot be used with a // TCP listener. LMTP bool Domain string MaxRecipients int MaxMessageBytes int MaxLineLength int AllowInsecureAuth bool Strict bool Debug io.Writer ErrorLog Logger ReadTimeout time.Duration WriteTimeout time.Duration // Advertise SMTPUTF8 (RFC 6531) capability. // Should be used only if backend supports it. EnableSMTPUTF8 bool // Advertise REQUIRETLS (draft-ietf-uta-smtp-require-tls-09) capability. // Should be used only if backend supports it. EnableREQUIRETLS bool // If set, the AUTH command will not be advertised and authentication // attempts will be rejected. This setting overrides AllowInsecureAuth. AuthDisabled bool // The server backend. Backend Backend listeners []net.Listener caps []string auths map[string]SaslServerFactory done chan struct{} locker sync.Mutex conns map[*Conn]struct{} } // New creates a new SMTP server. func NewServer(be Backend) *Server { return &Server{ // Doubled maximum line length per RFC 5321 (Section 4.5.3.1.6) MaxLineLength: 2000, Backend: be, done: make(chan struct{}, 1), ErrorLog: log.New(os.Stderr, "smtp/server ", log.LstdFlags), caps: []string{"PIPELINING", "8BITMIME", "ENHANCEDSTATUSCODES"}, auths: map[string]SaslServerFactory{ sasl.Plain: func(conn *Conn) sasl.Server { return sasl.NewPlainServer(func(identity, username, password string) error { if identity != "" && identity != username { return errors.New("Identities not supported") } state := conn.State() session, err := be.Login(&state, username, password) if err != nil { return err } conn.SetSession(session) return nil }) }, }, conns: make(map[*Conn]struct{}), } } // Serve accepts incoming connections on the Listener l. func (s *Server) Serve(l net.Listener) error { s.listeners = append(s.listeners, l) for { c, err := l.Accept() if err != nil { select { case <-s.done: // we called Close() return nil default: return err } } go s.handleConn(newConn(c, s)) } } 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() }() c.greet() for { line, err := c.ReadLine() if err == nil { cmd, arg, err := parseCmd(line) if err != nil { c.nbrErrors++ c.WriteResponse(501, EnhancedCode{5, 5, 2}, "Bad command") continue } c.handle(cmd, arg) } else { if err == io.EOF { return nil } if err == ErrTooLongLine { c.WriteResponse(500, EnhancedCode{5, 4, 0}, "Too long line, closing connection") return nil } if neterr, ok := err.(net.Error); ok && neterr.Timeout() { c.WriteResponse(221, EnhancedCode{2, 4, 2}, "Idle timeout, bye bye") return nil } c.WriteResponse(221, EnhancedCode{2, 4, 0}, "Connection error, sorry") return err } } } // ListenAndServe listens on the network address s.Addr and then calls Serve // to handle requests on incoming connections. // // If s.Addr is blank and LMTP is disabled, ":smtp" is used. func (s *Server) ListenAndServe() error { network := "tcp" if s.LMTP { network = "unix" } addr := s.Addr if !s.LMTP && addr == "" { addr = ":smtp" } l, err := net.Listen(network, addr) if err != nil { return err } return s.Serve(l) } // ListenAndServeTLS listens on the TCP network address s.Addr and then calls // Serve to handle requests on incoming TLS connections. // // If s.Addr is blank, ":smtps" is used. func (s *Server) ListenAndServeTLS() error { if s.LMTP { return errTCPAndLMTP } addr := s.Addr if addr == "" { addr = ":smtps" } l, err := tls.Listen("tcp", addr, s.TLSConfig) if err != nil { return err } return s.Serve(l) } // Close stops the server. func (s *Server) Close() { close(s.done) for _, l := range s.listeners { l.Close() } s.locker.Lock() defer s.locker.Unlock() for conn := range s.conns { conn.Close() } } // EnableAuth enables an authentication mechanism on this server. // // This function should not be called directly, it must only be used by // libraries implementing extensions of the SMTP protocol. func (s *Server) EnableAuth(name string, f SaslServerFactory) { s.auths[name] = f } // ForEachConn iterates through all opened connections. func (s *Server) ForEachConn(f func(*Conn)) { s.locker.Lock() defer s.locker.Unlock() for conn := range s.conns { f(conn) } } go-smtp-0.12.1/server_test.go000066400000000000000000000364671357270675100161120ustar00rootroot00000000000000package smtp_test import ( "bufio" "errors" "io" "io/ioutil" "log" "net" "strings" "testing" "github.com/emersion/go-smtp" ) type message struct { From string To []string Data []byte } type backend struct { messages []*message anonmsgs []*message implementLMTPData bool lmtpStatus []struct { addr string err error } lmtpStatusSync chan struct{} panicOnMail bool userErr error } func (be *backend) Login(_ *smtp.ConnectionState, username, password string) (smtp.Session, error) { if be.userErr != nil { return &session{}, be.userErr } if username != "username" || password != "password" { return nil, errors.New("Invalid username or password") } if be.implementLMTPData { return &lmtpSession{&session{backend: be}}, nil } return &session{backend: be}, nil } func (be *backend) AnonymousLogin(_ *smtp.ConnectionState) (smtp.Session, error) { if be.userErr != nil { return &session{}, be.userErr } 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 } 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.panicOnMail { panic("Everything is on fire!") } s.Reset() s.msg.From = from return nil } func (s *session) Rcpt(to string) 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 } 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 serverConfigureFunc func(*smtp.Server) var ( authDisabled = func(s *smtp.Server) { s.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 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 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()) } return } 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()) } return } 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()) } return } 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()) } return } 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()) } return } 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()) } return } 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()) } return } 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()) } return } 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()) } return } func TestServerTooBig(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM: SIZE=4294967295\r\n") scanner.Scan() if strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } return } 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()) } 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] 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) != "Hey <3\n" { t.Fatal("Invalid mail data:", string(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() != "500 5.5.2 Syntax error, AUTH command unrecognized" { 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) } } 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 testStrictServer(t *testing.T) (s *smtp.Server, c net.Conn, scanner *bufio.Scanner) { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } s = smtp.NewServer(new(backend)) s.Domain = "localhost" s.AllowInsecureAuth = true s.AuthDisabled = true s.Strict = true go s.Serve(l) c, err = net.Dial("tcp", l.Addr().String()) if err != nil { t.Fatal(err) } scanner = bufio.NewScanner(c) scanner.Scan() if scanner.Text() != "220 localhost ESMTP Service Ready" { t.Fatal("Invalid greeting:", scanner.Text()) } 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 TestStrictServerGood(t *testing.T) { s, c, scanner := testStrictServer(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 TestStrictServerBad(t *testing.T) { s, c, scanner := testStrictServer(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM: root@nsa.gov\r\n") scanner.Scan() if strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } } go-smtp-0.12.1/smtp.go000066400000000000000000000012641357270675100145130ustar00rootroot00000000000000// 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 draft-ietf-uta-smtp-require-tls-09 // // LMTP (RFC 2033) is also supported. // // Additional extensions may be handled by other packages. package smtp import ( "errors" "strings" ) // validateLine checks to see if a line has CR or LF as per RFC 5321 func validateLine(line string) error { if strings.ContainsAny(line, "\n\r") { return errors.New("smtp: A line must not contain CR or LF") } return nil }