Compare commits

...

5 commits

Author SHA1 Message Date
52ef13a5d5
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.
2024-06-19 15:25:37 +02:00
e95799ad60
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.
2024-06-19 14:41:13 +02:00
85c07c22cc
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.
2024-06-19 13:57:00 +02:00
29305675d6
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.
2024-06-19 10:52:09 +02:00
04c41a1f10
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.
2024-06-19 10:42:29 +02:00
2 changed files with 208 additions and 49 deletions

131
eml.go
View file

@ -112,26 +112,8 @@ func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error {
} }
// Extract content type, charset and encoding first // Extract content type, charset and encoding first
if value := mailHeader.Get(HeaderContentTransferEnc.String()); value != "" { parseEMLEncoding(mailHeader, msg)
switch { parseEMLContentTypeCharset(mailHeader, msg)
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)
}
}
// Extract address headers // Extract address headers
if value := mailHeader.Get(HeaderFrom.String()); value != "" { if value := mailHeader.Get(HeaderFrom.String()); value != "" {
@ -202,8 +184,8 @@ func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *M
return fmt.Errorf("failed to parse plain body: %w", err) 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"): strings.EqualFold(mediatype, TypeMultipartMixed.String()):
if err = parseEMLMultipartAlternative(params, bodybuf, msg); err != nil { 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/alternative body: %w", err)
} }
default: default:
@ -242,18 +224,32 @@ func parseEMLBodyPlain(mediatype string, parsedMsg *netmail.Message, bodybuf *by
return fmt.Errorf("unsupported Content-Transfer-Encoding") return fmt.Errorf("unsupported Content-Transfer-Encoding")
} }
// parseEMLMultipartAlternative parses a multipart/alternative body part of a EML // parseEMLMultipart parses a multipart body part of a EML
func parseEMLMultipartAlternative(params map[string]string, bodybuf *bytes.Buffer, msg *Msg) error { func parseEMLMultipart(params map[string]string, bodybuf *bytes.Buffer, msg *Msg) error {
boundary, ok := params["boundary"] boundary, ok := params["boundary"]
if !ok { if !ok {
return fmt.Errorf("no boundary tag found in multipart body") return fmt.Errorf("no boundary tag found in multipart body")
} }
multipartReader := multipart.NewReader(bodybuf, boundary) multipartReader := multipart.NewReader(bodybuf, boundary)
ReadNextPart:
multiPart, err := multipartReader.NextPart() 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) return fmt.Errorf("failed to get next part of multipart message: %w", err)
} }
for err == nil { for err == nil {
// 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 {
return fmt.Errorf("failed to parse attachment/embed: %w", err)
}
goto ReadNextPart
}
multiPartData, mperr := io.ReadAll(multiPart) multiPartData, mperr := io.ReadAll(multiPart)
if mperr != nil { if mperr != nil {
_ = multiPart.Close() _ = multiPart.Close()
@ -264,9 +260,11 @@ func parseEMLMultipartAlternative(params map[string]string, bodybuf *bytes.Buffe
if !ok { if !ok {
return fmt.Errorf("failed to get content-type from part") return fmt.Errorf("failed to get content-type from part")
} }
contentType, charSet := parseContentType(multiPartContentType[0]) contentType, optional := parseMultiPartHeader(multiPartContentType[0])
p := msg.newPart(ContentType(contentType)) part := msg.newPart(ContentType(contentType))
p.SetCharset(Charset(charSet)) if charset, ok := optional["charset"]; ok {
part.SetCharset(Charset(charset))
}
mutliPartTransferEnc, ok := multiPart.Header[HeaderContentTransferEnc.String()] mutliPartTransferEnc, ok := multiPart.Header[HeaderContentTransferEnc.String()]
if !ok { if !ok {
@ -278,25 +276,52 @@ func parseEMLMultipartAlternative(params map[string]string, bodybuf *bytes.Buffe
switch { switch {
case strings.EqualFold(mutliPartTransferEnc[0], EncodingB64.String()): 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) return fmt.Errorf("failed to handle multipart base64 transfer-encoding: %w", err)
} }
case strings.EqualFold(mutliPartTransferEnc[0], EncodingQP.String()): case strings.EqualFold(mutliPartTransferEnc[0], EncodingQP.String()):
p.SetContent(string(multiPartData)) part.SetContent(string(multiPartData))
default: default:
return fmt.Errorf("unsupported Content-Transfer-Encoding") return fmt.Errorf("unsupported Content-Transfer-Encoding")
} }
msg.parts = append(msg.parts, p) msg.parts = append(msg.parts, part)
multiPart, err = multipartReader.NextPart() multiPart, err = multipartReader.NextPart()
} }
if !errors.Is(err, io.EOF) { if !errors.Is(err, io.EOF) {
_ = multiPart.Close()
return fmt.Errorf("failed to read multipart: %w", err) return fmt.Errorf("failed to read multipart: %w", err)
} }
return nil 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, optional := parseMultiPartHeader(value)
if charset, ok := optional["charset"]; ok {
msg.SetCharset(Charset(charset))
}
msg.setEncoder()
if contentType != "" {
msg.SetGenHeader(HeaderContentType, contentType)
}
}
}
// handleEMLMultiPartBase64Encoding sets the content body of a base64 encoded Part // handleEMLMultiPartBase64Encoding sets the content body of a base64 encoded Part
func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error { func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error {
part.SetEncoding(EncodingB64) part.SetEncoding(EncodingB64)
@ -308,19 +333,37 @@ func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error {
return nil return nil
} }
// parseContentType parses the Content-Type header and returns the type and charse as // parseMultiPartHeader parses a multipart header and returns the value and optional parts as
// separate string values // separate map
func parseContentType(contentTypeHeader string) (contentType string, charSet string) { func parseMultiPartHeader(multiPartHeader string) (header string, optional map[string]string) {
contentTypeSplit := strings.SplitN(contentTypeHeader, "; ", 2) optional = make(map[string]string)
if len(contentTypeSplit) != 2 { headerSplit := strings.SplitN(multiPartHeader, "; ", 2)
return header = headerSplit[0]
} if len(headerSplit) == 2 {
contentType = contentTypeSplit[0] optSplit := strings.SplitN(headerSplit[1], "=", 2)
if strings.HasPrefix(strings.ToLower(contentTypeSplit[1]), "charset=") { if len(optSplit) == 2 {
charSetSplit := strings.SplitN(contentTypeSplit[1], "=", 2) optional[optSplit[0]] = optSplit[1]
if len(charSetSplit) == 2 {
charSet = charSetSplit[1]
} }
} }
return 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)
}
}
return nil
}

