From ac162dc7ac199531958aec0fc2f0d093da539c67 Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Sun, 13 Jul 2014 23:24:13 +0200 Subject: [PATCH] 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 +}