go-mail/eml.go
Winni Neessen 3facbde703
Add new content types and refactor message writer
Introduced "multipart/mixed" and "multipart/related" content types in encoding.go and updated msgwriter.go to accommodate these. Adjustments made in related tests for these new types. Additionally, removed unnecessary print statements and improved multipart alternative parsing in eml.go.
2024-02-10 13:36:42 +01:00

326 lines
9.4 KiB
Go

// 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"
nm "net/mail"
"os"
"strings"
)
// EMLToMsgFromString will parse a given EML string and returns a pre-filled Msg pointer
func EMLToMsgFromString(es string) (*Msg, error) {
eb := bytes.NewBufferString(es)
return EMLToMsgFromReader(eb)
}
// EMLToMsgFromReader will parse a reader that holds EML content and returns a pre-filled
// Msg pointer
func EMLToMsgFromReader(r io.Reader) (*Msg, error) {
m := &Msg{
addrHeader: make(map[AddrHeader][]*nm.Address),
genHeader: make(map[Header][]string),
preformHeader: make(map[Header]string),
mimever: MIME10,
}
pm, bodybuf, err := readEMLFromReader(r)
if err != nil || pm == nil {
return m, fmt.Errorf("failed to parse EML from reader: %w", err)
}
if err = parseEMLHeaders(&pm.Header, m); err != nil {
return m, fmt.Errorf("failed to parse EML headers: %w", err)
}
if err = parseEMLBodyParts(pm, bodybuf, m); err != nil {
return m, fmt.Errorf("failed to parse EML body parts: %w", err)
}
return m, nil
}
// EMLToMsgFromFile will open and parse a .eml file at a provided file path and returns a
// pre-filled Msg pointer
func EMLToMsgFromFile(fp string) (*Msg, error) {
m := &Msg{
addrHeader: make(map[AddrHeader][]*nm.Address),
genHeader: make(map[Header][]string),
preformHeader: make(map[Header]string),
mimever: MIME10,
}
pm, bodybuf, err := readEML(fp)
if err != nil || pm == nil {
return m, fmt.Errorf("failed to parse EML file: %w", err)
}
if err = parseEMLHeaders(&pm.Header, m); err != nil {
return m, fmt.Errorf("failed to parse EML headers: %w", err)
}
if err = parseEMLBodyParts(pm, bodybuf, m); err != nil {
return m, fmt.Errorf("failed to parse EML body parts: %w", err)
}
return m, nil
}
// readEML opens an EML file and uses net/mail to parse the header and body
func readEML(fp string) (*nm.Message, *bytes.Buffer, error) {
fh, err := os.Open(fp)
if err != nil {
return nil, nil, fmt.Errorf("failed to open EML file: %w", err)
}
defer func() {
_ = fh.Close()
}()
return readEMLFromReader(fh)
}
// readEMLFromReader uses net/mail to parse the header and body from a given io.Reader
func readEMLFromReader(r io.Reader) (*nm.Message, *bytes.Buffer, error) {
pm, err := nm.ReadMessage(r)
if err != nil {
return pm, nil, fmt.Errorf("failed to parse EML: %w", err)
}
buf := bytes.Buffer{}
if _, err = buf.ReadFrom(pm.Body); err != nil {
return nil, nil, err
}
return pm, &buf, nil
}
// parseEMLHeaders will check the EML headers for the most common headers and set the
// according settings in the Msg
func parseEMLHeaders(mh *nm.Header, m *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
if v := mh.Get(HeaderContentTransferEnc.String()); v != "" {
switch {
case strings.EqualFold(v, EncodingQP.String()):
m.SetEncoding(EncodingQP)
case strings.EqualFold(v, EncodingB64.String()):
m.SetEncoding(EncodingB64)
default:
m.SetEncoding(NoEncoding)
}
}
if v := mh.Get(HeaderContentType.String()); v != "" {
ct, cs := parseContentType(v)
if cs != "" {
m.SetCharset(Charset(cs))
}
m.setEncoder()
if ct != "" {
m.SetGenHeader(HeaderContentType, ct)
}
}
// Extract address headers
if v := mh.Get(HeaderFrom.String()); v != "" {
if err := m.From(v); err != nil {
return fmt.Errorf(`failed to parse %q header: %w`, HeaderFrom, err)
}
}
ahl := map[AddrHeader]func(...string) error{
HeaderTo: m.To,
HeaderCc: m.Cc,
HeaderBcc: m.Bcc,
}
for h, f := range ahl {
if v := mh.Get(h.String()); v != "" {
var als []string
pal, err := nm.ParseAddressList(v)
if err != nil {
return fmt.Errorf(`failed to parse address list: %w`, err)
}
for _, a := range pal {
als = append(als, a.String())
}
if err := f(als...); err != nil {
return fmt.Errorf(`failed to parse %q header: %w`, HeaderTo, err)
}
}
}
// Extract date from message
d, err := mh.Date()
if err != nil {
switch {
case errors.Is(err, nm.ErrHeaderNotPresent):
m.SetDate()
default:
return fmt.Errorf("failed to parse EML date: %w", err)
}
}
if err == nil {
m.SetDateWithValue(d)
}
// Extract common headers
for _, h := range commonHeaders {
if v := mh.Get(h.String()); v != "" {
m.SetGenHeader(h, v)
}
}
return nil
}
// parseEMLBodyParts parses the body of a EML based on the different content types and encodings
func parseEMLBodyParts(pm *nm.Message, bodybuf *bytes.Buffer, m *Msg) error {
// Extract the transfer encoding of the body
mediatype, params, err := mime.ParseMediaType(pm.Header.Get(HeaderContentType.String()))
if err != nil {
return fmt.Errorf("failed to extract content type: %w", err)
}
if v, ok := params["charset"]; ok {
m.SetCharset(Charset(v))
}
switch {
case strings.EqualFold(mediatype, TypeTextPlain.String()),
strings.EqualFold(mediatype, TypeTextHTML.String()):
if err := parseEMLBodyPlain(mediatype, pm, bodybuf, m); err != nil {
return fmt.Errorf("failed to parse plain body: %w", err)
}
case strings.EqualFold(mediatype, TypeMultipartAlternative.String()),
strings.EqualFold(mediatype, "multipart/mixed"):
if err := parseEMLMultipartAlternative(params, bodybuf, m); err != nil {
return fmt.Errorf("failed to parse multipart/alternative body: %w", err)
}
default:
}
return nil
}
// parseEMLBodyPlain parses the mail body of plain type mails
func parseEMLBodyPlain(mediatype string, pm *nm.Message, bodybuf *bytes.Buffer, m *Msg) error {
cte := pm.Header.Get(HeaderContentTransferEnc.String())
if strings.EqualFold(cte, NoEncoding.String()) {
m.SetEncoding(NoEncoding)
m.SetBodyString(ContentType(mediatype), bodybuf.String())
return nil
}
if strings.EqualFold(cte, EncodingQP.String()) {
m.SetEncoding(EncodingQP)
qpr := quotedprintable.NewReader(bodybuf)
qpbuf := bytes.Buffer{}
if _, err := qpbuf.ReadFrom(qpr); err != nil {
return fmt.Errorf("failed to read quoted-printable body: %w", err)
}
m.SetBodyString(ContentType(mediatype), qpbuf.String())
return nil
}
if strings.EqualFold(cte, EncodingB64.String()) {
m.SetEncoding(EncodingB64)
b64d := base64.NewDecoder(base64.StdEncoding, bodybuf)
b64buf := bytes.Buffer{}
if _, err := b64buf.ReadFrom(b64d); err != nil {
return fmt.Errorf("failed to read base64 body: %w", err)
}
m.SetBodyString(ContentType(mediatype), b64buf.String())
return nil
}
return fmt.Errorf("unsupported Content-Transfer-Encoding")
}
// parseEMLMultipartAlternative parses a multipart/alternative body part of a EML
func parseEMLMultipartAlternative(params map[string]string, bodybuf *bytes.Buffer, m *Msg) error {
boundary, ok := params["boundary"]
if !ok {
return fmt.Errorf("no boundary tag found in multipart body")
}
mpreader := multipart.NewReader(bodybuf, boundary)
mpart, err := mpreader.NextPart()
if err != nil {
return fmt.Errorf("failed to get next part of multipart message: %w", err)
}
for err == nil {
mpdata, mperr := io.ReadAll(mpart)
if mperr != nil {
_ = mpart.Close()
return fmt.Errorf("failed to read multipart: %w", err)
}
mpContentType, ok := mpart.Header[HeaderContentType.String()]
if !ok {
return fmt.Errorf("failed to get content-type from part")
}
conType, charSet := parseContentType(mpContentType[0])
p := m.newPart(ContentType(conType))
p.SetCharset(Charset(charSet))
mpTransferEnc, ok := mpart.Header[HeaderContentTransferEnc.String()]
if !ok {
// If CTE is empty we can assume that it's a quoted-printable CTE since the
// GO stdlib multipart packages deletes that header
// See: https://cs.opensource.google/go/go/+/refs/tags/go1.22.0:src/mime/multipart/multipart.go;l=161
mpTransferEnc = []string{EncodingQP.String()}
}
switch {
case strings.EqualFold(mpTransferEnc[0], EncodingB64.String()):
if err := handleEMLMultiPartBase64Encoding(mpdata, p); err != nil {
return fmt.Errorf("failed to handle multipart base64 transfer-encoding: %w", err)
}
case strings.EqualFold(mpTransferEnc[0], EncodingQP.String()):
p.SetContent(string(mpdata))
default:
return fmt.Errorf("unsupported Content-Transfer-Encoding")
}
m.parts = append(m.parts, p)
mpart, err = mpreader.NextPart()
}
if !errors.Is(err, io.EOF) {
_ = mpart.Close()
return fmt.Errorf("failed to read multipart: %w", err)
}
return nil
}
// handleEMLMultiPartBase64Encoding sets the content body of a base64 encoded Part
func handleEMLMultiPartBase64Encoding(mpdata []byte, p *Part) error {
p.SetEncoding(EncodingB64)
cont, err := base64.StdEncoding.DecodeString(string(mpdata))
if err != nil {
return fmt.Errorf("failed to decode base64 part: %w", err)
}
p.SetContent(string(cont))
return nil
}
// parseContentType parses the Content-Type header and returns the type and charse as
// separate string values
func parseContentType(cth string) (ct string, cs string) {
cts := strings.SplitN(cth, "; ", 2)
if len(cts) != 2 {
return
}
ct = cts[0]
if strings.HasPrefix(strings.ToLower(cts[1]), "charset=") {
css := strings.SplitN(cts[1], "=", 2)
if len(css) == 2 {
cs = css[1]
}
}
return
}