mirror of
https://gitlab.com/ric_harvey/MailHog.git
synced 2024-11-30 09:44:03 +00:00
Clean up message parsing and add tests
This commit is contained in:
parent
b5f41ce7b9
commit
05d9c9ddeb
4 changed files with 289 additions and 15 deletions
103
mailhog/data/message.go
Normal file
103
mailhog/data/message.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
package data;
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
"labix.org/v2/mgo/bson"
|
||||
"github.com/ian-kent/MailHog/mailhog"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Id string
|
||||
From *Path
|
||||
To []*Path
|
||||
Content *Content
|
||||
Created time.Time
|
||||
}
|
||||
|
||||
type Path struct {
|
||||
Relays []string
|
||||
Mailbox string
|
||||
Domain string
|
||||
Params string
|
||||
}
|
||||
|
||||
type Content struct {
|
||||
Headers map[string][]string
|
||||
Body string
|
||||
Size int
|
||||
}
|
||||
|
||||
type SMTPMessage struct {
|
||||
From string
|
||||
To []string
|
||||
Data string
|
||||
Helo string
|
||||
}
|
||||
|
||||
func ParseSMTPMessage(c *mailhog.Config, m *SMTPMessage) *Message {
|
||||
arr := make([]*Path, 0)
|
||||
for _, path := range m.To {
|
||||
arr = append(arr, PathFromString(path))
|
||||
}
|
||||
msg := &Message{
|
||||
Id: bson.NewObjectId().Hex(),
|
||||
From: PathFromString(m.From),
|
||||
To: arr,
|
||||
Content: ContentFromString(m.Data),
|
||||
Created: time.Now(),
|
||||
}
|
||||
msg.Content.Headers["Message-ID"] = []string{msg.Id + "@" + c.Hostname} // FIXME
|
||||
msg.Content.Headers["Received"] = []string{"from " + m.Helo + " by " + c.Hostname + " (Go-MailHog)"} // FIXME
|
||||
msg.Content.Headers["Return-Path"] = []string{"<" + m.From + ">"}
|
||||
return msg
|
||||
}
|
||||
|
||||
func PathFromString(path string) *Path {
|
||||
relays := make([]string, 0)
|
||||
email := path
|
||||
if(strings.Contains(path, ":")) {
|
||||
x := strings.SplitN(path, ":", 2)
|
||||
r, e := x[0], x[1]
|
||||
email = e
|
||||
relays = strings.Split(r, ",")
|
||||
}
|
||||
mailbox, domain := "", ""
|
||||
if(strings.Contains(email, "@")) {
|
||||
x := strings.SplitN(email, "@", 2)
|
||||
mailbox, domain = x[0], x[1]
|
||||
} else {
|
||||
mailbox = email
|
||||
}
|
||||
|
||||
return &Path{
|
||||
Relays: relays,
|
||||
Mailbox: mailbox,
|
||||
Domain: domain,
|
||||
Params: "", // FIXME?
|
||||
}
|
||||
}
|
||||
|
||||
func ContentFromString(data string) *Content {
|
||||
x := strings.Split(data, "\r\n\r\n")
|
||||
headers, body := x[0], x[1]
|
||||
|
||||
h := make(map[string][]string, 0)
|
||||
hdrs := strings.Split(headers, "\r\n")
|
||||
for _, hdr := range hdrs {
|
||||
if(strings.Contains(hdr, ": ")) {
|
||||
y := strings.SplitN(hdr, ": ", 2)
|
||||
key, value := y[0], y[1]
|
||||
h[key] = []string{value}
|
||||
} else {
|
||||
log.Printf("Found invalid header: '%s'", hdr)
|
||||
}
|
||||
}
|
||||
|
||||
return &Content{
|
||||
Size: len(data),
|
||||
Headers: h,
|
||||
Body: body,
|
||||
}
|
||||
}
|
|
@ -6,7 +6,10 @@ import (
|
|||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"regexp"
|
||||
"github.com/ian-kent/MailHog/mailhog"
|
||||
"github.com/ian-kent/MailHog/mailhog/storage"
|
||||
"github.com/ian-kent/MailHog/mailhog/data"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
|
@ -14,7 +17,7 @@ type Session struct {
|
|||
line string
|
||||
conf *mailhog.Config
|
||||
state int
|
||||
message *Message
|
||||
message *data.SMTPMessage
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -25,15 +28,11 @@ const (
|
|||
DONE
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
From string
|
||||
To []string
|
||||
Data string
|
||||
Helo string
|
||||
}
|
||||
// TODO add Received/Return-Path headers
|
||||
// TODO replace ".." lines with . in data
|
||||
|
||||
func StartSession(conn *net.TCPConn, conf *mailhog.Config) {
|
||||
conv := &Session{conn, "", conf, ESTABLISH, &Message{}}
|
||||
conv := &Session{conn, "", conf, ESTABLISH, &data.SMTPMessage{}}
|
||||
conv.log("Starting session")
|
||||
conv.Write("220", conv.conf.Hostname + " ESMTP Go-MailHog")
|
||||
conv.Read()
|
||||
|
@ -77,8 +76,15 @@ func (c *Session) Parse() {
|
|||
if c.state == DATA {
|
||||
c.message.Data += parts[0] + "\n"
|
||||
if(strings.HasSuffix(c.message.Data, "\r\n.\r\n")) {
|
||||
c.message.Data = strings.TrimSuffix(c.message.Data, "\r\n.\r\n")
|
||||
id, err := storage.Store(c.conf, c.message)
|
||||
c.state = DONE
|
||||
c.Write("250", "Ok: queued as nnnnnnnn")
|
||||
if err != nil {
|
||||
// FIXME
|
||||
c.Write("500", "Error")
|
||||
return
|
||||
}
|
||||
c.Write("250", "Ok: queued as " + id)
|
||||
}
|
||||
} else {
|
||||
c.Process(strings.Trim(parts[0], "\r\n"))
|
||||
|
@ -111,7 +117,7 @@ func (c *Session) Process(line string) {
|
|||
case command == "RSET":
|
||||
c.log("Got RSET command, switching to ESTABLISH state")
|
||||
c.state = ESTABLISH
|
||||
c.message = &Message{}
|
||||
c.message = &data.SMTPMessage{}
|
||||
c.Write("250", "Ok")
|
||||
case command == "NOOP":
|
||||
c.log("Got NOOP command")
|
||||
|
@ -119,6 +125,10 @@ func (c *Session) Process(line string) {
|
|||
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":
|
||||
|
@ -138,10 +148,11 @@ func (c *Session) Process(line string) {
|
|||
switch command {
|
||||
case "MAIL":
|
||||
c.log("Got MAIL command, switching to RCPT state")
|
||||
// TODO parse args
|
||||
c.message.From = args
|
||||
r, _ := regexp.Compile("From:<([^>]+)>")
|
||||
match := r.FindStringSubmatch(args)
|
||||
c.message.From = match[1]
|
||||
c.state = RCPT
|
||||
c.Write("250", "Ok")
|
||||
c.Write("250", "Sender " + match[1] + " ok")
|
||||
default:
|
||||
c.log("Got unknown command for MAIL state: '%s'", command)
|
||||
}
|
||||
|
@ -149,8 +160,11 @@ func (c *Session) Process(line string) {
|
|||
switch command {
|
||||
case "RCPT":
|
||||
c.log("Got RCPT command")
|
||||
c.message.To = append(c.message.To, args)
|
||||
c.Write("250", "Ok")
|
||||
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")
|
||||
case "DATA":
|
||||
c.log("Got DATA command, switching to DATA state")
|
||||
c.state = DATA
|
||||
|
@ -160,12 +174,14 @@ func (c *Session) Process(line string) {
|
|||
}
|
||||
case c.state == DONE:
|
||||
switch command {
|
||||
/*
|
||||
case "MAIL":
|
||||
c.log("Got MAIL command")
|
||||
// TODO parse args
|
||||
c.message.From = args
|
||||
c.state = RCPT
|
||||
c.Write("250", "Ok")
|
||||
*/
|
||||
default:
|
||||
c.log("Got unknown command for DONE state: '%s'", command)
|
||||
}
|
41
mailhog/storage/mongodb.go
Normal file
41
mailhog/storage/mongodb.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"log"
|
||||
"labix.org/v2/mgo"
|
||||
"labix.org/v2/mgo/bson"
|
||||
"github.com/ian-kent/MailHog/mailhog/data"
|
||||
"github.com/ian-kent/MailHog/mailhog"
|
||||
)
|
||||
|
||||
func Store(c *mailhog.Config, m *data.SMTPMessage) (string, error) {
|
||||
msg := data.ParseSMTPMessage(c, m)
|
||||
session, err := mgo.Dial("localhost:27017")
|
||||
if(err != nil) {
|
||||
log.Printf("Error connecting to MongoDB: %s", err)
|
||||
return "", err
|
||||
}
|
||||
defer session.Close()
|
||||
err = session.DB("mailhog").C("messages").Insert(msg)
|
||||
if err != nil {
|
||||
log.Printf("Error inserting message: %s", err)
|
||||
return "", err
|
||||
}
|
||||
return msg.Id, nil
|
||||
}
|
||||
|
||||
func Load(id string) (*data.Message, error) {
|
||||
session, err := mgo.Dial("localhost:27017")
|
||||
if(err != nil) {
|
||||
log.Printf("Error connecting to MongoDB: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
defer session.Close()
|
||||
result := &data.Message{}
|
||||
err = session.DB("mailhog").C("messages").Find(bson.M{"id": id}).One(&result)
|
||||
if err != nil {
|
||||
log.Printf("Error loading message: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
return result, nil;
|
||||
}
|
114
main_test.go
Normal file
114
main_test.go
Normal file
|
@ -0,0 +1,114 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net"
|
||||
"regexp"
|
||||
"github.com/ian-kent/MailHog/mailhog/storage"
|
||||
)
|
||||
|
||||
// FIXME requires a running instance of MailHog
|
||||
|
||||
func TestBasicHappyPath(t *testing.T) {
|
||||
buf := make([]byte, 1024)
|
||||
|
||||
// Open a connection
|
||||
conn, err := net.Dial("tcp", "127.0.0.1:1025")
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Read the greeting
|
||||
n, err := conn.Read(buf)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, string(buf[0:n]), "220 mailhog.example ESMTP Go-MailHog\n")
|
||||
|
||||
// Send EHLO
|
||||
_, err = conn.Write([]byte("EHLO localhost\r\n"))
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Read the response
|
||||
n, err = conn.Read(buf)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, string(buf[0:n]), "250 Hello localhost\n")
|
||||
|
||||
// Send MAIL
|
||||
_, err = conn.Write([]byte("MAIL From:<nobody@mailhog.example>\r\n"))
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Read the response
|
||||
n, err = conn.Read(buf)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, string(buf[0:n]), "250 Sender nobody@mailhog.example ok\n")
|
||||
|
||||
// Send RCPT
|
||||
_, err = conn.Write([]byte("RCPT To:<someone@mailhog.example>\r\n"))
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Read the response
|
||||
n, err = conn.Read(buf)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, string(buf[0:n]), "250 Recipient someone@mailhog.example ok\n")
|
||||
|
||||
// Send DATA
|
||||
_, err = conn.Write([]byte("DATA\r\n"))
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Read the response
|
||||
n, err = conn.Read(buf)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, string(buf[0:n]), "354 End data with <CR><LF>.<CR><LF>\n")
|
||||
|
||||
// Send the message
|
||||
content := "Content-Type: text/plain\r\n"
|
||||
content += "Content-Length: 220\r\n"
|
||||
content += "From: Nobody <nobody@mailhog.example>\r\n"
|
||||
content += "To: Someone <someone@mailhog.example>\r\n"
|
||||
content += "\r\n"
|
||||
content += "Hi there :)\r\n"
|
||||
content += ".\r\n"
|
||||
_, err = conn.Write([]byte(content))
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Read the response
|
||||
n, err = conn.Read(buf)
|
||||
assert.Nil(t, err)
|
||||
r, _ := regexp.Compile("250 Ok: queued as ([0-9a-f]+)\n")
|
||||
match := r.FindStringSubmatch(string(buf[0:n]))
|
||||
assert.NotNil(t, match)
|
||||
|
||||
// Send QUIT
|
||||
_, err = conn.Write([]byte("QUIT\r\n"))
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Read the response
|
||||
n, err = conn.Read(buf)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, string(buf[0:n]), "221 Bye\n")
|
||||
|
||||
message, err := storage.Load(match[1])
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, message)
|
||||
|
||||
assert.Equal(t, message.From.Domain, "mailhog.example", "sender domain is mailhog.example")
|
||||
assert.Equal(t, message.From.Mailbox, "nobody", "sender mailbox is nobody")
|
||||
assert.Equal(t, message.From.Params, "", "sender params is empty")
|
||||
assert.Equal(t, len(message.From.Relays), 0, "sender has no relays")
|
||||
|
||||
assert.Equal(t, len(message.To), 1, "message has 1 recipient")
|
||||
|
||||
assert.Equal(t, message.To[0].Domain, "mailhog.example", "recipient domain is mailhog.example")
|
||||
assert.Equal(t, message.To[0].Mailbox, "someone", "recipient mailbox is someone")
|
||||
assert.Equal(t, message.To[0].Params, "", "recipient params is empty")
|
||||
assert.Equal(t, len(message.To[0].Relays), 0, "recipient has no relays")
|
||||
|
||||
assert.Equal(t, len(message.Content.Headers), 7, "message has 7 headers")
|
||||
assert.Equal(t, message.Content.Headers["Content-Type"], []string{"text/plain"}, "Content-Type header is text/plain")
|
||||
assert.Equal(t, message.Content.Headers["Content-Length"], []string{"220"}, "Content-Length is 220")
|
||||
assert.Equal(t, message.Content.Headers["To"], []string{"Someone <someone@mailhog.example>"}, "To is Someone <someone@mailhog.example>")
|
||||
assert.Equal(t, message.Content.Headers["From"], []string{"Nobody <nobody@mailhog.example>"}, "From is Nobody <nobody@mailhog.example>")
|
||||
assert.Equal(t, message.Content.Headers["Received"], []string{"from localhost by mailhog.example (Go-MailHog)"}, "Received is from localhost by mailhog.example (Go-MailHog)")
|
||||
assert.Equal(t, message.Content.Headers["Return-Path"], []string{"<nobody@mailhog.example>"}, "Return-Path is <nobody@mailhog.example>")
|
||||
assert.Equal(t, message.Content.Headers["Message-ID"], []string{match[1] + "@mailhog.example"}, "Message-ID is " + match[1] + "@mailhog.example")
|
||||
|
||||
assert.Equal(t, message.Content.Body, "Hi there :)", "message has correct body")
|
||||
}
|
Loading…
Reference in a new issue