Switch to github.com/mailhog/smtp

This commit is contained in:
Ian Kent 2014-12-24 16:30:09 +00:00
parent 69fd88323f
commit 09c9701511
6 changed files with 6 additions and 1544 deletions

View file

@ -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)

View file

@ -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
}

View file

@ -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:<test>\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:<test>"))
proto.Command(ParseCommand("RCPT To:<test>"))
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:<test>"))
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:<test>"))
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:<test>"))
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:<test>"))
proto.Command(ParseCommand("RCPT TO:<test>"))
reply := proto.Command(ParseCommand("DATA"))
So(reply, ShouldNotBeNil)
So(reply.Status, ShouldEqual, 354)
So(reply.Lines(), ShouldResemble, []string{"354 End data with <CR><LF>.<CR><LF>\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:<test>"))
proto.Command(ParseCommand("RCPT TO:<test>"))
reply := proto.Command(ParseCommand("DATA"))
So(reply, ShouldNotBeNil)
So(reply.Status, ShouldEqual, 354)
So(reply.Lines(), ShouldResemble, []string{"354 End data with <CR><LF>.<CR><LF>\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:<test>"))
proto.Command(ParseCommand("RCPT TO:<test>"))
reply := proto.Command(ParseCommand("DATA"))
So(reply, ShouldNotBeNil)
So(reply.Status, ShouldEqual, 354)
So(reply.Lines(), ShouldResemble, []string{"354 End data with <CR><LF>.<CR><LF>\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:<test>"))
proto.Command(ParseCommand("RCPT TO:<test>"))
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:<test>"))
proto.Command(ParseCommand("RCPT TO:<test>"))
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:<oink@mailhog.example>")
So(err, ShouldBeNil)
So(m, ShouldEqual, "oink@mailhog.example")
m, err = proto.ParseMAIL("FROM:<oink>")
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:<oink>")
So(err, ShouldBeNil)
So(m, ShouldEqual, "oink")
m, err = proto.ParseMAIL("from:<oink@mailhog.example>")
So(err, ShouldBeNil)
So(m, ShouldEqual, "oink@mailhog.example")
m, err = proto.ParseMAIL("FrOm:<oink@oink.mailhog.example>")
So(err, ShouldBeNil)
So(m, ShouldEqual, "oink@oink.mailhog.example")
})
Convey("ParseMAIL should support broken sender syntax", t, func() {
m, err := proto.ParseMAIL("FROM: <oink>")
So(err, ShouldBeNil)
So(m, ShouldEqual, "oink")
m, err = proto.ParseMAIL("from: <oink@mailhog.example>")
So(err, ShouldBeNil)
So(m, ShouldEqual, "oink@mailhog.example")
m, err = proto.ParseMAIL("FrOm: <oink@oink.mailhog.example>")
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:<oink@mailhog.example>"))
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:<oink@mailhog.example>"))
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:<oink@mailhog.example>")
So(err, ShouldBeNil)
So(m, ShouldEqual, "oink@mailhog.example")
m, err = proto.ParseRCPT("TO:<oink>")
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:<oink>")
So(err, ShouldBeNil)
So(m, ShouldEqual, "oink")
m, err = proto.ParseRCPT("to:<oink@mailhog.example>")
So(err, ShouldBeNil)
So(m, ShouldEqual, "oink@mailhog.example")
m, err = proto.ParseRCPT("To:<oink@oink.mailhog.example>")
So(err, ShouldBeNil)
So(m, ShouldEqual, "oink@oink.mailhog.example")
})
Convey("ParseRCPT should support broken recipient syntax", t, func() {
m, err := proto.ParseRCPT("TO: <oink>")
So(err, ShouldBeNil)
So(m, ShouldEqual, "oink")
m, err = proto.ParseRCPT("to: <oink@mailhog.example>")
So(err, ShouldBeNil)
So(m, ShouldEqual, "oink@mailhog.example")
m, err = proto.ParseRCPT("To: <oink@oink.mailhog.example>")
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:<test>"))
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:<test>"))
So(proto.state, ShouldEqual, RCPT)
reply := proto.Command(ParseCommand("RCPT To:<oink@mailhog.example>"))
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:<test>"))
So(proto.state, ShouldEqual, RCPT)
reply := proto.Command(ParseCommand("RCPT To:<oink@mailhog.example>"))
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)
})
}

View file

@ -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 <CR><LF>.<CR><LF>"}} }
// 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()}} }

View file

@ -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 <CR><LF>.<CR><LF>")
})
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")
})
}

View file

@ -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",
}