Refactored to closely follow RFC5322
diff --git a/README.md b/README.md
index 2a32a50..74a7bbe 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,8 @@
# Parsemail - simple email parsing Go library
-Simple usage:
+This library allows for parsing an email message into a more convenient form than the `net/mail` provides. Where the `net/mail` just gives you a map of header fields and a `io.Reader` of its body, Parsemail allows access to all the standard header fields set in [RFC5322](https://tools.ietf.org/html/rfc5322), html/text body as well as attachements/embedded as binary streams with metadata.
+
+## Simple usage
```go
var reader io.Reader // this reads an email message
@@ -9,21 +11,19 @@
// handle error
}
-fmt.Println(email.Subject()) // parsed from header on demand
-fmt.Println(email.Cc()) // parsed from header on demand
-fmt.Println(email.HTMLBody) // this is not a method, parsed beforehand
+fmt.Println(email.Subject)
+fmt.Println(email.From)
+fmt.Println(email.To)
+fmt.Println(email.HTMLBody)
```
## This library is WIP.
It is missing some tests, and needs more work. Use at your own discretion.
-
-
## TODO
- CI
- Readme with use cases
- More tests for 100% coverage
-- email address type => getEmail, getName
-- quoted§ text?
\ No newline at end of file
+- quoted text?
\ No newline at end of file
diff --git a/parsemail.go b/parsemail.go
index f1a512e..a52dd45 100644
--- a/parsemail.go
+++ b/parsemail.go
@@ -14,53 +14,268 @@
"bytes"
)
-func Parse(r io.Reader) (Email, error) {
- email := Email{}
+const content_type_multipart_mixed = "multipart/mixed"
+const content_type_multipart_alternative = "multipart/alternative"
+const content_type_multipart_related = "multipart/related"
+const content_type_text_html = "text/html"
+const content_type_text_plain = "text/plain"
+func Parse(r io.Reader) (email Email, err error) {
msg, err := mail.ReadMessage(r);
if err != nil {
- return email, err
+ return
}
- var body []byte
- _,err = msg.Body.Read(body);
+ email, err = createEmailFromHeader(msg.Header)
if err != nil {
- return email, err
+ return
}
- email.Header, err = decodeHeaderMime(msg.Header)
+ contentType, params, err := parseContentType(msg.Header.Get("Content-Type"))
if err != nil {
- return email, err
+ return
}
- mediaType, params, err := mime.ParseMediaType(msg.Header.Get("Content-Type"))
- if err != nil {
- return email, err
- }
-
- if mediaType == "" {
- return email, errors.New("No top level mime type specified")
- } else if strings.HasPrefix(mediaType, "multipart/mixed") {
+ switch contentType {
+ case content_type_multipart_mixed:
email.TextBody, email.HTMLBody, email.Attachments, email.EmbeddedFiles, err = parseMultipartMixed(msg.Body, params["boundary"])
- if err != nil {
- return email, err
- }
- } else if strings.HasPrefix(mediaType, "multipart/alternative") {
+ case content_type_multipart_alternative:
email.TextBody, email.HTMLBody, email.EmbeddedFiles, err = parseMultipartAlternative(msg.Body, params["boundary"])
- if err != nil {
- return email, err
- }
- } else if strings.HasPrefix(mediaType, "text/plain") {
+ case content_type_text_plain:
message, _ := ioutil.ReadAll(msg.Body)
email.TextBody = strings.TrimSuffix(string(message[:]), "\n")
- } else if strings.HasPrefix(mediaType, "text/html") {
+ case content_type_text_html:
message, _ := ioutil.ReadAll(msg.Body)
email.HTMLBody = strings.TrimSuffix(string(message[:]), "\n")
- } else {
- return email, errors.New(fmt.Sprintf("Unknown top level mime type: %s", mediaType))
+ case "":
+ err = errors.New("No top level mime type specified")
+ default:
+ err = errors.New(fmt.Sprintf("Unknown top level mime type: %s", contentType))
}
- return email, nil
+ return
+}
+
+func createEmailFromHeader(header mail.Header) (email Email, err error) {
+ email.Subject = header.Get("Subject")
+
+ email.From, err = parseAddressList(header.Get("From"))
+ if err != nil {
+ return
+ }
+
+ email.Sender, err = parseAddress(header.Get("Sender"))
+ if err != nil {
+ return
+ }
+
+ email.ReplyTo, err = parseAddressList(header.Get("Reply-To"))
+ if err != nil {
+ return
+ }
+
+ email.To, err = parseAddressList(header.Get("To"))
+ if err != nil {
+ return
+ }
+
+ email.Cc, err = parseAddressList(header.Get("Cc"))
+ if err != nil {
+ return
+ }
+
+ email.Bcc, err = parseAddressList(header.Get("Bcc"))
+ if err != nil {
+ return
+ }
+
+ email.Date, err = parseTime(header.Get("Date"))
+ if err != nil {
+ return
+ }
+
+ email.ResentFrom, err = parseAddressList(header.Get("Resent-From"))
+ if err != nil {
+ return
+ }
+
+ email.ResentSender, err = parseAddress(header.Get("Resent-Sender"))
+ if err != nil {
+ return
+ }
+
+ email.ResentTo, err = parseAddressList(header.Get("Resent-To"))
+ if err != nil {
+ return
+ }
+
+ email.ResentCc, err = parseAddressList(header.Get("Resent-Cc"))
+ if err != nil {
+ return
+ }
+
+ email.ResentBcc, err = parseAddressList(header.Get("Resent-Bcc"))
+ if err != nil {
+ return
+ }
+
+ if header.Get("Resent-Date") == "" {
+ email.ResentDate = time.Time{}
+ } else {
+ email.ResentDate, err = parseTime(header.Get("Resent-Date"))
+ if err != nil {
+ return
+ }
+ }
+
+ email.ResentMessageID = parseMessageId(header.Get("Resent-Message-ID"))
+ email.MessageID = parseMessageId(header.Get("Message-ID"))
+ email.InReplyTo = parseMessageIdList(header.Get("In-Reply-To"))
+ email.References = parseMessageIdList(header.Get("References"))
+
+ //decode whole header for easier access to extra fields
+ //todo: should we decode? aren't only standard fields mime encoded?
+ email.Header, err = decodeHeaderMime(header)
+ if err != nil {
+ return
+ }
+
+ return
+}
+
+func parseContentType(contentTypeHeader string) (contentType string, params map[string]string, err error) {
+ if contentTypeHeader == "" {
+ contentType = content_type_text_plain
+ return
+ }
+
+ return mime.ParseMediaType(contentTypeHeader)
+}
+
+func parseAddress(s string) (*mail.Address, error) {
+ if strings.Trim(s, " \n") != "" {
+ return mail.ParseAddress(s)
+ }
+
+ return nil, nil
+}
+
+func parseAddressList(s string) ([]*mail.Address, error) {
+ if strings.Trim(s, " \n") != "" {
+ return mail.ParseAddressList(s)
+ }
+
+ return []*mail.Address{}, nil
+}
+
+func parseTime(s string) (time.Time, error) {
+ t, err := time.Parse(time.RFC1123Z, s)
+ if err == nil {
+ return t, err
+ }
+
+ return time.Parse("Mon, 2 Jan 2006 15:04:05 -0700", s)
+}
+
+func parseMessageId(s string) string {
+ return strings.Trim(s, "<> ")
+}
+
+func parseMessageIdList(s string) (result []string) {
+ for _, p := range(strings.Split(s, " ")) {
+ if strings.Trim(p, " \n") != "" {
+ result = append(result, parseMessageId(p))
+ }
+ }
+
+ return
+}
+
+func parseMultipartAlternative(msg io.Reader, boundary string) (textBody, htmlBody string, embeddedFiles []EmbeddedFile, err error) {
+ pmr := multipart.NewReader(msg, boundary)
+ for {
+ part, err := pmr.NextPart()
+
+ if err == io.EOF {
+ break
+ } else if err != nil {
+ return textBody, htmlBody, embeddedFiles, err
+ }
+
+ contentType, params, err := mime.ParseMediaType(part.Header.Get("Content-Type"))
+
+ switch contentType {
+ case content_type_text_plain:
+ ppContent, err := ioutil.ReadAll(part)
+ if err != nil {
+ return textBody, htmlBody, embeddedFiles, err
+ }
+
+ textBody += strings.TrimSuffix(string(ppContent[:]), "\n")
+ case content_type_text_html:
+ ppContent, err := ioutil.ReadAll(part)
+ if err != nil {
+ return textBody, htmlBody, embeddedFiles, err
+ }
+
+ htmlBody += strings.TrimSuffix(string(ppContent[:]), "\n")
+ case content_type_multipart_related:
+ var tb, hb string
+ var ef []EmbeddedFile
+ tb, hb, ef, err = parseMultipartAlternative(part, params["boundary"])
+ htmlBody += hb
+ textBody += tb
+ embeddedFiles = append(embeddedFiles, ef...)
+ default:
+ if isEmbeddedFile(part) {
+ ef, err := decodeEmbeddedFile(part)
+ if err != nil {
+ return textBody, htmlBody, embeddedFiles, err
+ }
+
+ embeddedFiles = append(embeddedFiles, ef)
+ } else {
+ return textBody, htmlBody, embeddedFiles, errors.New(fmt.Sprintf("Can't process multipart/alternative inner mime type: %s", contentType))
+ }
+ }
+ }
+
+ return textBody, htmlBody, embeddedFiles, err
+}
+
+func parseMultipartMixed(msg io.Reader, boundary string) (textBody, htmlBody string, attachments []Attachment, embeddedFiles []EmbeddedFile, err error) {
+ mr := multipart.NewReader(msg, boundary)
+ for {
+ part, err := mr.NextPart()
+ if err == io.EOF {
+ break
+ } else if err != nil {
+ return textBody, htmlBody, attachments, embeddedFiles, err
+ }
+
+ contentType, params, err := mime.ParseMediaType(part.Header.Get("Content-Type"))
+ if err != nil {
+ return textBody, htmlBody, attachments, embeddedFiles, err
+ }
+
+ if contentType == content_type_multipart_alternative {
+ textBody, htmlBody, embeddedFiles, err = parseMultipartAlternative(part, params["boundary"])
+ if err != nil {
+ return textBody, htmlBody, attachments, embeddedFiles, err
+ }
+ } else if isAttachment(part) {
+ at, err := decodeAttachment(part)
+ if err != nil {
+ return textBody, htmlBody, attachments, embeddedFiles, err
+ }
+
+ attachments = append(attachments, at)
+ } else {
+ return textBody, htmlBody, attachments, embeddedFiles, errors.New(fmt.Sprintf("Unknown multipart/mixed nested mime type: %s", contentType))
+ }
+ }
+
+ return textBody, htmlBody, attachments, embeddedFiles, err
}
func decodeMimeSentence(s string) (string, error) {
@@ -84,105 +299,6 @@
return strings.Join(result, ""), nil
}
-func parseMultipartAlternative(msg io.Reader, boundary string) (textBody, htmlBody string, embeddedFiles []EmbeddedFile, err error) {
- pmr := multipart.NewReader(msg, boundary)
- for {
-
- pp, err := pmr.NextPart()
-
- if err == io.EOF {
- break
- }
- if err != nil {
- return textBody, htmlBody, embeddedFiles, err
- }
-
- ppMediaType, ppParams, err := mime.ParseMediaType(pp.Header.Get("Content-Type"))
-
- if ppMediaType == "text/plain" {
- ppContent, err := ioutil.ReadAll(pp)
- if err != nil {
- return textBody, htmlBody, embeddedFiles, err
- }
-
- textBody += strings.TrimSuffix(string(ppContent[:]), "\n")
- } else if ppMediaType == "text/html" {
- ppContent, err := ioutil.ReadAll(pp)
- if err != nil {
- return textBody, htmlBody, embeddedFiles, err
- }
-
- htmlBody += strings.TrimSuffix(string(ppContent[:]), "\n")
- } else if ppMediaType == "multipart/related" {
- var tb, hb string
- var ef []EmbeddedFile
- tb, hb, ef, err = parseMultipartAlternative(pp, ppParams["boundary"])
- htmlBody += hb
- textBody += tb
- embeddedFiles = append(embeddedFiles, ef...)
- } else if pp.Header.Get("Content-Transfer-Encoding") != "" {
- reference, err := decodeMimeSentence(pp.Header.Get("Content-Id"));
- if err != nil {
- return textBody, htmlBody, embeddedFiles, err
- }
- reference = strings.Trim(reference, "<>")
-
- decoded, err := decodePartData(pp)
- if err != nil {
- return textBody, htmlBody, embeddedFiles, err
- }
-
- embeddedFiles = append(embeddedFiles, EmbeddedFile{reference, decoded})
- } else {
- return textBody, htmlBody, embeddedFiles, errors.New(fmt.Sprintf("Can't process multipart/alternative inner mime type: %s", ppMediaType))
- }
- }
-
- return textBody, htmlBody, embeddedFiles, err
-}
-
-func parseMultipartMixed(msg io.Reader, boundary string) (textBody, htmlBody string, attachments []Attachment, embeddedFiles []EmbeddedFile, err error) {
- mr := multipart.NewReader(msg, boundary)
- for {
- p, err := mr.NextPart()
- if err == io.EOF {
- break
- }
- if err != nil {
- return textBody, htmlBody, attachments, embeddedFiles, err
- }
-
- pMediaType, pParams, err := mime.ParseMediaType(p.Header.Get("Content-Type"))
- if err != nil {
- return textBody, htmlBody, attachments, embeddedFiles, err
- }
-
- if strings.HasPrefix(pMediaType, "multipart/alternative") {
- textBody, htmlBody, embeddedFiles, err = parseMultipartAlternative(p, pParams["boundary"])
- if err != nil {
- return textBody, htmlBody, attachments, embeddedFiles, err
- }
- } else if p.FileName() != "" {
-
- filename, err := decodeMimeSentence(p.FileName());
- if err != nil {
- return textBody, htmlBody, attachments, embeddedFiles, err
- }
-
- decoded, err := decodePartData(p)
- if err != nil {
- return textBody, htmlBody, attachments, embeddedFiles, err
- }
-
- attachments = append(attachments, Attachment{filename, decoded})
- } else {
- return textBody, htmlBody, attachments, embeddedFiles, errors.New(fmt.Sprintf("Unknown multipart/mixed nested mime type: %s", pMediaType))
- }
- }
-
- return textBody, htmlBody, attachments, embeddedFiles, err
-}
-
func decodeHeaderMime(header mail.Header) (mail.Header, error) {
parsedHeader := map[string][]string{}
@@ -219,130 +335,92 @@
}
}
+func isEmbeddedFile(part *multipart.Part) bool {
+ return part.Header.Get("Content-Transfer-Encoding") != ""
+}
+
+func decodeEmbeddedFile(part *multipart.Part) (ef EmbeddedFile, err error) {
+ cid, err := decodeMimeSentence(part.Header.Get("Content-Id"));
+ if err != nil {
+ return
+ }
+
+ decoded, err := decodePartData(part)
+ if err != nil {
+ return
+ }
+
+ ef.CID = strings.Trim(cid, "<>")
+ ef.Data = decoded
+ ef.ContentType = part.Header.Get("Content-Type")
+
+ return
+}
+
+func isAttachment(part *multipart.Part) bool {
+ return part.FileName() != ""
+}
+
+func decodeAttachment(part *multipart.Part) (at Attachment, err error) {
+ filename, err := decodeMimeSentence(part.FileName());
+ if err != nil {
+ return
+ }
+
+ decoded, err := decodePartData(part)
+ if err != nil {
+ return
+ }
+
+ at.Filename = filename
+ at.Data = decoded
+ at.ContentType = strings.Split(part.Header.Get("Content-Type"), ";")[0]
+
+ return
+}
+
type Attachment struct {
Filename string
+ ContentType string
Data io.Reader
}
type EmbeddedFile struct {
CID string
+ ContentType string
Data io.Reader
}
+type MessageID string
+
type Email struct {
Header mail.Header
+
+ Subject string
+ Sender *mail.Address
+ From []*mail.Address
+ ReplyTo []*mail.Address
+ To []*mail.Address
+ Cc []*mail.Address
+ Bcc []*mail.Address
+ Date time.Time
+ MessageID string
+ InReplyTo []string
+ References []string
+
+ ResentFrom []*mail.Address
+ ResentSender *mail.Address
+ ResentTo []*mail.Address
+ ResentDate time.Time
+ ResentCc []*mail.Address
+ ResentBcc []*mail.Address
+ ResentMessageID string
+
+ Received string
+
HTMLBody string
TextBody string
+
Attachments []Attachment
EmbeddedFiles []EmbeddedFile
-}
-
-func (e *Email) Subject() string {
- return e.Header.Get("Subject")
-}
-
-func (e *Email) Sender() string {
- return e.Header.Get("Sender")
-}
-
-func (e *Email) From() []string {
- result := []string{}
-
- for _, v := range(strings.Split(e.Header.Get("From"), ",")) {
- t := strings.Trim(v, " ")
- if t != "" {
- result = append(result, t)
- }
- }
-
- return result
-}
-
-func (e *Email) To() []string {
- result := []string{}
-
- for _, v := range(strings.Split(e.Header.Get("To"), ",")) {
- t := strings.Trim(v, " ")
- if t != "" {
- result = append(result, t)
- }
- }
-
- return result
-}
-
-func (e *Email) Cc() []string {
- result := []string{}
-
- for _, v := range(strings.Split(e.Header.Get("Cc"), ",")) {
- t := strings.Trim(v, " ")
- if t != "" {
- result = append(result, t)
- }
- }
-
- return result
-}
-
-func (e *Email) Bcc() []string {
- result := []string{}
-
- for _, v := range(strings.Split(e.Header.Get("Bcc"), ",")) {
- t := strings.Trim(v, " ")
- if t != "" {
- result = append(result, t)
- }
- }
-
- return result
-}
-
-func (e *Email) ReplyTo() []string {
- result := []string{}
-
- for _, v := range(strings.Split(e.Header.Get("Reply-To"), ",")) {
- t := strings.Trim(v, " ")
- if t != "" {
- result = append(result, t)
- }
- }
-
- return result
-}
-
-func (e *Email) Date() (time.Time, error) {
- t, err := time.Parse(time.RFC1123Z, e.Header.Get("Date"))
- if err == nil {
- return t, err
- }
-
- return time.Parse("Mon, 2 Jan 2006 15:04:05 -0700", e.Header.Get("Date"))
-}
-
-func (e *Email) MessageID() string {
- return strings.Trim(e.Header.Get("Message-ID"), "<>")
-}
-
-func (e *Email) InReplyTo() []string {
- result := []string{}
-
- for _, v := range(strings.Split(e.Header.Get("In-Reply-To"), " ")) {
- if v != "" {
- result = append(result, strings.Trim(v, "<> "))
- }
- }
-
- return result
-}
-
-func (e *Email) References() []string {
- result := []string{}
-
- for _, v := range(strings.Split(e.Header.Get("References"), " ")) {
- if v != "" {
- result = append(result, strings.Trim(v, "<> "))
- }
- }
-
- return result
-}
+}
\ No newline at end of file
diff --git a/parsemail_test.go b/parsemail_test.go
index 402ff11..6e2153c 100644
--- a/parsemail_test.go
+++ b/parsemail_test.go
@@ -11,47 +11,154 @@
)
func TestParseEmail(t *testing.T) {
- var testData = []struct{
+ var testData = map[int]struct{
mailData string
subject string
- from []string
- sender string
- to []string
- replyTo []string
- cc []string
- bcc []string
+ date time.Time
+ from []mail.Address
+ sender mail.Address
+ to []mail.Address
+ replyTo []mail.Address
+ cc []mail.Address
+ bcc []mail.Address
messageID string
+ resentDate time.Time
+ resentFrom []mail.Address
+ resentSender mail.Address
+ resentTo []mail.Address
+ resentReplyTo []mail.Address
+ resentCc []mail.Address
+ resentBcc []mail.Address
+ resentMessageID string
inReplyTo []string
references []string
- date time.Time
htmlBody string
textBody string
attachments []attachmentData
embeddedFiles []embeddedFileData
headerCheck func (mail.Header, *testing.T)
}{
- {
+ 1: {
+ mailData: RFC5322_Example_A11,
+ subject: "Saying Hello",
+ from: []mail.Address{
+ {"John Doe", "jdoe@machine.example"},
+ },
+ to: []mail.Address{
+ {"Mary Smith", "mary@example.net"},
+ },
+ sender: mail.Address{"Michael Jones", "mjones@machine.example"},
+ messageID: "1234@local.machine.example",
+ date: parseDate("Fri, 21 Nov 1997 09:55:06 -0600"),
+ textBody: `This is a message just to say hello.
+So, "Hello".`,
+ },
+ 2: {
+ mailData: RFC5322_Example_A12,
+ from: []mail.Address{
+ {"Joe Q. Public", "john.q.public@example.com"},
+ },
+ to: []mail.Address{
+ {"Mary Smith", "mary@x.test"},
+ {"", "jdoe@example.org"},
+ {"Who?", "one@y.test"},
+ },
+ cc: []mail.Address{
+ {"", "boss@nil.test"},
+ {"Giant; \"Big\" Box", "sysservices@example.net"},
+ },
+ messageID: "5678.21-Nov-1997@example.com",
+ date: parseDate("Tue, 01 Jul 2003 10:52:37 +0200"),
+ textBody: `Hi everyone.`,
+ },
+ 3: {
+ mailData: RFC5322_Example_A2a,
+ subject: "Re: Saying Hello",
+ from: []mail.Address{
+ {"Mary Smith", "mary@example.net"},
+ },
+ replyTo: []mail.Address{
+ {"Mary Smith: Personal Account", "smith@home.example"},
+ },
+ to: []mail.Address{
+ {"John Doe", "jdoe@machine.example"},
+ },
+ messageID: "3456@example.net",
+ inReplyTo: []string{"1234@local.machine.example"},
+ references: []string{"1234@local.machine.example"},
+ date: parseDate("Fri, 21 Nov 1997 10:01:10 -0600"),
+ textBody: `This is a reply to your hello.`,
+ },
+ 4: {
+ mailData: RFC5322_Example_A2b,
+ subject: "Re: Saying Hello",
+ from: []mail.Address{
+ {"John Doe", "jdoe@machine.example"},
+ },
+ to: []mail.Address{
+ {"Mary Smith: Personal Account", "smith@home.example"},
+ },
+ messageID: "abcd.1234@local.machine.test",
+ inReplyTo: []string{"3456@example.net"},
+ references: []string{"1234@local.machine.example", "3456@example.net"},
+ date: parseDate("Fri, 21 Nov 1997 11:00:00 -0600"),
+ textBody: `This is a reply to your reply.`,
+ },
+ 5: {
+ mailData: RFC5322_Example_A3,
+ subject: "Saying Hello",
+ from: []mail.Address{
+ {"John Doe", "jdoe@machine.example"},
+ },
+ to: []mail.Address{
+ {"Mary Smith", "mary@example.net"},
+ },
+ messageID: "1234@local.machine.example",
+ date: parseDate("Fri, 21 Nov 1997 09:55:06 -0600"),
+ resentFrom: []mail.Address{
+ {"Mary Smith", "mary@example.net"},
+ },
+ resentTo: []mail.Address{
+ {"Jane Brown", "j-brown@other.example"},
+ },
+ resentMessageID: "78910@example.net",
+ resentDate: parseDate("Mon, 24 Nov 1997 14:22:01 -0800"),
+ textBody: `This is a message just to say hello.
+So, "Hello".`,
+ },
+ 6: {
mailData: Data1,
subject: "Test Subject 1",
- from: []string{"Peter Paholík <peter.paholik@gmail.com>"},
- to: []string{"dusan@kasan.sk"},
+ from: []mail.Address{
+ {"Peter Paholík", "peter.paholik@gmail.com"},
+ },
+ to: []mail.Address{
+ {"", "dusan@kasan.sk"},
+ },
messageID: "CACtgX4kNXE7T5XKSKeH_zEcfUUmf2vXVASxYjaaK9cCn-3zb_g@mail.gmail.com",
date: parseDate("Fri, 07 Apr 2017 09:17:26 +0200"),
htmlBody: "<div dir=\"ltr\"><br></div>",
attachments: []attachmentData{
{
filename: "Peter Paholík 1 4 2017 2017-04-07.pdf",
+ contentType: "application/pdf",
base64data: "JVBERi0xLjQNCiW1tbW1DQoxIDAgb2JqDQo8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFIvTGFuZyhlbi1VUykgL1N0cnVjdFRyZWVSb290IDY3IDAgUi9NYXJrSW5mbzw8L01hcmtlZCB0cnVlPj4vT3V0cHV0SW50ZW50c1s8PC9UeXBlL091dHB1dEludGVudC9TL0dUU19QREZBMS9PdXRwdXRDb25kZXYgMzk1MzYyDQo+Pg0Kc3RhcnR4cmVmDQo0MTk4ODUNCiUlRU9GDQo=",
},
},
},
- {
+ 7: {
mailData: Data2,
subject: "Re: Test Subject 2",
- from: []string{"Sender Man <sender@domain.com>"},
- to: []string{"info@receiver.com"},
- cc: []string{"Cc Man <ccman@gmail.com>"},
+ from: []mail.Address{
+ {"Sender Man", "sender@domain.com"},
+ },
+ to: []mail.Address{
+ {"", "info@receiver.com"},
+ },
+ cc: []mail.Address{
+ {"Cc Man", "ccman@gmail.com"},
+ },
messageID: "0e9a21b4-01dc-e5c1-dcd6-58ce5aa61f4f@receiver.com",
inReplyTo: []string{"9ff38d03-c4ab-89b7-9328-e99d5e24e3ba@receiver.eu"},
references: []string{"2f6b7595-c01e-46e5-42bc-f263e1c4282d@receiver.com", "9ff38d03-c4ab-89b7-9328-e99d5e24e3ba@domain.com"},
@@ -65,75 +172,119 @@
embeddedFiles: []embeddedFileData{
{
cid: "part2.9599C449.04E5EC81@develhell.com",
+ contentType: "image/png",
base64data: "iVBORw0KGgoAAAANSUhEUgAAAQEAAAAYCAIAAAB1IN9NAAAACXBIWXMAAAsTAAALEwEAmpwYYKUKF+Os3baUndC0pDnwNAmLy1SUr2Gw0luxQuV/AwC6cEhVV5VRrwAAAABJRU5ErkJggg==",
},
},
},
}
- for _, td := range testData {
+ for index, td := range testData {
e, err := parsemail.Parse(strings.NewReader(td.mailData))
if err != nil {
t.Error(err)
}
- if td.subject != e.Subject() {
- t.Errorf("Wrong subject. Expected: %s, Got: %s", td.subject, e.Subject())
+ if td.subject != e.Subject {
+ t.Errorf("[Test Case %v] Wrong subject. Expected: %s, Got: %s", index, td.subject, e.Subject)
}
- if td.sender != e.Sender() {
- t.Errorf("Wrong sender. Expected: %s, Got: %s", td.sender, e.Sender())
+ if td.messageID != e.MessageID {
+ t.Errorf("[Test Case %v] Wrong messageID. Expected: '%s', Got: '%s'", index, td.messageID, e.MessageID)
}
- if !assertSliceEq(td.from, e.From()) {
- t.Errorf("Wrong from. Expected: %s, Got: %s", td.from, e.From())
+ if !td.date.Equal(e.Date) {
+ t.Errorf("[Test Case %v] Wrong date. Expected: %v, Got: %v", index, td.date, e.Date)
}
- if !assertSliceEq(td.inReplyTo, e.InReplyTo()) {
- t.Errorf("Wrong in reply to. Expected: %s, Got: %s", td.inReplyTo, e.InReplyTo())
+ d := dereferenceAddressList(e.From)
+ if !assertAddressListEq(td.from, d) {
+ t.Errorf("[Test Case %v] Wrong from. Expected: %s, Got: %s", index, td.from, d)
}
- if !assertSliceEq(td.references, e.References()) {
- t.Errorf("Wrong references. Expected: %s, Got: %s", td.references, e.References())
+ var sender mail.Address
+ if e.Sender != nil {
+ sender = *e.Sender
+ }
+ if td.sender != sender {
+ t.Errorf("[Test Case %v] Wrong sender. Expected: %s, Got: %s", index, td.sender, sender)
}
- if !assertSliceEq(td.to, e.To()) {
- t.Errorf("Wrong to. Expected: %s, Got: %s", td.to, e.To())
+ d = dereferenceAddressList(e.To)
+ if !assertAddressListEq(td.to, d) {
+ t.Errorf("[Test Case %v] Wrong to. Expected: %s, Got: %s", index, td.to, d)
}
- if !assertSliceEq(td.replyTo, e.ReplyTo()) {
- t.Errorf("Wrong reply to. Expected: %s, Got: %s", td.replyTo, e.ReplyTo())
+ d = dereferenceAddressList(e.Cc)
+ if !assertAddressListEq(td.cc, d) {
+ t.Errorf("[Test Case %v] Wrong cc. Expected: %s, Got: %s", index, td.cc, d)
}
- if !assertSliceEq(td.cc, e.Cc()) {
- t.Errorf("Wrong cc. Expected: %s, Got: %s", td.cc, e.Cc())
+ d = dereferenceAddressList(e.Bcc)
+ if !assertAddressListEq(td.bcc, d) {
+ t.Errorf("[Test Case %v] Wrong bcc. Expected: %s, Got: %s", index, td.bcc, d)
}
- if !assertSliceEq(td.bcc, e.Bcc()) {
- t.Errorf("Wrong cc. Expected: %s, Got: %s", td.cc, e.Cc())
+ if td.resentMessageID != e.ResentMessageID {
+ t.Errorf("[Test Case %v] Wrong resent messageID. Expected: '%s', Got: '%s'", index, td.resentMessageID, e.ResentMessageID)
}
- date, err := e.Date()
- if err != nil {
- t.Error(err)
- } else if td.date != date {
- t.Errorf("Wrong date. Expected: %v, Got: %v", td.date, date)
+ if !td.resentDate.Equal(e.ResentDate) && !td.resentDate.IsZero() && !e.ResentDate.IsZero() {
+ t.Errorf("[Test Case %v] Wrong resent date. Expected: %v, Got: %v", index, td.resentDate, e.ResentDate)
+ }
+
+ d = dereferenceAddressList(e.ResentFrom)
+ if !assertAddressListEq(td.resentFrom, d) {
+ t.Errorf("[Test Case %v] Wrong resent from. Expected: %s, Got: %s", index, td.resentFrom, d)
+ }
+
+ var resentSender mail.Address
+ if e.ResentSender != nil {
+ resentSender = *e.ResentSender
+ }
+ if td.resentSender != resentSender {
+ t.Errorf("[Test Case %v] Wrong resent sender. Expected: %s, Got: %s", index, td.resentSender, resentSender)
+ }
+
+ d = dereferenceAddressList(e.ResentTo)
+ if !assertAddressListEq(td.resentTo, d) {
+ t.Errorf("[Test Case %v] Wrong resent to. Expected: %s, Got: %s", index, td.resentTo, d)
+ }
+
+ d = dereferenceAddressList(e.ResentCc)
+ if !assertAddressListEq(td.resentCc, d) {
+ t.Errorf("[Test Case %v] Wrong resent cc. Expected: %s, Got: %s", index, td.resentCc, d)
+ }
+
+ d = dereferenceAddressList(e.ResentBcc)
+ if !assertAddressListEq(td.resentBcc, d) {
+ t.Errorf("[Test Case %v] Wrong resent bcc. Expected: %s, Got: %s", index, td.resentBcc, d)
+ }
+
+ if !assertSliceEq(td.inReplyTo, e.InReplyTo) {
+ t.Errorf("[Test Case %v] Wrong in reply to. Expected: %s, Got: %s", index, td.inReplyTo, e.InReplyTo)
+ }
+
+ if !assertSliceEq(td.references, e.References) {
+ t.Errorf("[Test Case %v] Wrong references. Expected: %s, Got: %s", index, td.references, e.References)
+ }
+
+ d = dereferenceAddressList(e.ReplyTo)
+ if !assertAddressListEq(td.replyTo, d) {
+ t.Errorf("[Test Case %v] Wrong reply to. Expected: %s, Got: %s", index, td.replyTo, d)
}
if td.htmlBody != e.HTMLBody {
- t.Errorf("Wrong html body. Expected: '%s', Got: '%s'", td.htmlBody, e.HTMLBody)
+ t.Errorf("[Test Case %v] Wrong html body. Expected: '%s', Got: '%s'", index, td.htmlBody, e.HTMLBody)
}
if td.textBody != e.TextBody {
- t.Errorf("Wrong text body. Expected: '%s', Got: '%s'", td.textBody, e.TextBody)
+ t.Errorf("[Test Case %v] Wrong text body. Expected: '%s', Got: '%s'", index, td.textBody, e.TextBody)
}
- if td.messageID != e.MessageID() {
- t.Errorf("Wrong messageID. Expected: '%s', Got: '%s'", td.messageID, e.MessageID())
- }
if len(td.attachments) != len(e.Attachments) {
- t.Errorf("Incorrect number of attachments! Expected: %v, Got: %v.", len(td.attachments), len(e.Attachments))
+ t.Errorf("[Test Case %v] Incorrect number of attachments! Expected: %v, Got: %v.", index, len(td.attachments), len(e.Attachments))
} else {
attachs := e.Attachments[:]
@@ -147,24 +298,24 @@
}
encoded := base64.StdEncoding.EncodeToString(b)
- if ra.Filename == ad.filename && encoded == ad.base64data {
+ if ra.Filename == ad.filename && encoded == ad.base64data && ra.ContentType == ad.contentType {
found = true
attachs = append(attachs[:i], attachs[i+1:]...)
}
}
if !found {
- t.Errorf("Attachment not found: %s", ad.filename)
+ t.Errorf("[Test Case %v] Attachment not found: %s", index, ad.filename)
}
}
if len(attachs) != 0 {
- t.Errorf("Email contains %v unexpected attachments: %v", len(attachs), attachs)
+ t.Errorf("[Test Case %v] Email contains %v unexpected attachments: %v", index, len(attachs), attachs)
}
}
if len(td.embeddedFiles) != len(e.EmbeddedFiles) {
- t.Errorf("Incorrect number of embedded files! Expected: %s, Got: %s.", len(td.embeddedFiles), len(e.EmbeddedFiles))
+ t.Errorf("[Test Case %v] Incorrect number of embedded files! Expected: %s, Got: %s.", index, len(td.embeddedFiles), len(e.EmbeddedFiles))
} else {
embeds := e.EmbeddedFiles[:]
@@ -179,19 +330,19 @@
encoded := base64.StdEncoding.EncodeToString(b)
- if ra.CID == ad.cid && encoded == ad.base64data {
+ if ra.CID == ad.cid && encoded == ad.base64data && ra.ContentType == ad.contentType {
found = true
embeds = append(embeds[:i], embeds[i+1:]...)
}
}
if !found {
- t.Errorf("Embedded file not found: %s", ad.cid)
+ t.Errorf("[Test Case %v] Embedded file not found: %s", index, ad.cid)
}
}
if len(embeds) != 0 {
- t.Errorf("Email contains %v unexpected embedded files: %v", len(embeds), embeds)
+ t.Errorf("[Test Case %v] Email contains %v unexpected embedded files: %v", index, len(embeds), embeds)
}
}
}
@@ -208,11 +359,13 @@
type attachmentData struct{
filename string
+ contentType string
base64data string
}
type embeddedFileData struct{
cid string
+ contentType string
base64data string
}
@@ -242,6 +395,40 @@
return true
}
+func assertAddressListEq(a, b []mail.Address) bool {
+ if len(a) == len(b) && len(a) == 0 {
+ return true
+ }
+
+ if a == nil && b == nil {
+ return true;
+ }
+
+ if a == nil || b == nil {
+ return false;
+ }
+
+ if len(a) != len(b) {
+ return false
+ }
+
+ for i := range a {
+ if a[i] != b[i] {
+ return false
+ }
+ }
+
+ return true
+}
+
+func dereferenceAddressList(al []*mail.Address) (result []mail.Address) {
+ for _, a := range(al) {
+ result = append(result, *a)
+ }
+
+ return
+}
+
var Data1 = `From: =?UTF-8?Q?Peter_Pahol=C3=ADk?= <peter.paholik@gmail.com>
Date: Fri, 7 Apr 2017 09:17:26 +0200
Message-ID: <CACtgX4kNXE7T5XKSKeH_zEcfUUmf2vXVASxYjaaK9cCn-3zb_g@mail.gmail.com>
@@ -327,4 +514,88 @@
--------------5DB4A1356834BB602A5F88B2
--------------C70C0458A558E585ACB75FB4--
-`
\ No newline at end of file
+`
+
+var RFC5322_Example_A11 = `From: John Doe <jdoe@machine.example>
+Sender: Michael Jones <mjones@machine.example>
+To: Mary Smith <mary@example.net>
+Subject: Saying Hello
+Date: Fri, 21 Nov 1997 09:55:06 -0600
+Message-ID: <1234@local.machine.example>
+
+This is a message just to say hello.
+So, "Hello".
+`
+
+var RFC5322_Example_A12 = `From: "Joe Q. Public" <john.q.public@example.com>
+To: Mary Smith <mary@x.test>, jdoe@example.org, Who? <one@y.test>
+Cc: <boss@nil.test>, "Giant; \"Big\" Box" <sysservices@example.net>
+Date: Tue, 1 Jul 2003 10:52:37 +0200
+Message-ID: <5678.21-Nov-1997@example.com>
+
+Hi everyone.
+`
+
+//todo: not yet implemented in net/mail
+//once there is support for this, add it
+var RFC5322_Example_A13 = `From: Pete <pete@silly.example>
+To: A Group:Ed Jones <c@a.test>,joe@where.test,John <jdoe@one.test>;
+Cc: Undisclosed recipients:;
+Date: Thu, 13 Feb 1969 23:32:54 -0330
+Message-ID: <testabcd.1234@silly.example>
+
+Testing.
+`
+
+//we skipped the first message bcause it's the same as A 1.1
+var RFC5322_Example_A2a = `From: Mary Smith <mary@example.net>
+To: John Doe <jdoe@machine.example>
+Reply-To: "Mary Smith: Personal Account" <smith@home.example>
+Subject: Re: Saying Hello
+Date: Fri, 21 Nov 1997 10:01:10 -0600
+Message-ID: <3456@example.net>
+In-Reply-To: <1234@local.machine.example>
+References: <1234@local.machine.example>
+
+This is a reply to your hello.
+`
+
+var RFC5322_Example_A2b = `To: "Mary Smith: Personal Account" <smith@home.example>
+From: John Doe <jdoe@machine.example>
+Subject: Re: Saying Hello
+Date: Fri, 21 Nov 1997 11:00:00 -0600
+Message-ID: <abcd.1234@local.machine.test>
+In-Reply-To: <3456@example.net>
+References: <1234@local.machine.example> <3456@example.net>
+
+This is a reply to your reply.
+`
+
+var RFC5322_Example_A3 = `Resent-From: Mary Smith <mary@example.net>
+Resent-To: Jane Brown <j-brown@other.example>
+Resent-Date: Mon, 24 Nov 1997 14:22:01 -0800
+Resent-Message-ID: <78910@example.net>
+From: John Doe <jdoe@machine.example>
+To: Mary Smith <mary@example.net>
+Subject: Saying Hello
+Date: Fri, 21 Nov 1997 09:55:06 -0600
+Message-ID: <1234@local.machine.example>
+
+This is a message just to say hello.
+So, "Hello".`
+
+var RFC5322_Example_A4 = `Received: from x.y.test
+ by example.net
+ via TCP
+ with ESMTP
+ id ABC12345
+ for <mary@example.net>; 21 Nov 1997 10:05:43 -0600
+Received: from node.example by x.y.test; 21 Nov 1997 10:01:22 -0600
+From: John Doe <jdoe@node.example>
+To: Mary Smith <mary@example.net>
+Subject: Saying Hello
+Date: Fri, 21 Nov 1997 09:55:06 -0600
+Message-ID: <1234@local.node.example>
+
+This is a message just to say hello.
+So, "Hello".`