mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-09 15:32:54 +01:00
Winni Neessen
f9140ce90e
Added two new methods `EMLToMsgFromString` and `EMLToMsgFromReader` in "eml.go". They allow EML parsing directly from a given string and a reader object, increasing overall functionality and versatility of the EML parsing process. This will enable the users to parse EML documents more flexibly."
206 lines
5.5 KiB
Go
206 lines
5.5 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/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, mbbuf, 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, mbbuf, 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, mbbuf, 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, mbbuf, 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 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, mbbuf *bytes.Buffer, m *Msg) error {
|
|
// Extract the transfer encoding of the body
|
|
mt, par, err := mime.ParseMediaType(pm.Header.Get(HeaderContentType.String()))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to extract content type: %w", err)
|
|
}
|
|
if v, ok := par["charset"]; ok {
|
|
m.SetCharset(Charset(v))
|
|
}
|
|
|
|
cte := pm.Header.Get(HeaderContentTransferEnc.String())
|
|
switch strings.ToLower(mt) {
|
|
case TypeTextPlain.String():
|
|
if strings.EqualFold(cte, NoEncoding.String()) {
|
|
m.SetEncoding(NoEncoding)
|
|
m.SetBodyString(TypeTextPlain, mbbuf.String())
|
|
break
|
|
}
|
|
if strings.EqualFold(cte, EncodingQP.String()) {
|
|
m.SetEncoding(EncodingQP)
|
|
qpr := quotedprintable.NewReader(mbbuf)
|
|
qpbuf := bytes.Buffer{}
|
|
if _, err = qpbuf.ReadFrom(qpr); err != nil {
|
|
return fmt.Errorf("failed to read quoted-printable body: %w", err)
|
|
}
|
|
m.SetBodyString(TypeTextPlain, qpbuf.String())
|
|
break
|
|
}
|
|
if strings.EqualFold(cte, EncodingB64.String()) {
|
|
m.SetEncoding(EncodingB64)
|
|
b64d := base64.NewDecoder(base64.StdEncoding, mbbuf)
|
|
b64buf := bytes.Buffer{}
|
|
if _, err = b64buf.ReadFrom(b64d); err != nil {
|
|
return fmt.Errorf("failed to read base64 body: %w", err)
|
|
}
|
|
m.SetBodyString(TypeTextPlain, b64buf.String())
|
|
break
|
|
}
|
|
default:
|
|
}
|
|
return nil
|
|
}
|