Merge pull request #249 from wneessen/feature/145_add-eml-parser-to-generate-msg-from-eml-files

This PR introduces an EML parser to go-mail. It allows to read generic EML data from a file, a string or a reader into a go-mail Msg struct. It supports all types of message parts and encodings. It should be able to recognize Mulitpart messages as well as attachments and embeds (inline attachments).

This PR closes #145
This commit is contained in:
Winni Neessen 2024-06-28 14:11:30 +02:00 committed by GitHub
commit ffdea8337e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1406 additions and 17 deletions

View file

@ -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.

2
doc.go
View file

@ -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"

418
eml.go Normal file
View file

@ -0,0 +1,418 @@
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
//
// SPDX-License-Identifier: MIT
package mail
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"mime"
"mime/multipart"
"mime/quotedprintable"
netmail "net/mail"
"os"
"strings"
)
// EMLToMsgFromString will parse a given EML string and returns a pre-filled Msg pointer
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(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,
}
parsedMsg, bodybuf, err := readEMLFromReader(reader)
if err != nil || parsedMsg == nil {
return msg, fmt.Errorf("failed to parse EML from reader: %w", err)
}
if err := parseEML(parsedMsg, bodybuf, msg); err != nil {
return msg, fmt.Errorf("failed to parse EML contents: %w", err)
}
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(filePath string) (*Msg, error) {
msg := &Msg{
addrHeader: make(map[AddrHeader][]*netmail.Address),
genHeader: make(map[Header][]string),
preformHeader: make(map[Header]string),
mimever: MIME10,
}
parsedMsg, bodybuf, err := readEML(filePath)
if err != nil || parsedMsg == nil {
return msg, fmt.Errorf("failed to parse EML file: %w", err)
}
if err := parseEML(parsedMsg, bodybuf, msg); err != nil {
return msg, fmt.Errorf("failed to parse EML contents: %w", err)
}
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)
if err != nil {
return nil, nil, fmt.Errorf("failed to open EML file: %w", err)
}
defer func() {
_ = fileHandle.Close()
}()
return readEMLFromReader(fileHandle)
}
// readEMLFromReader uses net/mail to parse the header and body from a given io.Reader
func readEMLFromReader(reader io.Reader) (*netmail.Message, *bytes.Buffer, error) {
parsedMsg, err := netmail.ReadMessage(reader)
if err != nil {
return parsedMsg, nil, fmt.Errorf("failed to parse EML: %w", err)
}
buf := bytes.Buffer{}
if _, err = buf.ReadFrom(parsedMsg.Body); err != nil {
return nil, nil, err
}
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(mailHeader *netmail.Header, msg *Msg) error {
commonHeaders := []Header{
HeaderContentType, HeaderImportance, HeaderInReplyTo, HeaderListUnsubscribe,
HeaderListUnsubscribePost, HeaderMessageID, HeaderMIMEVersion, HeaderOrganization,
HeaderPrecedence, HeaderPriority, HeaderReferences, HeaderSubject, HeaderUserAgent,
HeaderXMailer, HeaderXMSMailPriority, HeaderXPriority,
}
// Extract content type, charset and encoding first
parseEMLEncoding(mailHeader, msg)
parseEMLContentTypeCharset(mailHeader, msg)
// Extract address headers
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)
}
}
addrHeaders := map[AddrHeader]func(...string) error{
HeaderTo: msg.To,
HeaderCc: msg.Cc,
HeaderBcc: msg.Bcc,
}
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 _, addr := range parsedAddrs {
addrStrings = append(addrStrings, addr.String())
}
if err = addrFunc(addrStrings...); err != nil {
return fmt.Errorf(`failed to parse %q header: %w`, addrHeader, err)
}
}
}
// Extract date from message
date, err := mailHeader.Date()
if err != nil {
switch {
case errors.Is(err, netmail.ErrHeaderNotPresent):
msg.SetDate()
default:
return fmt.Errorf("failed to parse EML date: %w", err)
}
}
if err == nil {
msg.SetDateWithValue(date)
}
// 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)
}
}
return nil
}
// parseEMLBodyParts parses the body of a EML based on the different content types and encodings
func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
// Extract the transfer encoding of the body
mediatype, params, err := mime.ParseMediaType(parsedMsg.Header.Get(HeaderContentType.String()))
if err != nil {
return fmt.Errorf("failed to extract content type: %w", err)
}
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, parsedMsg, bodybuf, msg); err != nil {
return fmt.Errorf("failed to parse plain body: %w", err)
}
case strings.EqualFold(mediatype, TypeMultipartAlternative.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 body: %w", err)
}
default:
return fmt.Errorf("failed to parse body, unknown content type: %s", mediatype)
}
return nil
}
// parseEMLBodyPlain parses the mail body of plain type mails
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(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)
}
msg.SetBodyString(ContentType(mediatype), qpBuffer.String())
return 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)
}
msg.SetBodyString(ContentType(mediatype), b64Buffer.String())
return nil
}
return fmt.Errorf("unsupported Content-Transfer-Encoding")
}
// 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")
}
multipartReader := multipart.NewReader(bodybuf, boundary)
ReadNextPart:
multiPart, err := multipartReader.NextPart()
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 {
// 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])
if strings.EqualFold(contentType, TypeMultipartRelated.String()) ||
strings.EqualFold(contentType, TypeMultipartAlternative.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 {
return fmt.Errorf("failed to parse attachment/embed: %w", err)
}
goto ReadNextPart
}
multiPartData, mperr := io.ReadAll(multiPart)
if mperr != nil {
_ = multiPart.Close()
return fmt.Errorf("failed to read multipart: %w", err)
}
multiPartContentType, ok := multiPart.Header[HeaderContentType.String()]
if !ok {
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))
}
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
mutliPartTransferEnc = []string{EncodingQP.String()}
}
switch {
case strings.EqualFold(mutliPartTransferEnc[0], EncodingB64.String()):
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()):
part.SetContent(string(multiPartData))
default:
return fmt.Errorf("unsupported Content-Transfer-Encoding")
}
msg.parts = append(msg.parts, part)
multiPart, err = multipartReader.NextPart()
}
if !errors.Is(err, io.EOF) {
return fmt.Errorf("failed to read multipart: %w", err)
}
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 != "" && !strings.EqualFold(contentType, TypeMultipartMixed.String()) {
msg.SetGenHeader(HeaderContentType, contentType)
}
}
}
// handleEMLMultiPartBase64Encoding sets the content body of a base64 encoded Part
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)
}
part.SetContent(string(content))
return nil
}
// 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 {
optString := strings.TrimLeft(headerSplit[1], " ")
optSplit := strings.SplitN(optString, "=", 2)
if len(optSplit) == 2 {
optional[optSplit[0]] = optSplit[1]
}
}
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]
}
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, dataReader); err != nil {
return fmt.Errorf("failed to attach multipart body: %w", err)
}
case "inline":
if contentID, _ := parseMultiPartHeader(multiPart.Header.Get(HeaderContentID.String())); contentID != "" {
if err := msg.EmbedReader(filename, dataReader, WithFileContentID(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)
}
}
return nil
}

881
eml_test.go Normal file
View file

@ -0,0 +1,881 @@
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
//
// SPDX-License-Identifier: MIT
package mail
import (
"bytes"
"fmt"
"os"
"strings"
"testing"
"time"
)
const (
exampleMailPlainNoEnc = `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: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
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`
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" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
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" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
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" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
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" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
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: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
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" <go-mail@go-mail.dev>
To: go-mail+test.go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
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`
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" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
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" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
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`
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" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
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`
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" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
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
--
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" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: base64
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" <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--`
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" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
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
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--`
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--`
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" <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-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" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
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) {
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) {
msg, err := EMLToMsgFromString(tt.eml)
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])
}
})
}
}
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, tempFile, err := stringToTempFile(tt.eml, tt.name)
defer func() {
if err = os.RemoveAll(tempDir); err != nil {
t.Error("failed to remove temp dir:", err)
}
}()
msg, err := EMLToMsgFromFile(tempFile)
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])
}
})
}
}
func TestEMLToMsgFromReaderFailing(t *testing.T) {
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)
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 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")
}
if err = os.RemoveAll(tempDir); err != nil {
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")
}
if err = os.RemoveAll(tempDir); err != nil {
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")
}
if err = os.RemoveAll(tempDir); err != nil {
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")
}
if err = os.RemoveAll(tempDir); err != nil {
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")
}
if err = os.RemoveAll(tempDir); err != nil {
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")
}
if err = os.RemoveAll(tempDir); err != nil {
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")
}
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 {
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)
}
}
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 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)
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))
}
}
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)
}
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))
}
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")
}
}
// 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), 0o666)
if err != nil {
return tempDir, "", fmt.Errorf("failed to write data to temp file: %w", err)
}
return tempDir, filePath, nil
}

View file

@ -132,17 +132,20 @@ 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
const (
TypeTextPlain ContentType = "text/plain"
TypeTextHTML ContentType = "text/html"
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"
TypeTextPlain ContentType = "text/plain"
)
// List of MIMETypes
@ -152,12 +155,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)
}

View file

@ -27,6 +27,50 @@ 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: 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",
},
{
"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 {

View file

@ -22,6 +22,13 @@ type File struct {
Writer func(w io.Writer) (int64, error)
}
// WithFileContentID sets the Content-ID header for the File
func WithFileContentID(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) {

View file

@ -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 {

View file

@ -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"

View file

@ -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"},

2
msg.go
View file

@ -136,7 +136,7 @@ func NewMsg(opts ...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

View file

@ -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 {

View file

@ -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() {