Compare commits

...

12 commits

Author SHA1 Message Date
481fc1d48c
Refactor variable names in eml.go for clarity
Variable names in eml.go have been refactored for better readability and understanding. Shortened abbreviations have been expanded into meaningful names, and complex object names have been made simpler, making it easier to understand their role within the codebase. Cooperative variable names will improve maintainability and ease future development. This is a follow up to #179 which didn't consider this branch.
2024-05-27 10:59:38 +02:00
34f2141a26
Merge branch 'refs/heads/main' into feature/145_add-eml-parser-to-generate-msg-from-eml-files 2024-05-27 09:56:03 +02:00
661b7dace2
Merge pull request #237 from wneessen/sync_with_upstream
Sync with upstream
2024-05-24 18:58:57 +02:00
071ad66035
Sync with upstream
This PR syncs our smtp package with the upstream `net/smtp` changes introduced via bf91eb3a8b
2024-05-24 18:50:27 +02:00
85a99b3a4e
Merge pull request #236 from wneessen/dependabot/github_actions/sonarsource/sonarqube-scan-action-2.1.0
Bump sonarsource/sonarqube-scan-action from 2.0.2 to 2.1.0
2024-05-23 16:09:05 +02:00
dependabot[bot]
b174a9cce1
Bump sonarsource/sonarqube-scan-action from 2.0.2 to 2.1.0
Bumps [sonarsource/sonarqube-scan-action](https://github.com/sonarsource/sonarqube-scan-action) from 2.0.2 to 2.1.0.
- [Release notes](https://github.com/sonarsource/sonarqube-scan-action/releases)
- [Commits](53c3e3207f...86fe817756)

---
updated-dependencies:
- dependency-name: sonarsource/sonarqube-scan-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-23 14:08:07 +00:00
8af360a2b2
Merge pull request #235 from wneessen/dependabot/github_actions/step-security/harden-runner-2.8.0
Bump step-security/harden-runner from 2.7.1 to 2.8.0
2024-05-22 15:27:27 +02:00
dependabot[bot]
6991aecc5d
---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-22 13:15:46 +00:00
0733614cf5
Merge pull request #233 from wneessen/dependabot/github_actions/codecov/codecov-action-4.4.1
Bump codecov/codecov-action from 4.4.0 to 4.4.1
2024-05-21 15:37:34 +02:00
cf85ed6a80
Merge pull request #234 from wneessen/dependabot/github_actions/github/codeql-action-3.25.6
Bump github/codeql-action from 3.25.5 to 3.25.6
2024-05-21 15:37:25 +02:00
dependabot[bot]
af9dcfbdae
---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-21 13:22:23 +00:00
dependabot[bot]
533bd2938e
---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-21 13:22:13 +00:00
11 changed files with 139 additions and 138 deletions

View file

@ -39,7 +39,7 @@ jobs:
go: [1.18, 1.19, '1.20', '1.21', '1.22'] go: [1.18, 1.19, '1.20', '1.21', '1.22']
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
with: with:
egress-policy: audit egress-policy: audit
@ -58,6 +58,6 @@ jobs:
go test -v -race --coverprofile=coverage.coverprofile --covermode=atomic ./... go test -v -race --coverprofile=coverage.coverprofile --covermode=atomic ./...
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
if: success() && matrix.go == '1.22' && matrix.os == 'ubuntu-latest' if: success() && matrix.go == '1.22' && matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@6d798873df2b1b8e5846dba6fb86631229fbcb17 # v4.4.0 uses: codecov/codecov-action@125fc84a9a348dbcf27191600683ec096ec9021c # v4.4.1
with: with:
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos

View file

@ -45,7 +45,7 @@ jobs:
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
with: with:
egress-policy: audit egress-policy: audit
@ -54,7 +54,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5 uses: github/codeql-action/init@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@ -65,7 +65,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5 uses: github/codeql-action/autobuild@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@ -79,4 +79,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5 uses: github/codeql-action/analyze@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6

View file

@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
with: with:
egress-policy: audit egress-policy: audit

View file

@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
with: with:
egress-policy: audit egress-policy: audit

View file

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
with: with:
egress-policy: audit egress-policy: audit
- name: Run govulncheck - name: Run govulncheck

View file

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
with: with:
egress-policy: audit egress-policy: audit

View file

@ -35,7 +35,7 @@ jobs:
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
with: with:
egress-policy: audit egress-policy: audit
@ -75,6 +75,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard. # Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning" - name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5 uses: github/codeql-action/upload-sarif@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
with: with:
sarif_file: results.sarif sarif_file: results.sarif

View file

@ -27,7 +27,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
with: with:
egress-policy: audit egress-policy: audit
@ -44,7 +44,7 @@ jobs:
run: | run: |
go test -v -race --coverprofile=./cov.out ./... go test -v -race --coverprofile=./cov.out ./...
- uses: sonarsource/sonarqube-scan-action@53c3e3207fe4b8d52e2f1ac9d6eb1d2506f626c0 # master - uses: sonarsource/sonarqube-scan-action@86fe81775628f1c6349c28baab87881a2170f495 # master
env: env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}

240
eml.go
View file

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

View file

@ -210,7 +210,8 @@ func (c *Client) Auth(a Auth) error {
} }
resp64 := make([]byte, encoding.EncodedLen(len(resp))) resp64 := make([]byte, encoding.EncodedLen(len(resp)))
encoding.Encode(resp64, resp) encoding.Encode(resp64, resp)
code, msg64, err := c.cmd(0, strings.TrimSpace(fmt.Sprintf("AUTH %s %s", mech, resp64))) code, msg64, err := c.cmd(0, "%s", strings.TrimSpace(fmt.Sprintf("AUTH %s %s", mech,
resp64)))
for err == nil { for err == nil {
var msg []byte var msg []byte
switch code { switch code {
@ -238,7 +239,7 @@ func (c *Client) Auth(a Auth) error {
} }
resp64 = make([]byte, encoding.EncodedLen(len(resp))) resp64 = make([]byte, encoding.EncodedLen(len(resp)))
encoding.Encode(resp64, resp) encoding.Encode(resp64, resp)
code, msg64, err = c.cmd(0, string(resp64)) code, msg64, err = c.cmd(0, "%s", resp64)
} }
return err return err
} }

View file

@ -1042,12 +1042,12 @@ func TestSendMail(t *testing.T) {
tc := textproto.NewConn(conn) tc := textproto.NewConn(conn)
for i := 0; i < len(data) && data[i] != ""; i++ { for i := 0; i < len(data) && data[i] != ""; i++ {
if err := tc.PrintfLine(data[i]); err != nil { if err := tc.PrintfLine("%s", data[i]); err != nil {
t.Errorf("printing to textproto failed: %s", err) t.Errorf("printing to textproto failed: %s", err)
} }
for len(data[i]) >= 4 && data[i][3] == '-' { for len(data[i]) >= 4 && data[i][3] == '-' {
i++ i++
if err := tc.PrintfLine(data[i]); err != nil { if err := tc.PrintfLine("%s", data[i]); err != nil {
t.Errorf("printing to textproto failed: %s", err) t.Errorf("printing to textproto failed: %s", err)
} }
} }