mirror of
https://gitlab.com/ric_harvey/MailHog.git
synced 2024-11-27 16:24:04 +00:00
MIME detection and add some error responses
This commit is contained in:
parent
6282d54682
commit
ef941fb69c
4 changed files with 180 additions and 5 deletions
|
@ -4,6 +4,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"regexp"
|
||||||
"labix.org/v2/mgo/bson"
|
"labix.org/v2/mgo/bson"
|
||||||
"github.com/ian-kent/MailHog/mailhog"
|
"github.com/ian-kent/MailHog/mailhog"
|
||||||
)
|
)
|
||||||
|
@ -38,6 +39,10 @@ type SMTPMessage struct {
|
||||||
Helo string
|
Helo string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MIMEBody struct {
|
||||||
|
Parts []Content
|
||||||
|
}
|
||||||
|
|
||||||
func ParseSMTPMessage(c *mailhog.Config, m *SMTPMessage) *Message {
|
func ParseSMTPMessage(c *mailhog.Config, m *SMTPMessage) *Message {
|
||||||
arr := make([]*Path, 0)
|
arr := make([]*Path, 0)
|
||||||
for _, path := range m.To {
|
for _, path := range m.To {
|
||||||
|
@ -50,12 +55,28 @@ func ParseSMTPMessage(c *mailhog.Config, m *SMTPMessage) *Message {
|
||||||
Content: ContentFromString(m.Data),
|
Content: ContentFromString(m.Data),
|
||||||
Created: time.Now(),
|
Created: time.Now(),
|
||||||
}
|
}
|
||||||
msg.Content.Headers["Message-ID"] = []string{msg.Id + "@" + c.Hostname} // FIXME
|
log.Printf("Is MIME: %t\n", msg.Content.IsMIME());
|
||||||
|
msg.Content.Headers["Message-ID"] = []string{msg.Id + "@" + c.Hostname}
|
||||||
msg.Content.Headers["Received"] = []string{"from " + m.Helo + " by " + c.Hostname + " (Go-MailHog)\r\n id " + msg.Id + "@" + c.Hostname + "; " + time.Now().Format(time.RFC1123Z)}
|
msg.Content.Headers["Received"] = []string{"from " + m.Helo + " by " + c.Hostname + " (Go-MailHog)\r\n id " + msg.Id + "@" + c.Hostname + "; " + time.Now().Format(time.RFC1123Z)}
|
||||||
msg.Content.Headers["Return-Path"] = []string{"<" + m.From + ">"}
|
msg.Content.Headers["Return-Path"] = []string{"<" + m.From + ">"}
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (content *Content) IsMIME() bool {
|
||||||
|
if strings.HasPrefix(content.Headers["Content-Type"][0], "multipart/") {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (content *Content) ParseMIMEBody() *MIMEBody {
|
||||||
|
re := regexp.MustCompile("boundary=\"([^\"]+)\"")
|
||||||
|
match := re.FindStringSubmatch(content.Body)
|
||||||
|
log.Printf("Got boundary: %s", match[1])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func PathFromString(path string) *Path {
|
func PathFromString(path string) *Path {
|
||||||
relays := make([]string, 0)
|
relays := make([]string, 0)
|
||||||
email := path
|
email := path
|
||||||
|
@ -82,7 +103,7 @@ func PathFromString(path string) *Path {
|
||||||
}
|
}
|
||||||
|
|
||||||
func ContentFromString(data string) *Content {
|
func ContentFromString(data string) *Content {
|
||||||
x := strings.Split(data, "\r\n\r\n")
|
x := strings.SplitN(data, "\r\n\r\n", 2)
|
||||||
headers, body := x[0], x[1]
|
headers, body := x[0], x[1]
|
||||||
|
|
||||||
h := make(map[string][]string, 0)
|
h := make(map[string][]string, 0)
|
||||||
|
|
|
@ -76,12 +76,14 @@ func (c *Session) Parse() {
|
||||||
if c.state == DATA {
|
if c.state == DATA {
|
||||||
c.message.Data += parts[0] + "\n"
|
c.message.Data += parts[0] + "\n"
|
||||||
if(strings.HasSuffix(c.message.Data, "\r\n.\r\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")
|
c.message.Data = strings.TrimSuffix(c.message.Data, "\r\n.\r\n")
|
||||||
id, err := c.mongo.Store(c.message)
|
id, err := c.mongo.Store(c.message)
|
||||||
c.state = MAIL
|
c.state = MAIL
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// FIXME
|
c.log("Error storing message: %s", err)
|
||||||
c.Write("500", "Error")
|
c.Write("452", "Unable to store message")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Write("250", "Ok: queued as " + id)
|
c.Write("250", "Ok: queued as " + id)
|
||||||
|
@ -96,12 +98,15 @@ func (c *Session) Parse() {
|
||||||
|
|
||||||
func (c *Session) Write(code string, text ...string) {
|
func (c *Session) Write(code string, text ...string) {
|
||||||
if len(text) == 1 {
|
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"))
|
c.conn.Write([]byte(code + " " + text[0] + "\n"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for i := 0; i < len(text) - 1; i++ {
|
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.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"))
|
c.conn.Write([]byte(code + " " + text[len(text)-1] + "\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,6 +148,7 @@ func (c *Session) Process(line string) {
|
||||||
c.Write("250", "Hello " + args, "PIPELINING")
|
c.Write("250", "Hello " + args, "PIPELINING")
|
||||||
default:
|
default:
|
||||||
c.log("Got unknown command for ESTABLISH state: '%s'", command)
|
c.log("Got unknown command for ESTABLISH state: '%s'", command)
|
||||||
|
c.Write("500", "Unrecognised command")
|
||||||
}
|
}
|
||||||
case c.state == MAIL:
|
case c.state == MAIL:
|
||||||
switch command {
|
switch command {
|
||||||
|
@ -155,6 +161,7 @@ func (c *Session) Process(line string) {
|
||||||
c.Write("250", "Sender " + match[1] + " ok")
|
c.Write("250", "Sender " + match[1] + " ok")
|
||||||
default:
|
default:
|
||||||
c.log("Got unknown command for MAIL state: '%s'", command)
|
c.log("Got unknown command for MAIL state: '%s'", command)
|
||||||
|
c.Write("500", "Unrecognised command")
|
||||||
}
|
}
|
||||||
case c.state == RCPT:
|
case c.state == RCPT:
|
||||||
switch command {
|
switch command {
|
||||||
|
@ -171,6 +178,7 @@ func (c *Session) Process(line string) {
|
||||||
c.Write("354", "End data with <CR><LF>.<CR><LF>")
|
c.Write("354", "End data with <CR><LF>.<CR><LF>")
|
||||||
default:
|
default:
|
||||||
c.log("Got unknown command for RCPT state: '%s'", command)
|
c.log("Got unknown command for RCPT state: '%s'", command)
|
||||||
|
c.Write("500", "Unrecognised command")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,10 @@ func TestBasicHappyPath(t *testing.T) {
|
||||||
// Read the response
|
// Read the response
|
||||||
n, err = conn.Read(buf)
|
n, err = conn.Read(buf)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, string(buf[0:n]), "250 Hello localhost\n")
|
assert.Equal(t, string(buf[0:n]), "250-Hello localhost\n")
|
||||||
|
n, err = conn.Read(buf)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, string(buf[0:n]), "250 PIPELINING\n")
|
||||||
|
|
||||||
// Send MAIL
|
// Send MAIL
|
||||||
_, err = conn.Write([]byte("MAIL From:<nobody@mailhog.example>\r\n"))
|
_, err = conn.Write([]byte("MAIL From:<nobody@mailhog.example>\r\n"))
|
||||||
|
|
143
mime_test.go
Normal file
143
mime_test.go
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ian-kent/MailHog/mailhog"
|
||||||
|
"github.com/ian-kent/MailHog/mailhog/storage"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"net"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FIXME requires a running instance of MailHog
|
||||||
|
|
||||||
|
func TestBasicMIMEHappyPath(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")
|
||||||
|
n, err = conn.Read(buf)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, string(buf[0:n]), "250 PIPELINING\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: multipart/alternative; boundary=\"--mailhog-test-boundary\"\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 += "Subject: Example message\r\n"
|
||||||
|
content += "MIME-Version: 1.0\r\n"
|
||||||
|
content += "\r\n"
|
||||||
|
content += "--mailhog-test-boundary\r\n"
|
||||||
|
content += "Content-Type: text/plain\r\n"
|
||||||
|
content += "\r\n"
|
||||||
|
content += "Hi there :)\r\n"
|
||||||
|
content += "--mailhog-test-boundary\r\n"
|
||||||
|
content += "Content-Type: text/html\r\n"
|
||||||
|
content += "\r\n"
|
||||||
|
content += "<html>\r\n"
|
||||||
|
content += " <head>\r\n"
|
||||||
|
content += " <title>Example message</title>\r\n"
|
||||||
|
content += " </head>\r\n"
|
||||||
|
content += " <body>\r\n"
|
||||||
|
content += " <p style=\"font-weight: bold; color: #ff0000; text-decoration: underline\">\r\n"
|
||||||
|
content += " Hi there :)\r\n"
|
||||||
|
content += " </p>\r\n"
|
||||||
|
content += " </body>\r\n"
|
||||||
|
content += "</html>\r\n"
|
||||||
|
content += "--mailhog-test-boundary\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")
|
||||||
|
|
||||||
|
s := storage.CreateMongoDB(mailhog.DefaultConfig())
|
||||||
|
message, err := s.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), 9, "message has 7 headers")
|
||||||
|
assert.Equal(t, message.Content.Headers["Content-Type"], []string{"multipart/alternative; boundary=\"--mailhog-test-boundary\""}, "Content-Type header is multipart/alternative; boundary=\"--mailhog-test-boundary\"")
|
||||||
|
assert.Equal(t, message.Content.Headers["Subject"], []string{"Example message"}, "Subject header is Example message")
|
||||||
|
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.True(t, strings.HasPrefix(message.Content.Headers["Received"][0], "from localhost by mailhog.example (Go-MailHog)\r\n id "+match[1]+"@mailhog.example; "), "Received header is correct")
|
||||||
|
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")
|
||||||
|
|
||||||
|
expected := "--mailhog-test-boundary\r\nContent-Type: text/plain\r\n\r\nHi there :)\r\n--mailhog-test-boundary\r\nContent-Type: text/html\r\n\r\n<html>\r\n <head>\r\n <title>Example message</title>\r\n </head>\r\n <body>\r\n <p style=\"font-weight: bold; color: #ff0000; text-decoration: underline\">\r\n Hi there :)\r\n </p>\r\n </body>\r\n</html>\r\n--mailhog-test-boundary"
|
||||||
|
assert.Equal(t, message.Content.Body, expected, "message has correct body")
|
||||||
|
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue