2023-10-13 15:08:53 +02:00
|
|
|
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
|
|
|
//
|
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
2023-09-15 13:16:14 +02:00
|
|
|
package mail
|
|
|
|
|
|
|
|
import (
|
2023-10-13 15:06:28 +02:00
|
|
|
"bytes"
|
2023-10-13 16:27:57 +02:00
|
|
|
"encoding/base64"
|
2023-09-15 13:16:14 +02:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
2023-10-31 11:45:09 +01:00
|
|
|
"io"
|
2023-09-21 13:50:36 +02:00
|
|
|
"mime"
|
2024-01-22 17:49:58 +01:00
|
|
|
"mime/multipart"
|
2023-10-13 16:04:07 +02:00
|
|
|
"mime/quotedprintable"
|
2023-09-15 13:16:14 +02:00
|
|
|
nm "net/mail"
|
|
|
|
"os"
|
2023-10-13 15:06:28 +02:00
|
|
|
"strings"
|
2023-09-15 13:16:14 +02:00
|
|
|
)
|
|
|
|
|
2023-10-31 11:45:09 +01:00
|
|
|
// 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,
|
|
|
|
}
|
|
|
|
|
2024-02-09 12:34:11 +01:00
|
|
|
pm, bodybuf, err := readEMLFromReader(r)
|
2023-10-31 11:45:09 +01:00
|
|
|
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)
|
|
|
|
}
|
2024-02-09 12:34:11 +01:00
|
|
|
if err = parseEMLBodyParts(pm, bodybuf, m); err != nil {
|
2023-10-31 11:45:09 +01:00
|
|
|
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
|
2023-09-15 13:16:14 +02:00
|
|
|
// pre-filled Msg pointer
|
2023-10-31 11:45:09 +01:00
|
|
|
func EMLToMsgFromFile(fp string) (*Msg, error) {
|
2023-09-15 13:16:14 +02:00
|
|
|
m := &Msg{
|
|
|
|
addrHeader: make(map[AddrHeader][]*nm.Address),
|
|
|
|
genHeader: make(map[Header][]string),
|
|
|
|
preformHeader: make(map[Header]string),
|
2023-10-13 15:23:28 +02:00
|
|
|
mimever: MIME10,
|
2023-09-15 13:16:14 +02:00
|
|
|
}
|
|
|
|
|
2024-02-09 12:34:11 +01:00
|
|
|
pm, bodybuf, err := readEML(fp)
|
2023-09-15 13:16:14 +02:00
|
|
|
if err != nil || pm == nil {
|
|
|
|
return m, fmt.Errorf("failed to parse EML file: %w", err)
|
|
|
|
}
|
|
|
|
|
2023-10-17 10:41:42 +02:00
|
|
|
if err = parseEMLHeaders(&pm.Header, m); err != nil {
|
2023-09-15 13:16:14 +02:00
|
|
|
return m, fmt.Errorf("failed to parse EML headers: %w", err)
|
|
|
|
}
|
2024-02-09 12:34:11 +01:00
|
|
|
if err = parseEMLBodyParts(pm, bodybuf, m); err != nil {
|
2023-10-13 15:06:28 +02:00
|
|
|
return m, fmt.Errorf("failed to parse EML body parts: %w", err)
|
2023-09-27 11:29:58 +02:00
|
|
|
}
|
2023-09-21 13:50:36 +02:00
|
|
|
|
2023-09-15 13:16:14 +02:00
|
|
|
return m, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// readEML opens an EML file and uses net/mail to parse the header and body
|
2023-10-13 15:06:28 +02:00
|
|
|
func readEML(fp string) (*nm.Message, *bytes.Buffer, error) {
|
2023-09-15 13:16:14 +02:00
|
|
|
fh, err := os.Open(fp)
|
|
|
|
if err != nil {
|
2023-10-13 15:06:28 +02:00
|
|
|
return nil, nil, fmt.Errorf("failed to open EML file: %w", err)
|
2023-09-15 13:16:14 +02:00
|
|
|
}
|
|
|
|
defer func() {
|
|
|
|
_ = fh.Close()
|
|
|
|
}()
|
2023-10-31 11:45:09 +01:00
|
|
|
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)
|
2023-09-15 13:16:14 +02:00
|
|
|
if err != nil {
|
2023-10-13 15:06:28 +02:00
|
|
|
return pm, nil, fmt.Errorf("failed to parse EML: %w", err)
|
2023-09-15 13:16:14 +02:00
|
|
|
}
|
2023-10-13 15:06:28 +02:00
|
|
|
|
|
|
|
buf := bytes.Buffer{}
|
|
|
|
if _, err = buf.ReadFrom(pm.Body); err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
2023-10-17 10:41:42 +02:00
|
|
|
|
2023-10-13 15:06:28 +02:00
|
|
|
return pm, &buf, nil
|
2023-09-15 13:16:14 +02:00
|
|
|
}
|
|
|
|
|
2023-09-21 13:50:36 +02:00
|
|
|
// parseEMLHeaders will check the EML headers for the most common headers and set the
|
|
|
|
// according settings in the Msg
|
2023-09-15 13:16:14 +02:00
|
|
|
func parseEMLHeaders(mh *nm.Header, m *Msg) error {
|
|
|
|
commonHeaders := []Header{
|
|
|
|
HeaderContentType, HeaderImportance, HeaderInReplyTo, HeaderListUnsubscribe,
|
|
|
|
HeaderListUnsubscribePost, HeaderMessageID, HeaderMIMEVersion, HeaderOrganization,
|
2023-10-13 15:06:28 +02:00
|
|
|
HeaderPrecedence, HeaderPriority, HeaderReferences, HeaderSubject, HeaderUserAgent,
|
|
|
|
HeaderXMailer, HeaderXMSMailPriority, HeaderXPriority,
|
2023-09-15 13:16:14 +02:00
|
|
|
}
|
|
|
|
|
2024-02-09 15:56:39 +01:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-15 13:16:14 +02:00
|
|
|
// Extract address headers
|
|
|
|
if v := mh.Get(HeaderFrom.String()); v != "" {
|
|
|
|
if err := m.From(v); err != nil {
|
2023-10-17 10:41:42 +02:00
|
|
|
return fmt.Errorf(`failed to parse %q header: %w`, HeaderFrom, err)
|
2023-09-15 13:16:14 +02:00
|
|
|
}
|
|
|
|
}
|
2023-09-21 13:50:36 +02:00
|
|
|
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 {
|
2023-10-17 10:41:42 +02:00
|
|
|
return fmt.Errorf(`failed to parse %q header: %w`, HeaderTo, err)
|
2023-09-21 13:50:36 +02:00
|
|
|
}
|
2023-09-15 13:16:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
2024-02-09 15:56:39 +01:00
|
|
|
if strings.EqualFold(h.String(), "subject") {
|
|
|
|
fmt.Printf("SUBJECT: %s\n", m.GetGenHeader(HeaderSubject)[0])
|
|
|
|
}
|
2023-09-15 13:16:14 +02:00
|
|
|
}
|
|
|
|
}
|
2023-09-21 13:50:36 +02:00
|
|
|
|
2023-09-15 13:16:14 +02:00
|
|
|
return nil
|
|
|
|
}
|
2023-10-13 15:06:28 +02:00
|
|
|
|
2023-10-17 10:41:42 +02:00
|
|
|
// parseEMLBodyParts parses the body of a EML based on the different content types and encodings
|
2024-02-09 12:34:11 +01:00
|
|
|
func parseEMLBodyParts(pm *nm.Message, bodybuf *bytes.Buffer, m *Msg) error {
|
2023-10-13 15:06:28 +02:00
|
|
|
// Extract the transfer encoding of the body
|
2024-02-09 12:34:11 +01:00
|
|
|
mediatype, params, err := mime.ParseMediaType(pm.Header.Get(HeaderContentType.String()))
|
2023-10-13 15:06:28 +02:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to extract content type: %w", err)
|
|
|
|
}
|
2024-02-09 12:34:11 +01:00
|
|
|
if v, ok := params["charset"]; ok {
|
2023-10-13 15:06:28 +02:00
|
|
|
m.SetCharset(Charset(v))
|
|
|
|
}
|
|
|
|
|
2024-02-09 15:56:39 +01:00
|
|
|
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)
|
2023-10-13 16:04:07 +02:00
|
|
|
}
|
2024-02-09 15:56:39 +01:00
|
|
|
case strings.EqualFold(mediatype, TypeMultipartAlternative.String()):
|
2024-02-09 12:34:11 +01:00
|
|
|
if err := parseEMLMultipartAlternative(params, bodybuf, m); err != nil {
|
2024-02-09 15:56:39 +01:00
|
|
|
return fmt.Errorf("failed to parse multipart/alternative body: %w", err)
|
2024-02-09 12:34:11 +01:00
|
|
|
}
|
|
|
|
default:
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-02-09 15:56:39 +01:00
|
|
|
// 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")
|
|
|
|
}
|
|
|
|
|
2024-02-09 12:34:11 +01:00
|
|
|
// 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()]
|
2024-01-22 17:49:58 +01:00
|
|
|
if !ok {
|
2024-02-09 12:34:11 +01:00
|
|
|
return fmt.Errorf("failed to get content-type from part")
|
2024-01-22 17:49:58 +01:00
|
|
|
}
|
2024-02-09 12:34:11 +01:00
|
|
|
mpContentTypeSplit := strings.Split(mpContentType[0], "; ")
|
|
|
|
p := m.newPart(ContentType(mpContentTypeSplit[0]))
|
|
|
|
parseEMLMultiPartCharset(mpContentTypeSplit, p)
|
|
|
|
|
|
|
|
mpTransferEnc, ok := mpart.Header[HeaderContentTransferEnc.String()]
|
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("failed to get content-transfer-encoding from part")
|
2024-01-22 17:49:58 +01:00
|
|
|
}
|
2024-02-09 12:34:11 +01:00
|
|
|
switch {
|
|
|
|
case strings.EqualFold(mpTransferEnc[0], EncodingB64.String()):
|
2024-02-09 15:56:39 +01:00
|
|
|
if err := handleEMLMultiPartBase64Encoding(mpdata, p); err != nil {
|
2024-02-09 12:34:11 +01:00
|
|
|
return fmt.Errorf("failed to handle multipart base64 transfer-encoding: %w", err)
|
2024-01-22 17:49:58 +01:00
|
|
|
}
|
|
|
|
}
|
2024-02-09 12:34:11 +01:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-02-09 15:56:39 +01:00
|
|
|
// parseEMLMultiPartCharset parses the Charset from a ContentType header and assigns it to a Part
|
|
|
|
// TODO: This might be redundant to parseContentType
|
2024-02-09 12:34:11 +01:00
|
|
|
func parseEMLMultiPartCharset(mpContentTypeSplit []string, p *Part) {
|
|
|
|
if len(mpContentTypeSplit) > 1 && strings.HasPrefix(strings.ToLower(mpContentTypeSplit[1]), "charset=") {
|
|
|
|
valSplit := strings.Split(mpContentTypeSplit[1], "=")
|
|
|
|
if len(valSplit) > 1 {
|
|
|
|
p.SetCharset(Charset(valSplit[1]))
|
2024-01-22 17:49:58 +01:00
|
|
|
}
|
2024-02-09 12:34:11 +01:00
|
|
|
}
|
|
|
|
}
|
2024-01-22 17:49:58 +01:00
|
|
|
|
2024-02-09 15:56:39 +01:00
|
|
|
// handleEMLMultiPartBase64Encoding sets the content body of a base64 encoded Part
|
|
|
|
func handleEMLMultiPartBase64Encoding(mpdata []byte, p *Part) error {
|
2024-02-09 12:34:11 +01:00
|
|
|
p.SetEncoding(EncodingB64)
|
|
|
|
cont, err := base64.StdEncoding.DecodeString(string(mpdata))
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to decode base64 part: %w", err)
|
2023-10-13 15:06:28 +02:00
|
|
|
}
|
2024-02-09 12:34:11 +01:00
|
|
|
p.SetContent(string(cont))
|
2023-10-13 15:06:28 +02:00
|
|
|
return nil
|
|
|
|
}
|
2024-02-09 15:56:39 +01:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|