package smtp // http://www.rfc-editor.org/rfc/rfc5321.txt import ( "log" "net" "strings" "regexp" "github.com/ian-kent/MailHog/mailhog" "github.com/ian-kent/MailHog/mailhog/storage" "github.com/ian-kent/MailHog/mailhog/data" ) type Session struct { conn *net.TCPConn line string conf *mailhog.Config state int message *data.SMTPMessage } const ( ESTABLISH = iota MAIL RCPT DATA DONE ) // TODO replace ".." lines with . in data func StartSession(conn *net.TCPConn, conf *mailhog.Config) { conv := &Session{conn, "", conf, ESTABLISH, &data.SMTPMessage{}} conv.log("Starting session") conv.Write("220", conv.conf.Hostname + " ESMTP Go-MailHog") conv.Read() } func (c *Session) log(message string, args ...interface{}) { message = strings.Join([]string{"[SMTP %s, %d]", message}, " ") args = append([]interface{}{c.conn.RemoteAddr(), c.state}, args...) log.Printf(message, args...) } func (c *Session) Read() { buf := make([]byte, 1024) n, err := c.conn.Read(buf) if n == 0 { c.log("Connection closed by remote host\n") return } if err != nil { c.log("Error reading from socket: %s\n", err) return } text := string(buf[0:n]) c.log("Received %d bytes: '%s'\n", n, text) c.line += text c.Parse() } func (c *Session) Parse() { for strings.Contains(c.line, "\n") { parts := strings.SplitN(c.line, "\n", 2) if len(parts) == 2 { c.line = parts[1] } else { c.line = "" } if c.state == DATA { c.message.Data += parts[0] + "\n" if(strings.HasSuffix(c.message.Data, "\r\n.\r\n")) { c.message.Data = strings.TrimSuffix(c.message.Data, "\r\n.\r\n") id, err := storage.Store(c.conf, c.message) c.state = DONE if err != nil { // FIXME c.Write("500", "Error") return } c.Write("250", "Ok: queued as " + id) } } else { c.Process(strings.Trim(parts[0], "\r\n")) } } c.Read() } func (c *Session) Write(code string, text ...string) { if len(text) == 1 { c.conn.Write([]byte(code + " " + text[0] + "\n")) return } for i := 0; i < len(text) - 2; i++ { c.conn.Write([]byte(code + "-" + text[i] + "\n")) } c.conn.Write([]byte(code + " " + text[len(text)] + "\n")) } func (c *Session) Process(line string) { c.log("Processing line: %s", line) words := strings.Split(line, " ") command := words[0] args := strings.Join(words[1:len(words)], " ") c.log("In state %d, got command '%s', args '%s'", c.state, command, args) switch { case command == "RSET": c.log("Got RSET command, switching to ESTABLISH state") c.state = ESTABLISH c.message = &data.SMTPMessage{} c.Write("250", "Ok") case command == "NOOP": c.log("Got NOOP command") c.Write("250", "Ok") case command == "QUIT": c.log("Got QUIT command") c.Write("221", "Bye") err := c.conn.Close() if err != nil { c.log("Error closing connection") } case c.state == ESTABLISH: switch command { case "HELO": c.log("Got HELO command, switching to MAIL state") c.state = MAIL c.message.Helo = args c.Write("250", "Hello " + args) case "EHLO": c.log("Got EHLO command, switching to MAIL state") c.state = MAIL c.message.Helo = args c.Write("250", "Hello " + args) default: c.log("Got unknown command for ESTABLISH state: '%s'", command) } case c.state == MAIL: switch command { case "MAIL": c.log("Got MAIL command, switching to RCPT state") r, _ := regexp.Compile("From:<([^>]+)>") match := r.FindStringSubmatch(args) c.message.From = match[1] c.state = RCPT c.Write("250", "Sender " + match[1] + " ok") default: c.log("Got unknown command for MAIL state: '%s'", command) } case c.state == RCPT: switch command { case "RCPT": c.log("Got RCPT command") r, _ := regexp.Compile("To:<([^>]+)>") match := r.FindStringSubmatch(args) c.message.To = append(c.message.To, match[1]) c.state = RCPT c.Write("250", "Recipient " + match[1] + " ok") case "DATA": c.log("Got DATA command, switching to DATA state") c.state = DATA c.Write("354", "End data with .") default: c.log("Got unknown command for RCPT state: '%s'", command) } case c.state == DONE: switch command { /* case "MAIL": c.log("Got MAIL command") // TODO parse args c.message.From = args c.state = RCPT c.Write("250", "Ok") */ default: c.log("Got unknown command for DONE state: '%s'", command) } } }