From b46525f22339cf26f937048deae001f52c30f3e2 Mon Sep 17 00:00:00 2001 From: Ian Kent Date: Sat, 22 Nov 2014 16:11:04 +0000 Subject: [PATCH] Refactor SMTP protocol into separate package --- bindata.go | 10 +- mailhog/smtp/server/protocol.go | 318 ++++++++++++++++++++++ mailhog/smtp/server/session.go | 113 ++++++++ mailhog/smtp/{ => server}/session_test.go | 0 mailhog/smtp/session.go | 270 ------------------ main.go | 4 +- 6 files changed, 438 insertions(+), 277 deletions(-) create mode 100644 mailhog/smtp/server/protocol.go create mode 100644 mailhog/smtp/server/session.go rename mailhog/smtp/{ => server}/session_test.go (100%) delete mode 100644 mailhog/smtp/session.go diff --git a/bindata.go b/bindata.go index 1eab454..0eb930e 100644 --- a/bindata.go +++ b/bindata.go @@ -1593,6 +1593,11 @@ type _bintree_t struct { var _bintree = &_bintree_t{nil, map[string]*_bintree_t{ "assets": &_bintree_t{nil, map[string]*_bintree_t{ + "images": &_bintree_t{nil, map[string]*_bintree_t{ + "github.png": &_bintree_t{assets_images_github_png, map[string]*_bintree_t{}}, + "hog.png": &_bintree_t{assets_images_hog_png, map[string]*_bintree_t{}}, + "ajax-loader.gif": &_bintree_t{assets_images_ajax_loader_gif, map[string]*_bintree_t{}}, + }}, "js": &_bintree_t{nil, map[string]*_bintree_t{ "controllers.js": &_bintree_t{assets_js_controllers_js, map[string]*_bintree_t{}}, "strutil.js": &_bintree_t{assets_js_strutil_js, map[string]*_bintree_t{}}, @@ -1601,10 +1606,5 @@ var _bintree = &_bintree_t{nil, map[string]*_bintree_t{ "index.html": &_bintree_t{assets_templates_index_html, map[string]*_bintree_t{}}, "layout.html": &_bintree_t{assets_templates_layout_html, map[string]*_bintree_t{}}, }}, - "images": &_bintree_t{nil, map[string]*_bintree_t{ - "ajax-loader.gif": &_bintree_t{assets_images_ajax_loader_gif, map[string]*_bintree_t{}}, - "github.png": &_bintree_t{assets_images_github_png, map[string]*_bintree_t{}}, - "hog.png": &_bintree_t{assets_images_hog_png, map[string]*_bintree_t{}}, - }}, }}, }} diff --git a/mailhog/smtp/server/protocol.go b/mailhog/smtp/server/protocol.go new file mode 100644 index 0000000..5002f66 --- /dev/null +++ b/mailhog/smtp/server/protocol.go @@ -0,0 +1,318 @@ +package smtp + +// http://www.rfc-editor.org/rfc/rfc5321.txt + +import ( + "errors" + "log" + "regexp" + "strings" + + "github.com/ian-kent/Go-MailHog/mailhog/config" + "github.com/ian-kent/Go-MailHog/mailhog/data" +) + +// Protocol is a state machine representing an SMTP session +type Protocol struct { + conf *config.Config + state State + message *data.SMTPMessage + + LogHandler func(message string, args ...interface{}) + MessageReceivedHandler func(*data.Message) (string, error) +} + +// Command is a struct representing an SMTP command (verb + arguments) +type Command struct { + verb string + args string +} + +// Reply is a struct representing an SMTP reply (status code + lines) +type Reply struct { + status int + lines []string +} + +// ReplyOk creates a 250 Ok reply +func ReplyOk() *Reply { return &Reply{250, []string{"Ok"}} } + +// ReplyBye creates a 221 Bye reply +func ReplyBye() *Reply { return &Reply{221, []string{"Bye"}} } + +// ReplyUnrecognisedCommand creates a 500 Unrecognised command reply +func ReplyUnrecognisedCommand() *Reply { return &Reply{500, []string{"Unrecognised command"}} } + +// ReplySenderOk creates a 250 Sender ok reply +func ReplySenderOk(sender string) *Reply { return &Reply{250, []string{"Sender " + sender + " ok"}} } + +// ReplyRecipientOk creates a 250 Sender ok reply +func ReplyRecipientOk(recipient string) *Reply { + return &Reply{250, []string{"Recipient " + recipient + " ok"}} +} + +// ReplyError creates a 500 error reply +func ReplyError(err error) *Reply { return &Reply{550, []string{err.Error()}} } + +// State represents the state of an SMTP conversation +type State int + +// SMTP message conversation states +const ( + INVALID = State(-1) + ESTABLISH = State(iota) + AUTH + AUTH2 + MAIL + RCPT + DATA + DONE +) + +// StateMap provides string representations of SMTP conversation states +var StateMap = map[State]string{ + INVALID: "INVALID", + ESTABLISH: "ESTABLISH", + AUTH: "AUTH", + AUTH2: "AUTH2", + MAIL: "MAIL", + RCPT: "RCPT", + DATA: "DATA", + DONE: "DONE", +} + +// NewProtocol returns a new SMTP state machine in INVALID state +// handler is called when a message is received and should return a message ID +func NewProtocol(cfg *config.Config) *Protocol { + return &Protocol{ + conf: cfg, + state: INVALID, + message: &data.SMTPMessage{}, + } +} + +// TODO replace ".." lines with . in data + +func (proto *Protocol) logf(message string, args ...interface{}) { + message = strings.Join([]string{"[PROTO: %s]", message}, " ") + args = append([]interface{}{StateMap[proto.state]}, args...) + + if proto.LogHandler != nil { + proto.LogHandler(message, args...) + } else { + log.Printf(message, args...) + } +} + +// Start begins an SMTP conversation with a 220 reply +func (proto *Protocol) Start() *Reply { + proto.state = ESTABLISH + return &Reply{ + status: 220, + lines: []string{proto.conf.Hostname + " ESMTP Go-MailHog"}, + } +} + +// Parse parses a line string and returns any remaining line string +// and a reply, if a command was found. Parse does nothing until a +// new line is found. +// - TODO move this to a buffer inside proto? +func (proto *Protocol) Parse(line string) (string, *Reply) { + var reply *Reply + + for strings.Contains(line, "\n") { + parts := strings.SplitN(line, "\n", 2) + + if len(parts) == 2 { + line = parts[1] + } else { + line = "" + } + + if proto.state == DATA { + return line, proto.ProcessData(parts[0]) + } + + return line, proto.ProcessCommand(parts[0]) + } + + return line, reply +} + +// ProcessData handles content received (with newlines stripped) while +// in the SMTP DATA state +func (proto *Protocol) ProcessData(line string) (reply *Reply) { + proto.message.Data += line + "\n" + + if strings.HasSuffix(proto.message.Data, "\r\n.\r\n") { + proto.logf("Got EOF, storing message and switching to MAIL state") + proto.message.Data = strings.TrimSuffix(proto.message.Data, "\r\n.\r\n") + proto.state = MAIL + + msg := data.ParseSMTPMessage(proto.message, proto.conf.Hostname) + + if proto.MessageReceivedHandler != nil { + id, err := proto.MessageReceivedHandler(msg) + if err != nil { + proto.logf("Error storing message: %s", err) + reply = &Reply{452, []string{"Unable to store message"}} + } else { + reply = &Reply{250, []string{"Ok: queued as " + id}} + } + } else { + reply = &Reply{452, []string{"No storage backend"}} + } + } + + return +} + +// ProcessCommand processes a line of text as a command +// It expects the line string to be a properly formed SMTP verb and arguments +func (proto *Protocol) ProcessCommand(line string) (reply *Reply) { + line = strings.Trim(line, "\r\n") + proto.logf("Processing line: %s", line) + + words := strings.Split(line, " ") + command := strings.ToUpper(words[0]) + args := strings.Join(words[1:len(words)], " ") + proto.logf("In state %d, got command '%s', args '%s'", proto.state, command, args) + + cmd := &Command{command, args} + return proto.Command(cmd) +} + +// Command applies an SMTP verb and arguments to the state machine +func (proto *Protocol) Command(command *Command) (reply *Reply) { + switch { + case "RSET" == command.verb: + proto.logf("Got RSET command, switching to MAIL state") + proto.state = MAIL + proto.message = &data.SMTPMessage{} + return ReplyOk() + case "NOOP" == command.verb: + proto.logf("Got NOOP verb, staying in %s state", StateMap[proto.state]) + return ReplyOk() + case "QUIT" == command.verb: + proto.logf("Got QUIT verb, staying in %s state", StateMap[proto.state]) + return ReplyBye() + case ESTABLISH == proto.state: + switch command.verb { + case "HELO": + return proto.HELO(command.args) + case "EHLO": + return proto.EHLO(command.args) + default: + proto.logf("Got unknown command for ESTABLISH state: '%s'", command.verb) + return ReplyUnrecognisedCommand() + } + case AUTH == proto.state: + proto.logf("Got authentication response: '%s', switching to MAIL state", command.args) + proto.state = MAIL + return &Reply{235, []string{"Authentication successful"}} + case AUTH2 == proto.state: // TODO rename AUTH2 state... + proto.logf("Got LOGIN authentication response: '%s', switching to AUTH state", command.args) + proto.state = AUTH + return &Reply{334, []string{"UGFzc3dvcmQ6"}} + case MAIL == proto.state: // TODO rename/split state + switch command.verb { + case "AUTH": + proto.logf("Got AUTH command, staying in MAIL state") + switch { + case strings.HasPrefix(command.args, "PLAIN "): + proto.logf("Got PLAIN authentication: %s", strings.TrimPrefix(command.args, "PLAIN ")) + return &Reply{235, []string{"Authentication successful"}} + case "LOGIN" == command.args: + proto.logf("Got LOGIN authentication, switching to AUTH state") + proto.state = AUTH2 + return &Reply{334, []string{"VXNlcm5hbWU6"}} + case "PLAIN" == command.args: + proto.logf("Got PLAIN authentication (no args), switching to AUTH2 state") + proto.state = AUTH + return &Reply{334, []string{}} + case "CRAM-MD5" == command.args: + proto.logf("Got CRAM-MD5 authentication, switching to AUTH state") + proto.state = AUTH + return &Reply{334, []string{"PDQxOTI5NDIzNDEuMTI4Mjg0NzJAc291cmNlZm91ci5hbmRyZXcuY211LmVkdT4="}} + case strings.HasPrefix(command.args, "EXTERNAL "): + proto.logf("Got EXTERNAL authentication: %s", strings.TrimPrefix(command.args, "EXTERNAL ")) + return &Reply{235, []string{"Authentication successful"}} + default: + return &Reply{504, []string{"Unsupported authentication mechanism"}} + } + case "MAIL": + proto.logf("Got MAIL command, switching to RCPT state") + from, err := ParseMAIL(command.args) + if err != nil { + return ReplyError(err) + } + proto.message.From = from + proto.state = RCPT + return ReplySenderOk(from) + case "HELO": // TODO feels hacky + return proto.HELO(command.args) + case "EHLO": // TODO feels hacky? + return proto.EHLO(command.args) + default: + proto.logf("Got unknown command for MAIL state: '%s'", command) + return ReplyUnrecognisedCommand() + } + case RCPT == proto.state: + switch command.verb { + case "RCPT": + proto.logf("Got RCPT command") + to, err := ParseRCPT(command.args) + if err != nil { + return ReplyError(err) + } + proto.message.To = append(proto.message.To, to) + proto.state = RCPT + return ReplyRecipientOk(to) + case "DATA": + proto.logf("Got DATA command, switching to DATA state") + proto.state = DATA + return &Reply{354, []string{"End data with ."}} + default: + proto.logf("Got unknown command for RCPT state: '%s'", command) + return ReplyUnrecognisedCommand() + } + default: + return ReplyUnrecognisedCommand() + } +} + +// HELO creates a reply to a HELO command +func (proto *Protocol) HELO(args string) (reply *Reply) { + proto.logf("Got HELO command, switching to MAIL state") + proto.state = MAIL + proto.message.Helo = args + return &Reply{250, []string{"Hello " + args}} +} + +// EHLO creates a reply to a EHLO command +func (proto *Protocol) EHLO(args string) (reply *Reply) { + proto.logf("Got EHLO command, switching to MAIL state") + proto.state = MAIL + proto.message.Helo = args + return &Reply{250, []string{"Hello " + args, "PIPELINING", "AUTH EXTERNAL CRAM-MD5 LOGIN PLAIN"}} +} + +// ParseMAIL returns the forward-path from a MAIL command argument +func ParseMAIL(mail string) (string, error) { + r := regexp.MustCompile("(?i:From):<([^>]+)>") + match := r.FindStringSubmatch(mail) + if len(match) != 2 { + return "", errors.New("Invalid sender") + } + return match[1], nil +} + +// ParseRCPT returns the return-path from a RCPT command argument +func ParseRCPT(rcpt string) (string, error) { + r := regexp.MustCompile("(?i:To):<([^>]+)>") + match := r.FindStringSubmatch(rcpt) + if len(match) != 2 { + return "", errors.New("Invalid recipient") + } + return match[1], nil +} diff --git a/mailhog/smtp/server/session.go b/mailhog/smtp/server/session.go new file mode 100644 index 0000000..929bc51 --- /dev/null +++ b/mailhog/smtp/server/session.go @@ -0,0 +1,113 @@ +package smtp + +// http://www.rfc-editor.org/rfc/rfc5321.txt + +import ( + "errors" + "log" + "net" + "strconv" + "strings" + + "github.com/ian-kent/Go-MailHog/mailhog/config" + "github.com/ian-kent/Go-MailHog/mailhog/data" + "github.com/ian-kent/Go-MailHog/mailhog/storage" +) + +// Session represents a SMTP session using net.TCPConn +type Session struct { + conn *net.TCPConn + proto *Protocol + conf *config.Config + isTLS bool + line string +} + +// Accept starts a new SMTP session using net.TCPConn +func Accept(conn *net.TCPConn, conf *config.Config) { + proto := NewProtocol(conf) + session := &Session{conn, proto, conf, false, ""} + proto.LogHandler = session.logf + proto.MessageReceivedHandler = session.acceptMessageHandler + + session.logf("Starting session") + session.Write(proto.Start()) + for session.Read() == true { + } + session.logf("Session ended") +} + +func (c *Session) acceptMessageHandler(msg *data.Message) (id string, err error) { + switch c.conf.Storage.(type) { + case *storage.MongoDB: + c.logf("Storing message using MongoDB") + id, err = c.conf.Storage.(*storage.MongoDB).Store(msg) + case *storage.Memory: + c.logf("Storing message using Memory") + id, err = c.conf.Storage.(*storage.Memory).Store(msg) + default: + err = errors.New("Unknown storage stype") + } + return +} + +func (c *Session) logf(message string, args ...interface{}) { + message = strings.Join([]string{"[SMTP %s]", message}, " ") + args = append([]interface{}{c.conn.RemoteAddr()}, args...) + log.Printf(message, args...) +} + +// Read reads from the underlying net.TCPConn +func (c *Session) Read() bool { + buf := make([]byte, 1024) + n, err := c.conn.Read(buf) + + if n == 0 { + c.logf("Connection closed by remote host\n") + return false + } + if err != nil { + c.logf("Error reading from socket: %s\n", err) + return false + } + + text := string(buf[0:n]) + logText := strings.Replace(text, "\n", "\\n", -1) + logText = strings.Replace(logText, "\r", "\\r", -1) + c.logf("Received %d bytes: '%s'\n", n, logText) + + c.line += text + + line, reply := c.proto.Parse(c.line) + c.line = line + + if reply != nil { + c.Write(reply) + if reply.status == 221 { + c.conn.Close() + } + } + + return true +} + +// Write writes a reply to the underlying net.TCPConn +func (c *Session) Write(reply *Reply) { + if len(reply.lines) == 0 { + l := strconv.Itoa(reply.status) + c.logf("Sent %d bytes: '%s'", len(l), l) + c.conn.Write([]byte(l)) + } + for i, line := range reply.lines { + l := "" + if i == len(reply.lines)-1 { + l = strconv.Itoa(reply.status) + " " + line + "\n" + } else { + l = strconv.Itoa(reply.status) + "-" + line + "\n" + } + logText := strings.Replace(l, "\n", "\\n", -1) + logText = strings.Replace(logText, "\r", "\\r", -1) + c.logf("Sent %d bytes: '%s'", len(l), logText) + c.conn.Write([]byte(l)) + } +} diff --git a/mailhog/smtp/session_test.go b/mailhog/smtp/server/session_test.go similarity index 100% rename from mailhog/smtp/session_test.go rename to mailhog/smtp/server/session_test.go diff --git a/mailhog/smtp/session.go b/mailhog/smtp/session.go deleted file mode 100644 index e418f1f..0000000 --- a/mailhog/smtp/session.go +++ /dev/null @@ -1,270 +0,0 @@ -package smtp - -// http://www.rfc-editor.org/rfc/rfc5321.txt - -import ( - "errors" - "log" - "net" - "regexp" - "strings" - - "github.com/ian-kent/Go-MailHog/mailhog/config" - "github.com/ian-kent/Go-MailHog/mailhog/data" - "github.com/ian-kent/Go-MailHog/mailhog/storage" -) - -type Session struct { - conn *net.TCPConn - line string - conf *config.Config - state int - message *data.SMTPMessage - isTLS bool -} - -const ( - ESTABLISH = iota - AUTH - AUTH2 - MAIL - RCPT - DATA - DONE -) - -// TODO replace ".." lines with . in data - -func StartSession(conn *net.TCPConn, conf *config.Config) { - conv := &Session{conn, "", conf, ESTABLISH, &data.SMTPMessage{}, false} - 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.log("Got EOF, storing message and switching to MAIL state") - //c.log("Full message data: %s", c.message.Data) - c.message.Data = strings.TrimSuffix(c.message.Data, "\r\n.\r\n") - msg := data.ParseSMTPMessage(c.message, c.conf.Hostname) - var id string - var err error - switch c.conf.Storage.(type) { - case *storage.MongoDB: - c.log("Storing message using MongoDB") - id, err = c.conf.Storage.(*storage.MongoDB).Store(msg) - case *storage.Memory: - c.log("Storing message using Memory") - id, err = c.conf.Storage.(*storage.Memory).Store(msg) - default: - c.log("Unknown storage type") - // TODO send error reply - } - c.state = MAIL - if err != nil { - c.log("Error storing message: %s", err) - c.Write("452", "Unable to store message") - return - } - c.Write("250", "Ok: queued as "+id) - c.conf.MessageChan <- msg - } - } else { - c.Process(strings.Trim(parts[0], "\r\n")) - } - } - - c.Read() -} - -func (c *Session) Write(code string, text ...string) { - if len(text) == 1 { - c.log("Sent %d bytes: '%s'", len(text[0]+"\n"), text[0]+"\n") - c.conn.Write([]byte(code + " " + text[0] + "\n")) - return - } - for i := 0; i < len(text)-1; i++ { - c.log("Sent %d bytes: '%s'", len(text[i]+"\n"), text[i]+"\n") - c.conn.Write([]byte(code + "-" + text[i] + "\n")) - } - c.log("Sent %d bytes: '%s'", len(text[len(text)-1]+"\n"), text[len(text)-1]+"\n") - c.conn.Write([]byte(code + " " + text[len(text)-1] + "\n")) -} - -func (c *Session) Process(line string) { - c.log("Processing line: %s", line) - - words := strings.Split(line, " ") - command := strings.ToUpper(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 MAIL state") - c.state = MAIL - 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.DoHELO(args) - case "EHLO": - c.DoEHLO(args) - default: - c.log("Got unknown command for ESTABLISH state: '%s'", command) - c.Write("500", "Unrecognised command") - } - case c.state == AUTH: - c.log("Got authentication response: '%s', switching to MAIL state", args) - c.state = MAIL - c.Write("235", "Authentication successful") - case c.state == AUTH2: - c.log("Got LOGIN authentication response: '%s', switching to AUTH state", args) - c.state = AUTH - c.Write("334", "UGFzc3dvcmQ6") - case c.state == MAIL: // TODO rename/split state - switch command { - case "AUTH": - c.log("Got AUTH command, staying in MAIL state") - switch { - case strings.HasPrefix(args, "PLAIN "): - c.log("Got PLAIN authentication: %s", strings.TrimPrefix(args, "PLAIN ")) - c.Write("235", "Authentication successful") - case args == "LOGIN": - c.log("Got LOGIN authentication, switching to AUTH state") - c.state = AUTH2 - c.Write("334", "VXNlcm5hbWU6") - case args == "PLAIN": - c.log("Got PLAIN authentication (no args), switching to AUTH2 state") - c.state = AUTH - c.Write("334", "") - case args == "CRAM-MD5": - c.log("Got CRAM-MD5 authentication, switching to AUTH state") - c.state = AUTH - c.Write("334", "PDQxOTI5NDIzNDEuMTI4Mjg0NzJAc291cmNlZm91ci5hbmRyZXcuY211LmVkdT4=") - case strings.HasPrefix(args, "EXTERNAL "): - c.log("Got EXTERNAL authentication: %s", strings.TrimPrefix(args, "EXTERNAL ")) - c.Write("235", "Authentication successful") - default: - c.Write("504", "Unsupported authentication mechanism") - } - case "MAIL": - c.log("Got MAIL command, switching to RCPT state") - from, err := ParseMAIL(args) - if err != nil { - c.Write("550", err.Error()) - return - } - c.message.From = from - c.state = RCPT - c.Write("250", "Sender "+from+" ok") - case "HELO": - c.DoHELO(args) - case "EHLO": - c.DoEHLO(args) - default: - c.log("Got unknown command for MAIL state: '%s'", command) - c.Write("500", "Unrecognised command") - } - case c.state == RCPT: - switch command { - case "RCPT": - c.log("Got RCPT command") - to, err := ParseRCPT(args) - if err != nil { - c.Write("550", err.Error()) - return - } - c.message.To = append(c.message.To, to) - c.state = RCPT - c.Write("250", "Recipient "+to+" 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) - c.Write("500", "Unrecognised command") - } - } -} - -func (c *Session) DoHELO(args string) { - c.log("Got HELO command, switching to MAIL state") - c.state = MAIL - c.message.Helo = args - c.Write("250", "Hello "+args) -} - -func (c *Session) DoEHLO(args string) { - c.log("Got EHLO command, switching to MAIL state") - c.state = MAIL - c.message.Helo = args - c.Write("250", "Hello "+args, "PIPELINING", "AUTH EXTERNAL CRAM-MD5 LOGIN PLAIN") -} - -func ParseMAIL(mail string) (string, error) { - r := regexp.MustCompile("(?i:From):<([^>]+)>") - match := r.FindStringSubmatch(mail) - if len(match) != 2 { - return "", errors.New("Invalid sender") - } - return match[1], nil -} - -func ParseRCPT(rcpt string) (string, error) { - r := regexp.MustCompile("(?i:To):<([^>]+)>") - match := r.FindStringSubmatch(rcpt) - if len(match) != 2 { - return "", errors.New("Invalid recipient") - } - return match[1], nil -} diff --git a/main.go b/main.go index 4e8cc86..633190a 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,7 @@ import ( "github.com/ian-kent/Go-MailHog/mailhog/config" mhhttp "github.com/ian-kent/Go-MailHog/mailhog/http" "github.com/ian-kent/Go-MailHog/mailhog/http/api" - "github.com/ian-kent/Go-MailHog/mailhog/smtp" + smtp "github.com/ian-kent/Go-MailHog/mailhog/smtp/server" "github.com/ian-kent/Go-MailHog/mailhog/storage" "github.com/ian-kent/envconf" "github.com/ian-kent/go-log/log" @@ -122,6 +122,6 @@ func smtp_listen() *net.TCPListener { } defer conn.Close() - go smtp.StartSession(conn.(*net.TCPConn), conf) + go smtp.Accept(conn.(*net.TCPConn), conf) } }