View file

@ -5,6 +5,8 @@
package mail package mail
import ( import (
"fmt"
"os"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -119,6 +121,37 @@ RGVhciBDdXN0b21lciwKClRoaXMgaXMgYSB0ZXN0IG1haWwuIFBsZWFzZSBkbyBub3QgcmVwbHkg
dG8gdGhpcy4gQWxzbyB0aGlzIGxpbmUgaXMgdmVyeSBsb25nIHNvIGl0CnNob3VsZCBiZSB3cmFw dG8gdGhpcy4gQWxzbyB0aGlzIGxpbmUgaXMgdmVyeSBsb25nIHNvIGl0CnNob3VsZCBiZSB3cmFw
cGVkLgoKClRoYW5rIHlvdXIgZm9yIHlvdXIgYnVzaW5lc3MhClRoZSBnby1tYWlsIHRlYW0KCi0t cGVkLgoKClRoYW5rIHlvdXIgZm9yIHlvdXIgYnVzaW5lc3MhClRoZSBnby1tYWlsIHRlYW0KCi0t
ClRoaXMgaXMgYSBzaWduYXR1cmU=` 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" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
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) { func TestEMLToMsgFromString(t *testing.T) {
@ -143,15 +176,68 @@ func TestEMLToMsgFromString(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
m, err := EMLToMsgFromString(tt.eml) msg, err := EMLToMsgFromString(tt.eml)
if err != nil { if err != nil {
t.Errorf("failed to parse EML: %s", err) t.Errorf("failed to parse EML: %s", err)
} }
if m.Encoding() != tt.enc { if msg.Encoding() != tt.enc {
t.Errorf("EMLToMsgFromString failed: expected encoding: %s, but got: %s", tt.enc, m.Encoding()) t.Errorf("EMLToMsgFromString failed: expected encoding: %s, but got: %s", tt.enc, msg.Encoding())
} }
if s := m.GetGenHeader(HeaderSubject); len(s) > 0 && !strings.EqualFold(s[0], tt.sub) { if subject := msg.GetGenHeader(HeaderSubject); len(subject) > 0 && !strings.EqualFold(subject[0], tt.sub) {
t.Errorf("EMLToMsgFromString failed: expected subject: %s, but got: %s", tt.sub, s[0]) t.Errorf("EMLToMsgFromString failed: expected subject: %s, but got: %s",
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: %s", err)
}
if msg.Encoding() != tt.enc {
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: %s, but got: %s",
tt.sub, subject[0])
} }
}) })
} }
@ -177,3 +263,33 @@ func TestEMLToMsgFromStringBrokenDate(t *testing.T) {
t.Errorf("EML with no date expected: %s, got: %s", now.Format(time.RFC1123Z), d) 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])
}
}