diff --git a/MailHog-Server/smtp/session.go b/MailHog-Server/smtp/session.go index 4db5776..89dea67 100644 --- a/MailHog-Server/smtp/session.go +++ b/MailHog-Server/smtp/session.go @@ -9,15 +9,15 @@ import ( "github.com/ian-kent/Go-MailHog/MailHog-Server/monkey" "github.com/ian-kent/Go-MailHog/data" - "github.com/ian-kent/Go-MailHog/smtp/protocol" "github.com/ian-kent/Go-MailHog/storage" "github.com/ian-kent/linkio" + "github.com/mailhog/smtp" ) // Session represents a SMTP session using net.TCPConn type Session struct { conn io.ReadWriteCloser - proto *protocol.Protocol + proto *smtp.Protocol storage storage.Storage messageChan chan *data.Message remoteAddress string @@ -34,7 +34,7 @@ type Session struct { func Accept(remoteAddress string, conn io.ReadWriteCloser, storage storage.Storage, messageChan chan *data.Message, hostname string, monkey monkey.ChaosMonkey) { defer conn.Close() - proto := protocol.NewProtocol() + proto := smtp.NewProtocol() proto.Hostname = hostname var link *linkio.Link reader := io.Reader(conn) @@ -66,12 +66,12 @@ func Accept(remoteAddress string, conn io.ReadWriteCloser, storage storage.Stora session.logf("Session ended") } -func (c *Session) validateAuthentication(mechanism string, args ...string) (errorReply *protocol.Reply, ok bool) { +func (c *Session) validateAuthentication(mechanism string, args ...string) (errorReply *smtp.Reply, ok bool) { if c.monkey != nil { ok := c.monkey.ValidAUTH(mechanism, args...) if !ok { // FIXME better error? - return protocol.ReplyUnrecognisedCommand(), false + return smtp.ReplyUnrecognisedCommand(), false } } return nil, true @@ -150,7 +150,7 @@ func (c *Session) Read() bool { } // Write writes a reply to the underlying net.TCPConn -func (c *Session) Write(reply *protocol.Reply) { +func (c *Session) Write(reply *smtp.Reply) { lines := reply.Lines() for _, l := range lines { logText := strings.Replace(l, "\n", "\\n", -1) diff --git a/smtp/protocol/protocol.go b/smtp/protocol/protocol.go deleted file mode 100644 index 022a19f..0000000 --- a/smtp/protocol/protocol.go +++ /dev/null @@ -1,372 +0,0 @@ -package protocol - -// http://www.rfc-editor.org/rfc/rfc5321.txt - -import ( - "errors" - "log" - "regexp" - "strings" - - "github.com/ian-kent/Go-MailHog/data" -) - -// Command is a struct representing an SMTP command (verb + arguments) -type Command struct { - verb string - args string - orig string -} - -// ParseCommand returns a Command from the line string -func ParseCommand(line string) *Command { - words := strings.Split(line, " ") - command := strings.ToUpper(words[0]) - args := strings.Join(words[1:len(words)], " ") - - return &Command{ - verb: command, - args: args, - orig: line, - } -} - -// Protocol is a state machine representing an SMTP session -type Protocol struct { - state State - message *data.SMTPMessage - - lastCommand *Command - - Hostname string - Ident string - - // LogHandler is called for each log message. If nil, log messages will - // be output using log.Printf instead. - LogHandler func(message string, args ...interface{}) - // MessageReceivedHandler is called for each message accepted by the - // SMTP protocol. It must return a MessageID or error. If nil, messages - // will be rejected with an error. - MessageReceivedHandler func(*data.Message) (string, error) - // ValidateSenderHandler should return true if the sender is valid, - // otherwise false. If nil, all senders will be accepted. - ValidateSenderHandler func(from string) bool - // ValidateRecipientHandler should return true if the recipient is valid, - // otherwise false. If nil, all recipients will be accepted. - ValidateRecipientHandler func(to string) bool - // ValidateAuthenticationhandler should return true if the authentication - // parameters are valid, otherwise false. If nil, all authentication - // attempts will be accepted. - ValidateAuthenticationHandler func(mechanism string, args ...string) (errorReply *Reply, ok bool) - - // RejectBrokenRCPTSyntax controls whether the protocol accepts technically - // invalid syntax for the RCPT command. Set to true, the RCPT syntax requires - // no space between `TO:` and the opening `<` - RejectBrokenRCPTSyntax bool - // RejectBrokenMAILSyntax controls whether the protocol accepts technically - // invalid syntax for the MAIL command. Set to true, the MAIL syntax requires - // no space between `FROM:` and the opening `<` - RejectBrokenMAILSyntax bool -} - -// 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() *Protocol { - return &Protocol{ - state: INVALID, - message: &data.SMTPMessage{}, - Hostname: "mailhog.example", - Ident: "ESMTP Go-MailHog", - } -} - -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, placing the state -// machine in ESTABLISH state. -func (proto *Protocol) Start() *Reply { - proto.logf("Started session, switching to ESTABLISH state") - proto.state = ESTABLISH - return ReplyIdent(proto.Hostname + " " + proto.Ident) -} - -// 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 decide whether to move this to a buffer inside Protocol -// sort of like it this way, since it gives control back to the caller -func (proto *Protocol) Parse(line string) (string, *Reply) { - var reply *Reply - - if !strings.Contains(line, "\n") { - return line, reply - } - - parts := strings.SplitN(line, "\n", 2) - line = parts[1] - - // TODO collapse AUTH states into separate processing - if proto.state == DATA { - reply = proto.ProcessData(parts[0]) - } else { - reply = 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.message.Data = strings.Replace(proto.message.Data, "\r\n..", "\r\n.", -1) - - 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 := proto.message.Parse(proto.Hostname) - - if proto.MessageReceivedHandler == nil { - return ReplyStorageFailed("No storage backend") - } - - id, err := proto.MessageReceivedHandler(msg) - if err != nil { - proto.logf("Error storing message: %s", err) - return ReplyStorageFailed("Unable to store message") - } - return ReplyOk("Ok: queued as " + id) - } - - 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 := ParseCommand(strings.TrimSuffix(line, "\r\n")) - return proto.Command(cmd) -} - -// Command applies an SMTP verb and arguments to the state machine -func (proto *Protocol) Command(command *Command) (reply *Reply) { - defer func() { - proto.lastCommand = command - }() - 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]) - proto.state = DONE - 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 AUTHPLAIN == proto.state: - proto.logf("Got PLAIN authentication response: '%s', switching to MAIL state", command.args) - proto.state = MAIL - if proto.ValidateAuthenticationHandler != nil { - if reply, ok := proto.ValidateAuthenticationHandler("PLAIN", command.orig); !ok { - return reply - } - } - return ReplyAuthOk() - case AUTHLOGIN == proto.state: - proto.logf("Got LOGIN authentication response: '%s', switching to AUTHLOGIN2 state", command.args) - proto.state = AUTHLOGIN2 - return ReplyAuthResponse("UGFzc3dvcmQ6") - case AUTHLOGIN2 == proto.state: - proto.logf("Got LOGIN authentication response: '%s', switching to MAIL state", command.args) - proto.state = MAIL - if proto.ValidateAuthenticationHandler != nil { - if reply, ok := proto.ValidateAuthenticationHandler("LOGIN", proto.lastCommand.orig, command.orig); !ok { - return reply - } - } - return ReplyAuthOk() - case AUTHCRAMMD5 == proto.state: - proto.logf("Got CRAM-MD5 authentication response: '%s', switching to MAIL state", command.args) - proto.state = MAIL - if proto.ValidateAuthenticationHandler != nil { - if reply, ok := proto.ValidateAuthenticationHandler("CRAM-MD5", command.orig); !ok { - return reply - } - } - return ReplyAuthOk() - case MAIL == proto.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 ")) - if proto.ValidateAuthenticationHandler != nil { - if reply, ok := proto.ValidateAuthenticationHandler("PLAIN", strings.TrimPrefix(command.args, "PLAIN ")); !ok { - return reply - } - } - return ReplyAuthOk() - case "LOGIN" == command.args: - proto.logf("Got LOGIN authentication, switching to AUTH state") - proto.state = AUTHLOGIN - return ReplyAuthResponse("VXNlcm5hbWU6") - case "PLAIN" == command.args: - proto.logf("Got PLAIN authentication (no args), switching to AUTH2 state") - proto.state = AUTHPLAIN - return ReplyAuthResponse("") - case "CRAM-MD5" == command.args: - proto.logf("Got CRAM-MD5 authentication, switching to AUTH state") - proto.state = AUTHCRAMMD5 - return ReplyAuthResponse("PDQxOTI5NDIzNDEuMTI4Mjg0NzJAc291cmNlZm91ci5hbmRyZXcuY211LmVkdT4=") - case strings.HasPrefix(command.args, "EXTERNAL "): - proto.logf("Got EXTERNAL authentication: %s", strings.TrimPrefix(command.args, "EXTERNAL ")) - if proto.ValidateAuthenticationHandler != nil { - if reply, ok := proto.ValidateAuthenticationHandler("EXTERNAL", strings.TrimPrefix(command.args, "EXTERNAL ")); !ok { - return reply - } - } - return ReplyAuthOk() - default: - return ReplyUnsupportedAuth() - } - case "MAIL": - proto.logf("Got MAIL command, switching to RCPT state") - from, err := proto.ParseMAIL(command.args) - if err != nil { - return ReplyError(err) - } - if proto.ValidateSenderHandler != nil { - if !proto.ValidateSenderHandler(from) { - // TODO correct sender error response - return ReplyError(errors.New("Invalid sender " + from)) - } - } - proto.message.From = from - proto.state = RCPT - return ReplySenderOk(from) - case "HELO": - return proto.HELO(command.args) - case "EHLO": - 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 := proto.ParseRCPT(command.args) - if err != nil { - return ReplyError(err) - } - if proto.ValidateRecipientHandler != nil { - if !proto.ValidateRecipientHandler(to) { - // TODO correct send error response - return ReplyError(errors.New("Invalid recipient " + to)) - } - } - proto.message.To = append(proto.message.To, to) - proto.state = RCPT - return ReplyRecipientOk(to) - case "HELO": - return proto.HELO(command.args) - case "EHLO": - return proto.EHLO(command.args) - case "DATA": - proto.logf("Got DATA command, switching to DATA state") - proto.state = DATA - return ReplyDataResponse() - 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 ReplyOk("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 ReplyOk("Hello "+args, "PIPELINING", "AUTH EXTERNAL CRAM-MD5 LOGIN PLAIN") -} - -var parseMailBrokenRegexp = regexp.MustCompile("(?i:From):\\s*<([^>]+)>") -var parseMailRFCRegexp = regexp.MustCompile("(?i:From):<([^>]+)>") - -// ParseMAIL returns the forward-path from a MAIL command argument -func (proto *Protocol) ParseMAIL(mail string) (string, error) { - var match []string - if proto.RejectBrokenMAILSyntax { - match = parseMailRFCRegexp.FindStringSubmatch(mail) - } else { - match = parseMailBrokenRegexp.FindStringSubmatch(mail) - } - - if len(match) != 2 { - return "", errors.New("Invalid syntax in MAIL command") - } - return match[1], nil -} - -var parseRcptBrokenRegexp = regexp.MustCompile("(?i:To):\\s*<([^>]+)>") -var parseRcptRFCRegexp = regexp.MustCompile("(?i:To):<([^>]+)>") - -// ParseRCPT returns the return-path from a RCPT command argument -func (proto *Protocol) ParseRCPT(rcpt string) (string, error) { - var match []string - if proto.RejectBrokenRCPTSyntax { - match = parseRcptRFCRegexp.FindStringSubmatch(rcpt) - } else { - match = parseRcptBrokenRegexp.FindStringSubmatch(rcpt) - } - if len(match) != 2 { - return "", errors.New("Invalid syntax in RCPT command") - } - return match[1], nil -} diff --git a/smtp/protocol/protocol_test.go b/smtp/protocol/protocol_test.go deleted file mode 100644 index 567beb3..0000000 --- a/smtp/protocol/protocol_test.go +++ /dev/null @@ -1,933 +0,0 @@ -package protocol - -// http://www.rfc-editor.org/rfc/rfc5321.txt - -import ( - "errors" - "testing" - - "github.com/ian-kent/Go-MailHog/data" - . "github.com/smartystreets/goconvey/convey" -) - -func TestProtocol(t *testing.T) { - Convey("NewProtocol returns a new Protocol", t, func() { - proto := NewProtocol() - So(proto, ShouldNotBeNil) - So(proto, ShouldHaveSameTypeAs, &Protocol{}) - So(proto.Hostname, ShouldEqual, "mailhog.example") - So(proto.Ident, ShouldEqual, "ESMTP Go-MailHog") - So(proto.state, ShouldEqual, INVALID) - So(proto.message, ShouldNotBeNil) - So(proto.message, ShouldHaveSameTypeAs, &data.SMTPMessage{}) - }) - - Convey("LogHandler should be called for logging", t, func() { - proto := NewProtocol() - handlerCalled := false - proto.LogHandler = func(message string, args ...interface{}) { - handlerCalled = true - So(message, ShouldEqual, "[PROTO: %s] Test message %s %s") - So(len(args), ShouldEqual, 3) - So(args[0], ShouldEqual, "INVALID") - So(args[1], ShouldEqual, "test arg 1") - So(args[2], ShouldEqual, "test arg 2") - } - proto.logf("Test message %s %s", "test arg 1", "test arg 2") - So(handlerCalled, ShouldBeTrue) - }) - - Convey("Start should modify the state correctly", t, func() { - proto := NewProtocol() - So(proto.state, ShouldEqual, INVALID) - reply := proto.Start() - So(proto.state, ShouldEqual, ESTABLISH) - So(reply, ShouldNotBeNil) - So(reply, ShouldHaveSameTypeAs, &Reply{}) - So(reply.Status, ShouldEqual, 220) - So(reply.Lines(), ShouldResemble, []string{"220 mailhog.example ESMTP Go-MailHog\n"}) - }) - - Convey("Modifying the hostname should modify the ident reply", t, func() { - proto := NewProtocol() - proto.Ident = "OinkSMTP Go-MailHog" - reply := proto.Start() - So(reply, ShouldNotBeNil) - So(reply, ShouldHaveSameTypeAs, &Reply{}) - So(reply.Status, ShouldEqual, 220) - So(reply.Lines(), ShouldResemble, []string{"220 mailhog.example OinkSMTP Go-MailHog\n"}) - }) - - Convey("Modifying the ident should modify the ident reply", t, func() { - proto := NewProtocol() - proto.Hostname = "oink.oink" - reply := proto.Start() - So(reply, ShouldNotBeNil) - So(reply, ShouldHaveSameTypeAs, &Reply{}) - So(reply.Status, ShouldEqual, 220) - So(reply.Lines(), ShouldResemble, []string{"220 oink.oink ESMTP Go-MailHog\n"}) - }) -} - -func TestProcessCommand(t *testing.T) { - Convey("ProcessCommand should attempt to process anything", t, func() { - proto := NewProtocol() - - reply := proto.ProcessCommand("OINK mailhog.example") - So(proto.state, ShouldEqual, INVALID) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 500) - So(reply.Lines(), ShouldResemble, []string{"500 Unrecognised command\n"}) - - proto.Start() - So(proto.state, ShouldEqual, ESTABLISH) - - reply = proto.ProcessCommand("HELO localhost") - So(proto.state, ShouldEqual, MAIL) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 250) - So(reply.Lines(), ShouldResemble, []string{"250 Hello localhost\n"}) - - reply = proto.ProcessCommand("OINK mailhog.example") - So(proto.state, ShouldEqual, MAIL) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 500) - So(reply.Lines(), ShouldResemble, []string{"500 Unrecognised command\n"}) - }) -} - -func TestParse(t *testing.T) { - Convey("Parse can parse partial and multiple commands", t, func() { - proto := NewProtocol() - proto.Start() - So(proto.state, ShouldEqual, ESTABLISH) - - line, reply := proto.Parse("HELO localhost") - So(proto.state, ShouldEqual, ESTABLISH) - So(reply, ShouldBeNil) - So(line, ShouldEqual, "HELO localhost") - - line, reply = proto.Parse("HELO localhost\nMAIL Fro") - So(proto.state, ShouldEqual, MAIL) - So(reply, ShouldNotBeNil) - So(line, ShouldEqual, "MAIL Fro") - - line, reply = proto.Parse("MAIL From:\n") - So(proto.state, ShouldEqual, RCPT) - So(reply, ShouldNotBeNil) - So(line, ShouldEqual, "") - }) - Convey("Parse can call ProcessData", t, func() { - proto := NewProtocol() - proto.Start() - proto.Command(ParseCommand("EHLO localhost")) - proto.Command(ParseCommand("MAIL From:")) - proto.Command(ParseCommand("RCPT To:")) - proto.Command(ParseCommand("DATA")) - So(proto.state, ShouldEqual, DATA) - - line, reply := proto.Parse("Hi\n") - So(proto.state, ShouldEqual, DATA) - So(line, ShouldEqual, "") - So(proto.message.Data, ShouldEqual, "Hi\n") - So(reply, ShouldBeNil) - - line, reply = proto.Parse("\r\n") - So(proto.state, ShouldEqual, DATA) - So(line, ShouldEqual, "") - So(proto.message.Data, ShouldEqual, "Hi\n\r\n") - So(reply, ShouldBeNil) - - line, reply = proto.Parse(".\r\n") - So(proto.state, ShouldEqual, MAIL) - So(line, ShouldEqual, "") - So(reply, ShouldNotBeNil) - So(proto.message.Data, ShouldEqual, "Hi\n") - }) -} - -func TestUnknownCommands(t *testing.T) { - Convey("Unknown command in INVALID state", t, func() { - proto := NewProtocol() - So(proto.state, ShouldEqual, INVALID) - reply := proto.Command(ParseCommand("OINK")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 500) - So(reply.Lines(), ShouldResemble, []string{"500 Unrecognised command\n"}) - }) - Convey("Unknown command in ESTABLISH state", t, func() { - proto := NewProtocol() - proto.Start() - So(proto.state, ShouldEqual, ESTABLISH) - reply := proto.Command(ParseCommand("OINK")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 500) - So(reply.Lines(), ShouldResemble, []string{"500 Unrecognised command\n"}) - }) - Convey("Unknown command in MAIL state", t, func() { - proto := NewProtocol() - proto.Start() - proto.Command(ParseCommand("EHLO localhost")) - So(proto.state, ShouldEqual, MAIL) - reply := proto.Command(ParseCommand("OINK")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 500) - So(reply.Lines(), ShouldResemble, []string{"500 Unrecognised command\n"}) - }) - Convey("Unknown command in RCPT state", t, func() { - proto := NewProtocol() - proto.Start() - proto.Command(ParseCommand("EHLO localhost")) - proto.Command(ParseCommand("MAIL FROM:")) - So(proto.state, ShouldEqual, RCPT) - reply := proto.Command(ParseCommand("OINK")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 500) - So(reply.Lines(), ShouldResemble, []string{"500 Unrecognised command\n"}) - }) -} - -func TestESTABLISHCommands(t *testing.T) { - Convey("EHLO should work in ESTABLISH state", t, func() { - proto := NewProtocol() - proto.Start() - So(proto.state, ShouldEqual, ESTABLISH) - reply := proto.Command(ParseCommand("EHLO localhost")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 250) - }) - Convey("HELO should work in ESTABLISH state", t, func() { - proto := NewProtocol() - proto.Start() - So(proto.state, ShouldEqual, ESTABLISH) - reply := proto.Command(ParseCommand("HELO localhost")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 250) - }) - Convey("RSET should work in ESTABLISH state", t, func() { - proto := NewProtocol() - proto.Start() - So(proto.state, ShouldEqual, ESTABLISH) - reply := proto.Command(ParseCommand("RSET")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 250) - }) - Convey("NOOP should work in ESTABLISH state", t, func() { - proto := NewProtocol() - proto.Start() - So(proto.state, ShouldEqual, ESTABLISH) - reply := proto.Command(ParseCommand("NOOP")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 250) - }) - Convey("QUIT should work in ESTABLISH state", t, func() { - proto := NewProtocol() - proto.Start() - So(proto.state, ShouldEqual, ESTABLISH) - reply := proto.Command(ParseCommand("QUIT")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 221) - }) - Convey("MAIL shouldn't work in ESTABLISH state", t, func() { - proto := NewProtocol() - proto.Start() - So(proto.state, ShouldEqual, ESTABLISH) - reply := proto.Command(ParseCommand("MAIL")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 500) - So(reply.Lines(), ShouldResemble, []string{"500 Unrecognised command\n"}) - }) - Convey("RCPT shouldn't work in ESTABLISH state", t, func() { - proto := NewProtocol() - proto.Start() - So(proto.state, ShouldEqual, ESTABLISH) - reply := proto.Command(ParseCommand("RCPT")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 500) - So(reply.Lines(), ShouldResemble, []string{"500 Unrecognised command\n"}) - }) - Convey("DATA shouldn't work in ESTABLISH state", t, func() { - proto := NewProtocol() - proto.Start() - So(proto.state, ShouldEqual, ESTABLISH) - reply := proto.Command(ParseCommand("DATA")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 500) - So(reply.Lines(), ShouldResemble, []string{"500 Unrecognised command\n"}) - }) -} - -func TestEHLO(t *testing.T) { - Convey("EHLO should modify the state correctly", t, func() { - proto := NewProtocol() - proto.Start() - So(proto.state, ShouldEqual, ESTABLISH) - So(proto.message.Helo, ShouldEqual, "") - reply := proto.EHLO("localhost") - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 250) - So(reply.Lines(), ShouldResemble, []string{"250-Hello localhost\n", "250-PIPELINING\n", "250 AUTH EXTERNAL CRAM-MD5 LOGIN PLAIN\n"}) - So(proto.state, ShouldEqual, MAIL) - So(proto.message.Helo, ShouldEqual, "localhost") - }) - Convey("EHLO should work using Command", t, func() { - proto := NewProtocol() - proto.Start() - So(proto.state, ShouldEqual, ESTABLISH) - So(proto.message.Helo, ShouldEqual, "") - reply := proto.Command(ParseCommand("EHLO localhost")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 250) - So(reply.Lines(), ShouldResemble, []string{"250-Hello localhost\n", "250-PIPELINING\n", "250 AUTH EXTERNAL CRAM-MD5 LOGIN PLAIN\n"}) - So(proto.state, ShouldEqual, MAIL) - So(proto.message.Helo, ShouldEqual, "localhost") - }) - Convey("HELO should work in MAIL state", t, func() { - proto := NewProtocol() - proto.Start() - proto.Command(ParseCommand("HELO localhost")) - So(proto.state, ShouldEqual, MAIL) - So(proto.message.Helo, ShouldEqual, "localhost") - reply := proto.Command(ParseCommand("EHLO localhost")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 250) - So(reply.Lines(), ShouldResemble, []string{"250-Hello localhost\n", "250-PIPELINING\n", "250 AUTH EXTERNAL CRAM-MD5 LOGIN PLAIN\n"}) - So(proto.state, ShouldEqual, MAIL) - So(proto.message.Helo, ShouldEqual, "localhost") - }) - Convey("HELO should work in RCPT state", t, func() { - proto := NewProtocol() - proto.Start() - proto.Command(ParseCommand("HELO localhost")) - proto.Command(ParseCommand("MAIL From:")) - So(proto.state, ShouldEqual, RCPT) - So(proto.message.Helo, ShouldEqual, "localhost") - reply := proto.Command(ParseCommand("EHLO localhost")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 250) - So(reply.Lines(), ShouldResemble, []string{"250-Hello localhost\n", "250-PIPELINING\n", "250 AUTH EXTERNAL CRAM-MD5 LOGIN PLAIN\n"}) - So(proto.state, ShouldEqual, MAIL) - So(proto.message.Helo, ShouldEqual, "localhost") - }) -} - -func TestHELO(t *testing.T) { - Convey("HELO should modify the state correctly", t, func() { - proto := NewProtocol() - proto.Start() - So(proto.state, ShouldEqual, ESTABLISH) - So(proto.message.Helo, ShouldEqual, "") - reply := proto.HELO("localhost") - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 250) - So(reply.Lines(), ShouldResemble, []string{"250 Hello localhost\n"}) - So(proto.state, ShouldEqual, MAIL) - So(proto.message.Helo, ShouldEqual, "localhost") - }) - Convey("HELO should work using Command", t, func() { - proto := NewProtocol() - proto.Start() - So(proto.state, ShouldEqual, ESTABLISH) - So(proto.message.Helo, ShouldEqual, "") - reply := proto.Command(ParseCommand("HELO localhost")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 250) - So(reply.Lines(), ShouldResemble, []string{"250 Hello localhost\n"}) - So(proto.state, ShouldEqual, MAIL) - So(proto.message.Helo, ShouldEqual, "localhost") - }) - Convey("HELO should work in MAIL state", t, func() { - proto := NewProtocol() - proto.Start() - proto.Command(ParseCommand("HELO localhost")) - So(proto.state, ShouldEqual, MAIL) - So(proto.message.Helo, ShouldEqual, "localhost") - reply := proto.Command(ParseCommand("HELO localhost")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 250) - So(reply.Lines(), ShouldResemble, []string{"250 Hello localhost\n"}) - So(proto.state, ShouldEqual, MAIL) - So(proto.message.Helo, ShouldEqual, "localhost") - }) - Convey("HELO should work in RCPT state", t, func() { - proto := NewProtocol() - proto.Start() - proto.Command(ParseCommand("HELO localhost")) - proto.Command(ParseCommand("MAIL From:")) - So(proto.state, ShouldEqual, RCPT) - So(proto.message.Helo, ShouldEqual, "localhost") - reply := proto.Command(ParseCommand("HELO localhost")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 250) - So(reply.Lines(), ShouldResemble, []string{"250 Hello localhost\n"}) - So(proto.state, ShouldEqual, MAIL) - So(proto.message.Helo, ShouldEqual, "localhost") - }) -} - -func TestDATA(t *testing.T) { - Convey("DATA should accept data", t, func() { - proto := NewProtocol() - handlerCalled := false - proto.MessageReceivedHandler = func(msg *data.Message) (string, error) { - handlerCalled = true - return "abc", nil - } - proto.Start() - proto.HELO("localhost") - proto.Command(ParseCommand("MAIL FROM:")) - proto.Command(ParseCommand("RCPT TO:")) - reply := proto.Command(ParseCommand("DATA")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 354) - So(reply.Lines(), ShouldResemble, []string{"354 End data with .\n"}) - So(proto.state, ShouldEqual, DATA) - reply = proto.ProcessData("Hi") - So(reply, ShouldBeNil) - So(proto.state, ShouldEqual, DATA) - So(proto.message.Data, ShouldEqual, "Hi\n") - reply = proto.ProcessData("How are you?") - So(reply, ShouldBeNil) - So(proto.state, ShouldEqual, DATA) - So(proto.message.Data, ShouldEqual, "Hi\nHow are you?\n") - reply = proto.ProcessData("\r\n.\r") - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 250) - So(proto.state, ShouldEqual, MAIL) - So(reply.Lines(), ShouldResemble, []string{"250 Ok: queued as abc\n"}) - So(handlerCalled, ShouldBeTrue) - }) - Convey("Should return error if missing storage backend", t, func() { - proto := NewProtocol() - proto.Start() - proto.HELO("localhost") - proto.Command(ParseCommand("MAIL FROM:")) - proto.Command(ParseCommand("RCPT TO:")) - reply := proto.Command(ParseCommand("DATA")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 354) - So(reply.Lines(), ShouldResemble, []string{"354 End data with .\n"}) - So(proto.state, ShouldEqual, DATA) - reply = proto.ProcessData("Hi") - So(reply, ShouldBeNil) - So(proto.state, ShouldEqual, DATA) - So(proto.message.Data, ShouldEqual, "Hi\n") - reply = proto.ProcessData("How are you?") - So(reply, ShouldBeNil) - So(proto.state, ShouldEqual, DATA) - So(proto.message.Data, ShouldEqual, "Hi\nHow are you?\n") - reply = proto.ProcessData("\r\n.\r") - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 452) - So(proto.state, ShouldEqual, MAIL) - So(reply.Lines(), ShouldResemble, []string{"452 No storage backend\n"}) - }) - Convey("Should return error if storage backend fails", t, func() { - proto := NewProtocol() - handlerCalled := false - proto.MessageReceivedHandler = func(msg *data.Message) (string, error) { - handlerCalled = true - return "", errors.New("abc") - } - proto.Start() - proto.HELO("localhost") - proto.Command(ParseCommand("MAIL FROM:")) - proto.Command(ParseCommand("RCPT TO:")) - reply := proto.Command(ParseCommand("DATA")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 354) - So(reply.Lines(), ShouldResemble, []string{"354 End data with .\n"}) - So(proto.state, ShouldEqual, DATA) - reply = proto.ProcessData("Hi") - So(reply, ShouldBeNil) - So(proto.state, ShouldEqual, DATA) - So(proto.message.Data, ShouldEqual, "Hi\n") - reply = proto.ProcessData("How are you?") - So(reply, ShouldBeNil) - So(proto.state, ShouldEqual, DATA) - So(proto.message.Data, ShouldEqual, "Hi\nHow are you?\n") - reply = proto.ProcessData("\r\n.\r") - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 452) - So(proto.state, ShouldEqual, MAIL) - So(reply.Lines(), ShouldResemble, []string{"452 Unable to store message\n"}) - So(handlerCalled, ShouldBeTrue) - }) -} - -func TestRSET(t *testing.T) { - Convey("RSET should reset the state correctly", t, func() { - proto := NewProtocol() - proto.Start() - proto.HELO("localhost") - proto.Command(ParseCommand("MAIL FROM:")) - proto.Command(ParseCommand("RCPT TO:")) - So(proto.state, ShouldEqual, RCPT) - So(proto.message.From, ShouldEqual, "test") - So(len(proto.message.To), ShouldEqual, 1) - So(proto.message.To[0], ShouldEqual, "test") - reply := proto.Command(ParseCommand("RSET")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 250) - So(reply.Lines(), ShouldResemble, []string{"250 Ok\n"}) - So(proto.state, ShouldEqual, MAIL) - So(proto.message.From, ShouldEqual, "") - So(len(proto.message.To), ShouldEqual, 0) - }) -} - -func TestNOOP(t *testing.T) { - Convey("NOOP shouldn't modify the state", t, func() { - proto := NewProtocol() - proto.Start() - proto.HELO("localhost") - proto.Command(ParseCommand("MAIL FROM:")) - proto.Command(ParseCommand("RCPT TO:")) - So(proto.state, ShouldEqual, RCPT) - So(proto.message.From, ShouldEqual, "test") - So(len(proto.message.To), ShouldEqual, 1) - So(proto.message.To[0], ShouldEqual, "test") - reply := proto.Command(ParseCommand("NOOP")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 250) - So(reply.Lines(), ShouldResemble, []string{"250 Ok\n"}) - So(proto.state, ShouldEqual, RCPT) - So(proto.message.From, ShouldEqual, "test") - So(len(proto.message.To), ShouldEqual, 1) - So(proto.message.To[0], ShouldEqual, "test") - }) -} - -func TestQUIT(t *testing.T) { - Convey("QUIT should modify the state correctly", t, func() { - proto := NewProtocol() - proto.Start() - reply := proto.Command(ParseCommand("QUIT")) - So(proto.state, ShouldEqual, DONE) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 221) - So(reply.Lines(), ShouldResemble, []string{"221 Bye\n"}) - }) -} - -func TestParseMAIL(t *testing.T) { - proto := NewProtocol() - Convey("ParseMAIL should parse MAIL command arguments", t, func() { - m, err := proto.ParseMAIL("FROM:") - So(err, ShouldBeNil) - So(m, ShouldEqual, "oink@mailhog.example") - m, err = proto.ParseMAIL("FROM:") - So(err, ShouldBeNil) - So(m, ShouldEqual, "oink") - }) - Convey("ParseMAIL should return an error for invalid syntax", t, func() { - m, err := proto.ParseMAIL("FROM:oink") - So(err, ShouldNotBeNil) - So(err.Error(), ShouldEqual, "Invalid syntax in MAIL command") - So(m, ShouldEqual, "") - }) - Convey("ParseMAIL should be case-insensitive", t, func() { - m, err := proto.ParseMAIL("FROM:") - So(err, ShouldBeNil) - So(m, ShouldEqual, "oink") - m, err = proto.ParseMAIL("from:") - So(err, ShouldBeNil) - So(m, ShouldEqual, "oink@mailhog.example") - m, err = proto.ParseMAIL("FrOm:") - So(err, ShouldBeNil) - So(m, ShouldEqual, "oink@oink.mailhog.example") - }) - Convey("ParseMAIL should support broken sender syntax", t, func() { - m, err := proto.ParseMAIL("FROM: ") - So(err, ShouldBeNil) - So(m, ShouldEqual, "oink") - m, err = proto.ParseMAIL("from: ") - So(err, ShouldBeNil) - So(m, ShouldEqual, "oink@mailhog.example") - m, err = proto.ParseMAIL("FrOm: ") - So(err, ShouldBeNil) - So(m, ShouldEqual, "oink@oink.mailhog.example") - }) - Convey("Error should be returned via Command", t, func() { - proto := NewProtocol() - proto.Start() - proto.Command(ParseCommand("HELO localhost")) - So(proto.state, ShouldEqual, MAIL) - reply := proto.Command(ParseCommand("MAIL oink")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 550) - So(reply.Lines(), ShouldResemble, []string{"550 Invalid syntax in MAIL command\n"}) - So(proto.state, ShouldEqual, MAIL) - }) - Convey("ValidateSenderHandler should be called", t, func() { - proto := NewProtocol() - handlerCalled := false - proto.ValidateSenderHandler = func(sender string) bool { - handlerCalled = true - So(sender, ShouldEqual, "oink@mailhog.example") - return true - } - proto.Start() - proto.Command(ParseCommand("HELO localhost")) - So(proto.state, ShouldEqual, MAIL) - reply := proto.Command(ParseCommand("MAIL From:")) - So(handlerCalled, ShouldBeTrue) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 250) - So(reply.Lines(), ShouldResemble, []string{"250 Sender oink@mailhog.example ok\n"}) - So(proto.state, ShouldEqual, RCPT) - }) - Convey("ValidateSenderHandler errors should be returned", t, func() { - proto := NewProtocol() - handlerCalled := false - proto.ValidateSenderHandler = func(sender string) bool { - handlerCalled = true - So(sender, ShouldEqual, "oink@mailhog.example") - return false - } - proto.Start() - proto.Command(ParseCommand("HELO localhost")) - So(proto.state, ShouldEqual, MAIL) - reply := proto.Command(ParseCommand("MAIL From:")) - So(handlerCalled, ShouldBeTrue) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 550) - So(reply.Lines(), ShouldResemble, []string{"550 Invalid sender oink@mailhog.example\n"}) - So(proto.state, ShouldEqual, MAIL) - }) -} - -func TestParseRCPT(t *testing.T) { - proto := NewProtocol() - Convey("ParseRCPT should parse RCPT command arguments", t, func() { - m, err := proto.ParseRCPT("TO:") - So(err, ShouldBeNil) - So(m, ShouldEqual, "oink@mailhog.example") - m, err = proto.ParseRCPT("TO:") - So(err, ShouldBeNil) - So(m, ShouldEqual, "oink") - }) - Convey("ParseRCPT should return an error for invalid syntax", t, func() { - m, err := proto.ParseRCPT("TO:oink") - So(err, ShouldNotBeNil) - So(err.Error(), ShouldEqual, "Invalid syntax in RCPT command") - So(m, ShouldEqual, "") - }) - Convey("ParseRCPT should be case-insensitive", t, func() { - m, err := proto.ParseRCPT("TO:") - So(err, ShouldBeNil) - So(m, ShouldEqual, "oink") - m, err = proto.ParseRCPT("to:") - So(err, ShouldBeNil) - So(m, ShouldEqual, "oink@mailhog.example") - m, err = proto.ParseRCPT("To:") - So(err, ShouldBeNil) - So(m, ShouldEqual, "oink@oink.mailhog.example") - }) - Convey("ParseRCPT should support broken recipient syntax", t, func() { - m, err := proto.ParseRCPT("TO: ") - So(err, ShouldBeNil) - So(m, ShouldEqual, "oink") - m, err = proto.ParseRCPT("to: ") - So(err, ShouldBeNil) - So(m, ShouldEqual, "oink@mailhog.example") - m, err = proto.ParseRCPT("To: ") - So(err, ShouldBeNil) - So(m, ShouldEqual, "oink@oink.mailhog.example") - }) - Convey("Error should be returned via Command", t, func() { - proto := NewProtocol() - proto.Start() - proto.Command(ParseCommand("HELO localhost")) - proto.Command(ParseCommand("MAIL FROM:")) - So(proto.state, ShouldEqual, RCPT) - reply := proto.Command(ParseCommand("RCPT oink")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 550) - So(reply.Lines(), ShouldResemble, []string{"550 Invalid syntax in RCPT command\n"}) - So(proto.state, ShouldEqual, RCPT) - }) - Convey("ValidateRecipientHandler should be called", t, func() { - proto := NewProtocol() - handlerCalled := false - proto.ValidateRecipientHandler = func(recipient string) bool { - handlerCalled = true - So(recipient, ShouldEqual, "oink@mailhog.example") - return true - } - proto.Start() - proto.Command(ParseCommand("HELO localhost")) - proto.Command(ParseCommand("MAIL FROM:")) - So(proto.state, ShouldEqual, RCPT) - reply := proto.Command(ParseCommand("RCPT To:")) - So(handlerCalled, ShouldBeTrue) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 250) - So(reply.Lines(), ShouldResemble, []string{"250 Recipient oink@mailhog.example ok\n"}) - So(proto.state, ShouldEqual, RCPT) - }) - Convey("ValidateRecipientHandler errors should be returned", t, func() { - proto := NewProtocol() - handlerCalled := false - proto.ValidateRecipientHandler = func(recipient string) bool { - handlerCalled = true - So(recipient, ShouldEqual, "oink@mailhog.example") - return false - } - proto.Start() - proto.Command(ParseCommand("HELO localhost")) - proto.Command(ParseCommand("MAIL FROM:")) - So(proto.state, ShouldEqual, RCPT) - reply := proto.Command(ParseCommand("RCPT To:")) - So(handlerCalled, ShouldBeTrue) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 550) - So(reply.Lines(), ShouldResemble, []string{"550 Invalid recipient oink@mailhog.example\n"}) - So(proto.state, ShouldEqual, RCPT) - }) -} - -func TestAuth(t *testing.T) { - Convey("AUTH should be listed in EHLO response", t, func() { - proto := NewProtocol() - proto.Start() - reply := proto.Command(ParseCommand("EHLO localhost")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 250) - So(reply.Lines(), ShouldResemble, []string{"250-Hello localhost\n", "250-PIPELINING\n", "250 AUTH EXTERNAL CRAM-MD5 LOGIN PLAIN\n"}) - }) - - Convey("Invalid mechanism should be rejected", t, func() { - proto := NewProtocol() - proto.Start() - proto.Command(ParseCommand("EHLO localhost")) - reply := proto.Command(ParseCommand("AUTH OINK")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 504) - So(reply.Lines(), ShouldResemble, []string{"504 Unsupported authentication mechanism\n"}) - }) -} - -func TestAuthExternal(t *testing.T) { - Convey("AUTH EXTERNAL should call ValidateAuthenticationHandler", t, func() { - proto := NewProtocol() - handlerCalled := false - proto.ValidateAuthenticationHandler = func(mechanism string, args ...string) (*Reply, bool) { - handlerCalled = true - So(mechanism, ShouldEqual, "EXTERNAL") - So(len(args), ShouldEqual, 1) - So(args[0], ShouldEqual, "oink!") - return nil, true - } - proto.Start() - proto.Command(ParseCommand("EHLO localhost")) - reply := proto.Command(ParseCommand("AUTH EXTERNAL oink!")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 235) - So(reply.Lines(), ShouldResemble, []string{"235 Authentication successful\n"}) - So(handlerCalled, ShouldBeTrue) - }) - - Convey("AUTH EXTERNAL ValidateAuthenticationHandler errors should be returned", t, func() { - proto := NewProtocol() - handlerCalled := false - proto.ValidateAuthenticationHandler = func(mechanism string, args ...string) (*Reply, bool) { - handlerCalled = true - return ReplyError(errors.New("OINK :(")), false - } - proto.Start() - proto.Command(ParseCommand("EHLO localhost")) - reply := proto.Command(ParseCommand("AUTH EXTERNAL oink!")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 550) - So(reply.Lines(), ShouldResemble, []string{"550 OINK :(\n"}) - So(handlerCalled, ShouldBeTrue) - }) -} - -func TestAuthPlain(t *testing.T) { - Convey("Inline AUTH PLAIN should call ValidateAuthenticationHandler", t, func() { - proto := NewProtocol() - handlerCalled := false - proto.ValidateAuthenticationHandler = func(mechanism string, args ...string) (*Reply, bool) { - handlerCalled = true - So(mechanism, ShouldEqual, "PLAIN") - So(len(args), ShouldEqual, 1) - So(args[0], ShouldEqual, "oink!") - return nil, true - } - proto.Start() - proto.Command(ParseCommand("EHLO localhost")) - reply := proto.Command(ParseCommand("AUTH PLAIN oink!")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 235) - So(reply.Lines(), ShouldResemble, []string{"235 Authentication successful\n"}) - So(handlerCalled, ShouldBeTrue) - }) - - Convey("Inline AUTH PLAIN ValidateAuthenticationHandler errors should be returned", t, func() { - proto := NewProtocol() - handlerCalled := false - proto.ValidateAuthenticationHandler = func(mechanism string, args ...string) (*Reply, bool) { - handlerCalled = true - return ReplyError(errors.New("OINK :(")), false - } - proto.Start() - proto.Command(ParseCommand("EHLO localhost")) - reply := proto.Command(ParseCommand("AUTH PLAIN oink!")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 550) - So(reply.Lines(), ShouldResemble, []string{"550 OINK :(\n"}) - So(handlerCalled, ShouldBeTrue) - }) - - Convey("Two part AUTH PLAIN should call ValidateAuthenticationHandler", t, func() { - proto := NewProtocol() - handlerCalled := false - proto.ValidateAuthenticationHandler = func(mechanism string, args ...string) (*Reply, bool) { - handlerCalled = true - So(mechanism, ShouldEqual, "PLAIN") - So(len(args), ShouldEqual, 1) - So(args[0], ShouldEqual, "oink!") - return nil, true - } - proto.Start() - proto.Command(ParseCommand("EHLO localhost")) - reply := proto.Command(ParseCommand("AUTH PLAIN")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 334) - So(reply.Lines(), ShouldResemble, []string{"334 \n"}) - - _, reply = proto.Parse("oink!\n") - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 235) - So(reply.Lines(), ShouldResemble, []string{"235 Authentication successful\n"}) - So(handlerCalled, ShouldBeTrue) - }) - - Convey("Two part AUTH PLAIN ValidateAuthenticationHandler errors should be returned", t, func() { - proto := NewProtocol() - handlerCalled := false - proto.ValidateAuthenticationHandler = func(mechanism string, args ...string) (*Reply, bool) { - handlerCalled = true - return ReplyError(errors.New("OINK :(")), false - } - proto.Start() - proto.Command(ParseCommand("EHLO localhost")) - reply := proto.Command(ParseCommand("AUTH PLAIN")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 334) - So(reply.Lines(), ShouldResemble, []string{"334 \n"}) - - _, reply = proto.Parse("oink!\n") - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 550) - So(reply.Lines(), ShouldResemble, []string{"550 OINK :(\n"}) - So(handlerCalled, ShouldBeTrue) - }) -} - -func TestAuthCramMD5(t *testing.T) { - Convey("Two part AUTH CRAM-MD5 should call ValidateAuthenticationHandler", t, func() { - proto := NewProtocol() - handlerCalled := false - proto.ValidateAuthenticationHandler = func(mechanism string, args ...string) (*Reply, bool) { - handlerCalled = true - So(mechanism, ShouldEqual, "CRAM-MD5") - So(len(args), ShouldEqual, 1) - So(args[0], ShouldEqual, "oink!") - return nil, true - } - proto.Start() - proto.Command(ParseCommand("EHLO localhost")) - reply := proto.Command(ParseCommand("AUTH CRAM-MD5")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 334) - So(reply.Lines(), ShouldResemble, []string{"334 PDQxOTI5NDIzNDEuMTI4Mjg0NzJAc291cmNlZm91ci5hbmRyZXcuY211LmVkdT4=\n"}) - - _, reply = proto.Parse("oink!\n") - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 235) - So(reply.Lines(), ShouldResemble, []string{"235 Authentication successful\n"}) - So(handlerCalled, ShouldBeTrue) - }) - - Convey("Two part AUTH CRAM-MD5 ValidateAuthenticationHandler errors should be returned", t, func() { - proto := NewProtocol() - handlerCalled := false - proto.ValidateAuthenticationHandler = func(mechanism string, args ...string) (*Reply, bool) { - handlerCalled = true - return ReplyError(errors.New("OINK :(")), false - } - proto.Start() - proto.Command(ParseCommand("EHLO localhost")) - reply := proto.Command(ParseCommand("AUTH CRAM-MD5")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 334) - So(reply.Lines(), ShouldResemble, []string{"334 PDQxOTI5NDIzNDEuMTI4Mjg0NzJAc291cmNlZm91ci5hbmRyZXcuY211LmVkdT4=\n"}) - - _, reply = proto.Parse("oink!\n") - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 550) - So(reply.Lines(), ShouldResemble, []string{"550 OINK :(\n"}) - So(handlerCalled, ShouldBeTrue) - }) -} - -func TestAuthLogin(t *testing.T) { - Convey("AUTH LOGIN should call ValidateAuthenticationHandler", t, func() { - proto := NewProtocol() - handlerCalled := false - proto.ValidateAuthenticationHandler = func(mechanism string, args ...string) (*Reply, bool) { - handlerCalled = true - So(mechanism, ShouldEqual, "LOGIN") - So(len(args), ShouldEqual, 2) - So(args[0], ShouldEqual, "username!") - So(args[1], ShouldEqual, "password!") - return nil, true - } - proto.Start() - proto.Command(ParseCommand("EHLO localhost")) - reply := proto.Command(ParseCommand("AUTH LOGIN")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 334) - So(reply.Lines(), ShouldResemble, []string{"334 VXNlcm5hbWU6\n"}) - - _, reply = proto.Parse("username!\n") - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 334) - So(reply.Lines(), ShouldResemble, []string{"334 UGFzc3dvcmQ6\n"}) - - _, reply = proto.Parse("password!\n") - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 235) - So(reply.Lines(), ShouldResemble, []string{"235 Authentication successful\n"}) - So(handlerCalled, ShouldBeTrue) - }) - - Convey("AUTH LOGIN ValidateAuthenticationHandler errors should be returned", t, func() { - proto := NewProtocol() - handlerCalled := false - proto.ValidateAuthenticationHandler = func(mechanism string, args ...string) (*Reply, bool) { - handlerCalled = true - return ReplyError(errors.New("OINK :(")), false - } - proto.Start() - proto.Command(ParseCommand("EHLO localhost")) - reply := proto.Command(ParseCommand("AUTH LOGIN")) - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 334) - So(reply.Lines(), ShouldResemble, []string{"334 VXNlcm5hbWU6\n"}) - - _, reply = proto.Parse("username!\n") - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 334) - So(reply.Lines(), ShouldResemble, []string{"334 UGFzc3dvcmQ6\n"}) - - _, reply = proto.Parse("password!\n") - So(reply, ShouldNotBeNil) - So(reply.Status, ShouldEqual, 550) - So(reply.Lines(), ShouldResemble, []string{"550 OINK :(\n"}) - So(handlerCalled, ShouldBeTrue) - }) -} diff --git a/smtp/protocol/reply.go b/smtp/protocol/reply.go deleted file mode 100644 index 858325e..0000000 --- a/smtp/protocol/reply.go +++ /dev/null @@ -1,79 +0,0 @@ -package protocol - -import "strconv" - -// http://www.rfc-editor.org/rfc/rfc5321.txt - -// Reply is a struct representing an SMTP reply (status code + lines) -type Reply struct { - Status int - lines []string -} - -// Lines returns the formatted SMTP reply -func (r Reply) Lines() []string { - var lines []string - - if len(r.lines) == 0 { - l := strconv.Itoa(r.Status) - lines = append(lines, l+"\n") - return lines - } - - for i, line := range r.lines { - l := "" - if i == len(r.lines)-1 { - l = strconv.Itoa(r.Status) + " " + line + "\n" - } else { - l = strconv.Itoa(r.Status) + "-" + line + "\n" - } - lines = append(lines, l) - } - - return lines -} - -// ReplyIdent creates a 220 welcome reply -func ReplyIdent(ident string) *Reply { return &Reply{220, []string{ident}} } - -// ReplyBye creates a 221 Bye reply -func ReplyBye() *Reply { return &Reply{221, []string{"Bye"}} } - -// ReplyAuthOk creates a 235 authentication successful reply -func ReplyAuthOk() *Reply { return &Reply{235, []string{"Authentication successful"}} } - -// ReplyOk creates a 250 Ok reply -func ReplyOk(message ...string) *Reply { - if len(message) == 0 { - message = []string{"Ok"} - } - return &Reply{250, message} -} - -// 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"}} -} - -// ReplyAuthResponse creates a 334 authentication reply -func ReplyAuthResponse(response string) *Reply { return &Reply{334, []string{response}} } - -// ReplyDataResponse creates a 354 data reply -func ReplyDataResponse() *Reply { return &Reply{354, []string{"End data with ."}} } - -// ReplyStorageFailed creates a 452 error reply -func ReplyStorageFailed(reason string) *Reply { return &Reply{452, []string{reason}} } - -// ReplyUnrecognisedCommand creates a 500 Unrecognised command reply -func ReplyUnrecognisedCommand() *Reply { return &Reply{500, []string{"Unrecognised command"}} } - -// ReplyUnsupportedAuth creates a 504 unsupported authentication reply -func ReplyUnsupportedAuth() *Reply { - return &Reply{504, []string{"Unsupported authentication mechanism"}} -} - -// ReplyError creates a 500 error reply -func ReplyError(err error) *Reply { return &Reply{550, []string{err.Error()}} } diff --git a/smtp/protocol/reply_test.go b/smtp/protocol/reply_test.go deleted file mode 100644 index 1c18fed..0000000 --- a/smtp/protocol/reply_test.go +++ /dev/null @@ -1,122 +0,0 @@ -package protocol - -// http://www.rfc-editor.org/rfc/rfc5321.txt - -import ( - "testing" - - . "github.com/smartystreets/goconvey/convey" -) - -func TestReply(t *testing.T) { - Convey("Reply creates properly formatted responses", t, func() { - r := &Reply{200, []string{}} - l := r.Lines() - So(l[0], ShouldEqual, "200\n") - - r = &Reply{200, []string{"Ok"}} - l = r.Lines() - So(l[0], ShouldEqual, "200 Ok\n") - - r = &Reply{200, []string{"Ok", "Still ok!"}} - l = r.Lines() - So(l[0], ShouldEqual, "200-Ok\n") - So(l[1], ShouldEqual, "200 Still ok!\n") - - r = &Reply{200, []string{"Ok", "Still ok!", "OINK!"}} - l = r.Lines() - So(l[0], ShouldEqual, "200-Ok\n") - So(l[1], ShouldEqual, "200-Still ok!\n") - So(l[2], ShouldEqual, "200 OINK!\n") - }) -} - -func TestBuiltInReplies(t *testing.T) { - Convey("ReplyIdent is correct", t, func() { - r := ReplyIdent("oink") - So(r.Status, ShouldEqual, 220) - So(len(r.lines), ShouldEqual, 1) - So(r.lines[0], ShouldEqual, "oink") - }) - - Convey("ReplyBye is correct", t, func() { - r := ReplyBye() - So(r.Status, ShouldEqual, 221) - So(len(r.lines), ShouldEqual, 1) - So(r.lines[0], ShouldEqual, "Bye") - }) - - Convey("ReplyAuthOk is correct", t, func() { - r := ReplyAuthOk() - So(r.Status, ShouldEqual, 235) - So(len(r.lines), ShouldEqual, 1) - So(r.lines[0], ShouldEqual, "Authentication successful") - }) - - Convey("ReplyOk is correct", t, func() { - r := ReplyOk() - So(r.Status, ShouldEqual, 250) - So(len(r.lines), ShouldEqual, 1) - So(r.lines[0], ShouldEqual, "Ok") - - r = ReplyOk("oink") - So(r.Status, ShouldEqual, 250) - So(len(r.lines), ShouldEqual, 1) - So(r.lines[0], ShouldEqual, "oink") - - r = ReplyOk("mailhog", "OINK!") - So(r.Status, ShouldEqual, 250) - So(len(r.lines), ShouldEqual, 2) - So(r.lines[0], ShouldEqual, "mailhog") - So(r.lines[1], ShouldEqual, "OINK!") - }) - - Convey("ReplySenderOk is correct", t, func() { - r := ReplySenderOk("test") - So(r.Status, ShouldEqual, 250) - So(len(r.lines), ShouldEqual, 1) - So(r.lines[0], ShouldEqual, "Sender test ok") - }) - - Convey("ReplyRecipientOk is correct", t, func() { - r := ReplyRecipientOk("test") - So(r.Status, ShouldEqual, 250) - So(len(r.lines), ShouldEqual, 1) - So(r.lines[0], ShouldEqual, "Recipient test ok") - }) - - Convey("ReplyAuthResponse is correct", t, func() { - r := ReplyAuthResponse("test") - So(r.Status, ShouldEqual, 334) - So(len(r.lines), ShouldEqual, 1) - So(r.lines[0], ShouldEqual, "test") - }) - - Convey("ReplyDataResponse is correct", t, func() { - r := ReplyDataResponse() - So(r.Status, ShouldEqual, 354) - So(len(r.lines), ShouldEqual, 1) - So(r.lines[0], ShouldEqual, "End data with .") - }) - - Convey("ReplyStorageFailed is correct", t, func() { - r := ReplyStorageFailed("test") - So(r.Status, ShouldEqual, 452) - So(len(r.lines), ShouldEqual, 1) - So(r.lines[0], ShouldEqual, "test") - }) - - Convey("ReplyUnrecognisedCommand is correct", t, func() { - r := ReplyUnrecognisedCommand() - So(r.Status, ShouldEqual, 500) - So(len(r.lines), ShouldEqual, 1) - So(r.lines[0], ShouldEqual, "Unrecognised command") - }) - - Convey("ReplyUnsupportedAuth is correct", t, func() { - r := ReplyUnsupportedAuth() - So(r.Status, ShouldEqual, 504) - So(len(r.lines), ShouldEqual, 1) - So(r.lines[0], ShouldEqual, "Unsupported authentication mechanism") - }) -} diff --git a/smtp/protocol/state.go b/smtp/protocol/state.go deleted file mode 100644 index 91bdb77..0000000 --- a/smtp/protocol/state.go +++ /dev/null @@ -1,32 +0,0 @@ -package protocol - -// State represents the state of an SMTP conversation -type State int - -// SMTP message conversation states -const ( - INVALID = State(-1) - ESTABLISH = State(iota) - AUTHPLAIN - AUTHLOGIN - AUTHLOGIN2 - AUTHCRAMMD5 - MAIL - RCPT - DATA - DONE -) - -// StateMap provides string representations of SMTP conversation states -var StateMap = map[State]string{ - INVALID: "INVALID", - ESTABLISH: "ESTABLISH", - AUTHPLAIN: "AUTHPLAIN", - AUTHLOGIN: "AUTHLOGIN", - AUTHLOGIN2: "AUTHLOGIN2", - AUTHCRAMMD5: "AUTHCRAMMD5", - MAIL: "MAIL", - RCPT: "RCPT", - DATA: "DATA", - DONE: "DONE", -}