mirror of
https://gitlab.com/ric_harvey/MailHog.git
synced 2024-11-27 16:24:04 +00:00
Clean up reply handling
This commit is contained in:
parent
b174a42877
commit
cdf5577715
4 changed files with 130 additions and 102 deletions
10
bindata.go
10
bindata.go
|
@ -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{}},
|
|
||||||
}},
|
}},
|
||||||
}},
|
}},
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -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
|
||||||
|
|
54
mailhog/smtp/server/reply.go
Normal file
54
mailhog/smtp/server/reply.go
Normal 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()}} }
|
30
mailhog/smtp/server/state.go
Normal file
30
mailhog/smtp/server/state.go
Normal 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",
|
||||||
|
}
|
Loading…
Reference in a new issue