mirror of
https://gitlab.com/ric_harvey/MailHog.git
synced 2024-11-23 22:34:04 +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/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)
|
||||
|
|
|
@ -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…
Reference in a new issue