Compare commits

..

No commits in common. "52ef13a5d54f2b9986be9ca0f496b74e764578eb" and "047d923b45903fcb8c731448574e6008a9e6d2b3" have entirely different histories.

2 changed files with 49 additions and 208 deletions

131
eml.go
View file

@ -112,8 +112,26 @@ func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error {
} }
// Extract content type, charset and encoding first // Extract content type, charset and encoding first
parseEMLEncoding(mailHeader, msg) if value := mailHeader.Get(HeaderContentTransferEnc.String()); value != "" {
parseEMLContentTypeCharset(mailHeader, msg) 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)
}
}
// Extract address headers // Extract address headers
if value := mailHeader.Get(HeaderFrom.String()); value != "" { if value := mailHeader.Get(HeaderFrom.String()); value != "" {
@ -184,8 +202,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, TypeMultipartMixed.String()): strings.EqualFold(mediatype, "multipart/mixed"):
if err = parseEMLMultipart(params, bodybuf, msg); err != nil { if err = parseEMLMultipartAlternative(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:
@ -224,32 +242,18 @@ func parseEMLBodyPlain(mediatype string, parsedMsg *netmail.Message, bodybuf *by
return fmt.Errorf("unsupported Content-Transfer-Encoding") return fmt.Errorf("unsupported Content-Transfer-Encoding")
} }
// parseEMLMultipart parses a multipart body part of a EML // parseEMLMultipartAlternative parses a multipart/alternative body part of a EML
func parseEMLMultipart(params map[string]string, bodybuf *bytes.Buffer, msg *Msg) error { func parseEMLMultipartAlternative(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()
defer func() { if err != nil {
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()
@ -260,11 +264,9 @@ ReadNextPart:
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, optional := parseMultiPartHeader(multiPartContentType[0]) contentType, charSet := parseContentType(multiPartContentType[0])
part := msg.newPart(ContentType(contentType)) p := msg.newPart(ContentType(contentType))
if charset, ok := optional["charset"]; ok { p.SetCharset(Charset(charSet))
part.SetCharset(Charset(charset))
}
mutliPartTransferEnc, ok := multiPart.Header[HeaderContentTransferEnc.String()] mutliPartTransferEnc, ok := multiPart.Header[HeaderContentTransferEnc.String()]
if !ok { if !ok {
@ -276,52 +278,25 @@ ReadNextPart:
switch { switch {
case strings.EqualFold(mutliPartTransferEnc[0], EncodingB64.String()): case strings.EqualFold(mutliPartTransferEnc[0], EncodingB64.String()):
if err := handleEMLMultiPartBase64Encoding(multiPartData, part); err != nil { if err := handleEMLMultiPartBase64Encoding(multiPartData, p); 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()):
part.SetContent(string(multiPartData)) p.SetContent(string(multiPartData))
default: default:
return fmt.Errorf("unsupported Content-Transfer-Encoding") return fmt.Errorf("unsupported Content-Transfer-Encoding")
} }
msg.parts = append(msg.parts, part) msg.parts = append(msg.parts, p)
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)
@ -333,37 +308,19 @@ func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error {
return nil return nil
} }
// parseMultiPartHeader parses a multipart header and returns the value and optional parts as // parseContentType parses the Content-Type header and returns the type and charse as
// separate map // separate string values
func parseMultiPartHeader(multiPartHeader string) (header string, optional map[string]string) { func parseContentType(contentTypeHeader string) (contentType string, charSet string) {
optional = make(map[string]string) contentTypeSplit := strings.SplitN(contentTypeHeader, "; ", 2)
headerSplit := strings.SplitN(multiPartHeader, "; ", 2) if len(contentTypeSplit) != 2 {
header = headerSplit[0] return
if len(headerSplit) == 2 { }
optSplit := strings.SplitN(headerSplit[1], "=", 2) contentType = contentTypeSplit[0]
if len(optSplit) == 2 { if strings.HasPrefix(strings.ToLower(contentTypeSplit[1]), "charset=") {
optional[optSplit[0]] = optSplit[1] charSetSplit := strings.SplitN(contentTypeSplit[1], "=", 2)
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,8 +5,6 @@
package mail package mail
import ( import (
"fmt"
"os"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -121,37 +119,6 @@ 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) {
@ -176,68 +143,15 @@ 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) {
msg, err := EMLToMsgFromString(tt.eml) m, 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 msg.Encoding() != tt.enc { if m.Encoding() != tt.enc {
t.Errorf("EMLToMsgFromString failed: expected encoding: %s, but got: %s", tt.enc, msg.Encoding()) t.Errorf("EMLToMsgFromString failed: expected encoding: %s, but got: %s", tt.enc, m.Encoding())
} }
if subject := msg.GetGenHeader(HeaderSubject); len(subject) > 0 && !strings.EqualFold(subject[0], tt.sub) { if s := m.GetGenHeader(HeaderSubject); len(s) > 0 && !strings.EqualFold(s[0], tt.sub) {
t.Errorf("EMLToMsgFromString failed: expected subject: %s, but got: %s", t.Errorf("EMLToMsgFromString failed: expected subject: %s, but got: %s", tt.sub, s[0])
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])
} }
}) })
} }
@ -263,33 +177,3 @@ 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])
}
}