mirror of
https://gitlab.com/ric_harvey/MailHog.git
synced 2025-02-17 08:15:55 +00:00
Switch to github.com/mailhog/smtp
This commit is contained in:
parent
69fd88323f
commit
09c9701511
6 changed files with 6 additions and 1544 deletions
|
@ -9,15 +9,15 @@ import (
|
||||||
|
|
||||||
"github.com/ian-kent/Go-MailHog/MailHog-Server/monkey"
|
"github.com/ian-kent/Go-MailHog/MailHog-Server/monkey"
|
||||||
"github.com/ian-kent/Go-MailHog/data"
|
"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/Go-MailHog/storage"
|
||||||
"github.com/ian-kent/linkio"
|
"github.com/ian-kent/linkio"
|
||||||
|
"github.com/mailhog/smtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Session represents a SMTP session using net.TCPConn
|
// Session represents a SMTP session using net.TCPConn
|
||||||
type Session struct {
|
type Session struct {
|
||||||
conn io.ReadWriteCloser
|
conn io.ReadWriteCloser
|
||||||
proto *protocol.Protocol
|
proto *smtp.Protocol
|
||||||
storage storage.Storage
|
storage storage.Storage
|
||||||
messageChan chan *data.Message
|
messageChan chan *data.Message
|
||||||
remoteAddress string
|
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) {
|
func Accept(remoteAddress string, conn io.ReadWriteCloser, storage storage.Storage, messageChan chan *data.Message, hostname string, monkey monkey.ChaosMonkey) {
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
proto := protocol.NewProtocol()
|
proto := smtp.NewProtocol()
|
||||||
proto.Hostname = hostname
|
proto.Hostname = hostname
|
||||||
var link *linkio.Link
|
var link *linkio.Link
|
||||||
reader := io.Reader(conn)
|
reader := io.Reader(conn)
|
||||||
|
@ -66,12 +66,12 @@ func Accept(remoteAddress string, conn io.ReadWriteCloser, storage storage.Stora
|
||||||
session.logf("Session ended")
|
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 {
|
if c.monkey != nil {
|
||||||
ok := c.monkey.ValidAUTH(mechanism, args...)
|
ok := c.monkey.ValidAUTH(mechanism, args...)
|
||||||
if !ok {
|
if !ok {
|
||||||
// FIXME better error?
|
// FIXME better error?
|
||||||
return protocol.ReplyUnrecognisedCommand(), false
|
return smtp.ReplyUnrecognisedCommand(), false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, true
|
return nil, true
|
||||||
|
@ -150,7 +150,7 @@ func (c *Session) Read() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write writes a reply to the underlying net.TCPConn
|
// 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()
|
lines := reply.Lines()
|
||||||
for _, l := range lines {
|
for _, l := range lines {
|
||||||
logText := strings.Replace(l, "\n", "\\n", -1)
|
logText := strings.Replace(l, "\n", "\\n", -1)
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -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()}} }
|
|
|
@ -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")
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -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",
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue