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"
|
2024-05-27 10:59:38 +02:00
|
|
|
netmail "net/mail"
|
2023-09-15 13:16:14 +02:00
|
|
|
"os"
|
2023-10-13 15:06:28 +02:00
|
|
|
"strings"
|
2023-09-15 13:16:14 +02:00
|
|
|
)
|
|
|
|
|
2024-10-06 13:51:36 +02:00
|
|
|
// EMLToMsgFromString parses a given EML string and returns a pre-filled Msg pointer.
|
|
|
|
//
|
|
|
|
// This function takes an EML formatted string, converts it into a bytes buffer, and then
|
|
|
|
// calls EMLToMsgFromReader to parse the buffer and create a Msg object. This provides a
|
|
|
|
// convenient way to convert EML strings directly into Msg objects.
|
|
|
|
//
|
|
|
|
// Parameters:
|
|
|
|
// - emlString: A string containing the EML formatted message.
|
|
|
|
//
|
|
|
|
// Returns:
|
|
|
|
// - A pointer to the Msg object populated with the parsed data, and an error if parsing
|
|
|
|
// fails.
|
2024-05-27 10:59:38 +02:00
|
|
|
func EMLToMsgFromString(emlString string) (*Msg, error) {
|
|
|
|
eb := bytes.NewBufferString(emlString)
|
2023-10-31 11:45:09 +01:00
|
|
|
return EMLToMsgFromReader(eb)
|
|
|
|
}
|
|
|
|
|
2024-10-06 13:51:36 +02:00
|
|
|
// EMLToMsgFromReader parses a reader that holds EML content and returns a pre-filled Msg pointer.
|
|
|
|
//
|
|
|
|
// This function reads EML content from the provided io.Reader and populates a Msg object
|
|
|
|
// with the parsed data. It initializes the Msg and extracts headers and body parts from
|
|
|
|
// the EML content. Any errors encountered during parsing are returned.
|
|
|
|
//
|
|
|
|
// Parameters:
|
|
|
|
// - reader: An io.Reader containing the EML formatted message.
|
|
|
|
//
|
|
|
|
// Returns:
|
|
|
|
// - A pointer to the Msg object populated with the parsed data, and an error if parsing
|
|
|
|
// fails.
|
2024-05-27 10:59:38 +02:00
|
|
|
func EMLToMsgFromReader(reader io.Reader) (*Msg, error) {
|
|
|
|
msg := &Msg{
|
|
|
|
addrHeader: make(map[AddrHeader][]*netmail.Address),
|
2023-10-31 11:45:09 +01:00
|
|
|
genHeader: make(map[Header][]string),
|
|
|
|
preformHeader: make(map[Header]string),
|
|
|
|
mimever: MIME10,
|
|
|
|
}
|
|
|
|
|
2024-05-27 10:59:38 +02:00
|
|
|
parsedMsg, bodybuf, err := readEMLFromReader(reader)
|
|
|
|
if err != nil || parsedMsg == nil {
|
|
|
|
return msg, fmt.Errorf("failed to parse EML from reader: %w", err)
|
2023-10-31 11:45:09 +01:00
|
|
|
}
|
|
|
|
|
2024-10-24 13:20:09 +02:00
|
|
|
if err = parseEML(parsedMsg, bodybuf, msg); err != nil {
|
2024-06-24 14:30:07 +02:00
|
|
|
return msg, fmt.Errorf("failed to parse EML contents: %w", err)
|
2023-10-31 11:45:09 +01:00
|
|
|
}
|
|
|
|
|
2024-05-27 10:59:38 +02:00
|
|
|
return msg, nil
|
2023-10-31 11:45:09 +01:00
|
|
|
}
|
|
|
|
|
2024-10-06 13:51:36 +02:00
|
|
|
// EMLToMsgFromFile opens and parses a .eml file at a provided file path and returns a
|
|
|
|
// pre-filled Msg pointer.
|
|
|
|
//
|
|
|
|
// This function attempts to read and parse an EML file located at the specified file path.
|
|
|
|
// It initializes a Msg object and populates it with the parsed headers and body. Any errors
|
|
|
|
// encountered during the file operations or parsing are returned.
|
|
|
|
//
|
|
|
|
// Parameters:
|
|
|
|
// - filePath: The path to the .eml file to be parsed.
|
|
|
|
//
|
|
|
|
// Returns:
|
|
|
|
// - A pointer to the Msg object populated with the parsed data, and an error if parsing
|
|
|
|
// fails.
|
2024-05-27 10:59:38 +02:00
|
|
|
func EMLToMsgFromFile(filePath string) (*Msg, error) {
|
|
|
|
msg := &Msg{
|
|
|
|
addrHeader: make(map[AddrHeader][]*netmail.Address),
|
2023-09-15 13:16:14 +02:00
|
|
|
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-05-27 10:59:38 +02:00
|
|
|
parsedMsg, bodybuf, err := readEML(filePath)
|
|
|
|
if err != nil || parsedMsg == nil {
|
|
|
|
return msg, fmt.Errorf("failed to parse EML file: %w", err)
|
2023-09-15 13:16:14 +02:00
|
|
|
}
|
|
|
|
|
2024-10-24 14:49:24 +02:00
|
|
|
if err = parseEML(parsedMsg, bodybuf, msg); err != nil {
|
2024-06-24 14:30:07 +02:00
|
|
|
return msg, fmt.Errorf("failed to parse EML contents: %w", err)
|
2023-09-27 11:29:58 +02:00
|
|
|
}
|
2023-09-21 13:50:36 +02:00
|
|
|
|
2024-05-27 10:59:38 +02:00
|
|
|
return msg, nil
|
2023-09-15 13:16:14 +02:00
|
|
|
}
|
|
|
|
|
2024-10-05 11:11:17 +02:00
|
|
|
// parseEML parses the EML's headers and body and inserts the parsed values into the Msg.
|
2024-10-06 13:51:36 +02:00
|
|
|
//
|
|
|
|
// This function extracts relevant header fields and body content from the parsed EML message
|
|
|
|
// and stores them in the provided Msg object. It handles various header types and body
|
|
|
|
// parts, ensuring that the Msg is correctly populated with all necessary information.
|
|
|
|
//
|
|
|
|
// Parameters:
|
|
|
|
// - parsedMsg: A pointer to the netmail.Message containing the parsed EML data.
|
|
|
|
// - bodybuf: A bytes.Buffer containing the body content of the EML message.
|
|
|
|
// - msg: A pointer to the Msg object to be populated with the parsed data.
|
|
|
|
//
|
|
|
|
// Returns:
|
|
|
|
// - An error if any issues occur during the parsing process; otherwise, returns nil.
|
2024-06-24 14:30:07 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-10-05 11:11:17 +02:00
|
|
|
// readEML opens an EML file and uses net/mail to parse the header and body.
|
2024-10-06 13:51:36 +02:00
|
|
|
//
|
|
|
|
// This function opens the specified EML file for reading and utilizes the net/mail package
|
|
|
|
// to parse the message's headers and body. It returns the parsed message and a buffer
|
|
|
|
// containing the body content, along with any errors encountered during the process.
|
|
|
|
//
|
|
|
|
// Parameters:
|
|
|
|
// - filePath: The path to the EML file to be opened and parsed.
|
|
|
|
//
|
|
|
|
// Returns:
|
|
|
|
// - A pointer to the parsed netmail.Message, a bytes.Buffer containing the body, and an
|
|
|
|
// error if any issues occur during file operations or parsing.
|
2024-05-27 10:59:38 +02:00
|
|
|
func readEML(filePath string) (*netmail.Message, *bytes.Buffer, error) {
|
|
|
|
fileHandle, err := os.Open(filePath)
|
2023-09-15 13:16:14 +02:00
|
|
|
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() {
|
2024-05-27 10:59:38 +02:00
|
|
|
_ = fileHandle.Close()
|
2023-09-15 13:16:14 +02:00
|
|
|
}()
|
2024-05-27 10:59:38 +02:00
|
|
|
return readEMLFromReader(fileHandle)
|
2023-10-31 11:45:09 +01:00
|
|
|
}
|
|
|
|
|
2024-10-05 11:11:17 +02:00
|
|
|
// readEMLFromReader uses net/mail to parse the header and body from a given io.Reader.
|
2024-10-06 13:51:36 +02:00
|
|
|
//
|
|
|
|
// This function reads the EML content from the provided io.Reader and uses the net/mail
|
|
|
|
// package to parse the message's headers and body. It returns the parsed netmail.Message
|
|
|
|
// along with a bytes.Buffer containing the body content. Any errors encountered during
|
|
|
|
// the parsing process are returned.
|
|
|
|
//
|
|
|
|
// Parameters:
|
|
|
|
// - reader: An io.Reader containing the EML formatted message.
|
|
|
|
//
|
|
|
|
// Returns:
|
|
|
|
// - A pointer to the parsed netmail.Message, a bytes.Buffer containing the body, and an
|
|
|
|
// error if any issues occur during parsing.
|
2024-05-27 10:59:38 +02:00
|
|
|
func readEMLFromReader(reader io.Reader) (*netmail.Message, *bytes.Buffer, error) {
|
|
|
|
parsedMsg, err := netmail.ReadMessage(reader)
|
2023-09-15 13:16:14 +02:00
|
|
|
if err != nil {
|
2024-05-27 10:59:38 +02:00
|
|
|
return parsedMsg, 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{}
|
2024-05-27 10:59:38 +02:00
|
|
|
if _, err = buf.ReadFrom(parsedMsg.Body); err != nil {
|
2023-10-13 15:06:28 +02:00
|
|
|
return nil, nil, err
|
|
|
|
}
|
2023-10-17 10:41:42 +02:00
|
|
|
|
2024-05-27 10:59:38 +02:00
|
|
|
return parsedMsg, &buf, nil
|
2023-09-15 13:16:14 +02:00
|
|
|
}
|
|
|
|
|
2024-10-06 13:51:36 +02:00
|
|
|
// parseEMLHeaders parses the EML's headers and populates the Msg with relevant information.
|
|
|
|
//
|
|
|
|
// This function checks the EML headers for common headers and sets the corresponding fields
|
|
|
|
// in the Msg object. It extracts address headers, content types, and other relevant data
|
|
|
|
// for further processing.
|
|
|
|
//
|
|
|
|
// Parameters:
|
|
|
|
// - mailHeader: A pointer to the netmail.Header containing the EML headers.
|
|
|
|
// - msg: A pointer to the Msg object to be populated with parsed header information.
|
|
|
|
//
|
|
|
|
// Returns:
|
|
|
|
// - An error if parsing the headers fails; otherwise, returns nil.
|
2024-05-27 10:59:38 +02:00
|
|
|
func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error {
|
2023-09-15 13:16:14 +02:00
|
|
|
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
|
2024-06-19 10:52:09 +02:00
|
|
|
parseEMLEncoding(mailHeader, msg)
|
|
|
|
parseEMLContentTypeCharset(mailHeader, msg)
|
2024-02-09 15:56:39 +01:00
|
|
|
|
2023-09-15 13:16:14 +02:00
|
|
|
// Extract address headers
|
2024-05-27 10:59:38 +02:00
|
|
|
if value := mailHeader.Get(HeaderFrom.String()); value != "" {
|
|
|
|
if err := msg.From(value); 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
|
|
|
}
|
|
|
|
}
|
2024-05-27 10:59:38 +02:00
|
|
|
addrHeaders := map[AddrHeader]func(...string) error{
|
|
|
|
HeaderTo: msg.To,
|
|
|
|
HeaderCc: msg.Cc,
|
|
|
|
HeaderBcc: msg.Bcc,
|
2023-09-21 13:50:36 +02:00
|
|
|
}
|
2024-05-27 10:59:38 +02:00
|
|
|
for addrHeader, addrFunc := range addrHeaders {
|
|
|
|
if v := mailHeader.Get(addrHeader.String()); v != "" {
|
|
|
|
var addrStrings []string
|
|
|
|
parsedAddrs, err := netmail.ParseAddressList(v)
|
2023-09-21 13:50:36 +02:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf(`failed to parse address list: %w`, err)
|
|
|
|
}
|
2024-05-27 10:59:38 +02:00
|
|
|
for _, addr := range parsedAddrs {
|
|
|
|
addrStrings = append(addrStrings, addr.String())
|
2023-09-21 13:50:36 +02:00
|
|
|
}
|
2024-10-24 16:42:00 +02:00
|
|
|
// We can skip the error checking here since netmail.ParseAddressList already performed the
|
|
|
|
// same address checking that the msg methods do.
|
|
|
|
_ = addrFunc(addrStrings...)
|
2023-09-15 13:16:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Extract date from message
|
2024-05-27 10:59:38 +02:00
|
|
|
date, err := mailHeader.Date()
|
2023-09-15 13:16:14 +02:00
|
|
|
if err != nil {
|
|
|
|
switch {
|
2024-05-27 10:59:38 +02:00
|
|
|
case errors.Is(err, netmail.ErrHeaderNotPresent):
|
|
|
|
msg.SetDate()
|
2023-09-15 13:16:14 +02:00
|
|
|
default:
|
|
|
|
return fmt.Errorf("failed to parse EML date: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err == nil {
|
2024-05-27 10:59:38 +02:00
|
|
|
msg.SetDateWithValue(date)
|
2023-09-15 13:16:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Extract common headers
|
2024-05-27 10:59:38 +02:00
|
|
|
for _, header := range commonHeaders {
|
|
|
|
if value := mailHeader.Get(header.String()); value != "" {
|
2024-06-22 14:13:26 +02:00
|
|
|
if strings.EqualFold(header.String(), HeaderContentType.String()) &&
|
|
|
|
strings.HasPrefix(value, TypeMultipartMixed.String()) {
|
|
|
|
continue
|
|
|
|
}
|
2024-05-27 10:59:38 +02:00
|
|
|
msg.SetGenHeader(header, value)
|
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
|
|
|
|
2024-10-06 13:51:36 +02:00
|
|
|
// parseEMLBodyParts parses the body of an EML based on the different content types and encodings.
|
|
|
|
//
|
|
|
|
// This function examines the content type of the parsed EML message and processes the body
|
|
|
|
// parts accordingly. It handles both plain text and multipart types, ensuring that the
|
|
|
|
// Msg object is populated with the appropriate body content.
|
|
|
|
//
|
|
|
|
// Parameters:
|
|
|
|
// - parsedMsg: A pointer to the netmail.Message containing the parsed EML data.
|
|
|
|
// - bodybuf: A bytes.Buffer containing the body content of the EML message.
|
|
|
|
// - msg: A pointer to the Msg object to be populated with the parsed body content.
|
|
|
|
//
|
|
|
|
// Returns:
|
|
|
|
// - An error if any issues occur during the body parsing process; otherwise, returns nil.
|
2024-05-27 10:59:38 +02:00
|
|
|
func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
|
2023-10-13 15:06:28 +02:00
|
|
|
// Extract the transfer encoding of the body
|
2024-05-27 10:59:38 +02:00
|
|
|
mediatype, params, err := mime.ParseMediaType(parsedMsg.Header.Get(HeaderContentType.String()))
|
2023-10-13 15:06:28 +02:00
|
|
|
if err != nil {
|
2024-08-02 15:30:46 +02:00
|
|
|
switch {
|
|
|
|
// If no Content-Type header is found, we assume that this is a plain text, 7bit, US-ASCII mail
|
|
|
|
case strings.EqualFold(err.Error(), "mime: no media type"):
|
|
|
|
mediatype = TypeTextPlain.String()
|
|
|
|
params = make(map[string]string)
|
|
|
|
params["charset"] = CharsetASCII.String()
|
|
|
|
default:
|
|
|
|
return fmt.Errorf("failed to extract content type: %w", err)
|
|
|
|
}
|
2023-10-13 15:06:28 +02:00
|
|
|
}
|
2024-05-27 10:59:38 +02:00
|
|
|
if value, ok := params["charset"]; ok {
|
|
|
|
msg.SetCharset(Charset(value))
|
2023-10-13 15:06:28 +02:00
|
|
|
}
|
|
|
|
|
2024-02-09 15:56:39 +01:00
|
|
|
switch {
|
|
|
|
case strings.EqualFold(mediatype, TypeTextPlain.String()),
|
|
|
|
strings.EqualFold(mediatype, TypeTextHTML.String()):
|
2024-05-27 10:59:38 +02:00
|
|
|
if err = parseEMLBodyPlain(mediatype, parsedMsg, bodybuf, msg); err != nil {
|
2024-02-09 15:56:39 +01:00
|
|
|
return fmt.Errorf("failed to parse plain body: %w", err)
|
2023-10-13 16:04:07 +02:00
|
|
|
}
|
2024-02-10 13:36:42 +01:00
|
|
|
case strings.EqualFold(mediatype, TypeMultipartAlternative.String()),
|
2024-06-22 14:13:26 +02:00
|
|
|
strings.EqualFold(mediatype, TypeMultipartMixed.String()),
|
|
|
|
strings.EqualFold(mediatype, TypeMultipartRelated.String()):
|
2024-06-19 13:57:00 +02:00
|
|
|
if err = parseEMLMultipart(params, bodybuf, msg); err != nil {
|
2024-06-22 14:13:26 +02:00
|
|
|
return fmt.Errorf("failed to parse multipart body: %w", err)
|
2024-02-09 12:34:11 +01:00
|
|
|
}
|
|
|
|
default:
|
2024-06-24 13:41:48 +02:00
|
|
|
return fmt.Errorf("failed to parse body, unknown content type: %s", mediatype)
|
2024-02-09 12:34:11 +01:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-10-06 13:51:36 +02:00
|
|
|
// parseEMLBodyPlain parses the mail body of plain type messages.
|
|
|
|
//
|
|
|
|
// This function handles the parsing of plain text messages based on their encoding. It
|
|
|
|
// identifies the content transfer encoding and decodes the body content accordingly,
|
|
|
|
// storing the result in the provided Msg object.
|
|
|
|
//
|
|
|
|
// Parameters:
|
|
|
|
// - mediatype: The media type of the message (e.g., text/plain).
|
|
|
|
// - parsedMsg: A pointer to the netmail.Message containing the parsed EML data.
|
|
|
|
// - bodybuf: A bytes.Buffer containing the body content of the EML message.
|
|
|
|
// - msg: A pointer to the Msg object to be populated with the parsed body content.
|
|
|
|
//
|
|
|
|
// Returns:
|
|
|
|
// - An error if any issues occur during the parsing of the plain body; otherwise, returns nil.
|
2024-05-27 10:59:38 +02:00
|
|
|
func parseEMLBodyPlain(mediatype string, parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
|
|
|
|
contentTransferEnc := parsedMsg.Header.Get(HeaderContentTransferEnc.String())
|
2024-10-05 11:11:17 +02:00
|
|
|
// If no Content-Transfer-Encoding is set, we can imply 7bit US-ASCII encoding
|
|
|
|
// https://datatracker.ietf.org/doc/html/rfc2045#section-6.1
|
2024-08-01 10:55:28 +02:00
|
|
|
if contentTransferEnc == "" || strings.EqualFold(contentTransferEnc, EncodingUSASCII.String()) {
|
|
|
|
msg.SetEncoding(EncodingUSASCII)
|
|
|
|
msg.SetBodyString(ContentType(mediatype), bodybuf.String())
|
|
|
|
return nil
|
|
|
|
}
|
2024-05-27 10:59:38 +02:00
|
|
|
if strings.EqualFold(contentTransferEnc, NoEncoding.String()) {
|
|
|
|
msg.SetEncoding(NoEncoding)
|
|
|
|
msg.SetBodyString(ContentType(mediatype), bodybuf.String())
|
2024-02-09 15:56:39 +01:00
|
|
|
return nil
|
|
|
|
}
|
2024-05-27 10:59:38 +02:00
|
|
|
if strings.EqualFold(contentTransferEnc, EncodingQP.String()) {
|
|
|
|
msg.SetEncoding(EncodingQP)
|
|
|
|
qpReader := quotedprintable.NewReader(bodybuf)
|
|
|
|
qpBuffer := bytes.Buffer{}
|
|
|
|
if _, err := qpBuffer.ReadFrom(qpReader); err != nil {
|
2024-02-09 15:56:39 +01:00
|
|
|
return fmt.Errorf("failed to read quoted-printable body: %w", err)
|
|
|
|
}
|
2024-05-27 10:59:38 +02:00
|
|
|
msg.SetBodyString(ContentType(mediatype), qpBuffer.String())
|
2024-02-09 15:56:39 +01:00
|
|
|
return nil
|
|
|
|
}
|
2024-05-27 10:59:38 +02:00
|
|
|
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 {
|
2024-02-09 15:56:39 +01:00
|
|
|
return fmt.Errorf("failed to read base64 body: %w", err)
|
|
|
|
}
|
2024-05-27 10:59:38 +02:00
|
|
|
msg.SetBodyString(ContentType(mediatype), b64Buffer.String())
|
2024-02-09 15:56:39 +01:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return fmt.Errorf("unsupported Content-Transfer-Encoding")
|
|
|
|
}
|
|
|
|
|
2024-10-06 13:51:36 +02:00
|
|
|
// parseEMLMultipart parses a multipart body part of an EML message.
|
|
|
|
//
|
|
|
|
// This function handles the parsing of multipart messages, extracting the individual parts
|
|
|
|
// and determining their content types. It processes each part according to its content type
|
|
|
|
// and ensures that all relevant data is stored in the Msg object.
|
|
|
|
//
|
|
|
|
// Parameters:
|
|
|
|
// - params: A map containing the parameters from the multipart content type.
|
|
|
|
// - bodybuf: A bytes.Buffer containing the body content of the EML message.
|
|
|
|
// - msg: A pointer to the Msg object to be populated with the parsed body parts.
|
|
|
|
//
|
|
|
|
// Returns:
|
|
|
|
// - An error if any issues occur during the parsing of the multipart body; otherwise,
|
|
|
|
// returns nil.
|
2024-06-19 13:57:00 +02:00
|
|
|
func parseEMLMultipart(params map[string]string, bodybuf *bytes.Buffer, msg *Msg) error {
|
2024-02-09 12:34:11 +01:00
|
|
|
boundary, ok := params["boundary"]
|
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("no boundary tag found in multipart body")
|
|
|
|
}
|
2024-05-27 10:59:38 +02:00
|
|
|
multipartReader := multipart.NewReader(bodybuf, boundary)
|
2024-06-19 14:41:13 +02:00
|
|
|
ReadNextPart:
|
2024-05-27 10:59:38 +02:00
|
|
|
multiPart, err := multipartReader.NextPart()
|
2024-06-19 14:41:13 +02:00
|
|
|
defer func() {
|
|
|
|
if multiPart != nil {
|
|
|
|
_ = multiPart.Close()
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
if err != nil && !errors.Is(err, io.EOF) {
|
2024-02-09 12:34:11 +01:00
|
|
|
return fmt.Errorf("failed to get next part of multipart message: %w", err)
|
|
|
|
}
|
|
|
|
for err == nil {
|
2024-10-16 10:38:40 +02:00
|
|
|
// Multipart/related and Multipart/alternative parts need to be parsed separately
|
2024-06-22 14:13:26 +02:00
|
|
|
if contentTypeSlice, ok := multiPart.Header[HeaderContentType.String()]; ok && len(contentTypeSlice) == 1 {
|
|
|
|
contentType, _ := parseMultiPartHeader(contentTypeSlice[0])
|
2024-06-28 13:28:09 +02:00
|
|
|
if strings.EqualFold(contentType, TypeMultipartRelated.String()) ||
|
|
|
|
strings.EqualFold(contentType, TypeMultipartAlternative.String()) {
|
2024-06-22 14:13:26 +02:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-19 14:41:13 +02:00
|
|
|
// Content-Disposition header means we have an attachment or embed
|
2024-06-19 13:57:00 +02:00
|
|
|
if contentDisposition, ok := multiPart.Header[HeaderContentDisposition.String()]; ok {
|
2024-06-22 14:13:26 +02:00
|
|
|
if err = parseEMLAttachmentEmbed(contentDisposition, multiPart, msg); err != nil {
|
2024-06-19 14:41:13 +02:00
|
|
|
return fmt.Errorf("failed to parse attachment/embed: %w", err)
|
2024-06-19 13:57:00 +02:00
|
|
|
}
|
2024-06-19 14:41:13 +02:00
|
|
|
goto ReadNextPart
|
2024-06-19 13:57:00 +02:00
|
|
|
}
|
|
|
|
|
2024-05-27 10:59:38 +02:00
|
|
|
multiPartData, mperr := io.ReadAll(multiPart)
|
2024-02-09 12:34:11 +01:00
|
|
|
if mperr != nil {
|
2024-05-27 10:59:38 +02:00
|
|
|
_ = multiPart.Close()
|
2024-02-09 12:34:11 +01:00
|
|
|
return fmt.Errorf("failed to read multipart: %w", err)
|
|
|
|
}
|
|
|
|
|
2024-05-27 10:59:38 +02:00
|
|
|
multiPartContentType, ok := multiPart.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-06-19 13:57:00 +02:00
|
|
|
contentType, optional := parseMultiPartHeader(multiPartContentType[0])
|
2024-06-22 14:13:26 +02:00
|
|
|
if strings.EqualFold(contentType, TypeMultipartRelated.String()) {
|
|
|
|
goto ReadNextPart
|
|
|
|
}
|
2024-06-19 13:57:00 +02:00
|
|
|
part := msg.newPart(ContentType(contentType))
|
|
|
|
if charset, ok := optional["charset"]; ok {
|
|
|
|
part.SetCharset(Charset(charset))
|
|
|
|
}
|
2024-02-09 12:34:11 +01:00
|
|
|
|
2024-05-27 10:59:38 +02:00
|
|
|
mutliPartTransferEnc, ok := multiPart.Header[HeaderContentTransferEnc.String()]
|
2024-02-09 12:34:11 +01:00
|
|
|
if !ok {
|
2024-02-10 13:22:38 +01:00
|
|
|
// 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
|
2024-05-27 10:59:38 +02:00
|
|
|
mutliPartTransferEnc = []string{EncodingQP.String()}
|
2024-01-22 17:49:58 +01:00
|
|
|
}
|
2024-02-10 13:22:38 +01:00
|
|
|
|
2024-02-09 12:34:11 +01:00
|
|
|
switch {
|
2024-08-01 10:55:28 +02:00
|
|
|
case strings.EqualFold(mutliPartTransferEnc[0], EncodingUSASCII.String()):
|
|
|
|
part.SetEncoding(EncodingUSASCII)
|
|
|
|
part.SetContent(string(multiPartData))
|
|
|
|
case strings.EqualFold(mutliPartTransferEnc[0], NoEncoding.String()):
|
|
|
|
part.SetEncoding(NoEncoding)
|
|
|
|
part.SetContent(string(multiPartData))
|
2024-05-27 10:59:38 +02:00
|
|
|
case strings.EqualFold(mutliPartTransferEnc[0], EncodingB64.String()):
|
2024-08-01 10:55:28 +02:00
|
|
|
part.SetEncoding(EncodingB64)
|
|
|
|
if err = handleEMLMultiPartBase64Encoding(multiPartData, part); 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-05-27 10:59:38 +02:00
|
|
|
case strings.EqualFold(mutliPartTransferEnc[0], EncodingQP.String()):
|
2024-08-01 10:55:28 +02:00
|
|
|
part.SetEncoding(EncodingQP)
|
2024-06-19 13:57:00 +02:00
|
|
|
part.SetContent(string(multiPartData))
|
2024-02-10 13:22:38 +01:00
|
|
|
default:
|
2024-08-01 10:55:28 +02:00
|
|
|
return fmt.Errorf("unsupported Content-Transfer-Encoding: %s", mutliPartTransferEnc[0])
|
2024-01-22 17:49:58 +01:00
|
|
|
}
|
2024-02-09 12:34:11 +01:00
|
|
|
|
2024-06-19 13:57:00 +02:00
|
|
|
msg.parts = append(msg.parts, part)
|
2024-05-27 10:59:38 +02:00
|
|
|
multiPart, err = multipartReader.NextPart()
|
2024-02-09 12:34:11 +01:00
|
|
|
}
|
|
|
|
if !errors.Is(err, io.EOF) {
|
|
|
|
return fmt.Errorf("failed to read multipart: %w", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-10-05 11:11:17 +02:00
|
|
|
// parseEMLEncoding parses and determines the encoding of the message.
|
2024-10-06 13:51:36 +02:00
|
|
|
//
|
|
|
|
// This function extracts the content transfer encoding from the EML headers and sets the
|
|
|
|
// corresponding encoding in the Msg object. It ensures that the correct encoding is used
|
|
|
|
// for further processing of the message content.
|
|
|
|
//
|
|
|
|
// Parameters:
|
|
|
|
// - mailHeader: A pointer to the netmail.Header containing the EML headers.
|
|
|
|
// - msg: A pointer to the Msg object to be updated with the encoding information.
|
2024-06-19 10:52:09 +02:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-05 11:11:17 +02:00
|
|
|
// parseEMLContentTypeCharset parses and determines the charset and content type of the message.
|
2024-10-06 13:51:36 +02:00
|
|
|
//
|
|
|
|
// This function extracts the content type and charset from the EML headers, setting them
|
|
|
|
// appropriately in the Msg object. It ensures that the Msg object is configured with the
|
|
|
|
// correct content type for further processing.
|
|
|
|
//
|
|
|
|
// Parameters:
|
|
|
|
// - mailHeader: A pointer to the netmail.Header containing the EML headers.
|
|
|
|
// - msg: A pointer to the Msg object to be updated with content type and charset information.
|
2024-06-19 10:52:09 +02:00
|
|
|
func parseEMLContentTypeCharset(mailHeader *netmail.Header, msg *Msg) {
|
|
|
|
if value := mailHeader.Get(HeaderContentType.String()); value != "" {
|
2024-06-19 13:57:00 +02:00
|
|
|
contentType, optional := parseMultiPartHeader(value)
|
|
|
|
if charset, ok := optional["charset"]; ok {
|
|
|
|
msg.SetCharset(Charset(charset))
|
2024-06-19 10:52:09 +02:00
|
|
|
}
|
|
|
|
msg.setEncoder()
|
2024-06-22 14:13:26 +02:00
|
|
|
if contentType != "" && !strings.EqualFold(contentType, TypeMultipartMixed.String()) {
|
2024-06-19 10:52:09 +02:00
|
|
|
msg.SetGenHeader(HeaderContentType, contentType)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-05 11:11:17 +02:00
|
|
|
// handleEMLMultiPartBase64Encoding sets the content body of a base64 encoded Part.
|
2024-10-06 13:51:36 +02:00
|
|
|
//
|
|
|
|
// This function decodes the base64 encoded content of a multipart part and stores the
|
|
|
|
// resulting content in the provided Part object. It handles any errors that occur during
|
|
|
|
// the decoding process.
|
|
|
|
//
|
|
|
|
// Parameters:
|
|
|
|
// - multiPartData: A byte slice containing the base64 encoded data.
|
|
|
|
// - part: A pointer to the Part object where the decoded content will be stored.
|
|
|
|
//
|
|
|
|
// Returns:
|
|
|
|
// - An error if the base64 decoding fails; otherwise, returns nil.
|
2024-05-27 10:59:38 +02:00
|
|
|
func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error {
|
|
|
|
part.SetEncoding(EncodingB64)
|
|
|
|
content, err := base64.StdEncoding.DecodeString(string(multiPartData))
|
2024-02-09 12:34:11 +01:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to decode base64 part: %w", err)
|
2023-10-13 15:06:28 +02:00
|
|
|
}
|
2024-05-27 10:59:38 +02:00
|
|
|
part.SetContent(string(content))
|
2023-10-13 15:06:28 +02:00
|
|
|
return nil
|
|
|
|
}
|
2024-02-09 15:56:39 +01:00
|
|
|
|
2024-10-06 13:51:36 +02:00
|
|
|
// parseMultiPartHeader parses a multipart header and returns the value and optional parts as a map.
|
|
|
|
//
|
|
|
|
// This function splits a multipart header into its main value and any optional parameters,
|
|
|
|
// returning them separately. It helps in processing multipart messages by extracting
|
|
|
|
// relevant information from headers.
|
|
|
|
//
|
|
|
|
// Parameters:
|
|
|
|
// - multiPartHeader: A string representing the multipart header to be parsed.
|
|
|
|
//
|
|
|
|
// Returns:
|
|
|
|
// - The main header value as a string and a map of optional parameters.
|
2024-06-19 13:57:00 +02:00
|
|
|
func parseMultiPartHeader(multiPartHeader string) (header string, optional map[string]string) {
|
|
|
|
optional = make(map[string]string)
|
2024-06-25 10:59:04 +02:00
|
|
|
headerSplit := strings.SplitN(multiPartHeader, ";", 2)
|
2024-06-19 13:57:00 +02:00
|
|
|
header = headerSplit[0]
|
|
|
|
if len(headerSplit) == 2 {
|
2024-06-25 10:59:04 +02:00
|
|
|
optString := strings.TrimLeft(headerSplit[1], " ")
|
|
|
|
optSplit := strings.SplitN(optString, "=", 2)
|
2024-06-19 13:57:00 +02:00
|
|
|
if len(optSplit) == 2 {
|
|
|
|
optional[optSplit[0]] = optSplit[1]
|
2024-02-09 15:56:39 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
2024-06-19 14:41:13 +02:00
|
|
|
|
2024-10-05 11:11:17 +02:00
|
|
|
// parseEMLAttachmentEmbed parses a multipart that is an attachment or embed.
|
2024-10-06 13:51:36 +02:00
|
|
|
//
|
|
|
|
// This function handles the parsing of multipart sections that are marked as attachments or
|
|
|
|
// embedded content. It processes the content disposition and sets the appropriate fields in
|
|
|
|
// the Msg object based on the parsed data.
|
|
|
|
//
|
|
|
|
// Parameters:
|
|
|
|
// - contentDisposition: A slice of strings containing the content disposition header.
|
|
|
|
// - multiPart: A pointer to the multipart.Part to be parsed.
|
|
|
|
// - msg: A pointer to the Msg object to be populated with the attachment or embed data.
|
|
|
|
//
|
|
|
|
// Returns:
|
|
|
|
// - An error if any issues occur during the parsing of attachments or embeds; otherwise,
|
|
|
|
// returns nil.
|
2024-06-19 14:41:13 +02:00
|
|
|
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]
|
|
|
|
}
|
2024-06-28 11:54:30 +02:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-06-19 14:41:13 +02:00
|
|
|
switch strings.ToLower(cdType) {
|
|
|
|
case "attachment":
|
2024-06-28 11:54:30 +02:00
|
|
|
if err := msg.AttachReader(filename, dataReader); err != nil {
|
2024-06-19 14:41:13 +02:00
|
|
|
return fmt.Errorf("failed to attach multipart body: %w", err)
|
|
|
|
}
|
|
|
|
case "inline":
|
2024-06-28 11:54:30 +02:00
|
|
|
if contentID, _ := parseMultiPartHeader(multiPart.Header.Get(HeaderContentID.String())); contentID != "" {
|
2024-06-28 13:49:21 +02:00
|
|
|
if err := msg.EmbedReader(filename, dataReader, WithFileContentID(contentID)); err != nil {
|
2024-06-28 11:54:30 +02:00
|
|
|
return fmt.Errorf("failed to embed multipart body: %w", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if err := msg.EmbedReader(filename, dataReader); err != nil {
|
2024-06-19 14:41:13 +02:00
|
|
|
return fmt.Errorf("failed to embed multipart body: %w", err)
|
|
|
|
}
|
2024-10-24 16:42:00 +02:00
|
|
|
default:
|
|
|
|
return errors.New("unsupported content disposition type")
|
2024-06-19 14:41:13 +02:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|