mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-23 14:10:50 +01:00
Compare commits
No commits in common. "481fc1d48c63442f00793941677b4717543e6067" and "4ef24a5234f2ae0d72a2d0a059e531f60a2fb57a" have entirely different histories.
481fc1d48c
...
4ef24a5234
11 changed files with 138 additions and 139 deletions
4
.github/workflows/codecov.yml
vendored
4
.github/workflows/codecov.yml
vendored
|
@ -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@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
|
uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1
|
||||||
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@125fc84a9a348dbcf27191600683ec096ec9021c # v4.4.1
|
uses: codecov/codecov-action@6d798873df2b1b8e5846dba6fb86631229fbcb17 # v4.4.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
||||||
|
|
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
|
@ -45,7 +45,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
|
uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1
|
||||||
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@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
uses: github/codeql-action/init@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5
|
||||||
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@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
uses: github/codeql-action/autobuild@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5
|
||||||
|
|
||||||
# ℹ️ 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@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
uses: github/codeql-action/analyze@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5
|
||||||
|
|
2
.github/workflows/dependency-review.yml
vendored
2
.github/workflows/dependency-review.yml
vendored
|
@ -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@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
|
uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|
2
.github/workflows/golangci-lint.yml
vendored
2
.github/workflows/golangci-lint.yml
vendored
|
@ -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@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
|
uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|
2
.github/workflows/govulncheck.yml
vendored
2
.github/workflows/govulncheck.yml
vendored
|
@ -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@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
|
uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
- name: Run govulncheck
|
- name: Run govulncheck
|
||||||
|
|
2
.github/workflows/reuse.yml
vendored
2
.github/workflows/reuse.yml
vendored
|
@ -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@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
|
uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|
4
.github/workflows/scorecards.yml
vendored
4
.github/workflows/scorecards.yml
vendored
|
@ -35,7 +35,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
|
uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1
|
||||||
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@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
uses: github/codeql-action/upload-sarif@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
|
4
.github/workflows/sonarqube.yml
vendored
4
.github/workflows/sonarqube.yml
vendored
|
@ -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@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
|
uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1
|
||||||
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@86fe81775628f1c6349c28baab87881a2170f495 # master
|
- uses: sonarsource/sonarqube-scan-action@53c3e3207fe4b8d52e2f1ac9d6eb1d2506f626c0 # 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
240
eml.go
|
@ -13,97 +13,97 @@ import (
|
||||||
"mime"
|
"mime"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"mime/quotedprintable"
|
"mime/quotedprintable"
|
||||||
netmail "net/mail"
|
nm "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(emlString string) (*Msg, error) {
|
func EMLToMsgFromString(es string) (*Msg, error) {
|
||||||
eb := bytes.NewBufferString(emlString)
|
eb := bytes.NewBufferString(es)
|
||||||
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(reader io.Reader) (*Msg, error) {
|
func EMLToMsgFromReader(r io.Reader) (*Msg, error) {
|
||||||
msg := &Msg{
|
m := &Msg{
|
||||||
addrHeader: make(map[AddrHeader][]*netmail.Address),
|
addrHeader: make(map[AddrHeader][]*nm.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,
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedMsg, bodybuf, err := readEMLFromReader(reader)
|
pm, bodybuf, err := readEMLFromReader(r)
|
||||||
if err != nil || parsedMsg == nil {
|
if err != nil || pm == nil {
|
||||||
return msg, fmt.Errorf("failed to parse EML from reader: %w", err)
|
return m, fmt.Errorf("failed to parse EML from reader: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = parseEMLHeaders(&parsedMsg.Header, msg); err != nil {
|
if err = parseEMLHeaders(&pm.Header, m); err != nil {
|
||||||
return msg, fmt.Errorf("failed to parse EML headers: %w", err)
|
return m, fmt.Errorf("failed to parse EML headers: %w", err)
|
||||||
}
|
}
|
||||||
if err = parseEMLBodyParts(parsedMsg, bodybuf, msg); err != nil {
|
if err = parseEMLBodyParts(pm, bodybuf, m); err != nil {
|
||||||
return msg, fmt.Errorf("failed to parse EML body parts: %w", err)
|
return m, fmt.Errorf("failed to parse EML body parts: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return msg, nil
|
return m, 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(filePath string) (*Msg, error) {
|
func EMLToMsgFromFile(fp string) (*Msg, error) {
|
||||||
msg := &Msg{
|
m := &Msg{
|
||||||
addrHeader: make(map[AddrHeader][]*netmail.Address),
|
addrHeader: make(map[AddrHeader][]*nm.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,
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedMsg, bodybuf, err := readEML(filePath)
|
pm, bodybuf, err := readEML(fp)
|
||||||
if err != nil || parsedMsg == nil {
|
if err != nil || pm == nil {
|
||||||
return msg, fmt.Errorf("failed to parse EML file: %w", err)
|
return m, fmt.Errorf("failed to parse EML file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = parseEMLHeaders(&parsedMsg.Header, msg); err != nil {
|
if err = parseEMLHeaders(&pm.Header, m); err != nil {
|
||||||
return msg, fmt.Errorf("failed to parse EML headers: %w", err)
|
return m, fmt.Errorf("failed to parse EML headers: %w", err)
|
||||||
}
|
}
|
||||||
if err = parseEMLBodyParts(parsedMsg, bodybuf, msg); err != nil {
|
if err = parseEMLBodyParts(pm, bodybuf, m); err != nil {
|
||||||
return msg, fmt.Errorf("failed to parse EML body parts: %w", err)
|
return m, fmt.Errorf("failed to parse EML body parts: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return msg, nil
|
return m, 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(filePath string) (*netmail.Message, *bytes.Buffer, error) {
|
func readEML(fp string) (*nm.Message, *bytes.Buffer, error) {
|
||||||
fileHandle, err := os.Open(filePath)
|
fh, err := os.Open(fp)
|
||||||
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() {
|
||||||
_ = fileHandle.Close()
|
_ = fh.Close()
|
||||||
}()
|
}()
|
||||||
return readEMLFromReader(fileHandle)
|
return readEMLFromReader(fh)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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(reader io.Reader) (*netmail.Message, *bytes.Buffer, error) {
|
func readEMLFromReader(r io.Reader) (*nm.Message, *bytes.Buffer, error) {
|
||||||
parsedMsg, err := netmail.ReadMessage(reader)
|
pm, err := nm.ReadMessage(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return parsedMsg, nil, fmt.Errorf("failed to parse EML: %w", err)
|
return pm, nil, fmt.Errorf("failed to parse EML: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := bytes.Buffer{}
|
buf := bytes.Buffer{}
|
||||||
if _, err = buf.ReadFrom(parsedMsg.Body); err != nil {
|
if _, err = buf.ReadFrom(pm.Body); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsedMsg, &buf, nil
|
return pm, &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(mailHeader *netmail.Header, msg *Msg) error {
|
func parseEMLHeaders(mh *nm.Header, m *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(mailHeader *netmail.Header, msg *Msg) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract content type, charset and encoding first
|
// Extract content type, charset and encoding first
|
||||||
if value := mailHeader.Get(HeaderContentTransferEnc.String()); value != "" {
|
if v := mh.Get(HeaderContentTransferEnc.String()); v != "" {
|
||||||
switch {
|
switch {
|
||||||
case strings.EqualFold(value, EncodingQP.String()):
|
case strings.EqualFold(v, EncodingQP.String()):
|
||||||
msg.SetEncoding(EncodingQP)
|
m.SetEncoding(EncodingQP)
|
||||||
case strings.EqualFold(value, EncodingB64.String()):
|
case strings.EqualFold(v, EncodingB64.String()):
|
||||||
msg.SetEncoding(EncodingB64)
|
m.SetEncoding(EncodingB64)
|
||||||
default:
|
default:
|
||||||
msg.SetEncoding(NoEncoding)
|
m.SetEncoding(NoEncoding)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if value := mailHeader.Get(HeaderContentType.String()); value != "" {
|
if v := mh.Get(HeaderContentType.String()); v != "" {
|
||||||
contentType, charSet := parseContentType(value)
|
ct, cs := parseContentType(v)
|
||||||
if charSet != "" {
|
if cs != "" {
|
||||||
msg.SetCharset(Charset(charSet))
|
m.SetCharset(Charset(cs))
|
||||||
}
|
}
|
||||||
msg.setEncoder()
|
m.setEncoder()
|
||||||
if contentType != "" {
|
if ct != "" {
|
||||||
msg.SetGenHeader(HeaderContentType, contentType)
|
m.SetGenHeader(HeaderContentType, ct)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract address headers
|
// Extract address headers
|
||||||
if value := mailHeader.Get(HeaderFrom.String()); value != "" {
|
if v := mh.Get(HeaderFrom.String()); v != "" {
|
||||||
if err := msg.From(value); err != nil {
|
if err := m.From(v); err != nil {
|
||||||
return fmt.Errorf(`failed to parse %q header: %w`, HeaderFrom, err)
|
return fmt.Errorf(`failed to parse %q header: %w`, HeaderFrom, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
addrHeaders := map[AddrHeader]func(...string) error{
|
ahl := map[AddrHeader]func(...string) error{
|
||||||
HeaderTo: msg.To,
|
HeaderTo: m.To,
|
||||||
HeaderCc: msg.Cc,
|
HeaderCc: m.Cc,
|
||||||
HeaderBcc: msg.Bcc,
|
HeaderBcc: m.Bcc,
|
||||||
}
|
}
|
||||||
for addrHeader, addrFunc := range addrHeaders {
|
for h, f := range ahl {
|
||||||
if v := mailHeader.Get(addrHeader.String()); v != "" {
|
if v := mh.Get(h.String()); v != "" {
|
||||||
var addrStrings []string
|
var als []string
|
||||||
parsedAddrs, err := netmail.ParseAddressList(v)
|
pal, err := nm.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 _, addr := range parsedAddrs {
|
for _, a := range pal {
|
||||||
addrStrings = append(addrStrings, addr.String())
|
als = append(als, a.String())
|
||||||
}
|
}
|
||||||
if err = addrFunc(addrStrings...); err != nil {
|
if err := f(als...); 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
|
||||||
date, err := mailHeader.Date()
|
d, err := mh.Date()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, netmail.ErrHeaderNotPresent):
|
case errors.Is(err, nm.ErrHeaderNotPresent):
|
||||||
msg.SetDate()
|
m.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 {
|
||||||
msg.SetDateWithValue(date)
|
m.SetDateWithValue(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract common headers
|
// Extract common headers
|
||||||
for _, header := range commonHeaders {
|
for _, h := range commonHeaders {
|
||||||
if value := mailHeader.Get(header.String()); value != "" {
|
if v := mh.Get(h.String()); v != "" {
|
||||||
msg.SetGenHeader(header, value)
|
m.SetGenHeader(h, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,25 +185,25 @@ func parseEMLHeaders(mailHeader *netmail.Header, msg *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(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
|
func parseEMLBodyParts(pm *nm.Message, bodybuf *bytes.Buffer, m *Msg) error {
|
||||||
// Extract the transfer encoding of the body
|
// Extract the transfer encoding of the body
|
||||||
mediatype, params, err := mime.ParseMediaType(parsedMsg.Header.Get(HeaderContentType.String()))
|
mediatype, params, err := mime.ParseMediaType(pm.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 value, ok := params["charset"]; ok {
|
if v, ok := params["charset"]; ok {
|
||||||
msg.SetCharset(Charset(value))
|
m.SetCharset(Charset(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
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, parsedMsg, bodybuf, msg); err != nil {
|
if err := parseEMLBodyPlain(mediatype, pm, bodybuf, m); 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, msg); err != nil {
|
if err := parseEMLMultipartAlternative(params, bodybuf, m); 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(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *M
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseEMLBodyPlain parses the mail body of plain type mails
|
// parseEMLBodyPlain parses the mail body of plain type mails
|
||||||
func parseEMLBodyPlain(mediatype string, parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
|
func parseEMLBodyPlain(mediatype string, pm *nm.Message, bodybuf *bytes.Buffer, m *Msg) error {
|
||||||
contentTransferEnc := parsedMsg.Header.Get(HeaderContentTransferEnc.String())
|
cte := pm.Header.Get(HeaderContentTransferEnc.String())
|
||||||
if strings.EqualFold(contentTransferEnc, NoEncoding.String()) {
|
if strings.EqualFold(cte, NoEncoding.String()) {
|
||||||
msg.SetEncoding(NoEncoding)
|
m.SetEncoding(NoEncoding)
|
||||||
msg.SetBodyString(ContentType(mediatype), bodybuf.String())
|
m.SetBodyString(ContentType(mediatype), bodybuf.String())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if strings.EqualFold(contentTransferEnc, EncodingQP.String()) {
|
if strings.EqualFold(cte, EncodingQP.String()) {
|
||||||
msg.SetEncoding(EncodingQP)
|
m.SetEncoding(EncodingQP)
|
||||||
qpReader := quotedprintable.NewReader(bodybuf)
|
qpr := quotedprintable.NewReader(bodybuf)
|
||||||
qpBuffer := bytes.Buffer{}
|
qpbuf := bytes.Buffer{}
|
||||||
if _, err := qpBuffer.ReadFrom(qpReader); err != nil {
|
if _, err := qpbuf.ReadFrom(qpr); err != nil {
|
||||||
return fmt.Errorf("failed to read quoted-printable body: %w", err)
|
return fmt.Errorf("failed to read quoted-printable body: %w", err)
|
||||||
}
|
}
|
||||||
msg.SetBodyString(ContentType(mediatype), qpBuffer.String())
|
m.SetBodyString(ContentType(mediatype), qpbuf.String())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if strings.EqualFold(contentTransferEnc, EncodingB64.String()) {
|
if strings.EqualFold(cte, EncodingB64.String()) {
|
||||||
msg.SetEncoding(EncodingB64)
|
m.SetEncoding(EncodingB64)
|
||||||
b64Decoder := base64.NewDecoder(base64.StdEncoding, bodybuf)
|
b64d := base64.NewDecoder(base64.StdEncoding, bodybuf)
|
||||||
b64Buffer := bytes.Buffer{}
|
b64buf := bytes.Buffer{}
|
||||||
if _, err := b64Buffer.ReadFrom(b64Decoder); err != nil {
|
if _, err := b64buf.ReadFrom(b64d); err != nil {
|
||||||
return fmt.Errorf("failed to read base64 body: %w", err)
|
return fmt.Errorf("failed to read base64 body: %w", err)
|
||||||
}
|
}
|
||||||
msg.SetBodyString(ContentType(mediatype), b64Buffer.String())
|
m.SetBodyString(ContentType(mediatype), b64buf.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, msg *Msg) error {
|
func parseEMLMultipartAlternative(params map[string]string, bodybuf *bytes.Buffer, m *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")
|
||||||
}
|
}
|
||||||
multipartReader := multipart.NewReader(bodybuf, boundary)
|
mpreader := multipart.NewReader(bodybuf, boundary)
|
||||||
multiPart, err := multipartReader.NextPart()
|
mpart, err := mpreader.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 {
|
||||||
multiPartData, mperr := io.ReadAll(multiPart)
|
mpdata, mperr := io.ReadAll(mpart)
|
||||||
if mperr != nil {
|
if mperr != nil {
|
||||||
_ = multiPart.Close()
|
_ = mpart.Close()
|
||||||
return fmt.Errorf("failed to read multipart: %w", err)
|
return fmt.Errorf("failed to read multipart: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
multiPartContentType, ok := multiPart.Header[HeaderContentType.String()]
|
mpContentType, ok := mpart.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")
|
||||||
}
|
}
|
||||||
contentType, charSet := parseContentType(multiPartContentType[0])
|
conType, charSet := parseContentType(mpContentType[0])
|
||||||
p := msg.newPart(ContentType(contentType))
|
p := m.newPart(ContentType(conType))
|
||||||
p.SetCharset(Charset(charSet))
|
p.SetCharset(Charset(charSet))
|
||||||
|
|
||||||
mutliPartTransferEnc, ok := multiPart.Header[HeaderContentTransferEnc.String()]
|
mpTransferEnc, ok := mpart.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
|
||||||
mutliPartTransferEnc = []string{EncodingQP.String()}
|
mpTransferEnc = []string{EncodingQP.String()}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case strings.EqualFold(mutliPartTransferEnc[0], EncodingB64.String()):
|
case strings.EqualFold(mpTransferEnc[0], EncodingB64.String()):
|
||||||
if err := handleEMLMultiPartBase64Encoding(multiPartData, p); err != nil {
|
if err := handleEMLMultiPartBase64Encoding(mpdata, 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(mutliPartTransferEnc[0], EncodingQP.String()):
|
case strings.EqualFold(mpTransferEnc[0], EncodingQP.String()):
|
||||||
p.SetContent(string(multiPartData))
|
p.SetContent(string(mpdata))
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported Content-Transfer-Encoding")
|
return fmt.Errorf("unsupported Content-Transfer-Encoding")
|
||||||
}
|
}
|
||||||
|
|
||||||
msg.parts = append(msg.parts, p)
|
m.parts = append(m.parts, p)
|
||||||
multiPart, err = multipartReader.NextPart()
|
mpart, err = mpreader.NextPart()
|
||||||
}
|
}
|
||||||
if !errors.Is(err, io.EOF) {
|
if !errors.Is(err, io.EOF) {
|
||||||
_ = multiPart.Close()
|
_ = mpart.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(multiPartData []byte, part *Part) error {
|
func handleEMLMultiPartBase64Encoding(mpdata []byte, p *Part) error {
|
||||||
part.SetEncoding(EncodingB64)
|
p.SetEncoding(EncodingB64)
|
||||||
content, err := base64.StdEncoding.DecodeString(string(multiPartData))
|
cont, err := base64.StdEncoding.DecodeString(string(mpdata))
|
||||||
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)
|
||||||
}
|
}
|
||||||
part.SetContent(string(content))
|
p.SetContent(string(cont))
|
||||||
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(contentTypeHeader string) (contentType string, charSet string) {
|
func parseContentType(cth string) (ct string, cs string) {
|
||||||
contentTypeSplit := strings.SplitN(contentTypeHeader, "; ", 2)
|
cts := strings.SplitN(cth, "; ", 2)
|
||||||
if len(contentTypeSplit) != 2 {
|
if len(cts) != 2 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
contentType = contentTypeSplit[0]
|
ct = cts[0]
|
||||||
if strings.HasPrefix(strings.ToLower(contentTypeSplit[1]), "charset=") {
|
if strings.HasPrefix(strings.ToLower(cts[1]), "charset=") {
|
||||||
charSetSplit := strings.SplitN(contentTypeSplit[1], "=", 2)
|
css := strings.SplitN(cts[1], "=", 2)
|
||||||
if len(charSetSplit) == 2 {
|
if len(css) == 2 {
|
||||||
charSet = charSetSplit[1]
|
cs = css[1]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|
|
@ -210,8 +210,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, "%s", strings.TrimSpace(fmt.Sprintf("AUTH %s %s", mech,
|
code, msg64, err := c.cmd(0, strings.TrimSpace(fmt.Sprintf("AUTH %s %s", mech, resp64)))
|
||||||
resp64)))
|
|
||||||
for err == nil {
|
for err == nil {
|
||||||
var msg []byte
|
var msg []byte
|
||||||
switch code {
|
switch code {
|
||||||
|
@ -239,7 +238,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, "%s", resp64)
|
code, msg64, err = c.cmd(0, string(resp64))
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -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("%s", data[i]); err != nil {
|
if err := tc.PrintfLine(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("%s", data[i]); err != nil {
|
if err := tc.PrintfLine(data[i]); err != nil {
|
||||||
t.Errorf("printing to textproto failed: %s", err)
|
t.Errorf("printing to textproto failed: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue