diff --git a/eml.go b/eml.go index bc96c0a..9db20ab 100644 --- a/eml.go +++ b/eml.go @@ -13,97 +13,97 @@ import ( "mime" "mime/multipart" "mime/quotedprintable" - nm "net/mail" + netmail "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) +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(r io.Reader) (*Msg, error) { - m := &Msg{ - addrHeader: make(map[AddrHeader][]*nm.Address), +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, } - pm, bodybuf, err := readEMLFromReader(r) - if err != nil || pm == nil { - return m, fmt.Errorf("failed to parse EML from reader: %w", err) + parsedMsg, bodybuf, err := readEMLFromReader(reader) + if err != nil || parsedMsg == nil { + return msg, 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 = parseEMLHeaders(&parsedMsg.Header, msg); err != nil { + return msg, 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) + if err = parseEMLBodyParts(parsedMsg, bodybuf, msg); err != nil { + return msg, fmt.Errorf("failed to parse EML body parts: %w", err) } - return m, nil + 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(fp string) (*Msg, error) { - m := &Msg{ - addrHeader: make(map[AddrHeader][]*nm.Address), +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, } - pm, bodybuf, err := readEML(fp) - if err != nil || pm == nil { - return m, fmt.Errorf("failed to parse EML file: %w", err) + parsedMsg, bodybuf, err := readEML(filePath) + if err != nil || parsedMsg == nil { + return msg, 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 = parseEMLHeaders(&parsedMsg.Header, msg); err != nil { + return msg, 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) + if err = parseEMLBodyParts(parsedMsg, bodybuf, msg); err != nil { + return msg, fmt.Errorf("failed to parse EML body parts: %w", err) } - return m, nil + return msg, 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) +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() { - _ = fh.Close() + _ = fileHandle.Close() }() - return readEMLFromReader(fh) + return readEMLFromReader(fileHandle) } // 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) +func readEMLFromReader(reader io.Reader) (*netmail.Message, *bytes.Buffer, error) { + parsedMsg, err := netmail.ReadMessage(reader) if err != nil { - return pm, nil, fmt.Errorf("failed to parse EML: %w", err) + return parsedMsg, nil, fmt.Errorf("failed to parse EML: %w", err) } buf := bytes.Buffer{} - if _, err = buf.ReadFrom(pm.Body); err != nil { + if _, err = buf.ReadFrom(parsedMsg.Body); err != nil { return nil, nil, err } - return pm, &buf, nil + 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(mh *nm.Header, m *Msg) error { +func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error { commonHeaders := []Header{ HeaderContentType, HeaderImportance, HeaderInReplyTo, HeaderListUnsubscribe, HeaderListUnsubscribePost, HeaderMessageID, HeaderMIMEVersion, HeaderOrganization, @@ -112,72 +112,72 @@ func parseEMLHeaders(mh *nm.Header, m *Msg) error { } // Extract content type, charset and encoding first - if v := mh.Get(HeaderContentTransferEnc.String()); v != "" { + if value := mailHeader.Get(HeaderContentTransferEnc.String()); value != "" { switch { - case strings.EqualFold(v, EncodingQP.String()): - m.SetEncoding(EncodingQP) - case strings.EqualFold(v, EncodingB64.String()): - m.SetEncoding(EncodingB64) + case strings.EqualFold(value, EncodingQP.String()): + msg.SetEncoding(EncodingQP) + case strings.EqualFold(value, EncodingB64.String()): + msg.SetEncoding(EncodingB64) default: - m.SetEncoding(NoEncoding) + msg.SetEncoding(NoEncoding) } } - if v := mh.Get(HeaderContentType.String()); v != "" { - ct, cs := parseContentType(v) - if cs != "" { - m.SetCharset(Charset(cs)) + if value := mailHeader.Get(HeaderContentType.String()); value != "" { + contentType, charSet := parseContentType(value) + if charSet != "" { + msg.SetCharset(Charset(charSet)) } - m.setEncoder() - if ct != "" { - m.SetGenHeader(HeaderContentType, ct) + msg.setEncoder() + if contentType != "" { + msg.SetGenHeader(HeaderContentType, contentType) } } // Extract address headers - if v := mh.Get(HeaderFrom.String()); v != "" { - if err := m.From(v); err != nil { + 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) } } - ahl := map[AddrHeader]func(...string) error{ - HeaderTo: m.To, - HeaderCc: m.Cc, - HeaderBcc: m.Bcc, + addrHeaders := map[AddrHeader]func(...string) error{ + HeaderTo: msg.To, + HeaderCc: msg.Cc, + HeaderBcc: msg.Bcc, } - for h, f := range ahl { - if v := mh.Get(h.String()); v != "" { - var als []string - pal, err := nm.ParseAddressList(v) + 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 _, a := range pal { - als = append(als, a.String()) + for _, addr := range parsedAddrs { + addrStrings = append(addrStrings, addr.String()) } - if err := f(als...); err != nil { + if err = addrFunc(addrStrings...); err != nil { return fmt.Errorf(`failed to parse %q header: %w`, HeaderTo, err) } } } // Extract date from message - d, err := mh.Date() + date, err := mailHeader.Date() if err != nil { switch { - case errors.Is(err, nm.ErrHeaderNotPresent): - m.SetDate() + case errors.Is(err, netmail.ErrHeaderNotPresent): + msg.SetDate() default: return fmt.Errorf("failed to parse EML date: %w", err) } } if err == nil { - m.SetDateWithValue(d) + msg.SetDateWithValue(date) } // Extract common headers - for _, h := range commonHeaders { - if v := mh.Get(h.String()); v != "" { - m.SetGenHeader(h, v) + for _, header := range commonHeaders { + if value := mailHeader.Get(header.String()); value != "" { + msg.SetGenHeader(header, value) } } @@ -185,25 +185,25 @@ func parseEMLHeaders(mh *nm.Header, m *Msg) error { } // 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 { +func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error { // Extract the transfer encoding of the body - mediatype, params, err := mime.ParseMediaType(pm.Header.Get(HeaderContentType.String())) + mediatype, params, err := mime.ParseMediaType(parsedMsg.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)) + 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, pm, bodybuf, m); err != nil { + 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, "multipart/mixed"): - if err := parseEMLMultipartAlternative(params, bodybuf, m); err != nil { + if err = parseEMLMultipartAlternative(params, bodybuf, msg); err != nil { return fmt.Errorf("failed to parse multipart/alternative body: %w", err) } default: @@ -212,114 +212,114 @@ func parseEMLBodyParts(pm *nm.Message, bodybuf *bytes.Buffer, m *Msg) error { } // 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()) +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(cte, EncodingQP.String()) { - m.SetEncoding(EncodingQP) - qpr := quotedprintable.NewReader(bodybuf) - qpbuf := bytes.Buffer{} - if _, err := qpbuf.ReadFrom(qpr); err != 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) } - m.SetBodyString(ContentType(mediatype), qpbuf.String()) + msg.SetBodyString(ContentType(mediatype), qpBuffer.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 { + 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) } - m.SetBodyString(ContentType(mediatype), b64buf.String()) + msg.SetBodyString(ContentType(mediatype), b64Buffer.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 { +func parseEMLMultipartAlternative(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") } - mpreader := multipart.NewReader(bodybuf, boundary) - mpart, err := mpreader.NextPart() + multipartReader := multipart.NewReader(bodybuf, boundary) + multiPart, err := multipartReader.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) + multiPartData, mperr := io.ReadAll(multiPart) if mperr != nil { - _ = mpart.Close() + _ = multiPart.Close() return fmt.Errorf("failed to read multipart: %w", err) } - mpContentType, ok := mpart.Header[HeaderContentType.String()] + multiPartContentType, ok := multiPart.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)) + contentType, charSet := parseContentType(multiPartContentType[0]) + p := msg.newPart(ContentType(contentType)) p.SetCharset(Charset(charSet)) - mpTransferEnc, ok := mpart.Header[HeaderContentTransferEnc.String()] + 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 - mpTransferEnc = []string{EncodingQP.String()} + mutliPartTransferEnc = []string{EncodingQP.String()} } switch { - case strings.EqualFold(mpTransferEnc[0], EncodingB64.String()): - if err := handleEMLMultiPartBase64Encoding(mpdata, p); err != nil { + case strings.EqualFold(mutliPartTransferEnc[0], EncodingB64.String()): + if err := handleEMLMultiPartBase64Encoding(multiPartData, 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)) + case strings.EqualFold(mutliPartTransferEnc[0], EncodingQP.String()): + p.SetContent(string(multiPartData)) default: return fmt.Errorf("unsupported Content-Transfer-Encoding") } - m.parts = append(m.parts, p) - mpart, err = mpreader.NextPart() + msg.parts = append(msg.parts, p) + multiPart, err = multipartReader.NextPart() } if !errors.Is(err, io.EOF) { - _ = mpart.Close() + _ = multiPart.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)) +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) } - p.SetContent(string(cont)) + part.SetContent(string(content)) 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 { +func parseContentType(contentTypeHeader string) (contentType string, charSet string) { + contentTypeSplit := strings.SplitN(contentTypeHeader, "; ", 2) + if len(contentTypeSplit) != 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] + contentType = contentTypeSplit[0] + if strings.HasPrefix(strings.ToLower(contentTypeSplit[1]), "charset=") { + charSetSplit := strings.SplitN(contentTypeSplit[1], "=", 2) + if len(charSetSplit) == 2 { + charSet = charSetSplit[1] } } return