MailHog/mailhog/data/message.go

204 lines
4.6 KiB
Go
Raw Normal View History

package data
2014-04-19 22:37:11 +00:00
import (
2014-11-22 18:45:14 +00:00
"crypto/rand"
"encoding/base64"
2014-04-19 22:37:11 +00:00
"log"
"regexp"
2014-04-19 22:37:11 +00:00
"strings"
"time"
)
2014-11-22 18:45:14 +00:00
// MessageID represents the ID of an SMTP message including the hostname part
type MessageID string
// NewMessageID generates a new message ID
func NewMessageID(hostname string) (MessageID, error) {
size := 32
rb := make([]byte, size)
_, err := rand.Read(rb)
if err != nil {
return MessageID(""), err
}
rs := base64.URLEncoding.EncodeToString(rb)
return MessageID(rs + "@" + hostname), nil
}
// Messages represents an array of Messages
// - TODO is this even required?
2014-04-20 15:05:50 +00:00
type Messages []Message
2014-11-22 18:45:14 +00:00
// Message represents a parsed SMTP message
2014-04-19 22:37:11 +00:00
type Message struct {
2014-11-22 18:45:14 +00:00
ID MessageID
From *Path
To []*Path
2014-04-19 22:37:11 +00:00
Content *Content
Created time.Time
MIME *MIMEBody // FIXME refactor to use Content.MIME
2014-04-19 22:37:11 +00:00
}
2014-11-22 18:45:14 +00:00
// Path represents an SMTP forward-path or return-path
2014-04-19 22:37:11 +00:00
type Path struct {
Relays []string
2014-04-19 22:37:11 +00:00
Mailbox string
Domain string
Params string
2014-04-19 22:37:11 +00:00
}
2014-11-22 18:45:14 +00:00
// Content represents the body content of an SMTP message
2014-04-19 22:37:11 +00:00
type Content struct {
Headers map[string][]string
Body string
Size int
MIME *MIMEBody
2014-04-19 22:37:11 +00:00
}
2014-11-22 18:45:14 +00:00
// SMTPMessage represents a raw SMTP message
2014-04-19 22:37:11 +00:00
type SMTPMessage struct {
From string
To []string
2014-04-19 22:37:11 +00:00
Data string
Helo string
}
2014-11-22 18:45:14 +00:00
// MIMEBody represents a collection of MIME parts
type MIMEBody struct {
2014-04-21 21:32:34 +00:00
Parts []*Content
}
2014-11-22 18:45:14 +00:00
// Parse converts a raw SMTP message to a parsed MIME message
func (m *SMTPMessage) Parse(hostname string) *Message {
var arr []*Path
2014-04-19 22:37:11 +00:00
for _, path := range m.To {
arr = append(arr, PathFromString(path))
}
2014-11-22 18:45:14 +00:00
id, _ := NewMessageID(hostname)
2014-04-19 22:37:11 +00:00
msg := &Message{
2014-11-22 18:45:14 +00:00
ID: id,
From: PathFromString(m.From),
To: arr,
2014-04-19 22:37:11 +00:00
Content: ContentFromString(m.Data),
Created: time.Now(),
}
2014-04-21 21:32:34 +00:00
if msg.Content.IsMIME() {
log.Printf("Parsing MIME body")
msg.MIME = msg.Content.ParseMIMEBody()
}
2014-11-22 18:45:14 +00:00
msg.Content.Headers["Message-ID"] = []string{string(id)}
msg.Content.Headers["Received"] = []string{"from " + m.Helo + " by " + hostname + " (Go-MailHog)\r\n id " + string(id) + "; " + time.Now().Format(time.RFC1123Z)}
2014-04-19 22:37:11 +00:00
msg.Content.Headers["Return-Path"] = []string{"<" + m.From + ">"}
return msg
}
2014-11-22 18:45:14 +00:00
// IsMIME detects a valid MIME header
func (content *Content) IsMIME() bool {
2014-04-27 23:21:57 +00:00
header, ok := content.Headers["Content-Type"]
if !ok {
return false
}
2014-04-27 23:21:57 +00:00
return strings.HasPrefix(header[0], "multipart/")
}
2014-11-22 18:45:14 +00:00
// ParseMIMEBody parses SMTP message content into multiple MIME parts
func (content *Content) ParseMIMEBody() *MIMEBody {
2014-11-22 18:45:14 +00:00
var parts []*Content
2014-10-29 15:13:29 +00:00
if hdr, ok := content.Headers["Content-Type"]; ok {
if len(hdr) > 0 {
re := regexp.MustCompile("boundary=\"([^\"]+)\"")
match := re.FindStringSubmatch(hdr[0])
if len(match) < 2 {
2014-11-22 18:45:14 +00:00
log.Printf("Boundary not found: %s", hdr[0])
2014-10-29 15:13:29 +00:00
}
log.Printf("Got boundary: %s", match[1])
p := strings.Split(content.Body, "--"+match[1])
for _, s := range p {
if len(s) > 0 {
part := ContentFromString(strings.Trim(s, "\r\n"))
if part.IsMIME() {
log.Printf("Parsing inner MIME body")
part.MIME = part.ParseMIMEBody()
}
parts = append(parts, part)
}
2014-04-27 23:21:57 +00:00
}
2014-04-21 21:32:34 +00:00
}
}
return &MIMEBody{
Parts: parts,
}
}
2014-11-22 18:45:14 +00:00
// PathFromString parses a forward-path or reverse-path into its parts
2014-04-19 22:37:11 +00:00
func PathFromString(path string) *Path {
2014-11-22 18:45:14 +00:00
var relays []string
2014-04-19 22:37:11 +00:00
email := path
if strings.Contains(path, ":") {
2014-04-19 22:37:11 +00:00
x := strings.SplitN(path, ":", 2)
r, e := x[0], x[1]
email = e
relays = strings.Split(r, ",")
}
mailbox, domain := "", ""
if strings.Contains(email, "@") {
2014-04-19 22:37:11 +00:00
x := strings.SplitN(email, "@", 2)
mailbox, domain = x[0], x[1]
} else {
mailbox = email
}
return &Path{
Relays: relays,
2014-04-19 22:37:11 +00:00
Mailbox: mailbox,
Domain: domain,
Params: "", // FIXME?
2014-04-19 22:37:11 +00:00
}
}
2014-11-22 18:45:14 +00:00
// ContentFromString parses SMTP content into separate headers and body
2014-04-19 22:37:11 +00:00
func ContentFromString(data string) *Content {
2014-04-21 21:32:34 +00:00
log.Printf("Parsing Content from string: '%s'", data)
x := strings.SplitN(data, "\r\n\r\n", 2)
2014-04-19 22:37:11 +00:00
h := make(map[string][]string, 0)
2014-04-26 11:16:57 +00:00
if len(x) == 2 {
headers, body := x[0], x[1]
hdrs := strings.Split(headers, "\r\n")
var lastHdr = ""
for _, hdr := range hdrs {
if lastHdr != "" && (strings.HasPrefix(hdr, " ") || strings.HasPrefix(hdr, "\t")) {
2014-04-26 11:16:57 +00:00
h[lastHdr][len(h[lastHdr])-1] = h[lastHdr][len(h[lastHdr])-1] + hdr
} else if strings.Contains(hdr, ": ") {
y := strings.SplitN(hdr, ": ", 2)
key, value := y[0], y[1]
// TODO multiple header fields
h[key] = []string{value}
lastHdr = key
} else {
log.Printf("Found invalid header: '%s'", hdr)
}
}
return &Content{
Size: len(data),
2014-04-26 11:16:57 +00:00
Headers: h,
Body: body,
2014-04-26 11:16:57 +00:00
}
2014-11-22 18:45:14 +00:00
}
return &Content{
Size: len(data),
Headers: h,
Body: x[0],
2014-04-19 22:37:11 +00:00
}
}