MailHog/mailhog/smtp/session.go

220 lines
6.2 KiB
Go
Raw Normal View History

2014-04-16 22:59:25 +00:00
package smtp
// http://www.rfc-editor.org/rfc/rfc5321.txt
import (
"log"
"net"
2014-04-18 17:25:36 +00:00
"strings"
2014-04-19 22:37:11 +00:00
"regexp"
"github.com/ian-kent/MailHog/mailhog"
2014-04-19 22:37:11 +00:00
"github.com/ian-kent/MailHog/mailhog/storage"
"github.com/ian-kent/MailHog/mailhog/data"
2014-04-16 22:59:25 +00:00
)
type Session struct {
conn *net.TCPConn
2014-04-18 17:25:36 +00:00
line string
conf *mailhog.Config
state int
2014-04-19 22:37:11 +00:00
message *data.SMTPMessage
2014-04-20 19:33:42 +00:00
mongo *storage.MongoDB
2014-04-22 20:19:48 +00:00
isTLS bool
2014-04-16 22:59:25 +00:00
}
const (
ESTABLISH = iota
2014-04-22 20:19:48 +00:00
AUTH
AUTH2
MAIL
RCPT
DATA
2014-04-19 11:44:05 +00:00
DONE
)
2014-04-19 22:37:11 +00:00
// TODO replace ".." lines with . in data
2014-04-16 22:59:25 +00:00
2014-04-20 19:33:42 +00:00
func StartSession(conn *net.TCPConn, conf *mailhog.Config, mongo *storage.MongoDB) {
2014-04-22 20:19:48 +00:00
conv := &Session{conn, "", conf, ESTABLISH, &data.SMTPMessage{}, mongo, false}
conv.log("Starting session")
2014-04-19 11:44:05 +00:00
conv.Write("220", conv.conf.Hostname + " ESMTP Go-MailHog")
conv.Read()
2014-04-16 23:11:56 +00:00
}
func (c *Session) log(message string, args ...interface{}) {
2014-04-20 14:35:59 +00:00
message = strings.Join([]string{"[SMTP %s, %d]", message}, " ")
args = append([]interface{}{c.conn.RemoteAddr(), c.state}, args...)
2014-04-18 17:25:36 +00:00
log.Printf(message, args...)
}
func (c *Session) Read() {
2014-04-18 17:25:36 +00:00
buf := make([]byte, 1024)
2014-04-16 23:11:56 +00:00
n, err := c.conn.Read(buf)
2014-04-18 17:25:36 +00:00
2014-04-16 23:11:56 +00:00
if n == 0 {
2014-04-18 17:25:36 +00:00
c.log("Connection closed by remote host\n")
2014-04-16 23:11:56 +00:00
return
}
if err != nil {
2014-04-18 17:25:36 +00:00
c.log("Error reading from socket: %s\n", err)
2014-04-16 23:11:56 +00:00
return
}
2014-04-18 17:25:36 +00:00
text := string(buf[0:n])
c.log("Received %d bytes: '%s'\n", n, text)
2014-04-18 17:25:36 +00:00
c.line += text
c.Parse()
2014-04-16 23:11:56 +00:00
}
func (c *Session) Parse() {
2014-04-18 17:25:36 +00:00
for strings.Contains(c.line, "\n") {
parts := strings.SplitN(c.line, "\n", 2)
if len(parts) == 2 {
c.line = parts[1]
} else {
c.line = ""
}
2014-04-19 11:44:05 +00:00
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)
2014-04-19 22:37:11 +00:00
c.message.Data = strings.TrimSuffix(c.message.Data, "\r\n.\r\n")
2014-04-20 19:33:42 +00:00
id, err := c.mongo.Store(c.message)
c.state = MAIL
2014-04-19 22:37:11 +00:00
if err != nil {
c.log("Error storing message: %s", err)
c.Write("452", "Unable to store message")
2014-04-19 22:37:11 +00:00
return
}
c.Write("250", "Ok: queued as " + id)
2014-04-19 11:44:05 +00:00
}
} else {
c.Process(strings.Trim(parts[0], "\r\n"))
}
2014-04-18 17:25:36 +00:00
}
2014-04-16 23:11:56 +00:00
c.Read()
2014-04-16 22:59:25 +00:00
}
func (c *Session) Write(code string, text ...string) {
2014-04-18 17:25:36 +00:00
if len(text) == 1 {
c.log("Sent %d bytes: '%s'", len(text[0] + "\n"), text[0] + "\n")
2014-04-18 17:25:36 +00:00
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")
2014-04-18 17:25:36 +00:00
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"))
2014-04-18 17:25:36 +00:00
}
func (c *Session) Process(line string) {
2014-04-18 17:25:36 +00:00
c.log("Processing line: %s", line)
words := strings.Split(line, " ")
command := words[0]
2014-04-19 11:44:05 +00:00
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 ESTABLISH state")
c.state = ESTABLISH
2014-04-19 22:37:11 +00:00
c.message = &data.SMTPMessage{}
2014-04-19 11:44:05 +00:00
c.Write("250", "Ok")
case command == "NOOP":
c.log("Got NOOP command")
2014-04-19 11:44:05 +00:00
c.Write("250", "Ok")
case command == "QUIT":
c.log("Got QUIT command")
2014-04-19 11:44:05 +00:00
c.Write("221", "Bye")
2014-04-19 22:37:11 +00:00
err := c.conn.Close()
if err != nil {
c.log("Error closing connection")
}
case c.state == ESTABLISH:
switch command {
case "HELO":
c.log("Got HELO command, switching to MAIL state")
c.state = MAIL
2014-04-19 11:44:05 +00:00
c.message.Helo = args
c.Write("250", "Hello " + args)
case "EHLO":
c.log("Got EHLO command, switching to MAIL state")
c.state = MAIL
2014-04-19 11:44:05 +00:00
c.message.Helo = args
2014-04-22 20:19:48 +00:00
c.Write("250", "Hello " + args, "PIPELINING", "AUTH EXTERNAL CRAM-MD5 LOGIN PLAIN")
default:
c.log("Got unknown command for ESTABLISH state: '%s'", command)
c.Write("500", "Unrecognised command")
}
2014-04-22 20:19:48 +00:00
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", "VXNlcm5hbWU6")
case c.state == MAIL: // TODO rename/split state
switch command {
2014-04-22 20:19:48 +00:00
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 = AUTH
c.Write("334", "VXNlcm5hbWU6")
case args == "PLAIN":
c.log("Got PLAIN authentication (no args), switching to AUTH 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=")
2014-04-22 20:31:32 +00:00
case strings.HasPrefix(args, "EXTERNAL "):
2014-04-22 20:19:48 +00:00
c.log("Got EXTERNAL authentication: %s", strings.TrimPrefix(args, "EXTERNAL "))
c.Write("235", "Authentication successful")
default:
c.Write("504", "Unsupported authentication mechanism")
}
case "MAIL":
2014-04-19 11:44:05 +00:00
c.log("Got MAIL command, switching to RCPT state")
2014-04-19 22:37:11 +00:00
r, _ := regexp.Compile("From:<([^>]+)>")
match := r.FindStringSubmatch(args)
c.message.From = match[1]
2014-04-19 11:44:05 +00:00
c.state = RCPT
2014-04-19 22:37:11 +00:00
c.Write("250", "Sender " + match[1] + " ok")
default:
c.log("Got unknown command for MAIL state: '%s'", command)
c.Write("500", "Unrecognised command")
}
2014-04-19 11:44:05 +00:00
case c.state == RCPT:
switch command {
case "RCPT":
c.log("Got RCPT command")
2014-04-19 22:37:11 +00:00
r, _ := regexp.Compile("To:<([^>]+)>")
match := r.FindStringSubmatch(args)
c.message.To = append(c.message.To, match[1])
c.state = RCPT
c.Write("250", "Recipient " + match[1] + " ok")
2014-04-19 11:44:05 +00:00
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")
2014-04-19 11:44:05 +00:00
}
2014-04-18 17:25:36 +00:00
}
}