Compare commits

..

2 commits

Author SHA1 Message Date
4c88dce2a8
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.
2024-06-22 14:24:52 +02:00
bb33c4a921
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.
2024-06-22 14:13:26 +02:00
2 changed files with 71 additions and 15 deletions

35
eml.go
View file

@ -70,6 +70,7 @@ func EMLToMsgFromFile(filePath string) (*Msg, error) {
if err = parseEMLBodyParts(parsedMsg, bodybuf, msg); err != nil { if err = parseEMLBodyParts(parsedMsg, bodybuf, msg); err != nil {
return msg, fmt.Errorf("failed to parse EML body parts: %w", err) return msg, fmt.Errorf("failed to parse EML body parts: %w", err)
} }
//fmt.Printf("FOO: %+v\n", msg)
return msg, nil return msg, nil
} }
@ -159,6 +160,10 @@ func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error {
// Extract common headers // Extract common headers
for _, header := range commonHeaders { for _, header := range commonHeaders {
if value := mailHeader.Get(header.String()); value != "" { if value := mailHeader.Get(header.String()); value != "" {
if strings.EqualFold(header.String(), HeaderContentType.String()) &&
strings.HasPrefix(value, TypeMultipartMixed.String()) {
continue
}
msg.SetGenHeader(header, value) 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) 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, TypeMultipartMixed.String()),
strings.EqualFold(mediatype, TypeMultipartRelated.String()):
if err = parseEMLMultipart(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 body: %w", err)
} }
default: default:
} }
@ -242,9 +248,27 @@ ReadNextPart:
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 {
// 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 // Content-Disposition header means we have an attachment or embed
if contentDisposition, ok := multiPart.Header[HeaderContentDisposition.String()]; ok { 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) return fmt.Errorf("failed to parse attachment/embed: %w", err)
} }
goto ReadNextPart goto ReadNextPart
@ -261,6 +285,9 @@ ReadNextPart:
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, optional := parseMultiPartHeader(multiPartContentType[0])
if strings.EqualFold(contentType, TypeMultipartRelated.String()) {
goto ReadNextPart
}
part := msg.newPart(ContentType(contentType)) part := msg.newPart(ContentType(contentType))
if charset, ok := optional["charset"]; ok { if charset, ok := optional["charset"]; ok {
part.SetCharset(Charset(charset)) part.SetCharset(Charset(charset))
@ -316,7 +343,7 @@ func parseEMLContentTypeCharset(mailHeader *netmail.Header, msg *Msg) {
msg.SetCharset(Charset(charset)) msg.SetCharset(Charset(charset))
} }
msg.setEncoder() msg.setEncoder()
if contentType != "" { if contentType != "" && !strings.EqualFold(contentType, TypeMultipartMixed.String()) {
msg.SetGenHeader(HeaderContentType, contentType) msg.SetGenHeader(HeaderContentType, contentType)
} }
} }

View file

@ -152,6 +152,33 @@ ICAgc2V2ZXJhbAogICAgICAgICAgICBuZXdsaW5lcwoJICAgICAgICAgICAgYW5kCgkgICAgc3Bh
Y2VzCiAgICAgaW4KICBpdAouCgpBcyB3ZWxsIGFzIGFuIGVtb2ppOiDwn5mCCg== Y2VzCiAgICAgaW4KICBpdAouCgpBcyB3ZWxsIGFzIGFuIGVtb2ppOiDwn5mCCg==
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7--` --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" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
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: <pixel.png>
Content-Transfer-Encoding: base64
Content-Type: image/png; name="pixel.png"
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2NgYGD4DwABBAEAwS2O
UAAAAABJRU5ErkJggg==
--ffbcfb94b44e5297325102f6ced05b3b37f1d70fc38a5e78dc73c1a8434b--`
) )
func TestEMLToMsgFromString(t *testing.T) { 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", t.Errorf("EMLToMsgFromString of EML with attachment failed: expected no. of attachments: %d, but got: %d",
1, len(msg.attachments)) 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 { func TestEMLToMsgFromStringWithEmbed(t *testing.T) {
t.Error("failed to split Content-Type header") wantSubject := "Example mail // plain text base64 with embed"
return msg, err := EMLToMsgFromString(exampleMailPlainB64WithEmbed)
if err != nil {
t.Errorf("EML with embed failed: %s", err)
} }
if !strings.EqualFold(contentTypeSplit[0], "multipart/mixed") { if subject := msg.GetGenHeader(HeaderSubject); len(subject) > 0 && !strings.EqualFold(subject[0], wantSubject) {
t.Errorf("EMLToMsgFromString of EML with attachment failed: expected content-type: %s, "+ t.Errorf("EMLToMsgFromString of EML with embed failed: expected subject: %s, but got: %s",
"but got: %s", "multipart/mixed", contentTypeSplit[0]) 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.attachments))
} }
} }