A basic SMTP server.
authorRobert Sesek <rsesek@bluestatic.org>
Mon, 12 Dec 2016 05:57:40 +0000 (00:57 -0500)
committerRobert Sesek <rsesek@bluestatic.org>
Mon, 12 Dec 2016 05:57:40 +0000 (00:57 -0500)
config.go
smtp.go
smtp/conn.go
smtp/server.go

index 8f3c7347d0ec419dd0fef0ad8b2babe3cf76981f..c50104bf628b0cd9d896897acd88577b815f8226 100644 (file)
--- a/config.go
+++ b/config.go
@@ -4,6 +4,9 @@ type Config struct {
        SMTPPort int
        POP3Port int
 
+       // Hostname is the name of the MX server that is running.
+       Hostname string
+
        Servers []Server
 }
 
@@ -11,9 +14,6 @@ type Server struct {
        // Domain is the second component of a mail address: <local-part@domain.com>.
        Domain string
 
-       // Hostname is the name of the MX server that is running.
-       Hostname string
-
        TLSKeyPath  string
        TLSCertPath string
 
diff --git a/smtp.go b/smtp.go
index b7131d50e73300ea65f53539bc51c7d4e88f76b1..92ac2e50546b3e030062916d12a066bd6ff462a3 100644 (file)
--- a/smtp.go
+++ b/smtp.go
@@ -39,6 +39,10 @@ func (server *smtpServer) run() {
        }
 }
 
+func (server *smtpServer) Name() string {
+       return server.config.Hostname
+}
+
 func (server *smtpServer) OnEHLO() error {
        return nil
 }
index e4734ae093ffcd2a75b17d62ab725cb851620ab5..f07afe11d8b1e1742b3e1069d0abdfc7c734421e 100644 (file)
 package smtp
 
 import (
-       "io"
+       "fmt"
        "net"
+       "net/mail"
+       "net/textproto"
 )
 
-func AcceptConnection(conn net.Conn, server Server) error {
-       conn.Close()
-       return nil
+type state int
+
+const (
+       stateNew state = iota // Before EHOL.
+       stateInitial
+       stateMail
+       stateRecipient
+       stateData
+)
+
+type connection struct {
+       tp         *textproto.Conn
+       remoteAddr net.Addr
+
+       state
+       line string
+
+       ehlo     string
+       mailFrom *mail.Address
+       rcptTo   []mail.Address
+}
+
+func AcceptConnection(netConn net.Conn, server Server) error {
+       conn := connection{
+               tp:         textproto.NewConn(netConn),
+               remoteAddr: netConn.RemoteAddr(),
+               state:      stateNew,
+       }
+
+       var err error
+
+       conn.writeReply(250, fmt.Sprintf("%s ESMTP [%s] mailpopbox", server.Name(), netConn.LocalAddr().String()))
+
+       for {
+               conn.line, err = conn.tp.ReadLine()
+               if err != nil {
+                       conn.writeReply(500, "line too long")
+                       continue
+               }
+
+               var cmd string
+               if _, err = fmt.Sscanf(conn.line, "%s", &cmd); err != nil {
+                       conn.writeBadSyntax()
+                       continue
+               }
+
+               switch cmd {
+               case "QUIT":
+                       conn.tp.Close()
+                       break
+               case "HELO":
+                       fallthrough
+               case "EHLO":
+                       conn.doEHLO()
+               case "MAIL":
+                       conn.doMAIL()
+               case "RCPT":
+                       conn.doRCPT()
+               case "DATA":
+                       conn.doDATA()
+               case "RSET":
+                       conn.doRSET()
+               case "VRFY":
+                       conn.doVRFY()
+               case "EXPN":
+                       conn.writeReply(550, "access denied")
+               case "NOOP":
+                       conn.writeOK()
+               case "HELP":
+                       conn.writeReply(250, "https://tools.ietf.org/html/rfc5321")
+               default:
+                       conn.writeReply(500, "unrecognized command")
+               }
+       }
+
+       return err
+}
+
+func (conn *connection) writeReply(code int, msg string) {
+       if len(msg) > 0 {
+               conn.tp.PrintfLine("%d %s", code, msg)
+       } else {
+               conn.tp.PrintfLine("%d", code)
+       }
+}
+
+func (conn *connection) writeOK() {
+       conn.writeReply(250, "OK")
+}
+
+func (conn *connection) writeBadSyntax() {
+       conn.writeReply(501, "syntax error")
+}
+
+func (conn *connection) writeBadSequence() {
+       conn.writeReply(503, "bad sequence of commands")
+}
+
+func (conn *connection) doEHLO() {
+       conn.resetBuffers()
+
+       var cmd string
+       _, err := fmt.Sscanf(conn.line, "%s %s", &cmd, &conn.ehlo)
+       if err != nil {
+               conn.writeBadSyntax()
+               return
+       }
+
+       conn.writeReply(250, fmt.Sprintf("Hello %s, I am glad to meet you", conn.ehlo))
+
+       conn.state = stateInitial
+}
+
+func (conn *connection) doMAIL() {
+       if conn.state != stateInitial {
+               conn.writeBadSequence()
+               return
+       }
+
+       var mailFrom string
+       _, err := fmt.Sscanf(conn.line, "MAIL FROM:%s", &mailFrom)
+       if err != nil {
+               conn.writeBadSyntax()
+               return
+       }
+
+       conn.mailFrom, err = mail.ParseAddress(mailFrom)
+       if err != nil {
+               conn.writeBadSyntax()
+               return
+       }
+
+       conn.state = stateMail
+       conn.writeOK()
+}
+
+func (conn *connection) doRCPT() {
+       if conn.state != stateMail && conn.state != stateRecipient {
+               conn.writeBadSequence()
+               return
+       }
+
+       var rcptTo string
+       _, err := fmt.Sscanf(conn.line, "RCPT TO:%s", &rcptTo)
+       if err != nil {
+               conn.writeBadSyntax()
+               return
+       }
+
+       address, err := mail.ParseAddress(rcptTo)
+       if err != nil {
+               conn.writeBadSyntax()
+       }
+
+       conn.rcptTo = append(conn.rcptTo, *address)
+
+       conn.state = stateRecipient
+       conn.writeOK()
+}
+
+func (conn *connection) doDATA() {
+       if conn.state != stateRecipient {
+               conn.writeBadSequence()
+               return
+       }
+
+       conn.writeReply(354, "Start mail input; end with <CRLF>.<CRLF>")
+
+       data, err := conn.tp.ReadDotBytes()
+       if err != nil {
+               // TODO: log error
+               conn.writeReply(552, "transaction failed")
+               return
+       }
+
+       fmt.Println(string(data))
+
+       conn.state = stateInitial
+       conn.writeOK()
+}
+
+func (conn *connection) doVRFY() {
+}
+
+func (conn *connection) doRSET() {
+       conn.state = stateInitial
+       conn.resetBuffers()
+       conn.writeOK()
+}
+
+func (conn *connection) resetBuffers() {
+       conn.mailFrom = nil
+       conn.rcptTo = make([]mail.Address, 0)
 }
index e9539e857bcc29bf81d6dad7d829c293c2613074..fe935efa29c4de20d5e191a4726dc1b2ce9035a9 100644 (file)
@@ -1,6 +1,7 @@
 package smtp
 
 type Server interface {
+       Name() string
        OnEHLO() error
        OnMessageDelivered() error
 }