mirror of
https://gitlab.com/ric_harvey/MailHog.git
synced 2024-11-27 16:24:04 +00:00
Refactor SMTP protocol into separate package
This commit is contained in:
parent
cbc85d9647
commit
b46525f223
6 changed files with 438 additions and 277 deletions
10
bindata.go
10
bindata.go
|
@ -1593,6 +1593,11 @@ type _bintree_t struct {
|
|||
|
||||
var _bintree = &_bintree_t{nil, map[string]*_bintree_t{
|
||||
"assets": &_bintree_t{nil, map[string]*_bintree_t{
|
||||
"images": &_bintree_t{nil, 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{}},
|
||||
"ajax-loader.gif": &_bintree_t{assets_images_ajax_loader_gif, map[string]*_bintree_t{}},
|
||||
}},
|
||||
"js": &_bintree_t{nil, 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{}},
|
||||
|
@ -1601,10 +1606,5 @@ var _bintree = &_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{
|
||||
"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{}},
|
||||
"hog.png": &_bintree_t{assets_images_hog_png, map[string]*_bintree_t{}},
|
||||
}},
|
||||
}},
|
||||
}}
|
||||
|
|
318
mailhog/smtp/server/protocol.go
Normal file
318
mailhog/smtp/server/protocol.go
Normal file
|
@ -0,0 +1,318 @@
|
|||
package smtp
|
||||
|
||||
// http://www.rfc-editor.org/rfc/rfc5321.txt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/ian-kent/Go-MailHog/mailhog/config"
|
||||
"github.com/ian-kent/Go-MailHog/mailhog/data"
|
||||
)
|
||||
|
||||
// Protocol is a state machine representing an SMTP session
|
||||
type Protocol struct {
|
||||
conf *config.Config
|
||||
state State
|
||||
message *data.SMTPMessage
|
||||
|
||||
LogHandler func(message string, args ...interface{})
|
||||
MessageReceivedHandler func(*data.Message) (string, error)
|
||||
}
|
||||
|
||||
// Command is a struct representing an SMTP command (verb + arguments)
|
||||
type Command struct {
|
||||
verb string
|
||||
args string
|
||||
}
|
||||
|
||||
// Reply is a struct representing an SMTP reply (status code + lines)
|
||||
type Reply struct {
|
||||
status int
|
||||
lines []string
|
||||
}
|
||||
|
||||
// ReplyOk creates a 250 Ok reply
|
||||
func ReplyOk() *Reply { return &Reply{250, []string{"Ok"}} }
|
||||
|
||||
// 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
|
||||
AUTH2
|
||||
MAIL
|
||||
RCPT
|
||||
DATA
|
||||
DONE
|
||||
)
|
||||
|
||||
// StateMap provides string representations of SMTP conversation states
|
||||
var StateMap = map[State]string{
|
||||
INVALID: "INVALID",
|
||||
ESTABLISH: "ESTABLISH",
|
||||
AUTH: "AUTH",
|
||||
AUTH2: "AUTH2",
|
||||
MAIL: "MAIL",
|
||||
RCPT: "RCPT",
|
||||
DATA: "DATA",
|
||||
DONE: "DONE",
|
||||
}
|
||||
|
||||
// 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(cfg *config.Config) *Protocol {
|
||||
return &Protocol{
|
||||
conf: cfg,
|
||||
state: INVALID,
|
||||
message: &data.SMTPMessage{},
|
||||
}
|
||||
}
|
||||
|
||||
// TODO replace ".." lines with . in data
|
||||
|
||||
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
|
||||
func (proto *Protocol) Start() *Reply {
|
||||
proto.state = ESTABLISH
|
||||
return &Reply{
|
||||
status: 220,
|
||||
lines: []string{proto.conf.Hostname + " ESMTP Go-MailHog"},
|
||||
}
|
||||
}
|
||||
|
||||
// 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 move this to a buffer inside proto?
|
||||
func (proto *Protocol) Parse(line string) (string, *Reply) {
|
||||
var reply *Reply
|
||||
|
||||
for strings.Contains(line, "\n") {
|
||||
parts := strings.SplitN(line, "\n", 2)
|
||||
|
||||
if len(parts) == 2 {
|
||||
line = parts[1]
|
||||
} else {
|
||||
line = ""
|
||||
}
|
||||
|
||||
if proto.state == DATA {
|
||||
return line, proto.ProcessData(parts[0])
|
||||
}
|
||||
|
||||
return line, 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.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 := data.ParseSMTPMessage(proto.message, proto.conf.Hostname)
|
||||
|
||||
if proto.MessageReceivedHandler != nil {
|
||||
id, err := proto.MessageReceivedHandler(msg)
|
||||
if err != nil {
|
||||
proto.logf("Error storing message: %s", err)
|
||||
reply = &Reply{452, []string{"Unable to store message"}}
|
||||
} else {
|
||||
reply = &Reply{250, []string{"Ok: queued as " + id}}
|
||||
}
|
||||
} else {
|
||||
reply = &Reply{452, []string{"No storage backend"}}
|
||||
}
|
||||
}
|
||||
|
||||
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 := &Command{command, args}
|
||||
return proto.Command(cmd)
|
||||
}
|
||||
|
||||
// Command applies an SMTP verb and arguments to the state machine
|
||||
func (proto *Protocol) Command(command *Command) (reply *Reply) {
|
||||
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])
|
||||
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 AUTH == proto.state:
|
||||
proto.logf("Got authentication response: '%s', switching to MAIL state", command.args)
|
||||
proto.state = MAIL
|
||||
return &Reply{235, []string{"Authentication successful"}}
|
||||
case AUTH2 == proto.state: // TODO rename AUTH2 state...
|
||||
proto.logf("Got LOGIN authentication response: '%s', switching to AUTH state", command.args)
|
||||
proto.state = AUTH
|
||||
return &Reply{334, []string{"UGFzc3dvcmQ6"}}
|
||||
case MAIL == proto.state: // TODO rename/split 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 "))
|
||||
return &Reply{235, []string{"Authentication successful"}}
|
||||
case "LOGIN" == command.args:
|
||||
proto.logf("Got LOGIN authentication, switching to AUTH state")
|
||||
proto.state = AUTH2
|
||||
return &Reply{334, []string{"VXNlcm5hbWU6"}}
|
||||
case "PLAIN" == command.args:
|
||||
proto.logf("Got PLAIN authentication (no args), switching to AUTH2 state")
|
||||
proto.state = AUTH
|
||||
return &Reply{334, []string{}}
|
||||
case "CRAM-MD5" == command.args:
|
||||
proto.logf("Got CRAM-MD5 authentication, switching to AUTH state")
|
||||
proto.state = AUTH
|
||||
return &Reply{334, []string{"PDQxOTI5NDIzNDEuMTI4Mjg0NzJAc291cmNlZm91ci5hbmRyZXcuY211LmVkdT4="}}
|
||||
case strings.HasPrefix(command.args, "EXTERNAL "):
|
||||
proto.logf("Got EXTERNAL authentication: %s", strings.TrimPrefix(command.args, "EXTERNAL "))
|
||||
return &Reply{235, []string{"Authentication successful"}}
|
||||
default:
|
||||
return &Reply{504, []string{"Unsupported authentication mechanism"}}
|
||||
}
|
||||
case "MAIL":
|
||||
proto.logf("Got MAIL command, switching to RCPT state")
|
||||
from, err := ParseMAIL(command.args)
|
||||
if err != nil {
|
||||
return ReplyError(err)
|
||||
}
|
||||
proto.message.From = from
|
||||
proto.state = RCPT
|
||||
return ReplySenderOk(from)
|
||||
case "HELO": // TODO feels hacky
|
||||
return proto.HELO(command.args)
|
||||
case "EHLO": // TODO feels hacky?
|
||||
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 := ParseRCPT(command.args)
|
||||
if err != nil {
|
||||
return ReplyError(err)
|
||||
}
|
||||
proto.message.To = append(proto.message.To, to)
|
||||
proto.state = RCPT
|
||||
return ReplyRecipientOk(to)
|
||||
case "DATA":
|
||||
proto.logf("Got DATA command, switching to DATA state")
|
||||
proto.state = DATA
|
||||
return &Reply{354, []string{"End data with <CR><LF>.<CR><LF>"}}
|
||||
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 &Reply{250, []string{"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 &Reply{250, []string{"Hello " + args, "PIPELINING", "AUTH EXTERNAL CRAM-MD5 LOGIN PLAIN"}}
|
||||
}
|
||||
|
||||
// ParseMAIL returns the forward-path from a MAIL command argument
|
||||
func ParseMAIL(mail string) (string, error) {
|
||||
r := regexp.MustCompile("(?i:From):<([^>]+)>")
|
||||
match := r.FindStringSubmatch(mail)
|
||||
if len(match) != 2 {
|
||||
return "", errors.New("Invalid sender")
|
||||
}
|
||||
return match[1], nil
|
||||
}
|
||||
|
||||
// ParseRCPT returns the return-path from a RCPT command argument
|
||||
func ParseRCPT(rcpt string) (string, error) {
|
||||
r := regexp.MustCompile("(?i:To):<([^>]+)>")
|
||||
match := r.FindStringSubmatch(rcpt)
|
||||
if len(match) != 2 {
|
||||
return "", errors.New("Invalid recipient")
|
||||
}
|
||||
return match[1], nil
|
||||
}
|
113
mailhog/smtp/server/session.go
Normal file
113
mailhog/smtp/server/session.go
Normal file
|
@ -0,0 +1,113 @@
|
|||
package smtp
|
||||
|
||||
// http://www.rfc-editor.org/rfc/rfc5321.txt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ian-kent/Go-MailHog/mailhog/config"
|
||||
"github.com/ian-kent/Go-MailHog/mailhog/data"
|
||||
"github.com/ian-kent/Go-MailHog/mailhog/storage"
|
||||
)
|
||||
|
||||
// Session represents a SMTP session using net.TCPConn
|
||||
type Session struct {
|
||||
conn *net.TCPConn
|
||||
proto *Protocol
|
||||
conf *config.Config
|
||||
isTLS bool
|
||||
line string
|
||||
}
|
||||
|
||||
// Accept starts a new SMTP session using net.TCPConn
|
||||
func Accept(conn *net.TCPConn, conf *config.Config) {
|
||||
proto := NewProtocol(conf)
|
||||
session := &Session{conn, proto, conf, false, ""}
|
||||
proto.LogHandler = session.logf
|
||||
proto.MessageReceivedHandler = session.acceptMessageHandler
|
||||
|
||||
session.logf("Starting session")
|
||||
session.Write(proto.Start())
|
||||
for session.Read() == true {
|
||||
}
|
||||
session.logf("Session ended")
|
||||
}
|
||||
|
||||
func (c *Session) acceptMessageHandler(msg *data.Message) (id string, err error) {
|
||||
switch c.conf.Storage.(type) {
|
||||
case *storage.MongoDB:
|
||||
c.logf("Storing message using MongoDB")
|
||||
id, err = c.conf.Storage.(*storage.MongoDB).Store(msg)
|
||||
case *storage.Memory:
|
||||
c.logf("Storing message using Memory")
|
||||
id, err = c.conf.Storage.(*storage.Memory).Store(msg)
|
||||
default:
|
||||
err = errors.New("Unknown storage stype")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Session) logf(message string, args ...interface{}) {
|
||||
message = strings.Join([]string{"[SMTP %s]", message}, " ")
|
||||
args = append([]interface{}{c.conn.RemoteAddr()}, args...)
|
||||
log.Printf(message, args...)
|
||||
}
|
||||
|
||||
// Read reads from the underlying net.TCPConn
|
||||
func (c *Session) Read() bool {
|
||||
buf := make([]byte, 1024)
|
||||
n, err := c.conn.Read(buf)
|
||||
|
||||
if n == 0 {
|
||||
c.logf("Connection closed by remote host\n")
|
||||
return false
|
||||
}
|
||||
if err != nil {
|
||||
c.logf("Error reading from socket: %s\n", err)
|
||||
return false
|
||||
}
|
||||
|
||||
text := string(buf[0:n])
|
||||
logText := strings.Replace(text, "\n", "\\n", -1)
|
||||
logText = strings.Replace(logText, "\r", "\\r", -1)
|
||||
c.logf("Received %d bytes: '%s'\n", n, logText)
|
||||
|
||||
c.line += text
|
||||
|
||||
line, reply := c.proto.Parse(c.line)
|
||||
c.line = line
|
||||
|
||||
if reply != nil {
|
||||
c.Write(reply)
|
||||
if reply.status == 221 {
|
||||
c.conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Write writes a reply to the underlying net.TCPConn
|
||||
func (c *Session) Write(reply *Reply) {
|
||||
if len(reply.lines) == 0 {
|
||||
l := strconv.Itoa(reply.status)
|
||||
c.logf("Sent %d bytes: '%s'", len(l), l)
|
||||
c.conn.Write([]byte(l))
|
||||
}
|
||||
for i, line := range reply.lines {
|
||||
l := ""
|
||||
if i == len(reply.lines)-1 {
|
||||
l = strconv.Itoa(reply.status) + " " + line + "\n"
|
||||
} else {
|
||||
l = strconv.Itoa(reply.status) + "-" + line + "\n"
|
||||
}
|
||||
logText := strings.Replace(l, "\n", "\\n", -1)
|
||||
logText = strings.Replace(logText, "\r", "\\r", -1)
|
||||
c.logf("Sent %d bytes: '%s'", len(l), logText)
|
||||
c.conn.Write([]byte(l))
|
||||
}
|
||||
}
|
|
@ -1,270 +0,0 @@
|
|||
package smtp
|
||||
|
||||
// http://www.rfc-editor.org/rfc/rfc5321.txt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/ian-kent/Go-MailHog/mailhog/config"
|
||||
"github.com/ian-kent/Go-MailHog/mailhog/data"
|
||||
"github.com/ian-kent/Go-MailHog/mailhog/storage"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
conn *net.TCPConn
|
||||
line string
|
||||
conf *config.Config
|
||||
state int
|
||||
message *data.SMTPMessage
|
||||
isTLS bool
|
||||
}
|
||||
|
||||
const (
|
||||
ESTABLISH = iota
|
||||
AUTH
|
||||
AUTH2
|
||||
MAIL
|
||||
RCPT
|
||||
DATA
|
||||
DONE
|
||||
)
|
||||
|
||||
// TODO replace ".." lines with . in data
|
||||
|
||||
func StartSession(conn *net.TCPConn, conf *config.Config) {
|
||||
conv := &Session{conn, "", conf, ESTABLISH, &data.SMTPMessage{}, false}
|
||||
conv.log("Starting session")
|
||||
conv.Write("220", conv.conf.Hostname+" ESMTP Go-MailHog")
|
||||
conv.Read()
|
||||
}
|
||||
|
||||
func (c *Session) log(message string, args ...interface{}) {
|
||||
message = strings.Join([]string{"[SMTP %s, %d]", message}, " ")
|
||||
args = append([]interface{}{c.conn.RemoteAddr(), c.state}, args...)
|
||||
log.Printf(message, args...)
|
||||
}
|
||||
|
||||
func (c *Session) Read() {
|
||||
buf := make([]byte, 1024)
|
||||
n, err := c.conn.Read(buf)
|
||||
|
||||
if n == 0 {
|
||||
c.log("Connection closed by remote host\n")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.log("Error reading from socket: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
text := string(buf[0:n])
|
||||
c.log("Received %d bytes: '%s'\n", n, text)
|
||||
|
||||
c.line += text
|
||||
|
||||
c.Parse()
|
||||
}
|
||||
|
||||
func (c *Session) Parse() {
|
||||
for strings.Contains(c.line, "\n") {
|
||||
parts := strings.SplitN(c.line, "\n", 2)
|
||||
if len(parts) == 2 {
|
||||
c.line = parts[1]
|
||||
} else {
|
||||
c.line = ""
|
||||
}
|
||||
if c.state == DATA {
|
||||
c.message.Data += parts[0] + "\n"
|
||||
if strings.HasSuffix(c.message.Data, "\r\n.\r\n") {
|
||||
c.log("Got EOF, storing message and switching to MAIL state")
|
||||
//c.log("Full message data: %s", c.message.Data)
|
||||
c.message.Data = strings.TrimSuffix(c.message.Data, "\r\n.\r\n")
|
||||
msg := data.ParseSMTPMessage(c.message, c.conf.Hostname)
|
||||
var id string
|
||||
var err error
|
||||
switch c.conf.Storage.(type) {
|
||||
case *storage.MongoDB:
|
||||
c.log("Storing message using MongoDB")
|
||||
id, err = c.conf.Storage.(*storage.MongoDB).Store(msg)
|
||||
case *storage.Memory:
|
||||
c.log("Storing message using Memory")
|
||||
id, err = c.conf.Storage.(*storage.Memory).Store(msg)
|
||||
default:
|
||||
c.log("Unknown storage type")
|
||||
// TODO send error reply
|
||||
}
|
||||
c.state = MAIL
|
||||
if err != nil {
|
||||
c.log("Error storing message: %s", err)
|
||||
c.Write("452", "Unable to store message")
|
||||
return
|
||||
}
|
||||
c.Write("250", "Ok: queued as "+id)
|
||||
c.conf.MessageChan <- msg
|
||||
}
|
||||
} else {
|
||||
c.Process(strings.Trim(parts[0], "\r\n"))
|
||||
}
|
||||
}
|
||||
|
||||
c.Read()
|
||||
}
|
||||
|
||||
func (c *Session) Write(code string, text ...string) {
|
||||
if len(text) == 1 {
|
||||
c.log("Sent %d bytes: '%s'", len(text[0]+"\n"), text[0]+"\n")
|
||||
c.conn.Write([]byte(code + " " + text[0] + "\n"))
|
||||
return
|
||||
}
|
||||
for i := 0; i < len(text)-1; i++ {
|
||||
c.log("Sent %d bytes: '%s'", len(text[i]+"\n"), text[i]+"\n")
|
||||
c.conn.Write([]byte(code + "-" + text[i] + "\n"))
|
||||
}
|
||||
c.log("Sent %d bytes: '%s'", len(text[len(text)-1]+"\n"), text[len(text)-1]+"\n")
|
||||
c.conn.Write([]byte(code + " " + text[len(text)-1] + "\n"))
|
||||
}
|
||||
|
||||
func (c *Session) Process(line string) {
|
||||
c.log("Processing line: %s", line)
|
||||
|
||||
words := strings.Split(line, " ")
|
||||
command := strings.ToUpper(words[0])
|
||||
args := strings.Join(words[1:len(words)], " ")
|
||||
c.log("In state %d, got command '%s', args '%s'", c.state, command, args)
|
||||
|
||||
switch {
|
||||
case command == "RSET":
|
||||
c.log("Got RSET command, switching to MAIL state")
|
||||
c.state = MAIL
|
||||
c.message = &data.SMTPMessage{}
|
||||
c.Write("250", "Ok")
|
||||
case command == "NOOP":
|
||||
c.log("Got NOOP command")
|
||||
c.Write("250", "Ok")
|
||||
case command == "QUIT":
|
||||
c.log("Got QUIT command")
|
||||
c.Write("221", "Bye")
|
||||
err := c.conn.Close()
|
||||
if err != nil {
|
||||
c.log("Error closing connection")
|
||||
}
|
||||
case c.state == ESTABLISH:
|
||||
switch command {
|
||||
case "HELO":
|
||||
c.DoHELO(args)
|
||||
case "EHLO":
|
||||
c.DoEHLO(args)
|
||||
default:
|
||||
c.log("Got unknown command for ESTABLISH state: '%s'", command)
|
||||
c.Write("500", "Unrecognised command")
|
||||
}
|
||||
case c.state == AUTH:
|
||||
c.log("Got authentication response: '%s', switching to MAIL state", args)
|
||||
c.state = MAIL
|
||||
c.Write("235", "Authentication successful")
|
||||
case c.state == AUTH2:
|
||||
c.log("Got LOGIN authentication response: '%s', switching to AUTH state", args)
|
||||
c.state = AUTH
|
||||
c.Write("334", "UGFzc3dvcmQ6")
|
||||
case c.state == MAIL: // TODO rename/split state
|
||||
switch command {
|
||||
case "AUTH":
|
||||
c.log("Got AUTH command, staying in MAIL state")
|
||||
switch {
|
||||
case strings.HasPrefix(args, "PLAIN "):
|
||||
c.log("Got PLAIN authentication: %s", strings.TrimPrefix(args, "PLAIN "))
|
||||
c.Write("235", "Authentication successful")
|
||||
case args == "LOGIN":
|
||||
c.log("Got LOGIN authentication, switching to AUTH state")
|
||||
c.state = AUTH2
|
||||
c.Write("334", "VXNlcm5hbWU6")
|
||||
case args == "PLAIN":
|
||||
c.log("Got PLAIN authentication (no args), switching to AUTH2 state")
|
||||
c.state = AUTH
|
||||
c.Write("334", "")
|
||||
case args == "CRAM-MD5":
|
||||
c.log("Got CRAM-MD5 authentication, switching to AUTH state")
|
||||
c.state = AUTH
|
||||
c.Write("334", "PDQxOTI5NDIzNDEuMTI4Mjg0NzJAc291cmNlZm91ci5hbmRyZXcuY211LmVkdT4=")
|
||||
case strings.HasPrefix(args, "EXTERNAL "):
|
||||
c.log("Got EXTERNAL authentication: %s", strings.TrimPrefix(args, "EXTERNAL "))
|
||||
c.Write("235", "Authentication successful")
|
||||
default:
|
||||
c.Write("504", "Unsupported authentication mechanism")
|
||||
}
|
||||
case "MAIL":
|
||||
c.log("Got MAIL command, switching to RCPT state")
|
||||
from, err := ParseMAIL(args)
|
||||
if err != nil {
|
||||
c.Write("550", err.Error())
|
||||
return
|
||||
}
|
||||
c.message.From = from
|
||||
c.state = RCPT
|
||||
c.Write("250", "Sender "+from+" ok")
|
||||
case "HELO":
|
||||
c.DoHELO(args)
|
||||
case "EHLO":
|
||||
c.DoEHLO(args)
|
||||
default:
|
||||
c.log("Got unknown command for MAIL state: '%s'", command)
|
||||
c.Write("500", "Unrecognised command")
|
||||
}
|
||||
case c.state == RCPT:
|
||||
switch command {
|
||||
case "RCPT":
|
||||
c.log("Got RCPT command")
|
||||
to, err := ParseRCPT(args)
|
||||
if err != nil {
|
||||
c.Write("550", err.Error())
|
||||
return
|
||||
}
|
||||
c.message.To = append(c.message.To, to)
|
||||
c.state = RCPT
|
||||
c.Write("250", "Recipient "+to+" ok")
|
||||
case "DATA":
|
||||
c.log("Got DATA command, switching to DATA state")
|
||||
c.state = DATA
|
||||
c.Write("354", "End data with <CR><LF>.<CR><LF>")
|
||||
default:
|
||||
c.log("Got unknown command for RCPT state: '%s'", command)
|
||||
c.Write("500", "Unrecognised command")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Session) DoHELO(args string) {
|
||||
c.log("Got HELO command, switching to MAIL state")
|
||||
c.state = MAIL
|
||||
c.message.Helo = args
|
||||
c.Write("250", "Hello "+args)
|
||||
}
|
||||
|
||||
func (c *Session) DoEHLO(args string) {
|
||||
c.log("Got EHLO command, switching to MAIL state")
|
||||
c.state = MAIL
|
||||
c.message.Helo = args
|
||||
c.Write("250", "Hello "+args, "PIPELINING", "AUTH EXTERNAL CRAM-MD5 LOGIN PLAIN")
|
||||
}
|
||||
|
||||
func ParseMAIL(mail string) (string, error) {
|
||||
r := regexp.MustCompile("(?i:From):<([^>]+)>")
|
||||
match := r.FindStringSubmatch(mail)
|
||||
if len(match) != 2 {
|
||||
return "", errors.New("Invalid sender")
|
||||
}
|
||||
return match[1], nil
|
||||
}
|
||||
|
||||
func ParseRCPT(rcpt string) (string, error) {
|
||||
r := regexp.MustCompile("(?i:To):<([^>]+)>")
|
||||
match := r.FindStringSubmatch(rcpt)
|
||||
if len(match) != 2 {
|
||||
return "", errors.New("Invalid recipient")
|
||||
}
|
||||
return match[1], nil
|
||||
}
|
4
main.go
4
main.go
|
@ -8,7 +8,7 @@ import (
|
|||
"github.com/ian-kent/Go-MailHog/mailhog/config"
|
||||
mhhttp "github.com/ian-kent/Go-MailHog/mailhog/http"
|
||||
"github.com/ian-kent/Go-MailHog/mailhog/http/api"
|
||||
"github.com/ian-kent/Go-MailHog/mailhog/smtp"
|
||||
smtp "github.com/ian-kent/Go-MailHog/mailhog/smtp/server"
|
||||
"github.com/ian-kent/Go-MailHog/mailhog/storage"
|
||||
"github.com/ian-kent/envconf"
|
||||
"github.com/ian-kent/go-log/log"
|
||||
|
@ -122,6 +122,6 @@ func smtp_listen() *net.TCPListener {
|
|||
}
|
||||
defer conn.Close()
|
||||
|
||||
go smtp.StartSession(conn.(*net.TCPConn), conf)
|
||||
go smtp.Accept(conn.(*net.TCPConn), conf)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue