From ac162dc7ac199531958aec0fc2f0d093da539c67 Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Sun, 13 Jul 2014 23:24:13 +0200 Subject: [PATCH 01/34] Initial checkin. --- cmd/smtpd/main.go | 46 ++++++ cmd/smtpd/smtpd.crt | 20 +++ cmd/smtpd/smtpd.csr | 17 ++ cmd/smtpd/smtpd.key | 27 ++++ smtpd.go | 368 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 478 insertions(+) create mode 100644 cmd/smtpd/main.go create mode 100644 cmd/smtpd/smtpd.crt create mode 100644 cmd/smtpd/smtpd.csr create mode 100644 cmd/smtpd/smtpd.key create mode 100644 smtpd.go diff --git a/cmd/smtpd/main.go b/cmd/smtpd/main.go new file mode 100644 index 0000000..a39068f --- /dev/null +++ b/cmd/smtpd/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "bitbucket.org/chrj/smtpd" + "crypto/tls" + "flag" + "log" +) + +func dumpMessage(peer smtpd.Peer, env smtpd.Envelope) error { + log.Printf("New mail from: %s", env.MailFrom) + return nil +} + +var tlsCert = flag.String("tlscert", "", "TLS: Certificate file") +var tlsKey = flag.String("tlskey", "", "TLS: Private key") + +func main() { + + flag.Parse() + + var tlsConfig *tls.Config + + if *tlsCert != "" { + cert, err := tls.LoadX509KeyPair(*tlsCert, *tlsKey) + if err != nil { + log.Fatal("certificate error:", err) + } + tlsConfig = &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + } + + server := &smtpd.Server{ + Addr: "127.0.0.1:10025", + WelcomeMessage: "localhost ESMTP ready.", + Handler: dumpMessage, + TLSConfig: tlsConfig, + ForceTLS: true, + } + + server.ListenAndServe() + + return + +} diff --git a/cmd/smtpd/smtpd.crt b/cmd/smtpd/smtpd.crt new file mode 100644 index 0000000..7897738 --- /dev/null +++ b/cmd/smtpd/smtpd.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDLjCCAhYCCQD7wib+be6ipjANBgkqhkiG9w0BAQUFADBZMQswCQYDVQQGEwJE +SzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 +cyBQdHkgTHRkMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTQwNzEzMjA0MTE2WhcN +MTUwNzEzMjA0MTE2WjBZMQswCQYDVQQGEwJESzETMBEGA1UECAwKU29tZS1TdGF0 +ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDDAls +b2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDK41RNmjLD +NVs3ZOX1IpCfWITMZ8kx0TB9BXh86XhgaH47DNoOnSeDvawGfmKXYF7ISuFRacbc +C1xeiN+hah0CAJQJXpzYO8dpyXrPVIiZ/mKFRAnz/Kp/PApDjkpJ13VnLkuZLbJg +dQ0dtsb2BW+T/jEHDpyCOwR2g1AdlnsjuP+V1WxZvCKYvv5awv5AWwmCbKGjA1Jv +8j54WZzK7bFxp19Eyg2WVXhf7ZB+zs8RbliYzUqgT7GnUEBQkofaxb5j+n/PR7AU +/U1dFSVM7i1mn58SjsDx5v6GIh/Z3ekdEKbBiJJSvyhPV6K1b7IOWo9tQGeEMQP4 +tvkLDPPgPXOZAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAAniCpu2zPujExDMp36l +3VKtMZBbbOn8rwAcGOUjeSTZT62VQJX4CSsXJGuSHLV9fKPO8K3pob9mZ/CGL3Xj +JnLKDMgAQEiLq9IZPZg0/vYJjP96Hlgf0sOT6Q4dX36kDvGsWKJZilPEOKFvZh+R +acwWmN8bEGhFThijvTfY7sxEnTem1R2qs5cqCRfc4vCammTCRpLSWcD4p/WVZc5K +MCv8N2/JDg9plaBiQZyzaaiXI4X90IZQlWzIT6E2+i3V6bRwLioTituxu1r6Pwx9 +lniOQrA1+5waqottyMWQGHzmrFFg93HDX0WmP4IXHkXWhAcR611DLIw3NQuqt7Q4 +ecM= +-----END CERTIFICATE----- diff --git a/cmd/smtpd/smtpd.csr b/cmd/smtpd/smtpd.csr new file mode 100644 index 0000000..054ac6f --- /dev/null +++ b/cmd/smtpd/smtpd.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICnjCCAYYCAQAwWTELMAkGA1UEBhMCREsxEzARBgNVBAgMClNvbWUtU3RhdGUx +ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9j +YWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyuNUTZoywzVb +N2Tl9SKQn1iEzGfJMdEwfQV4fOl4YGh+OwzaDp0ng72sBn5il2BeyErhUWnG3Atc +XojfoWodAgCUCV6c2DvHacl6z1SImf5ihUQJ8/yqfzwKQ45KSdd1Zy5LmS2yYHUN +HbbG9gVvk/4xBw6cgjsEdoNQHZZ7I7j/ldVsWbwimL7+WsL+QFsJgmyhowNSb/I+ +eFmcyu2xcadfRMoNllV4X+2Qfs7PEW5YmM1KoE+xp1BAUJKH2sW+Y/p/z0ewFP1N +XRUlTO4tZp+fEo7A8eb+hiIf2d3pHRCmwYiSUr8oT1eitW+yDlqPbUBnhDED+Lb5 +Cwzz4D1zmQIDAQABoAAwDQYJKoZIhvcNAQEFBQADggEBALkJ6moQnDeT91Y37nQP +pXmcbiL/bj34v3MnUYArmxtZcfMJ3B9qxe5/0psq4r6hjxPWNaW92NkkE1aJZwuO +cAqGWcPBVFH309siq5J0NGkjArdtd84NBewoBZVqpcqwrfVAI6adINlF2dGLeeJW +SAlVEKCt3SLz3X+lVgKIzZTEsMuYmTaUrr490ecDWsh2eey0pbhtSqXkPkQOVUla +8QqysE5DuaES8ysTIuAh28uIxWmLXIWnVqia2+eltEgiuaiAZVH3CYH136/FTEL1 +a5toCmQFWv9rAc+EfVxIh1CgUNsWx5ARPVuSRZaBjH4qXwIg8V138eC482MNtMfM +fDs= +-----END CERTIFICATE REQUEST----- diff --git a/cmd/smtpd/smtpd.key b/cmd/smtpd/smtpd.key new file mode 100644 index 0000000..7428b4f --- /dev/null +++ b/cmd/smtpd/smtpd.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAyuNUTZoywzVbN2Tl9SKQn1iEzGfJMdEwfQV4fOl4YGh+Owza +Dp0ng72sBn5il2BeyErhUWnG3AtcXojfoWodAgCUCV6c2DvHacl6z1SImf5ihUQJ +8/yqfzwKQ45KSdd1Zy5LmS2yYHUNHbbG9gVvk/4xBw6cgjsEdoNQHZZ7I7j/ldVs +WbwimL7+WsL+QFsJgmyhowNSb/I+eFmcyu2xcadfRMoNllV4X+2Qfs7PEW5YmM1K +oE+xp1BAUJKH2sW+Y/p/z0ewFP1NXRUlTO4tZp+fEo7A8eb+hiIf2d3pHRCmwYiS +Ur8oT1eitW+yDlqPbUBnhDED+Lb5Cwzz4D1zmQIDAQABAoIBAQCoterncP8fRqIo +aRW0B18dsj0TwIYUj/BzNfZgYMCB4sJ9Fg3JszMloLaI29XeLPwEMAg3a+86EZRo +5AaaMiQXAyYWuH9SbDtBo5IlEBVbgKaqTM69/fBFR0b9sDfkOW9eMqgYo2A+R3d1 +qwS9lf2Xoftg8+x/etYWOtGHGRgitflirlW3uLvgCo/gP5gcb+HbtQNVRyKnqR1n +hNUeDCxTMLuhkvS2NxMUcAkuNYSLRiM3bXtER5RfatPHgvmFEmtKoB3TbQsw4Z6e +E+xlgFEnTVLkPoblhQjcUpiDqaZCfRUxmFvgfB+/0zrCZh3TtmMxSnUm1uFcNyul +dBICKx2lAoGBAP1e8GZKJ2hB5MIIG8TyZvmv1EnNfgAAhSbhWWXufoKflEAIBihg +7NBQAuHdq76e+G1F8GJzsHtNZquQhrIwo0U0/eBycrLBkQgIAeZJEZx7EH3EsqL3 +7RuJIaOQBy5LBnnnxjwlcNQvS7FiZheqEsN7RYScGE1RFAREj86B54bLAoGBAMz+ +SUpCTXHzpgOLhN6KBTmgr0fk4SKVLSdyFjNJbd7bokPoy2aO8IkBKn/jWrxNOZij +5XU0NryYuMq1dsJViZ1kRzF8Q3xw1IjKOUeWBp1221FrA+nouinIYNdtoNmIOLXO +1IOF0jInLqjBHC0MdaZDaupEJ0ZbFV+8EQCka/6rAoGANPkSffBnCM8uCrszQxwD +F5UBZ2TFQS7ap+RZkowoexruHe0PjIWnPW5dC+gSrkoCWqZSueLCNSVbn+cZoku0 +9xU7Nx/2hxUdQ3aZHxKL0hGQwxrK1nPLaQRkuhO0zKL2+anRsmWJj3NL+gw+mBgA +0EoHoNAZ7KBU9Qd4oY5bX70CgYBCdWJPZ+VxvxsgZRgjib2d7EFHXqW6r4BfHHak +E/dB3BTkTVG8IzVKRY2AvrXI/IRivygB8naYeC7Y0TH6WP7vfvYxzeaXLoFJA77E +PZhRbpo18Crpp6DLMQJsdUdDnw07rB1rsnPt/JP88/ZtiG+QAqVj48qT3a21RuSA +P84fVwKBgQCFdUzNpwsDVZ0L51yk7D9LwsA9jwzBxc5Jtd8CIDVylAlj1BM7hkiG +durZfNVtkhi+RXgD3SjZXWtCCprvrrjl8T52+deOCx2qM/5qhtJRKIHEkqndx4e5 +lmt3J5alekerwijR/F8+qnrrEsvtp6rozMDCNSGa6ir4HWYQUJ2C4g== +-----END RSA PRIVATE KEY----- diff --git a/smtpd.go b/smtpd.go new file mode 100644 index 0000000..73994e7 --- /dev/null +++ b/smtpd.go @@ -0,0 +1,368 @@ +package smtpd + +import ( + "bufio" + "bytes" + "crypto/tls" + "fmt" + "log" + "net" + "strings" + "time" +) + +type Server struct { + Addr string // Address to listen on + WelcomeMessage string // Initial server banner + + ReadTimeout time.Duration // Socket timeout for read operations (default: 60s) + WriteTimeout time.Duration // Socket timeout for write operations (default: 60s) + + // New e-mails are handed off to this function. + // If an error is returned, it will be reported in the SMTP session + Handler func(peer Peer, env Envelope) error + + // Enable PLAIN/LOGIN authentication + Authenticator func(peer Peer, username, password string) error + + TLSConfig *tls.Config // Enable STARTTLS support + ForceTLS bool // Force STARTTLS usage + + MaxMessageSize int // Max message size in bytes (default: 10240000) +} + +type sessionState int + +const ( + _STATE_HELO sessionState = iota + _STATE_AUTH + _STATE_MAIL + _STATE_RCPT + _STATE_DATA +) + +type session struct { + server *Server + conn net.Conn + reader *bufio.Reader + writer *bufio.Writer + peer Peer + state sessionState + tls bool +} + +type Peer struct { + HeloName string // Server name used in HELO/EHLO command + UserName string // Username from authentication + Addr net.Addr // Network address +} + +type MailAddress string + +type Envelope struct { + MailFrom MailAddress + Recipients []MailAddress + Data []byte + Peer *Peer +} + +func (srv *Server) newConnection(c net.Conn) (s *session, err error) { + + log.Printf("New connection from: %s", c.RemoteAddr()) + + s = &session{ + server: srv, + conn: c, + reader: bufio.NewReader(c), + writer: bufio.NewWriter(c), + peer: Peer{Addr: c.RemoteAddr()}, + } + + return s, nil + +} + +func (srv *Server) ListenAndServe() error { + l, err := net.Listen("tcp", srv.Addr) + if err != nil { + return err + } + return srv.Serve(l) +} + +func (srv *Server) Serve(l net.Listener) error { + + srv.configureDefaults() + + defer l.Close() + + for { + + conn, e := l.Accept() + if e != nil { + if ne, ok := e.(net.Error); ok && ne.Temporary() { + time.Sleep(time.Second) + continue + } + return e + } + + session, err := srv.newConnection(conn) + if err != nil { + continue + } + + session.state = _STATE_HELO + + go session.serve() + + } + +} + +func (srv *Server) configureDefaults() { + + if srv.MaxMessageSize == 0 { + srv.MaxMessageSize = 10240000 + } + + if srv.ReadTimeout == 0 { + srv.ReadTimeout = time.Second * 60 + } + + if srv.WriteTimeout == 0 { + srv.WriteTimeout = time.Second * 60 + } + + if srv.ForceTLS && srv.TLSConfig == nil { + log.Fatal("Cannot use ForceTLS with no TLSConfig") + } + +} + +func (session *session) serve() { + + log.Print("Serving") + + defer func() { + session.writer.Flush() + session.conn.Close() + }() + + session.reply(220, session.server.WelcomeMessage) + + scanner := bufio.NewScanner(session.reader) + + var env Envelope + var data *bytes.Buffer + + for scanner.Scan() { + + line := scanner.Text() + command := "" + fields := []string{} + params := []string{} + + if session.state != _STATE_DATA { + fields = strings.Fields(line) + command = strings.ToUpper(fields[0]) + if len(fields) > 1 { + params = strings.Split(fields[1], ":") + } + } + + log.Printf("Line: %s, fields: %#v, params: %#v", line, fields, params) + + if command == "QUIT" { + session.reply(250, "Ok, bye") + return + } + + switch session.state { + + case _STATE_HELO: + + if command == "HELO" || command == "EHLO" { + if len(fields) < 2 { + session.reply(502, "Missing parameter") + continue + } else { + session.peer.HeloName = fields[1] + } + } else { + session.reply(502, "Command not recognized, expected HELO/EHLO") + continue + } + + if command == "EHLO" { + session.WriteExtensions() + } else { + session.reply(250, "Go ahead") + } + + if session.server.Authenticator == nil { + session.state = _STATE_MAIL + } else { + session.state = _STATE_AUTH + } + + continue + + case _STATE_MAIL: + + if !session.tls && command == "STARTTLS" && session.server.TLSConfig != nil { + + tls_conn := tls.Server(session.conn, session.server.TLSConfig) + session.reply(250, "Go ahead") + + if err := tls_conn.Handshake(); err != nil { + log.Printf("TLS Handshake error:", err) + session.reply(550, "Handshake error") + continue + } + + session.conn = tls_conn + + session.reader = bufio.NewReader(tls_conn) + session.writer = bufio.NewWriter(tls_conn) + + scanner = bufio.NewScanner(session.reader) + + session.tls = true + session.state = _STATE_HELO + + continue + + } + + if !session.tls && session.server.ForceTLS { + session.reply(550, "Must run STARTTLS first") + continue + } + + if command == "MAIL" && strings.ToUpper(params[0]) == "FROM" { + + addr, err := parseMailAddress(params[1]) + + if err != nil { + session.reply(502, "Ill-formatted e-mail address") + continue + } + + env = Envelope{ + Peer: &session.peer, + MailFrom: addr, + } + + session.reply(250, "Go ahead") + session.state = _STATE_RCPT + continue + + } else { + session.reply(502, "Command not recognized, expected MAIL FROM") + continue + } + + case _STATE_RCPT: + + if command == "RCPT" && strings.ToUpper(params[0]) == "TO" { + + addr, err := parseMailAddress(params[1]) + + if err != nil { + session.reply(502, "Ill-formatted e-mail address") + continue + } + + env.Recipients = append(env.Recipients, addr) + + session.reply(250, "Go ahead") + continue + + } else if command == "DATA" && len(env.Recipients) > 0 { + session.reply(250, "Go ahead. End your data with .") + data = &bytes.Buffer{} + session.state = _STATE_DATA + continue + } + + if len(env.Recipients) == 0 { + session.reply(502, "Command not recognized, expected RCPT") + } else { + session.reply(502, "Command not recognized, expected RCPT or DATA") + } + + continue + + case _STATE_DATA: + + if line == "." { + env.Data = data.Bytes() + data.Reset() + err := session.handle(env) + + if err != nil { + session.reply(502, fmt.Sprintf("%s", err)) + } else { + session.reply(200, "Thank you.") + } + + session.state = _STATE_MAIL + continue + } + + } + + } + +} + +func (session *session) reply(code int, message string) { + + fmt.Fprintf(session.writer, "%d %s\r\n", code, message) + + session.conn.SetWriteDeadline(time.Now().Add(session.server.WriteTimeout)) + session.writer.Flush() + + session.conn.SetReadDeadline(time.Now().Add(session.server.ReadTimeout)) + +} + +func (session *session) WriteExtensions() { + + extensions := []string{ + "SIZE 10240000", + } + + if session.server.TLSConfig != nil && !session.tls { + extensions = append(extensions, "STARTTLS") + } + + if session.tls { + extensions = append(extensions, "AUTH PLAIN LOGIN") + } + + if len(extensions) > 1 { + for _, ext := range extensions[:len(extensions)-1] { + fmt.Fprintf(session.writer, "250-%s\r\n", ext) + } + } + + session.reply(250, extensions[len(extensions)-1]) + +} + +func (session *session) handle(env Envelope) error { + if session.server.Handler != nil { + return session.server.Handler(session.peer, env) + } else { + return nil + } +} + +func parseMailAddress(src string) (MailAddress, error) { + if src[0] != '<' || src[len(src)-1] != '>' || strings.Count(src, "@") != 1 { + return MailAddress(""), fmt.Errorf("Ill-formatted e-mail address: %s", src) + } + return MailAddress(src[1 : len(src)-1]), nil +} From 3127bd4ed806d0a77f4210d390c6ce2ef5e8073b Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Mon, 14 Jul 2014 13:55:41 +0200 Subject: [PATCH 02/34] Refactor. --- address.go | 15 +++ cmd/smtpd/main.go | 4 +- protocol.go | 236 +++++++++++++++++++++++++++++++++++ smtpd.go | 309 ++++++++++++++++------------------------------ 4 files changed, 358 insertions(+), 206 deletions(-) create mode 100644 address.go create mode 100644 protocol.go diff --git a/address.go b/address.go new file mode 100644 index 0000000..b663537 --- /dev/null +++ b/address.go @@ -0,0 +1,15 @@ +package smtpd + +import ( + "strings" + "fmt" +) + +type MailAddress string + +func parseMailAddress(src string) (MailAddress, error) { + if src[0] != '<' || src[len(src)-1] != '>' || strings.Count(src, "@") != 1 { + return MailAddress(""), fmt.Errorf("Ill-formatted e-mail address: %s", src) + } + return MailAddress(src[1 : len(src)-1]), nil +} diff --git a/cmd/smtpd/main.go b/cmd/smtpd/main.go index a39068f..350f6f3 100644 --- a/cmd/smtpd/main.go +++ b/cmd/smtpd/main.go @@ -8,7 +8,7 @@ import ( ) func dumpMessage(peer smtpd.Peer, env smtpd.Envelope) error { - log.Printf("New mail from: %s", env.MailFrom) + log.Printf("New mail from: %s", env.Sender) return nil } @@ -32,8 +32,6 @@ func main() { } server := &smtpd.Server{ - Addr: "127.0.0.1:10025", - WelcomeMessage: "localhost ESMTP ready.", Handler: dumpMessage, TLSConfig: tlsConfig, ForceTLS: true, diff --git a/protocol.go b/protocol.go new file mode 100644 index 0000000..a647d9f --- /dev/null +++ b/protocol.go @@ -0,0 +1,236 @@ +package smtpd + +import ( + "fmt" + "strings" + "crypto/tls" + "bufio" + "log" + "bytes" +) + +type command struct { + line string + action string + fields []string + params []string +} + +func parseLine(line string) (cmd command) { + + cmd.line = line + cmd.fields = strings.Fields(line) + cmd.action = strings.ToUpper(cmd.fields[0]) + + if len(cmd.fields) > 1 { + cmd.params = strings.Split(cmd.fields[1], ":") + } + + return + +} + +func (session *session) handleHELO(cmd command) { + + if len(cmd.fields) < 2 { + session.reply(502, "Missing parameter") + return + } + + session.peer.HeloName = cmd.fields[1] + + if session.server.HeloChecker != nil { + err := session.server.HeloChecker(session.peer) + if err != nil { + session.error(err) + session.close() + return + } + } + + session.reply(250, "Go ahead") + + return + +} + +func (session *session) handleEHLO(cmd command) { + + if len(cmd.fields) < 2 { + session.reply(502, "Missing parameter") + return + } + + session.peer.HeloName = cmd.fields[1] + + if session.server.HeloChecker != nil { + err := session.server.HeloChecker(session.peer) + if err != nil { + session.error(err) + session.close() + return + } + } + + extensions := session.extensions() + + if len(extensions) > 1 { + for _, ext := range extensions[:len(extensions)-1] { + fmt.Fprintf(session.writer, "250-%s\r\n", ext) + } + } + + session.reply(250, extensions[len(extensions)-1]) + + return + +} + +func (session *session) handleMAIL(cmd command) { + + if session.peer.HeloName == "" { + session.reply(502, "Please introduce yourself first.") + return + } + + addr, err := parseMailAddress(cmd.params[1]) + + if err != nil { + session.reply(502, "Ill-formatted e-mail address") + return + } + + if session.server.SenderChecker != nil { + err = session.server.SenderChecker(session.peer, addr) + session.error(err) + return + } + + session.envelope = &Envelope{ + Sender: addr, + } + + session.reply(250, "Go ahead") + + return + +} + +func (session *session) handleRCPT(cmd command) { + + if session.envelope == nil { + session.reply(502, "Missing MAIL FROM command.") + return + } + + addr, err := parseMailAddress(cmd.params[1]) + + if err != nil { + session.reply(502, "Ill-formatted e-mail address") + return + } + + if session.server.RecipientChecker != nil { + err = session.server.RecipientChecker(session.peer, addr) + session.error(err) + return + } + + session.envelope.Recipients = append(session.envelope.Recipients, addr) + + session.reply(250, "Go ahead") + + return + +} + + +func (session *session) handleSTARTTLS(cmd command) { + + if session.tls { + session.reply(502, "Already running in TLS") + return + } + + if session.server.TLSConfig == nil { + session.reply(502, "TLS not supported") + return + } + + tls_conn := tls.Server(session.conn, session.server.TLSConfig) + session.reply(250, "Go ahead") + + if err := tls_conn.Handshake(); err != nil { + log.Printf("TLS Handshake error:", err) + session.reply(550, "Handshake error") + return + } + + session.conn = tls_conn + session.reader = bufio.NewReader(tls_conn) + session.writer = bufio.NewWriter(tls_conn) + session.scanner = bufio.NewScanner(session.reader) + session.tls = true + + return + +} + +func (session *session) handleDATA(cmd command) { + + if session.envelope == nil || len(session.envelope.Recipients) == 0 { + session.reply(502, "Missing RCPT TO command.") + return + } + + session.reply(250, "Go ahead. End your data with .") + + data := &bytes.Buffer{} + done := false + + for session.scanner.Scan() { + + line := session.scanner.Text() + + if line == "." { + done = true + break + } + + data.Write([]byte(line)) + data.Write([]byte("\r\n")) + + } + + if !done { + return + } + + session.envelope.Data = data.Bytes() + + err := session.deliver() + + if err != nil { + session.error(err) + } else { + session.reply(200, "Thank you.") + } + +} + +func (session *session) handleRSET(cmd command) { + session.envelope = nil + session.reply(250, "Go ahead") + return +} + +func (session *session) handleNOOP(cmd command) { + session.reply(250, "Go ahead") + return +} + +func (session *session) handleQUIT(cmd command) { + session.reply(250, "OK, bye") + session.close() + return +} diff --git a/smtpd.go b/smtpd.go index 73994e7..da6ee80 100644 --- a/smtpd.go +++ b/smtpd.go @@ -1,28 +1,38 @@ +// Package smtpd implements a SMTP server with support for STARTTLS, authentication and restrictions on the different stages of the SMTP session. package smtpd import ( "bufio" - "bytes" "crypto/tls" "fmt" "log" "net" - "strings" "time" + "os" ) type Server struct { - Addr string // Address to listen on - WelcomeMessage string // Initial server banner + + Addr string // Address to listen on when using ListenAndServe (default: "127.0.0.1:10025") + WelcomeMessage string // Initial server banner (default: " ESMTP ready.") ReadTimeout time.Duration // Socket timeout for read operations (default: 60s) WriteTimeout time.Duration // Socket timeout for write operations (default: 60s) // New e-mails are handed off to this function. - // If an error is returned, it will be reported in the SMTP session + // Can be left empty for a NOOP server. + // If an error is returned, it will be reported in the SMTP session. Handler func(peer Peer, env Envelope) error - // Enable PLAIN/LOGIN authentication + // Enable various checks during the SMTP session. + // Can be left empty for no restrictions. + // If an error is returned, it will be reported in the SMTP session. + HeloChecker func(peer Peer) error // Called after HELO/EHLO. + SenderChecker func(peer Peer, addr MailAddress) error // Called after MAIL FROM. + RecipientChecker func(peer Peer, addr MailAddress) error // Called after each RCPT TO. + + // Enable PLAIN/LOGIN authentication, only available after STARTTLS. + // Can be left empty for no authentication support. Authenticator func(peer Peer, username, password string) error TLSConfig *tls.Config // Enable STARTTLS support @@ -31,42 +41,36 @@ type Server struct { MaxMessageSize int // Max message size in bytes (default: 10240000) } -type sessionState int - -const ( - _STATE_HELO sessionState = iota - _STATE_AUTH - _STATE_MAIL - _STATE_RCPT - _STATE_DATA -) - -type session struct { - server *Server - conn net.Conn - reader *bufio.Reader - writer *bufio.Writer - peer Peer - state sessionState - tls bool -} - type Peer struct { HeloName string // Server name used in HELO/EHLO command - UserName string // Username from authentication + Username string // Username from authentication + Password string // Password from authentication Addr net.Addr // Network address } -type MailAddress string - type Envelope struct { - MailFrom MailAddress + Sender MailAddress Recipients []MailAddress Data []byte - Peer *Peer } -func (srv *Server) newConnection(c net.Conn) (s *session, err error) { +type session struct { + + server *Server + + peer Peer + envelope *Envelope + + conn net.Conn + + reader *bufio.Reader + writer *bufio.Writer + scanner *bufio.Scanner + + tls bool +} + +func (srv *Server) newSession(c net.Conn) (s *session, err error) { log.Printf("New connection from: %s", c.RemoteAddr()) @@ -77,16 +81,22 @@ func (srv *Server) newConnection(c net.Conn) (s *session, err error) { writer: bufio.NewWriter(c), peer: Peer{Addr: c.RemoteAddr()}, } + + s.scanner = bufio.NewScanner(s.reader) return s, nil } func (srv *Server) ListenAndServe() error { + + srv.configureDefaults() + l, err := net.Listen("tcp", srv.Addr) if err != nil { return err } + log.Printf("Listening on: %s", srv.Addr) return srv.Serve(l) } @@ -107,13 +117,11 @@ func (srv *Server) Serve(l net.Listener) error { return e } - session, err := srv.newConnection(conn) + session, err := srv.newSession(conn) if err != nil { continue } - session.state = _STATE_HELO - go session.serve() } @@ -138,181 +146,79 @@ func (srv *Server) configureDefaults() { log.Fatal("Cannot use ForceTLS with no TLSConfig") } + if srv.Addr == "" { + srv.Addr = "127.0.0.1:10025" + } + + if srv.WelcomeMessage == "" { + + hostname, err := os.Hostname() + + if err != nil { + log.Fatal("Couldn't determine hostname: %s", err) + } + + srv.WelcomeMessage = fmt.Sprintf("%s ESMTP ready.", hostname) + + } + } func (session *session) serve() { log.Print("Serving") - defer func() { - session.writer.Flush() - session.conn.Close() - }() + defer session.close() session.reply(220, session.server.WelcomeMessage) - scanner := bufio.NewScanner(session.reader) + for session.scanner.Scan() { - var env Envelope - var data *bytes.Buffer + line := session.scanner.Text() + cmd := parseLine(line) - for scanner.Scan() { - - line := scanner.Text() - command := "" - fields := []string{} - params := []string{} - - if session.state != _STATE_DATA { - fields = strings.Fields(line) - command = strings.ToUpper(fields[0]) - if len(fields) > 1 { - params = strings.Split(fields[1], ":") - } - } - - log.Printf("Line: %s, fields: %#v, params: %#v", line, fields, params) - - if command == "QUIT" { - session.reply(250, "Ok, bye") - return - } - - switch session.state { - - case _STATE_HELO: - - if command == "HELO" || command == "EHLO" { - if len(fields) < 2 { - session.reply(502, "Missing parameter") - continue - } else { - session.peer.HeloName = fields[1] - } - } else { - session.reply(502, "Command not recognized, expected HELO/EHLO") - continue - } - - if command == "EHLO" { - session.WriteExtensions() - } else { - session.reply(250, "Go ahead") - } - - if session.server.Authenticator == nil { - session.state = _STATE_MAIL - } else { - session.state = _STATE_AUTH - } + switch cmd.action { + case "HELO": + session.handleHELO(cmd) continue - case _STATE_MAIL: - - if !session.tls && command == "STARTTLS" && session.server.TLSConfig != nil { - - tls_conn := tls.Server(session.conn, session.server.TLSConfig) - session.reply(250, "Go ahead") - - if err := tls_conn.Handshake(); err != nil { - log.Printf("TLS Handshake error:", err) - session.reply(550, "Handshake error") - continue - } - - session.conn = tls_conn - - session.reader = bufio.NewReader(tls_conn) - session.writer = bufio.NewWriter(tls_conn) - - scanner = bufio.NewScanner(session.reader) - - session.tls = true - session.state = _STATE_HELO - - continue - - } - - if !session.tls && session.server.ForceTLS { - session.reply(550, "Must run STARTTLS first") - continue - } - - if command == "MAIL" && strings.ToUpper(params[0]) == "FROM" { - - addr, err := parseMailAddress(params[1]) - - if err != nil { - session.reply(502, "Ill-formatted e-mail address") - continue - } - - env = Envelope{ - Peer: &session.peer, - MailFrom: addr, - } - - session.reply(250, "Go ahead") - session.state = _STATE_RCPT - continue - - } else { - session.reply(502, "Command not recognized, expected MAIL FROM") - continue - } - - case _STATE_RCPT: - - if command == "RCPT" && strings.ToUpper(params[0]) == "TO" { - - addr, err := parseMailAddress(params[1]) - - if err != nil { - session.reply(502, "Ill-formatted e-mail address") - continue - } - - env.Recipients = append(env.Recipients, addr) - - session.reply(250, "Go ahead") - continue - - } else if command == "DATA" && len(env.Recipients) > 0 { - session.reply(250, "Go ahead. End your data with .") - data = &bytes.Buffer{} - session.state = _STATE_DATA - continue - } - - if len(env.Recipients) == 0 { - session.reply(502, "Command not recognized, expected RCPT") - } else { - session.reply(502, "Command not recognized, expected RCPT or DATA") - } - + case "EHLO": + session.handleEHLO(cmd) continue - case _STATE_DATA: + case "MAIL": + session.handleMAIL(cmd) + continue - if line == "." { - env.Data = data.Bytes() - data.Reset() - err := session.handle(env) + case "RCPT": + session.handleRCPT(cmd) + continue - if err != nil { - session.reply(502, fmt.Sprintf("%s", err)) - } else { - session.reply(200, "Thank you.") - } + case "STARTTLS": + session.handleSTARTTLS(cmd) + continue - session.state = _STATE_MAIL - continue - } + case "DATA": + session.handleDATA(cmd) + continue + + case "RSET": + session.handleRSET(cmd) + continue + + case "NOOP": + session.handleNOOP(cmd) + continue + + case "QUIT": + session.handleQUIT(cmd) + continue } + session.reply(502, "Unsupported command.") + } } @@ -328,7 +234,12 @@ func (session *session) reply(code int, message string) { } -func (session *session) WriteExtensions() { +func (session *session) error(err error) { + session.reply(502, fmt.Sprintf("%s", err)) +} + + +func (session *session) extensions() []string { extensions := []string{ "SIZE 10240000", @@ -342,27 +253,19 @@ func (session *session) WriteExtensions() { extensions = append(extensions, "AUTH PLAIN LOGIN") } - if len(extensions) > 1 { - for _, ext := range extensions[:len(extensions)-1] { - fmt.Fprintf(session.writer, "250-%s\r\n", ext) - } - } - - session.reply(250, extensions[len(extensions)-1]) + return extensions } -func (session *session) handle(env Envelope) error { +func (session *session) deliver() error { if session.server.Handler != nil { - return session.server.Handler(session.peer, env) + return session.server.Handler(session.peer, *session.envelope) } else { return nil } } -func parseMailAddress(src string) (MailAddress, error) { - if src[0] != '<' || src[len(src)-1] != '>' || strings.Count(src, "@") != 1 { - return MailAddress(""), fmt.Errorf("Ill-formatted e-mail address: %s", src) - } - return MailAddress(src[1 : len(src)-1]), nil +func (session *session) close() { + session.writer.Flush() + session.conn.Close() } From 1b8b0c4863dffdb0ef245ec6e331e475cae53ff7 Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Mon, 14 Jul 2014 13:59:30 +0200 Subject: [PATCH 03/34] Fix. --- cmd/smtpd/main.go | 6 ++++++ smtpd.go | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/smtpd/main.go b/cmd/smtpd/main.go index 350f6f3..a0a511b 100644 --- a/cmd/smtpd/main.go +++ b/cmd/smtpd/main.go @@ -7,6 +7,11 @@ import ( "log" ) +func authenticate(peer smtpd.Peer, username, password string) error { + log.Printf("Auth: %s / %s", username, password) + return nil +} + func dumpMessage(peer smtpd.Peer, env smtpd.Envelope) error { log.Printf("New mail from: %s", env.Sender) return nil @@ -33,6 +38,7 @@ func main() { server := &smtpd.Server{ Handler: dumpMessage, + Authenticator: authenticate, TLSConfig: tlsConfig, ForceTLS: true, } diff --git a/smtpd.go b/smtpd.go index da6ee80..1314186 100644 --- a/smtpd.go +++ b/smtpd.go @@ -249,7 +249,7 @@ func (session *session) extensions() []string { extensions = append(extensions, "STARTTLS") } - if session.tls { + if session.server.Authenticator != nil && session.tls { extensions = append(extensions, "AUTH PLAIN LOGIN") } From ab4df6d73e260b6c777372d65562c9bb1934dcde Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Mon, 14 Jul 2014 14:20:08 +0200 Subject: [PATCH 04/34] Authentication support. --- protocol.go | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++ smtpd.go | 4 +++ 2 files changed, 96 insertions(+) diff --git a/protocol.go b/protocol.go index a647d9f..f70ae18 100644 --- a/protocol.go +++ b/protocol.go @@ -7,6 +7,7 @@ import ( "bufio" "log" "bytes" + "encoding/base64" ) type command struct { @@ -234,3 +235,94 @@ func (session *session) handleQUIT(cmd command) { session.close() return } + +func (session *session) handleAUTH(cmd command) { + + mechanism := strings.ToUpper(cmd.fields[1]) + + username := "" + password := "" + + switch mechanism { + + case "PLAIN": + + auth := "" + + if len(cmd.fields) < 3 { + session.reply(334, "Give me your credentials") + if !session.scanner.Scan() { + return + } + auth = session.scanner.Text() + } else { + auth = cmd.fields[2] + } + + data, err := base64.StdEncoding.DecodeString(auth) + + if err != nil { + session.reply(502, "Couldn't decode your credentials") + return + } + + parts := bytes.Split(data, []byte{0}) + + if len(parts) != 3 { + session.reply(502, "Couldn't decode your credentials") + return + } + + username = string(parts[0]) + password = string(parts[2]) + + case "LOGIN": + + session.reply(334, "VXNlcm5hbWU6") + + if !session.scanner.Scan() { + return + } + + byte_username, err := base64.StdEncoding.DecodeString(session.scanner.Text()) + + if err != nil { + session.reply(502, "Couldn't decode your credentials") + return + } + + session.reply(334, "UGFzc3dvcmQ6") + + if !session.scanner.Scan() { + return + } + + byte_password, err := base64.StdEncoding.DecodeString(session.scanner.Text()) + + if err != nil { + session.reply(502, "Couldn't decode your credentials") + return + } + + username = string(byte_username) + password = string(byte_password) + + default: + + session.reply(502, "Unknown authentication mechanism") + return + + } + + err := session.server.Authenticator(session.peer, username, password) + if err != nil { + session.error(err) + return + } + + session.peer.Username = username + session.peer.Password = password + + session.reply(250, "OK, you are now authenticated") + +} diff --git a/smtpd.go b/smtpd.go index 1314186..aec7f18 100644 --- a/smtpd.go +++ b/smtpd.go @@ -215,6 +215,10 @@ func (session *session) serve() { session.handleQUIT(cmd) continue + case "AUTH": + session.handleAUTH(cmd) + continue + } session.reply(502, "Unsupported command.") From dd3db75d5c6930a96e200de52a75284f299ce135 Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Mon, 14 Jul 2014 14:20:36 +0200 Subject: [PATCH 05/34] go fmt. --- address.go | 2 +- cmd/smtpd/main.go | 8 ++++---- protocol.go | 11 +++++------ smtpd.go | 29 +++++++++++++---------------- 4 files changed, 23 insertions(+), 27 deletions(-) diff --git a/address.go b/address.go index b663537..08c5046 100644 --- a/address.go +++ b/address.go @@ -1,8 +1,8 @@ package smtpd import ( - "strings" "fmt" + "strings" ) type MailAddress string diff --git a/cmd/smtpd/main.go b/cmd/smtpd/main.go index a0a511b..506b4ac 100644 --- a/cmd/smtpd/main.go +++ b/cmd/smtpd/main.go @@ -37,10 +37,10 @@ func main() { } server := &smtpd.Server{ - Handler: dumpMessage, - Authenticator: authenticate, - TLSConfig: tlsConfig, - ForceTLS: true, + Handler: dumpMessage, + Authenticator: authenticate, + TLSConfig: tlsConfig, + ForceTLS: true, } server.ListenAndServe() diff --git a/protocol.go b/protocol.go index f70ae18..0c4ae2d 100644 --- a/protocol.go +++ b/protocol.go @@ -1,13 +1,13 @@ package smtpd import ( - "fmt" - "strings" - "crypto/tls" "bufio" - "log" "bytes" + "crypto/tls" "encoding/base64" + "fmt" + "log" + "strings" ) type command struct { @@ -145,7 +145,6 @@ func (session *session) handleRCPT(cmd command) { } - func (session *session) handleSTARTTLS(cmd command) { if session.tls { @@ -279,7 +278,7 @@ func (session *session) handleAUTH(cmd command) { case "LOGIN": session.reply(334, "VXNlcm5hbWU6") - + if !session.scanner.Scan() { return } diff --git a/smtpd.go b/smtpd.go index aec7f18..3de37cc 100644 --- a/smtpd.go +++ b/smtpd.go @@ -7,13 +7,12 @@ import ( "fmt" "log" "net" - "time" "os" + "time" ) type Server struct { - - Addr string // Address to listen on when using ListenAndServe (default: "127.0.0.1:10025") + Addr string // Address to listen on when using ListenAndServe (default: "127.0.0.1:10025") WelcomeMessage string // Initial server banner (default: " ESMTP ready.") ReadTimeout time.Duration // Socket timeout for read operations (default: 60s) @@ -27,8 +26,8 @@ type Server struct { // Enable various checks during the SMTP session. // Can be left empty for no restrictions. // If an error is returned, it will be reported in the SMTP session. - HeloChecker func(peer Peer) error // Called after HELO/EHLO. - SenderChecker func(peer Peer, addr MailAddress) error // Called after MAIL FROM. + HeloChecker func(peer Peer) error // Called after HELO/EHLO. + SenderChecker func(peer Peer, addr MailAddress) error // Called after MAIL FROM. RecipientChecker func(peer Peer, addr MailAddress) error // Called after each RCPT TO. // Enable PLAIN/LOGIN authentication, only available after STARTTLS. @@ -42,9 +41,9 @@ type Server struct { } type Peer struct { - HeloName string // Server name used in HELO/EHLO command - Username string // Username from authentication - Password string // Password from authentication + HeloName string // Server name used in HELO/EHLO command + Username string // Username from authentication + Password string // Password from authentication Addr net.Addr // Network address } @@ -55,19 +54,18 @@ type Envelope struct { } type session struct { - server *Server - peer Peer + peer Peer envelope *Envelope - conn net.Conn + conn net.Conn - reader *bufio.Reader - writer *bufio.Writer + reader *bufio.Reader + writer *bufio.Writer scanner *bufio.Scanner - tls bool + tls bool } func (srv *Server) newSession(c net.Conn) (s *session, err error) { @@ -81,7 +79,7 @@ func (srv *Server) newSession(c net.Conn) (s *session, err error) { writer: bufio.NewWriter(c), peer: Peer{Addr: c.RemoteAddr()}, } - + s.scanner = bufio.NewScanner(s.reader) return s, nil @@ -242,7 +240,6 @@ func (session *session) error(err error) { session.reply(502, fmt.Sprintf("%s", err)) } - func (session *session) extensions() []string { extensions := []string{ From 4bb7e21f409d3ed54e987189b87c374c0181b97b Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Mon, 14 Jul 2014 14:51:31 +0200 Subject: [PATCH 06/34] Fixes. --- address.go | 1 + protocol.go | 31 ++++++++++++++++++++++--------- smtpd.go | 14 +++++++++----- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/address.go b/address.go index 08c5046..1ffce35 100644 --- a/address.go +++ b/address.go @@ -5,6 +5,7 @@ import ( "strings" ) +// MailAddress holds an e-mail address type MailAddress string func parseMailAddress(src string) (MailAddress, error) { diff --git a/protocol.go b/protocol.go index 0c4ae2d..4c07fad 100644 --- a/protocol.go +++ b/protocol.go @@ -94,6 +94,11 @@ func (session *session) handleMAIL(cmd command) { return } + if !session.tls && session.server.ForceTLS { + session.reply(502, "Please turn on TLS by issuing a STARTTLS command.") + return + } + addr, err := parseMailAddress(cmd.params[1]) if err != nil { @@ -157,18 +162,18 @@ func (session *session) handleSTARTTLS(cmd command) { return } - tls_conn := tls.Server(session.conn, session.server.TLSConfig) + tlsConn := tls.Server(session.conn, session.server.TLSConfig) session.reply(250, "Go ahead") - if err := tls_conn.Handshake(); err != nil { + if err := tlsConn.Handshake(); err != nil { log.Printf("TLS Handshake error:", err) session.reply(550, "Handshake error") return } - session.conn = tls_conn - session.reader = bufio.NewReader(tls_conn) - session.writer = bufio.NewWriter(tls_conn) + session.conn = tlsConn + session.reader = bufio.NewReader(tlsConn) + session.writer = bufio.NewWriter(tlsConn) session.scanner = bufio.NewScanner(session.reader) session.tls = true @@ -206,6 +211,14 @@ func (session *session) handleDATA(cmd command) { return } + if data.Len() > session.server.MaxMessageSize { + session.reply(550, fmt.Sprintf( + "Message exceeded max message size of %d bytes", + session.server.MaxMessageSize, + )) + return + } + session.envelope.Data = data.Bytes() err := session.deliver() @@ -283,7 +296,7 @@ func (session *session) handleAUTH(cmd command) { return } - byte_username, err := base64.StdEncoding.DecodeString(session.scanner.Text()) + byteUsername, err := base64.StdEncoding.DecodeString(session.scanner.Text()) if err != nil { session.reply(502, "Couldn't decode your credentials") @@ -296,15 +309,15 @@ func (session *session) handleAUTH(cmd command) { return } - byte_password, err := base64.StdEncoding.DecodeString(session.scanner.Text()) + bytePassword, err := base64.StdEncoding.DecodeString(session.scanner.Text()) if err != nil { session.reply(502, "Couldn't decode your credentials") return } - username = string(byte_username) - password = string(byte_password) + username = string(byteUsername) + password = string(bytePassword) default: diff --git a/smtpd.go b/smtpd.go index 3de37cc..c2b0194 100644 --- a/smtpd.go +++ b/smtpd.go @@ -11,6 +11,7 @@ import ( "time" ) +// Server defines the parameters for running the SMTP server type Server struct { Addr string // Address to listen on when using ListenAndServe (default: "127.0.0.1:10025") WelcomeMessage string // Initial server banner (default: " ESMTP ready.") @@ -40,13 +41,15 @@ type Server struct { MaxMessageSize int // Max message size in bytes (default: 10240000) } +// Peer represents the client connecting to the server type Peer struct { HeloName string // Server name used in HELO/EHLO command - Username string // Username from authentication - Password string // Password from authentication + Username string // Username from authentication, if authenticated + Password string // Password from authentication, if authenticated Addr net.Addr // Network address } +// Envelope holds a message type Envelope struct { Sender MailAddress Recipients []MailAddress @@ -86,6 +89,7 @@ func (srv *Server) newSession(c net.Conn) (s *session, err error) { } +// ListenAndServe starts the SMTP server and listens on the address provided in Server.Addr func (srv *Server) ListenAndServe() error { srv.configureDefaults() @@ -98,6 +102,7 @@ func (srv *Server) ListenAndServe() error { return srv.Serve(l) } +// Serve starts the SMTP server and listens on the Listener provided func (srv *Server) Serve(l net.Listener) error { srv.configureDefaults() @@ -243,7 +248,7 @@ func (session *session) error(err error) { func (session *session) extensions() []string { extensions := []string{ - "SIZE 10240000", + fmt.Sprintf("SIZE %d", session.server.MaxMessageSize), } if session.server.TLSConfig != nil && !session.tls { @@ -261,9 +266,8 @@ func (session *session) extensions() []string { func (session *session) deliver() error { if session.server.Handler != nil { return session.server.Handler(session.peer, *session.envelope) - } else { - return nil } + return nil } func (session *session) close() { From be033068608df109ac85f74ff70e686a32e3d645 Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Mon, 14 Jul 2014 19:44:10 +0200 Subject: [PATCH 07/34] Test cases, fixes. --- protocol.go | 74 +++++++++++++++++-- smtpd.go | 58 +-------------- smtpd_test.go | 195 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 265 insertions(+), 62 deletions(-) create mode 100644 smtpd_test.go diff --git a/protocol.go b/protocol.go index 4c07fad..348b6e7 100644 --- a/protocol.go +++ b/protocol.go @@ -6,7 +6,6 @@ import ( "crypto/tls" "encoding/base64" "fmt" - "log" "strings" ) @@ -31,6 +30,58 @@ func parseLine(line string) (cmd command) { } +func (session *session) handle(line string) { + + cmd := parseLine(line) + + switch cmd.action { + + case "HELO": + session.handleHELO(cmd) + return + + case "EHLO": + session.handleEHLO(cmd) + return + + case "MAIL": + session.handleMAIL(cmd) + return + + case "RCPT": + session.handleRCPT(cmd) + return + + case "STARTTLS": + session.handleSTARTTLS(cmd) + return + + case "DATA": + session.handleDATA(cmd) + return + + case "RSET": + session.handleRSET(cmd) + return + + case "NOOP": + session.handleNOOP(cmd) + return + + case "QUIT": + session.handleQUIT(cmd) + return + + case "AUTH": + session.handleAUTH(cmd) + return + + } + + session.reply(502, "Unsupported command.") + +} + func (session *session) handleHELO(cmd command) { if len(cmd.fields) < 2 { @@ -163,10 +214,9 @@ func (session *session) handleSTARTTLS(cmd command) { } tlsConn := tls.Server(session.conn, session.server.TLSConfig) - session.reply(250, "Go ahead") + session.reply(220, "Go ahead") if err := tlsConn.Handshake(); err != nil { - log.Printf("TLS Handshake error:", err) session.reply(550, "Handshake error") return } @@ -188,7 +238,7 @@ func (session *session) handleDATA(cmd command) { return } - session.reply(250, "Go ahead. End your data with .") + session.reply(354, "Go ahead. End your data with .") data := &bytes.Buffer{} done := false @@ -226,7 +276,7 @@ func (session *session) handleDATA(cmd command) { if err != nil { session.error(err) } else { - session.reply(200, "Thank you.") + session.reply(250, "Thank you.") } } @@ -243,13 +293,23 @@ func (session *session) handleNOOP(cmd command) { } func (session *session) handleQUIT(cmd command) { - session.reply(250, "OK, bye") + session.reply(221, "OK, bye") session.close() return } func (session *session) handleAUTH(cmd command) { + if session.peer.HeloName == "" { + session.reply(502, "Please introduce yourself first.") + return + } + + if !session.tls { + session.reply(502, "Cannot AUTH in plain text mode. Use STARTTLS.") + return + } + mechanism := strings.ToUpper(cmd.fields[1]) username := "" @@ -335,6 +395,6 @@ func (session *session) handleAUTH(cmd command) { session.peer.Username = username session.peer.Password = password - session.reply(250, "OK, you are now authenticated") + session.reply(235, "OK, you are now authenticated") } diff --git a/smtpd.go b/smtpd.go index c2b0194..6d8fed3 100644 --- a/smtpd.go +++ b/smtpd.go @@ -73,8 +73,6 @@ type session struct { func (srv *Server) newSession(c net.Conn) (s *session, err error) { - log.Printf("New connection from: %s", c.RemoteAddr()) - s = &session{ server: srv, conn: c, @@ -98,7 +96,7 @@ func (srv *Server) ListenAndServe() error { if err != nil { return err } - log.Printf("Listening on: %s", srv.Addr) + return srv.Serve(l) } @@ -169,63 +167,12 @@ func (srv *Server) configureDefaults() { func (session *session) serve() { - log.Print("Serving") - defer session.close() session.reply(220, session.server.WelcomeMessage) for session.scanner.Scan() { - - line := session.scanner.Text() - cmd := parseLine(line) - - switch cmd.action { - - case "HELO": - session.handleHELO(cmd) - continue - - case "EHLO": - session.handleEHLO(cmd) - continue - - case "MAIL": - session.handleMAIL(cmd) - continue - - case "RCPT": - session.handleRCPT(cmd) - continue - - case "STARTTLS": - session.handleSTARTTLS(cmd) - continue - - case "DATA": - session.handleDATA(cmd) - continue - - case "RSET": - session.handleRSET(cmd) - continue - - case "NOOP": - session.handleNOOP(cmd) - continue - - case "QUIT": - session.handleQUIT(cmd) - continue - - case "AUTH": - session.handleAUTH(cmd) - continue - - } - - session.reply(502, "Unsupported command.") - + session.handle(session.scanner.Text()) } } @@ -249,6 +196,7 @@ func (session *session) extensions() []string { extensions := []string{ fmt.Sprintf("SIZE %d", session.server.MaxMessageSize), + "8BITMIME", } if session.server.TLSConfig != nil && !session.tls { diff --git a/smtpd_test.go b/smtpd_test.go new file mode 100644 index 0000000..16d036e --- /dev/null +++ b/smtpd_test.go @@ -0,0 +1,195 @@ +package smtpd + +import ( + "crypto/tls" + "fmt" + "net" + "net/smtp" + "strings" + "testing" +) + +var localhostCert = []byte(`-----BEGIN CERTIFICATE----- +MIIBkzCCAT+gAwIBAgIQf4LO8+QzcbXRHJUo6MvX7zALBgkqhkiG9w0BAQswEjEQ +MA4GA1UEChMHQWNtZSBDbzAeFw03MDAxMDEwMDAwMDBaFw04MTA1MjkxNjAwMDBa +MBIxEDAOBgNVBAoTB0FjbWUgQ28wXDANBgkqhkiG9w0BAQEFAANLADBIAkEAx2Uj +2nl0ESnMMrdUOwQnpnIPQzQBX9MIYT87VxhHzImOukWcq5DrmN1ZB//diyrgiCLv +D0udX3YXNHMn1Ki8awIDAQABo3MwcTAOBgNVHQ8BAf8EBAMCAKQwEwYDVR0lBAww +CgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB/zA5BgNVHREEMjAwggtleGFtcGxl +LmNvbYIJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMAsGCSqGSIb3 +DQEBCwNBAGcaB2Il0TIXFcJOdOLGPa6F8qZH1ZHBtVlCBnaJn4vZJGzID+V36Gn0 +hA1AYfGAaF0c43oQofvv+XqQlTe4a+M= +-----END CERTIFICATE-----`) + +var localhostKey = []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIBPAIBAAJBAMdlI9p5dBEpzDK3VDsEJ6ZyD0M0AV/TCGE/O1cYR8yJjrpFnKuQ +65jdWQf/3Ysq4Igi7w9LnV92FzRzJ9SovGsCAwEAAQJAVaFw2VWJbAmIQUuMJ+Ar +6wZW2aSO5okpsyHFqSyrQQIcAj/QOq8P83F8J10IreFWNlBlywJU9c7IlJtn/lqq +AQIhAOxHXOxrKPxqTIdIcNnWye/HRQ+5VD54QQr1+M77+bEBAiEA2AmsNNqj2fKj +j2xk+4vnBSY0vrb4q/O3WZ46oorawWsCIQDWdpfzx/i11E6OZMR6FinJSNh4w0Gi +SkjPiCBE0BX+AQIhAI/TiLk7YmBkQG3ovSYW0vvDntPlXpKj08ovJFw4U0D3AiEA +lGjGna4oaauI0CWI6pG0wg4zklTnrDWK7w9h/S/T4e0= +-----END RSA PRIVATE KEY-----`) + +func TestSMTP(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + server := &Server{} + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if supported, _ := c.Extension("AUTH"); supported { + t.Fatal("AUTH supported before TLS") + } + + if supported, _ := c.Extension("8BITMIME"); !supported { + t.Fatal("8BITMIME not supported") + } + + if supported, _ := c.Extension("STARTTLS"); supported { + t.Fatal("STARTTLS supported") + } + + if err := c.Mail("sender@example.org"); err != nil { + t.Fatalf("Mail failed: %v", err) + } + + if err := c.Rcpt("recipient@example.net"); err != nil { + t.Fatalf("Rcpt failed: %v", err) + } + + if err := c.Rcpt("recipient2@example.net"); err != nil { + t.Fatalf("Rcpt2 failed: %v", err) + } + + wc, err := c.Data() + if err != nil { + t.Fatalf("Data failed: %v", err) + } + + _, err = fmt.Fprintf(wc, "This is the email body") + if err != nil { + t.Fatalf("Data body failed: %v", err) + } + + err = wc.Close() + if err != nil { + t.Fatalf("Data close failed: %v", err) + } + + if err := c.Reset(); err != nil { + t.Fatalf("Reset failed: %v", err) + } + + if err := c.Verify("foobar@example.net"); err == nil { + t.Fatal("Unexpected support for VRFY") + } + + if err := c.Quit(); err != nil { + t.Fatalf("Quit failed: %v", err) + } +} + +func TestSTARTTLS(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + cert, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + t.Fatalf("Cert load failed: %v", err) + } + + server := &Server{ + Authenticator: func(peer Peer, username, password string) error { return nil }, + TLSConfig: &tls.Config{ + Certificates: []tls.Certificate{cert}, + }, + ForceTLS: true, + } + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if supported, _ := c.Extension("AUTH"); supported { + t.Fatal("AUTH supported before TLS") + } + + if err := c.Mail("sender@example.org"); err == nil { + t.Fatal("Mail workded before TLS with ForceTLS") + } + + if err := c.StartTLS(&tls.Config{InsecureSkipVerify: true}); err != nil { + t.Fatalf("STARTTLS failed: %v", err) + } + + if supported, _ := c.Extension("AUTH"); !supported { + t.Fatal("AUTH not supported after TLS") + } + + if _, mechs := c.Extension("AUTH"); !strings.Contains(mechs, "PLAIN") { + t.Fatal("PLAIN AUTH not supported after TLS") + } + + if _, mechs := c.Extension("AUTH"); !strings.Contains(mechs, "LOGIN") { + t.Fatal("LOGIN AUTH not supported after TLS") + } + + if err := c.Auth(smtp.PlainAuth("foo", "foo", "bar", "127.0.0.1")); err != nil { + t.Fatalf("Auth failed: %v", err) + } + + if err := c.Mail("sender@example.org"); err != nil { + t.Fatalf("Mail failed: %v", err) + } + + if err := c.Rcpt("recipient@example.net"); err != nil { + t.Fatalf("Rcpt failed: %v", err) + } + + if err := c.Rcpt("recipient2@example.net"); err != nil { + t.Fatalf("Rcpt2 failed: %v", err) + } + + wc, err := c.Data() + if err != nil { + t.Fatalf("Data failed: %v", err) + } + + _, err = fmt.Fprintf(wc, "This is the email body") + if err != nil { + t.Fatalf("Data body failed: %v", err) + } + + err = wc.Close() + if err != nil { + t.Fatalf("Data close failed: %v", err) + } + + if err := c.Quit(); err != nil { + t.Fatalf("Quit failed: %v", err) + } +} From b78de3a03e54d925311fc3fa7490cbb1a367cecd Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Mon, 14 Jul 2014 20:06:32 +0200 Subject: [PATCH 08/34] Extra tests. --- protocol.go | 2 - smtpd_test.go | 205 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+), 2 deletions(-) diff --git a/protocol.go b/protocol.go index 348b6e7..370a811 100644 --- a/protocol.go +++ b/protocol.go @@ -95,7 +95,6 @@ func (session *session) handleHELO(cmd command) { err := session.server.HeloChecker(session.peer) if err != nil { session.error(err) - session.close() return } } @@ -119,7 +118,6 @@ func (session *session) handleEHLO(cmd command) { err := session.server.HeloChecker(session.peer) if err != nil { session.error(err) - session.close() return } } diff --git a/smtpd_test.go b/smtpd_test.go index 16d036e..51b664f 100644 --- a/smtpd_test.go +++ b/smtpd_test.go @@ -2,6 +2,7 @@ package smtpd import ( "crypto/tls" + "errors" "fmt" "net" "net/smtp" @@ -193,3 +194,207 @@ func TestSTARTTLS(t *testing.T) { t.Fatalf("Quit failed: %v", err) } } + +func TestHELOCheck(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + server := &Server{ + HeloChecker: func(peer Peer) error { return errors.New("Denied") }, + } + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if err := c.Hello("localhost"); err == nil { + t.Fatal("Unexpected HELO success") + } + +} + +func TestSenderCheck(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + server := &Server{ + SenderChecker: func(peer Peer, addr MailAddress) error { return errors.New("Denied") }, + } + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if err := c.Mail("sender@example.org"); err == nil { + t.Fatal("Unexpected MAIL success") + } + +} + +func TestRecipientCheck(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + server := &Server{ + RecipientChecker: func(peer Peer, addr MailAddress) error { return errors.New("Denied") }, + } + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if err := c.Mail("sender@example.org"); err != nil { + t.Fatalf("Mail failed: %v", err) + } + + if err := c.Rcpt("recipient@example.net"); err == nil { + t.Fatal("Unexpected RCPT success") + } + +} + +func TestMaxMessageSize(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + server := &Server{ + MaxMessageSize: 5, + } + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if err := c.Mail("sender@example.org"); err != nil { + t.Fatalf("MAIL failed: %v", err) + } + + if err := c.Rcpt("recipient@example.net"); err != nil { + t.Fatalf("RCPT failed: %v", err) + } + + wc, err := c.Data() + if err != nil { + t.Fatalf("Data failed: %v", err) + } + + _, err = fmt.Fprintf(wc, "This is the email body") + if err != nil { + t.Fatalf("Data body failed: %v", err) + } + + err = wc.Close() + if err == nil { + t.Fatal("Allowed message larger than 5 bytes to pass.") + } + + if err := c.Quit(); err != nil { + t.Fatalf("QUIT failed: %v", err) + } + +} + +func TestHandler(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + server := &Server{ + Handler: func(peer Peer, env Envelope) error { + if env.Sender != "sender@example.org" { + t.Fatalf("Unknown sender: %v", env.Sender) + } + if len(env.Recipients) != 1 { + t.Fatalf("Too many recipients: %d", len(env.Recipients)) + } + if env.Recipients[0] != "recipient@example.net" { + t.Fatalf("Unknown recipient: %v", env.Recipients[0]) + } + if string(env.Data) != "This is the email body\r\n" { + t.Fatalf("Wrong message body: %v", env.Data) + } + return nil + }, + } + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if err := c.Mail("sender@example.org"); err != nil { + t.Fatalf("MAIL failed: %v", err) + } + + if err := c.Rcpt("recipient@example.net"); err != nil { + t.Fatalf("RCPT failed: %v", err) + } + + wc, err := c.Data() + if err != nil { + t.Fatalf("Data failed: %v", err) + } + + _, err = fmt.Fprintf(wc, "This is the email body") + if err != nil { + t.Fatalf("Data body failed: %v", err) + } + + err = wc.Close() + if err != nil { + t.Fatalf("Data close failed: %v", err) + } + + if err := c.Quit(); err != nil { + t.Fatalf("QUIT failed: %v", err) + } + +} From 587b6ad4acdb7acda294c5aa0eb86baaa80e44c6 Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Mon, 14 Jul 2014 20:44:14 +0200 Subject: [PATCH 09/34] Examples. --- example_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ protocol.go | 2 +- smtpd.go | 2 +- 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 example_test.go diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..4e784d3 --- /dev/null +++ b/example_test.go @@ -0,0 +1,48 @@ +package smtpd + +import ( + "bitbucket.org/chrj/smtpd" + "errors" + "net" + "net/smtp" + "strings" +) + +func ExampleServer() { + + // No-op server. Accepts and discards + server := &smtpd.Server{} + server.serve() + + // Relay server. Accepts only from single IP address and forwards using the Gmail smtp + server := &smtpd.Server{ + + Addr: "0.0.0.0:10025", + + HeloChecker: func(peer smtpd.Peer) error { + if !strings.HasPrefix(peer.Addr.String(), "42.42.42.42:") { + return errors.New("Denied") + } + return nil + }, + + Handler: func(peer smtpd.Peer, env smtpd.Envelope) error { + return smtp.SendMail( + "smtp.gmail.com:587", + smtp.PlainAuth( + "", + "username@gmail.com", + "password", + "smtp.gmail.com", + ), + env.Sender, + env.Recipients, + env.Data, + ) + }, + + } + + server.serve() + +} diff --git a/protocol.go b/protocol.go index 370a811..359aa98 100644 --- a/protocol.go +++ b/protocol.go @@ -343,7 +343,7 @@ func (session *session) handleAUTH(cmd command) { return } - username = string(parts[0]) + username = string(parts[1]) password = string(parts[2]) case "LOGIN": diff --git a/smtpd.go b/smtpd.go index 6d8fed3..ef463df 100644 --- a/smtpd.go +++ b/smtpd.go @@ -1,4 +1,4 @@ -// Package smtpd implements a SMTP server with support for STARTTLS, authentication and restrictions on the different stages of the SMTP session. +// Package smtpd implements a SMTP server with support for STARTTLS, authentication (PLAIN/LOGIN) and optional restrictions on the different stages of the SMTP session. package smtpd import ( From 64d201aecb0408269ab9d95ae0f4247cbd29b148 Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Tue, 15 Jul 2014 10:07:58 +0200 Subject: [PATCH 10/34] Implemented MaxConnections, corrected examples, removed MailAddress. --- address.go | 9 +++------ example_test.go | 20 ++++++++++---------- protocol.go | 4 ++-- smtpd.go | 44 +++++++++++++++++++++++++++++++++++++------- smtpd_test.go | 34 ++++++++++++++++++++++++++++++++-- 5 files changed, 84 insertions(+), 27 deletions(-) diff --git a/address.go b/address.go index 1ffce35..76b7507 100644 --- a/address.go +++ b/address.go @@ -5,12 +5,9 @@ import ( "strings" ) -// MailAddress holds an e-mail address -type MailAddress string - -func parseMailAddress(src string) (MailAddress, error) { +func parseAddress(src string) (string, error) { if src[0] != '<' || src[len(src)-1] != '>' || strings.Count(src, "@") != 1 { - return MailAddress(""), fmt.Errorf("Ill-formatted e-mail address: %s", src) + return "", fmt.Errorf("Ill-formatted e-mail address: %s", src) } - return MailAddress(src[1 : len(src)-1]), nil + return src[1 : len(src)-1], nil } diff --git a/example_test.go b/example_test.go index 4e784d3..ea2c23e 100644 --- a/example_test.go +++ b/example_test.go @@ -1,32 +1,32 @@ package smtpd import ( - "bitbucket.org/chrj/smtpd" "errors" - "net" "net/smtp" "strings" ) func ExampleServer() { + var server *Server + // No-op server. Accepts and discards - server := &smtpd.Server{} - server.serve() + server = &Server{} + server.ListenAndServe() // Relay server. Accepts only from single IP address and forwards using the Gmail smtp - server := &smtpd.Server{ + server = &Server{ Addr: "0.0.0.0:10025", - HeloChecker: func(peer smtpd.Peer) error { + HeloChecker: func(peer Peer) error { if !strings.HasPrefix(peer.Addr.String(), "42.42.42.42:") { return errors.New("Denied") } return nil }, - Handler: func(peer smtpd.Peer, env smtpd.Envelope) error { + Handler: func(peer Peer, env Envelope) error { return smtp.SendMail( "smtp.gmail.com:587", smtp.PlainAuth( @@ -35,14 +35,14 @@ func ExampleServer() { "password", "smtp.gmail.com", ), - env.Sender, - env.Recipients, + string(env.Sender), + []string(env.Recipients), env.Data, ) }, } - server.serve() + server.ListenAndServe() } diff --git a/protocol.go b/protocol.go index 359aa98..1b6304c 100644 --- a/protocol.go +++ b/protocol.go @@ -148,7 +148,7 @@ func (session *session) handleMAIL(cmd command) { return } - addr, err := parseMailAddress(cmd.params[1]) + addr, err := parseAddress(cmd.params[1]) if err != nil { session.reply(502, "Ill-formatted e-mail address") @@ -178,7 +178,7 @@ func (session *session) handleRCPT(cmd command) { return } - addr, err := parseMailAddress(cmd.params[1]) + addr, err := parseAddress(cmd.params[1]) if err != nil { session.reply(502, "Ill-formatted e-mail address") diff --git a/smtpd.go b/smtpd.go index ef463df..85fcdc2 100644 --- a/smtpd.go +++ b/smtpd.go @@ -19,6 +19,9 @@ type Server struct { ReadTimeout time.Duration // Socket timeout for read operations (default: 60s) WriteTimeout time.Duration // Socket timeout for write operations (default: 60s) + MaxMessageSize int // Max message size in bytes (default: 10240000) + MaxConnections int // Max concurrent connections, use -1 to disable (default: 100) + // New e-mails are handed off to this function. // Can be left empty for a NOOP server. // If an error is returned, it will be reported in the SMTP session. @@ -28,8 +31,8 @@ type Server struct { // Can be left empty for no restrictions. // If an error is returned, it will be reported in the SMTP session. HeloChecker func(peer Peer) error // Called after HELO/EHLO. - SenderChecker func(peer Peer, addr MailAddress) error // Called after MAIL FROM. - RecipientChecker func(peer Peer, addr MailAddress) error // Called after each RCPT TO. + SenderChecker func(peer Peer, addr string) error // Called after MAIL FROM. + RecipientChecker func(peer Peer, addr string) error // Called after each RCPT TO. // Enable PLAIN/LOGIN authentication, only available after STARTTLS. // Can be left empty for no authentication support. @@ -37,8 +40,6 @@ type Server struct { TLSConfig *tls.Config // Enable STARTTLS support ForceTLS bool // Force STARTTLS usage - - MaxMessageSize int // Max message size in bytes (default: 10240000) } // Peer represents the client connecting to the server @@ -51,8 +52,8 @@ type Peer struct { // Envelope holds a message type Envelope struct { - Sender MailAddress - Recipients []MailAddress + Sender string + Recipients []string Data []byte } @@ -107,6 +108,14 @@ func (srv *Server) Serve(l net.Listener) error { defer l.Close() + var limiter chan struct{} + + if srv.MaxConnections > 0 { + limiter = make(chan struct{}, srv.MaxConnections) + } else { + limiter = nil + } + for { conn, e := l.Accept() @@ -123,7 +132,19 @@ func (srv *Server) Serve(l net.Listener) error { continue } - go session.serve() + if limiter != nil { + go func() { + select { + case limiter <- struct{}{}: + session.serve() + <-limiter + default: + session.reject() + } + }() + } else { + go session.serve() + } } @@ -135,6 +156,10 @@ func (srv *Server) configureDefaults() { srv.MaxMessageSize = 10240000 } + if srv.MaxConnections == 0 { + srv.MaxConnections = 100 + } + if srv.ReadTimeout == 0 { srv.ReadTimeout = time.Second * 60 } @@ -177,6 +202,11 @@ func (session *session) serve() { } +func (session *session) reject() { + session.reply(450, "Too busy. Try again later.") + session.close() +} + func (session *session) reply(code int, message string) { fmt.Fprintf(session.writer, "%d %s\r\n", code, message) diff --git a/smtpd_test.go b/smtpd_test.go index 51b664f..ccc80d5 100644 --- a/smtpd_test.go +++ b/smtpd_test.go @@ -233,7 +233,7 @@ func TestSenderCheck(t *testing.T) { defer ln.Close() server := &Server{ - SenderChecker: func(peer Peer, addr MailAddress) error { return errors.New("Denied") }, + SenderChecker: func(peer Peer, addr string) error { return errors.New("Denied") }, } go func() { @@ -261,7 +261,7 @@ func TestRecipientCheck(t *testing.T) { defer ln.Close() server := &Server{ - RecipientChecker: func(peer Peer, addr MailAddress) error { return errors.New("Denied") }, + RecipientChecker: func(peer Peer, addr string) error { return errors.New("Denied") }, } go func() { @@ -398,3 +398,33 @@ func TestHandler(t *testing.T) { } } + +func TestMaxConnections(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + server := &Server{ + MaxConnections: 1, + } + + go func() { + server.Serve(ln) + }() + + c1, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + _, err = smtp.Dial(ln.Addr().String()) + if err == nil { + t.Fatal("Dial succeeded despite MaxConnections = 1") + } + + c1.Close() +} From 333ada9388c3c2de9f3c58bb5a5ae60c6cf4d0fd Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Tue, 15 Jul 2014 10:09:34 +0200 Subject: [PATCH 11/34] go fmt. --- example_test.go | 9 ++++----- smtpd.go | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/example_test.go b/example_test.go index ea2c23e..ac49a86 100644 --- a/example_test.go +++ b/example_test.go @@ -28,11 +28,11 @@ func ExampleServer() { Handler: func(peer Peer, env Envelope) error { return smtp.SendMail( - "smtp.gmail.com:587", + "smtp.gmail.com:587", smtp.PlainAuth( - "", - "username@gmail.com", - "password", + "", + "username@gmail.com", + "password", "smtp.gmail.com", ), string(env.Sender), @@ -40,7 +40,6 @@ func ExampleServer() { env.Data, ) }, - } server.ListenAndServe() diff --git a/smtpd.go b/smtpd.go index 85fcdc2..e70078b 100644 --- a/smtpd.go +++ b/smtpd.go @@ -30,7 +30,7 @@ type Server struct { // Enable various checks during the SMTP session. // Can be left empty for no restrictions. // If an error is returned, it will be reported in the SMTP session. - HeloChecker func(peer Peer) error // Called after HELO/EHLO. + HeloChecker func(peer Peer) error // Called after HELO/EHLO. SenderChecker func(peer Peer, addr string) error // Called after MAIL FROM. RecipientChecker func(peer Peer, addr string) error // Called after each RCPT TO. From cda2908ec8ec2961fb8ba6837ac5b585700366b9 Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Tue, 15 Jul 2014 10:11:37 +0200 Subject: [PATCH 12/34] newSession cannot fail. --- smtpd.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/smtpd.go b/smtpd.go index e70078b..200cc21 100644 --- a/smtpd.go +++ b/smtpd.go @@ -72,7 +72,7 @@ type session struct { tls bool } -func (srv *Server) newSession(c net.Conn) (s *session, err error) { +func (srv *Server) newSession(c net.Conn) (s *session) { s = &session{ server: srv, @@ -84,7 +84,7 @@ func (srv *Server) newSession(c net.Conn) (s *session, err error) { s.scanner = bufio.NewScanner(s.reader) - return s, nil + return } @@ -127,10 +127,7 @@ func (srv *Server) Serve(l net.Listener) error { return e } - session, err := srv.newSession(conn) - if err != nil { - continue - } + session := srv.newSession(conn) if limiter != nil { go func() { From b2f59a653eb020676f755b2bd7ca1e18c790cab9 Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Tue, 15 Jul 2014 11:16:34 +0200 Subject: [PATCH 13/34] Fixes. --- smtpd.go | 56 +++++++++++++++++++++++++++++++++++++++------------ smtpd_test.go | 38 ++++++++++++++++++++++++++++++---- 2 files changed, 77 insertions(+), 17 deletions(-) diff --git a/smtpd.go b/smtpd.go index 200cc21..f002e48 100644 --- a/smtpd.go +++ b/smtpd.go @@ -13,14 +13,14 @@ import ( // Server defines the parameters for running the SMTP server type Server struct { - Addr string // Address to listen on when using ListenAndServe (default: "127.0.0.1:10025") - WelcomeMessage string // Initial server banner (default: " ESMTP ready.") + Addr string // Address to listen on when using ListenAndServe. (default: "127.0.0.1:10025") + WelcomeMessage string // Initial server banner. (default: " ESMTP ready.") - ReadTimeout time.Duration // Socket timeout for read operations (default: 60s) - WriteTimeout time.Duration // Socket timeout for write operations (default: 60s) + ReadTimeout time.Duration // Socket timeout for read operations. (default: 60s) + WriteTimeout time.Duration // Socket timeout for write operations. (default: 60s) - MaxMessageSize int // Max message size in bytes (default: 10240000) - MaxConnections int // Max concurrent connections, use -1 to disable (default: 100) + MaxMessageSize int // Max message size in bytes. (default: 10240000) + MaxConnections int // Max concurrent connections, use -1 to disable. (default: 100) // New e-mails are handed off to this function. // Can be left empty for a NOOP server. @@ -30,16 +30,18 @@ type Server struct { // Enable various checks during the SMTP session. // Can be left empty for no restrictions. // If an error is returned, it will be reported in the SMTP session. - HeloChecker func(peer Peer) error // Called after HELO/EHLO. - SenderChecker func(peer Peer, addr string) error // Called after MAIL FROM. - RecipientChecker func(peer Peer, addr string) error // Called after each RCPT TO. + // Use the Error struct for access to error codes. + ConnectionChecker func(peer Peer) error // Called upon new connection. + HeloChecker func(peer Peer) error // Called after HELO/EHLO. + SenderChecker func(peer Peer, addr string) error // Called after MAIL FROM. + RecipientChecker func(peer Peer, addr string) error // Called after each RCPT TO. // Enable PLAIN/LOGIN authentication, only available after STARTTLS. // Can be left empty for no authentication support. Authenticator func(peer Peer, username, password string) error - TLSConfig *tls.Config // Enable STARTTLS support - ForceTLS bool // Force STARTTLS usage + TLSConfig *tls.Config // Enable STARTTLS support. + ForceTLS bool // Force STARTTLS usage. } // Peer represents the client connecting to the server @@ -57,6 +59,15 @@ type Envelope struct { Data []byte } +// Error represents an Error reported in the SMTP session. +type Error struct { + Code int // The integer error code + Message string // The error message +} + +// Error returns a string representation of the SMTP error +func (e Error) Error() string { return fmt.Sprintf("%d %s", e.Code, e.Message) } + type session struct { server *Server @@ -191,7 +202,7 @@ func (session *session) serve() { defer session.close() - session.reply(220, session.server.WelcomeMessage) + session.welcome() for session.scanner.Scan() { session.handle(session.scanner.Text()) @@ -204,6 +215,21 @@ func (session *session) reject() { session.close() } +func (session *session) welcome() { + + if session.server.ConnectionChecker != nil { + err := session.server.ConnectionChecker(session.peer) + if err != nil { + session.error(err) + session.close() + return + } + } + + session.reply(220, session.server.WelcomeMessage) + +} + func (session *session) reply(code int, message string) { fmt.Fprintf(session.writer, "%d %s\r\n", code, message) @@ -216,7 +242,11 @@ func (session *session) reply(code int, message string) { } func (session *session) error(err error) { - session.reply(502, fmt.Sprintf("%s", err)) + if smtpdError, ok := err.(Error); ok { + session.reply(smtpdError.Code, smtpdError.Message) + } else { + session.reply(502, fmt.Sprintf("%s", err)) + } } func (session *session) extensions() []string { diff --git a/smtpd_test.go b/smtpd_test.go index ccc80d5..64d35d4 100644 --- a/smtpd_test.go +++ b/smtpd_test.go @@ -2,7 +2,6 @@ package smtpd import ( "crypto/tls" - "errors" "fmt" "net" "net/smtp" @@ -195,6 +194,31 @@ func TestSTARTTLS(t *testing.T) { } } +func TestConnectionCheck(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + server := &Server{ + ConnectionChecker: func(peer Peer) error { + return Error{Code: 552, Message: "Denied"} + }, + } + + go func() { + server.Serve(ln) + }() + + if _, err := smtp.Dial(ln.Addr().String()); err == nil { + t.Fatal("Dial succeeded despite ConnectionCheck") + } + +} + func TestHELOCheck(t *testing.T) { ln, err := net.Listen("tcp", "127.0.0.1:0") @@ -205,7 +229,9 @@ func TestHELOCheck(t *testing.T) { defer ln.Close() server := &Server{ - HeloChecker: func(peer Peer) error { return errors.New("Denied") }, + HeloChecker: func(peer Peer) error { + return Error{Code: 552, Message: "Denied"} + }, } go func() { @@ -233,7 +259,9 @@ func TestSenderCheck(t *testing.T) { defer ln.Close() server := &Server{ - SenderChecker: func(peer Peer, addr string) error { return errors.New("Denied") }, + SenderChecker: func(peer Peer, addr string) error { + return Error{Code: 552, Message: "Denied"} + }, } go func() { @@ -261,7 +289,9 @@ func TestRecipientCheck(t *testing.T) { defer ln.Close() server := &Server{ - RecipientChecker: func(peer Peer, addr string) error { return errors.New("Denied") }, + RecipientChecker: func(peer Peer, addr string) error { + return Error{Code: 552, Message: "Denied"} + }, } go func() { From 98573fb190577b385d3ff5157de2620a63d97f61 Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Tue, 15 Jul 2014 12:37:25 +0200 Subject: [PATCH 14/34] Cleanup, added license. --- LICENSE | 20 ++++++++++++++++++ cmd/smtpd/main.go | 50 --------------------------------------------- cmd/smtpd/smtpd.crt | 20 ------------------ cmd/smtpd/smtpd.csr | 17 --------------- cmd/smtpd/smtpd.key | 27 ------------------------ example_test.go | 4 ++-- smtpd.go | 1 + 7 files changed, 23 insertions(+), 116 deletions(-) create mode 100644 LICENSE delete mode 100644 cmd/smtpd/main.go delete mode 100644 cmd/smtpd/smtpd.crt delete mode 100644 cmd/smtpd/smtpd.csr delete mode 100644 cmd/smtpd/smtpd.key diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f8c34e2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2014 Christian Joergensen (christian@technobabble.dk) + +MIT License + +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. \ No newline at end of file diff --git a/cmd/smtpd/main.go b/cmd/smtpd/main.go deleted file mode 100644 index 506b4ac..0000000 --- a/cmd/smtpd/main.go +++ /dev/null @@ -1,50 +0,0 @@ -package main - -import ( - "bitbucket.org/chrj/smtpd" - "crypto/tls" - "flag" - "log" -) - -func authenticate(peer smtpd.Peer, username, password string) error { - log.Printf("Auth: %s / %s", username, password) - return nil -} - -func dumpMessage(peer smtpd.Peer, env smtpd.Envelope) error { - log.Printf("New mail from: %s", env.Sender) - return nil -} - -var tlsCert = flag.String("tlscert", "", "TLS: Certificate file") -var tlsKey = flag.String("tlskey", "", "TLS: Private key") - -func main() { - - flag.Parse() - - var tlsConfig *tls.Config - - if *tlsCert != "" { - cert, err := tls.LoadX509KeyPair(*tlsCert, *tlsKey) - if err != nil { - log.Fatal("certificate error:", err) - } - tlsConfig = &tls.Config{ - Certificates: []tls.Certificate{cert}, - } - } - - server := &smtpd.Server{ - Handler: dumpMessage, - Authenticator: authenticate, - TLSConfig: tlsConfig, - ForceTLS: true, - } - - server.ListenAndServe() - - return - -} diff --git a/cmd/smtpd/smtpd.crt b/cmd/smtpd/smtpd.crt deleted file mode 100644 index 7897738..0000000 --- a/cmd/smtpd/smtpd.crt +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDLjCCAhYCCQD7wib+be6ipjANBgkqhkiG9w0BAQUFADBZMQswCQYDVQQGEwJE -SzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 -cyBQdHkgTHRkMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTQwNzEzMjA0MTE2WhcN -MTUwNzEzMjA0MTE2WjBZMQswCQYDVQQGEwJESzETMBEGA1UECAwKU29tZS1TdGF0 -ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDDAls -b2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDK41RNmjLD -NVs3ZOX1IpCfWITMZ8kx0TB9BXh86XhgaH47DNoOnSeDvawGfmKXYF7ISuFRacbc -C1xeiN+hah0CAJQJXpzYO8dpyXrPVIiZ/mKFRAnz/Kp/PApDjkpJ13VnLkuZLbJg -dQ0dtsb2BW+T/jEHDpyCOwR2g1AdlnsjuP+V1WxZvCKYvv5awv5AWwmCbKGjA1Jv -8j54WZzK7bFxp19Eyg2WVXhf7ZB+zs8RbliYzUqgT7GnUEBQkofaxb5j+n/PR7AU -/U1dFSVM7i1mn58SjsDx5v6GIh/Z3ekdEKbBiJJSvyhPV6K1b7IOWo9tQGeEMQP4 -tvkLDPPgPXOZAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAAniCpu2zPujExDMp36l -3VKtMZBbbOn8rwAcGOUjeSTZT62VQJX4CSsXJGuSHLV9fKPO8K3pob9mZ/CGL3Xj -JnLKDMgAQEiLq9IZPZg0/vYJjP96Hlgf0sOT6Q4dX36kDvGsWKJZilPEOKFvZh+R -acwWmN8bEGhFThijvTfY7sxEnTem1R2qs5cqCRfc4vCammTCRpLSWcD4p/WVZc5K -MCv8N2/JDg9plaBiQZyzaaiXI4X90IZQlWzIT6E2+i3V6bRwLioTituxu1r6Pwx9 -lniOQrA1+5waqottyMWQGHzmrFFg93HDX0WmP4IXHkXWhAcR611DLIw3NQuqt7Q4 -ecM= ------END CERTIFICATE----- diff --git a/cmd/smtpd/smtpd.csr b/cmd/smtpd/smtpd.csr deleted file mode 100644 index 054ac6f..0000000 --- a/cmd/smtpd/smtpd.csr +++ /dev/null @@ -1,17 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIICnjCCAYYCAQAwWTELMAkGA1UEBhMCREsxEzARBgNVBAgMClNvbWUtU3RhdGUx -ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9j -YWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyuNUTZoywzVb -N2Tl9SKQn1iEzGfJMdEwfQV4fOl4YGh+OwzaDp0ng72sBn5il2BeyErhUWnG3Atc -XojfoWodAgCUCV6c2DvHacl6z1SImf5ihUQJ8/yqfzwKQ45KSdd1Zy5LmS2yYHUN -HbbG9gVvk/4xBw6cgjsEdoNQHZZ7I7j/ldVsWbwimL7+WsL+QFsJgmyhowNSb/I+ -eFmcyu2xcadfRMoNllV4X+2Qfs7PEW5YmM1KoE+xp1BAUJKH2sW+Y/p/z0ewFP1N -XRUlTO4tZp+fEo7A8eb+hiIf2d3pHRCmwYiSUr8oT1eitW+yDlqPbUBnhDED+Lb5 -Cwzz4D1zmQIDAQABoAAwDQYJKoZIhvcNAQEFBQADggEBALkJ6moQnDeT91Y37nQP -pXmcbiL/bj34v3MnUYArmxtZcfMJ3B9qxe5/0psq4r6hjxPWNaW92NkkE1aJZwuO -cAqGWcPBVFH309siq5J0NGkjArdtd84NBewoBZVqpcqwrfVAI6adINlF2dGLeeJW -SAlVEKCt3SLz3X+lVgKIzZTEsMuYmTaUrr490ecDWsh2eey0pbhtSqXkPkQOVUla -8QqysE5DuaES8ysTIuAh28uIxWmLXIWnVqia2+eltEgiuaiAZVH3CYH136/FTEL1 -a5toCmQFWv9rAc+EfVxIh1CgUNsWx5ARPVuSRZaBjH4qXwIg8V138eC482MNtMfM -fDs= ------END CERTIFICATE REQUEST----- diff --git a/cmd/smtpd/smtpd.key b/cmd/smtpd/smtpd.key deleted file mode 100644 index 7428b4f..0000000 --- a/cmd/smtpd/smtpd.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAyuNUTZoywzVbN2Tl9SKQn1iEzGfJMdEwfQV4fOl4YGh+Owza -Dp0ng72sBn5il2BeyErhUWnG3AtcXojfoWodAgCUCV6c2DvHacl6z1SImf5ihUQJ -8/yqfzwKQ45KSdd1Zy5LmS2yYHUNHbbG9gVvk/4xBw6cgjsEdoNQHZZ7I7j/ldVs -WbwimL7+WsL+QFsJgmyhowNSb/I+eFmcyu2xcadfRMoNllV4X+2Qfs7PEW5YmM1K -oE+xp1BAUJKH2sW+Y/p/z0ewFP1NXRUlTO4tZp+fEo7A8eb+hiIf2d3pHRCmwYiS -Ur8oT1eitW+yDlqPbUBnhDED+Lb5Cwzz4D1zmQIDAQABAoIBAQCoterncP8fRqIo -aRW0B18dsj0TwIYUj/BzNfZgYMCB4sJ9Fg3JszMloLaI29XeLPwEMAg3a+86EZRo -5AaaMiQXAyYWuH9SbDtBo5IlEBVbgKaqTM69/fBFR0b9sDfkOW9eMqgYo2A+R3d1 -qwS9lf2Xoftg8+x/etYWOtGHGRgitflirlW3uLvgCo/gP5gcb+HbtQNVRyKnqR1n -hNUeDCxTMLuhkvS2NxMUcAkuNYSLRiM3bXtER5RfatPHgvmFEmtKoB3TbQsw4Z6e -E+xlgFEnTVLkPoblhQjcUpiDqaZCfRUxmFvgfB+/0zrCZh3TtmMxSnUm1uFcNyul -dBICKx2lAoGBAP1e8GZKJ2hB5MIIG8TyZvmv1EnNfgAAhSbhWWXufoKflEAIBihg -7NBQAuHdq76e+G1F8GJzsHtNZquQhrIwo0U0/eBycrLBkQgIAeZJEZx7EH3EsqL3 -7RuJIaOQBy5LBnnnxjwlcNQvS7FiZheqEsN7RYScGE1RFAREj86B54bLAoGBAMz+ -SUpCTXHzpgOLhN6KBTmgr0fk4SKVLSdyFjNJbd7bokPoy2aO8IkBKn/jWrxNOZij -5XU0NryYuMq1dsJViZ1kRzF8Q3xw1IjKOUeWBp1221FrA+nouinIYNdtoNmIOLXO -1IOF0jInLqjBHC0MdaZDaupEJ0ZbFV+8EQCka/6rAoGANPkSffBnCM8uCrszQxwD -F5UBZ2TFQS7ap+RZkowoexruHe0PjIWnPW5dC+gSrkoCWqZSueLCNSVbn+cZoku0 -9xU7Nx/2hxUdQ3aZHxKL0hGQwxrK1nPLaQRkuhO0zKL2+anRsmWJj3NL+gw+mBgA -0EoHoNAZ7KBU9Qd4oY5bX70CgYBCdWJPZ+VxvxsgZRgjib2d7EFHXqW6r4BfHHak -E/dB3BTkTVG8IzVKRY2AvrXI/IRivygB8naYeC7Y0TH6WP7vfvYxzeaXLoFJA77E -PZhRbpo18Crpp6DLMQJsdUdDnw07rB1rsnPt/JP88/ZtiG+QAqVj48qT3a21RuSA -P84fVwKBgQCFdUzNpwsDVZ0L51yk7D9LwsA9jwzBxc5Jtd8CIDVylAlj1BM7hkiG -durZfNVtkhi+RXgD3SjZXWtCCprvrrjl8T52+deOCx2qM/5qhtJRKIHEkqndx4e5 -lmt3J5alekerwijR/F8+qnrrEsvtp6rozMDCNSGa6ir4HWYQUJ2C4g== ------END RSA PRIVATE KEY----- diff --git a/example_test.go b/example_test.go index ac49a86..1c0fbef 100644 --- a/example_test.go +++ b/example_test.go @@ -35,8 +35,8 @@ func ExampleServer() { "password", "smtp.gmail.com", ), - string(env.Sender), - []string(env.Recipients), + env.Sender, + env.Recipients, env.Data, ) }, diff --git a/smtpd.go b/smtpd.go index f002e48..f654ea9 100644 --- a/smtpd.go +++ b/smtpd.go @@ -277,5 +277,6 @@ func (session *session) deliver() error { func (session *session) close() { session.writer.Flush() + time.Sleep(200 * time.Millisecond) session.conn.Close() } From f32411bd90fee795c4f7f09b5986ff0c110eda24 Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Tue, 15 Jul 2014 13:07:47 +0200 Subject: [PATCH 15/34] Extra test cases. --- smtpd_test.go | 379 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 379 insertions(+) diff --git a/smtpd_test.go b/smtpd_test.go index 64d35d4..37cc108 100644 --- a/smtpd_test.go +++ b/smtpd_test.go @@ -2,11 +2,13 @@ package smtpd import ( "crypto/tls" + "errors" "fmt" "net" "net/smtp" "strings" "testing" + "time" ) var localhostCert = []byte(`-----BEGIN CERTIFICATE----- @@ -51,6 +53,10 @@ func TestSMTP(t *testing.T) { t.Fatalf("Dial failed: %v", err) } + if err := c.Hello("localhost"); err != nil { + t.Fatalf("HELO failed: %v", err) + } + if supported, _ := c.Extension("AUTH"); supported { t.Fatal("AUTH supported before TLS") } @@ -103,6 +109,36 @@ func TestSMTP(t *testing.T) { } } +func TestListenAndServe(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + addr := ln.Addr().String() + + ln.Close() + + server := &Server{Addr: addr} + + go func() { + server.ListenAndServe() + }() + + time.Sleep(100 * time.Millisecond) + + c, err := smtp.Dial(addr) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if err := c.Quit(); err != nil { + t.Fatalf("Quit failed: %v", err) + } + +} + func TestSTARTTLS(t *testing.T) { ln, err := net.Listen("tcp", "127.0.0.1:0") @@ -146,6 +182,10 @@ func TestSTARTTLS(t *testing.T) { t.Fatalf("STARTTLS failed: %v", err) } + if err := c.StartTLS(&tls.Config{InsecureSkipVerify: true}); err == nil { + t.Fatal("STARTTLS worked twice") + } + if supported, _ := c.Extension("AUTH"); !supported { t.Fatal("AUTH not supported after TLS") } @@ -194,6 +234,49 @@ func TestSTARTTLS(t *testing.T) { } } +func TestAuthRejection(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + cert, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + t.Fatalf("Cert load failed: %v", err) + } + + server := &Server{ + Authenticator: func(peer Peer, username, password string) error { + return Error{Code: 550, Message: "Denied"} + }, + TLSConfig: &tls.Config{ + Certificates: []tls.Certificate{cert}, + }, + ForceTLS: true, + } + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if err := c.StartTLS(&tls.Config{InsecureSkipVerify: true}); err != nil { + t.Fatalf("STARTTLS failed: %v", err) + } + + if err := c.Auth(smtp.PlainAuth("foo", "foo", "bar", "127.0.0.1")); err == nil { + t.Fatal("Auth worked despite rejection") + } + +} + func TestConnectionCheck(t *testing.T) { ln, err := net.Listen("tcp", "127.0.0.1:0") @@ -219,6 +302,31 @@ func TestConnectionCheck(t *testing.T) { } +func TestConnectionCheckSimpleError(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + server := &Server{ + ConnectionChecker: func(peer Peer) error { + return errors.New("Denied") + }, + } + + go func() { + server.Serve(ln) + }() + + if _, err := smtp.Dial(ln.Addr().String()); err == nil { + t.Fatal("Dial succeeded despite ConnectionCheck") + } + +} + func TestHELOCheck(t *testing.T) { ln, err := net.Listen("tcp", "127.0.0.1:0") @@ -429,6 +537,59 @@ func TestHandler(t *testing.T) { } +func TestRejectHandler(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + server := &Server{ + Handler: func(peer Peer, env Envelope) error { + return Error{Code: 550, Message: "Rejected"} + }, + } + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if err := c.Mail("sender@example.org"); err != nil { + t.Fatalf("MAIL failed: %v", err) + } + + if err := c.Rcpt("recipient@example.net"); err != nil { + t.Fatalf("RCPT failed: %v", err) + } + + wc, err := c.Data() + if err != nil { + t.Fatalf("Data failed: %v", err) + } + + _, err = fmt.Fprintf(wc, "This is the email body") + if err != nil { + t.Fatalf("Data body failed: %v", err) + } + + err = wc.Close() + if err == nil { + t.Fatal("Unexpected accept of data") + } + + if err := c.Quit(); err != nil { + t.Fatalf("QUIT failed: %v", err) + } + +} + func TestMaxConnections(t *testing.T) { ln, err := net.Listen("tcp", "127.0.0.1:0") @@ -458,3 +619,221 @@ func TestMaxConnections(t *testing.T) { c1.Close() } + +func TestNoMaxConnections(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + server := &Server{ + MaxConnections: -1, + } + + go func() { + server.Serve(ln) + }() + + c1, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + c1.Close() +} + +func TestInvalidHelo(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + server := &Server{} + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if err := c.Hello(""); err == nil { + t.Fatal("Unexpected HELO success") + } + +} + +func TestInvalidSender(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + server := &Server{} + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if err := c.Mail("invalid"); err == nil { + t.Fatal("Unexpected MAIL success") + } + +} + +func TestInvalidRecipient(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + server := &Server{} + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if err := c.Mail("sender@example.org"); err != nil { + t.Fatalf("Mail failed: %v", err) + } + + if err := c.Rcpt("invalid"); err == nil { + t.Fatal("Unexpected RCPT success") + } + +} + +func TestRCPTbeforeMAIL(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + server := &Server{} + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if err := c.Rcpt("recipient@example.net"); err == nil { + t.Fatal("Unexpected RCPT success") + } + +} + +func TestDATAbeforeRCPT(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + server := &Server{ + Handler: func(peer Peer, env Envelope) error { + return Error{Code: 550, Message: "Rejected"} + }, + } + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if err := c.Mail("sender@example.org"); err != nil { + t.Fatalf("MAIL failed: %v", err) + } + + if _, err := c.Data(); err == nil { + t.Fatal("Data accepted despite no recipients") + } + + if err := c.Quit(); err != nil { + t.Fatalf("QUIT failed: %v", err) + } + +} + +func TestInterruptedDATA(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + server := &Server{ + Handler: func(peer Peer, env Envelope) error { + t.Fatal("Accepted DATA despite disconnection") + return nil + }, + } + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if err := c.Mail("sender@example.org"); err != nil { + t.Fatalf("MAIL failed: %v", err) + } + + if err := c.Rcpt("recipient@example.net"); err != nil { + t.Fatalf("RCPT failed: %v", err) + } + + wc, err := c.Data() + if err != nil { + t.Fatalf("Data failed: %v", err) + } + + _, err = fmt.Fprintf(wc, "This is the email body") + if err != nil { + t.Fatalf("Data body failed: %v", err) + } + + c.Close() + +} From ca017c195555eb823f72b62ee3858ae42770c589 Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Tue, 15 Jul 2014 13:22:18 +0200 Subject: [PATCH 16/34] Typo, README. --- README.md | 1 + smtpd.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..75033b3 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +[![GoDoc](https://godoc.org/bitbucket.org/chrj/smtpd?status.png)](https://godoc.org/bitbucket.org/chrj/smtpd) diff --git a/smtpd.go b/smtpd.go index f654ea9..de6d39f 100644 --- a/smtpd.go +++ b/smtpd.go @@ -1,4 +1,4 @@ -// Package smtpd implements a SMTP server with support for STARTTLS, authentication (PLAIN/LOGIN) and optional restrictions on the different stages of the SMTP session. +// Package smtpd implements an SMTP server with support for STARTTLS, authentication (PLAIN/LOGIN) and optional restrictions on the different stages of the SMTP session. package smtpd import ( From 3bd281bb3cbbb10f27f129b2927945525f520c98 Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Tue, 15 Jul 2014 13:38:50 +0200 Subject: [PATCH 17/34] Use separate package for tests and example. --- example_test.go | 15 +++++----- smtpd_test.go | 79 +++++++++++++++++++++++++------------------------ 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/example_test.go b/example_test.go index 1c0fbef..f0c669d 100644 --- a/example_test.go +++ b/example_test.go @@ -1,32 +1,32 @@ -package smtpd +package smtpd_test import ( + "bitbucket.org/chrj/smtpd" "errors" "net/smtp" "strings" ) func ExampleServer() { - - var server *Server + var server *smtpd.Server // No-op server. Accepts and discards - server = &Server{} + server = &smtpd.Server{} server.ListenAndServe() // Relay server. Accepts only from single IP address and forwards using the Gmail smtp - server = &Server{ + server = &smtpd.Server{ Addr: "0.0.0.0:10025", - HeloChecker: func(peer Peer) error { + HeloChecker: func(peer smtpd.Peer) error { if !strings.HasPrefix(peer.Addr.String(), "42.42.42.42:") { return errors.New("Denied") } return nil }, - Handler: func(peer Peer, env Envelope) error { + Handler: func(peer smtpd.Peer, env smtpd.Envelope) error { return smtp.SendMail( "smtp.gmail.com:587", smtp.PlainAuth( @@ -43,5 +43,4 @@ func ExampleServer() { } server.ListenAndServe() - } diff --git a/smtpd_test.go b/smtpd_test.go index 37cc108..4998052 100644 --- a/smtpd_test.go +++ b/smtpd_test.go @@ -1,6 +1,7 @@ -package smtpd +package smtpd_test import ( + "bitbucket.org/chrj/smtpd" "crypto/tls" "errors" "fmt" @@ -42,7 +43,7 @@ func TestSMTP(t *testing.T) { defer ln.Close() - server := &Server{} + server := &smtpd.Server{} go func() { server.Serve(ln) @@ -120,7 +121,7 @@ func TestListenAndServe(t *testing.T) { ln.Close() - server := &Server{Addr: addr} + server := &smtpd.Server{Addr: addr} go func() { server.ListenAndServe() @@ -153,8 +154,8 @@ func TestSTARTTLS(t *testing.T) { t.Fatalf("Cert load failed: %v", err) } - server := &Server{ - Authenticator: func(peer Peer, username, password string) error { return nil }, + server := &smtpd.Server{ + Authenticator: func(peer smtpd.Peer, username, password string) error { return nil }, TLSConfig: &tls.Config{ Certificates: []tls.Certificate{cert}, }, @@ -248,9 +249,9 @@ func TestAuthRejection(t *testing.T) { t.Fatalf("Cert load failed: %v", err) } - server := &Server{ - Authenticator: func(peer Peer, username, password string) error { - return Error{Code: 550, Message: "Denied"} + server := &smtpd.Server{ + Authenticator: func(peer smtpd.Peer, username, password string) error { + return smtpd.Error{Code: 550, Message: "Denied"} }, TLSConfig: &tls.Config{ Certificates: []tls.Certificate{cert}, @@ -286,9 +287,9 @@ func TestConnectionCheck(t *testing.T) { defer ln.Close() - server := &Server{ - ConnectionChecker: func(peer Peer) error { - return Error{Code: 552, Message: "Denied"} + server := &smtpd.Server{ + ConnectionChecker: func(peer smtpd.Peer) error { + return smtpd.Error{Code: 552, Message: "Denied"} }, } @@ -311,8 +312,8 @@ func TestConnectionCheckSimpleError(t *testing.T) { defer ln.Close() - server := &Server{ - ConnectionChecker: func(peer Peer) error { + server := &smtpd.Server{ + ConnectionChecker: func(peer smtpd.Peer) error { return errors.New("Denied") }, } @@ -336,9 +337,9 @@ func TestHELOCheck(t *testing.T) { defer ln.Close() - server := &Server{ - HeloChecker: func(peer Peer) error { - return Error{Code: 552, Message: "Denied"} + server := &smtpd.Server{ + HeloChecker: func(peer smtpd.Peer) error { + return smtpd.Error{Code: 552, Message: "Denied"} }, } @@ -366,9 +367,9 @@ func TestSenderCheck(t *testing.T) { defer ln.Close() - server := &Server{ - SenderChecker: func(peer Peer, addr string) error { - return Error{Code: 552, Message: "Denied"} + server := &smtpd.Server{ + SenderChecker: func(peer smtpd.Peer, addr string) error { + return smtpd.Error{Code: 552, Message: "Denied"} }, } @@ -396,9 +397,9 @@ func TestRecipientCheck(t *testing.T) { defer ln.Close() - server := &Server{ - RecipientChecker: func(peer Peer, addr string) error { - return Error{Code: 552, Message: "Denied"} + server := &smtpd.Server{ + RecipientChecker: func(peer smtpd.Peer, addr string) error { + return smtpd.Error{Code: 552, Message: "Denied"} }, } @@ -430,7 +431,7 @@ func TestMaxMessageSize(t *testing.T) { defer ln.Close() - server := &Server{ + server := &smtpd.Server{ MaxMessageSize: 5, } @@ -481,8 +482,8 @@ func TestHandler(t *testing.T) { defer ln.Close() - server := &Server{ - Handler: func(peer Peer, env Envelope) error { + server := &smtpd.Server{ + Handler: func(peer smtpd.Peer, env smtpd.Envelope) error { if env.Sender != "sender@example.org" { t.Fatalf("Unknown sender: %v", env.Sender) } @@ -546,9 +547,9 @@ func TestRejectHandler(t *testing.T) { defer ln.Close() - server := &Server{ - Handler: func(peer Peer, env Envelope) error { - return Error{Code: 550, Message: "Rejected"} + server := &smtpd.Server{ + Handler: func(peer smtpd.Peer, env smtpd.Envelope) error { + return smtpd.Error{Code: 550, Message: "Rejected"} }, } @@ -599,7 +600,7 @@ func TestMaxConnections(t *testing.T) { defer ln.Close() - server := &Server{ + server := &smtpd.Server{ MaxConnections: 1, } @@ -629,7 +630,7 @@ func TestNoMaxConnections(t *testing.T) { defer ln.Close() - server := &Server{ + server := &smtpd.Server{ MaxConnections: -1, } @@ -654,7 +655,7 @@ func TestInvalidHelo(t *testing.T) { defer ln.Close() - server := &Server{} + server := &smtpd.Server{} go func() { server.Serve(ln) @@ -680,7 +681,7 @@ func TestInvalidSender(t *testing.T) { defer ln.Close() - server := &Server{} + server := &smtpd.Server{} go func() { server.Serve(ln) @@ -706,7 +707,7 @@ func TestInvalidRecipient(t *testing.T) { defer ln.Close() - server := &Server{} + server := &smtpd.Server{} go func() { server.Serve(ln) @@ -736,7 +737,7 @@ func TestRCPTbeforeMAIL(t *testing.T) { defer ln.Close() - server := &Server{} + server := &smtpd.Server{} go func() { server.Serve(ln) @@ -762,9 +763,9 @@ func TestDATAbeforeRCPT(t *testing.T) { defer ln.Close() - server := &Server{ - Handler: func(peer Peer, env Envelope) error { - return Error{Code: 550, Message: "Rejected"} + server := &smtpd.Server{ + Handler: func(peer smtpd.Peer, env smtpd.Envelope) error { + return smtpd.Error{Code: 550, Message: "Rejected"} }, } @@ -800,8 +801,8 @@ func TestInterruptedDATA(t *testing.T) { defer ln.Close() - server := &Server{ - Handler: func(peer Peer, env Envelope) error { + server := &smtpd.Server{ + Handler: func(peer smtpd.Peer, env smtpd.Envelope) error { t.Fatal("Accepted DATA despite disconnection") return nil }, From 3b49f5e70df84bd80c8b175cb8321a56daf9923c Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Tue, 15 Jul 2014 20:14:29 +0200 Subject: [PATCH 18/34] Add Authenticator test in AUTH handler. --- protocol.go | 5 +++++ smtpd_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/protocol.go b/protocol.go index 1b6304c..6465b29 100644 --- a/protocol.go +++ b/protocol.go @@ -298,6 +298,11 @@ func (session *session) handleQUIT(cmd command) { func (session *session) handleAUTH(cmd command) { + if session.server.Authenticator == nil { + session.reply(502, "AUTH not supported.") + return + } + if session.peer.HeloName == "" { session.reply(502, "Please introduce yourself first.") return diff --git a/smtpd_test.go b/smtpd_test.go index 4998052..76fb440 100644 --- a/smtpd_test.go +++ b/smtpd_test.go @@ -278,6 +278,46 @@ func TestAuthRejection(t *testing.T) { } +func TestAuthNotSupported(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + cert, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + t.Fatalf("Cert load failed: %v", err) + } + + server := &smtpd.Server{ + TLSConfig: &tls.Config{ + Certificates: []tls.Certificate{cert}, + }, + ForceTLS: true, + } + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if err := c.StartTLS(&tls.Config{InsecureSkipVerify: true}); err != nil { + t.Fatalf("STARTTLS failed: %v", err) + } + + if err := c.Auth(smtp.PlainAuth("foo", "foo", "bar", "127.0.0.1")); err == nil { + t.Fatal("Auth worked despite no authenticator") + } + +} + func TestConnectionCheck(t *testing.T) { ln, err := net.Listen("tcp", "127.0.0.1:0") From a0c514f12d94406fcf6f8fa9e055aceb1dd7104b Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Thu, 17 Jul 2014 16:00:44 +0200 Subject: [PATCH 19/34] Different fixes. Handle empty e-mail addresses. Implemented MaxRecipients check. Announce PIPELINING support. Corrected error code on Too Busy error. Implemented DataTimeout for timeouts on DATA. Use textproto.DotReader in handleDATA to implement dot-stuffing. Reset deadlines on old socket in STARTTLS, add new deadlines to new TLS socket. --- address.go | 8 ++- example_test.go | 2 + protocol.go | 79 +++++++++++++++--------- smtpd.go | 21 +++++-- smtpd_test.go | 155 ++++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 226 insertions(+), 39 deletions(-) diff --git a/address.go b/address.go index 76b7507..84bf735 100644 --- a/address.go +++ b/address.go @@ -6,8 +6,14 @@ import ( ) func parseAddress(src string) (string, error) { - if src[0] != '<' || src[len(src)-1] != '>' || strings.Count(src, "@") != 1 { + + if src[0] != '<' || src[len(src)-1] != '>' { return "", fmt.Errorf("Ill-formatted e-mail address: %s", src) } + + if strings.Count(src, "@") > 1 { + return "", fmt.Errorf("Ill-formatted e-mail address: %s", src) + } + return src[1 : len(src)-1], nil } diff --git a/example_test.go b/example_test.go index f0c669d..a143890 100644 --- a/example_test.go +++ b/example_test.go @@ -27,6 +27,7 @@ func ExampleServer() { }, Handler: func(peer smtpd.Peer, env smtpd.Envelope) error { + return smtp.SendMail( "smtp.gmail.com:587", smtp.PlainAuth( @@ -39,6 +40,7 @@ func ExampleServer() { env.Recipients, env.Data, ) + }, } diff --git a/protocol.go b/protocol.go index 6465b29..4a17c8e 100644 --- a/protocol.go +++ b/protocol.go @@ -6,7 +6,11 @@ import ( "crypto/tls" "encoding/base64" "fmt" + "io" + "io/ioutil" + "net/textproto" "strings" + "time" ) type command struct { @@ -178,6 +182,11 @@ func (session *session) handleRCPT(cmd command) { return } + if len(session.envelope.Recipients) >= session.server.MaxRecipients { + session.reply(550, "Too many recipients") + return + } + addr, err := parseAddress(cmd.params[1]) if err != nil { @@ -219,12 +228,23 @@ func (session *session) handleSTARTTLS(cmd command) { return } + // Reset HeloName as a new EHLO/HELO is required after STARTTLS + session.peer.HeloName = "" + + // Reset deadlines on the underlying connection before I replace it + // with a TLS connection + session.conn.SetDeadline(time.Time{}) + + // Replace connection with a TLS connection session.conn = tlsConn session.reader = bufio.NewReader(tlsConn) session.writer = bufio.NewWriter(tlsConn) session.scanner = bufio.NewScanner(session.reader) session.tls = true + // Flush the connection to set new timeout deadlines + session.flush() + return } @@ -237,46 +257,47 @@ func (session *session) handleDATA(cmd command) { } session.reply(354, "Go ahead. End your data with .") + session.conn.SetDeadline(time.Now().Add(session.server.DataTimeout)) data := &bytes.Buffer{} - done := false + reader := textproto.NewReader(session.reader).DotReader() - for session.scanner.Scan() { + _, err := io.CopyN(data, reader, int64(session.server.MaxMessageSize)) - line := session.scanner.Text() + if err == io.EOF { - if line == "." { - done = true - break + // EOF was reached before MaxMessageSize + // Accept and deliver message + + session.envelope.Data = data.Bytes() + if err := session.deliver(); err != nil { + session.error(err) + } else { + session.reply(250, "Thank you.") } - data.Write([]byte(line)) - data.Write([]byte("\r\n")) - } - if !done { - return - } - - if data.Len() > session.server.MaxMessageSize { - session.reply(550, fmt.Sprintf( - "Message exceeded max message size of %d bytes", - session.server.MaxMessageSize, - )) - return - } - - session.envelope.Data = data.Bytes() - - err := session.deliver() - if err != nil { - session.error(err) - } else { - session.reply(250, "Thank you.") + // Network error, ignore + return } + // Discard the rest and report an error. + _, err = io.Copy(ioutil.Discard, reader) + + if err != nil { + // Network error, ignore + return + } + + session.reply(552, fmt.Sprintf( + "Message exceeded max message size of %d bytes", + session.server.MaxMessageSize, + )) + + return + } func (session *session) handleRSET(cmd command) { @@ -298,7 +319,7 @@ func (session *session) handleQUIT(cmd command) { func (session *session) handleAUTH(cmd command) { - if session.server.Authenticator == nil { + if session.server.Authenticator == nil { session.reply(502, "AUTH not supported.") return } diff --git a/smtpd.go b/smtpd.go index de6d39f..aae2ddb 100644 --- a/smtpd.go +++ b/smtpd.go @@ -18,9 +18,11 @@ type Server struct { ReadTimeout time.Duration // Socket timeout for read operations. (default: 60s) WriteTimeout time.Duration // Socket timeout for write operations. (default: 60s) + DataTimeout time.Duration // Socket timeout for DATA command (default: 5m) - MaxMessageSize int // Max message size in bytes. (default: 10240000) MaxConnections int // Max concurrent connections, use -1 to disable. (default: 100) + MaxMessageSize int // Max message size in bytes. (default: 10240000) + MaxRecipients int // Max RCPT TO calls for each envelope. (default: 100) // New e-mails are handed off to this function. // Can be left empty for a NOOP server. @@ -168,6 +170,10 @@ func (srv *Server) configureDefaults() { srv.MaxConnections = 100 } + if srv.MaxRecipients == 0 { + srv.MaxRecipients = 100 + } + if srv.ReadTimeout == 0 { srv.ReadTimeout = time.Second * 60 } @@ -176,6 +182,10 @@ func (srv *Server) configureDefaults() { srv.WriteTimeout = time.Second * 60 } + if srv.DataTimeout == 0 { + srv.DataTimeout = time.Minute * 5 + } + if srv.ForceTLS && srv.TLSConfig == nil { log.Fatal("Cannot use ForceTLS with no TLSConfig") } @@ -211,7 +221,7 @@ func (session *session) serve() { } func (session *session) reject() { - session.reply(450, "Too busy. Try again later.") + session.reply(421, "Too busy. Try again later.") session.close() } @@ -231,14 +241,14 @@ func (session *session) welcome() { } func (session *session) reply(code int, message string) { - fmt.Fprintf(session.writer, "%d %s\r\n", code, message) + session.flush() +} +func (session *session) flush() { session.conn.SetWriteDeadline(time.Now().Add(session.server.WriteTimeout)) session.writer.Flush() - session.conn.SetReadDeadline(time.Now().Add(session.server.ReadTimeout)) - } func (session *session) error(err error) { @@ -254,6 +264,7 @@ func (session *session) extensions() []string { extensions := []string{ fmt.Sprintf("SIZE %d", session.server.MaxMessageSize), "8BITMIME", + "PIPELINING", } if session.server.TLSConfig != nil && !session.tls { diff --git a/smtpd_test.go b/smtpd_test.go index 76fb440..17e0539 100644 --- a/smtpd_test.go +++ b/smtpd_test.go @@ -533,8 +533,8 @@ func TestHandler(t *testing.T) { if env.Recipients[0] != "recipient@example.net" { t.Fatalf("Unknown recipient: %v", env.Recipients[0]) } - if string(env.Data) != "This is the email body\r\n" { - t.Fatalf("Wrong message body: %v", env.Data) + if string(env.Data) != "This is the email body\n" { + t.Fatalf("Wrong message body: %v", string(env.Data)) } return nil }, @@ -686,6 +686,46 @@ func TestNoMaxConnections(t *testing.T) { c1.Close() } +func TestMaxRecipients(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + server := &smtpd.Server{ + MaxRecipients: 1, + } + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if err := c.Mail("sender@example.org"); err != nil { + t.Fatalf("MAIL failed: %v", err) + } + + if err := c.Rcpt("recipient@example.net"); err != nil { + t.Fatalf("RCPT failed: %v", err) + } + + if err := c.Rcpt("recipient@example.net"); err == nil { + t.Fatal("RCPT succeeded despite MaxRecipients = 1") + } + + if err := c.Quit(); err != nil { + t.Fatalf("QUIT failed: %v", err) + } + +} + func TestInvalidHelo(t *testing.T) { ln, err := net.Listen("tcp", "127.0.0.1:0") @@ -732,7 +772,7 @@ func TestInvalidSender(t *testing.T) { t.Fatalf("Dial failed: %v", err) } - if err := c.Mail("invalid"); err == nil { + if err := c.Mail("invalid@@example.org"); err == nil { t.Fatal("Unexpected MAIL success") } @@ -762,7 +802,7 @@ func TestInvalidRecipient(t *testing.T) { t.Fatalf("Mail failed: %v", err) } - if err := c.Rcpt("invalid"); err == nil { + if err := c.Rcpt("invalid@@example.org"); err == nil { t.Fatal("Unexpected RCPT success") } @@ -878,3 +918,110 @@ func TestInterruptedDATA(t *testing.T) { c.Close() } + +func TestTimeoutClose(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + server := &smtpd.Server{ + MaxConnections: 1, + ReadTimeout: time.Second, + WriteTimeout: time.Second, + } + + go func() { + server.Serve(ln) + }() + + c1, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + time.Sleep(time.Second * 2) + + c2, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if err := c1.Mail("sender@example.org"); err == nil { + t.Fatal("MAIL succeeded despite being timed out.") + } + + if err := c2.Mail("sender@example.org"); err != nil { + t.Fatalf("MAIL failed: %v", err) + } + + if err := c2.Quit(); err != nil { + t.Fatalf("Quit failed: %v", err) + } + + c2.Close() +} + +func TestTLSTimeout(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + cert, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + t.Fatalf("Cert load failed: %v", err) + } + + server := &smtpd.Server{ + TLSConfig: &tls.Config{ + Certificates: []tls.Certificate{cert}, + }, + ReadTimeout: time.Second * 2, + WriteTimeout: time.Second * 2, + } + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if err := c.StartTLS(&tls.Config{InsecureSkipVerify: true}); err != nil { + t.Fatalf("STARTTLS failed: %v", err) + } + + time.Sleep(time.Second) + + if err := c.Mail("sender@example.org"); err != nil { + t.Fatalf("MAIL failed: %v", err) + } + + time.Sleep(time.Second) + + if err := c.Rcpt("recipient@example.net"); err != nil { + t.Fatalf("RCPT failed: %v", err) + } + + time.Sleep(time.Second) + + if err := c.Rcpt("recipient@example.net"); err != nil { + t.Fatalf("RCPT failed: %v", err) + } + + time.Sleep(time.Second) + + if err := c.Quit(); err != nil { + t.Fatalf("Quit failed: %v", err) + } + +} From 46b3a7668ee290b3e2414d5ecaa1a8e342fc7542 Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Thu, 17 Jul 2014 16:49:58 +0200 Subject: [PATCH 20/34] Reset envelope on duplicate HELO/EHLO and after DATA. --- protocol.go | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/protocol.go b/protocol.go index 4a17c8e..f77512c 100644 --- a/protocol.go +++ b/protocol.go @@ -93,6 +93,11 @@ func (session *session) handleHELO(cmd command) { return } + if session.peer.HeloName != "" { + // Reset envelope in case of duplicate HELO + session.envelope = nil + } + session.peer.HeloName = cmd.fields[1] if session.server.HeloChecker != nil { @@ -116,6 +121,11 @@ func (session *session) handleEHLO(cmd command) { return } + if session.peer.HeloName != "" { + // Reset envelope in case of duplicate EHLO + session.envelope = nil + } + session.peer.HeloName = cmd.fields[1] if session.server.HeloChecker != nil { @@ -152,6 +162,11 @@ func (session *session) handleMAIL(cmd command) { return } + if session.envelope != nil { + session.reply(502, "Duplicate MAIL") + return + } + addr, err := parseAddress(cmd.params[1]) if err != nil { @@ -228,8 +243,8 @@ func (session *session) handleSTARTTLS(cmd command) { return } - // Reset HeloName as a new EHLO/HELO is required after STARTTLS - session.peer.HeloName = "" + // Reset envelope as a new EHLO/HELO is required after STARTTLS + session.envelope = nil // Reset deadlines on the underlying connection before I replace it // with a TLS connection @@ -270,12 +285,15 @@ func (session *session) handleDATA(cmd command) { // Accept and deliver message session.envelope.Data = data.Bytes() + if err := session.deliver(); err != nil { session.error(err) } else { session.reply(250, "Thank you.") } + session.envelope = nil + } if err != nil { @@ -296,6 +314,8 @@ func (session *session) handleDATA(cmd command) { session.server.MaxMessageSize, )) + session.envelope = nil + return } From 97b38af3b4ef7965712677358512ef1a9779c31e Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Fri, 18 Jul 2014 08:46:39 +0200 Subject: [PATCH 21/34] Send the HELO hostname to the HeloChecker. --- .hgignore | 3 +++ example_test.go | 2 +- protocol.go | 11 +++++------ smtpd.go | 2 +- smtpd_test.go | 7 +++++-- 5 files changed, 15 insertions(+), 10 deletions(-) create mode 100644 .hgignore diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..758d190 --- /dev/null +++ b/.hgignore @@ -0,0 +1,3 @@ +syntax: glob + +*.orig diff --git a/example_test.go b/example_test.go index a143890..611a79b 100644 --- a/example_test.go +++ b/example_test.go @@ -19,7 +19,7 @@ func ExampleServer() { Addr: "0.0.0.0:10025", - HeloChecker: func(peer smtpd.Peer) error { + HeloChecker: func(peer smtpd.Peer, name string) error { if !strings.HasPrefix(peer.Addr.String(), "42.42.42.42:") { return errors.New("Denied") } diff --git a/protocol.go b/protocol.go index f77512c..ab5c217 100644 --- a/protocol.go +++ b/protocol.go @@ -98,16 +98,15 @@ func (session *session) handleHELO(cmd command) { session.envelope = nil } - session.peer.HeloName = cmd.fields[1] - if session.server.HeloChecker != nil { - err := session.server.HeloChecker(session.peer) + err := session.server.HeloChecker(session.peer, cmd.fields[1]) if err != nil { session.error(err) return } } + session.peer.HeloName = cmd.fields[1] session.reply(250, "Go ahead") return @@ -126,16 +125,16 @@ func (session *session) handleEHLO(cmd command) { session.envelope = nil } - session.peer.HeloName = cmd.fields[1] - if session.server.HeloChecker != nil { - err := session.server.HeloChecker(session.peer) + err := session.server.HeloChecker(session.peer, cmd.fields[1]) if err != nil { session.error(err) return } } + session.peer.HeloName = cmd.fields[1] + extensions := session.extensions() if len(extensions) > 1 { diff --git a/smtpd.go b/smtpd.go index aae2ddb..ac6585d 100644 --- a/smtpd.go +++ b/smtpd.go @@ -34,7 +34,7 @@ type Server struct { // If an error is returned, it will be reported in the SMTP session. // Use the Error struct for access to error codes. ConnectionChecker func(peer Peer) error // Called upon new connection. - HeloChecker func(peer Peer) error // Called after HELO/EHLO. + HeloChecker func(peer Peer, name string) error // Called after HELO/EHLO. SenderChecker func(peer Peer, addr string) error // Called after MAIL FROM. RecipientChecker func(peer Peer, addr string) error // Called after each RCPT TO. diff --git a/smtpd_test.go b/smtpd_test.go index 17e0539..f13b200 100644 --- a/smtpd_test.go +++ b/smtpd_test.go @@ -378,7 +378,10 @@ func TestHELOCheck(t *testing.T) { defer ln.Close() server := &smtpd.Server{ - HeloChecker: func(peer smtpd.Peer) error { + HeloChecker: func(peer smtpd.Peer, name string) error { + if name != "foobar.local" { + t.Fatal("Wrong HELO name") + } return smtpd.Error{Code: 552, Message: "Denied"} }, } @@ -392,7 +395,7 @@ func TestHELOCheck(t *testing.T) { t.Fatalf("Dial failed: %v", err) } - if err := c.Hello("localhost"); err == nil { + if err := c.Hello("foobar.local"); err == nil { t.Fatal("Unexpected HELO success") } From 0fa1acf7060f3f863918f6981243160e8aefbd73 Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Sat, 19 Jul 2014 20:55:40 +0200 Subject: [PATCH 22/34] Handle too long lines. Make envelope reset into a session method. --- protocol.go | 18 +++++++++++------- smtpd.go | 31 +++++++++++++++++++++++++++++-- smtpd_test.go | 30 ++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 9 deletions(-) diff --git a/protocol.go b/protocol.go index ab5c217..bf783a3 100644 --- a/protocol.go +++ b/protocol.go @@ -38,6 +38,10 @@ func (session *session) handle(line string) { cmd := parseLine(line) + // Commands are dispatched to the appropriate handler functions. + // If a network error occurs during handling, the handler should + // just return and let the error be handled on the next read. + switch cmd.action { case "HELO": @@ -95,7 +99,7 @@ func (session *session) handleHELO(cmd command) { if session.peer.HeloName != "" { // Reset envelope in case of duplicate HELO - session.envelope = nil + session.reset() } if session.server.HeloChecker != nil { @@ -122,7 +126,7 @@ func (session *session) handleEHLO(cmd command) { if session.peer.HeloName != "" { // Reset envelope in case of duplicate EHLO - session.envelope = nil + session.reset() } if session.server.HeloChecker != nil { @@ -197,7 +201,7 @@ func (session *session) handleRCPT(cmd command) { } if len(session.envelope.Recipients) >= session.server.MaxRecipients { - session.reply(550, "Too many recipients") + session.reply(452, "Too many recipients") return } @@ -243,7 +247,7 @@ func (session *session) handleSTARTTLS(cmd command) { } // Reset envelope as a new EHLO/HELO is required after STARTTLS - session.envelope = nil + session.reset() // Reset deadlines on the underlying connection before I replace it // with a TLS connection @@ -291,7 +295,7 @@ func (session *session) handleDATA(cmd command) { session.reply(250, "Thank you.") } - session.envelope = nil + session.reset() } @@ -313,14 +317,14 @@ func (session *session) handleDATA(cmd command) { session.server.MaxMessageSize, )) - session.envelope = nil + session.reset() return } func (session *session) handleRSET(cmd command) { - session.envelope = nil + session.reset() session.reply(250, "Go ahead") return } diff --git a/smtpd.go b/smtpd.go index ac6585d..490a546 100644 --- a/smtpd.go +++ b/smtpd.go @@ -214,8 +214,31 @@ func (session *session) serve() { session.welcome() - for session.scanner.Scan() { - session.handle(session.scanner.Text()) + for { + + for session.scanner.Scan() { + session.handle(session.scanner.Text()) + } + + err := session.scanner.Err() + + if err == bufio.ErrTooLong { + + session.reply(500, "Line too long") + + // Advance reader to the next newline + + session.reader.ReadString('\n') + session.scanner = bufio.NewScanner(session.reader) + + // Reset and have the client start over. + + session.reset() + + continue + } + + break } } @@ -225,6 +248,10 @@ func (session *session) reject() { session.close() } +func (session *session) reset() { + session.envelope = nil +} + func (session *session) welcome() { if session.server.ConnectionChecker != nil { diff --git a/smtpd_test.go b/smtpd_test.go index f13b200..a8b47a6 100644 --- a/smtpd_test.go +++ b/smtpd_test.go @@ -1028,3 +1028,33 @@ func TestTLSTimeout(t *testing.T) { } } + +func TestLongLine(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + server := &smtpd.Server{} + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if err := c.Mail(fmt.Sprintf("%s@example.org", strings.Repeat("x", 65*1024))); err == nil { + t.Fatalf("MAIL failed: %v", err) + } + + if err := c.Quit(); err != nil { + t.Fatalf("Quit failed: %v", err) + } + +} From 9695f7c734a76dff3b54c890d30e46fa46f5474d Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Sun, 20 Jul 2014 21:51:39 +0200 Subject: [PATCH 23/34] XCLIENT support. --- protocol.go | 119 ++++++++++++++++++++++++++++++++++++++++++++++++-- smtpd.go | 15 +++++++ smtpd_test.go | 87 ++++++++++++++++++++++++++++++++++++ 3 files changed, 217 insertions(+), 4 deletions(-) diff --git a/protocol.go b/protocol.go index bf783a3..6caaf36 100644 --- a/protocol.go +++ b/protocol.go @@ -8,7 +8,9 @@ import ( "fmt" "io" "io/ioutil" + "net" "net/textproto" + "strconv" "strings" "time" ) @@ -84,6 +86,10 @@ func (session *session) handle(line string) { session.handleAUTH(cmd) return + case "XCLIENT": + session.handleXCLIENT(cmd) + return + } session.reply(502, "Unsupported command.") @@ -111,6 +117,7 @@ func (session *session) handleHELO(cmd command) { } session.peer.HeloName = cmd.fields[1] + session.peer.Protocol = SMTP session.reply(250, "Go ahead") return @@ -138,6 +145,7 @@ func (session *session) handleEHLO(cmd command) { } session.peer.HeloName = cmd.fields[1] + session.peer.Protocol = ESMTP extensions := session.extensions() @@ -179,8 +187,10 @@ func (session *session) handleMAIL(cmd command) { if session.server.SenderChecker != nil { err = session.server.SenderChecker(session.peer, addr) - session.error(err) - return + if err != nil { + session.error(err) + return + } } session.envelope = &Envelope{ @@ -214,8 +224,10 @@ func (session *session) handleRCPT(cmd command) { if session.server.RecipientChecker != nil { err = session.server.RecipientChecker(session.peer, addr) - session.error(err) - return + if err != nil { + session.error(err) + return + } } session.envelope.Recipients = append(session.envelope.Recipients, addr) @@ -445,3 +457,102 @@ func (session *session) handleAUTH(cmd command) { session.reply(235, "OK, you are now authenticated") } + +func (session *session) handleXCLIENT(cmd command) { + + if !session.server.EnableXCLIENT { + session.reply(550, "XCLIENT not enabled") + return + } + + var ( + newHeloName = "" + newAddr net.IP = nil + newTCPPort uint64 = 0 + newUsername = "" + newProto Protocol = "" + ) + + for _, item := range cmd.fields[1:] { + + parts := strings.Split(item, "=") + + if len(parts) != 2 { + session.reply(502, "Couldn't decode the command.") + return + } + + name := parts[0] + value := parts[1] + + switch name { + + case "NAME": + // Unused in smtpd package + continue + + case "HELO": + newHeloName = value + continue + + case "ADDR": + newAddr = net.ParseIP(value) + continue + + case "PORT": + var err error + newTCPPort, err = strconv.ParseUint(value, 10, 16) + if err != nil { + session.reply(502, "Couldn't decode the command.") + return + } + continue + + case "LOGIN": + newUsername = value + continue + + case "PROTO": + if value == "SMTP" { + newProto = SMTP + } else if value == "ESMTP" { + newProto = ESMTP + } + continue + + default: + session.reply(502, "Couldn't decode the command.") + return + } + + } + + tcpAddr, ok := session.peer.Addr.(*net.TCPAddr) + if !ok { + session.reply(502, "Unsupported network connection") + return + } + + if newHeloName != "" { + session.peer.HeloName = newHeloName + } + + if newAddr != nil { + tcpAddr.IP = newAddr + } + + if newTCPPort != 0 { + tcpAddr.Port = int(newTCPPort) + } + + if newUsername != "" { + session.peer.Username = newUsername + } + + if newProto != "" { + session.peer.Protocol = newProto + } + + session.welcome() + +} diff --git a/smtpd.go b/smtpd.go index 490a546..3319baa 100644 --- a/smtpd.go +++ b/smtpd.go @@ -42,15 +42,26 @@ type Server struct { // Can be left empty for no authentication support. Authenticator func(peer Peer, username, password string) error + EnableXCLIENT bool // Enable XCLIENT support (default: false) + TLSConfig *tls.Config // Enable STARTTLS support. ForceTLS bool // Force STARTTLS usage. } +// Protocol represents the protocol used in the SMTP session +type Protocol string + +const ( + SMTP Protocol = "SMTP" + ESMTP = "ESMTP" +) + // Peer represents the client connecting to the server type Peer struct { HeloName string // Server name used in HELO/EHLO command Username string // Username from authentication, if authenticated Password string // Password from authentication, if authenticated + Protocol Protocol // Protocol used, SMTP or ESMTP Addr net.Addr // Network address } @@ -294,6 +305,10 @@ func (session *session) extensions() []string { "PIPELINING", } + if session.server.EnableXCLIENT { + extensions = append(extensions, "XCLIENT") + } + if session.server.TLSConfig != nil && !session.tls { extensions = append(extensions, "STARTTLS") } diff --git a/smtpd_test.go b/smtpd_test.go index a8b47a6..3770744 100644 --- a/smtpd_test.go +++ b/smtpd_test.go @@ -1058,3 +1058,90 @@ func TestLongLine(t *testing.T) { } } + +func TestXCLIENT(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + server := &smtpd.Server{ + EnableXCLIENT: true, + SenderChecker: func(peer smtpd.Peer, addr string) error { + if peer.HeloName != "new.example.net" { + t.Fatalf("Didn't override HELO name: %v", peer.HeloName) + } + if peer.Addr.String() != "42.42.42.42:4242" { + t.Fatalf("Didn't override IP/Port: %v", peer.Addr) + } + if peer.Username != "newusername" { + t.Fatalf("Didn't override username: %v", peer.Username) + } + if peer.Protocol != smtpd.SMTP { + t.Fatalf("Didn't override protocol: %v", peer.Protocol) + } + return nil + }, + } + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if supported, _ := c.Extension("XCLIENT"); !supported { + t.Fatal("XCLIENT not supported") + } + + id, err := c.Text.Cmd("XCLIENT NAME=ignored ADDR=42.42.42.42 PORT=4242 PROTO=SMTP HELO=new.example.net LOGIN=newusername") + if err != nil { + t.Fatalf("Cmd failed: %v", err) + } + + c.Text.StartResponse(id) + _, _, err = c.Text.ReadResponse(220) + c.Text.EndResponse(id) + + if err != nil { + t.Fatalf("XCLIENT failed: %v", err) + } + + if err := c.Mail("sender@example.org"); err != nil { + t.Fatalf("Mail failed: %v", err) + } + + if err := c.Rcpt("recipient@example.net"); err != nil { + t.Fatalf("Rcpt failed: %v", err) + } + + if err := c.Rcpt("recipient2@example.net"); err != nil { + t.Fatalf("Rcpt2 failed: %v", err) + } + + wc, err := c.Data() + if err != nil { + t.Fatalf("Data failed: %v", err) + } + + _, err = fmt.Fprintf(wc, "This is the email body") + if err != nil { + t.Fatalf("Data body failed: %v", err) + } + + err = wc.Close() + if err != nil { + t.Fatalf("Data close failed: %v", err) + } + + if err := c.Quit(); err != nil { + t.Fatalf("Quit failed: %v", err) + } + +} From c6fe39d4dcdd977df9f7109bcb929cadd7fbf2c8 Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Sun, 20 Jul 2014 21:53:47 +0200 Subject: [PATCH 24/34] Update synopsis. --- smtpd.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smtpd.go b/smtpd.go index 3319baa..b6a07c7 100644 --- a/smtpd.go +++ b/smtpd.go @@ -1,4 +1,4 @@ -// Package smtpd implements an SMTP server with support for STARTTLS, authentication (PLAIN/LOGIN) and optional restrictions on the different stages of the SMTP session. +// Package smtpd implements an SMTP server with support for STARTTLS, authentication (PLAIN/LOGIN), XCLIENT and optional restrictions on the different stages of the SMTP session. package smtpd import ( From d28767953f8105c756f2aa66febfc74ae7060d54 Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Mon, 21 Jul 2014 00:06:56 +0200 Subject: [PATCH 25/34] Added TLS param to Peer. Added option to prepend Received header to envelope data. --- envelope.go | 54 +++++++++++++++++++++++++++++++++++++++++++++ protocol.go | 4 ++++ smtpd.go | 18 +++++---------- smtpd_test.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++- wrap.go | 22 +++++++++++++++++++ wrap_test.go | 24 ++++++++++++++++++++ 6 files changed, 170 insertions(+), 13 deletions(-) create mode 100644 envelope.go create mode 100644 wrap.go create mode 100644 wrap_test.go diff --git a/envelope.go b/envelope.go new file mode 100644 index 0000000..29dd009 --- /dev/null +++ b/envelope.go @@ -0,0 +1,54 @@ +package smtpd + +import ( + "crypto/tls" + "fmt" + "strings" + "time" +) + +// Envelope holds a message +type Envelope struct { + Sender string + Recipients []string + Data []byte +} + +// AddReceivedLine prepends a Received header to the Data +func (env *Envelope) AddReceivedLine(peer Peer, serverName string) { + + tlsDetails := "" + + tlsVersions := map[uint16]string{ + tls.VersionSSL30: "SSL3.0", + tls.VersionTLS10: "TLS1.0", + tls.VersionTLS11: "TLS1.1", + tls.VersionTLS12: "TLS1.2", + } + + if peer.TLS != nil { + tlsDetails = fmt.Sprintf( + "\r\n\t(version=%s cipher=0x%x);", + tlsVersions[peer.TLS.Version], + peer.TLS.CipherSuite, + ) + } + + line := wrap([]byte(fmt.Sprintf( + "Received: from %s [%s] by %s with %s;%s\r\n\t%s\r\n", + peer.HeloName, + strings.Split(peer.Addr.String(), ":")[0], + serverName, + peer.Protocol, + tlsDetails, + time.Now().Format("Mon Jan 2 15:04:05 -0700 2006"), + ))) + + env.Data = append(env.Data, line...) + + // Move the new Received line up front + + copy(env.Data[len(line):], env.Data[0:len(env.Data)-len(line)]) + copy(env.Data, line) + +} diff --git a/protocol.go b/protocol.go index 6caaf36..5382040 100644 --- a/protocol.go +++ b/protocol.go @@ -272,6 +272,10 @@ func (session *session) handleSTARTTLS(cmd command) { session.scanner = bufio.NewScanner(session.reader) session.tls = true + // Save connection state on peer + state := tlsConn.ConnectionState() + session.peer.TLS = &state + // Flush the connection to set new timeout deadlines session.flush() diff --git a/smtpd.go b/smtpd.go index b6a07c7..460c416 100644 --- a/smtpd.go +++ b/smtpd.go @@ -58,18 +58,12 @@ const ( // Peer represents the client connecting to the server type Peer struct { - HeloName string // Server name used in HELO/EHLO command - Username string // Username from authentication, if authenticated - Password string // Password from authentication, if authenticated - Protocol Protocol // Protocol used, SMTP or ESMTP - Addr net.Addr // Network address -} - -// Envelope holds a message -type Envelope struct { - Sender string - Recipients []string - Data []byte + HeloName string // Server name used in HELO/EHLO command + Username string // Username from authentication, if authenticated + Password string // Password from authentication, if authenticated + Protocol Protocol // Protocol used, SMTP or ESMTP + Addr net.Addr // Network address + TLS *tls.ConnectionState // TLS Connection details, if on TLS } // Error represents an Error reported in the SMTP session. diff --git a/smtpd_test.go b/smtpd_test.go index 3770744..2d817fd 100644 --- a/smtpd_test.go +++ b/smtpd_test.go @@ -1,7 +1,7 @@ package smtpd_test import ( - "bitbucket.org/chrj/smtpd" + "bytes" "crypto/tls" "errors" "fmt" @@ -10,6 +10,8 @@ import ( "strings" "testing" "time" + + "bitbucket.org/chrj/smtpd" ) var localhostCert = []byte(`-----BEGIN CERTIFICATE----- @@ -1145,3 +1147,60 @@ func TestXCLIENT(t *testing.T) { } } + +func TestEnvelopeReceived(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + server := &smtpd.Server{ + Handler: func(peer smtpd.Peer, env smtpd.Envelope) error { + env.AddReceivedLine(peer, "foobar.example.net") + if !bytes.HasPrefix(env.Data, []byte("Received: from localhost [127.0.0.1] by foobar.example.net with ESMTP;")) { + t.Fatal("Wrong received line.") + } + return nil + }, + } + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if err := c.Mail("sender@example.org"); err != nil { + t.Fatalf("MAIL failed: %v", err) + } + + if err := c.Rcpt("recipient@example.net"); err != nil { + t.Fatalf("RCPT failed: %v", err) + } + + wc, err := c.Data() + if err != nil { + t.Fatalf("Data failed: %v", err) + } + + _, err = fmt.Fprintf(wc, "This is the email body") + if err != nil { + t.Fatalf("Data body failed: %v", err) + } + + err = wc.Close() + if err != nil { + t.Fatalf("Data close failed: %v", err) + } + + if err := c.Quit(); err != nil { + t.Fatalf("QUIT failed: %v", err) + } + +} diff --git a/wrap.go b/wrap.go new file mode 100644 index 0000000..91a6cce --- /dev/null +++ b/wrap.go @@ -0,0 +1,22 @@ +package smtpd + +// Wrap a byte slice paragraph for use in SMTP header +func wrap(sl []byte) []byte { + length := 0 + for i := 0; i < len(sl); i++ { + if length > 76 && sl[i] == ' ' { + sl = append(sl, 0, 0) + copy(sl[i+2:], sl[i:]) + sl[i] = '\r' + sl[i+1] = '\n' + sl[i+2] = '\t' + i += 2 + length = 0 + } + if sl[i] == '\n' { + length = 0 + } + length++ + } + return sl +} diff --git a/wrap_test.go b/wrap_test.go new file mode 100644 index 0000000..a8b65de --- /dev/null +++ b/wrap_test.go @@ -0,0 +1,24 @@ +package smtpd + +import ( + "testing" +) + +func TestWrap(t *testing.T) { + + cases := map[string]string{ + "foobar": "foobar", + "foobar quux": "foobar quux", + "foobar\r\n": "foobar\r\n", + "foobar\r\nquux": "foobar\r\nquux", + "foobar quux foobar quux foobar quux foobar quux foobar quux foobar quux foobar quux foobar quux": "foobar quux foobar quux foobar quux foobar quux foobar quux foobar quux foobar\r\n\tquux foobar quux", + "foobar quux foobar quux foobar quux foobar quux foobar quux foobar\r\n\tquux foobar quux foobar quux": "foobar quux foobar quux foobar quux foobar quux foobar quux foobar\r\n\tquux foobar quux foobar quux", + } + + for k, v := range cases { + if string(wrap([]byte(k))) != v { + t.Fatal("Didn't wrap correctly.") + } + } + +} From 1d5d658be6e9406ef7f5b23bb671030368ac266e Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Mon, 21 Jul 2014 12:43:42 +0200 Subject: [PATCH 26/34] Added new Server field Hostname for the FQDN over the server (and updated various uses of the hostname). Moved Server.Addr to a parameter for ListenAndServe, as it was only used there. --- envelope.go | 4 ++-- example_test.go | 6 ++---- protocol.go | 2 ++ smtpd.go | 41 ++++++++++++++++++----------------------- smtpd_test.go | 7 ++++--- 5 files changed, 28 insertions(+), 32 deletions(-) diff --git a/envelope.go b/envelope.go index 29dd009..0e0f1bc 100644 --- a/envelope.go +++ b/envelope.go @@ -15,7 +15,7 @@ type Envelope struct { } // AddReceivedLine prepends a Received header to the Data -func (env *Envelope) AddReceivedLine(peer Peer, serverName string) { +func (env *Envelope) AddReceivedLine(peer Peer) { tlsDetails := "" @@ -38,7 +38,7 @@ func (env *Envelope) AddReceivedLine(peer Peer, serverName string) { "Received: from %s [%s] by %s with %s;%s\r\n\t%s\r\n", peer.HeloName, strings.Split(peer.Addr.String(), ":")[0], - serverName, + peer.ServerName, peer.Protocol, tlsDetails, time.Now().Format("Mon Jan 2 15:04:05 -0700 2006"), diff --git a/example_test.go b/example_test.go index 611a79b..bdfe3f5 100644 --- a/example_test.go +++ b/example_test.go @@ -12,13 +12,11 @@ func ExampleServer() { // No-op server. Accepts and discards server = &smtpd.Server{} - server.ListenAndServe() + server.ListenAndServe("127.0.0.1:10025") // Relay server. Accepts only from single IP address and forwards using the Gmail smtp server = &smtpd.Server{ - Addr: "0.0.0.0:10025", - HeloChecker: func(peer smtpd.Peer, name string) error { if !strings.HasPrefix(peer.Addr.String(), "42.42.42.42:") { return errors.New("Denied") @@ -44,5 +42,5 @@ func ExampleServer() { }, } - server.ListenAndServe() + server.ListenAndServe("127.0.0.1:10025") } diff --git a/protocol.go b/protocol.go index 5382040..7a7c09f 100644 --- a/protocol.go +++ b/protocol.go @@ -147,6 +147,8 @@ func (session *session) handleEHLO(cmd command) { session.peer.HeloName = cmd.fields[1] session.peer.Protocol = ESMTP + fmt.Fprintf(session.writer, "250-%s\r\n", session.server.Hostname) + extensions := session.extensions() if len(extensions) > 1 { diff --git a/smtpd.go b/smtpd.go index 460c416..08f84e0 100644 --- a/smtpd.go +++ b/smtpd.go @@ -7,13 +7,12 @@ import ( "fmt" "log" "net" - "os" "time" ) // Server defines the parameters for running the SMTP server type Server struct { - Addr string // Address to listen on when using ListenAndServe. (default: "127.0.0.1:10025") + Hostname string // Server hostname. (default: "localhost.localdomain") WelcomeMessage string // Initial server banner. (default: " ESMTP ready.") ReadTimeout time.Duration // Socket timeout for read operations. (default: 60s) @@ -58,12 +57,13 @@ const ( // Peer represents the client connecting to the server type Peer struct { - HeloName string // Server name used in HELO/EHLO command - Username string // Username from authentication, if authenticated - Password string // Password from authentication, if authenticated - Protocol Protocol // Protocol used, SMTP or ESMTP - Addr net.Addr // Network address - TLS *tls.ConnectionState // TLS Connection details, if on TLS + HeloName string // Server name used in HELO/EHLO command + Username string // Username from authentication, if authenticated + Password string // Password from authentication, if authenticated + Protocol Protocol // Protocol used, SMTP or ESMTP + ServerName string // A copy of Server.Hostname + Addr net.Addr // Network address + TLS *tls.ConnectionState // TLS Connection details, if on TLS } // Error represents an Error reported in the SMTP session. @@ -97,7 +97,10 @@ func (srv *Server) newSession(c net.Conn) (s *session) { conn: c, reader: bufio.NewReader(c), writer: bufio.NewWriter(c), - peer: Peer{Addr: c.RemoteAddr()}, + peer: Peer{ + Addr: c.RemoteAddr(), + ServerName: srv.Hostname, + }, } s.scanner = bufio.NewScanner(s.reader) @@ -106,12 +109,12 @@ func (srv *Server) newSession(c net.Conn) (s *session) { } -// ListenAndServe starts the SMTP server and listens on the address provided in Server.Addr -func (srv *Server) ListenAndServe() error { +// ListenAndServe starts the SMTP server and listens on the address provided +func (srv *Server) ListenAndServe(addr string) error { srv.configureDefaults() - l, err := net.Listen("tcp", srv.Addr) + l, err := net.Listen("tcp", addr) if err != nil { return err } @@ -195,20 +198,12 @@ func (srv *Server) configureDefaults() { log.Fatal("Cannot use ForceTLS with no TLSConfig") } - if srv.Addr == "" { - srv.Addr = "127.0.0.1:10025" + if srv.Hostname == "" { + srv.Hostname = "localhost.localdomain" } if srv.WelcomeMessage == "" { - - hostname, err := os.Hostname() - - if err != nil { - log.Fatal("Couldn't determine hostname: %s", err) - } - - srv.WelcomeMessage = fmt.Sprintf("%s ESMTP ready.", hostname) - + srv.WelcomeMessage = fmt.Sprintf("%s ESMTP ready.", srv.Hostname) } } diff --git a/smtpd_test.go b/smtpd_test.go index 2d817fd..c3d8b9a 100644 --- a/smtpd_test.go +++ b/smtpd_test.go @@ -123,10 +123,10 @@ func TestListenAndServe(t *testing.T) { ln.Close() - server := &smtpd.Server{Addr: addr} + server := &smtpd.Server{} go func() { - server.ListenAndServe() + server.ListenAndServe(addr) }() time.Sleep(100 * time.Millisecond) @@ -1158,8 +1158,9 @@ func TestEnvelopeReceived(t *testing.T) { defer ln.Close() server := &smtpd.Server{ + Hostname: "foobar.example.net", Handler: func(peer smtpd.Peer, env smtpd.Envelope) error { - env.AddReceivedLine(peer, "foobar.example.net") + env.AddReceivedLine(peer) if !bytes.HasPrefix(env.Data, []byte("Received: from localhost [127.0.0.1] by foobar.example.net with ESMTP;")) { t.Fatal("Wrong received line.") } From 753dd09501542df7fe83ddcd92bbd286540f0b2c Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Mon, 21 Jul 2014 13:27:05 +0200 Subject: [PATCH 27/34] More test cases. --- smtpd_test.go | 251 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 242 insertions(+), 9 deletions(-) diff --git a/smtpd_test.go b/smtpd_test.go index c3d8b9a..0b6afe3 100644 --- a/smtpd_test.go +++ b/smtpd_test.go @@ -7,6 +7,7 @@ import ( "fmt" "net" "net/smtp" + "net/textproto" "strings" "testing" "time" @@ -36,6 +37,19 @@ SkjPiCBE0BX+AQIhAI/TiLk7YmBkQG3ovSYW0vvDntPlXpKj08ovJFw4U0D3AiEA lGjGna4oaauI0CWI6pG0wg4zklTnrDWK7w9h/S/T4e0= -----END RSA PRIVATE KEY-----`) +func cmd(c *textproto.Conn, expectedCode int, format string, args ...interface{}) error { + id, err := c.Cmd(format, args...) + if err != nil { + return err + } + + c.StartResponse(id) + _, _, err = c.ReadResponse(expectedCode) + c.EndResponse(id) + + return err +} + func TestSMTP(t *testing.T) { ln, err := net.Listen("tcp", "127.0.0.1:0") @@ -107,6 +121,10 @@ func TestSMTP(t *testing.T) { t.Fatal("Unexpected support for VRFY") } + if err := cmd(c.Text, 250, "NOOP"); err != nil { + t.Fatalf("NOOP failed: %v", err) + } + if err := c.Quit(); err != nil { t.Fatalf("Quit failed: %v", err) } @@ -181,6 +199,14 @@ func TestSTARTTLS(t *testing.T) { t.Fatal("Mail workded before TLS with ForceTLS") } + if err := cmd(c.Text, 220, "STARTTLS"); err != nil { + t.Fatalf("STARTTLS failed: %v", err) + } + + if err := cmd(c.Text, 250, "foobar"); err == nil { + t.Fatal("STARTTLS didn't fail with invalid handshake") + } + if err := c.StartTLS(&tls.Config{InsecureSkipVerify: true}); err != nil { t.Fatalf("STARTTLS failed: %v", err) } @@ -1102,15 +1128,7 @@ func TestXCLIENT(t *testing.T) { t.Fatal("XCLIENT not supported") } - id, err := c.Text.Cmd("XCLIENT NAME=ignored ADDR=42.42.42.42 PORT=4242 PROTO=SMTP HELO=new.example.net LOGIN=newusername") - if err != nil { - t.Fatalf("Cmd failed: %v", err) - } - - c.Text.StartResponse(id) - _, _, err = c.Text.ReadResponse(220) - c.Text.EndResponse(id) - + err = cmd(c.Text, 220, "XCLIENT NAME=ignored ADDR=42.42.42.42 PORT=4242 PROTO=SMTP HELO=new.example.net LOGIN=newusername") if err != nil { t.Fatalf("XCLIENT failed: %v", err) } @@ -1157,6 +1175,11 @@ func TestEnvelopeReceived(t *testing.T) { defer ln.Close() + cert, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + t.Fatalf("Cert load failed: %v", err) + } + server := &smtpd.Server{ Hostname: "foobar.example.net", Handler: func(peer smtpd.Peer, env smtpd.Envelope) error { @@ -1166,6 +1189,10 @@ func TestEnvelopeReceived(t *testing.T) { } return nil }, + TLSConfig: &tls.Config{ + Certificates: []tls.Certificate{cert}, + }, + ForceTLS: true, } go func() { @@ -1177,6 +1204,10 @@ func TestEnvelopeReceived(t *testing.T) { t.Fatalf("Dial failed: %v", err) } + if err := c.StartTLS(&tls.Config{InsecureSkipVerify: true}); err != nil { + t.Fatalf("STARTTLS failed: %v", err) + } + if err := c.Mail("sender@example.org"); err != nil { t.Fatalf("MAIL failed: %v", err) } @@ -1205,3 +1236,205 @@ func TestEnvelopeReceived(t *testing.T) { } } + +func TestHELO(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + server := &smtpd.Server{} + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if err := cmd(c.Text, 502, "MAIL FROM:"); err != nil { + t.Fatalf("MAIL didn't fail: %v", err) + } + + if err := cmd(c.Text, 250, "HELO localhost"); err != nil { + t.Fatalf("HELO failed: %v", err) + } + + if err := cmd(c.Text, 502, "MAIL FROM:christian@technobabble.dk"); err != nil { + t.Fatalf("MAIL didn't fail: %v", err) + } + + if err := cmd(c.Text, 250, "HELO localhost"); err != nil { + t.Fatalf("HELO failed: %v", err) + } + + if err := c.Quit(); err != nil { + t.Fatalf("Quit failed: %v", err) + } + +} + +func TestLOGINAuth(t *testing.T) { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + cert, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + t.Fatalf("Cert load failed: %v", err) + } + + server := &smtpd.Server{ + Authenticator: func(peer smtpd.Peer, username, password string) error { return nil }, + TLSConfig: &tls.Config{ + Certificates: []tls.Certificate{cert}, + }, + } + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if err := c.StartTLS(&tls.Config{InsecureSkipVerify: true}); err != nil { + t.Fatalf("STARTTLS failed: %v", err) + } + + if err := cmd(c.Text, 334, "AUTH LOGIN"); err != nil { + t.Fatalf("AUTH didn't work: %v", err) + } + + if err := cmd(c.Text, 502, "foo"); err != nil { + t.Fatalf("AUTH didn't fail: %v", err) + } + + if err := cmd(c.Text, 334, "AUTH LOGIN"); err != nil { + t.Fatalf("AUTH didn't work: %v", err) + } + + if err := cmd(c.Text, 334, "Zm9v"); err != nil { + t.Fatalf("AUTH didn't work: %v", err) + } + + if err := cmd(c.Text, 502, "foo"); err != nil { + t.Fatalf("AUTH didn't fail: %v", err) + } + + if err := cmd(c.Text, 334, "AUTH LOGIN"); err != nil { + t.Fatalf("AUTH didn't work: %v", err) + } + + if err := cmd(c.Text, 334, "Zm9v"); err != nil { + t.Fatalf("AUTH didn't work: %v", err) + } + + if err := cmd(c.Text, 235, "Zm9v"); err != nil { + t.Fatalf("AUTH didn't work: %v", err) + } + + if err := c.Quit(); err != nil { + t.Fatalf("Quit failed: %v", err) + } + +} + +func TestErrors(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + cert, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + t.Fatalf("Cert load failed: %v", err) + } + + server := &smtpd.Server{ + Authenticator: func(peer smtpd.Peer, username, password string) error { return nil }, + } + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if err := cmd(c.Text, 502, "AUTH PLAIN foobar"); err != nil { + t.Fatalf("AUTH didn't fail: %v", err) + } + + if err := c.Hello("localhost"); err != nil { + t.Fatalf("HELO failed: %v", err) + } + + if err := cmd(c.Text, 502, "AUTH PLAIN foobar"); err != nil { + t.Fatalf("AUTH didn't fail: %v", err) + } + + if err := cmd(c.Text, 502, "MAIL FROM:christian@technobabble.dk"); err != nil { + t.Fatalf("MAIL didn't fail: %v", err) + } + + if err := c.Mail("sender@example.org"); err != nil { + t.Fatalf("MAIL failed: %v", err) + } + + if err := c.Mail("sender@example.org"); err == nil { + t.Fatal("Duplicate MAIL didn't fail") + } + + if err := cmd(c.Text, 502, "STARTTLS"); err != nil { + t.Fatalf("STARTTLS didn't fail: %v", err) + } + + server.TLSConfig = &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + + if err := c.StartTLS(&tls.Config{InsecureSkipVerify: true}); err != nil { + t.Fatalf("STARTTLS failed: %v", err) + } + + if err := cmd(c.Text, 502, "AUTH UNKNOWN"); err != nil { + t.Fatalf("AUTH didn't fail: %v", err) + } + + if err := cmd(c.Text, 502, "AUTH PLAIN foobar"); err != nil { + t.Fatalf("AUTH didn't fail: %v", err) + } + + if err := cmd(c.Text, 502, "AUTH PLAIN Zm9vAGJhcg=="); err != nil { + t.Fatalf("AUTH didn't fail: %v", err) + } + + if err := cmd(c.Text, 334, "AUTH PLAIN"); err != nil { + t.Fatalf("AUTH didn't work: %v", err) + } + + if err := cmd(c.Text, 235, "Zm9vAGJhcgBxdXV4"); err != nil { + t.Fatalf("AUTH didn't work: %v", err) + } + + if err := c.Quit(); err != nil { + t.Fatalf("Quit failed: %v", err) + } + +} From 437121004c55f300fe6bceee8272859828b9b6c1 Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Tue, 22 Jul 2014 08:49:12 +0200 Subject: [PATCH 28/34] Fix panic on blank line. --- protocol.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/protocol.go b/protocol.go index 7a7c09f..85ade92 100644 --- a/protocol.go +++ b/protocol.go @@ -26,10 +26,12 @@ func parseLine(line string) (cmd command) { cmd.line = line cmd.fields = strings.Fields(line) - cmd.action = strings.ToUpper(cmd.fields[0]) - if len(cmd.fields) > 1 { - cmd.params = strings.Split(cmd.fields[1], ":") + if len(cmd.fields) > 0 { + cmd.action = strings.ToUpper(cmd.fields[0]) + if len(cmd.fields) > 1 { + cmd.params = strings.Split(cmd.fields[1], ":") + } } return From c0d4dc204d39c473765aed590e6d7d62d19ceb5b Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Sat, 26 Jul 2014 18:29:14 +0200 Subject: [PATCH 29/34] Tests refactoring, readme updated. --- README.md | 20 +- smtpd_test.go | 503 +++++++++++++------------------------------------- 2 files changed, 148 insertions(+), 375 deletions(-) diff --git a/README.md b/README.md index 75033b3..a428fea 100644 --- a/README.md +++ b/README.md @@ -1 +1,19 @@ -[![GoDoc](https://godoc.org/bitbucket.org/chrj/smtpd?status.png)](https://godoc.org/bitbucket.org/chrj/smtpd) +Go smtpd [![GoDoc](https://godoc.org/bitbucket.org/chrj/smtpd?status.png)](https://godoc.org/bitbucket.org/chrj/smtpd) +======== + +Package smtpd implements an SMTP server in golang. + +Features +-------- + +* STARTTLS (using `crypto/tls`) +* Authentication (PLAIN/LOGIN, only after STARTTLS) +* XCLIENT (for running behind a proxy) +* Connection, HELO, sender and recipient checks for rejecting e-mails using callbacks +* Configurable limits for: connection count, message size and recipient count +* Hands incoming e-mail off to a configured callback function + +Feedback +-------- + +If you end up using this package or have any feedback, I'd very much like to hear about it. You can reach me by [email](mailto:christian@technobabble.dk). diff --git a/smtpd_test.go b/smtpd_test.go index 0b6afe3..f487df7 100644 --- a/smtpd_test.go +++ b/smtpd_test.go @@ -50,22 +50,36 @@ func cmd(c *textproto.Conn, expectedCode int, format string, args ...interface{} return err } -func TestSMTP(t *testing.T) { +func runserver(t *testing.T, server *smtpd.Server) (addr string, closer func()) { ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("Listen failed: %v", err) } - defer ln.Close() - - server := &smtpd.Server{} - go func() { server.Serve(ln) }() - c, err := smtp.Dial(ln.Addr().String()) + done := make(chan bool) + + go func() { + <-done + ln.Close() + }() + + return ln.Addr().String(), func () { + done <- true + } + +} + +func TestSMTP(t *testing.T) { + + addr, closer := runserver(t, &smtpd.Server{}) + defer closer() + + c, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -132,14 +146,8 @@ func TestSMTP(t *testing.T) { func TestListenAndServe(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } - - addr := ln.Addr().String() - - ln.Close() + addr, closer := runserver(t, &smtpd.Server{}) + closer() server := &smtpd.Server{} @@ -162,31 +170,22 @@ func TestListenAndServe(t *testing.T) { func TestSTARTTLS(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } - - defer ln.Close() - cert, err := tls.X509KeyPair(localhostCert, localhostKey) if err != nil { t.Fatalf("Cert load failed: %v", err) } - server := &smtpd.Server{ + addr, closer := runserver(t, &smtpd.Server{ Authenticator: func(peer smtpd.Peer, username, password string) error { return nil }, TLSConfig: &tls.Config{ Certificates: []tls.Certificate{cert}, }, ForceTLS: true, - } + }) - go func() { - server.Serve(ln) - }() + defer closer() - c, err := smtp.Dial(ln.Addr().String()) + c, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -265,19 +264,12 @@ func TestSTARTTLS(t *testing.T) { func TestAuthRejection(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } - - defer ln.Close() - cert, err := tls.X509KeyPair(localhostCert, localhostKey) if err != nil { t.Fatalf("Cert load failed: %v", err) } - server := &smtpd.Server{ + addr, closer := runserver(t, &smtpd.Server{ Authenticator: func(peer smtpd.Peer, username, password string) error { return smtpd.Error{Code: 550, Message: "Denied"} }, @@ -285,13 +277,11 @@ func TestAuthRejection(t *testing.T) { Certificates: []tls.Certificate{cert}, }, ForceTLS: true, - } + }) - go func() { - server.Serve(ln) - }() + defer closer() - c, err := smtp.Dial(ln.Addr().String()) + c, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -308,30 +298,21 @@ func TestAuthRejection(t *testing.T) { func TestAuthNotSupported(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } - - defer ln.Close() - cert, err := tls.X509KeyPair(localhostCert, localhostKey) if err != nil { t.Fatalf("Cert load failed: %v", err) } - server := &smtpd.Server{ + addr, closer := runserver(t, &smtpd.Server{ TLSConfig: &tls.Config{ Certificates: []tls.Certificate{cert}, }, ForceTLS: true, - } + }) - go func() { - server.Serve(ln) - }() + defer closer() - c, err := smtp.Dial(ln.Addr().String()) + c, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -348,24 +329,15 @@ func TestAuthNotSupported(t *testing.T) { func TestConnectionCheck(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } - - defer ln.Close() - - server := &smtpd.Server{ + addr, closer := runserver(t, &smtpd.Server{ ConnectionChecker: func(peer smtpd.Peer) error { return smtpd.Error{Code: 552, Message: "Denied"} }, - } + }) - go func() { - server.Serve(ln) - }() + defer closer() - if _, err := smtp.Dial(ln.Addr().String()); err == nil { + if _, err := smtp.Dial(addr); err == nil { t.Fatal("Dial succeeded despite ConnectionCheck") } @@ -373,24 +345,15 @@ func TestConnectionCheck(t *testing.T) { func TestConnectionCheckSimpleError(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } - - defer ln.Close() - - server := &smtpd.Server{ + addr, closer := runserver(t, &smtpd.Server{ ConnectionChecker: func(peer smtpd.Peer) error { return errors.New("Denied") }, - } + }) - go func() { - server.Serve(ln) - }() + defer closer() - if _, err := smtp.Dial(ln.Addr().String()); err == nil { + if _, err := smtp.Dial(addr); err == nil { t.Fatal("Dial succeeded despite ConnectionCheck") } @@ -398,27 +361,18 @@ func TestConnectionCheckSimpleError(t *testing.T) { func TestHELOCheck(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } - - defer ln.Close() - - server := &smtpd.Server{ + addr, closer := runserver(t, &smtpd.Server{ HeloChecker: func(peer smtpd.Peer, name string) error { if name != "foobar.local" { t.Fatal("Wrong HELO name") } return smtpd.Error{Code: 552, Message: "Denied"} }, - } + }) - go func() { - server.Serve(ln) - }() + defer closer() - c, err := smtp.Dial(ln.Addr().String()) + c, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -431,24 +385,15 @@ func TestHELOCheck(t *testing.T) { func TestSenderCheck(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } - - defer ln.Close() - - server := &smtpd.Server{ + addr, closer := runserver(t, &smtpd.Server{ SenderChecker: func(peer smtpd.Peer, addr string) error { return smtpd.Error{Code: 552, Message: "Denied"} }, - } + }) - go func() { - server.Serve(ln) - }() + defer closer() - c, err := smtp.Dial(ln.Addr().String()) + c, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -461,24 +406,15 @@ func TestSenderCheck(t *testing.T) { func TestRecipientCheck(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } - - defer ln.Close() - - server := &smtpd.Server{ + addr, closer := runserver(t, &smtpd.Server{ RecipientChecker: func(peer smtpd.Peer, addr string) error { return smtpd.Error{Code: 552, Message: "Denied"} }, - } + }) - go func() { - server.Serve(ln) - }() + defer closer() - c, err := smtp.Dial(ln.Addr().String()) + c, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -495,22 +431,13 @@ func TestRecipientCheck(t *testing.T) { func TestMaxMessageSize(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } - - defer ln.Close() - - server := &smtpd.Server{ + addr, closer := runserver(t, &smtpd.Server{ MaxMessageSize: 5, - } + }) - go func() { - server.Serve(ln) - }() + defer closer() - c, err := smtp.Dial(ln.Addr().String()) + c, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -546,14 +473,7 @@ func TestMaxMessageSize(t *testing.T) { func TestHandler(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } - - defer ln.Close() - - server := &smtpd.Server{ + addr, closer := runserver(t, &smtpd.Server{ Handler: func(peer smtpd.Peer, env smtpd.Envelope) error { if env.Sender != "sender@example.org" { t.Fatalf("Unknown sender: %v", env.Sender) @@ -569,13 +489,11 @@ func TestHandler(t *testing.T) { } return nil }, - } + }) - go func() { - server.Serve(ln) - }() + defer closer() - c, err := smtp.Dial(ln.Addr().String()) + c, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -611,24 +529,15 @@ func TestHandler(t *testing.T) { func TestRejectHandler(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } - - defer ln.Close() - - server := &smtpd.Server{ + addr, closer := runserver(t, &smtpd.Server{ Handler: func(peer smtpd.Peer, env smtpd.Envelope) error { return smtpd.Error{Code: 550, Message: "Rejected"} }, - } + }) - go func() { - server.Serve(ln) - }() + defer closer() - c, err := smtp.Dial(ln.Addr().String()) + c, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -664,27 +573,18 @@ func TestRejectHandler(t *testing.T) { func TestMaxConnections(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } - - defer ln.Close() - - server := &smtpd.Server{ + addr, closer := runserver(t, &smtpd.Server{ MaxConnections: 1, - } + }) - go func() { - server.Serve(ln) - }() + defer closer() - c1, err := smtp.Dial(ln.Addr().String()) + c1, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } - _, err = smtp.Dial(ln.Addr().String()) + _, err = smtp.Dial(addr) if err == nil { t.Fatal("Dial succeeded despite MaxConnections = 1") } @@ -694,22 +594,13 @@ func TestMaxConnections(t *testing.T) { func TestNoMaxConnections(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } - - defer ln.Close() - - server := &smtpd.Server{ + addr, closer := runserver(t, &smtpd.Server{ MaxConnections: -1, - } + }) - go func() { - server.Serve(ln) - }() + defer closer() - c1, err := smtp.Dial(ln.Addr().String()) + c1, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -719,22 +610,13 @@ func TestNoMaxConnections(t *testing.T) { func TestMaxRecipients(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } - - defer ln.Close() - - server := &smtpd.Server{ + addr, closer := runserver(t, &smtpd.Server{ MaxRecipients: 1, - } + }) - go func() { - server.Serve(ln) - }() + defer closer() - c, err := smtp.Dial(ln.Addr().String()) + c, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -759,20 +641,11 @@ func TestMaxRecipients(t *testing.T) { func TestInvalidHelo(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } + addr, closer := runserver(t, &smtpd.Server{}) - defer ln.Close() + defer closer() - server := &smtpd.Server{} - - go func() { - server.Serve(ln) - }() - - c, err := smtp.Dial(ln.Addr().String()) + c, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -785,20 +658,11 @@ func TestInvalidHelo(t *testing.T) { func TestInvalidSender(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } + addr, closer := runserver(t, &smtpd.Server{}) - defer ln.Close() + defer closer() - server := &smtpd.Server{} - - go func() { - server.Serve(ln) - }() - - c, err := smtp.Dial(ln.Addr().String()) + c, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -811,20 +675,11 @@ func TestInvalidSender(t *testing.T) { func TestInvalidRecipient(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } + addr, closer := runserver(t, &smtpd.Server{}) - defer ln.Close() + defer closer() - server := &smtpd.Server{} - - go func() { - server.Serve(ln) - }() - - c, err := smtp.Dial(ln.Addr().String()) + c, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -841,20 +696,11 @@ func TestInvalidRecipient(t *testing.T) { func TestRCPTbeforeMAIL(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } + addr, closer := runserver(t, &smtpd.Server{}) - defer ln.Close() + defer closer() - server := &smtpd.Server{} - - go func() { - server.Serve(ln) - }() - - c, err := smtp.Dial(ln.Addr().String()) + c, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -867,24 +713,11 @@ func TestRCPTbeforeMAIL(t *testing.T) { func TestDATAbeforeRCPT(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } + addr, closer := runserver(t, &smtpd.Server{}) - defer ln.Close() + defer closer() - server := &smtpd.Server{ - Handler: func(peer smtpd.Peer, env smtpd.Envelope) error { - return smtpd.Error{Code: 550, Message: "Rejected"} - }, - } - - go func() { - server.Serve(ln) - }() - - c, err := smtp.Dial(ln.Addr().String()) + c, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -905,25 +738,16 @@ func TestDATAbeforeRCPT(t *testing.T) { func TestInterruptedDATA(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } - - defer ln.Close() - - server := &smtpd.Server{ + addr, closer := runserver(t, &smtpd.Server{ Handler: func(peer smtpd.Peer, env smtpd.Envelope) error { t.Fatal("Accepted DATA despite disconnection") return nil }, - } + }) - go func() { - server.Serve(ln) - }() + defer closer() - c, err := smtp.Dial(ln.Addr().String()) + c, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -952,31 +776,22 @@ func TestInterruptedDATA(t *testing.T) { func TestTimeoutClose(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } - - defer ln.Close() - - server := &smtpd.Server{ + addr, closer := runserver(t, &smtpd.Server{ MaxConnections: 1, ReadTimeout: time.Second, WriteTimeout: time.Second, - } + }) - go func() { - server.Serve(ln) - }() + defer closer() - c1, err := smtp.Dial(ln.Addr().String()) + c1, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } time.Sleep(time.Second * 2) - c2, err := smtp.Dial(ln.Addr().String()) + c2, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -998,31 +813,22 @@ func TestTimeoutClose(t *testing.T) { func TestTLSTimeout(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } - - defer ln.Close() - cert, err := tls.X509KeyPair(localhostCert, localhostKey) if err != nil { t.Fatalf("Cert load failed: %v", err) } - server := &smtpd.Server{ + addr, closer := runserver(t, &smtpd.Server{ TLSConfig: &tls.Config{ Certificates: []tls.Certificate{cert}, }, ReadTimeout: time.Second * 2, WriteTimeout: time.Second * 2, - } + }) - go func() { - server.Serve(ln) - }() + defer closer() - c, err := smtp.Dial(ln.Addr().String()) + c, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -1059,20 +865,11 @@ func TestTLSTimeout(t *testing.T) { func TestLongLine(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } + addr, closer := runserver(t, &smtpd.Server{}) - defer ln.Close() + defer closer() - server := &smtpd.Server{} - - go func() { - server.Serve(ln) - }() - - c, err := smtp.Dial(ln.Addr().String()) + c, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -1089,14 +886,7 @@ func TestLongLine(t *testing.T) { func TestXCLIENT(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } - - defer ln.Close() - - server := &smtpd.Server{ + addr, closer := runserver(t, &smtpd.Server{ EnableXCLIENT: true, SenderChecker: func(peer smtpd.Peer, addr string) error { if peer.HeloName != "new.example.net" { @@ -1113,13 +903,11 @@ func TestXCLIENT(t *testing.T) { } return nil }, - } + }) - go func() { - server.Serve(ln) - }() + defer closer() - c, err := smtp.Dial(ln.Addr().String()) + c, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -1168,19 +956,12 @@ func TestXCLIENT(t *testing.T) { func TestEnvelopeReceived(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } - - defer ln.Close() - cert, err := tls.X509KeyPair(localhostCert, localhostKey) if err != nil { t.Fatalf("Cert load failed: %v", err) } - server := &smtpd.Server{ + addr, closer := runserver(t, &smtpd.Server{ Hostname: "foobar.example.net", Handler: func(peer smtpd.Peer, env smtpd.Envelope) error { env.AddReceivedLine(peer) @@ -1193,13 +974,11 @@ func TestEnvelopeReceived(t *testing.T) { Certificates: []tls.Certificate{cert}, }, ForceTLS: true, - } + }) - go func() { - server.Serve(ln) - }() + defer closer() - c, err := smtp.Dial(ln.Addr().String()) + c, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -1239,20 +1018,11 @@ func TestEnvelopeReceived(t *testing.T) { func TestHELO(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } + addr, closer := runserver(t, &smtpd.Server{}) - defer ln.Close() + defer closer() - server := &smtpd.Server{} - - go func() { - server.Serve(ln) - }() - - c, err := smtp.Dial(ln.Addr().String()) + c, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -1280,30 +1050,22 @@ func TestHELO(t *testing.T) { } func TestLOGINAuth(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } - - defer ln.Close() cert, err := tls.X509KeyPair(localhostCert, localhostKey) if err != nil { t.Fatalf("Cert load failed: %v", err) } - server := &smtpd.Server{ + addr, closer := runserver(t, &smtpd.Server{ Authenticator: func(peer smtpd.Peer, username, password string) error { return nil }, TLSConfig: &tls.Config{ Certificates: []tls.Certificate{cert}, }, - } + }) - go func() { - server.Serve(ln) - }() + defer closer() - c, err := smtp.Dial(ln.Addr().String()) + c, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -1352,13 +1114,6 @@ func TestLOGINAuth(t *testing.T) { func TestErrors(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Listen failed: %v", err) - } - - defer ln.Close() - cert, err := tls.X509KeyPair(localhostCert, localhostKey) if err != nil { t.Fatalf("Cert load failed: %v", err) @@ -1368,11 +1123,11 @@ func TestErrors(t *testing.T) { Authenticator: func(peer smtpd.Peer, username, password string) error { return nil }, } - go func() { - server.Serve(ln) - }() + addr, closer := runserver(t, server) - c, err := smtp.Dial(ln.Addr().String()) + defer closer() + + c, err := smtp.Dial(addr) if err != nil { t.Fatalf("Dial failed: %v", err) } From 16f0241d5e253666abfad35feb32e96df0d1aea3 Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Sun, 27 Jul 2014 09:37:42 +0200 Subject: [PATCH 30/34] Refactored ssl test setup. --- smtpd_test.go | 75 +++++++++++++++------------------------------------ 1 file changed, 21 insertions(+), 54 deletions(-) diff --git a/smtpd_test.go b/smtpd_test.go index f487df7..1002e33 100644 --- a/smtpd_test.go +++ b/smtpd_test.go @@ -74,6 +74,21 @@ func runserver(t *testing.T, server *smtpd.Server) (addr string, closer func()) } +func runsslserver(t *testing.T, server *smtpd.Server) (addr string, closer func()) { + + cert, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + t.Fatalf("Cert load failed: %v", err) + } + + server.TLSConfig = &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + + return runserver(t, server) + +} + func TestSMTP(t *testing.T) { addr, closer := runserver(t, &smtpd.Server{}) @@ -170,16 +185,8 @@ func TestListenAndServe(t *testing.T) { func TestSTARTTLS(t *testing.T) { - cert, err := tls.X509KeyPair(localhostCert, localhostKey) - if err != nil { - t.Fatalf("Cert load failed: %v", err) - } - - addr, closer := runserver(t, &smtpd.Server{ + addr, closer := runsslserver(t, &smtpd.Server{ Authenticator: func(peer smtpd.Peer, username, password string) error { return nil }, - TLSConfig: &tls.Config{ - Certificates: []tls.Certificate{cert}, - }, ForceTLS: true, }) @@ -264,18 +271,10 @@ func TestSTARTTLS(t *testing.T) { func TestAuthRejection(t *testing.T) { - cert, err := tls.X509KeyPair(localhostCert, localhostKey) - if err != nil { - t.Fatalf("Cert load failed: %v", err) - } - - addr, closer := runserver(t, &smtpd.Server{ + addr, closer := runsslserver(t, &smtpd.Server{ Authenticator: func(peer smtpd.Peer, username, password string) error { return smtpd.Error{Code: 550, Message: "Denied"} }, - TLSConfig: &tls.Config{ - Certificates: []tls.Certificate{cert}, - }, ForceTLS: true, }) @@ -298,15 +297,7 @@ func TestAuthRejection(t *testing.T) { func TestAuthNotSupported(t *testing.T) { - cert, err := tls.X509KeyPair(localhostCert, localhostKey) - if err != nil { - t.Fatalf("Cert load failed: %v", err) - } - - addr, closer := runserver(t, &smtpd.Server{ - TLSConfig: &tls.Config{ - Certificates: []tls.Certificate{cert}, - }, + addr, closer := runsslserver(t, &smtpd.Server{ ForceTLS: true, }) @@ -813,15 +804,7 @@ func TestTimeoutClose(t *testing.T) { func TestTLSTimeout(t *testing.T) { - cert, err := tls.X509KeyPair(localhostCert, localhostKey) - if err != nil { - t.Fatalf("Cert load failed: %v", err) - } - - addr, closer := runserver(t, &smtpd.Server{ - TLSConfig: &tls.Config{ - Certificates: []tls.Certificate{cert}, - }, + addr, closer := runsslserver(t, &smtpd.Server{ ReadTimeout: time.Second * 2, WriteTimeout: time.Second * 2, }) @@ -956,12 +939,7 @@ func TestXCLIENT(t *testing.T) { func TestEnvelopeReceived(t *testing.T) { - cert, err := tls.X509KeyPair(localhostCert, localhostKey) - if err != nil { - t.Fatalf("Cert load failed: %v", err) - } - - addr, closer := runserver(t, &smtpd.Server{ + addr, closer := runsslserver(t, &smtpd.Server{ Hostname: "foobar.example.net", Handler: func(peer smtpd.Peer, env smtpd.Envelope) error { env.AddReceivedLine(peer) @@ -970,9 +948,6 @@ func TestEnvelopeReceived(t *testing.T) { } return nil }, - TLSConfig: &tls.Config{ - Certificates: []tls.Certificate{cert}, - }, ForceTLS: true, }) @@ -1051,16 +1026,8 @@ func TestHELO(t *testing.T) { func TestLOGINAuth(t *testing.T) { - cert, err := tls.X509KeyPair(localhostCert, localhostKey) - if err != nil { - t.Fatalf("Cert load failed: %v", err) - } - - addr, closer := runserver(t, &smtpd.Server{ + addr, closer := runsslserver(t, &smtpd.Server{ Authenticator: func(peer smtpd.Peer, username, password string) error { return nil }, - TLSConfig: &tls.Config{ - Certificates: []tls.Certificate{cert}, - }, }) defer closer() From 6ea6ce2c7537af4ae77ea2d701e320f5a35236a0 Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Sun, 27 Jul 2014 13:47:14 +0200 Subject: [PATCH 31/34] go fmt --- smtpd_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smtpd_test.go b/smtpd_test.go index 1002e33..3f6a2a0 100644 --- a/smtpd_test.go +++ b/smtpd_test.go @@ -68,7 +68,7 @@ func runserver(t *testing.T, server *smtpd.Server) (addr string, closer func()) ln.Close() }() - return ln.Addr().String(), func () { + return ln.Addr().String(), func() { done <- true } @@ -187,7 +187,7 @@ func TestSTARTTLS(t *testing.T) { addr, closer := runsslserver(t, &smtpd.Server{ Authenticator: func(peer smtpd.Peer, username, password string) error { return nil }, - ForceTLS: true, + ForceTLS: true, }) defer closer() From bca0c8ba29519608d5d751bb1468ee7d8daf0890 Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Sun, 27 Jul 2014 13:47:32 +0200 Subject: [PATCH 32/34] Example DKIM proxy using the smtpd package. --- examples/dkim-proxy/main.go | 79 +++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 examples/dkim-proxy/main.go diff --git a/examples/dkim-proxy/main.go b/examples/dkim-proxy/main.go new file mode 100644 index 0000000..64429b4 --- /dev/null +++ b/examples/dkim-proxy/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "bytes" + "flag" + "io/ioutil" + "log" + "net/smtp" + + "bitbucket.org/chrj/smtpd" + "github.com/eaigner/dkim" +) + +var ( + welcomeMsg = flag.String("welcome", "DKIM-proxy ESMTP ready.", "Welcome message for SMTP session") + inAddr = flag.String("inaddr", "localhost:10025", "Address to listen for incoming SMTP on") + outAddr = flag.String("outaddr", "localhost:25", "Address to deliver outgoing SMTP on") + privKeyFile = flag.String("key", "", "Private key file.") + dkimS = flag.String("s", "default", "DKIM selector") + dkimD = flag.String("d", "", "DKIM domain") + + dkimConf dkim.Conf + privKey []byte +) + +func handler(peer smtpd.Peer, env smtpd.Envelope) error { + + d, err := dkim.New(dkimConf, privKey) + if err != nil { + log.Printf("DKIM error: %v", err) + return smtpd.Error{450, "Internal server error"} + } + + // The dkim package expects \r\n newlines, so replace to that + data, err := d.Sign(bytes.Replace(env.Data, []byte("\n"), []byte("\r\n"), -1)) + if err != nil { + log.Printf("DKIM signing error: %v", err) + return smtpd.Error{450, "Internal server error"} + } + + return smtp.SendMail( + *outAddr, + nil, + env.Sender, + env.Recipients, + data, + ) + +} + +func main() { + + flag.Parse() + + var err error + + dkimConf, err = dkim.NewConf(*dkimD, *dkimS) + if err != nil { + log.Fatalf("DKIM configuration error: %v", err) + } + + privKey, err = ioutil.ReadFile(*privKeyFile) + if err != nil { + log.Fatalf("Couldn't read private key: %v", err) + } + + _, err = dkim.New(dkimConf, privKey) + if err != nil { + log.Fatalf("DKIM error: %v", err) + } + + server := &smtpd.Server{ + WelcomeMessage: *welcomeMsg, + Handler: handler, + } + + server.ListenAndServe(*inAddr) + +} From 24e6eeee9d5b0398fc08fd5072398cf5da1ea1a5 Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Sun, 27 Jul 2014 14:35:08 +0200 Subject: [PATCH 33/34] Add synopsis for dkim-proxy. --- examples/dkim-proxy/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/dkim-proxy/main.go b/examples/dkim-proxy/main.go index 64429b4..b9a8f45 100644 --- a/examples/dkim-proxy/main.go +++ b/examples/dkim-proxy/main.go @@ -1,3 +1,4 @@ +// Command dkim-proxy implements a simple SMTP proxy that DKIM signs incoming e-mail and relays to another SMTP server for delivery package main import ( From 843d6734482375d9e0ce9f9cb019d41867bd5998 Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Mon, 22 May 2017 19:45:42 +0200 Subject: [PATCH 34/34] Proxy protocol support, go vet. --- protocol.go | 64 +++++++++++++++++++++++++++++++++++++++++++++++++---- smtpd.go | 31 +++++++++++++++++++++++--- 2 files changed, 88 insertions(+), 7 deletions(-) diff --git a/protocol.go b/protocol.go index 85ade92..6d70294 100644 --- a/protocol.go +++ b/protocol.go @@ -48,6 +48,10 @@ func (session *session) handle(line string) { switch cmd.action { + case "PROXY": + session.handlePROXY(cmd) + return + case "HELO": session.handleHELO(cmd) return @@ -258,6 +262,7 @@ func (session *session) handleSTARTTLS(cmd command) { session.reply(220, "Go ahead") if err := tlsConn.Handshake(); err != nil { + session.logError(err, "couldn't perform handshake") session.reply(550, "Handshake error") return } @@ -417,13 +422,19 @@ func (session *session) handleAUTH(cmd command) { case "LOGIN": - session.reply(334, "VXNlcm5hbWU6") + encodedUsername := "" - if !session.scanner.Scan() { - return + if len(cmd.fields) < 3 { + session.reply(334, "VXNlcm5hbWU6") + if !session.scanner.Scan() { + return + } + encodedUsername = session.scanner.Text() + } else { + encodedUsername = cmd.fields[2] } - byteUsername, err := base64.StdEncoding.DecodeString(session.scanner.Text()) + byteUsername, err := base64.StdEncoding.DecodeString(encodedUsername) if err != nil { session.reply(502, "Couldn't decode your credentials") @@ -448,6 +459,7 @@ func (session *session) handleAUTH(cmd command) { default: + session.logf("unknown authentication mechanism: %s", mechanism) session.reply(502, "Unknown authentication mechanism") return @@ -564,3 +576,47 @@ func (session *session) handleXCLIENT(cmd command) { session.welcome() } + +func (session *session) handlePROXY(cmd command) { + + if !session.server.EnableProxyProtocol { + session.reply(550, "Proxy Protocol not enabled") + return + } + + if len(cmd.fields) < 6 { + session.reply(502, "Couldn't decode the command.") + return + } + + var ( + newAddr net.IP = nil + newTCPPort uint64 = 0 + err error + ) + + newAddr = net.ParseIP(cmd.fields[2]) + + newTCPPort, err = strconv.ParseUint(cmd.fields[4], 10, 16) + if err != nil { + session.reply(502, "Couldn't decode the command.") + return + } + + tcpAddr, ok := session.peer.Addr.(*net.TCPAddr) + if !ok { + session.reply(502, "Unsupported network connection") + return + } + + if newAddr != nil { + tcpAddr.IP = newAddr + } + + if newTCPPort != 0 { + tcpAddr.Port = int(newTCPPort) + } + + session.welcome() + +} diff --git a/smtpd.go b/smtpd.go index 08f84e0..0c24d4c 100644 --- a/smtpd.go +++ b/smtpd.go @@ -7,6 +7,7 @@ import ( "fmt" "log" "net" + "strings" "time" ) @@ -41,10 +42,13 @@ type Server struct { // Can be left empty for no authentication support. Authenticator func(peer Peer, username, password string) error - EnableXCLIENT bool // Enable XCLIENT support (default: false) + EnableXCLIENT bool // Enable XCLIENT support (default: false) + EnableProxyProtocol bool // Enable proxy protocol support (default: false) TLSConfig *tls.Config // Enable STARTTLS support. ForceTLS bool // Force STARTTLS usage. + + Logger *log.Logger } // Protocol represents the protocol used in the SMTP session @@ -212,12 +216,16 @@ func (session *session) serve() { defer session.close() - session.welcome() + if !session.server.EnableProxyProtocol { + session.welcome() + } for { for session.scanner.Scan() { - session.handle(session.scanner.Text()) + line := session.scanner.Text() + session.logf("received line: %s", strings.TrimSpace(line)) + session.handle(line) } err := session.scanner.Err() @@ -268,6 +276,7 @@ func (session *session) welcome() { } func (session *session) reply(code int, message string) { + session.logf("sending line: %d %s", code, message) fmt.Fprintf(session.writer, "%d %s\r\n", code, message) session.flush() } @@ -286,6 +295,22 @@ func (session *session) error(err error) { } } +func (session *session) logf(format string, v ...interface{}) { + if session.server.Logger == nil { + return + } + session.server.Logger.Output(2, fmt.Sprintf( + "%s [peer:%s]", + fmt.Sprintf(format, v...), + session.peer.Addr, + )) + +} + +func (session *session) logError(err error, desc string) { + session.logf("%s: %v ", desc, err) +} + func (session *session) extensions() []string { extensions := []string{