Clean up reply handling

This commit is contained in:
Ian Kent 2014-11-22 19:05:21 +00:00
parent b174a42877
commit cdf5577715
4 changed files with 130 additions and 102 deletions

View file

@ -1593,18 +1593,18 @@ type _bintree_t struct {
var _bintree = &_bintree_t{nil, map[string]*_bintree_t{ var _bintree = &_bintree_t{nil, map[string]*_bintree_t{
"assets": &_bintree_t{nil, map[string]*_bintree_t{ "assets": &_bintree_t{nil, map[string]*_bintree_t{
"templates": &_bintree_t{nil, map[string]*_bintree_t{
"index.html": &_bintree_t{assets_templates_index_html, map[string]*_bintree_t{}},
"layout.html": &_bintree_t{assets_templates_layout_html, map[string]*_bintree_t{}},
}},
"images": &_bintree_t{nil, map[string]*_bintree_t{ "images": &_bintree_t{nil, map[string]*_bintree_t{
"ajax-loader.gif": &_bintree_t{assets_images_ajax_loader_gif, map[string]*_bintree_t{}}, "ajax-loader.gif": &_bintree_t{assets_images_ajax_loader_gif, map[string]*_bintree_t{}},
"github.png": &_bintree_t{assets_images_github_png, map[string]*_bintree_t{}}, "github.png": &_bintree_t{assets_images_github_png, map[string]*_bintree_t{}},
"hog.png": &_bintree_t{assets_images_hog_png, map[string]*_bintree_t{}}, "hog.png": &_bintree_t{assets_images_hog_png, map[string]*_bintree_t{}},
}}, }},
"js": &_bintree_t{nil, map[string]*_bintree_t{ "js": &_bintree_t{nil, map[string]*_bintree_t{
"strutil.js": &_bintree_t{assets_js_strutil_js, map[string]*_bintree_t{}},
"controllers.js": &_bintree_t{assets_js_controllers_js, map[string]*_bintree_t{}}, "controllers.js": &_bintree_t{assets_js_controllers_js, map[string]*_bintree_t{}},
}}, "strutil.js": &_bintree_t{assets_js_strutil_js, map[string]*_bintree_t{}},
"templates": &_bintree_t{nil, map[string]*_bintree_t{
"index.html": &_bintree_t{assets_templates_index_html, map[string]*_bintree_t{}},
"layout.html": &_bintree_t{assets_templates_layout_html, map[string]*_bintree_t{}},
}}, }},
}}, }},
}} }}

View file

@ -11,74 +11,20 @@ import (
"github.com/ian-kent/Go-MailHog/mailhog/data" "github.com/ian-kent/Go-MailHog/mailhog/data"
) )
// Protocol is a state machine representing an SMTP session
type Protocol struct {
state State
message *data.SMTPMessage
hostname string
MessageIDHandler func() (string, error)
LogHandler func(message string, args ...interface{})
MessageReceivedHandler func(*data.Message) (string, error)
}
// Command is a struct representing an SMTP command (verb + arguments) // Command is a struct representing an SMTP command (verb + arguments)
type Command struct { type Command struct {
verb string verb string
args string args string
} }
// Reply is a struct representing an SMTP reply (status code + lines) // Protocol is a state machine representing an SMTP session
type Reply struct { type Protocol struct {
status int state State
lines []string message *data.SMTPMessage
} hostname string
// ReplyOk creates a 250 Ok reply LogHandler func(message string, args ...interface{})
func ReplyOk() *Reply { return &Reply{250, []string{"Ok"}} } MessageReceivedHandler func(*data.Message) (string, error)
// ReplyBye creates a 221 Bye reply
func ReplyBye() *Reply { return &Reply{221, []string{"Bye"}} }
// ReplyUnrecognisedCommand creates a 500 Unrecognised command reply
func ReplyUnrecognisedCommand() *Reply { return &Reply{500, []string{"Unrecognised command"}} }
// ReplySenderOk creates a 250 Sender ok reply
func ReplySenderOk(sender string) *Reply { return &Reply{250, []string{"Sender " + sender + " ok"}} }
// ReplyRecipientOk creates a 250 Sender ok reply
func ReplyRecipientOk(recipient string) *Reply {
return &Reply{250, []string{"Recipient " + recipient + " ok"}}
}
// ReplyError creates a 500 error reply
func ReplyError(err error) *Reply { return &Reply{550, []string{err.Error()}} }
// State represents the state of an SMTP conversation
type State int
// SMTP message conversation states
const (
INVALID = State(-1)
ESTABLISH = State(iota)
AUTH
AUTHLOGIN
MAIL
RCPT
DATA
DONE
)
// StateMap provides string representations of SMTP conversation states
var StateMap = map[State]string{
INVALID: "INVALID",
ESTABLISH: "ESTABLISH",
AUTH: "AUTH",
AUTHLOGIN: "AUTHLOGIN",
MAIL: "MAIL",
RCPT: "RCPT",
DATA: "DATA",
DONE: "DONE",
} }
// NewProtocol returns a new SMTP state machine in INVALID state // NewProtocol returns a new SMTP state machine in INVALID state
@ -105,10 +51,7 @@ func (proto *Protocol) logf(message string, args ...interface{}) {
func (proto *Protocol) Start(hostname string) *Reply { func (proto *Protocol) Start(hostname string) *Reply {
proto.state = ESTABLISH proto.state = ESTABLISH
proto.hostname = hostname proto.hostname = hostname
return &Reply{ return ReplyIdent(hostname + " ESMTP Go-MailHog")
status: 220,
lines: []string{hostname + " ESMTP Go-MailHog"},
}
} }
// Parse parses a line string and returns any remaining line string // Parse parses a line string and returns any remaining line string
@ -118,7 +61,10 @@ func (proto *Protocol) Start(hostname string) *Reply {
func (proto *Protocol) Parse(line string) (string, *Reply) { func (proto *Protocol) Parse(line string) (string, *Reply) {
var reply *Reply var reply *Reply
for strings.Contains(line, "\n") { if !strings.Contains(line, "\n") {
return line, reply
}
parts := strings.SplitN(line, "\n", 2) parts := strings.SplitN(line, "\n", 2)
if len(parts) == 2 { if len(parts) == 2 {
@ -128,10 +74,9 @@ func (proto *Protocol) Parse(line string) (string, *Reply) {
} }
if proto.state == DATA { if proto.state == DATA {
return line, proto.ProcessData(parts[0]) reply = proto.ProcessData(parts[0])
} } else {
reply = proto.ProcessCommand(parts[0])
return line, proto.ProcessCommand(parts[0])
} }
return line, reply return line, reply
@ -152,17 +97,16 @@ func (proto *Protocol) ProcessData(line string) (reply *Reply) {
msg := proto.message.Parse(proto.hostname) msg := proto.message.Parse(proto.hostname)
if proto.MessageReceivedHandler != nil { if proto.MessageReceivedHandler == nil {
return ReplyStorageFailed("No storage backend")
}
id, err := proto.MessageReceivedHandler(msg) id, err := proto.MessageReceivedHandler(msg)
if err != nil { if err != nil {
proto.logf("Error storing message: %s", err) proto.logf("Error storing message: %s", err)
reply = &Reply{452, []string{"Unable to store message"}} return ReplyStorageFailed("Unable to store message")
} else {
reply = &Reply{250, []string{"Ok: queued as " + id}}
}
} else {
reply = &Reply{452, []string{"No storage backend"}}
} }
return ReplyOk("Ok: queued as " + id)
} }
return return
@ -210,11 +154,11 @@ func (proto *Protocol) Command(command *Command) (reply *Reply) {
case AUTH == proto.state: case AUTH == proto.state:
proto.logf("Got authentication response: '%s', switching to MAIL state", command.args) proto.logf("Got authentication response: '%s', switching to MAIL state", command.args)
proto.state = MAIL proto.state = MAIL
return &Reply{235, []string{"Authentication successful"}} return ReplyAuthOk()
case AUTHLOGIN == proto.state: case AUTHLOGIN == proto.state:
proto.logf("Got LOGIN authentication response: '%s', switching to AUTH state", command.args) proto.logf("Got LOGIN authentication response: '%s', switching to AUTH state", command.args)
proto.state = AUTH proto.state = AUTH
return &Reply{334, []string{"UGFzc3dvcmQ6"}} return ReplyAuthResponse("UGFzc3dvcmQ6")
case MAIL == proto.state: case MAIL == proto.state:
switch command.verb { switch command.verb {
case "AUTH": case "AUTH":
@ -222,24 +166,24 @@ func (proto *Protocol) Command(command *Command) (reply *Reply) {
switch { switch {
case strings.HasPrefix(command.args, "PLAIN "): case strings.HasPrefix(command.args, "PLAIN "):
proto.logf("Got PLAIN authentication: %s", strings.TrimPrefix(command.args, "PLAIN ")) proto.logf("Got PLAIN authentication: %s", strings.TrimPrefix(command.args, "PLAIN "))
return &Reply{235, []string{"Authentication successful"}} return ReplyAuthOk()
case "LOGIN" == command.args: case "LOGIN" == command.args:
proto.logf("Got LOGIN authentication, switching to AUTH state") proto.logf("Got LOGIN authentication, switching to AUTH state")
proto.state = AUTHLOGIN proto.state = AUTHLOGIN
return &Reply{334, []string{"VXNlcm5hbWU6"}} return ReplyAuthResponse("VXNlcm5hbWU6")
case "PLAIN" == command.args: case "PLAIN" == command.args:
proto.logf("Got PLAIN authentication (no args), switching to AUTH2 state") proto.logf("Got PLAIN authentication (no args), switching to AUTH2 state")
proto.state = AUTH proto.state = AUTH
return &Reply{334, []string{}} return ReplyAuthResponse("")
case "CRAM-MD5" == command.args: case "CRAM-MD5" == command.args:
proto.logf("Got CRAM-MD5 authentication, switching to AUTH state") proto.logf("Got CRAM-MD5 authentication, switching to AUTH state")
proto.state = AUTH proto.state = AUTH
return &Reply{334, []string{"PDQxOTI5NDIzNDEuMTI4Mjg0NzJAc291cmNlZm91ci5hbmRyZXcuY211LmVkdT4="}} return ReplyAuthResponse("PDQxOTI5NDIzNDEuMTI4Mjg0NzJAc291cmNlZm91ci5hbmRyZXcuY211LmVkdT4=")
case strings.HasPrefix(command.args, "EXTERNAL "): case strings.HasPrefix(command.args, "EXTERNAL "):
proto.logf("Got EXTERNAL authentication: %s", strings.TrimPrefix(command.args, "EXTERNAL ")) proto.logf("Got EXTERNAL authentication: %s", strings.TrimPrefix(command.args, "EXTERNAL "))
return &Reply{235, []string{"Authentication successful"}} return ReplyAuthOk()
default: default:
return &Reply{504, []string{"Unsupported authentication mechanism"}} return ReplyUnsupportedAuth()
} }
case "MAIL": case "MAIL":
proto.logf("Got MAIL command, switching to RCPT state") proto.logf("Got MAIL command, switching to RCPT state")
@ -272,7 +216,7 @@ func (proto *Protocol) Command(command *Command) (reply *Reply) {
case "DATA": case "DATA":
proto.logf("Got DATA command, switching to DATA state") proto.logf("Got DATA command, switching to DATA state")
proto.state = DATA proto.state = DATA
return &Reply{354, []string{"End data with <CR><LF>.<CR><LF>"}} return ReplyDataResponse()
default: default:
proto.logf("Got unknown command for RCPT state: '%s'", command) proto.logf("Got unknown command for RCPT state: '%s'", command)
return ReplyUnrecognisedCommand() return ReplyUnrecognisedCommand()
@ -287,7 +231,7 @@ func (proto *Protocol) HELO(args string) (reply *Reply) {
proto.logf("Got HELO command, switching to MAIL state") proto.logf("Got HELO command, switching to MAIL state")
proto.state = MAIL proto.state = MAIL
proto.message.Helo = args proto.message.Helo = args
return &Reply{250, []string{"Hello " + args}} return ReplyOk("Hello " + args)
} }
// EHLO creates a reply to a EHLO command // EHLO creates a reply to a EHLO command
@ -295,7 +239,7 @@ func (proto *Protocol) EHLO(args string) (reply *Reply) {
proto.logf("Got EHLO command, switching to MAIL state") proto.logf("Got EHLO command, switching to MAIL state")
proto.state = MAIL proto.state = MAIL
proto.message.Helo = args proto.message.Helo = args
return &Reply{250, []string{"Hello " + args, "PIPELINING", "AUTH EXTERNAL CRAM-MD5 LOGIN PLAIN"}} return ReplyOk("Hello "+args, "PIPELINING", "AUTH EXTERNAL CRAM-MD5 LOGIN PLAIN")
} }
// ParseMAIL returns the forward-path from a MAIL command argument // ParseMAIL returns the forward-path from a MAIL command argument

View file

@ -0,0 +1,54 @@
package smtp
// 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
}
// ReplyIdent creates a 220 welcome reply
func ReplyIdent(ident string) *Reply { return &Reply{220, []string{ident}} }
// ReplyBye creates a 221 Bye reply
func ReplyBye() *Reply { return &Reply{221, []string{"Bye"}} }
// ReplyAuthOk creates a 235 authentication successful reply
func ReplyAuthOk() *Reply { return &Reply{235, []string{"Authentication successful"}} }
// ReplyOk creates a 250 Ok reply
func ReplyOk(message ...string) *Reply {
if len(message) == 0 {
message = []string{"Ok"}
}
return &Reply{250, message}
}
// ReplySenderOk creates a 250 Sender ok reply
func ReplySenderOk(sender string) *Reply { return &Reply{250, []string{"Sender " + sender + " ok"}} }
// ReplyRecipientOk creates a 250 Sender ok reply
func ReplyRecipientOk(recipient string) *Reply {
return &Reply{250, []string{"Recipient " + recipient + " ok"}}
}
// ReplyAuthResponse creates a 334 authentication reply
func ReplyAuthResponse(response string) *Reply { return &Reply{334, []string{response}} }
// ReplyDataResponse creates a 354 data reply
func ReplyDataResponse() *Reply { return &Reply{354, []string{"End data with <CR><LF>.<CR><LF>"}} }
// ReplyStorageFailed creates a 452 error reply
func ReplyStorageFailed(reason string) *Reply { return &Reply{452, []string{reason}} }
// ReplyUnrecognisedCommand creates a 500 Unrecognised command reply
func ReplyUnrecognisedCommand() *Reply { return &Reply{500, []string{"Unrecognised command"}} }
// ReplyUnsupportedAuth creates a 504 unsupported authentication reply
func ReplyUnsupportedAuth() *Reply {
return &Reply{504, []string{"Unsupported authentication mechanism"}}
}
// ReplyError creates a 500 error reply
func ReplyError(err error) *Reply { return &Reply{550, []string{err.Error()}} }

View file

@ -0,0 +1,30 @@
package smtp
// http://www.rfc-editor.org/rfc/rfc5321.txt
// State represents the state of an SMTP conversation
type State int
// SMTP message conversation states
const (
INVALID = State(-1)
ESTABLISH = State(iota)
AUTH
AUTHLOGIN
MAIL
RCPT
DATA
DONE
)
// StateMap provides string representations of SMTP conversation states
var StateMap = map[State]string{
INVALID: "INVALID",
ESTABLISH: "ESTABLISH",
AUTH: "AUTH",
AUTHLOGIN: "AUTHLOGIN",
MAIL: "MAIL",
RCPT: "RCPT",
DATA: "DATA",
DONE: "DONE",
}