From d8d2a6e7142228fdb3bacb281ba4fe415ab6df31 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 15 Sep 2023 13:16:14 +0200 Subject: [PATCH 01/47] Add EML parsing functionality to mail package Added two new functions `EMLToMsg` and `readEML` to the `mail` package. `EMLToMsg` function opens and parses a .eml file and returns a pre-filled Msg pointer. `readEML` opens an EML file and uses net/mail to parse the header and body. These changes are made to provide support for EML file parsing, which is a common requirement in many email-based applications. --- eml.go | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 eml.go diff --git a/eml.go b/eml.go new file mode 100644 index 0000000..6d8ff49 --- /dev/null +++ b/eml.go @@ -0,0 +1,97 @@ +package mail + +import ( + "errors" + "fmt" + nm "net/mail" + "os" +) + +// EMLToMsg will open an parse a .eml file at a provided file path and return a +// pre-filled Msg pointer +func EMLToMsg(fp string) (*Msg, error) { + m := &Msg{ + addrHeader: make(map[AddrHeader][]*nm.Address), + genHeader: make(map[Header][]string), + preformHeader: make(map[Header]string), + mimever: Mime10, + } + + pm, err := readEML(fp) + if err != nil || pm == nil { + return m, fmt.Errorf("failed to parse EML file: %w", err) + } + + if err := parseEMLHeaders(&pm.Header, m); err != nil { + return m, fmt.Errorf("failed to parse EML headers: %w", err) + } + + return m, nil +} + +// readEML opens an EML file and uses net/mail to parse the header and body +func readEML(fp string) (*nm.Message, error) { + fh, err := os.Open(fp) + if err != nil { + return nil, fmt.Errorf("failed to open EML file: %w", err) + } + defer func() { + _ = fh.Close() + }() + pm, err := nm.ReadMessage(fh) + if err != nil { + return pm, fmt.Errorf("failed to parse EML: %w", err) + } + return pm, nil +} + +func parseEMLHeaders(mh *nm.Header, m *Msg) error { + commonHeaders := []Header{ + HeaderContentType, HeaderImportance, HeaderInReplyTo, HeaderListUnsubscribe, + HeaderListUnsubscribePost, HeaderMessageID, HeaderMIMEVersion, HeaderOrganization, + HeaderPrecedence, HeaderPriority, HeaderSubject, HeaderUserAgent, HeaderXMailer, + HeaderXMSMailPriority, HeaderXPriority, + } + + // Extract address headers + if v := mh.Get(HeaderFrom.String()); v != "" { + if err := m.From(v); err != nil { + return fmt.Errorf(`failed to parse "From:" header: %w`, err) + } + } + if v := mh.Get(HeaderTo.String()); v != "" { + var als []string + pal, err := nm.ParseAddressList(v) + if err != nil { + return fmt.Errorf(`failed to parse address list: %w`, err) + } + for _, a := range pal { + als = append(als, a.String()) + } + if err := m.To(als...); err != nil { + return fmt.Errorf(`failed to parse "To:" header: %w`, err) + } + } + + // Extract date from message + d, err := mh.Date() + if err != nil { + switch { + case errors.Is(err, nm.ErrHeaderNotPresent): + m.SetDate() + default: + return fmt.Errorf("failed to parse EML date: %w", err) + } + } + if err == nil { + m.SetDateWithValue(d) + } + + // Extract common headers + for _, h := range commonHeaders { + if v := mh.Get(h.String()); v != "" { + m.SetGenHeader(h, v) + } + } + return nil +} From d733b6e17d6de0ed7dac0c9eb219f0e33c3a0ef8 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 21 Sep 2023 13:50:36 +0200 Subject: [PATCH 02/47] Making slight progress on EML parsing --- eml.go | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/eml.go b/eml.go index 6d8ff49..d9562c8 100644 --- a/eml.go +++ b/eml.go @@ -3,6 +3,7 @@ package mail import ( "errors" "fmt" + "mime" nm "net/mail" "os" ) @@ -22,10 +23,19 @@ func EMLToMsg(fp string) (*Msg, error) { return m, fmt.Errorf("failed to parse EML file: %w", err) } + // Parse the header if err := parseEMLHeaders(&pm.Header, m); err != nil { return m, fmt.Errorf("failed to parse EML headers: %w", err) } + // Extract the transfer encoding of the body + x, y, err := mime.ParseMediaType(pm.Header.Get(HeaderContentType.String())) + if err != nil { + return m, fmt.Errorf("failed to extract content type: %w", err) + } + fmt.Printf("Encoding: %s\n", x) + fmt.Printf("Params: %+v\n", y) + return m, nil } @@ -45,6 +55,8 @@ func readEML(fp string) (*nm.Message, error) { return pm, nil } +// parseEMLHeaders will check the EML headers for the most common headers and set the +// according settings in the Msg func parseEMLHeaders(mh *nm.Header, m *Msg) error { commonHeaders := []Header{ HeaderContentType, HeaderImportance, HeaderInReplyTo, HeaderListUnsubscribe, @@ -59,17 +71,24 @@ func parseEMLHeaders(mh *nm.Header, m *Msg) error { return fmt.Errorf(`failed to parse "From:" header: %w`, err) } } - if v := mh.Get(HeaderTo.String()); v != "" { - var als []string - pal, err := nm.ParseAddressList(v) - if err != nil { - return fmt.Errorf(`failed to parse address list: %w`, err) - } - for _, a := range pal { - als = append(als, a.String()) - } - if err := m.To(als...); err != nil { - return fmt.Errorf(`failed to parse "To:" header: %w`, err) + ahl := map[AddrHeader]func(...string) error{ + HeaderTo: m.To, + HeaderCc: m.Cc, + HeaderBcc: m.Bcc, + } + for h, f := range ahl { + if v := mh.Get(h.String()); v != "" { + var als []string + pal, err := nm.ParseAddressList(v) + if err != nil { + return fmt.Errorf(`failed to parse address list: %w`, err) + } + for _, a := range pal { + als = append(als, a.String()) + } + if err := f(als...); err != nil { + return fmt.Errorf(`failed to parse "To:" header: %w`, err) + } } } @@ -93,5 +112,6 @@ func parseEMLHeaders(mh *nm.Header, m *Msg) error { m.SetGenHeader(h, v) } } + return nil } From 3d50370a4c5464d1041a616cced37a3b34dd6221 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 27 Sep 2023 11:29:58 +0200 Subject: [PATCH 03/47] Extract and set charset from email content type The diff modifies how the email library handles the extraction of the mime media type from an email header. It uses the mime.ParseMediaType function to parse the content type header. The function gives back the media type as a string and a mapping of different associated parameters. This mapping was previously just printed, but now the charset parameter is also used for setting the charset of the email if it exists. --- eml.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/eml.go b/eml.go index d9562c8..ea1eaf5 100644 --- a/eml.go +++ b/eml.go @@ -29,12 +29,15 @@ func EMLToMsg(fp string) (*Msg, error) { } // Extract the transfer encoding of the body - x, y, err := mime.ParseMediaType(pm.Header.Get(HeaderContentType.String())) + mi, ar, err := mime.ParseMediaType(pm.Header.Get(HeaderContentType.String())) if err != nil { return m, fmt.Errorf("failed to extract content type: %w", err) } - fmt.Printf("Encoding: %s\n", x) - fmt.Printf("Params: %+v\n", y) + if v, ok := ar["charset"]; ok { + m.SetCharset(Charset(v)) + } + fmt.Printf("Encoding: %s\n", mi) + fmt.Printf("Params: %+v\n", ar) return m, nil } From f60b689b031505874c25c630e810f85370631a3a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 13 Oct 2023 15:06:28 +0200 Subject: [PATCH 04/47] Refactor EML file parsing and header extraction We can no parse simple mails (multipart is not working yet). The existing implementation was made more efficient by refactoring the EML file parsing and header extraction mechanism. Added 'strings' and 'bytes' packages to facilitate these changes. Previously, headers and body were parsed separately which was unnecessarily complex and increased the chance of errors. Now, with the new function 'readEML' and the helper function 'parseEMLBodyParts', we are able to parse headers and body together which not only simplifies the code but also increases its reliability. Specifically, 'bytes.Buffer' now helps us capture body while parsing, which removes need for separate handling. Additionally, certain headers like 'charset' and body types are also accounted for in the new implementation, enhancing the completeness of information extracted from EML files. --- eml.go | 60 ++++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/eml.go b/eml.go index ea1eaf5..59bb0d3 100644 --- a/eml.go +++ b/eml.go @@ -1,11 +1,13 @@ package mail import ( + "bytes" "errors" "fmt" "mime" nm "net/mail" "os" + "strings" ) // EMLToMsg will open an parse a .eml file at a provided file path and return a @@ -18,44 +20,40 @@ func EMLToMsg(fp string) (*Msg, error) { mimever: Mime10, } - pm, err := readEML(fp) + pm, mbbuf, err := readEML(fp) if err != nil || pm == nil { return m, fmt.Errorf("failed to parse EML file: %w", err) } - // Parse the header if err := parseEMLHeaders(&pm.Header, m); err != nil { return m, fmt.Errorf("failed to parse EML headers: %w", err) } - - // Extract the transfer encoding of the body - mi, ar, err := mime.ParseMediaType(pm.Header.Get(HeaderContentType.String())) - if err != nil { - return m, fmt.Errorf("failed to extract content type: %w", err) + if err := parseEMLBodyParts(pm, mbbuf, m); err != nil { + return m, fmt.Errorf("failed to parse EML body parts: %w", err) } - if v, ok := ar["charset"]; ok { - m.SetCharset(Charset(v)) - } - fmt.Printf("Encoding: %s\n", mi) - fmt.Printf("Params: %+v\n", ar) return m, nil } // readEML opens an EML file and uses net/mail to parse the header and body -func readEML(fp string) (*nm.Message, error) { +func readEML(fp string) (*nm.Message, *bytes.Buffer, error) { fh, err := os.Open(fp) if err != nil { - return nil, fmt.Errorf("failed to open EML file: %w", err) + return nil, nil, fmt.Errorf("failed to open EML file: %w", err) } defer func() { _ = fh.Close() }() pm, err := nm.ReadMessage(fh) if err != nil { - return pm, fmt.Errorf("failed to parse EML: %w", err) + return pm, nil, fmt.Errorf("failed to parse EML: %w", err) } - return pm, nil + + buf := bytes.Buffer{} + if _, err = buf.ReadFrom(pm.Body); err != nil { + return nil, nil, err + } + return pm, &buf, nil } // parseEMLHeaders will check the EML headers for the most common headers and set the @@ -64,8 +62,8 @@ func parseEMLHeaders(mh *nm.Header, m *Msg) error { commonHeaders := []Header{ HeaderContentType, HeaderImportance, HeaderInReplyTo, HeaderListUnsubscribe, HeaderListUnsubscribePost, HeaderMessageID, HeaderMIMEVersion, HeaderOrganization, - HeaderPrecedence, HeaderPriority, HeaderSubject, HeaderUserAgent, HeaderXMailer, - HeaderXMSMailPriority, HeaderXPriority, + HeaderPrecedence, HeaderPriority, HeaderReferences, HeaderSubject, HeaderUserAgent, + HeaderXMailer, HeaderXMSMailPriority, HeaderXPriority, } // Extract address headers @@ -118,3 +116,29 @@ func parseEMLHeaders(mh *nm.Header, m *Msg) error { return nil } + +// parseEMLBodyParts ... +func parseEMLBodyParts(pm *nm.Message, mbbuf *bytes.Buffer, m *Msg) error { + // Extract the transfer encoding of the body + mt, par, err := mime.ParseMediaType(pm.Header.Get(HeaderContentType.String())) + if err != nil { + return fmt.Errorf("failed to extract content type: %w", err) + } + if v, ok := par["charset"]; ok { + m.SetCharset(Charset(v)) + } + + if cte := pm.Header.Get(HeaderContentTransferEnc.String()); cte != "" { + switch strings.ToLower(cte) { + case NoEncoding.String(): + m.SetEncoding(NoEncoding) + } + } + + switch strings.ToLower(mt) { + case TypeTextPlain.String(): + m.SetBodyString(TypeTextPlain, mbbuf.String()) + default: + } + return nil +} From 1a9141074a8a057a277469b029e13f0d8d82e311 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 13 Oct 2023 15:06:54 +0200 Subject: [PATCH 05/47] Add References header field and improve string methods Added "References" header field to cover more potential use cases and enhance versatility. This field will allow applications to track series of related messages. Test for "References" field has also been added for validation. Also included are string methods for Content-type objects with relevant tests, ensuring accurate string conversion. Unnecessary duplicate method of string conversion for Charset has been removed to streamline the code and improve readability. --- encoding.go | 15 ++++++++++----- encoding_test.go | 32 ++++++++++++++++++++++++++++++++ header.go | 3 +++ header_test.go | 1 + 4 files changed, 46 insertions(+), 5 deletions(-) diff --git a/encoding.go b/encoding.go index dff1e6f..0aed3d3 100644 --- a/encoding.go +++ b/encoding.go @@ -152,12 +152,17 @@ const ( MIMERelated MIMEType = "related" ) -// String is a standard method to convert an Encoding into a printable format -func (e Encoding) String() string { - return string(e) -} - // String is a standard method to convert an Charset into a printable format func (c Charset) String() string { return string(c) } + +// String is a standard method to convert an ContentType into a printable format +func (c ContentType) String() string { + return string(c) +} + +// String is a standard method to convert an Encoding into a printable format +func (e Encoding) String() string { + return string(e) +} diff --git a/encoding_test.go b/encoding_test.go index 71624f3..4a3affc 100644 --- a/encoding_test.go +++ b/encoding_test.go @@ -27,6 +27,38 @@ func TestEncoding_String(t *testing.T) { } } +// TestContentType_String tests the string method of the ContentType object +func TestContentType_String(t *testing.T) { + tests := []struct { + name string + ct ContentType + want string + }{ + {"ContentType: text/plain", TypeTextPlain, "text/plain"}, + {"ContentType: text/html", TypeTextHTML, "text/html"}, + { + "ContentType: application/octet-stream", TypeAppOctetStream, + "application/octet-stream", + }, + { + "ContentType: application/pgp-signature", TypePGPSignature, + "application/pgp-signature", + }, + { + "ContentType: application/pgp-encrypted", TypePGPEncrypted, + "application/pgp-encrypted", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.ct.String() != tt.want { + t.Errorf("wrong string for Content-Type returned. Expected: %s, got: %s", + tt.want, tt.ct.String()) + } + }) + } +} + // TestCharset_String tests the string method of the Charset object func TestCharset_String(t *testing.T) { tests := []struct { diff --git a/header.go b/header.go index 4fca337..35c5284 100644 --- a/header.go +++ b/header.go @@ -73,6 +73,9 @@ const ( // HeaderPriority represents the "Priority" field HeaderPriority Header = "Priority" + // HeaderReferences is the "References" header field + HeaderReferences Header = "References" + // HeaderReplyTo is the "Reply-To" header field HeaderReplyTo Header = "Reply-To" diff --git a/header_test.go b/header_test.go index aea2f4c..a060ae6 100644 --- a/header_test.go +++ b/header_test.go @@ -87,6 +87,7 @@ func TestHeader_String(t *testing.T) { {"Header: Organization", HeaderOrganization, "Organization"}, {"Header: Precedence", HeaderPrecedence, "Precedence"}, {"Header: Priority", HeaderPriority, "Priority"}, + {"Header: HeaderReferences", HeaderReferences, "References"}, {"Header: Reply-To", HeaderReplyTo, "Reply-To"}, {"Header: Subject", HeaderSubject, "Subject"}, {"Header: User-Agent", HeaderUserAgent, "User-Agent"}, From 07a3450103fd27693b3f57bbc8e8a80fc2448d08 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 13 Oct 2023 15:08:53 +0200 Subject: [PATCH 06/47] Added SPDX license header --- eml.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/eml.go b/eml.go index 59bb0d3..2ffc96f 100644 --- a/eml.go +++ b/eml.go @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors +// +// SPDX-License-Identifier: MIT + package mail import ( From e7c717d0fc0e15f3608494c2206908127ac8a28e Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 13 Oct 2023 15:23:28 +0200 Subject: [PATCH 07/47] "Rename Mime10 to MIME10 in codebase Renamed field 'Mime10' to 'MIME10' across multiple files for canonical representation and consistency with standard MIME naming format in the protocol." --- eml.go | 2 +- encoding.go | 4 ++-- msg.go | 2 +- msg_test.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/eml.go b/eml.go index 2ffc96f..2436f75 100644 --- a/eml.go +++ b/eml.go @@ -21,7 +21,7 @@ func EMLToMsg(fp string) (*Msg, error) { addrHeader: make(map[AddrHeader][]*nm.Address), genHeader: make(map[Header][]string), preformHeader: make(map[Header]string), - mimever: Mime10, + mimever: MIME10, } pm, mbbuf, err := readEML(fp) diff --git a/encoding.go b/encoding.go index 0aed3d3..1491ab0 100644 --- a/encoding.go +++ b/encoding.go @@ -132,8 +132,8 @@ const ( // List of MIME versions const ( - // Mime10 is the MIME Version 1.0 - Mime10 MIMEVersion = "1.0" + // MIME10 is the MIME Version 1.0 + MIME10 MIMEVersion = "1.0" ) // List of common content types diff --git a/msg.go b/msg.go index 0d3482e..a0cece4 100644 --- a/msg.go +++ b/msg.go @@ -129,7 +129,7 @@ func NewMsg(o ...MsgOption) *Msg { encoding: EncodingQP, genHeader: make(map[Header][]string), preformHeader: make(map[Header]string), - mimever: Mime10, + mimever: MIME10, } // Override defaults with optionally provided MsgOption functions diff --git a/msg_test.go b/msg_test.go index 98e004e..8539457 100644 --- a/msg_test.go +++ b/msg_test.go @@ -137,7 +137,7 @@ func TestNewMsgWithMIMEVersion(t *testing.T) { value MIMEVersion want MIMEVersion }{ - {"MIME version is 1.0", Mime10, "1.0"}, + {"MIME version is 1.0", MIME10, "1.0"}, } for _, tt := range tests { From a3b3deb4675b8a9c7da8968916bbc4904b72e957 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 13 Oct 2023 16:04:07 +0200 Subject: [PATCH 08/47] #145: Support for quoted-printable encoding in email parser Added support for quoted-printable encoding in email parser to increase its functionality. The change includes a case handling feature for 'EncodingQP' and related conversions to allow for proper message body reading and encoding setting. This improves the robustness and the scope of email content types that the parser can handle." --- eml.go | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/eml.go b/eml.go index 2436f75..2fd413b 100644 --- a/eml.go +++ b/eml.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "mime" + "mime/quotedprintable" nm "net/mail" "os" "strings" @@ -132,16 +133,24 @@ func parseEMLBodyParts(pm *nm.Message, mbbuf *bytes.Buffer, m *Msg) error { m.SetCharset(Charset(v)) } - if cte := pm.Header.Get(HeaderContentTransferEnc.String()); cte != "" { - switch strings.ToLower(cte) { - case NoEncoding.String(): - m.SetEncoding(NoEncoding) - } - } - + cte := pm.Header.Get(HeaderContentTransferEnc.String()) switch strings.ToLower(mt) { case TypeTextPlain.String(): - m.SetBodyString(TypeTextPlain, mbbuf.String()) + if cte == NoEncoding.String() { + m.SetEncoding(NoEncoding) + m.SetBodyString(TypeTextPlain, mbbuf.String()) + break + } + if cte == EncodingQP.String() { + m.SetEncoding(EncodingQP) + qpr := quotedprintable.NewReader(mbbuf) + qpbuf := bytes.Buffer{} + if _, err := qpbuf.ReadFrom(qpr); err != nil { + return fmt.Errorf("failed to read quoted-printable body: %w", err) + } + m.SetBodyString(TypeTextPlain, qpbuf.String()) + break + } default: } return nil From 54ccb80925bac448b2cc79fe2020b6c7ebdeaf99 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 13 Oct 2023 16:27:57 +0200 Subject: [PATCH 09/47] Add base64 support in email parser Implemented base64 encoding support in the email parser. This addition allows the parser to read and decode base64 encoded emails. --- eml.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/eml.go b/eml.go index 2fd413b..7dd3686 100644 --- a/eml.go +++ b/eml.go @@ -6,6 +6,7 @@ package mail import ( "bytes" + "encoding/base64" "errors" "fmt" "mime" @@ -151,6 +152,16 @@ func parseEMLBodyParts(pm *nm.Message, mbbuf *bytes.Buffer, m *Msg) error { m.SetBodyString(TypeTextPlain, qpbuf.String()) break } + if cte == EncodingB64.String() { + m.SetEncoding(EncodingB64) + b64d := base64.NewDecoder(base64.StdEncoding, mbbuf) + b64buf := bytes.Buffer{} + if _, err := b64buf.ReadFrom(b64d); err != nil { + return fmt.Errorf("failed to read base64 body: %w", err) + } + m.SetBodyString(TypeTextPlain, b64buf.String()) + break + } default: } return nil From 2bba7b902baa29e58b8c4ce76d150c25e111ebb5 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 17 Oct 2023 10:41:42 +0200 Subject: [PATCH 10/47] Updated error handling and encoding comparison in EML parsing This commit changes the usage of error value and improves the string comparison for encoding types in EML file parsing. It ensures file closure after read operations to avoid memory leaks. Error messages are made dynamic for improved error reporting. Comments on function has also been made more descriptive. --- eml.go | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/eml.go b/eml.go index 7dd3686..110f831 100644 --- a/eml.go +++ b/eml.go @@ -31,10 +31,10 @@ func EMLToMsg(fp string) (*Msg, error) { return m, fmt.Errorf("failed to parse EML file: %w", err) } - if err := parseEMLHeaders(&pm.Header, m); err != nil { + if err = parseEMLHeaders(&pm.Header, m); err != nil { return m, fmt.Errorf("failed to parse EML headers: %w", err) } - if err := parseEMLBodyParts(pm, mbbuf, m); err != nil { + if err = parseEMLBodyParts(pm, mbbuf, m); err != nil { return m, fmt.Errorf("failed to parse EML body parts: %w", err) } @@ -59,6 +59,10 @@ func readEML(fp string) (*nm.Message, *bytes.Buffer, error) { if _, err = buf.ReadFrom(pm.Body); err != nil { return nil, nil, err } + + if err = fh.Close(); err != nil { + return nil, nil, fmt.Errorf("failed to close EML file: %w", err) + } return pm, &buf, nil } @@ -75,7 +79,7 @@ func parseEMLHeaders(mh *nm.Header, m *Msg) error { // Extract address headers if v := mh.Get(HeaderFrom.String()); v != "" { if err := m.From(v); err != nil { - return fmt.Errorf(`failed to parse "From:" header: %w`, err) + return fmt.Errorf(`failed to parse %q header: %w`, HeaderFrom, err) } } ahl := map[AddrHeader]func(...string) error{ @@ -94,7 +98,7 @@ func parseEMLHeaders(mh *nm.Header, m *Msg) error { als = append(als, a.String()) } if err := f(als...); err != nil { - return fmt.Errorf(`failed to parse "To:" header: %w`, err) + return fmt.Errorf(`failed to parse %q header: %w`, HeaderTo, err) } } } @@ -123,7 +127,7 @@ func parseEMLHeaders(mh *nm.Header, m *Msg) error { return nil } -// parseEMLBodyParts ... +// parseEMLBodyParts parses the body of a EML based on the different content types and encodings func parseEMLBodyParts(pm *nm.Message, mbbuf *bytes.Buffer, m *Msg) error { // Extract the transfer encoding of the body mt, par, err := mime.ParseMediaType(pm.Header.Get(HeaderContentType.String())) @@ -137,26 +141,26 @@ func parseEMLBodyParts(pm *nm.Message, mbbuf *bytes.Buffer, m *Msg) error { cte := pm.Header.Get(HeaderContentTransferEnc.String()) switch strings.ToLower(mt) { case TypeTextPlain.String(): - if cte == NoEncoding.String() { + if strings.EqualFold(cte, NoEncoding.String()) { m.SetEncoding(NoEncoding) m.SetBodyString(TypeTextPlain, mbbuf.String()) break } - if cte == EncodingQP.String() { + if strings.EqualFold(cte, EncodingQP.String()) { m.SetEncoding(EncodingQP) qpr := quotedprintable.NewReader(mbbuf) qpbuf := bytes.Buffer{} - if _, err := qpbuf.ReadFrom(qpr); err != nil { + if _, err = qpbuf.ReadFrom(qpr); err != nil { return fmt.Errorf("failed to read quoted-printable body: %w", err) } m.SetBodyString(TypeTextPlain, qpbuf.String()) break } - if cte == EncodingB64.String() { + if strings.EqualFold(cte, EncodingB64.String()) { m.SetEncoding(EncodingB64) b64d := base64.NewDecoder(base64.StdEncoding, mbbuf) b64buf := bytes.Buffer{} - if _, err := b64buf.ReadFrom(b64d); err != nil { + if _, err = b64buf.ReadFrom(b64d); err != nil { return fmt.Errorf("failed to read base64 body: %w", err) } m.SetBodyString(TypeTextPlain, b64buf.String()) From f9140ce90ec006fb8c43f74b4cbbce507b8ecfc2 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 31 Oct 2023 11:45:09 +0100 Subject: [PATCH 11/47] Add parsing of EML from string and reader Added two new methods `EMLToMsgFromString` and `EMLToMsgFromReader` in "eml.go". They allow EML parsing directly from a given string and a reader object, increasing overall functionality and versatility of the EML parsing process. This will enable the users to parse EML documents more flexibly." --- eml.go | 46 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/eml.go b/eml.go index 110f831..ad65ad2 100644 --- a/eml.go +++ b/eml.go @@ -9,6 +9,7 @@ import ( "encoding/base64" "errors" "fmt" + "io" "mime" "mime/quotedprintable" nm "net/mail" @@ -16,9 +17,40 @@ import ( "strings" ) -// EMLToMsg will open an parse a .eml file at a provided file path and return a +// EMLToMsgFromString will parse a given EML string and returns a pre-filled Msg pointer +func EMLToMsgFromString(es string) (*Msg, error) { + eb := bytes.NewBufferString(es) + return EMLToMsgFromReader(eb) +} + +// EMLToMsgFromReader will parse a reader that holds EML content and returns a pre-filled +// Msg pointer +func EMLToMsgFromReader(r io.Reader) (*Msg, error) { + m := &Msg{ + addrHeader: make(map[AddrHeader][]*nm.Address), + genHeader: make(map[Header][]string), + preformHeader: make(map[Header]string), + mimever: MIME10, + } + + pm, mbbuf, err := readEMLFromReader(r) + if err != nil || pm == nil { + return m, fmt.Errorf("failed to parse EML from reader: %w", err) + } + + if err = parseEMLHeaders(&pm.Header, m); err != nil { + return m, fmt.Errorf("failed to parse EML headers: %w", err) + } + if err = parseEMLBodyParts(pm, mbbuf, m); err != nil { + return m, fmt.Errorf("failed to parse EML body parts: %w", err) + } + + return m, nil +} + +// EMLToMsgFromFile will open and parse a .eml file at a provided file path and returns a // pre-filled Msg pointer -func EMLToMsg(fp string) (*Msg, error) { +func EMLToMsgFromFile(fp string) (*Msg, error) { m := &Msg{ addrHeader: make(map[AddrHeader][]*nm.Address), genHeader: make(map[Header][]string), @@ -50,7 +82,12 @@ func readEML(fp string) (*nm.Message, *bytes.Buffer, error) { defer func() { _ = fh.Close() }() - pm, err := nm.ReadMessage(fh) + return readEMLFromReader(fh) +} + +// readEMLFromReader uses net/mail to parse the header and body from a given io.Reader +func readEMLFromReader(r io.Reader) (*nm.Message, *bytes.Buffer, error) { + pm, err := nm.ReadMessage(r) if err != nil { return pm, nil, fmt.Errorf("failed to parse EML: %w", err) } @@ -60,9 +97,6 @@ func readEML(fp string) (*nm.Message, *bytes.Buffer, error) { return nil, nil, err } - if err = fh.Close(); err != nil { - return nil, nil, fmt.Errorf("failed to close EML file: %w", err) - } return pm, &buf, nil } From 9607d08469194a53574f9b7b5b9f16fa0e45a897 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 31 Oct 2023 11:45:37 +0100 Subject: [PATCH 12/47] Add EML parsing from string to tests A new test `TestEMLToMsgFromString` was added to "eml_test.go". This test asserts the proper functionality of `EMLToMsgFromString` method that allows us to parse EMLs directly from a string input. This test is a necessary part of ensuring the functionality and reliability of our EML parsing process. --- eml_test.go | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 eml_test.go diff --git a/eml_test.go b/eml_test.go new file mode 100644 index 0000000..83e26ab --- /dev/null +++ b/eml_test.go @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors +// +// SPDX-License-Identifier: MIT + +package mail + +import ( + "strings" + "testing" +) + +const ( + exampleMailPlainNoEnc = `Date: Thu, 14 Sep 2023 14:35:28 +0200 +MIME-Version: 1.0 +Message-ID: + <1305604950.683004066175.AAAAAAAAaaaaaaaaB@test.com> +Subject: Example mail Plain text no Encoding +User-Agent: go-mail v0.3.9 // https://github.com/wneessen/go-mail +X-Mailer: go-mail v0.3.8 // https://github.com/wneessen/go-mail +From: "Toni Tester" +To: "Go Mail" +Cc: Second Recipient +Bcc: "Invisible User" +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Dear Customer, + +This is a test mail. Please do not reply to this. + + +Thank your for your business! +The go-mail team + +-- +This is a signature` +) + +func TestEMLToMsgFromString(t *testing.T) { + tests := []struct { + name string + eml string + enc string + sub string + }{ + { + "Plain text no encoding", exampleMailPlainNoEnc, "8bit", + "Example mail Plain text no Encoding", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := EMLToMsgFromString(tt.eml) + if err != nil { + t.Errorf("failed to parse EML: %s", err) + } + if m.Encoding() != tt.enc { + t.Errorf("EMLToMsgFromString failed: expected encoding: %s, but got: %s", tt.enc, m.Encoding()) + } + if s := m.GetGenHeader(HeaderSubject); len(s) > 0 && !strings.EqualFold(s[0], tt.sub) { + t.Errorf("EMLToMsgFromString failed: expected subject: %s, but got: %s", tt.sub, s[0]) + } + }) + } +} From 8a1391b9dfbb26581c2e49e604716f3f7931b92d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 2 Nov 2023 16:06:27 +0100 Subject: [PATCH 13/47] Update test emails and enhance parser tests The test emails in the eml_test.go file have been updated with more diverse fields, including variations of encoding types. These changes help improve the robustness of our parser tests by evaluating its function with a wider range of email structures. Tests including quoted-printable and base64 encoded emails have been added. --- eml_test.go | 71 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 59 insertions(+), 12 deletions(-) diff --git a/eml_test.go b/eml_test.go index 83e26ab..e94d07c 100644 --- a/eml_test.go +++ b/eml_test.go @@ -10,30 +10,69 @@ import ( ) const ( - exampleMailPlainNoEnc = `Date: Thu, 14 Sep 2023 14:35:28 +0200 + exampleMailPlainNoEnc = `Date: Wed, 01 Nov 2023 00:00:00 +0000 MIME-Version: 1.0 -Message-ID: - <1305604950.683004066175.AAAAAAAAaaaaaaaaB@test.com> -Subject: Example mail Plain text no Encoding -User-Agent: go-mail v0.3.9 // https://github.com/wneessen/go-mail -X-Mailer: go-mail v0.3.8 // https://github.com/wneessen/go-mail +Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev> +Subject: Example mail // plain text without encoding +User-Agent: go-mail v0.4.0 // https://github.com/wneessen/go-mail +X-Mailer: go-mail v0.4.0 // https://github.com/wneessen/go-mail From: "Toni Tester" -To: "Go Mail" -Cc: Second Recipient -Bcc: "Invisible User" +To: +Cc: Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dear Customer, -This is a test mail. Please do not reply to this. +This is a test mail. Please do not reply to this. Also this line is very long so it +should be wrapped. Thank your for your business! The go-mail team --- +-- This is a signature` + exampleMailPlainQP = `Date: Wed, 01 Nov 2023 00:00:00 +0000 +MIME-Version: 1.0 +Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev> +Subject: Example mail // plain text quoted-printable +User-Agent: go-mail v0.4.0 // https://github.com/wneessen/go-mail +X-Mailer: go-mail v0.4.0 // https://github.com/wneessen/go-mail +From: "Toni Tester" +To: +Cc: +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: quoted-printable + +Dear Customer, + +This is a test mail. Please do not reply to this. Also this line is very lo= +ng so it +should be wrapped. + + +Thank your for your business! +The go-mail team + +-- +This is a signature` + exampleMailPlainB64 = `Date: Wed, 01 Nov 2023 00:00:00 +0000 +MIME-Version: 1.0 +Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev> +Subject: Example mail // plain text base64 +User-Agent: go-mail v0.4.0 // https://github.com/wneessen/go-mail +X-Mailer: go-mail v0.4.0 // https://github.com/wneessen/go-mail +From: "Toni Tester" +To: +Cc: +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: base64 + +RGVhciBDdXN0b21lciwKClRoaXMgaXMgYSB0ZXN0IG1haWwuIFBsZWFzZSBkbyBub3QgcmVwbHkg +dG8gdGhpcy4gQWxzbyB0aGlzIGxpbmUgaXMgdmVyeSBsb25nIHNvIGl0CnNob3VsZCBiZSB3cmFw +cGVkLgoKClRoYW5rIHlvdXIgZm9yIHlvdXIgYnVzaW5lc3MhClRoZSBnby1tYWlsIHRlYW0KCi0t +ClRoaXMgaXMgYSBzaWduYXR1cmU=` ) func TestEMLToMsgFromString(t *testing.T) { @@ -45,7 +84,15 @@ func TestEMLToMsgFromString(t *testing.T) { }{ { "Plain text no encoding", exampleMailPlainNoEnc, "8bit", - "Example mail Plain text no Encoding", + "Example mail // plain text without encoding", + }, + { + "Plain text quoted-printable", exampleMailPlainQP, "quoted-printable", + "Example mail // plain text quoted-printable", + }, + { + "Plain text base64", exampleMailPlainB64, "base64", + "Example mail // plain text base64", }, } for _, tt := range tests { From e9331e0b7c69b8d4d51e1528ceace53305ba48a0 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 5 Nov 2023 19:48:43 +0100 Subject: [PATCH 14/47] Add time import and tests for invalid date in email Added `time` import in the eml_test.go and added two new test use-cases: `exampleMailPlainNoEncInvalidDate` and `exampleMailPlainNoEncNoDate`. The `exampleMailPlainNoEncInvalidDate` is used to check if the parser can correctly handle email with invalid date. Meanwhile, `exampleMailPlainNoEncNoDate` checks if the parser can correctly add the current date to an email that didn't specify a date. This will improve the parser's resilience and flexibility in handling various email scenarios. --- eml_test.go | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/eml_test.go b/eml_test.go index e94d07c..ca58e43 100644 --- a/eml_test.go +++ b/eml_test.go @@ -7,6 +7,7 @@ package mail import ( "strings" "testing" + "time" ) const ( @@ -28,6 +29,51 @@ This is a test mail. Please do not reply to this. Also this line is very long so should be wrapped. +Thank your for your business! +The go-mail team + +-- +This is a signature` + exampleMailPlainNoEncInvalidDate = `Date: Inv, 99 Nov 9999 99:99:00 +0000 +MIME-Version: 1.0 +Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev> +Subject: Example mail // plain text without encoding +User-Agent: go-mail v0.4.0 // https://github.com/wneessen/go-mail +X-Mailer: go-mail v0.4.0 // https://github.com/wneessen/go-mail +From: "Toni Tester" +To: +Cc: +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Dear Customer, + +This is a test mail. Please do not reply to this. Also this line is very long so it +should be wrapped. + + +Thank your for your business! +The go-mail team + +-- +This is a signature` + exampleMailPlainNoEncNoDate = `MIME-Version: 1.0 +Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev> +Subject: Example mail // plain text without encoding +User-Agent: go-mail v0.4.0 // https://github.com/wneessen/go-mail +X-Mailer: go-mail v0.4.0 // https://github.com/wneessen/go-mail +From: "Toni Tester" +To: +Cc: +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Dear Customer, + +This is a test mail. Please do not reply to this. Also this line is very long so it +should be wrapped. + + Thank your for your business! The go-mail team @@ -110,3 +156,24 @@ func TestEMLToMsgFromString(t *testing.T) { }) } } + +func TestEMLToMsgFromStringBrokenDate(t *testing.T) { + _, err := EMLToMsgFromString(exampleMailPlainNoEncInvalidDate) + if err == nil { + t.Error("EML with invalid date was supposed to fail, but didn't") + } + now := time.Now() + m, err := EMLToMsgFromString(exampleMailPlainNoEncNoDate) + if err != nil { + t.Errorf("EML with no date parsing failed: %s", err) + } + da := m.GetGenHeader(HeaderDate) + if len(da) < 1 { + t.Error("EML with no date expected current date, but got nothing") + return + } + d := da[0] + if d != now.Format(time.RFC1123Z) { + t.Errorf("EML with no date expected: %s, got: %s", now.Format(time.RFC1123Z), d) + } +} From 0d9ba278fea0110c5510171d87a6a5a172385267 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 22 Jan 2024 17:48:49 +0100 Subject: [PATCH 15/47] Update common content types in encoding.go The list of common content types in encoding.go has been revised. The type "multipart/alternative" has been added and the order of types has been adjusted for consistency with net/smtp upstream. --- encoding.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/encoding.go b/encoding.go index 1491ab0..39c58e3 100644 --- a/encoding.go +++ b/encoding.go @@ -138,11 +138,12 @@ const ( // List of common content types const ( - TypeTextPlain ContentType = "text/plain" - TypeTextHTML ContentType = "text/html" - TypeAppOctetStream ContentType = "application/octet-stream" - TypePGPSignature ContentType = "application/pgp-signature" - TypePGPEncrypted ContentType = "application/pgp-encrypted" + TypeAppOctetStream ContentType = "application/octet-stream" + TypeMultipartAlternative ContentType = "multipart/alternative" + TypePGPSignature ContentType = "application/pgp-signature" + TypePGPEncrypted ContentType = "application/pgp-encrypted" + TypeTextHTML ContentType = "text/html" + TypeTextPlain ContentType = "text/plain" ) // List of MIMETypes From 60222c95e7c512d150971ce7f94412b29cd879fc Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 22 Jan 2024 17:49:58 +0100 Subject: [PATCH 16/47] Add multipart message parsing in eml.go This commit introduces the ability to handle multipart messages within the eml.go file. It reads individual parts of multipart messages, sets the encoding and content for each part, and implements error handling for potential issues like a missing boundary tag or difficulties acquiring the next part of a multipart message. --- eml.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/eml.go b/eml.go index ad65ad2..9adeda1 100644 --- a/eml.go +++ b/eml.go @@ -11,6 +11,7 @@ import ( "fmt" "io" "mime" + "mime/multipart" "mime/quotedprintable" nm "net/mail" "os" @@ -200,6 +201,59 @@ func parseEMLBodyParts(pm *nm.Message, mbbuf *bytes.Buffer, m *Msg) error { m.SetBodyString(TypeTextPlain, b64buf.String()) break } + case TypeMultipartAlternative.String(): + // TODO: Refactor in its own function + b, ok := par["boundary"] + if !ok { + return fmt.Errorf("no boundary tag found in multipart body") + } + mpr := multipart.NewReader(mbbuf, b) + mp, err := mpr.NextPart() + if err != nil { + return fmt.Errorf("failed to get next part of multipart message: %w", err) + } + for err == nil { + // TODO: Clean up code + mpd, mperr := io.ReadAll(mp) + if mperr != nil { + _ = mp.Close() + return fmt.Errorf("failed to read multipart: %w", err) + } + mpctc, ok := mp.Header[HeaderContentType.String()] + if !ok { + return fmt.Errorf("failed to get content-type from part") + } + mpcts := strings.Split(mpctc[0], "; ") + if len(mpcts) > 1 && strings.HasPrefix(strings.ToLower(mpcts[1]), "charset=") { + vs := strings.Split(mpcts[1], "=") + if len(vs) > 1 { + // TODO: We probably want per-part charset instead + m.SetCharset(Charset(vs[1])) + } + } + mpcte, ok := mp.Header[HeaderContentTransferEnc.String()] + if !ok { + return fmt.Errorf("failed to get content-transfer-encoding from part") + } + m.SetCharset(Charset(mpcts[1])) + p := m.newPart(ContentType(mpcts[0])) + switch { + case strings.EqualFold(mpcte[0], EncodingB64.String()): + p.SetEncoding(EncodingB64) + cont, err := base64.StdEncoding.DecodeString(string(mpd)) + if err != nil { + return fmt.Errorf("failed to decode base64 part: %w", err) + } + p.SetContent(string(cont)) + } + m.parts = append(m.parts, p) + mp, err = mpr.NextPart() + } + if err != io.EOF { + _ = mp.Close() + return fmt.Errorf("failed to read multipart: %w", err) + } + default: } return nil From 4202f705a02c276e46db63f135322589e00f482a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 9 Feb 2024 12:34:11 +0100 Subject: [PATCH 17/47] Refactor variable naming and multipart parse function in eml.go The variable names "mbbuf", "mt", and "par" have been renamed to "bodybuf", "mediatype", and "params" respectively, for clarification. Moreover, the multipart parsing block within the parseEMLBodyParts function was extracted into its own function, parseEMLMultipartAlternative, for improved code structure and readability. --- eml.go | 141 ++++++++++++++++++++++++++++++++------------------------- 1 file changed, 79 insertions(+), 62 deletions(-) diff --git a/eml.go b/eml.go index 9adeda1..8333120 100644 --- a/eml.go +++ b/eml.go @@ -34,7 +34,7 @@ func EMLToMsgFromReader(r io.Reader) (*Msg, error) { mimever: MIME10, } - pm, mbbuf, err := readEMLFromReader(r) + pm, bodybuf, err := readEMLFromReader(r) if err != nil || pm == nil { return m, fmt.Errorf("failed to parse EML from reader: %w", err) } @@ -42,7 +42,7 @@ func EMLToMsgFromReader(r io.Reader) (*Msg, error) { if err = parseEMLHeaders(&pm.Header, m); err != nil { return m, fmt.Errorf("failed to parse EML headers: %w", err) } - if err = parseEMLBodyParts(pm, mbbuf, m); err != nil { + if err = parseEMLBodyParts(pm, bodybuf, m); err != nil { return m, fmt.Errorf("failed to parse EML body parts: %w", err) } @@ -59,7 +59,7 @@ func EMLToMsgFromFile(fp string) (*Msg, error) { mimever: MIME10, } - pm, mbbuf, err := readEML(fp) + pm, bodybuf, err := readEML(fp) if err != nil || pm == nil { return m, fmt.Errorf("failed to parse EML file: %w", err) } @@ -67,7 +67,7 @@ func EMLToMsgFromFile(fp string) (*Msg, error) { if err = parseEMLHeaders(&pm.Header, m); err != nil { return m, fmt.Errorf("failed to parse EML headers: %w", err) } - if err = parseEMLBodyParts(pm, mbbuf, m); err != nil { + if err = parseEMLBodyParts(pm, bodybuf, m); err != nil { return m, fmt.Errorf("failed to parse EML body parts: %w", err) } @@ -163,27 +163,27 @@ func parseEMLHeaders(mh *nm.Header, m *Msg) error { } // parseEMLBodyParts parses the body of a EML based on the different content types and encodings -func parseEMLBodyParts(pm *nm.Message, mbbuf *bytes.Buffer, m *Msg) error { +func parseEMLBodyParts(pm *nm.Message, bodybuf *bytes.Buffer, m *Msg) error { // Extract the transfer encoding of the body - mt, par, err := mime.ParseMediaType(pm.Header.Get(HeaderContentType.String())) + mediatype, params, err := mime.ParseMediaType(pm.Header.Get(HeaderContentType.String())) if err != nil { return fmt.Errorf("failed to extract content type: %w", err) } - if v, ok := par["charset"]; ok { + if v, ok := params["charset"]; ok { m.SetCharset(Charset(v)) } cte := pm.Header.Get(HeaderContentTransferEnc.String()) - switch strings.ToLower(mt) { + switch strings.ToLower(mediatype) { case TypeTextPlain.String(): if strings.EqualFold(cte, NoEncoding.String()) { m.SetEncoding(NoEncoding) - m.SetBodyString(TypeTextPlain, mbbuf.String()) + m.SetBodyString(TypeTextPlain, bodybuf.String()) break } if strings.EqualFold(cte, EncodingQP.String()) { m.SetEncoding(EncodingQP) - qpr := quotedprintable.NewReader(mbbuf) + qpr := quotedprintable.NewReader(bodybuf) qpbuf := bytes.Buffer{} if _, err = qpbuf.ReadFrom(qpr); err != nil { return fmt.Errorf("failed to read quoted-printable body: %w", err) @@ -193,7 +193,7 @@ func parseEMLBodyParts(pm *nm.Message, mbbuf *bytes.Buffer, m *Msg) error { } if strings.EqualFold(cte, EncodingB64.String()) { m.SetEncoding(EncodingB64) - b64d := base64.NewDecoder(base64.StdEncoding, mbbuf) + b64d := base64.NewDecoder(base64.StdEncoding, bodybuf) b64buf := bytes.Buffer{} if _, err = b64buf.ReadFrom(b64d); err != nil { return fmt.Errorf("failed to read base64 body: %w", err) @@ -202,59 +202,76 @@ func parseEMLBodyParts(pm *nm.Message, mbbuf *bytes.Buffer, m *Msg) error { break } case TypeMultipartAlternative.String(): - // TODO: Refactor in its own function - b, ok := par["boundary"] - if !ok { - return fmt.Errorf("no boundary tag found in multipart body") + if err := parseEMLMultipartAlternative(params, bodybuf, m); err != nil { + return fmt.Errorf("failed to parse multipart/alternative: %w", err) } - mpr := multipart.NewReader(mbbuf, b) - mp, err := mpr.NextPart() - if err != nil { - return fmt.Errorf("failed to get next part of multipart message: %w", err) - } - for err == nil { - // TODO: Clean up code - mpd, mperr := io.ReadAll(mp) - if mperr != nil { - _ = mp.Close() - return fmt.Errorf("failed to read multipart: %w", err) - } - mpctc, ok := mp.Header[HeaderContentType.String()] - if !ok { - return fmt.Errorf("failed to get content-type from part") - } - mpcts := strings.Split(mpctc[0], "; ") - if len(mpcts) > 1 && strings.HasPrefix(strings.ToLower(mpcts[1]), "charset=") { - vs := strings.Split(mpcts[1], "=") - if len(vs) > 1 { - // TODO: We probably want per-part charset instead - m.SetCharset(Charset(vs[1])) - } - } - mpcte, ok := mp.Header[HeaderContentTransferEnc.String()] - if !ok { - return fmt.Errorf("failed to get content-transfer-encoding from part") - } - m.SetCharset(Charset(mpcts[1])) - p := m.newPart(ContentType(mpcts[0])) - switch { - case strings.EqualFold(mpcte[0], EncodingB64.String()): - p.SetEncoding(EncodingB64) - cont, err := base64.StdEncoding.DecodeString(string(mpd)) - if err != nil { - return fmt.Errorf("failed to decode base64 part: %w", err) - } - p.SetContent(string(cont)) - } - m.parts = append(m.parts, p) - mp, err = mpr.NextPart() - } - if err != io.EOF { - _ = mp.Close() - return fmt.Errorf("failed to read multipart: %w", err) - } - default: } return nil } + +// parseEMLMultipartAlternative parses a multipart/alternative body part of a EML +func parseEMLMultipartAlternative(params map[string]string, bodybuf *bytes.Buffer, m *Msg) error { + boundary, ok := params["boundary"] + if !ok { + return fmt.Errorf("no boundary tag found in multipart body") + } + mpreader := multipart.NewReader(bodybuf, boundary) + mpart, err := mpreader.NextPart() + if err != nil { + return fmt.Errorf("failed to get next part of multipart message: %w", err) + } + for err == nil { + mpdata, mperr := io.ReadAll(mpart) + if mperr != nil { + _ = mpart.Close() + return fmt.Errorf("failed to read multipart: %w", err) + } + + mpContentType, ok := mpart.Header[HeaderContentType.String()] + if !ok { + return fmt.Errorf("failed to get content-type from part") + } + mpContentTypeSplit := strings.Split(mpContentType[0], "; ") + p := m.newPart(ContentType(mpContentTypeSplit[0])) + parseEMLMultiPartCharset(mpContentTypeSplit, p) + + mpTransferEnc, ok := mpart.Header[HeaderContentTransferEnc.String()] + if !ok { + return fmt.Errorf("failed to get content-transfer-encoding from part") + } + switch { + case strings.EqualFold(mpTransferEnc[0], EncodingB64.String()): + if err := handleEMLMultiPartBase64Encoding(mpTransferEnc, mpdata, p); err != nil { + return fmt.Errorf("failed to handle multipart base64 transfer-encoding: %w", err) + } + } + + m.parts = append(m.parts, p) + mpart, err = mpreader.NextPart() + } + if !errors.Is(err, io.EOF) { + _ = mpart.Close() + return fmt.Errorf("failed to read multipart: %w", err) + } + return nil +} + +func parseEMLMultiPartCharset(mpContentTypeSplit []string, p *Part) { + if len(mpContentTypeSplit) > 1 && strings.HasPrefix(strings.ToLower(mpContentTypeSplit[1]), "charset=") { + valSplit := strings.Split(mpContentTypeSplit[1], "=") + if len(valSplit) > 1 { + p.SetCharset(Charset(valSplit[1])) + } + } +} + +func handleEMLMultiPartBase64Encoding(mpTransferEnc []string, mpdata []byte, p *Part) error { + p.SetEncoding(EncodingB64) + cont, err := base64.StdEncoding.DecodeString(string(mpdata)) + if err != nil { + return fmt.Errorf("failed to decode base64 part: %w", err) + } + p.SetContent(string(cont)) + return nil +} From 53566a93cd6e1bf0f451bd3aac4f4cf699a0bf01 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 9 Feb 2024 15:56:39 +0100 Subject: [PATCH 18/47] Add content type, charset processing and refactor encoding process Extended the settings for content type and charset from headers. Also, refactored the handling of encoding types - 'QP' and 'B64' - within the mail header and body parsing sections. The process of handling encoding for plain type mail specifically is now encapsulated in a new function, parseEMLBodyPlain. These changes enhance code readability, maintainability, and error handling efficiency. --- eml.go | 116 ++++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 85 insertions(+), 31 deletions(-) diff --git a/eml.go b/eml.go index 8333120..17c5635 100644 --- a/eml.go +++ b/eml.go @@ -111,6 +111,28 @@ func parseEMLHeaders(mh *nm.Header, m *Msg) error { HeaderXMailer, HeaderXMSMailPriority, HeaderXPriority, } + // Extract content type, charset and encoding first + if v := mh.Get(HeaderContentTransferEnc.String()); v != "" { + switch { + case strings.EqualFold(v, EncodingQP.String()): + m.SetEncoding(EncodingQP) + case strings.EqualFold(v, EncodingB64.String()): + m.SetEncoding(EncodingB64) + default: + m.SetEncoding(NoEncoding) + } + } + if v := mh.Get(HeaderContentType.String()); v != "" { + ct, cs := parseContentType(v) + if cs != "" { + m.SetCharset(Charset(cs)) + } + m.setEncoder() + if ct != "" { + m.SetGenHeader(HeaderContentType, ct) + } + } + // Extract address headers if v := mh.Get(HeaderFrom.String()); v != "" { if err := m.From(v); err != nil { @@ -156,6 +178,9 @@ func parseEMLHeaders(mh *nm.Header, m *Msg) error { for _, h := range commonHeaders { if v := mh.Get(h.String()); v != "" { m.SetGenHeader(h, v) + if strings.EqualFold(h.String(), "subject") { + fmt.Printf("SUBJECT: %s\n", m.GetGenHeader(HeaderSubject)[0]) + } } } @@ -173,43 +198,52 @@ func parseEMLBodyParts(pm *nm.Message, bodybuf *bytes.Buffer, m *Msg) error { m.SetCharset(Charset(v)) } - cte := pm.Header.Get(HeaderContentTransferEnc.String()) - switch strings.ToLower(mediatype) { - case TypeTextPlain.String(): - if strings.EqualFold(cte, NoEncoding.String()) { - m.SetEncoding(NoEncoding) - m.SetBodyString(TypeTextPlain, bodybuf.String()) - break + switch { + case strings.EqualFold(mediatype, TypeTextPlain.String()), + strings.EqualFold(mediatype, TypeTextHTML.String()): + if err := parseEMLBodyPlain(mediatype, pm, bodybuf, m); err != nil { + return fmt.Errorf("failed to parse plain body: %w", err) } - if strings.EqualFold(cte, EncodingQP.String()) { - m.SetEncoding(EncodingQP) - qpr := quotedprintable.NewReader(bodybuf) - qpbuf := bytes.Buffer{} - if _, err = qpbuf.ReadFrom(qpr); err != nil { - return fmt.Errorf("failed to read quoted-printable body: %w", err) - } - m.SetBodyString(TypeTextPlain, qpbuf.String()) - break - } - if strings.EqualFold(cte, EncodingB64.String()) { - m.SetEncoding(EncodingB64) - b64d := base64.NewDecoder(base64.StdEncoding, bodybuf) - b64buf := bytes.Buffer{} - if _, err = b64buf.ReadFrom(b64d); err != nil { - return fmt.Errorf("failed to read base64 body: %w", err) - } - m.SetBodyString(TypeTextPlain, b64buf.String()) - break - } - case TypeMultipartAlternative.String(): + case strings.EqualFold(mediatype, TypeMultipartAlternative.String()): if err := parseEMLMultipartAlternative(params, bodybuf, m); err != nil { - return fmt.Errorf("failed to parse multipart/alternative: %w", err) + return fmt.Errorf("failed to parse multipart/alternative body: %w", err) } default: } return nil } +// parseEMLBodyPlain parses the mail body of plain type mails +func parseEMLBodyPlain(mediatype string, pm *nm.Message, bodybuf *bytes.Buffer, m *Msg) error { + cte := pm.Header.Get(HeaderContentTransferEnc.String()) + if strings.EqualFold(cte, NoEncoding.String()) { + m.SetEncoding(NoEncoding) + m.SetBodyString(ContentType(mediatype), bodybuf.String()) + return nil + } + if strings.EqualFold(cte, EncodingQP.String()) { + m.SetEncoding(EncodingQP) + qpr := quotedprintable.NewReader(bodybuf) + qpbuf := bytes.Buffer{} + if _, err := qpbuf.ReadFrom(qpr); err != nil { + return fmt.Errorf("failed to read quoted-printable body: %w", err) + } + m.SetBodyString(ContentType(mediatype), qpbuf.String()) + return nil + } + if strings.EqualFold(cte, EncodingB64.String()) { + m.SetEncoding(EncodingB64) + b64d := base64.NewDecoder(base64.StdEncoding, bodybuf) + b64buf := bytes.Buffer{} + if _, err := b64buf.ReadFrom(b64d); err != nil { + return fmt.Errorf("failed to read base64 body: %w", err) + } + m.SetBodyString(ContentType(mediatype), b64buf.String()) + return nil + } + return fmt.Errorf("unsupported Content-Transfer-Encoding") +} + // parseEMLMultipartAlternative parses a multipart/alternative body part of a EML func parseEMLMultipartAlternative(params map[string]string, bodybuf *bytes.Buffer, m *Msg) error { boundary, ok := params["boundary"] @@ -242,7 +276,7 @@ func parseEMLMultipartAlternative(params map[string]string, bodybuf *bytes.Buffe } switch { case strings.EqualFold(mpTransferEnc[0], EncodingB64.String()): - if err := handleEMLMultiPartBase64Encoding(mpTransferEnc, mpdata, p); err != nil { + if err := handleEMLMultiPartBase64Encoding(mpdata, p); err != nil { return fmt.Errorf("failed to handle multipart base64 transfer-encoding: %w", err) } } @@ -257,6 +291,8 @@ func parseEMLMultipartAlternative(params map[string]string, bodybuf *bytes.Buffe return nil } +// parseEMLMultiPartCharset parses the Charset from a ContentType header and assigns it to a Part +// TODO: This might be redundant to parseContentType func parseEMLMultiPartCharset(mpContentTypeSplit []string, p *Part) { if len(mpContentTypeSplit) > 1 && strings.HasPrefix(strings.ToLower(mpContentTypeSplit[1]), "charset=") { valSplit := strings.Split(mpContentTypeSplit[1], "=") @@ -266,7 +302,8 @@ func parseEMLMultiPartCharset(mpContentTypeSplit []string, p *Part) { } } -func handleEMLMultiPartBase64Encoding(mpTransferEnc []string, mpdata []byte, p *Part) error { +// handleEMLMultiPartBase64Encoding sets the content body of a base64 encoded Part +func handleEMLMultiPartBase64Encoding(mpdata []byte, p *Part) error { p.SetEncoding(EncodingB64) cont, err := base64.StdEncoding.DecodeString(string(mpdata)) if err != nil { @@ -275,3 +312,20 @@ func handleEMLMultiPartBase64Encoding(mpTransferEnc []string, mpdata []byte, p * p.SetContent(string(cont)) return nil } + +// parseContentType parses the Content-Type header and returns the type and charse as +// separate string values +func parseContentType(cth string) (ct string, cs string) { + cts := strings.SplitN(cth, "; ", 2) + if len(cts) != 2 { + return + } + ct = cts[0] + if strings.HasPrefix(strings.ToLower(cts[1]), "charset=") { + css := strings.SplitN(cts[1], "=", 2) + if len(css) == 2 { + cs = css[1] + } + } + return +} From 59e85809f7e95f91fd2b07c3514601e4ca83ba3c Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 10 Feb 2024 13:22:38 +0100 Subject: [PATCH 19/47] Refactor multipart encoding handling and improve content parsing Refactored the processing of multipart encoding to be robust and easily maintainable. The changes include setting 'QP' encoding as default when the Content-Transfer-Encoding header is empty, accounting for the removal of this header by the standard Go multipart package. Also, parser functions for content type and charset are now independently handling the headers, replacing the split-string approach, thus improving efficiency and code readability. --- eml.go | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/eml.go b/eml.go index 17c5635..632102d 100644 --- a/eml.go +++ b/eml.go @@ -261,24 +261,33 @@ func parseEMLMultipartAlternative(params map[string]string, bodybuf *bytes.Buffe _ = mpart.Close() return fmt.Errorf("failed to read multipart: %w", err) } + fmt.Printf("CTE: %+v", params) mpContentType, ok := mpart.Header[HeaderContentType.String()] if !ok { return fmt.Errorf("failed to get content-type from part") } - mpContentTypeSplit := strings.Split(mpContentType[0], "; ") - p := m.newPart(ContentType(mpContentTypeSplit[0])) - parseEMLMultiPartCharset(mpContentTypeSplit, p) + conType, charSet := parseContentType(mpContentType[0]) + p := m.newPart(ContentType(conType)) + p.SetCharset(Charset(charSet)) mpTransferEnc, ok := mpart.Header[HeaderContentTransferEnc.String()] if !ok { - return fmt.Errorf("failed to get content-transfer-encoding from part") + // If CTE is empty we can assume that it's a quoted-printable CTE since the + // GO stdlib multipart packages deletes that header + // See: https://cs.opensource.google/go/go/+/refs/tags/go1.22.0:src/mime/multipart/multipart.go;l=161 + mpTransferEnc = []string{EncodingQP.String()} } + switch { case strings.EqualFold(mpTransferEnc[0], EncodingB64.String()): if err := handleEMLMultiPartBase64Encoding(mpdata, p); err != nil { return fmt.Errorf("failed to handle multipart base64 transfer-encoding: %w", err) } + case strings.EqualFold(mpTransferEnc[0], EncodingQP.String()): + p.SetContent(string(mpdata)) + default: + return fmt.Errorf("unsupported Content-Transfer-Encoding") } m.parts = append(m.parts, p) @@ -291,17 +300,6 @@ func parseEMLMultipartAlternative(params map[string]string, bodybuf *bytes.Buffe return nil } -// parseEMLMultiPartCharset parses the Charset from a ContentType header and assigns it to a Part -// TODO: This might be redundant to parseContentType -func parseEMLMultiPartCharset(mpContentTypeSplit []string, p *Part) { - if len(mpContentTypeSplit) > 1 && strings.HasPrefix(strings.ToLower(mpContentTypeSplit[1]), "charset=") { - valSplit := strings.Split(mpContentTypeSplit[1], "=") - if len(valSplit) > 1 { - p.SetCharset(Charset(valSplit[1])) - } - } -} - // handleEMLMultiPartBase64Encoding sets the content body of a base64 encoded Part func handleEMLMultiPartBase64Encoding(mpdata []byte, p *Part) error { p.SetEncoding(EncodingB64) From 3facbde703050ac84b047d7b2332b51e03895c6d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 10 Feb 2024 13:36:42 +0100 Subject: [PATCH 20/47] Add new content types and refactor message writer Introduced "multipart/mixed" and "multipart/related" content types in encoding.go and updated msgwriter.go to accommodate these. Adjustments made in related tests for these new types. Additionally, removed unnecessary print statements and improved multipart alternative parsing in eml.go. --- eml.go | 7 ++----- encoding.go | 2 ++ encoding_test.go | 12 ++++++++++++ msgwriter.go | 4 ++-- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/eml.go b/eml.go index 632102d..bc96c0a 100644 --- a/eml.go +++ b/eml.go @@ -178,9 +178,6 @@ func parseEMLHeaders(mh *nm.Header, m *Msg) error { for _, h := range commonHeaders { if v := mh.Get(h.String()); v != "" { m.SetGenHeader(h, v) - if strings.EqualFold(h.String(), "subject") { - fmt.Printf("SUBJECT: %s\n", m.GetGenHeader(HeaderSubject)[0]) - } } } @@ -204,7 +201,8 @@ func parseEMLBodyParts(pm *nm.Message, bodybuf *bytes.Buffer, m *Msg) error { if err := parseEMLBodyPlain(mediatype, pm, bodybuf, m); err != nil { return fmt.Errorf("failed to parse plain body: %w", err) } - case strings.EqualFold(mediatype, TypeMultipartAlternative.String()): + case strings.EqualFold(mediatype, TypeMultipartAlternative.String()), + strings.EqualFold(mediatype, "multipart/mixed"): if err := parseEMLMultipartAlternative(params, bodybuf, m); err != nil { return fmt.Errorf("failed to parse multipart/alternative body: %w", err) } @@ -261,7 +259,6 @@ func parseEMLMultipartAlternative(params map[string]string, bodybuf *bytes.Buffe _ = mpart.Close() return fmt.Errorf("failed to read multipart: %w", err) } - fmt.Printf("CTE: %+v", params) mpContentType, ok := mpart.Header[HeaderContentType.String()] if !ok { diff --git a/encoding.go b/encoding.go index 39c58e3..2187e5f 100644 --- a/encoding.go +++ b/encoding.go @@ -140,6 +140,8 @@ const ( const ( TypeAppOctetStream ContentType = "application/octet-stream" TypeMultipartAlternative ContentType = "multipart/alternative" + TypeMultipartMixed ContentType = "multipart/mixed" + TypeMultipartRelated ContentType = "multipart/related" TypePGPSignature ContentType = "application/pgp-signature" TypePGPEncrypted ContentType = "application/pgp-encrypted" TypeTextHTML ContentType = "text/html" diff --git a/encoding_test.go b/encoding_test.go index 4a3affc..86f686a 100644 --- a/encoding_test.go +++ b/encoding_test.go @@ -40,6 +40,18 @@ func TestContentType_String(t *testing.T) { "ContentType: application/octet-stream", TypeAppOctetStream, "application/octet-stream", }, + { + "ContentType: multipart/alternative", TypeMultipartAlternative, + "multipart/alternative", + }, + { + "ContentType: multipart/mixed", TypeMultipartMixed, + "multipart/mixed", + }, + { + "ContentType: multipart/related", TypeMultipartRelated, + "multipart/related", + }, { "ContentType: application/pgp-signature", TypePGPSignature, "application/pgp-signature", diff --git a/msgwriter.go b/msgwriter.go index b2ff118..377435e 100644 --- a/msgwriter.go +++ b/msgwriter.go @@ -89,11 +89,11 @@ func (mw *msgWriter) writeMsg(m *Msg) { } if m.hasMixed() { - mw.startMP("mixed", m.boundary) + mw.startMP(MIMEMixed, m.boundary) mw.writeString(DoubleNewLine) } if m.hasRelated() { - mw.startMP("related", m.boundary) + mw.startMP(MIMERelated, m.boundary) mw.writeString(DoubleNewLine) } if m.hasAlt() { From ee3283b00b32682d6dfc4ee8cbd27ef46f4fb9ef Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 16 May 2024 15:17:15 +0200 Subject: [PATCH 21/47] Resolve merge conflict --- msgwriter.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/msgwriter.go b/msgwriter.go index 377435e..b2ff118 100644 --- a/msgwriter.go +++ b/msgwriter.go @@ -89,11 +89,11 @@ func (mw *msgWriter) writeMsg(m *Msg) { } if m.hasMixed() { - mw.startMP(MIMEMixed, m.boundary) + mw.startMP("mixed", m.boundary) mw.writeString(DoubleNewLine) } if m.hasRelated() { - mw.startMP(MIMERelated, m.boundary) + mw.startMP("related", m.boundary) mw.writeString(DoubleNewLine) } if m.hasAlt() { From 4ef24a5234f2ae0d72a2d0a059e531f60a2fb57a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 16 May 2024 15:18:26 +0200 Subject: [PATCH 22/47] Revert after successful merge --- msgwriter.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/msgwriter.go b/msgwriter.go index 08073db..ff1b47b 100644 --- a/msgwriter.go +++ b/msgwriter.go @@ -89,11 +89,11 @@ func (mw *msgWriter) writeMsg(msg *Msg) { } if msg.hasMixed() { - mw.startMP("mixed", msg.boundary) + mw.startMP(MIMEMixed, msg.boundary) mw.writeString(DoubleNewLine) } if msg.hasRelated() { - mw.startMP("related", msg.boundary) + mw.startMP(MIMERelated, msg.boundary) mw.writeString(DoubleNewLine) } if msg.hasAlt() { From 481fc1d48c63442f00793941677b4717543e6067 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 27 May 2024 10:59:38 +0200 Subject: [PATCH 23/47] Refactor variable names in eml.go for clarity Variable names in eml.go have been refactored for better readability and understanding. Shortened abbreviations have been expanded into meaningful names, and complex object names have been made simpler, making it easier to understand their role within the codebase. Cooperative variable names will improve maintainability and ease future development. This is a follow up to #179 which didn't consider this branch. --- eml.go | 240 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 120 insertions(+), 120 deletions(-) diff --git a/eml.go b/eml.go index bc96c0a..9db20ab 100644 --- a/eml.go +++ b/eml.go @@ -13,97 +13,97 @@ import ( "mime" "mime/multipart" "mime/quotedprintable" - nm "net/mail" + netmail "net/mail" "os" "strings" ) // EMLToMsgFromString will parse a given EML string and returns a pre-filled Msg pointer -func EMLToMsgFromString(es string) (*Msg, error) { - eb := bytes.NewBufferString(es) +func EMLToMsgFromString(emlString string) (*Msg, error) { + eb := bytes.NewBufferString(emlString) return EMLToMsgFromReader(eb) } // EMLToMsgFromReader will parse a reader that holds EML content and returns a pre-filled // Msg pointer -func EMLToMsgFromReader(r io.Reader) (*Msg, error) { - m := &Msg{ - addrHeader: make(map[AddrHeader][]*nm.Address), +func EMLToMsgFromReader(reader io.Reader) (*Msg, error) { + msg := &Msg{ + addrHeader: make(map[AddrHeader][]*netmail.Address), genHeader: make(map[Header][]string), preformHeader: make(map[Header]string), mimever: MIME10, } - pm, bodybuf, err := readEMLFromReader(r) - if err != nil || pm == nil { - return m, fmt.Errorf("failed to parse EML from reader: %w", err) + parsedMsg, bodybuf, err := readEMLFromReader(reader) + if err != nil || parsedMsg == nil { + return msg, fmt.Errorf("failed to parse EML from reader: %w", err) } - if err = parseEMLHeaders(&pm.Header, m); err != nil { - return m, fmt.Errorf("failed to parse EML headers: %w", err) + if err = parseEMLHeaders(&parsedMsg.Header, msg); err != nil { + return msg, fmt.Errorf("failed to parse EML headers: %w", err) } - if err = parseEMLBodyParts(pm, bodybuf, m); err != nil { - return m, fmt.Errorf("failed to parse EML body parts: %w", err) + if err = parseEMLBodyParts(parsedMsg, bodybuf, msg); err != nil { + return msg, fmt.Errorf("failed to parse EML body parts: %w", err) } - return m, nil + return msg, nil } // EMLToMsgFromFile will open and parse a .eml file at a provided file path and returns a // pre-filled Msg pointer -func EMLToMsgFromFile(fp string) (*Msg, error) { - m := &Msg{ - addrHeader: make(map[AddrHeader][]*nm.Address), +func EMLToMsgFromFile(filePath string) (*Msg, error) { + msg := &Msg{ + addrHeader: make(map[AddrHeader][]*netmail.Address), genHeader: make(map[Header][]string), preformHeader: make(map[Header]string), mimever: MIME10, } - pm, bodybuf, err := readEML(fp) - if err != nil || pm == nil { - return m, fmt.Errorf("failed to parse EML file: %w", err) + parsedMsg, bodybuf, err := readEML(filePath) + if err != nil || parsedMsg == nil { + return msg, fmt.Errorf("failed to parse EML file: %w", err) } - if err = parseEMLHeaders(&pm.Header, m); err != nil { - return m, fmt.Errorf("failed to parse EML headers: %w", err) + if err = parseEMLHeaders(&parsedMsg.Header, msg); err != nil { + return msg, fmt.Errorf("failed to parse EML headers: %w", err) } - if err = parseEMLBodyParts(pm, bodybuf, m); err != nil { - return m, fmt.Errorf("failed to parse EML body parts: %w", err) + if err = parseEMLBodyParts(parsedMsg, bodybuf, msg); err != nil { + return msg, fmt.Errorf("failed to parse EML body parts: %w", err) } - return m, nil + return msg, nil } // readEML opens an EML file and uses net/mail to parse the header and body -func readEML(fp string) (*nm.Message, *bytes.Buffer, error) { - fh, err := os.Open(fp) +func readEML(filePath string) (*netmail.Message, *bytes.Buffer, error) { + fileHandle, err := os.Open(filePath) if err != nil { return nil, nil, fmt.Errorf("failed to open EML file: %w", err) } defer func() { - _ = fh.Close() + _ = fileHandle.Close() }() - return readEMLFromReader(fh) + return readEMLFromReader(fileHandle) } // readEMLFromReader uses net/mail to parse the header and body from a given io.Reader -func readEMLFromReader(r io.Reader) (*nm.Message, *bytes.Buffer, error) { - pm, err := nm.ReadMessage(r) +func readEMLFromReader(reader io.Reader) (*netmail.Message, *bytes.Buffer, error) { + parsedMsg, err := netmail.ReadMessage(reader) if err != nil { - return pm, nil, fmt.Errorf("failed to parse EML: %w", err) + return parsedMsg, nil, fmt.Errorf("failed to parse EML: %w", err) } buf := bytes.Buffer{} - if _, err = buf.ReadFrom(pm.Body); err != nil { + if _, err = buf.ReadFrom(parsedMsg.Body); err != nil { return nil, nil, err } - return pm, &buf, nil + return parsedMsg, &buf, nil } // parseEMLHeaders will check the EML headers for the most common headers and set the // according settings in the Msg -func parseEMLHeaders(mh *nm.Header, m *Msg) error { +func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error { commonHeaders := []Header{ HeaderContentType, HeaderImportance, HeaderInReplyTo, HeaderListUnsubscribe, HeaderListUnsubscribePost, HeaderMessageID, HeaderMIMEVersion, HeaderOrganization, @@ -112,72 +112,72 @@ func parseEMLHeaders(mh *nm.Header, m *Msg) error { } // Extract content type, charset and encoding first - if v := mh.Get(HeaderContentTransferEnc.String()); v != "" { + if value := mailHeader.Get(HeaderContentTransferEnc.String()); value != "" { switch { - case strings.EqualFold(v, EncodingQP.String()): - m.SetEncoding(EncodingQP) - case strings.EqualFold(v, EncodingB64.String()): - m.SetEncoding(EncodingB64) + case strings.EqualFold(value, EncodingQP.String()): + msg.SetEncoding(EncodingQP) + case strings.EqualFold(value, EncodingB64.String()): + msg.SetEncoding(EncodingB64) default: - m.SetEncoding(NoEncoding) + msg.SetEncoding(NoEncoding) } } - if v := mh.Get(HeaderContentType.String()); v != "" { - ct, cs := parseContentType(v) - if cs != "" { - m.SetCharset(Charset(cs)) + if value := mailHeader.Get(HeaderContentType.String()); value != "" { + contentType, charSet := parseContentType(value) + if charSet != "" { + msg.SetCharset(Charset(charSet)) } - m.setEncoder() - if ct != "" { - m.SetGenHeader(HeaderContentType, ct) + msg.setEncoder() + if contentType != "" { + msg.SetGenHeader(HeaderContentType, contentType) } } // Extract address headers - if v := mh.Get(HeaderFrom.String()); v != "" { - if err := m.From(v); err != nil { + if value := mailHeader.Get(HeaderFrom.String()); value != "" { + if err := msg.From(value); err != nil { return fmt.Errorf(`failed to parse %q header: %w`, HeaderFrom, err) } } - ahl := map[AddrHeader]func(...string) error{ - HeaderTo: m.To, - HeaderCc: m.Cc, - HeaderBcc: m.Bcc, + addrHeaders := map[AddrHeader]func(...string) error{ + HeaderTo: msg.To, + HeaderCc: msg.Cc, + HeaderBcc: msg.Bcc, } - for h, f := range ahl { - if v := mh.Get(h.String()); v != "" { - var als []string - pal, err := nm.ParseAddressList(v) + for addrHeader, addrFunc := range addrHeaders { + if v := mailHeader.Get(addrHeader.String()); v != "" { + var addrStrings []string + parsedAddrs, err := netmail.ParseAddressList(v) if err != nil { return fmt.Errorf(`failed to parse address list: %w`, err) } - for _, a := range pal { - als = append(als, a.String()) + for _, addr := range parsedAddrs { + addrStrings = append(addrStrings, addr.String()) } - if err := f(als...); err != nil { + if err = addrFunc(addrStrings...); err != nil { return fmt.Errorf(`failed to parse %q header: %w`, HeaderTo, err) } } } // Extract date from message - d, err := mh.Date() + date, err := mailHeader.Date() if err != nil { switch { - case errors.Is(err, nm.ErrHeaderNotPresent): - m.SetDate() + case errors.Is(err, netmail.ErrHeaderNotPresent): + msg.SetDate() default: return fmt.Errorf("failed to parse EML date: %w", err) } } if err == nil { - m.SetDateWithValue(d) + msg.SetDateWithValue(date) } // Extract common headers - for _, h := range commonHeaders { - if v := mh.Get(h.String()); v != "" { - m.SetGenHeader(h, v) + for _, header := range commonHeaders { + if value := mailHeader.Get(header.String()); value != "" { + msg.SetGenHeader(header, value) } } @@ -185,25 +185,25 @@ func parseEMLHeaders(mh *nm.Header, m *Msg) error { } // parseEMLBodyParts parses the body of a EML based on the different content types and encodings -func parseEMLBodyParts(pm *nm.Message, bodybuf *bytes.Buffer, m *Msg) error { +func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error { // Extract the transfer encoding of the body - mediatype, params, err := mime.ParseMediaType(pm.Header.Get(HeaderContentType.String())) + mediatype, params, err := mime.ParseMediaType(parsedMsg.Header.Get(HeaderContentType.String())) if err != nil { return fmt.Errorf("failed to extract content type: %w", err) } - if v, ok := params["charset"]; ok { - m.SetCharset(Charset(v)) + if value, ok := params["charset"]; ok { + msg.SetCharset(Charset(value)) } switch { case strings.EqualFold(mediatype, TypeTextPlain.String()), strings.EqualFold(mediatype, TypeTextHTML.String()): - if err := parseEMLBodyPlain(mediatype, pm, bodybuf, m); err != nil { + if err = parseEMLBodyPlain(mediatype, parsedMsg, bodybuf, msg); err != nil { return fmt.Errorf("failed to parse plain body: %w", err) } case strings.EqualFold(mediatype, TypeMultipartAlternative.String()), strings.EqualFold(mediatype, "multipart/mixed"): - if err := parseEMLMultipartAlternative(params, bodybuf, m); err != nil { + if err = parseEMLMultipartAlternative(params, bodybuf, msg); err != nil { return fmt.Errorf("failed to parse multipart/alternative body: %w", err) } default: @@ -212,114 +212,114 @@ func parseEMLBodyParts(pm *nm.Message, bodybuf *bytes.Buffer, m *Msg) error { } // parseEMLBodyPlain parses the mail body of plain type mails -func parseEMLBodyPlain(mediatype string, pm *nm.Message, bodybuf *bytes.Buffer, m *Msg) error { - cte := pm.Header.Get(HeaderContentTransferEnc.String()) - if strings.EqualFold(cte, NoEncoding.String()) { - m.SetEncoding(NoEncoding) - m.SetBodyString(ContentType(mediatype), bodybuf.String()) +func parseEMLBodyPlain(mediatype string, parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error { + contentTransferEnc := parsedMsg.Header.Get(HeaderContentTransferEnc.String()) + if strings.EqualFold(contentTransferEnc, NoEncoding.String()) { + msg.SetEncoding(NoEncoding) + msg.SetBodyString(ContentType(mediatype), bodybuf.String()) return nil } - if strings.EqualFold(cte, EncodingQP.String()) { - m.SetEncoding(EncodingQP) - qpr := quotedprintable.NewReader(bodybuf) - qpbuf := bytes.Buffer{} - if _, err := qpbuf.ReadFrom(qpr); err != nil { + if strings.EqualFold(contentTransferEnc, EncodingQP.String()) { + msg.SetEncoding(EncodingQP) + qpReader := quotedprintable.NewReader(bodybuf) + qpBuffer := bytes.Buffer{} + if _, err := qpBuffer.ReadFrom(qpReader); err != nil { return fmt.Errorf("failed to read quoted-printable body: %w", err) } - m.SetBodyString(ContentType(mediatype), qpbuf.String()) + msg.SetBodyString(ContentType(mediatype), qpBuffer.String()) return nil } - if strings.EqualFold(cte, EncodingB64.String()) { - m.SetEncoding(EncodingB64) - b64d := base64.NewDecoder(base64.StdEncoding, bodybuf) - b64buf := bytes.Buffer{} - if _, err := b64buf.ReadFrom(b64d); err != nil { + if strings.EqualFold(contentTransferEnc, EncodingB64.String()) { + msg.SetEncoding(EncodingB64) + b64Decoder := base64.NewDecoder(base64.StdEncoding, bodybuf) + b64Buffer := bytes.Buffer{} + if _, err := b64Buffer.ReadFrom(b64Decoder); err != nil { return fmt.Errorf("failed to read base64 body: %w", err) } - m.SetBodyString(ContentType(mediatype), b64buf.String()) + msg.SetBodyString(ContentType(mediatype), b64Buffer.String()) return nil } return fmt.Errorf("unsupported Content-Transfer-Encoding") } // parseEMLMultipartAlternative parses a multipart/alternative body part of a EML -func parseEMLMultipartAlternative(params map[string]string, bodybuf *bytes.Buffer, m *Msg) error { +func parseEMLMultipartAlternative(params map[string]string, bodybuf *bytes.Buffer, msg *Msg) error { boundary, ok := params["boundary"] if !ok { return fmt.Errorf("no boundary tag found in multipart body") } - mpreader := multipart.NewReader(bodybuf, boundary) - mpart, err := mpreader.NextPart() + multipartReader := multipart.NewReader(bodybuf, boundary) + multiPart, err := multipartReader.NextPart() if err != nil { return fmt.Errorf("failed to get next part of multipart message: %w", err) } for err == nil { - mpdata, mperr := io.ReadAll(mpart) + multiPartData, mperr := io.ReadAll(multiPart) if mperr != nil { - _ = mpart.Close() + _ = multiPart.Close() return fmt.Errorf("failed to read multipart: %w", err) } - mpContentType, ok := mpart.Header[HeaderContentType.String()] + multiPartContentType, ok := multiPart.Header[HeaderContentType.String()] if !ok { return fmt.Errorf("failed to get content-type from part") } - conType, charSet := parseContentType(mpContentType[0]) - p := m.newPart(ContentType(conType)) + contentType, charSet := parseContentType(multiPartContentType[0]) + p := msg.newPart(ContentType(contentType)) p.SetCharset(Charset(charSet)) - mpTransferEnc, ok := mpart.Header[HeaderContentTransferEnc.String()] + mutliPartTransferEnc, ok := multiPart.Header[HeaderContentTransferEnc.String()] if !ok { // If CTE is empty we can assume that it's a quoted-printable CTE since the // GO stdlib multipart packages deletes that header // See: https://cs.opensource.google/go/go/+/refs/tags/go1.22.0:src/mime/multipart/multipart.go;l=161 - mpTransferEnc = []string{EncodingQP.String()} + mutliPartTransferEnc = []string{EncodingQP.String()} } switch { - case strings.EqualFold(mpTransferEnc[0], EncodingB64.String()): - if err := handleEMLMultiPartBase64Encoding(mpdata, p); err != nil { + case strings.EqualFold(mutliPartTransferEnc[0], EncodingB64.String()): + if err := handleEMLMultiPartBase64Encoding(multiPartData, p); err != nil { return fmt.Errorf("failed to handle multipart base64 transfer-encoding: %w", err) } - case strings.EqualFold(mpTransferEnc[0], EncodingQP.String()): - p.SetContent(string(mpdata)) + case strings.EqualFold(mutliPartTransferEnc[0], EncodingQP.String()): + p.SetContent(string(multiPartData)) default: return fmt.Errorf("unsupported Content-Transfer-Encoding") } - m.parts = append(m.parts, p) - mpart, err = mpreader.NextPart() + msg.parts = append(msg.parts, p) + multiPart, err = multipartReader.NextPart() } if !errors.Is(err, io.EOF) { - _ = mpart.Close() + _ = multiPart.Close() return fmt.Errorf("failed to read multipart: %w", err) } return nil } // handleEMLMultiPartBase64Encoding sets the content body of a base64 encoded Part -func handleEMLMultiPartBase64Encoding(mpdata []byte, p *Part) error { - p.SetEncoding(EncodingB64) - cont, err := base64.StdEncoding.DecodeString(string(mpdata)) +func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error { + part.SetEncoding(EncodingB64) + content, err := base64.StdEncoding.DecodeString(string(multiPartData)) if err != nil { return fmt.Errorf("failed to decode base64 part: %w", err) } - p.SetContent(string(cont)) + part.SetContent(string(content)) return nil } // parseContentType parses the Content-Type header and returns the type and charse as // separate string values -func parseContentType(cth string) (ct string, cs string) { - cts := strings.SplitN(cth, "; ", 2) - if len(cts) != 2 { +func parseContentType(contentTypeHeader string) (contentType string, charSet string) { + contentTypeSplit := strings.SplitN(contentTypeHeader, "; ", 2) + if len(contentTypeSplit) != 2 { return } - ct = cts[0] - if strings.HasPrefix(strings.ToLower(cts[1]), "charset=") { - css := strings.SplitN(cts[1], "=", 2) - if len(css) == 2 { - cs = css[1] + contentType = contentTypeSplit[0] + if strings.HasPrefix(strings.ToLower(contentTypeSplit[1]), "charset=") { + charSetSplit := strings.SplitN(contentTypeSplit[1], "=", 2) + if len(charSetSplit) == 2 { + charSet = charSetSplit[1] } } return From 04c41a1f103b78974599111c93b5517800fc0c66 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 19 Jun 2024 10:42:29 +0200 Subject: [PATCH 24/47] Add EML message parsing tests Introduced a new test, `TestEMLToMsgFromFile`, to validate the functions responsible for EML message parsing. This complements the existing `EMLToMsgFromString` test, holding them accountable for subject and encoding accuracy. Also, a temporary directory is now created for testing File-related operations in isolation. --- eml_test.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 6 deletions(-) diff --git a/eml_test.go b/eml_test.go index ca58e43..89f0550 100644 --- a/eml_test.go +++ b/eml_test.go @@ -5,6 +5,8 @@ package mail import ( + "fmt" + "os" "strings" "testing" "time" @@ -143,15 +145,68 @@ func TestEMLToMsgFromString(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - m, err := EMLToMsgFromString(tt.eml) + msg, err := EMLToMsgFromString(tt.eml) if err != nil { - t.Errorf("failed to parse EML: %s", err) + t.Errorf("failed to parse EML: %subject", err) } - if m.Encoding() != tt.enc { - t.Errorf("EMLToMsgFromString failed: expected encoding: %s, but got: %s", tt.enc, m.Encoding()) + if msg.Encoding() != tt.enc { + t.Errorf("EMLToMsgFromString failed: expected encoding: %subject, but got: %subject", tt.enc, msg.Encoding()) } - if s := m.GetGenHeader(HeaderSubject); len(s) > 0 && !strings.EqualFold(s[0], tt.sub) { - t.Errorf("EMLToMsgFromString failed: expected subject: %s, but got: %s", tt.sub, s[0]) + if subject := msg.GetGenHeader(HeaderSubject); len(subject) > 0 && !strings.EqualFold(subject[0], tt.sub) { + t.Errorf("EMLToMsgFromString failed: expected subject: %subject, but got: %subject", + tt.sub, subject[0]) + } + }) + } +} + +func TestEMLToMsgFromFile(t *testing.T) { + tests := []struct { + name string + eml string + enc string + sub string + }{ + { + "Plain text no encoding", exampleMailPlainNoEnc, "8bit", + "Example mail // plain text without encoding", + }, + { + "Plain text quoted-printable", exampleMailPlainQP, "quoted-printable", + "Example mail // plain text quoted-printable", + }, + { + "Plain text base64", exampleMailPlainB64, "base64", + "Example mail // plain text base64", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tempDir, err := os.MkdirTemp("", fmt.Sprintf("*-%s", tt.name)) + if err != nil { + t.Errorf("failed to create temp dir: %s", err) + return + } + defer func() { + if err = os.RemoveAll(tempDir); err != nil { + t.Error("failed to remove temp dir:", err) + } + }() + err = os.WriteFile(fmt.Sprintf("%s/%s.eml", tempDir, tt.name), []byte(tt.eml), 0666) + if err != nil { + t.Error("failed to write mail to temp file:", err) + return + } + msg, err := EMLToMsgFromFile(fmt.Sprintf("%s/%s.eml", tempDir, tt.name)) + if err != nil { + t.Errorf("failed to parse EML: %subject", err) + } + if msg.Encoding() != tt.enc { + t.Errorf("EMLToMsgFromString failed: expected encoding: %subject, but got: %subject", tt.enc, msg.Encoding()) + } + if subject := msg.GetGenHeader(HeaderSubject); len(subject) > 0 && !strings.EqualFold(subject[0], tt.sub) { + t.Errorf("EMLToMsgFromString failed: expected subject: %subject, but got: %subject", + tt.sub, subject[0]) } }) } From 29305675d67e2a9c2b9bc39b1cc5995fb3d6e768 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 19 Jun 2024 10:52:09 +0200 Subject: [PATCH 25/47] Refactor EML encoding and content-type parsing to separate functions The commit includes extraction of blocks of code related to EML message encoding and content-type parsing into their own separate functions. By doing so, it improves code readability and maintainability. --- eml.go | 50 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/eml.go b/eml.go index 9db20ab..b59a2ca 100644 --- a/eml.go +++ b/eml.go @@ -112,26 +112,8 @@ func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error { } // Extract content type, charset and encoding first - if value := mailHeader.Get(HeaderContentTransferEnc.String()); value != "" { - switch { - case strings.EqualFold(value, EncodingQP.String()): - msg.SetEncoding(EncodingQP) - case strings.EqualFold(value, EncodingB64.String()): - msg.SetEncoding(EncodingB64) - default: - msg.SetEncoding(NoEncoding) - } - } - if value := mailHeader.Get(HeaderContentType.String()); value != "" { - contentType, charSet := parseContentType(value) - if charSet != "" { - msg.SetCharset(Charset(charSet)) - } - msg.setEncoder() - if contentType != "" { - msg.SetGenHeader(HeaderContentType, contentType) - } - } + parseEMLEncoding(mailHeader, msg) + parseEMLContentTypeCharset(mailHeader, msg) // Extract address headers if value := mailHeader.Get(HeaderFrom.String()); value != "" { @@ -297,6 +279,34 @@ func parseEMLMultipartAlternative(params map[string]string, bodybuf *bytes.Buffe return nil } +// parseEMLEncoding parses and determines the encoding of the message +func parseEMLEncoding(mailHeader *netmail.Header, msg *Msg) { + if value := mailHeader.Get(HeaderContentTransferEnc.String()); value != "" { + switch { + case strings.EqualFold(value, EncodingQP.String()): + msg.SetEncoding(EncodingQP) + case strings.EqualFold(value, EncodingB64.String()): + msg.SetEncoding(EncodingB64) + default: + msg.SetEncoding(NoEncoding) + } + } +} + +// parseEMLContentTypeCharset parses and determines the charset and content type of the message +func parseEMLContentTypeCharset(mailHeader *netmail.Header, msg *Msg) { + if value := mailHeader.Get(HeaderContentType.String()); value != "" { + contentType, charSet := parseContentType(value) + if charSet != "" { + msg.SetCharset(Charset(charSet)) + } + msg.setEncoder() + if contentType != "" { + msg.SetGenHeader(HeaderContentType, contentType) + } + } +} + // handleEMLMultiPartBase64Encoding sets the content body of a base64 encoded Part func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error { part.SetEncoding(EncodingB64) From 85c07c22cc380fea67131cda5703f3c2a011cb36 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 19 Jun 2024 13:57:00 +0200 Subject: [PATCH 26/47] Refactor multipart parsing in eml.go The code is refactored to improve multipart parsing in EML. The `parseEMLMultipartAlternative` function is updated to `parseEMLMultipart` for more general utilization. This involves iterating through the parts of a multipart message until content disposition is found and appended. A new function `parseMultiPartHeader` is introduced to parse multipart header and handle charset more sensibly. --- eml.go | 60 ++++++++++++++++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/eml.go b/eml.go index b59a2ca..ed52d02 100644 --- a/eml.go +++ b/eml.go @@ -184,8 +184,8 @@ func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *M return fmt.Errorf("failed to parse plain body: %w", err) } case strings.EqualFold(mediatype, TypeMultipartAlternative.String()), - strings.EqualFold(mediatype, "multipart/mixed"): - if err = parseEMLMultipartAlternative(params, bodybuf, msg); err != nil { + strings.EqualFold(mediatype, TypeMultipartMixed.String()): + if err = parseEMLMultipart(params, bodybuf, msg); err != nil { return fmt.Errorf("failed to parse multipart/alternative body: %w", err) } default: @@ -224,8 +224,8 @@ func parseEMLBodyPlain(mediatype string, parsedMsg *netmail.Message, bodybuf *by return fmt.Errorf("unsupported Content-Transfer-Encoding") } -// parseEMLMultipartAlternative parses a multipart/alternative body part of a EML -func parseEMLMultipartAlternative(params map[string]string, bodybuf *bytes.Buffer, msg *Msg) error { +// parseEMLMultipart parses a multipart body part of a EML +func parseEMLMultipart(params map[string]string, bodybuf *bytes.Buffer, msg *Msg) error { boundary, ok := params["boundary"] if !ok { return fmt.Errorf("no boundary tag found in multipart body") @@ -236,6 +236,16 @@ func parseEMLMultipartAlternative(params map[string]string, bodybuf *bytes.Buffe return fmt.Errorf("failed to get next part of multipart message: %w", err) } for err == nil { + if contentDisposition, ok := multiPart.Header[HeaderContentDisposition.String()]; ok { + cdType, optional := parseMultiPartHeader(contentDisposition[0]) + fmt.Println("CTD:", cdType) + fmt.Printf("optional: %+v\n", optional) + if err = msg.AttachReader("", multiPart); err != nil { + return fmt.Errorf("failed to attach multipart body: %w", err) + } + return nil + } + multiPartData, mperr := io.ReadAll(multiPart) if mperr != nil { _ = multiPart.Close() @@ -246,9 +256,11 @@ func parseEMLMultipartAlternative(params map[string]string, bodybuf *bytes.Buffe if !ok { return fmt.Errorf("failed to get content-type from part") } - contentType, charSet := parseContentType(multiPartContentType[0]) - p := msg.newPart(ContentType(contentType)) - p.SetCharset(Charset(charSet)) + contentType, optional := parseMultiPartHeader(multiPartContentType[0]) + part := msg.newPart(ContentType(contentType)) + if charset, ok := optional["charset"]; ok { + part.SetCharset(Charset(charset)) + } mutliPartTransferEnc, ok := multiPart.Header[HeaderContentTransferEnc.String()] if !ok { @@ -260,16 +272,16 @@ func parseEMLMultipartAlternative(params map[string]string, bodybuf *bytes.Buffe switch { case strings.EqualFold(mutliPartTransferEnc[0], EncodingB64.String()): - if err := handleEMLMultiPartBase64Encoding(multiPartData, p); err != nil { + if err := handleEMLMultiPartBase64Encoding(multiPartData, part); err != nil { return fmt.Errorf("failed to handle multipart base64 transfer-encoding: %w", err) } case strings.EqualFold(mutliPartTransferEnc[0], EncodingQP.String()): - p.SetContent(string(multiPartData)) + part.SetContent(string(multiPartData)) default: return fmt.Errorf("unsupported Content-Transfer-Encoding") } - msg.parts = append(msg.parts, p) + msg.parts = append(msg.parts, part) multiPart, err = multipartReader.NextPart() } if !errors.Is(err, io.EOF) { @@ -296,9 +308,9 @@ func parseEMLEncoding(mailHeader *netmail.Header, msg *Msg) { // parseEMLContentTypeCharset parses and determines the charset and content type of the message func parseEMLContentTypeCharset(mailHeader *netmail.Header, msg *Msg) { if value := mailHeader.Get(HeaderContentType.String()); value != "" { - contentType, charSet := parseContentType(value) - if charSet != "" { - msg.SetCharset(Charset(charSet)) + contentType, optional := parseMultiPartHeader(value) + if charset, ok := optional["charset"]; ok { + msg.SetCharset(Charset(charset)) } msg.setEncoder() if contentType != "" { @@ -318,18 +330,16 @@ func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error { return nil } -// parseContentType parses the Content-Type header and returns the type and charse as -// separate string values -func parseContentType(contentTypeHeader string) (contentType string, charSet string) { - contentTypeSplit := strings.SplitN(contentTypeHeader, "; ", 2) - if len(contentTypeSplit) != 2 { - return - } - contentType = contentTypeSplit[0] - if strings.HasPrefix(strings.ToLower(contentTypeSplit[1]), "charset=") { - charSetSplit := strings.SplitN(contentTypeSplit[1], "=", 2) - if len(charSetSplit) == 2 { - charSet = charSetSplit[1] +// parseMultiPartHeader parses a multipart header and returns the value and optional parts as +// separate map +func parseMultiPartHeader(multiPartHeader string) (header string, optional map[string]string) { + optional = make(map[string]string) + headerSplit := strings.SplitN(multiPartHeader, "; ", 2) + header = headerSplit[0] + if len(headerSplit) == 2 { + optSplit := strings.SplitN(headerSplit[1], "=", 2) + if len(optSplit) == 2 { + optional[optSplit[0]] = optSplit[1] } } return From e95799ad60e3f5e686ef012ccff3bc69ee3afc9d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 19 Jun 2024 14:41:13 +0200 Subject: [PATCH 27/47] Refactor attachment and embed parsing in EML The EML parsing has been refactored to separate the handling of attachments and embeds into a new helper function. This improves the organization of the code, makes it easier to understand and helps to better manage error handling and resource closing. --- eml.go | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/eml.go b/eml.go index ed52d02..cb7d064 100644 --- a/eml.go +++ b/eml.go @@ -231,19 +231,23 @@ func parseEMLMultipart(params map[string]string, bodybuf *bytes.Buffer, msg *Msg return fmt.Errorf("no boundary tag found in multipart body") } multipartReader := multipart.NewReader(bodybuf, boundary) +ReadNextPart: multiPart, err := multipartReader.NextPart() - if err != nil { + defer func() { + if multiPart != nil { + _ = multiPart.Close() + } + }() + if err != nil && !errors.Is(err, io.EOF) { return fmt.Errorf("failed to get next part of multipart message: %w", err) } for err == nil { + // Content-Disposition header means we have an attachment or embed if contentDisposition, ok := multiPart.Header[HeaderContentDisposition.String()]; ok { - cdType, optional := parseMultiPartHeader(contentDisposition[0]) - fmt.Println("CTD:", cdType) - fmt.Printf("optional: %+v\n", optional) - if err = msg.AttachReader("", multiPart); err != nil { - return fmt.Errorf("failed to attach multipart body: %w", err) + if err := parseEMLAttachmentEmbed(contentDisposition, multiPart, msg); err != nil { + return fmt.Errorf("failed to parse attachment/embed: %w", err) } - return nil + goto ReadNextPart } multiPartData, mperr := io.ReadAll(multiPart) @@ -285,7 +289,6 @@ func parseEMLMultipart(params map[string]string, bodybuf *bytes.Buffer, msg *Msg multiPart, err = multipartReader.NextPart() } if !errors.Is(err, io.EOF) { - _ = multiPart.Close() return fmt.Errorf("failed to read multipart: %w", err) } return nil @@ -344,3 +347,24 @@ func parseMultiPartHeader(multiPartHeader string) (header string, optional map[s } return } + +// parseEMLAttachmentEmbed parses a multipart that is an attachment or embed +func parseEMLAttachmentEmbed(contentDisposition []string, multiPart *multipart.Part, msg *Msg) error { + cdType, optional := parseMultiPartHeader(contentDisposition[0]) + filename := "generic.attachment" + if name, ok := optional["filename"]; ok { + filename = name[1 : len(name)-1] + } + switch strings.ToLower(cdType) { + case "attachment": + if err := msg.AttachReader(filename, multiPart); err != nil { + return fmt.Errorf("failed to attach multipart body: %w", err) + } + case "inline": + if err := msg.EmbedReader(filename, multiPart); err != nil { + return fmt.Errorf("failed to embed multipart body: %w", err) + } + } + fmt.Printf("FOUND Content: %s\n", cdType) + return nil +} From 52ef13a5d54f2b9986be9ca0f496b74e764578eb Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 19 Jun 2024 15:25:37 +0200 Subject: [PATCH 28/47] Refactor test cases and remove content print statement The content print statement in eml.go was removed to optimize code readability and performance. In addition, several assertions in the test cases of eml_test.go were corrected for string formatting errors and a new test case was added for handling emails with attachments. These changes aim to enhance the robustness of tests for email encoding and decoding operations. --- eml.go | 1 - eml_test.go | 73 ++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/eml.go b/eml.go index cb7d064..b066daf 100644 --- a/eml.go +++ b/eml.go @@ -365,6 +365,5 @@ func parseEMLAttachmentEmbed(contentDisposition []string, multiPart *multipart.P return fmt.Errorf("failed to embed multipart body: %w", err) } } - fmt.Printf("FOUND Content: %s\n", cdType) return nil } diff --git a/eml_test.go b/eml_test.go index 89f0550..e3d9440 100644 --- a/eml_test.go +++ b/eml_test.go @@ -121,6 +121,37 @@ RGVhciBDdXN0b21lciwKClRoaXMgaXMgYSB0ZXN0IG1haWwuIFBsZWFzZSBkbyBub3QgcmVwbHkg dG8gdGhpcy4gQWxzbyB0aGlzIGxpbmUgaXMgdmVyeSBsb25nIHNvIGl0CnNob3VsZCBiZSB3cmFw cGVkLgoKClRoYW5rIHlvdXIgZm9yIHlvdXIgYnVzaW5lc3MhClRoZSBnby1tYWlsIHRlYW0KCi0t ClRoaXMgaXMgYSBzaWduYXR1cmU=` + exampleMailPlainB64WithAttachment = `Date: Wed, 01 Nov 2023 00:00:00 +0000 +MIME-Version: 1.0 +Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev> +Subject: Example mail // plain text base64 with attachment +User-Agent: go-mail v0.4.1 // https://github.com/wneessen/go-mail +X-Mailer: go-mail v0.4.1 // https://github.com/wneessen/go-mail +From: "Toni Tester" +To: +Cc: +Content-Type: multipart/mixed; + boundary=45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7 + +--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7 +Content-Transfer-Encoding: base64 +Content-Type: text/plain; charset=UTF-8 + +RGVhciBDdXN0b21lciwKClRoaXMgaXMgYSB0ZXN0IG1haWwuIFBsZWFzZSBkbyBub3QgcmVwbHkg +dG8gdGhpcy4gQWxzbyB0aGlzIGxpbmUgaXMgdmVyeSBsb25nIHNvIGl0CnNob3VsZCBiZSB3cmFw +cGVkLgoKClRoYW5rIHlvdXIgZm9yIHlvdXIgYnVzaW5lc3MhClRoZSBnby1tYWlsIHRlYW0KCi0t +ClRoaXMgaXMgYSBzaWduYXR1cmU= + +--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7 +Content-Disposition: attachment; filename="test.attachment" +Content-Transfer-Encoding: base64 +Content-Type: application/octet-stream; name="test.attachment" + +VGhpcyBpcyBhIHNpbXBsZSB0ZXN0IHRleHQgZmlsZSBhdHRhY2htZW50LgoKSXQgCiAgaGFzCiAg +ICAgc2V2ZXJhbAogICAgICAgICAgICBuZXdsaW5lcwoJICAgICAgICAgICAgYW5kCgkgICAgc3Bh +Y2VzCiAgICAgaW4KICBpdAouCgpBcyB3ZWxsIGFzIGFuIGVtb2ppOiDwn5mCCg== + +--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7--` ) func TestEMLToMsgFromString(t *testing.T) { @@ -147,13 +178,13 @@ func TestEMLToMsgFromString(t *testing.T) { t.Run(tt.name, func(t *testing.T) { msg, err := EMLToMsgFromString(tt.eml) if err != nil { - t.Errorf("failed to parse EML: %subject", err) + t.Errorf("failed to parse EML: %s", err) } if msg.Encoding() != tt.enc { - t.Errorf("EMLToMsgFromString failed: expected encoding: %subject, but got: %subject", tt.enc, msg.Encoding()) + t.Errorf("EMLToMsgFromString failed: expected encoding: %s, but got: %s", tt.enc, msg.Encoding()) } if subject := msg.GetGenHeader(HeaderSubject); len(subject) > 0 && !strings.EqualFold(subject[0], tt.sub) { - t.Errorf("EMLToMsgFromString failed: expected subject: %subject, but got: %subject", + t.Errorf("EMLToMsgFromString failed: expected subject: %s, but got: %s", tt.sub, subject[0]) } }) @@ -199,13 +230,13 @@ func TestEMLToMsgFromFile(t *testing.T) { } msg, err := EMLToMsgFromFile(fmt.Sprintf("%s/%s.eml", tempDir, tt.name)) if err != nil { - t.Errorf("failed to parse EML: %subject", err) + t.Errorf("failed to parse EML: %s", err) } if msg.Encoding() != tt.enc { - t.Errorf("EMLToMsgFromString failed: expected encoding: %subject, but got: %subject", tt.enc, msg.Encoding()) + t.Errorf("EMLToMsgFromString failed: expected encoding: %s, but got: %s", tt.enc, msg.Encoding()) } if subject := msg.GetGenHeader(HeaderSubject); len(subject) > 0 && !strings.EqualFold(subject[0], tt.sub) { - t.Errorf("EMLToMsgFromString failed: expected subject: %subject, but got: %subject", + t.Errorf("EMLToMsgFromString failed: expected subject: %s, but got: %s", tt.sub, subject[0]) } }) @@ -232,3 +263,33 @@ func TestEMLToMsgFromStringBrokenDate(t *testing.T) { t.Errorf("EML with no date expected: %s, got: %s", now.Format(time.RFC1123Z), d) } } + +func TestEMLToMsgFromStringWithAttachment(t *testing.T) { + wantSubject := "Example mail // plain text base64 with attachment" + msg, err := EMLToMsgFromString(exampleMailPlainB64WithAttachment) + if err != nil { + t.Errorf("EML with attachment failed: %s", err) + } + if subject := msg.GetGenHeader(HeaderSubject); len(subject) > 0 && !strings.EqualFold(subject[0], wantSubject) { + t.Errorf("EMLToMsgFromString of EML with attachment failed: expected subject: %s, but got: %s", + wantSubject, subject[0]) + } + if len(msg.attachments) != 1 { + t.Errorf("EMLToMsgFromString of EML with attachment failed: expected no. of attachments: %d, but got: %d", + 1, len(msg.attachments)) + } + contentTypeHeader := msg.GetGenHeader(HeaderContentType) + if len(contentTypeHeader) != 1 { + t.Errorf("EMLToMsgFromString of EML with attachment failed: expected no. of content-type header: %d, "+ + "but got: %d", 1, len(contentTypeHeader)) + } + contentTypeSplit := strings.SplitN(contentTypeHeader[0], "; ", 2) + if len(contentTypeSplit) != 2 { + t.Error("failed to split Content-Type header") + return + } + if !strings.EqualFold(contentTypeSplit[0], "multipart/mixed") { + t.Errorf("EMLToMsgFromString of EML with attachment failed: expected content-type: %s, "+ + "but got: %s", "multipart/mixed", contentTypeSplit[0]) + } +} From bb33c4a92179c574510ae45115a3e50093658cb1 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 22 Jun 2024 14:13:26 +0200 Subject: [PATCH 29/47] Add support for multipart/related EML parsing This update expands the EML parser to support multipart/related content types. It also includes relevant error handling and creates a specific routine for parsing multipart/related parts separately. Furthermore, adjustments were made to avoid processing headers unnecessarily when TypeMultipartMixed is used. The diff also shows some refactoring for clearer error messages and cleaner code. --- eml.go | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/eml.go b/eml.go index b066daf..4766ed4 100644 --- a/eml.go +++ b/eml.go @@ -70,6 +70,7 @@ func EMLToMsgFromFile(filePath string) (*Msg, error) { if err = parseEMLBodyParts(parsedMsg, bodybuf, msg); err != nil { return msg, fmt.Errorf("failed to parse EML body parts: %w", err) } + //fmt.Printf("FOO: %+v\n", msg) return msg, nil } @@ -159,6 +160,10 @@ func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error { // Extract common headers for _, header := range commonHeaders { if value := mailHeader.Get(header.String()); value != "" { + if strings.EqualFold(header.String(), HeaderContentType.String()) && + strings.HasPrefix(value, TypeMultipartMixed.String()) { + continue + } msg.SetGenHeader(header, value) } } @@ -184,9 +189,10 @@ func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *M return fmt.Errorf("failed to parse plain body: %w", err) } case strings.EqualFold(mediatype, TypeMultipartAlternative.String()), - strings.EqualFold(mediatype, TypeMultipartMixed.String()): + strings.EqualFold(mediatype, TypeMultipartMixed.String()), + strings.EqualFold(mediatype, TypeMultipartRelated.String()): if err = parseEMLMultipart(params, bodybuf, msg); err != nil { - return fmt.Errorf("failed to parse multipart/alternative body: %w", err) + return fmt.Errorf("failed to parse multipart body: %w", err) } default: } @@ -242,9 +248,27 @@ ReadNextPart: return fmt.Errorf("failed to get next part of multipart message: %w", err) } for err == nil { + // Multipart/related parts need to be parsed seperately + if contentTypeSlice, ok := multiPart.Header[HeaderContentType.String()]; ok && len(contentTypeSlice) == 1 { + contentType, _ := parseMultiPartHeader(contentTypeSlice[0]) + if strings.EqualFold(contentType, TypeMultipartRelated.String()) { + relatedPart := &netmail.Message{ + Header: netmail.Header(multiPart.Header), + Body: multiPart, + } + relatedBuf := &bytes.Buffer{} + if _, err = relatedBuf.ReadFrom(multiPart); err != nil { + return fmt.Errorf("failed to read related multipart message to buffer: %w", err) + } + if err := parseEMLBodyParts(relatedPart, relatedBuf, msg); err != nil { + return fmt.Errorf("failed to parse related multipart body: %w", err) + } + } + } + // Content-Disposition header means we have an attachment or embed if contentDisposition, ok := multiPart.Header[HeaderContentDisposition.String()]; ok { - if err := parseEMLAttachmentEmbed(contentDisposition, multiPart, msg); err != nil { + if err = parseEMLAttachmentEmbed(contentDisposition, multiPart, msg); err != nil { return fmt.Errorf("failed to parse attachment/embed: %w", err) } goto ReadNextPart @@ -261,6 +285,9 @@ ReadNextPart: return fmt.Errorf("failed to get content-type from part") } contentType, optional := parseMultiPartHeader(multiPartContentType[0]) + if strings.EqualFold(contentType, TypeMultipartRelated.String()) { + goto ReadNextPart + } part := msg.newPart(ContentType(contentType)) if charset, ok := optional["charset"]; ok { part.SetCharset(Charset(charset)) @@ -316,7 +343,7 @@ func parseEMLContentTypeCharset(mailHeader *netmail.Header, msg *Msg) { msg.SetCharset(Charset(charset)) } msg.setEncoder() - if contentType != "" { + if contentType != "" && !strings.EqualFold(contentType, TypeMultipartMixed.String()) { msg.SetGenHeader(HeaderContentType, contentType) } } From 4c88dce2a854e46cc665ebd5f6502bae57d512c7 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 22 Jun 2024 14:24:52 +0200 Subject: [PATCH 30/47] Add test for EMLToMsgFromString with embedded content The new test ensures that the EMLToMsgFromString function properly handles an EML that contains embedded content. The expected subject content and number of embedded objects are checked to confirm correct parsing. --- eml_test.go | 51 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/eml_test.go b/eml_test.go index e3d9440..cc9e27b 100644 --- a/eml_test.go +++ b/eml_test.go @@ -152,6 +152,33 @@ ICAgc2V2ZXJhbAogICAgICAgICAgICBuZXdsaW5lcwoJICAgICAgICAgICAgYW5kCgkgICAgc3Bh Y2VzCiAgICAgaW4KICBpdAouCgpBcyB3ZWxsIGFzIGFuIGVtb2ppOiDwn5mCCg== --45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7--` + exampleMailPlainB64WithEmbed = `Date: Wed, 01 Nov 2023 00:00:00 +0000 +MIME-Version: 1.0 +Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev> +Subject: Example mail // plain text base64 with embed +User-Agent: go-mail v0.4.1 // https://github.com/wneessen/go-mail +X-Mailer: go-mail v0.4.1 // https://github.com/wneessen/go-mail +From: "Toni Tester" +To: +Cc: +Content-Type: multipart/related; + boundary=ffbcfb94b44e5297325102f6ced05b3b37f1d70fc38a5e78dc73c1a8434b + +--ffbcfb94b44e5297325102f6ced05b3b37f1d70fc38a5e78dc73c1a8434b +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset=UTF-8 + +This is a test body string +--ffbcfb94b44e5297325102f6ced05b3b37f1d70fc38a5e78dc73c1a8434b +Content-Disposition: inline; filename="pixel.png" +Content-Id: +Content-Transfer-Encoding: base64 +Content-Type: image/png; name="pixel.png" + +iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2NgYGD4DwABBAEAwS2O +UAAAAABJRU5ErkJggg== + +--ffbcfb94b44e5297325102f6ced05b3b37f1d70fc38a5e78dc73c1a8434b--` ) func TestEMLToMsgFromString(t *testing.T) { @@ -278,18 +305,20 @@ func TestEMLToMsgFromStringWithAttachment(t *testing.T) { t.Errorf("EMLToMsgFromString of EML with attachment failed: expected no. of attachments: %d, but got: %d", 1, len(msg.attachments)) } - contentTypeHeader := msg.GetGenHeader(HeaderContentType) - if len(contentTypeHeader) != 1 { - t.Errorf("EMLToMsgFromString of EML with attachment failed: expected no. of content-type header: %d, "+ - "but got: %d", 1, len(contentTypeHeader)) +} + +func TestEMLToMsgFromStringWithEmbed(t *testing.T) { + wantSubject := "Example mail // plain text base64 with embed" + msg, err := EMLToMsgFromString(exampleMailPlainB64WithEmbed) + if err != nil { + t.Errorf("EML with embed failed: %s", err) } - contentTypeSplit := strings.SplitN(contentTypeHeader[0], "; ", 2) - if len(contentTypeSplit) != 2 { - t.Error("failed to split Content-Type header") - return + if subject := msg.GetGenHeader(HeaderSubject); len(subject) > 0 && !strings.EqualFold(subject[0], wantSubject) { + t.Errorf("EMLToMsgFromString of EML with embed failed: expected subject: %s, but got: %s", + wantSubject, subject[0]) } - if !strings.EqualFold(contentTypeSplit[0], "multipart/mixed") { - t.Errorf("EMLToMsgFromString of EML with attachment failed: expected content-type: %s, "+ - "but got: %s", "multipart/mixed", contentTypeSplit[0]) + if len(msg.embeds) != 1 { + t.Errorf("EMLToMsgFromString of EML with embed failed: expected no. of embeds: %d, but got: %d", + 1, len(msg.attachments)) } } From 70c9387003b7a6ecbb7917c8a780b56e14c7e5ea Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 24 Jun 2024 13:41:48 +0200 Subject: [PATCH 31/47] Add error handling for unknown content types in eml.go The update adds a case to the switch clause in eml.go for properly handling unknown content types. An error will now be returned when the media type of the body to be parsed is not recognized, increasing the robustness of the system. --- eml.go | 1 + 1 file changed, 1 insertion(+) diff --git a/eml.go b/eml.go index 4766ed4..524c340 100644 --- a/eml.go +++ b/eml.go @@ -195,6 +195,7 @@ func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *M return fmt.Errorf("failed to parse multipart body: %w", err) } default: + return fmt.Errorf("failed to parse body, unknown content type: %s", mediatype) } return nil } From 9fa8644a9a8bcb0a6b69c513d497820574a6bfa9 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 24 Jun 2024 13:42:28 +0200 Subject: [PATCH 32/47] More test coverage New failing tests have been added to 'eml_test.go' to account for a variety of error situations, such as broken FROM, TO, headers, bodies, and unknown or unsupported content types. Improving the robustness of test coverage helps identify potential issues and ensure the resilience and correctness of the code. --- eml_test.go | 222 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) diff --git a/eml_test.go b/eml_test.go index cc9e27b..c56e154 100644 --- a/eml_test.go +++ b/eml_test.go @@ -5,6 +5,7 @@ package mail import ( + "bytes" "fmt" "os" "strings" @@ -31,6 +32,115 @@ This is a test mail. Please do not reply to this. Also this line is very long so should be wrapped. +Thank your for your business! +The go-mail team + +-- +This is a signature` + exampleMailPlainBrokenBody = `Date: Wed, 01 Nov 2023 00:00:00 +0000 +MIME-Version: 1.0 +Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev> +Subject: Example mail // plain text without encoding +User-Agent: go-mail v0.4.0 // https://github.com/wneessen/go-mail +X-Mailer: go-mail v0.4.0 // https://github.com/wneessen/go-mail +From: "Toni Tester" +To: +Cc: +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: base64 + +This plain text body should not be parsed as Base64. +` + exampleMailPlainNoContentType = `Date: Wed, 01 Nov 2023 00:00:00 +0000 +MIME-Version: 1.0 +Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev> +Subject: Example mail // plain text without encoding +User-Agent: go-mail v0.4.0 // https://github.com/wneessen/go-mail +X-Mailer: go-mail v0.4.0 // https://github.com/wneessen/go-mail +From: "Toni Tester" +To: +Cc: + +This plain text body should not be parsed as Base64. +` + exampleMailPlainUnknownContentType = `Date: Wed, 01 Nov 2023 00:00:00 +0000 +MIME-Version: 1.0 +Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev> +Subject: Example mail // plain text without encoding +User-Agent: go-mail v0.4.0 // https://github.com/wneessen/go-mail +X-Mailer: go-mail v0.4.0 // https://github.com/wneessen/go-mail +From: "Toni Tester" +To: +Cc: +Content-Type: application/unknown; charset=UTF-8 +Content-Transfer-Encoding: base64 + +This plain text body should not be parsed as Base64. +` + exampleMailPlainBrokenHeader = `Date: Wed, 01 Nov 2023 00:00:00 +0000 +MIME-Version = 1.0 +Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev> +Subject: Example mail // plain text without encoding +User-Agent = go-mail v0.4.0 // https://github.com/wneessen/go-mail +X-Mailer: go-mail v0.4.0 // https://github.com/wneessen/go-mail +From = "Toni Tester" +To: +Cc: +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding = 8bit + +Dear Customer, + +This is a test mail. Please do not reply to this. Also this line is very long so it +should be wrapped. + + +Thank your for your business! +The go-mail team + +-- +This is a signature` + exampleMailPlainBrokenFrom = `Date: Wed, 01 Nov 2023 00:00:00 +0000 +MIME-Version: 1.0 +Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev> +Subject: Example mail // plain text without encoding +User-Agent: go-mail v0.4.0 // https://github.com/wneessen/go-mail +X-Mailer: go-mail v0.4.0 // https://github.com/wneessen/go-mail +From: Toni Tester" go-mail@go-mail.dev> +To: +Cc: +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Dear Customer, + +This is a test mail. Please do not reply to this. Also this line is very long so it +should be wrapped. + + +Thank your for your business! +The go-mail team + +-- +This is a signature` + exampleMailPlainBrokenTo = `Date: Wed, 01 Nov 2023 00:00:00 +0000 +MIME-Version: 1.0 +Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev> +Subject: Example mail // plain text without encoding +User-Agent: go-mail v0.4.0 // https://github.com/wneessen/go-mail +X-Mailer: go-mail v0.4.0 // https://github.com/wneessen/go-mail +From: "Toni Tester" +To: go-mail+test.go-mail.dev> +Cc: +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Dear Customer, + +This is a test mail. Please do not reply to this. Also this line is very long so it +should be wrapped. + + Thank your for your business! The go-mail team @@ -99,6 +209,28 @@ This is a test mail. Please do not reply to this. Also this line is very lo= ng so it should be wrapped. +Thank your for your business! +The go-mail team + +-- +This is a signature` + exampleMailPlainUnsupportedTransferEnc = `Date: Wed, 01 Nov 2023 00:00:00 +0000 +MIME-Version: 1.0 +Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev> +Subject: Example mail // plain text quoted-printable +User-Agent: go-mail v0.4.0 // https://github.com/wneessen/go-mail +X-Mailer: go-mail v0.4.0 // https://github.com/wneessen/go-mail +From: "Toni Tester" +To: +Cc: +Content-Type: text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: unknown + +Dear Customer, + +This is a test mail. Please do not reply to this. Also this line is very long so it should be wrapped. +㋛ +This is not ==D3 Thank your for your business! The go-mail team @@ -151,6 +283,37 @@ VGhpcyBpcyBhIHNpbXBsZSB0ZXN0IHRleHQgZmlsZSBhdHRhY2htZW50LgoKSXQgCiAgaGFzCiAg ICAgc2V2ZXJhbAogICAgICAgICAgICBuZXdsaW5lcwoJICAgICAgICAgICAgYW5kCgkgICAgc3Bh Y2VzCiAgICAgaW4KICBpdAouCgpBcyB3ZWxsIGFzIGFuIGVtb2ppOiDwn5mCCg== +--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7--` + exampleMailPlainB64BrokenBody = `Date: Wed, 01 Nov 2023 00:00:00 +0000 +MIME-Version: 1.0 +Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev> +Subject: Example mail // plain text base64 with attachment +User-Agent: go-mail v0.4.1 // https://github.com/wneessen/go-mail +X-Mailer: go-mail v0.4.1 // https://github.com/wneessen/go-mail +From: "Toni Tester" +To: +Cc: +Content-Type: multipart/mixed; + boundary=45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7 + +--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7 +Content-Transfer-Encoding = base64 +Content-Type = text/plain; charset=UTF-8 + +RGVhciBDdXN0b21lciwKClRoaXMgaXMgYSB0ZXN0IG1haWwuIFBsZWFzZSBkbyBub3QgcmVwbHkg +dG8gdGhpcy4gQWxzbyB0aGlzIGxpbmUgaXMgdmVyeSBsb25nIHNvIGl0CnNob3VsZCBiZSB3cmFw +cGVkLgoKClRoYW5rIHlvdXIgZm9yIHlvdXIgYnVzaW5lc3MhClRoZSBnby1tYWlsIHRlYW0KCi0t +ClRoaXMgaXMgYSBzaWduYXR1cmU= + +--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7 +Content-Disposition: attachment; filename="test.attachment" +Content-Transfer-Encoding: base64 +Content-Type: application/octet-stream; name="test.attachment" + +VGhpcyBpcyBhIHNpbXBsZSB0ZXN0IHRleHQgZmlsZSBhdHRhY2htZW50LgoKSXQgCiAgaGFzCiAg +ICAgc2V2ZXJhbAogICAgICAgICAgICBuZXdsaW5lcwoJICAgICAgICAgICAgYW5kCgkgICAgc3Bh +Y2VzCiAgICAgaW4KICBpdAouCgpBcyB3ZWxsIGFzIGFuIGVtb2ppOiDwn5mCCg== + --45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7--` exampleMailPlainB64WithEmbed = `Date: Wed, 01 Nov 2023 00:00:00 +0000 MIME-Version: 1.0 @@ -270,6 +433,51 @@ func TestEMLToMsgFromFile(t *testing.T) { } } +func TestEMLToMsgFromReaderFailing(t *testing.T) { + mailbuf := bytes.Buffer{} + mailbuf.WriteString(exampleMailPlainBrokenFrom) + _, err := EMLToMsgFromReader(&mailbuf) + if err == nil { + t.Error("EML from Reader with broken FROM was supposed to fail, but didn't") + } + mailbuf.Reset() + mailbuf.WriteString(exampleMailPlainBrokenHeader) + _, err = EMLToMsgFromReader(&mailbuf) + if err == nil { + t.Error("EML from Reader with broken header was supposed to fail, but didn't") + } + mailbuf.Reset() + mailbuf.WriteString(exampleMailPlainB64BrokenBody) + _, err = EMLToMsgFromReader(&mailbuf) + if err == nil { + t.Error("EML from Reader with broken body was supposed to fail, but didn't") + } + mailbuf.Reset() + mailbuf.WriteString(exampleMailPlainBrokenBody) + _, err = EMLToMsgFromReader(&mailbuf) + if err == nil { + t.Error("EML from Reader with broken body was supposed to fail, but didn't") + } + mailbuf.Reset() + mailbuf.WriteString(exampleMailPlainUnknownContentType) + _, err = EMLToMsgFromReader(&mailbuf) + if err == nil { + t.Error("EML from Reader with unknown content type was supposed to fail, but didn't") + } + mailbuf.Reset() + mailbuf.WriteString(exampleMailPlainNoContentType) + _, err = EMLToMsgFromReader(&mailbuf) + if err == nil { + t.Error("EML from Reader with no content type was supposed to fail, but didn't") + } + mailbuf.Reset() + mailbuf.WriteString(exampleMailPlainUnsupportedTransferEnc) + _, err = EMLToMsgFromReader(&mailbuf) + if err == nil { + t.Error("EML from Reader with unsupported Transer-Encoding was supposed to fail, but didn't") + } +} + func TestEMLToMsgFromStringBrokenDate(t *testing.T) { _, err := EMLToMsgFromString(exampleMailPlainNoEncInvalidDate) if err == nil { @@ -291,6 +499,20 @@ func TestEMLToMsgFromStringBrokenDate(t *testing.T) { } } +func TestEMLToMsgFromStringBrokenFrom(t *testing.T) { + _, err := EMLToMsgFromString(exampleMailPlainBrokenFrom) + if err == nil { + t.Error("EML with broken FROM was supposed to fail, but didn't") + } +} + +func TestEMLToMsgFromStringBrokenTo(t *testing.T) { + _, err := EMLToMsgFromString(exampleMailPlainBrokenTo) + if err == nil { + t.Error("EML with broken TO was supposed to fail, but didn't") + } +} + func TestEMLToMsgFromStringWithAttachment(t *testing.T) { wantSubject := "Example mail // plain text base64 with attachment" msg, err := EMLToMsgFromString(exampleMailPlainB64WithAttachment) From 0cec8a28f736fd7260e20d00babc62d320e3714f Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 24 Jun 2024 14:30:07 +0200 Subject: [PATCH 33/47] Refactor EML parsing into a single function The previous separate parsing of EML headers and body parts has been refactored into a single function, parseEML. This change simplifies the operations in the readEML and makes the code cleaner by reducing repetition. --- eml.go | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/eml.go b/eml.go index 524c340..ef9e1a7 100644 --- a/eml.go +++ b/eml.go @@ -39,11 +39,8 @@ func EMLToMsgFromReader(reader io.Reader) (*Msg, error) { return msg, fmt.Errorf("failed to parse EML from reader: %w", err) } - if err = parseEMLHeaders(&parsedMsg.Header, msg); err != nil { - return msg, fmt.Errorf("failed to parse EML headers: %w", err) - } - if err = parseEMLBodyParts(parsedMsg, bodybuf, msg); err != nil { - return msg, fmt.Errorf("failed to parse EML body parts: %w", err) + if err := parseEML(parsedMsg, bodybuf, msg); err != nil { + return msg, fmt.Errorf("failed to parse EML contents: %w", err) } return msg, nil @@ -64,17 +61,24 @@ func EMLToMsgFromFile(filePath string) (*Msg, error) { return msg, fmt.Errorf("failed to parse EML file: %w", err) } - if err = parseEMLHeaders(&parsedMsg.Header, msg); err != nil { - return msg, fmt.Errorf("failed to parse EML headers: %w", err) + if err := parseEML(parsedMsg, bodybuf, msg); err != nil { + return msg, fmt.Errorf("failed to parse EML contents: %w", err) } - if err = parseEMLBodyParts(parsedMsg, bodybuf, msg); err != nil { - return msg, fmt.Errorf("failed to parse EML body parts: %w", err) - } - //fmt.Printf("FOO: %+v\n", msg) return msg, nil } +// parseEML parses the EML's headers and body and inserts the parsed values into the Msg +func parseEML(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error { + if err := parseEMLHeaders(&parsedMsg.Header, msg); err != nil { + return fmt.Errorf("failed to parse EML headers: %w", err) + } + if err := parseEMLBodyParts(parsedMsg, bodybuf, msg); err != nil { + return fmt.Errorf("failed to parse EML body parts: %w", err) + } + return nil +} + // readEML opens an EML file and uses net/mail to parse the header and body func readEML(filePath string) (*netmail.Message, *bytes.Buffer, error) { fileHandle, err := os.Open(filePath) From db9893f15a00c2da23b26d1a0891bbb7cd04253f Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 24 Jun 2024 14:30:28 +0200 Subject: [PATCH 34/47] More test coverage Refactored the way EML files are tested, the errors are now handled more efficiently. Temporary directory and file creation, as well as file writing, have been moved to a helper function named 'stringToTempFile'. Moreover, additional test cases were added to ensure proper parsing failure for various types of email-related errors. --- eml_test.go | 103 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 83 insertions(+), 20 deletions(-) diff --git a/eml_test.go b/eml_test.go index c56e154..43fe9e3 100644 --- a/eml_test.go +++ b/eml_test.go @@ -403,22 +403,13 @@ func TestEMLToMsgFromFile(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tempDir, err := os.MkdirTemp("", fmt.Sprintf("*-%s", tt.name)) - if err != nil { - t.Errorf("failed to create temp dir: %s", err) - return - } + tempDir, tempFile, err := stringToTempFile(tt.eml, tt.name) defer func() { if err = os.RemoveAll(tempDir); err != nil { t.Error("failed to remove temp dir:", err) } }() - err = os.WriteFile(fmt.Sprintf("%s/%s.eml", tempDir, tt.name), []byte(tt.eml), 0666) - if err != nil { - t.Error("failed to write mail to temp file:", err) - return - } - msg, err := EMLToMsgFromFile(fmt.Sprintf("%s/%s.eml", tempDir, tt.name)) + msg, err := EMLToMsgFromFile(tempFile) if err != nil { t.Errorf("failed to parse EML: %s", err) } @@ -434,50 +425,108 @@ func TestEMLToMsgFromFile(t *testing.T) { } func TestEMLToMsgFromReaderFailing(t *testing.T) { - mailbuf := bytes.Buffer{} - mailbuf.WriteString(exampleMailPlainBrokenFrom) - _, err := EMLToMsgFromReader(&mailbuf) + mailbuf := bytes.NewBufferString(exampleMailPlainBrokenFrom) + _, err := EMLToMsgFromReader(mailbuf) if err == nil { t.Error("EML from Reader with broken FROM was supposed to fail, but didn't") } mailbuf.Reset() mailbuf.WriteString(exampleMailPlainBrokenHeader) - _, err = EMLToMsgFromReader(&mailbuf) + _, err = EMLToMsgFromReader(mailbuf) if err == nil { t.Error("EML from Reader with broken header was supposed to fail, but didn't") } mailbuf.Reset() mailbuf.WriteString(exampleMailPlainB64BrokenBody) - _, err = EMLToMsgFromReader(&mailbuf) + _, err = EMLToMsgFromReader(mailbuf) if err == nil { t.Error("EML from Reader with broken body was supposed to fail, but didn't") } mailbuf.Reset() mailbuf.WriteString(exampleMailPlainBrokenBody) - _, err = EMLToMsgFromReader(&mailbuf) + _, err = EMLToMsgFromReader(mailbuf) if err == nil { t.Error("EML from Reader with broken body was supposed to fail, but didn't") } mailbuf.Reset() mailbuf.WriteString(exampleMailPlainUnknownContentType) - _, err = EMLToMsgFromReader(&mailbuf) + _, err = EMLToMsgFromReader(mailbuf) if err == nil { t.Error("EML from Reader with unknown content type was supposed to fail, but didn't") } mailbuf.Reset() mailbuf.WriteString(exampleMailPlainNoContentType) - _, err = EMLToMsgFromReader(&mailbuf) + _, err = EMLToMsgFromReader(mailbuf) if err == nil { t.Error("EML from Reader with no content type was supposed to fail, but didn't") } mailbuf.Reset() mailbuf.WriteString(exampleMailPlainUnsupportedTransferEnc) - _, err = EMLToMsgFromReader(&mailbuf) + _, err = EMLToMsgFromReader(mailbuf) if err == nil { t.Error("EML from Reader with unsupported Transer-Encoding was supposed to fail, but didn't") } } +func TestEMLToMsgFromFileFailing(t *testing.T) { + tempDir, tempFile, err := stringToTempFile(exampleMailPlainBrokenFrom, "testmail") + _, err = EMLToMsgFromFile(tempFile) + if err == nil { + t.Error("EML from Reader with broken FROM was supposed to fail, but didn't") + } + if err = os.RemoveAll(tempDir); err != nil { + t.Error("failed to remove temp dir:", err) + } + tempDir, tempFile, err = stringToTempFile(exampleMailPlainBrokenHeader, "testmail") + _, err = EMLToMsgFromFile(tempFile) + if err == nil { + t.Error("EML from Reader with broken header was supposed to fail, but didn't") + } + if err = os.RemoveAll(tempDir); err != nil { + t.Error("failed to remove temp dir:", err) + } + tempDir, tempFile, err = stringToTempFile(exampleMailPlainB64BrokenBody, "testmail") + _, err = EMLToMsgFromFile(tempFile) + if err == nil { + t.Error("EML from Reader with broken body was supposed to fail, but didn't") + } + if err = os.RemoveAll(tempDir); err != nil { + t.Error("failed to remove temp dir:", err) + } + tempDir, tempFile, err = stringToTempFile(exampleMailPlainBrokenBody, "testmail") + _, err = EMLToMsgFromFile(tempFile) + if err == nil { + t.Error("EML from Reader with broken body was supposed to fail, but didn't") + } + if err = os.RemoveAll(tempDir); err != nil { + t.Error("failed to remove temp dir:", err) + } + tempDir, tempFile, err = stringToTempFile(exampleMailPlainUnknownContentType, "testmail") + _, err = EMLToMsgFromFile(tempFile) + if err == nil { + t.Error("EML from Reader with unknown content type was supposed to fail, but didn't") + } + if err = os.RemoveAll(tempDir); err != nil { + t.Error("failed to remove temp dir:", err) + } + tempDir, tempFile, err = stringToTempFile(exampleMailPlainNoContentType, "testmail") + _, err = EMLToMsgFromFile(tempFile) + if err == nil { + t.Error("EML from Reader with no content type was supposed to fail, but didn't") + } + if err = os.RemoveAll(tempDir); err != nil { + t.Error("failed to remove temp dir:", err) + } + tempDir, tempFile, err = stringToTempFile(exampleMailPlainUnsupportedTransferEnc, "testmail") + _, err = EMLToMsgFromFile(tempFile) + if err == nil { + t.Error("EML from Reader with unsupported Transer-Encoding was supposed to fail, but didn't") + } + if err = os.RemoveAll(tempDir); err != nil { + t.Error("failed to remove temp dir:", err) + } +} + func TestEMLToMsgFromStringBrokenDate(t *testing.T) { _, err := EMLToMsgFromString(exampleMailPlainNoEncInvalidDate) if err == nil { @@ -544,3 +593,17 @@ func TestEMLToMsgFromStringWithEmbed(t *testing.T) { 1, len(msg.attachments)) } } + +// stringToTempFile is a helper method that will create a temporary file form a give data string +func stringToTempFile(data, name string) (string, string, error) { + tempDir, err := os.MkdirTemp("", fmt.Sprintf("*-%s", name)) + if err != nil { + return tempDir, "", fmt.Errorf("failed to create temp dir: %w", err) + } + filePath := fmt.Sprintf("%s/%s", tempDir, name) + err = os.WriteFile(filePath, []byte(data), 0666) + if err != nil { + return tempDir, "", fmt.Errorf("failed to write data to temp file: %w", err) + } + return tempDir, filePath, nil +} From faab5323fd007dd36f64202c0e534771d1b4d3ec Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 25 Jun 2024 09:47:12 +0200 Subject: [PATCH 35/47] Fix erroneous header name in error message The error message previously referenced a constant 'HeaderTo' which might not always be the header being parsed. The commit replaces this with 'addrHeader', significantly improving the accuracy of error messages. --- eml.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eml.go b/eml.go index ef9e1a7..499087b 100644 --- a/eml.go +++ b/eml.go @@ -142,7 +142,7 @@ func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error { addrStrings = append(addrStrings, addr.String()) } if err = addrFunc(addrStrings...); err != nil { - return fmt.Errorf(`failed to parse %q header: %w`, HeaderTo, err) + return fmt.Errorf(`failed to parse %q header: %w`, addrHeader, err) } } } From 8b1208949f6d9249447208a1a4c3ad2d0a44fcb1 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 25 Jun 2024 10:59:04 +0200 Subject: [PATCH 36/47] Fix multipart header parsing in eml.go The commit modifies the parseMultiPartHeader function to handle optional fields accurately. The delimiter was changed from "; " to ";" and whitespace is being trimmed from the start of optional fields to ensure correct splitting and mapping. --- eml.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/eml.go b/eml.go index 499087b..b7a903b 100644 --- a/eml.go +++ b/eml.go @@ -369,10 +369,11 @@ func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error { // separate map func parseMultiPartHeader(multiPartHeader string) (header string, optional map[string]string) { optional = make(map[string]string) - headerSplit := strings.SplitN(multiPartHeader, "; ", 2) + headerSplit := strings.SplitN(multiPartHeader, ";", 2) header = headerSplit[0] if len(headerSplit) == 2 { - optSplit := strings.SplitN(headerSplit[1], "=", 2) + optString := strings.TrimLeft(headerSplit[1], " ") + optSplit := strings.SplitN(optString, "=", 2) if len(optSplit) == 2 { optional[optSplit[0]] = optSplit[1] } From b709df4b2dd475928cc1aef1f00a88989e5da5a8 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 28 Jun 2024 11:54:06 +0200 Subject: [PATCH 37/47] Add WithContentID function to file.go The newly added function, WithContentID, allows for setting the Content-ID header for the File. This provides enhanced handling and differentiation of files. --- file.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/file.go b/file.go index 1ad2d5a..a759b3d 100644 --- a/file.go +++ b/file.go @@ -22,6 +22,13 @@ type File struct { Writer func(w io.Writer) (int64, error) } +// WithContentID sets the Content-ID header for the File +func WithContentID(id string) FileOption { + return func(f *File) { + f.Header.Set(HeaderContentID.String(), id) + } +} + // WithFileName sets the filename of the File func WithFileName(name string) FileOption { return func(f *File) { From 84e6275cb27762c12e365b5f5482eb331abaa0df Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 28 Jun 2024 11:54:30 +0200 Subject: [PATCH 38/47] Add base64 encoding support to email attachments The updated code adds base64 encoding support to email attachments and inline content in eml.go. It does this by introducing a new dataReader which uses a base64 decoder if the content transfer encoding is base64. With this update, attachments with base64 content will be correctly decoded when processed. --- eml.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/eml.go b/eml.go index b7a903b..ee5f099 100644 --- a/eml.go +++ b/eml.go @@ -388,13 +388,28 @@ func parseEMLAttachmentEmbed(contentDisposition []string, multiPart *multipart.P if name, ok := optional["filename"]; ok { filename = name[1 : len(name)-1] } + + var dataReader io.Reader + dataReader = multiPart + contentTransferEnc, _ := parseMultiPartHeader(multiPart.Header.Get(HeaderContentTransferEnc.String())) + b64Decoder := base64.NewDecoder(base64.StdEncoding, multiPart) + if strings.EqualFold(contentTransferEnc, EncodingB64.String()) { + dataReader = b64Decoder + } + switch strings.ToLower(cdType) { case "attachment": - if err := msg.AttachReader(filename, multiPart); err != nil { + if err := msg.AttachReader(filename, dataReader); err != nil { return fmt.Errorf("failed to attach multipart body: %w", err) } case "inline": - if err := msg.EmbedReader(filename, multiPart); err != nil { + if contentID, _ := parseMultiPartHeader(multiPart.Header.Get(HeaderContentID.String())); contentID != "" { + if err := msg.EmbedReader(filename, dataReader, WithContentID(contentID)); err != nil { + return fmt.Errorf("failed to embed multipart body: %w", err) + } + return nil + } + if err := msg.EmbedReader(filename, dataReader); err != nil { return fmt.Errorf("failed to embed multipart body: %w", err) } } From fcc52098521ebb684df211ac204d852757ff9424 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 28 Jun 2024 13:28:09 +0200 Subject: [PATCH 39/47] Add handling for multipart/alternative content type The EML parser now includes logic to manage 'multipart/alternative' content types. This adjustment is made within the section handling 'multipart/related' parts, allowing for better handling and parsing of varying content types. --- eml.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/eml.go b/eml.go index ee5f099..27eb616 100644 --- a/eml.go +++ b/eml.go @@ -256,7 +256,9 @@ ReadNextPart: // Multipart/related parts need to be parsed seperately if contentTypeSlice, ok := multiPart.Header[HeaderContentType.String()]; ok && len(contentTypeSlice) == 1 { contentType, _ := parseMultiPartHeader(contentTypeSlice[0]) - if strings.EqualFold(contentType, TypeMultipartRelated.String()) { + fmt.Printf("CONTENT-TYPE: %s\n", contentType) + if strings.EqualFold(contentType, TypeMultipartRelated.String()) || + strings.EqualFold(contentType, TypeMultipartAlternative.String()) { relatedPart := &netmail.Message{ Header: netmail.Header(multiPart.Header), Body: multiPart, From 0bf3f73e9bac42ba5b531656b0876c0626905984 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 28 Jun 2024 13:28:57 +0200 Subject: [PATCH 40/47] Update comment in eml.go for parsing multipart messages The comment in the eml.go file was extended to include the possibility of 'Multipart/alternative' parts. Previously, it only mentioned 'Multipart/related' parts. The actual code functionality remains unchanged, this is purely a clarification in the documentation. --- eml.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eml.go b/eml.go index 27eb616..fbffaea 100644 --- a/eml.go +++ b/eml.go @@ -253,7 +253,7 @@ ReadNextPart: return fmt.Errorf("failed to get next part of multipart message: %w", err) } for err == nil { - // Multipart/related parts need to be parsed seperately + // Multipart/related and Multipart/alternative parts need to be parsed seperately if contentTypeSlice, ok := multiPart.Header[HeaderContentType.String()]; ok && len(contentTypeSlice) == 1 { contentType, _ := parseMultiPartHeader(contentTypeSlice[0]) fmt.Printf("CONTENT-TYPE: %s\n", contentType) From 68d0fe9cd3dd2469d70b290ceb6060a34d79eba9 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 28 Jun 2024 13:33:57 +0200 Subject: [PATCH 41/47] Remove debug print statement from eml.go The debug print statement that outputs the content type of the email has been removed from eml.go. This change improves code cleanliness and avoids unnecessary console output in production. --- eml.go | 1 - 1 file changed, 1 deletion(-) diff --git a/eml.go b/eml.go index fbffaea..844a5ad 100644 --- a/eml.go +++ b/eml.go @@ -256,7 +256,6 @@ ReadNextPart: // Multipart/related and Multipart/alternative parts need to be parsed seperately if contentTypeSlice, ok := multiPart.Header[HeaderContentType.String()]; ok && len(contentTypeSlice) == 1 { contentType, _ := parseMultiPartHeader(contentTypeSlice[0]) - fmt.Printf("CONTENT-TYPE: %s\n", contentType) if strings.EqualFold(contentType, TypeMultipartRelated.String()) || strings.EqualFold(contentType, TypeMultipartAlternative.String()) { relatedPart := &netmail.Message{ From ca201b15488d3038fa587d6460a158f177e782d1 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 28 Jun 2024 13:45:22 +0200 Subject: [PATCH 42/47] Add tests for various email scenarios This commit introduces tests for different email scenarios including emails with no boundaries, emails with attachments, emails with embedded items, and multipart mixed emails. The tests ensure that the code correctly returns all expected parts of the email (e.g., embeds, attachments, parts) and that it correctly processes the email subject. --- eml_test.go | 253 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 252 insertions(+), 1 deletion(-) diff --git a/eml_test.go b/eml_test.go index 43fe9e3..587c4bf 100644 --- a/eml_test.go +++ b/eml_test.go @@ -283,6 +283,36 @@ VGhpcyBpcyBhIHNpbXBsZSB0ZXN0IHRleHQgZmlsZSBhdHRhY2htZW50LgoKSXQgCiAgaGFzCiAg ICAgc2V2ZXJhbAogICAgICAgICAgICBuZXdsaW5lcwoJICAgICAgICAgICAgYW5kCgkgICAgc3Bh Y2VzCiAgICAgaW4KICBpdAouCgpBcyB3ZWxsIGFzIGFuIGVtb2ppOiDwn5mCCg== +--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7--` + exampleMailPlainB64WithAttachmentNoBoundary = `Date: Wed, 01 Nov 2023 00:00:00 +0000 +MIME-Version: 1.0 +Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev> +Subject: Example mail // plain text base64 with attachment +User-Agent: go-mail v0.4.1 // https://github.com/wneessen/go-mail +X-Mailer: go-mail v0.4.1 // https://github.com/wneessen/go-mail +From: "Toni Tester" +To: +Cc: +Content-Type: multipart/mixed; + +--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7 +Content-Transfer-Encoding: base64 +Content-Type: text/plain; charset=UTF-8 + +RGVhciBDdXN0b21lciwKClRoaXMgaXMgYSB0ZXN0IG1haWwuIFBsZWFzZSBkbyBub3QgcmVwbHkg +dG8gdGhpcy4gQWxzbyB0aGlzIGxpbmUgaXMgdmVyeSBsb25nIHNvIGl0CnNob3VsZCBiZSB3cmFw +cGVkLgoKClRoYW5rIHlvdXIgZm9yIHlvdXIgYnVzaW5lc3MhClRoZSBnby1tYWlsIHRlYW0KCi0t +ClRoaXMgaXMgYSBzaWduYXR1cmU= + +--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7 +Content-Disposition: attachment; filename="test.attachment" +Content-Transfer-Encoding: base64 +Content-Type: application/octet-stream; name="test.attachment" + +VGhpcyBpcyBhIHNpbXBsZSB0ZXN0IHRleHQgZmlsZSBhdHRhY2htZW50LgoKSXQgCiAgaGFzCiAg +ICAgc2V2ZXJhbAogICAgICAgICAgICBuZXdsaW5lcwoJICAgICAgICAgICAgYW5kCgkgICAgc3Bh +Y2VzCiAgICAgaW4KICBpdAouCgpBcyB3ZWxsIGFzIGFuIGVtb2ppOiDwn5mCCg== + --45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7--` exampleMailPlainB64BrokenBody = `Date: Wed, 01 Nov 2023 00:00:00 +0000 MIME-Version: 1.0 @@ -342,6 +372,159 @@ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2NgYGD4DwABBAEAwS2O UAAAAABJRU5ErkJggg== --ffbcfb94b44e5297325102f6ced05b3b37f1d70fc38a5e78dc73c1a8434b--` + exampleMailPlainB64WithEmbedNoContentID = `Date: Wed, 01 Nov 2023 00:00:00 +0000 +MIME-Version: 1.0 +Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev> +Subject: Example mail // plain text base64 with embed +User-Agent: go-mail v0.4.1 // https://github.com/wneessen/go-mail +X-Mailer: go-mail v0.4.1 // https://github.com/wneessen/go-mail +From: "Toni Tester" +To: +Cc: +Content-Type: multipart/related; + boundary=ffbcfb94b44e5297325102f6ced05b3b37f1d70fc38a5e78dc73c1a8434b + +--ffbcfb94b44e5297325102f6ced05b3b37f1d70fc38a5e78dc73c1a8434b +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset=UTF-8 + +This is a test body string +--ffbcfb94b44e5297325102f6ced05b3b37f1d70fc38a5e78dc73c1a8434b +Content-Disposition: inline; filename="pixel.png" +Content-Transfer-Encoding: base64 +Content-Type: image/png; name="pixel.png" + +iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2NgYGD4DwABBAEAwS2O +UAAAAABJRU5ErkJggg== + +--ffbcfb94b44e5297325102f6ced05b3b37f1d70fc38a5e78dc73c1a8434b--` + exampleMailMultipartMixedAlternativeRelated = `Date: Wed, 01 Nov 2023 00:00:00 +0000 +MIME-Version: 1.0 +Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev> +Subject: Example mail // plain text base64 with attachment, embed and alternative part +User-Agent: go-mail v0.4.1 // https://github.com/wneessen/go-mail +X-Mailer: go-mail v0.4.1 // https://github.com/wneessen/go-mail +From: "Toni Tester" +To: +Cc: +Content-Type: multipart/mixed; + boundary=fe785e0384e2607697cc2ecb17cce003003bb7ca9112104f3e8ce727edb5 + +--fe785e0384e2607697cc2ecb17cce003003bb7ca9112104f3e8ce727edb5 +Content-Type: multipart/related; + boundary=5897e40a22c608e252cfab849e966112fcbd5a1c291208026b3ca2bfab8a + + + +--5897e40a22c608e252cfab849e966112fcbd5a1c291208026b3ca2bfab8a +Content-Type: multipart/alternative; + boundary=cbace12de35ef4eae53fd974ccd41cb2aee4f9c9c76057ec8bfdd0c97813 + + + +--cbace12de35ef4eae53fd974ccd41cb2aee4f9c9c76057ec8bfdd0c97813 +Content-Transfer-Encoding: base64 +Content-Type: text/plain; charset=UTF-8 + +RGVhciBDdXN0b21lciwKCkdvb2QgbmV3cyEgWW91ciBvcmRlciBpcyBvbiB0aGUgd2F5IGFuZCBp +bnZvaWNlIGlzIGF0dGFjaGVkIQoKWW91IHdpbGwgZmluZCB5b3VyIHRyYWNraW5nIG51bWJlciBv +biB0aGUgaW52b2ljZS4gVHJhY2tpbmcgZGF0YSBtYXkgdGFrZQp1cCB0byAyNCBob3VycyB0byBi +ZSBhY2Nlc3NpYmxlIG9ubGluZS4KCuKAoiBQbGVhc2UgcmVtaXQgcGF5bWVudCBhdCB5b3VyIGVh +cmxpZXN0IGNvbnZlbmllbmNlIHVubGVzcyBpbnZvaWNlIGlzCm1hcmtlZCDigJxQQUlE4oCdLgri +gKIgU29tZSBpdGVtcyBtYXkgc2hpcCBzZXBhcmF0ZWx5IGZyb20gbXVsdGlwbGUgbG9jYXRpb25z +LiBTZXBhcmF0ZQppbnZvaWNlcyB3aWxsIGJlIGlzc3VlZCB3aGVuIGFwcGxpY2FibGUuCuKAoiBQ +TEVBU0UgSU5TUEVDVCBVUE9OIFJFQ0VJUFQgRk9SIFBBVFRFUk4sIENPTE9SLCBERUZFQ1RTLCBE +QU1BR0UgRlJPTQpTSElQUElORywgQ09SUkVDVCBZQVJEQUdFLCBFVEMhIE9uY2UgYW4gb3JkZXIg +aXMgY3V0IG9yIHNld24sIG5vIHJldHVybnMKd2lsbCBiZSBhY2NlcHRlZCBmb3IgYW55IHJlYXNv +biwgbm8gbWF0dGVyIHRoZSBwYXJ0eSBpbiBlcnJvci4gTm8gcmV0dXJucwp3aWxsIGJlIGF1dGhv +cml6ZWQgYWZ0ZXIgMzAgZGF5cyBvZiBpbnZvaWNlIGRhdGUuIE5vIGV4Y2VwdGlvbnMgd2lsbCBi +ZQptYWRlLgoKVGhhbmsgeW91ciBmb3IgeW91ciBidXNpbmVzcyEKCk5haWxkb2N0b3IgRmFicmlj +cw== + +--cbace12de35ef4eae53fd974ccd41cb2aee4f9c9c76057ec8bfdd0c97813 +Content-Transfer-Encoding: base64 +Content-Type: text/html; charset=UTF-8 + +PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ImVuIj4KPGJvZHk+CjxwPlRoaXMgaXMgSFRNTCA8 +c3Ryb25nPmluIEJPTEQ8L3N0cm9uZz4KPHA+ClRoaXMgaXMgYW4gZW1iZWRkZWQgcGljdHVyZTog +CjxpbWcgYWx0PSJwaXhlbC5wbmciIHNyYz0iY2lkOmltYWdlMS5wbmciPgo8YnI+Rm9vPC9wPg== + +--cbace12de35ef4eae53fd974ccd41cb2aee4f9c9c76057ec8bfdd0c97813-- + +--5897e40a22c608e252cfab849e966112fcbd5a1c291208026b3ca2bfab8a +Content-Disposition: inline; filename="pixel.png" +Content-Id: image1.png +Content-Transfer-Encoding: base64 +Content-Type: image/png; name="pixel.png" + +iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAAAAACoWZBhAAAFEmlUWHRYTUw6Y29tLmFkb2JlLnht +cAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQi +Pz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUg +NS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIy +LXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1s +bnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgeG1sbnM6cGhvdG9zaG9w +PSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIgogICAgeG1sbnM6ZXhpZj0iaHR0 +cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRv +YmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hh +cC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9z +VHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgeG1wOkNyZWF0ZURhdGU9IjIwMjQtMDYtMjhUMTM6MjY6 +MDYrMDIwMCIKICAgeG1wOk1vZGlmeURhdGU9IjIwMjQtMDYtMjhUMTM6Mjc6MDgrMDI6MDAiCiAg +IHhtcDpNZXRhZGF0YURhdGU9IjIwMjQtMDYtMjhUMTM6Mjc6MDgrMDI6MDAiCiAgIHBob3Rvc2hv +cDpEYXRlQ3JlYXRlZD0iMjAyNC0wNi0yOFQxMzoyNjowNiswMjAwIgogICBwaG90b3Nob3A6Q29s +b3JNb2RlPSIxIgogICBwaG90b3Nob3A6SUNDUHJvZmlsZT0iR3JleXNjYWxlIEQ1MCIKICAgZXhp +ZjpQaXhlbFhEaW1lbnNpb249IjEwIgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iMTAiCiAgIGV4 +aWY6Q29sb3JTcGFjZT0iNjU1MzUiCiAgIHRpZmY6SW1hZ2VXaWR0aD0iMTAiCiAgIHRpZmY6SW1h +Z2VMZW5ndGg9IjEwIgogICB0aWZmOlJlc29sdXRpb25Vbml0PSIyIgogICB0aWZmOlhSZXNvbHV0 +aW9uPSI3Mi8xIgogICB0aWZmOllSZXNvbHV0aW9uPSI3Mi8xIj4KICAgPHhtcE1NOkhpc3Rvcnk+ +CiAgICA8cmRmOlNlcT4KICAgICA8cmRmOmxpCiAgICAgIHN0RXZ0OmFjdGlvbj0icHJvZHVjZWQi +CiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFmZmluaXR5IFBob3RvIDIgMi4zLjAiCiAgICAg +IHN0RXZ0OndoZW49IjIwMjQtMDYtMjhUMTM6Mjc6MDgrMDI6MDAiLz4KICAgIDwvcmRmOlNlcT4K +ICAgPC94bXBNTTpIaXN0b3J5PgogIDwvcmRmOkRlc2NyaXB0aW9uPgogPC9yZGY6UkRGPgo8L3g6 +eG1wbWV0YT4KPD94cGFja2V0IGVuZD0iciI/PpwIGG4AAADdaUNDUEdyZXlzY2FsZSBENTAAABiV +dVC9CsJAGEul6KCDg4vSoQ+gIIjiKoou6qAVrLiUs/5gq8e1In0v30TwGRycnc0VcRD9IJdwfMmR +A4xlIMLIrAPhIVaDSceduws7d0cWFvIow/JEJEfTvoO/87zB0Hyt6az/ez/HXPmRIF+IlpAqJj+I +4TmW1EaburR3Jl3qIXUxDE7i7dWvFvzDbEquEBYGUPCRIIKAh4DaRg9N6H6/ffXUN8aRm4KnpFth +hw22iFHl7YlpOmedZvtMTfQffXeXnvI+rTKNxguyvDKvB7U4qQAAAAlwSFlzAAALEwAACxMBAJqc +GAAAABFJREFUCJljnMoAA0wMNGcCAEQrAKk9oHKhAAAAAElFTkSuQmCC + +--5897e40a22c608e252cfab849e966112fcbd5a1c291208026b3ca2bfab8a-- + +--fe785e0384e2607697cc2ecb17cce003003bb7ca9112104f3e8ce727edb5 +Content-Disposition: attachment; filename="attachment.png" +Content-Transfer-Encoding: base64 +Content-Type: image/png; name="attachment.png" + +iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAAAAACoWZBhAAAFEmlUWHRYTUw6Y29tLmFkb2JlLnht +cAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQi +Pz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUg +NS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIy +LXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1s +bnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgeG1sbnM6cGhvdG9zaG9w +PSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIgogICAgeG1sbnM6ZXhpZj0iaHR0 +cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRv +YmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hh +cC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9z +VHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgeG1wOkNyZWF0ZURhdGU9IjIwMjQtMDYtMjhUMTM6MjY6 +MDYrMDIwMCIKICAgeG1wOk1vZGlmeURhdGU9IjIwMjQtMDYtMjhUMTM6Mjc6MDgrMDI6MDAiCiAg +IHhtcDpNZXRhZGF0YURhdGU9IjIwMjQtMDYtMjhUMTM6Mjc6MDgrMDI6MDAiCiAgIHBob3Rvc2hv +cDpEYXRlQ3JlYXRlZD0iMjAyNC0wNi0yOFQxMzoyNjowNiswMjAwIgogICBwaG90b3Nob3A6Q29s +b3JNb2RlPSIxIgogICBwaG90b3Nob3A6SUNDUHJvZmlsZT0iR3JleXNjYWxlIEQ1MCIKICAgZXhp +ZjpQaXhlbFhEaW1lbnNpb249IjEwIgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iMTAiCiAgIGV4 +aWY6Q29sb3JTcGFjZT0iNjU1MzUiCiAgIHRpZmY6SW1hZ2VXaWR0aD0iMTAiCiAgIHRpZmY6SW1h +Z2VMZW5ndGg9IjEwIgogICB0aWZmOlJlc29sdXRpb25Vbml0PSIyIgogICB0aWZmOlhSZXNvbHV0 +aW9uPSI3Mi8xIgogICB0aWZmOllSZXNvbHV0aW9uPSI3Mi8xIj4KICAgPHhtcE1NOkhpc3Rvcnk+ +CiAgICA8cmRmOlNlcT4KICAgICA8cmRmOmxpCiAgICAgIHN0RXZ0OmFjdGlvbj0icHJvZHVjZWQi +CiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFmZmluaXR5IFBob3RvIDIgMi4zLjAiCiAgICAg +IHN0RXZ0OndoZW49IjIwMjQtMDYtMjhUMTM6Mjc6MDgrMDI6MDAiLz4KICAgIDwvcmRmOlNlcT4K +ICAgPC94bXBNTTpIaXN0b3J5PgogIDwvcmRmOkRlc2NyaXB0aW9uPgogPC9yZGY6UkRGPgo8L3g6 +eG1wbWV0YT4KPD94cGFja2V0IGVuZD0iciI/PpwIGG4AAADdaUNDUEdyZXlzY2FsZSBENTAAABiV +dVC9CsJAGEul6KCDg4vSoQ+gIIjiKoou6qAVrLiUs/5gq8e1In0v30TwGRycnc0VcRD9IJdwfMmR +A4xlIMLIrAPhIVaDSceduws7d0cWFvIow/JEJEfTvoO/87zB0Hyt6az/ez/HXPmRIF+IlpAqJj+I +4TmW1EaburR3Jl3qIXUxDE7i7dWvFvzDbEquEBYGUPCRIIKAh4DaRg9N6H6/ffXUN8aRm4KnpFth +hw22iFHl7YlpOmedZvtMTfQffXeXnvI+rTKNxguyvDKvB7U4qQAAAAlwSFlzAAALEwAACxMBAJqc +GAAAABFJREFUCJljnMoAA0wMNGcCAEQrAKk9oHKhAAAAAElFTkSuQmCC + +--fe785e0384e2607697cc2ecb17cce003003bb7ca9112104f3e8ce727edb5--` ) func TestEMLToMsgFromString(t *testing.T) { @@ -562,6 +745,13 @@ func TestEMLToMsgFromStringBrokenTo(t *testing.T) { } } +func TestEMLToMsgFromStringNoBoundary(t *testing.T) { + _, err := EMLToMsgFromString(exampleMailPlainB64WithAttachmentNoBoundary) + if err == nil { + t.Error("EML with no boundary was supposed to fail, but didn't") + } +} + func TestEMLToMsgFromStringWithAttachment(t *testing.T) { wantSubject := "Example mail // plain text base64 with attachment" msg, err := EMLToMsgFromString(exampleMailPlainB64WithAttachment) @@ -590,7 +780,68 @@ func TestEMLToMsgFromStringWithEmbed(t *testing.T) { } if len(msg.embeds) != 1 { t.Errorf("EMLToMsgFromString of EML with embed failed: expected no. of embeds: %d, but got: %d", - 1, len(msg.attachments)) + 1, len(msg.embeds)) + } + msg, err = EMLToMsgFromString(exampleMailPlainB64WithEmbedNoContentID) + if err != nil { + t.Errorf("EML with embed failed: %s", err) + } + if subject := msg.GetGenHeader(HeaderSubject); len(subject) > 0 && !strings.EqualFold(subject[0], wantSubject) { + t.Errorf("EMLToMsgFromString of EML with embed failed: expected subject: %s, but got: %s", + wantSubject, subject[0]) + } + if len(msg.embeds) != 1 { + t.Errorf("EMLToMsgFromString of EML with embed failed: expected no. of embeds: %d, but got: %d", + 1, len(msg.embeds)) + } +} + +func TestEMLToMsgFromStringMultipartMixedAlternativeRelated(t *testing.T) { + wantSubject := "Example mail // plain text base64 with attachment, embed and alternative part" + msg, err := EMLToMsgFromString(exampleMailMultipartMixedAlternativeRelated) + if err != nil { + t.Errorf("EML multipart mixed, related, alternative failed: %s", err) + } + if subject := msg.GetGenHeader(HeaderSubject); len(subject) > 0 && !strings.EqualFold(subject[0], wantSubject) { + t.Errorf("EMLToMsgFromString of EML multipart mixed, related, alternative failed: expected subject: %s,"+ + " but got: %s", wantSubject, subject[0]) + } + if len(msg.embeds) != 1 { + t.Errorf("EMLToMsgFromString of EML multipart mixed, related, alternative failed: expected no. of "+ + "embeds: %d, but got: %d", 1, len(msg.embeds)) + } + if len(msg.attachments) != 1 { + t.Errorf("EMLToMsgFromString of EML multipart mixed, related, alternative failed: expected no. of "+ + "attachments: %d, but got: %d", 1, len(msg.attachments)) + } + if len(msg.parts) != 3 { + t.Errorf("EMLToMsgFromString of EML multipart mixed, related, alternative failed: expected no. of "+ + "parts: %d, but got: %d", 3, len(msg.parts)) + } + + var hasPlain, hasHTML, hasAlternative bool + for _, part := range msg.parts { + if strings.EqualFold(part.contentType.String(), TypeMultipartAlternative.String()) { + hasAlternative = true + } + if strings.EqualFold(part.contentType.String(), TypeTextPlain.String()) { + hasPlain = true + } + if strings.EqualFold(part.contentType.String(), TypeTextHTML.String()) { + hasHTML = true + } + } + if !hasPlain { + t.Error("EMLToMsgFromString of EML multipart mixed, related, alternative failed: expected PLAIN " + + "but got none") + } + if !hasHTML { + t.Error("EMLToMsgFromString of EML multipart mixed, related, alternative failed: expected HTML " + + "but got none") + } + if !hasAlternative { + t.Error("EMLToMsgFromString of EML multipart mixed, related, alternative failed: expected Alternative " + + "but got none") } } From 4e7082a54065c7fef762ff76d69855594ee20550 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 28 Jun 2024 13:49:21 +0200 Subject: [PATCH 43/47] Rename WithContentID to WithFileContentID In the eml.go and file.go files, the function WithContentID has been renamed to WithFileContentID. This aligns more accurately with the function purpose, which is to set the content ID for a File object. --- eml.go | 2 +- file.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eml.go b/eml.go index 844a5ad..ed20d0d 100644 --- a/eml.go +++ b/eml.go @@ -405,7 +405,7 @@ func parseEMLAttachmentEmbed(contentDisposition []string, multiPart *multipart.P } case "inline": if contentID, _ := parseMultiPartHeader(multiPart.Header.Get(HeaderContentID.String())); contentID != "" { - if err := msg.EmbedReader(filename, dataReader, WithContentID(contentID)); err != nil { + if err := msg.EmbedReader(filename, dataReader, WithFileContentID(contentID)); err != nil { return fmt.Errorf("failed to embed multipart body: %w", err) } return nil diff --git a/file.go b/file.go index a759b3d..45e142a 100644 --- a/file.go +++ b/file.go @@ -22,8 +22,8 @@ type File struct { Writer func(w io.Writer) (int64, error) } -// WithContentID sets the Content-ID header for the File -func WithContentID(id string) FileOption { +// WithFileContentID sets the Content-ID header for the File +func WithFileContentID(id string) FileOption { return func(f *File) { f.Header.Set(HeaderContentID.String(), id) } From 8924e426d65ca0601b552d4e2a23be6f88723c67 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 28 Jun 2024 13:49:34 +0200 Subject: [PATCH 44/47] Add test for WithFileContentID option The change introduces a new unit test, TestFile_WithContentID, in file_test.go. This test aims to verify the correct function of the WithFileContentID option by using differing scenarios, with assertions for error control and validation of expected content. --- file_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/file_test.go b/file_test.go index 8815d05..43b8cfe 100644 --- a/file_test.go +++ b/file_test.go @@ -56,6 +56,32 @@ func TestFile_WithFileDescription(t *testing.T) { } } +// TestFile_WithContentID tests the WithFileContentID option +func TestFile_WithContentID(t *testing.T) { + tests := []struct { + name string + contentid string + }{ + {"File Content-ID: test", "test"}, + {"File Content-ID: empty", ""}, + } + for _, tt := range tests { + m := NewMsg() + t.Run(tt.name, func(t *testing.T) { + m.AttachFile("file.go", WithFileContentID(tt.contentid)) + al := m.GetAttachments() + if len(al) <= 0 { + t.Errorf("AttachFile() failed. Attachment list is empty") + } + a := al[0] + if a.Header.Get(HeaderContentID.String()) != tt.contentid { + t.Errorf("WithFileContentID() failed. Expected: %s, got: %s", tt.contentid, + a.Header.Get(HeaderContentID.String())) + } + }) + } +} + // TestFile_WithFileEncoding tests the WithFileEncoding option func TestFile_WithFileEncoding(t *testing.T) { tests := []struct { From 2fa52e534132a0f93183d2c5c861fb8c75a6d289 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 28 Jun 2024 13:51:49 +0200 Subject: [PATCH 45/47] Add EML file support to go-mail in README The README has been updated to show that go-mail now supports outputting a message as an EML file and parsing an EML file into a go-mail message. This addition enhances the flexibility and control over the email content and format. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d9b4e07..260b888 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ Some of the features of this library: * [X] Debug logging of SMTP traffic * [X] Custom error types for delivery errors * [X] Custom dial-context functions for more control over the connection (proxing, DNS hooking, etc.) +* [X] Output a go-mail message as EML file and parse EML file into a go-mail message go-mail works like a programatic email client and provides lots of methods and functionalities you would consider standard in a MUA. From d9290973be37109d4c221d4b8f3b94b5cc179fd9 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 28 Jun 2024 13:52:04 +0200 Subject: [PATCH 46/47] Bump version to 0.4.2 --- doc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc.go b/doc.go index f38b857..5f41bc9 100644 --- a/doc.go +++ b/doc.go @@ -6,4 +6,4 @@ package mail // VERSION is used in the default user agent string -const VERSION = "0.4.1" +const VERSION = "0.4.2" From 7e4bb00538d7e260053e2a9a97c44249254707bc Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 28 Jun 2024 13:59:08 +0200 Subject: [PATCH 47/47] Add error handling for writing EML string to temp file Added conditional statements to handle potential failures when writing the EML string to the temporary files during testing. These updates ensure test failures due to write errors are accurately reflected in test results. Also, a minor fix is implemented on file permission in the os.WriteFile function. --- eml_test.go | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/eml_test.go b/eml_test.go index 587c4bf..8823eec 100644 --- a/eml_test.go +++ b/eml_test.go @@ -653,6 +653,9 @@ func TestEMLToMsgFromReaderFailing(t *testing.T) { func TestEMLToMsgFromFileFailing(t *testing.T) { tempDir, tempFile, err := stringToTempFile(exampleMailPlainBrokenFrom, "testmail") + if err != nil { + t.Errorf("failed to write EML string to temp file: %s", err) + } _, err = EMLToMsgFromFile(tempFile) if err == nil { t.Error("EML from Reader with broken FROM was supposed to fail, but didn't") @@ -661,6 +664,9 @@ func TestEMLToMsgFromFileFailing(t *testing.T) { t.Error("failed to remove temp dir:", err) } tempDir, tempFile, err = stringToTempFile(exampleMailPlainBrokenHeader, "testmail") + if err != nil { + t.Errorf("failed to write EML string to temp file: %s", err) + } _, err = EMLToMsgFromFile(tempFile) if err == nil { t.Error("EML from Reader with broken header was supposed to fail, but didn't") @@ -669,6 +675,9 @@ func TestEMLToMsgFromFileFailing(t *testing.T) { t.Error("failed to remove temp dir:", err) } tempDir, tempFile, err = stringToTempFile(exampleMailPlainB64BrokenBody, "testmail") + if err != nil { + t.Errorf("failed to write EML string to temp file: %s", err) + } _, err = EMLToMsgFromFile(tempFile) if err == nil { t.Error("EML from Reader with broken body was supposed to fail, but didn't") @@ -677,6 +686,9 @@ func TestEMLToMsgFromFileFailing(t *testing.T) { t.Error("failed to remove temp dir:", err) } tempDir, tempFile, err = stringToTempFile(exampleMailPlainBrokenBody, "testmail") + if err != nil { + t.Errorf("failed to write EML string to temp file: %s", err) + } _, err = EMLToMsgFromFile(tempFile) if err == nil { t.Error("EML from Reader with broken body was supposed to fail, but didn't") @@ -685,6 +697,9 @@ func TestEMLToMsgFromFileFailing(t *testing.T) { t.Error("failed to remove temp dir:", err) } tempDir, tempFile, err = stringToTempFile(exampleMailPlainUnknownContentType, "testmail") + if err != nil { + t.Errorf("failed to write EML string to temp file: %s", err) + } _, err = EMLToMsgFromFile(tempFile) if err == nil { t.Error("EML from Reader with unknown content type was supposed to fail, but didn't") @@ -693,6 +708,9 @@ func TestEMLToMsgFromFileFailing(t *testing.T) { t.Error("failed to remove temp dir:", err) } tempDir, tempFile, err = stringToTempFile(exampleMailPlainNoContentType, "testmail") + if err != nil { + t.Errorf("failed to write EML string to temp file: %s", err) + } _, err = EMLToMsgFromFile(tempFile) if err == nil { t.Error("EML from Reader with no content type was supposed to fail, but didn't") @@ -701,6 +719,9 @@ func TestEMLToMsgFromFileFailing(t *testing.T) { t.Error("failed to remove temp dir:", err) } tempDir, tempFile, err = stringToTempFile(exampleMailPlainUnsupportedTransferEnc, "testmail") + if err != nil { + t.Errorf("failed to write EML string to temp file: %s", err) + } _, err = EMLToMsgFromFile(tempFile) if err == nil { t.Error("EML from Reader with unsupported Transer-Encoding was supposed to fail, but didn't") @@ -852,7 +873,7 @@ func stringToTempFile(data, name string) (string, string, error) { return tempDir, "", fmt.Errorf("failed to create temp dir: %w", err) } filePath := fmt.Sprintf("%s/%s", tempDir, name) - err = os.WriteFile(filePath, []byte(data), 0666) + err = os.WriteFile(filePath, []byte(data), 0o666) if err != nil { return tempDir, "", fmt.Errorf("failed to write data to temp file: %w", err) }