Compare commits

..

No commits in common. "4ef24a5234f2ae0d72a2d0a059e531f60a2fb57a" and "4ec95adb30cbd926e211c60caa18f39aec9c63cc" have entirely different histories.

34 changed files with 1094 additions and 1521 deletions

View file

@ -1,15 +0,0 @@
# SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
#
# SPDX-License-Identifier: CC0-1.0
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: daily
- package-ecosystem: gomod
directory: /
schedule:
interval: daily

View file

@ -27,9 +27,6 @@ env:
TEST_SMTPAUTH_USER: ${{ secrets.TEST_USER }} TEST_SMTPAUTH_USER: ${{ secrets.TEST_USER }}
TEST_SMTPAUTH_PASS: ${{ secrets.TEST_PASS }} TEST_SMTPAUTH_PASS: ${{ secrets.TEST_PASS }}
TEST_SMTPAUTH_TYPE: "LOGIN" TEST_SMTPAUTH_TYPE: "LOGIN"
permissions:
contents: read
jobs: jobs:
run: run:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@ -38,15 +35,10 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
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
uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1
with:
egress-policy: audit
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master uses: actions/checkout@master
- name: Setup go - name: Setup go
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 uses: actions/setup-go@v3
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
- name: Install sendmail - name: Install sendmail
@ -58,6 +50,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@v3
with: with:
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos

View file

@ -24,9 +24,6 @@ on:
schedule: schedule:
- cron: '37 23 * * 5' - cron: '37 23 * * 5'
permissions:
contents: read
jobs: jobs:
analyze: analyze:
name: Analyze name: Analyze
@ -44,17 +41,12 @@ jobs:
# Learn more about CodeQL language support at https://git.io/codeql-language-support # Learn more about CodeQL language support at https://git.io/codeql-language-support
steps: steps:
- name: Harden Runner
uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1
with:
egress-policy: audit
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0 uses: actions/checkout@v2
# 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@v1
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 +57,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@v1
# 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 +71,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@v1

View file

@ -1,31 +0,0 @@
# SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
#
# SPDX-License-Identifier: CC0-1.0
# Dependency Review Action
#
# This Action will scan dependency manifest files that change as part of a Pull Request,
# surfacing known-vulnerable versions of the packages declared or updated in the PR.
# Once installed, if the workflow run is marked as required,
# PRs introducing known-vulnerable packages will be blocked from merging.
#
# Source repository: https://github.com/actions/dependency-review-action
name: 'Dependency Review'
on: [pull_request]
permissions:
contents: read
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1
with:
egress-policy: audit
- name: 'Checkout Repository'
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- name: 'Dependency Review'
uses: actions/dependency-review-action@0c155c5e8556a497adf53f2c18edabf945ed8e70 # v4.3.2

View file

@ -19,17 +19,12 @@ jobs:
name: lint name: lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner - uses: actions/setup-go@v3
uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1
with:
egress-policy: audit
- uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with: with:
go-version: '1.22' go-version: '1.22'
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - uses: actions/checkout@v3
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # v6.0.1 uses: golangci/golangci-lint-action@v3
with: with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
version: latest version: latest

View file

@ -1,21 +0,0 @@
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
#
# SPDX-License-Identifier: CC0-1.0
name: Govulncheck Security Scan
on: [push, pull_request]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1
with:
egress-policy: audit
- name: Run govulncheck
uses: golang/govulncheck-action@3a32958c2706f7048305d5a2e53633d7e37e97d0 # v1.0.2

View file

@ -6,18 +6,10 @@ name: REUSE Compliance Check
on: [push, pull_request] on: [push, pull_request]
permissions:
contents: read
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner - uses: actions/checkout@v2
uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1
with:
egress-policy: audit
- uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
- name: REUSE Compliance Check - name: REUSE Compliance Check
uses: fsfe/reuse-action@a46482ca367aef4454a87620aa37c2be4b2f8106 # v3.0.0 uses: fsfe/reuse-action@v1

View file

@ -1,80 +0,0 @@
# SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
#
# SPDX-License-Identifier: CC0-1.0
# This workflow uses actions that are not certified by GitHub. They are provided
# by a third-party and are governed by separate terms of service, privacy
# policy, and support documentation.
name: Scorecard supply-chain security
on:
# For Branch-Protection check. Only the default branch is supported. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
branch_protection_rule:
# To guarantee Maintained check is occasionally updated. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
schedule:
- cron: '20 7 * * 2'
push:
branches: ["main"]
# Declare default permissions as read only.
permissions: read-all
jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-latest
permissions:
# Needed to upload the results to code-scanning dashboard.
security-events: write
# Needed to publish results and get a badge (see publish_results below).
id-token: write
contents: read
actions: read
steps:
- name: Harden Runner
uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1
with:
egress-policy: audit
- name: "Checkout code"
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
with:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3
with:
results_file: results.sarif
results_format: sarif
# (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
# - you want to enable the Branch-Protection check on a *public* repository, or
# - you are installing Scorecards on a *private* repository
# To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.
# repo_token: ${{ secrets.SCORECARD_TOKEN }}
# Public repositories:
# - Publish results to OpenSSF REST API for easy access by consumers
# - Allows the repository to include the Scorecard badge.
# - See https://github.com/ossf/scorecard-action#publishing-results.
# For private repositories:
# - `publish_results` will always be set to `false`, regardless
# of the value entered here.
publish_results: true
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: SARIF file
path: results.sarif
retention-days: 5
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5
with:
sarif_file: results.sarif

View file

@ -3,10 +3,6 @@
# SPDX-License-Identifier: CC0-1.0 # SPDX-License-Identifier: CC0-1.0
name: SonarQube name: SonarQube
permissions:
contents: read
on: on:
push: push:
branches: branches:
@ -26,17 +22,12 @@ jobs:
name: Build name: Build
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner - uses: actions/checkout@v2
uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1
with:
egress-policy: audit
- uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 uses: actions/setup-go@v3
with: with:
go-version: '1.22.x' go-version: '1.22.x'
@ -44,12 +35,12 @@ 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@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 }}
- uses: sonarsource/sonarqube-quality-gate-action@72f24ebf1f81eda168a979ce14b8203273b7c3ad # master - uses: sonarsource/sonarqube-quality-gate-action@master
timeout-minutes: 5 timeout-minutes: 5
env: env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

2
.gitignore vendored
View file

@ -55,5 +55,3 @@ com_crashlytics_export_strings.xml
crashlytics.properties crashlytics.properties
crashlytics-build.properties crashlytics-build.properties
fabric.properties fabric.properties
testdata

View file

@ -12,8 +12,6 @@ SPDX-License-Identifier: CC0-1.0
[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge-flat.svg)](https://github.com/avelino/awesome-go) [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge-flat.svg)](https://github.com/avelino/awesome-go)
[![#go-mail on Discord](https://img.shields.io/badge/Discord-%23go%E2%80%93mail-blue.svg)](https://discord.gg/ysQXkaccXk) [![#go-mail on Discord](https://img.shields.io/badge/Discord-%23go%E2%80%93mail-blue.svg)](https://discord.gg/ysQXkaccXk)
[![REUSE status](https://api.reuse.software/badge/github.com/wneessen/go-mail)](https://api.reuse.software/info/github.com/wneessen/go-mail) [![REUSE status](https://api.reuse.software/badge/github.com/wneessen/go-mail)](https://api.reuse.software/info/github.com/wneessen/go-mail)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/8701/badge)](https://www.bestpractices.dev/projects/8701)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/wneessen/go-mail/badge)](https://securityscorecards.dev/viewer/?uri=github.com/wneessen/go-mail)
<a href="https://ko-fi.com/D1D24V9IX"><img src="https://uploads-ssl.webflow.com/5c14e387dab576fe667689cf/5cbed8a4ae2b88347c06c923_BuyMeACoffee_blue.png" height="20" alt="buy ma a coffee"></a> <a href="https://ko-fi.com/D1D24V9IX"><img src="https://uploads-ssl.webflow.com/5c14e387dab576fe667689cf/5cbed8a4ae2b88347c06c923_BuyMeACoffee_blue.png" height="20" alt="buy ma a coffee"></a>
<p align="center"><img src="./assets/gopher2.svg" width="250" alt="go-mail logo"/></p> <p align="center"><img src="./assets/gopher2.svg" width="250" alt="go-mail logo"/></p>
@ -85,13 +83,6 @@ alter a given mail message to their needs without relying on `go-mail` to suppor
To get our users started with message middleware, we've created a collection of useful middlewares. It can be To get our users started with message middleware, we've created a collection of useful middlewares. It can be
found in a seperate repository: [go-mail-middlware](https://github.com/wneessen/go-mail-middleware). found in a seperate repository: [go-mail-middlware](https://github.com/wneessen/go-mail-middleware).
## Merch
Thanks to our wonderful friends at [HelloTux](https://www.hellotux.com) we can offer great go-mail merchandising. All merch articles are embroidery
to provide the best and most long-lasting quality possible.
If you want to support the open source community and represent your favourite Go mail library with some cool drip, check out our merch shop at:
[https://www.hellotux.com/go-mail](https://www.hellotux.com/go-mail).
## Examples ## Examples
We provide example code in both our GoDocs as well as on our official Website (see [Documentation](#documentation)). For a quick start into go-mail We provide example code in both our GoDocs as well as on our official Website (see [Documentation](#documentation)). For a quick start into go-mail

View file

@ -20,39 +20,39 @@ type Base64LineBreaker struct {
out io.Writer out io.Writer
} }
var newlineBytes = []byte(SingleNewLine) var nl = []byte(SingleNewLine)
// Write writes the data stream and inserts a SingleNewLine when the maximum // Write writes the data stream and inserts a SingleNewLine when the maximum
// line length is reached // line length is reached
func (l *Base64LineBreaker) Write(data []byte) (numBytes int, err error) { func (l *Base64LineBreaker) Write(b []byte) (n int, err error) {
if l.out == nil { if l.out == nil {
err = fmt.Errorf(ErrNoOutWriter) err = fmt.Errorf(ErrNoOutWriter)
return return
} }
if l.used+len(data) < MaxBodyLength { if l.used+len(b) < MaxBodyLength {
copy(l.line[l.used:], data) copy(l.line[l.used:], b)
l.used += len(data) l.used += len(b)
return len(data), nil return len(b), nil
} }
numBytes, err = l.out.Write(l.line[0:l.used]) n, err = l.out.Write(l.line[0:l.used])
if err != nil { if err != nil {
return return
} }
excess := MaxBodyLength - l.used excess := MaxBodyLength - l.used
l.used = 0 l.used = 0
numBytes, err = l.out.Write(data[0:excess]) n, err = l.out.Write(b[0:excess])
if err != nil { if err != nil {
return return
} }
numBytes, err = l.out.Write(newlineBytes) n, err = l.out.Write(nl)
if err != nil { if err != nil {
return return
} }
return l.Write(data[excess:]) return l.Write(b[excess:])
} }
// Close closes the Base64LineBreaker and writes any access data that is still // Close closes the Base64LineBreaker and writes any access data that is still
@ -63,7 +63,7 @@ func (l *Base64LineBreaker) Close() (err error) {
if err != nil { if err != nil {
return return
} }
_, err = l.out.Write(newlineBytes) _, err = l.out.Write(nl)
} }
return return

View file

@ -5,10 +5,8 @@
package mail package mail
import ( import (
"bufio"
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -384,11 +382,6 @@ LjI4MiIgc3R5bGU9ImZpbGw6I2ZmYjI1YztzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6NC45NXB4
OyIvPjwvZz48L3N2Zz4= OyIvPjwvZz48L3N2Zz4=
` `
var (
errMockDefault = errors.New("mock write error")
errMockNewline = errors.New("mock newline error")
)
// TestBase64LineBreaker tests the Write and Close methods of the Base64LineBreaker // TestBase64LineBreaker tests the Write and Close methods of the Base64LineBreaker
func TestBase64LineBreaker(t *testing.T) { func TestBase64LineBreaker(t *testing.T) {
l, err := os.Open("assets/gopher2.svg") l, err := os.Open("assets/gopher2.svg")
@ -443,47 +436,6 @@ func TestBase64LineBreakerFailures(t *testing.T) {
} }
} }
func TestBase64LineBreaker_WriteAndClose(t *testing.T) {
tests := []struct {
name string
data []byte
writer io.Writer
}{
{
name: "Write data within MaxBodyLength",
data: []byte("testdata"),
writer: &mockWriterExcess{writeError: errMockDefault},
},
{
name: "Write data exceeds MaxBodyLength",
data: []byte("verylongtestdataverylongtestdataverylongtestdata" +
"verylongtestdataverylongtestdataverylongtestdata"),
writer: &mockWriterExcess{writeError: errMockDefault},
},
{
name: "Write data exceeds MaxBodyLength with newline",
data: []byte("verylongtestdataverylongtestdataverylongtestdata" +
"verylongtestdataverylongtestdataverylongtestdata"),
writer: &mockWriterNewline{writeError: errMockDefault},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
blr := &Base64LineBreaker{out: tt.writer}
_, err := blr.Write(tt.data)
if err != nil && !errors.Is(err, errMockDefault) && !errors.Is(err, errMockNewline) {
t.Errorf("Unexpected error while writing: %v", err)
}
err = blr.Close()
if err != nil && !errors.Is(err, errMockDefault) && !errors.Is(err, errMockNewline) {
t.Errorf("Unexpected error while closing: %v", err)
}
})
}
}
// removeNewLines removes any newline characters from the given data // removeNewLines removes any newline characters from the given data
func removeNewLines(data []byte) []byte { func removeNewLines(data []byte) []byte {
result := make([]byte, len(data)) result := make([]byte, len(data))
@ -509,49 +461,3 @@ func (e errorWriter) Write([]byte) (int, error) {
func (e errorWriter) Close() error { func (e errorWriter) Close() error {
return fmt.Errorf("supposed to always fail") return fmt.Errorf("supposed to always fail")
} }
type mockWriterExcess struct {
writeError error
}
type mockWriterNewline struct {
writeError error
}
func (w *mockWriterExcess) Write(p []byte) (n int, err error) {
switch len(p) {
case 0:
return 0, nil
case 2:
return 2, nil
default:
return len(p), errMockDefault
}
}
func (w *mockWriterNewline) Write(p []byte) (n int, err error) {
switch len(p) {
case 0:
return 0, nil
case 2:
return 2, errMockNewline
default:
return len(p), nil
}
}
func FuzzBase64LineBreaker_Write(f *testing.F) {
f.Add([]byte("abc"))
f.Add([]byte("def"))
f.Add([]uint8{0o0, 0o1, 0o2, 30, 255})
buf := bytes.Buffer{}
bw := bufio.NewWriter(&buf)
f.Fuzz(func(t *testing.T, data []byte) {
b := &Base64LineBreaker{out: bw}
if _, err := b.Write(data); err != nil {
t.Errorf("failed to write to B64LineBreaker: %s", err)
}
if err := b.Close(); err != nil {
t.Errorf("failed to close B64LineBreaker: %s", err)
}
})
}

343
client.go
View file

@ -87,11 +87,11 @@ type DialContextFunc func(ctx context.Context, network, address string) (net.Con
// Client is the SMTP client struct // Client is the SMTP client struct
type Client struct { type Client struct {
// connection is the net.Conn that the smtp.Client is based on // co is the net.Conn that the smtp.Client is based on
connection net.Conn co net.Conn
// Timeout for the SMTP server connection // Timeout for the SMTP server connection
connTimeout time.Duration cto time.Duration
// dsn indicates that we want to use DSN for the Client // dsn indicates that we want to use DSN for the Client
dsn bool dsn bool
@ -102,8 +102,8 @@ type Client struct {
// dsnrntype defines the DSNRcptNotifyOption in case DSN is enabled // dsnrntype defines the DSNRcptNotifyOption in case DSN is enabled
dsnrntype []string dsnrntype []string
// isEncrypted indicates if a Client connection is encrypted or not // enc indicates if a Client connection is encrypted or not
isEncrypted bool enc bool
// noNoop indicates the Noop is to be skipped // noNoop indicates the Noop is to be skipped
noNoop bool noNoop bool
@ -121,17 +121,17 @@ type Client struct {
port int port int
fallbackPort int fallbackPort int
// smtpAuth is a pointer to smtp.Auth // sa is a pointer to smtp.Auth
smtpAuth smtp.Auth sa smtp.Auth
// smtpAuthType represents the authentication type for SMTP AUTH // satype represents the authentication type for SMTP AUTH
smtpAuthType SMTPAuthType satype SMTPAuthType
// smtpClient is the smtp.Client that is set up when using the Dial*() methods // sc is the smtp.Client that is set up when using the Dial*() methods
smtpClient *smtp.Client sc *smtp.Client
// Use SSL for the connection // Use SSL for the connection
useSSL bool ssl bool
// tlspolicy sets the client to use the provided TLSPolicy for the STARTTLS protocol // tlspolicy sets the client to use the provided TLSPolicy for the STARTTLS protocol
tlspolicy TLSPolicy tlspolicy TLSPolicy
@ -142,11 +142,11 @@ type Client struct {
// user is the SMTP AUTH username // user is the SMTP AUTH username
user string user string
// useDebugLog enables the debug logging on the SMTP client // dl enables the debug logging on the SMTP client
useDebugLog bool dl bool
// logger is a logger that implements the log.Logger interface // l is a logger that implements the log.Logger interface
logger log.Logger l log.Logger
// dialContextFunc is a custom DialContext function to dial target SMTP server // dialContextFunc is a custom DialContext function to dial target SMTP server
dialContextFunc DialContextFunc dialContextFunc DialContextFunc
@ -198,12 +198,12 @@ var (
) )
// NewClient returns a new Session client object // NewClient returns a new Session client object
func NewClient(host string, opts ...Option) (*Client, error) { func NewClient(h string, o ...Option) (*Client, error) {
c := &Client{ c := &Client{
connTimeout: DefaultTimeout, cto: DefaultTimeout,
host: host, host: h,
port: DefaultPort, port: DefaultPort,
tlsconfig: &tls.Config{ServerName: host, MinVersion: DefaultTLSMinVersion}, tlsconfig: &tls.Config{ServerName: h, MinVersion: DefaultTLSMinVersion},
tlspolicy: DefaultTLSPolicy, tlspolicy: DefaultTLSPolicy,
} }
@ -213,11 +213,11 @@ func NewClient(host string, opts ...Option) (*Client, error) {
} }
// Override defaults with optionally provided Option functions // Override defaults with optionally provided Option functions
for _, opt := range opts { for _, co := range o {
if opt == nil { if co == nil {
continue continue
} }
if err := opt(c); err != nil { if err := co(c); err != nil {
return c, fmt.Errorf("failed to apply option: %w", err) return c, fmt.Errorf("failed to apply option: %w", err)
} }
} }
@ -231,48 +231,45 @@ func NewClient(host string, opts ...Option) (*Client, error) {
} }
// WithPort overrides the default connection port // WithPort overrides the default connection port
func WithPort(port int) Option { func WithPort(p int) Option {
return func(c *Client) error { return func(c *Client) error {
if port < 1 || port > 65535 { if p < 1 || p > 65535 {
return ErrInvalidPort return ErrInvalidPort
} }
c.port = port c.port = p
return nil return nil
} }
} }
// WithTimeout overrides the default connection timeout // WithTimeout overrides the default connection timeout
func WithTimeout(timeout time.Duration) Option { func WithTimeout(t time.Duration) Option {
return func(c *Client) error { return func(c *Client) error {
if timeout <= 0 { if t <= 0 {
return ErrInvalidTimeout return ErrInvalidTimeout
} }
c.connTimeout = timeout c.cto = t
return nil return nil
} }
} }
// WithSSL tells the client to use a SSL/TLS connection // WithSSL tells the client to use a SSL/TLS connection
//
// Deprecated: use WithSSLPort instead.
func WithSSL() Option { func WithSSL() Option {
return func(c *Client) error { return func(c *Client) error {
c.useSSL = true c.ssl = true
return nil return nil
} }
} }
// WithSSLPort tells the Client wether or not to use SSL and fallback. // WithSSLPort tells the client to use a SSL/TLS connection.
// The correct port is automatically set. // It automatically sets the port to 465.
// //
// Port 465 is used when SSL set (true). // When the SSL connection fails and fallback is set to true,
// Port 25 is used when SSL is unset (false).
// When the SSL connection fails and fb is set to true,
// the client will attempt to connect on port 25 using plaintext. // the client will attempt to connect on port 25 using plaintext.
// func WithSSLPort(fb bool) Option {
// Note: If a different port has already been set otherwise, the port-choosing
// and fallback automatism will be skipped.
func WithSSLPort(fallback bool) Option {
return func(c *Client) error { return func(c *Client) error {
c.SetSSLPort(true, fallback) c.SetSSLPort(true, fb)
return nil return nil
} }
} }
@ -281,37 +278,36 @@ func WithSSLPort(fallback bool) Option {
// to StdErr // to StdErr
func WithDebugLog() Option { func WithDebugLog() Option {
return func(c *Client) error { return func(c *Client) error {
c.useDebugLog = true c.dl = true
return nil return nil
} }
} }
// WithLogger overrides the default log.Logger that is used for debug logging // WithLogger overrides the default log.Logger that is used for debug logging
func WithLogger(logger log.Logger) Option { func WithLogger(l log.Logger) Option {
return func(c *Client) error { return func(c *Client) error {
c.logger = logger c.l = l
return nil return nil
} }
} }
// WithHELO tells the client to use the provided string as HELO/EHLO greeting host // WithHELO tells the client to use the provided string as HELO/EHLO greeting host
func WithHELO(helo string) Option { func WithHELO(h string) Option {
return func(c *Client) error { return func(c *Client) error {
if helo == "" { if h == "" {
return ErrInvalidHELO return ErrInvalidHELO
} }
c.helo = helo c.helo = h
return nil return nil
} }
} }
// WithTLSPolicy tells the client to use the provided TLSPolicy // WithTLSPolicy tells the client to use the provided TLSPolicy
// //
// Note: To follow best-practices for SMTP TLS connections, it is recommended // Deprecated: use WithTLSPortPolicy instead.
// to use WithTLSPortPolicy instead. func WithTLSPolicy(p TLSPolicy) Option {
func WithTLSPolicy(policy TLSPolicy) Option {
return func(c *Client) error { return func(c *Client) error {
c.tlspolicy = policy c.tlspolicy = p
return nil return nil
} }
} }
@ -323,55 +319,52 @@ func WithTLSPolicy(policy TLSPolicy) Option {
// If the connection fails with TLSOpportunistic, // If the connection fails with TLSOpportunistic,
// a plaintext connection is attempted on port 25 as a fallback. // a plaintext connection is attempted on port 25 as a fallback.
// NoTLS will allways use port 25. // NoTLS will allways use port 25.
// func WithTLSPortPolicy(p TLSPolicy) Option {
// Note: If a different port has already been set otherwise, the port-choosing
// and fallback automatism will be skipped.
func WithTLSPortPolicy(policy TLSPolicy) Option {
return func(c *Client) error { return func(c *Client) error {
c.SetTLSPortPolicy(policy) c.SetTLSPortPolicy(p)
return nil return nil
} }
} }
// WithTLSConfig tells the client to use the provided *tls.Config // WithTLSConfig tells the client to use the provided *tls.Config
func WithTLSConfig(tlsconfig *tls.Config) Option { func WithTLSConfig(co *tls.Config) Option {
return func(c *Client) error { return func(c *Client) error {
if tlsconfig == nil { if co == nil {
return ErrInvalidTLSConfig return ErrInvalidTLSConfig
} }
c.tlsconfig = tlsconfig c.tlsconfig = co
return nil return nil
} }
} }
// WithSMTPAuth tells the client to use the provided SMTPAuthType for authentication // WithSMTPAuth tells the client to use the provided SMTPAuthType for authentication
func WithSMTPAuth(authtype SMTPAuthType) Option { func WithSMTPAuth(t SMTPAuthType) Option {
return func(c *Client) error { return func(c *Client) error {
c.smtpAuthType = authtype c.satype = t
return nil return nil
} }
} }
// WithSMTPAuthCustom tells the client to use the provided smtp.Auth for SMTP authentication // WithSMTPAuthCustom tells the client to use the provided smtp.Auth for SMTP authentication
func WithSMTPAuthCustom(smtpAuth smtp.Auth) Option { func WithSMTPAuthCustom(a smtp.Auth) Option {
return func(c *Client) error { return func(c *Client) error {
c.smtpAuth = smtpAuth c.sa = a
return nil return nil
} }
} }
// WithUsername tells the client to use the provided string as username for authentication // WithUsername tells the client to use the provided string as username for authentication
func WithUsername(username string) Option { func WithUsername(u string) Option {
return func(c *Client) error { return func(c *Client) error {
c.user = username c.user = u
return nil return nil
} }
} }
// WithPassword tells the client to use the provided string as password/secret for authentication // WithPassword tells the client to use the provided string as password/secret for authentication
func WithPassword(password string) Option { func WithPassword(p string) Option {
return func(c *Client) error { return func(c *Client) error {
c.pass = password c.pass = p
return nil return nil
} }
} }
@ -393,9 +386,9 @@ func WithDSN() Option {
// as described in the RFC 1891 and set the MAIL FROM Return option type to the // as described in the RFC 1891 and set the MAIL FROM Return option type to the
// given DSNMailReturnOption // given DSNMailReturnOption
// See: https://www.rfc-editor.org/rfc/rfc1891 // See: https://www.rfc-editor.org/rfc/rfc1891
func WithDSNMailReturnType(option DSNMailReturnOption) Option { func WithDSNMailReturnType(mro DSNMailReturnOption) Option {
return func(c *Client) error { return func(c *Client) error {
switch option { switch mro {
case DSNMailReturnHeadersOnly: case DSNMailReturnHeadersOnly:
case DSNMailReturnFull: case DSNMailReturnFull:
default: default:
@ -403,7 +396,7 @@ func WithDSNMailReturnType(option DSNMailReturnOption) Option {
} }
c.dsn = true c.dsn = true
c.dsnmrtype = option c.dsnmrtype = mro
return nil return nil
} }
} }
@ -411,13 +404,13 @@ func WithDSNMailReturnType(option DSNMailReturnOption) Option {
// WithDSNRcptNotifyType enables the Client to request DSNs as described in the RFC 1891 // WithDSNRcptNotifyType enables the Client to request DSNs as described in the RFC 1891
// and sets the RCPT TO notify options to the given list of DSNRcptNotifyOption // and sets the RCPT TO notify options to the given list of DSNRcptNotifyOption
// See: https://www.rfc-editor.org/rfc/rfc1891 // See: https://www.rfc-editor.org/rfc/rfc1891
func WithDSNRcptNotifyType(opts ...DSNRcptNotifyOption) Option { func WithDSNRcptNotifyType(rno ...DSNRcptNotifyOption) Option {
return func(c *Client) error { return func(c *Client) error {
var rcptOpts []string var rnol []string
var ns, nns bool var ns, nns bool
if len(opts) > 0 { if len(rno) > 0 {
for _, opt := range opts { for _, crno := range rno {
switch opt { switch crno {
case DSNRcptNotifyNever: case DSNRcptNotifyNever:
ns = true ns = true
case DSNRcptNotifySuccess: case DSNRcptNotifySuccess:
@ -429,7 +422,7 @@ func WithDSNRcptNotifyType(opts ...DSNRcptNotifyOption) Option {
default: default:
return ErrInvalidDSNRcptNotifyOption return ErrInvalidDSNRcptNotifyOption
} }
rcptOpts = append(rcptOpts, string(opt)) rnol = append(rnol, string(crno))
} }
} }
if ns && nns { if ns && nns {
@ -437,7 +430,7 @@ func WithDSNRcptNotifyType(opts ...DSNRcptNotifyOption) Option {
} }
c.dsn = true c.dsn = true
c.dsnrntype = rcptOpts c.dsnrntype = rnol
return nil return nil
} }
} }
@ -452,9 +445,9 @@ func WithoutNoop() Option {
} }
// WithDialContextFunc overrides the default DialContext for connecting SMTP server // WithDialContextFunc overrides the default DialContext for connecting SMTP server
func WithDialContextFunc(dialCtxFunc DialContextFunc) Option { func WithDialContextFunc(f DialContextFunc) Option {
return func(c *Client) error { return func(c *Client) error {
c.dialContextFunc = dialCtxFunc c.dialContextFunc = f
return nil return nil
} }
} }
@ -470,11 +463,8 @@ func (c *Client) ServerAddr() string {
} }
// SetTLSPolicy overrides the current TLSPolicy with the given TLSPolicy value // SetTLSPolicy overrides the current TLSPolicy with the given TLSPolicy value
// func (c *Client) SetTLSPolicy(p TLSPolicy) {
// Note: To follow best-practices for SMTP TLS connections, it is recommended c.tlspolicy = p
// to use SetTLSPortPolicy instead.
func (c *Client) SetTLSPolicy(policy TLSPolicy) {
c.tlspolicy = policy
} }
// SetTLSPortPolicy overrides the current TLSPolicy with the given TLSPolicy // SetTLSPortPolicy overrides the current TLSPolicy with the given TLSPolicy
@ -484,27 +474,22 @@ func (c *Client) SetTLSPolicy(policy TLSPolicy) {
// If the connection fails with TLSOpportunistic, a plaintext connection is // If the connection fails with TLSOpportunistic, a plaintext connection is
// attempted on port 25 as a fallback. // attempted on port 25 as a fallback.
// NoTLS will allways use port 25. // NoTLS will allways use port 25.
// func (c *Client) SetTLSPortPolicy(p TLSPolicy) {
// Note: If a different port has already been set otherwise, the port-choosing
// and fallback automatism will be skipped.
func (c *Client) SetTLSPortPolicy(policy TLSPolicy) {
if c.port == DefaultPort {
c.port = DefaultPortTLS c.port = DefaultPortTLS
if policy == TLSOpportunistic { if p == TLSOpportunistic {
c.fallbackPort = DefaultPort c.fallbackPort = DefaultPort
} }
if policy == NoTLS { if p == NoTLS {
c.port = DefaultPort c.port = DefaultPort
} }
}
c.tlspolicy = policy c.tlspolicy = p
} }
// SetSSL tells the Client wether to use SSL or not // SetSSL tells the Client wether to use SSL or not
func (c *Client) SetSSL(ssl bool) { func (c *Client) SetSSL(s bool) {
c.useSSL = ssl c.ssl = s
} }
// SetSSLPort tells the Client wether or not to use SSL and fallback. // SetSSLPort tells the Client wether or not to use SSL and fallback.
@ -514,128 +499,124 @@ func (c *Client) SetSSL(ssl bool) {
// Port 25 is used when SSL is unset (false). // Port 25 is used when SSL is unset (false).
// When the SSL connection fails and fb is set to true, // When the SSL connection fails and fb is set to true,
// the client will attempt to connect on port 25 using plaintext. // the client will attempt to connect on port 25 using plaintext.
// func (c *Client) SetSSLPort(ssl bool, fb bool) {
// Note: If a different port has already been set otherwise, the port-choosing c.port = DefaultPort
// and fallback automatism will be skipped.
func (c *Client) SetSSLPort(ssl bool, fallback bool) {
if c.port == DefaultPort {
if ssl { if ssl {
c.port = DefaultPortSSL c.port = DefaultPortSSL
} }
c.fallbackPort = 0 c.fallbackPort = 0
if fallback { if fb {
c.fallbackPort = DefaultPort c.fallbackPort = DefaultPort
} }
}
c.useSSL = ssl c.ssl = ssl
} }
// SetDebugLog tells the Client whether debug logging is enabled or not // SetDebugLog tells the Client whether debug logging is enabled or not
func (c *Client) SetDebugLog(val bool) { func (c *Client) SetDebugLog(v bool) {
c.useDebugLog = val c.dl = v
if c.smtpClient != nil { if c.sc != nil {
c.smtpClient.SetDebugLog(val) c.sc.SetDebugLog(v)
} }
} }
// SetLogger tells the Client which log.Logger to use // SetLogger tells the Client which log.Logger to use
func (c *Client) SetLogger(logger log.Logger) { func (c *Client) SetLogger(l log.Logger) {
c.logger = logger c.l = l
if c.smtpClient != nil { if c.sc != nil {
c.smtpClient.SetLogger(logger) c.sc.SetLogger(l)
} }
} }
// SetTLSConfig overrides the current *tls.Config with the given *tls.Config value // SetTLSConfig overrides the current *tls.Config with the given *tls.Config value
func (c *Client) SetTLSConfig(tlsconfig *tls.Config) error { func (c *Client) SetTLSConfig(co *tls.Config) error {
if tlsconfig == nil { if co == nil {
return ErrInvalidTLSConfig return ErrInvalidTLSConfig
} }
c.tlsconfig = tlsconfig c.tlsconfig = co
return nil return nil
} }
// SetUsername overrides the current username string with the given value // SetUsername overrides the current username string with the given value
func (c *Client) SetUsername(username string) { func (c *Client) SetUsername(u string) {
c.user = username c.user = u
} }
// SetPassword overrides the current password string with the given value // SetPassword overrides the current password string with the given value
func (c *Client) SetPassword(password string) { func (c *Client) SetPassword(p string) {
c.pass = password c.pass = p
} }
// SetSMTPAuth overrides the current SMTP AUTH type setting with the given value // SetSMTPAuth overrides the current SMTP AUTH type setting with the given value
func (c *Client) SetSMTPAuth(authtype SMTPAuthType) { func (c *Client) SetSMTPAuth(a SMTPAuthType) {
c.smtpAuthType = authtype c.satype = a
} }
// SetSMTPAuthCustom overrides the current SMTP AUTH setting with the given custom smtp.Auth // SetSMTPAuthCustom overrides the current SMTP AUTH setting with the given custom smtp.Auth
func (c *Client) SetSMTPAuthCustom(smtpAuth smtp.Auth) { func (c *Client) SetSMTPAuthCustom(sa smtp.Auth) {
c.smtpAuth = smtpAuth c.sa = sa
} }
// setDefaultHelo retrieves the current hostname and sets it as HELO/EHLO hostname // setDefaultHelo retrieves the current hostname and sets it as HELO/EHLO hostname
func (c *Client) setDefaultHelo() error { func (c *Client) setDefaultHelo() error {
hostname, err := os.Hostname() hn, err := os.Hostname()
if err != nil { if err != nil {
return fmt.Errorf("failed to read local hostname: %w", err) return fmt.Errorf("failed cto read local hostname: %w", err)
} }
c.helo = hostname c.helo = hn
return nil return nil
} }
// DialWithContext establishes a connection to the SMTP server with a given context.Context // DialWithContext establishes a connection cto the SMTP server with a given context.Context
func (c *Client) DialWithContext(dialCtx context.Context) error { func (c *Client) DialWithContext(pc context.Context) error {
ctx, cancel := context.WithDeadline(dialCtx, time.Now().Add(c.connTimeout)) ctx, cfn := context.WithDeadline(pc, time.Now().Add(c.cto))
defer cancel() defer cfn()
if c.dialContextFunc == nil { if c.dialContextFunc == nil {
netDialer := net.Dialer{} nd := net.Dialer{}
c.dialContextFunc = netDialer.DialContext c.dialContextFunc = nd.DialContext
if c.useSSL { if c.ssl {
tlsDialer := tls.Dialer{NetDialer: &netDialer, Config: c.tlsconfig} td := tls.Dialer{NetDialer: &nd, Config: c.tlsconfig}
c.isEncrypted = true c.enc = true
c.dialContextFunc = tlsDialer.DialContext c.dialContextFunc = td.DialContext
} }
} }
var err error var err error
c.connection, err = c.dialContextFunc(ctx, "tcp", c.ServerAddr()) c.co, err = c.dialContextFunc(ctx, "tcp", c.ServerAddr())
if err != nil && c.fallbackPort != 0 { if err != nil && c.fallbackPort != 0 {
// TODO: should we somehow log or append the previous error? // TODO: should we somehow log or append the previous error?
c.connection, err = c.dialContextFunc(ctx, "tcp", c.serverFallbackAddr()) c.co, err = c.dialContextFunc(ctx, "tcp", c.serverFallbackAddr())
} }
if err != nil { if err != nil {
return err return err
} }
client, err := smtp.NewClient(c.connection, c.host) sc, err := smtp.NewClient(c.co, c.host)
if err != nil { if err != nil {
return err return err
} }
if client == nil { if sc == nil {
return fmt.Errorf("SMTP client is nil") return fmt.Errorf("SMTP client is nil")
} }
c.smtpClient = client c.sc = sc
if c.logger != nil { if c.l != nil {
c.smtpClient.SetLogger(c.logger) c.sc.SetLogger(c.l)
} }
if c.useDebugLog { if c.dl {
c.smtpClient.SetDebugLog(true) c.sc.SetDebugLog(true)
} }
if err = c.smtpClient.Hello(c.helo); err != nil { if err := c.sc.Hello(c.helo); err != nil {
return err return err
} }
if err = c.tls(); err != nil { if err := c.tls(); err != nil {
return err return err
} }
if err = c.auth(); err != nil { if err := c.auth(); err != nil {
return err return err
} }
@ -647,7 +628,7 @@ func (c *Client) Close() error {
if err := c.checkConn(); err != nil { if err := c.checkConn(); err != nil {
return err return err
} }
if err := c.smtpClient.Quit(); err != nil { if err := c.sc.Quit(); err != nil {
return fmt.Errorf("failed to close SMTP client: %w", err) return fmt.Errorf("failed to close SMTP client: %w", err)
} }
@ -659,7 +640,7 @@ func (c *Client) Reset() error {
if err := c.checkConn(); err != nil { if err := c.checkConn(); err != nil {
return err return err
} }
if err := c.smtpClient.Reset(); err != nil { if err := c.sc.Reset(); err != nil {
return fmt.Errorf("failed to send RSET to SMTP client: %w", err) return fmt.Errorf("failed to send RSET to SMTP client: %w", err)
} }
@ -668,18 +649,18 @@ func (c *Client) Reset() error {
// DialAndSend establishes a connection to the SMTP server with a // DialAndSend establishes a connection to the SMTP server with a
// default context.Background and sends the mail // default context.Background and sends the mail
func (c *Client) DialAndSend(messages ...*Msg) error { func (c *Client) DialAndSend(ml ...*Msg) error {
ctx := context.Background() ctx := context.Background()
return c.DialAndSendWithContext(ctx, messages...) return c.DialAndSendWithContext(ctx, ml...)
} }
// DialAndSendWithContext establishes a connection to the SMTP server with a // DialAndSendWithContext establishes a connection to the SMTP server with a
// custom context and sends the mail // custom context and sends the mail
func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) error { func (c *Client) DialAndSendWithContext(ctx context.Context, ml ...*Msg) error {
if err := c.DialWithContext(ctx); err != nil { if err := c.DialWithContext(ctx); err != nil {
return fmt.Errorf("dial failed: %w", err) return fmt.Errorf("dial failed: %w", err)
} }
if err := c.Send(messages...); err != nil { if err := c.Send(ml...); err != nil {
return fmt.Errorf("send failed: %w", err) return fmt.Errorf("send failed: %w", err)
} }
if err := c.Close(); err != nil { if err := c.Close(); err != nil {
@ -691,17 +672,17 @@ func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) e
// checkConn makes sure that a required server connection is available and extends the // checkConn makes sure that a required server connection is available and extends the
// connection deadline // connection deadline
func (c *Client) checkConn() error { func (c *Client) checkConn() error {
if c.connection == nil { if c.co == nil {
return ErrNoActiveConnection return ErrNoActiveConnection
} }
if !c.noNoop { if !c.noNoop {
if err := c.smtpClient.Noop(); err != nil { if err := c.sc.Noop(); err != nil {
return ErrNoActiveConnection return ErrNoActiveConnection
} }
} }
if err := c.connection.SetDeadline(time.Now().Add(c.connTimeout)); err != nil { if err := c.co.SetDeadline(time.Now().Add(c.cto)); err != nil {
return ErrDeadlineExtendFailed return ErrDeadlineExtendFailed
} }
return nil return nil
@ -715,30 +696,30 @@ func (c *Client) serverFallbackAddr() string {
// tls tries to make sure that the STARTTLS requirements are satisfied // tls tries to make sure that the STARTTLS requirements are satisfied
func (c *Client) tls() error { func (c *Client) tls() error {
if c.connection == nil { if c.co == nil {
return ErrNoActiveConnection return ErrNoActiveConnection
} }
if !c.useSSL && c.tlspolicy != NoTLS { if !c.ssl && c.tlspolicy != NoTLS {
hasStartTLS := false est := false
extension, _ := c.smtpClient.Extension("STARTTLS") st, _ := c.sc.Extension("STARTTLS")
if c.tlspolicy == TLSMandatory { if c.tlspolicy == TLSMandatory {
hasStartTLS = true est = true
if !extension { if !st {
return fmt.Errorf("STARTTLS mode set to: %q, but target host does not support STARTTLS", return fmt.Errorf("STARTTLS mode set to: %q, but target host does not support STARTTLS",
c.tlspolicy) c.tlspolicy)
} }
} }
if c.tlspolicy == TLSOpportunistic { if c.tlspolicy == TLSOpportunistic {
if extension { if st {
hasStartTLS = true est = true
} }
} }
if hasStartTLS { if est {
if err := c.smtpClient.StartTLS(c.tlsconfig); err != nil { if err := c.sc.StartTLS(c.tlsconfig); err != nil {
return err return err
} }
} }
_, c.isEncrypted = c.smtpClient.TLSConnectionState() _, c.enc = c.sc.TLSConnectionState()
} }
return nil return nil
} }
@ -748,40 +729,40 @@ func (c *Client) auth() error {
if err := c.checkConn(); err != nil { if err := c.checkConn(); err != nil {
return fmt.Errorf("failed to authenticate: %w", err) return fmt.Errorf("failed to authenticate: %w", err)
} }
if c.smtpAuth == nil && c.smtpAuthType != "" { if c.sa == nil && c.satype != "" {
hasSMTPAuth, smtpAuthType := c.smtpClient.Extension("AUTH") sa, sat := c.sc.Extension("AUTH")
if !hasSMTPAuth { if !sa {
return fmt.Errorf("server does not support SMTP AUTH") return fmt.Errorf("server does not support SMTP AUTH")
} }
switch c.smtpAuthType { switch c.satype {
case SMTPAuthPlain: case SMTPAuthPlain:
if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) { if !strings.Contains(sat, string(SMTPAuthPlain)) {
return ErrPlainAuthNotSupported return ErrPlainAuthNotSupported
} }
c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host) c.sa = smtp.PlainAuth("", c.user, c.pass, c.host)
case SMTPAuthLogin: case SMTPAuthLogin:
if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) { if !strings.Contains(sat, string(SMTPAuthLogin)) {
return ErrLoginAuthNotSupported return ErrLoginAuthNotSupported
} }
c.smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host) c.sa = smtp.LoginAuth(c.user, c.pass, c.host)
case SMTPAuthCramMD5: case SMTPAuthCramMD5:
if !strings.Contains(smtpAuthType, string(SMTPAuthCramMD5)) { if !strings.Contains(sat, string(SMTPAuthCramMD5)) {
return ErrCramMD5AuthNotSupported return ErrCramMD5AuthNotSupported
} }
c.smtpAuth = smtp.CRAMMD5Auth(c.user, c.pass) c.sa = smtp.CRAMMD5Auth(c.user, c.pass)
case SMTPAuthXOAUTH2: case SMTPAuthXOAUTH2:
if !strings.Contains(smtpAuthType, string(SMTPAuthXOAUTH2)) { if !strings.Contains(sat, string(SMTPAuthXOAUTH2)) {
return ErrXOauth2AuthNotSupported return ErrXOauth2AuthNotSupported
} }
c.smtpAuth = smtp.XOAuth2Auth(c.user, c.pass) c.sa = smtp.XOAuth2Auth(c.user, c.pass)
default: default:
return fmt.Errorf("unsupported SMTP AUTH type %q", c.smtpAuthType) return fmt.Errorf("unsupported SMTP AUTH type %q", c.satype)
} }
} }
if c.smtpAuth != nil { if c.sa != nil {
if err := c.smtpClient.Auth(c.smtpAuth); err != nil { if err := c.sc.Auth(c.sa); err != nil {
return fmt.Errorf("SMTP AUTH failed: %w", err) return fmt.Errorf("SMTP AUTH failed: %w", err)
} }
} }

View file

@ -10,123 +10,123 @@ package mail
import "strings" import "strings"
// Send sends out the mail message // Send sends out the mail message
func (c *Client) Send(messages ...*Msg) error { func (c *Client) Send(ml ...*Msg) error {
if cerr := c.checkConn(); cerr != nil { if cerr := c.checkConn(); cerr != nil {
return &SendError{Reason: ErrConnCheck, errlist: []error{cerr}, isTemp: isTempError(cerr)} return &SendError{Reason: ErrConnCheck, errlist: []error{cerr}, isTemp: isTempError(cerr)}
} }
var errs []*SendError var errs []*SendError
for _, message := range messages { for _, m := range ml {
message.sendError = nil m.sendError = nil
if message.encoding == NoEncoding { if m.encoding == NoEncoding {
if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok { if ok, _ := c.sc.Extension("8BITMIME"); !ok {
sendErr := &SendError{Reason: ErrNoUnencoded, isTemp: false} se := &SendError{Reason: ErrNoUnencoded, isTemp: false}
message.sendError = sendErr m.sendError = se
errs = append(errs, sendErr) errs = append(errs, se)
continue continue
} }
} }
from, err := message.GetSender(false) f, err := m.GetSender(false)
if err != nil { if err != nil {
sendErr := &SendError{Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err)} se := &SendError{Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err)}
message.sendError = sendErr m.sendError = se
errs = append(errs, sendErr) errs = append(errs, se)
continue continue
} }
rcpts, err := message.GetRecipients() rl, err := m.GetRecipients()
if err != nil { if err != nil {
sendErr := &SendError{Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err)} se := &SendError{Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err)}
message.sendError = sendErr m.sendError = se
errs = append(errs, sendErr) errs = append(errs, se)
continue continue
} }
if c.dsn { if c.dsn {
if c.dsnmrtype != "" { if c.dsnmrtype != "" {
c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype)) c.sc.SetDSNMailReturnOption(string(c.dsnmrtype))
} }
} }
if err = c.smtpClient.Mail(from); err != nil { if err := c.sc.Mail(f); err != nil {
sendErr := &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)} se := &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)}
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil { if reserr := c.sc.Reset(); reserr != nil {
sendErr.errlist = append(sendErr.errlist, resetSendErr) se.errlist = append(se.errlist, reserr)
} }
message.sendError = sendErr m.sendError = se
errs = append(errs, sendErr) errs = append(errs, se)
continue continue
} }
failed := false failed := false
rcptSendErr := &SendError{} rse := &SendError{}
rcptSendErr.errlist = make([]error, 0) rse.errlist = make([]error, 0)
rcptSendErr.rcpt = make([]string, 0) rse.rcpt = make([]string, 0)
rcptNotifyOpt := strings.Join(c.dsnrntype, ",") rno := strings.Join(c.dsnrntype, ",")
c.smtpClient.SetDSNRcptNotifyOption(rcptNotifyOpt) c.sc.SetDSNRcptNotifyOption(rno)
for _, rcpt := range rcpts { for _, r := range rl {
if err = c.smtpClient.Rcpt(rcpt); err != nil { if err := c.sc.Rcpt(r); err != nil {
rcptSendErr.Reason = ErrSMTPRcptTo rse.Reason = ErrSMTPRcptTo
rcptSendErr.errlist = append(rcptSendErr.errlist, err) rse.errlist = append(rse.errlist, err)
rcptSendErr.rcpt = append(rcptSendErr.rcpt, rcpt) rse.rcpt = append(rse.rcpt, r)
rcptSendErr.isTemp = isTempError(err) rse.isTemp = isTempError(err)
failed = true failed = true
} }
} }
if failed { if failed {
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil { if reserr := c.sc.Reset(); reserr != nil {
rcptSendErr.errlist = append(rcptSendErr.errlist, err) rse.errlist = append(rse.errlist, err)
} }
message.sendError = rcptSendErr m.sendError = rse
errs = append(errs, rcptSendErr) errs = append(errs, rse)
continue continue
} }
writer, err := c.smtpClient.Data() w, err := c.sc.Data()
if err != nil { if err != nil {
sendErr := &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)} se := &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)}
message.sendError = sendErr m.sendError = se
errs = append(errs, sendErr) errs = append(errs, se)
continue continue
} }
_, err = message.WriteTo(writer) _, err = m.WriteTo(w)
if err != nil { if err != nil {
sendErr := &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err)} se := &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err)}
message.sendError = sendErr m.sendError = se
errs = append(errs, sendErr) errs = append(errs, se)
continue continue
} }
message.isDelivered = true m.isDelivered = true
if err = writer.Close(); err != nil { if err := w.Close(); err != nil {
sendErr := &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)} se := &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)}
message.sendError = sendErr m.sendError = se
errs = append(errs, sendErr) errs = append(errs, se)
continue continue
} }
if err = c.Reset(); err != nil { if err := c.Reset(); err != nil {
sendErr := &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err)} se := &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err)}
message.sendError = sendErr m.sendError = se
errs = append(errs, sendErr) errs = append(errs, se)
continue continue
} }
if err = c.checkConn(); err != nil { if err := c.checkConn(); err != nil {
sendErr := &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} se := &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
message.sendError = sendErr m.sendError = se
errs = append(errs, sendErr) errs = append(errs, se)
continue continue
} }
} }
if len(errs) > 0 { if len(errs) > 0 {
if len(errs) > 1 { if len(errs) > 1 {
returnErr := &SendError{Reason: ErrAmbiguous} re := &SendError{Reason: ErrAmbiguous}
for i := range errs { for i := range errs {
returnErr.errlist = append(returnErr.errlist, errs[i].errlist...) re.errlist = append(re.errlist, errs[i].errlist...)
returnErr.rcpt = append(returnErr.rcpt, errs[i].rcpt...) re.rcpt = append(re.rcpt, errs[i].rcpt...)
} }
// We assume that the isTemp flag from the last error we received should be the // We assume that the isTemp flag from the last error we received should be the
// indicator for the returned isTemp flag as well // indicator for the returned isTemp flag as well
returnErr.isTemp = errs[len(errs)-1].isTemp re.isTemp = errs[len(errs)-1].isTemp
return returnErr return re
} }
return errs[0] return errs[0]
} }

View file

@ -13,97 +13,97 @@ import (
) )
// Send sends out the mail message // Send sends out the mail message
func (c *Client) Send(messages ...*Msg) (returnErr error) { func (c *Client) Send(ml ...*Msg) (rerr error) {
if err := c.checkConn(); err != nil { if err := c.checkConn(); err != nil {
returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} rerr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
return return
} }
for _, message := range messages { for _, m := range ml {
message.sendError = nil m.sendError = nil
if message.encoding == NoEncoding { if m.encoding == NoEncoding {
if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok { if ok, _ := c.sc.Extension("8BITMIME"); !ok {
message.sendError = &SendError{Reason: ErrNoUnencoded, isTemp: false} m.sendError = &SendError{Reason: ErrNoUnencoded, isTemp: false}
returnErr = errors.Join(returnErr, message.sendError) rerr = errors.Join(rerr, m.sendError)
continue continue
} }
} }
from, err := message.GetSender(false) f, err := m.GetSender(false)
if err != nil { if err != nil {
message.sendError = &SendError{Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err)} m.sendError = &SendError{Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err)}
returnErr = errors.Join(returnErr, message.sendError) rerr = errors.Join(rerr, m.sendError)
continue continue
} }
rcpts, err := message.GetRecipients() rl, err := m.GetRecipients()
if err != nil { if err != nil {
message.sendError = &SendError{Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err)} m.sendError = &SendError{Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err)}
returnErr = errors.Join(returnErr, message.sendError) rerr = errors.Join(rerr, m.sendError)
continue continue
} }
if c.dsn { if c.dsn {
if c.dsnmrtype != "" { if c.dsnmrtype != "" {
c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype)) c.sc.SetDSNMailReturnOption(string(c.dsnmrtype))
} }
} }
if err = c.smtpClient.Mail(from); err != nil { if err := c.sc.Mail(f); err != nil {
message.sendError = &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)} m.sendError = &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)}
returnErr = errors.Join(returnErr, message.sendError) rerr = errors.Join(rerr, m.sendError)
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil { if reserr := c.sc.Reset(); reserr != nil {
returnErr = errors.Join(returnErr, resetSendErr) rerr = errors.Join(rerr, reserr)
} }
continue continue
} }
failed := false failed := false
rcptSendErr := &SendError{} rse := &SendError{}
rcptSendErr.errlist = make([]error, 0) rse.errlist = make([]error, 0)
rcptSendErr.rcpt = make([]string, 0) rse.rcpt = make([]string, 0)
rcptNotifyOpt := strings.Join(c.dsnrntype, ",") rno := strings.Join(c.dsnrntype, ",")
c.smtpClient.SetDSNRcptNotifyOption(rcptNotifyOpt) c.sc.SetDSNRcptNotifyOption(rno)
for _, rcpt := range rcpts { for _, r := range rl {
if err = c.smtpClient.Rcpt(rcpt); err != nil { if err := c.sc.Rcpt(r); err != nil {
rcptSendErr.Reason = ErrSMTPRcptTo rse.Reason = ErrSMTPRcptTo
rcptSendErr.errlist = append(rcptSendErr.errlist, err) rse.errlist = append(rse.errlist, err)
rcptSendErr.rcpt = append(rcptSendErr.rcpt, rcpt) rse.rcpt = append(rse.rcpt, r)
rcptSendErr.isTemp = isTempError(err) rse.isTemp = isTempError(err)
failed = true failed = true
} }
} }
if failed { if failed {
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil { if reserr := c.sc.Reset(); reserr != nil {
returnErr = errors.Join(returnErr, resetSendErr) rerr = errors.Join(rerr, reserr)
} }
message.sendError = rcptSendErr m.sendError = rse
returnErr = errors.Join(returnErr, message.sendError) rerr = errors.Join(rerr, m.sendError)
continue continue
} }
writer, err := c.smtpClient.Data() w, err := c.sc.Data()
if err != nil { if err != nil {
message.sendError = &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)} m.sendError = &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)}
returnErr = errors.Join(returnErr, message.sendError) rerr = errors.Join(rerr, m.sendError)
continue continue
} }
_, err = message.WriteTo(writer) _, err = m.WriteTo(w)
if err != nil { if err != nil {
message.sendError = &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err)} m.sendError = &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err)}
returnErr = errors.Join(returnErr, message.sendError) rerr = errors.Join(rerr, m.sendError)
continue continue
} }
message.isDelivered = true m.isDelivered = true
if err = writer.Close(); err != nil { if err := w.Close(); err != nil {
message.sendError = &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)} m.sendError = &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)}
returnErr = errors.Join(returnErr, message.sendError) rerr = errors.Join(rerr, m.sendError)
continue continue
} }
if err = c.Reset(); err != nil { if err := c.Reset(); err != nil {
message.sendError = &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err)} m.sendError = &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err)}
returnErr = errors.Join(returnErr, message.sendError) rerr = errors.Join(rerr, m.sendError)
continue continue
} }
if err = c.checkConn(); err != nil { if err := c.checkConn(); err != nil {
message.sendError = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} m.sendError = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
returnErr = errors.Join(returnErr, message.sendError) rerr = errors.Join(rerr, m.sendError)
} }
} }

View file

@ -49,9 +49,9 @@ func TestNewClient(t *testing.T) {
if c.host != tt.host { if c.host != tt.host {
t.Errorf("failed to create new client. Host expected: %s, got: %s", host, c.host) t.Errorf("failed to create new client. Host expected: %s, got: %s", host, c.host)
} }
if c.connTimeout != DefaultTimeout { if c.cto != DefaultTimeout {
t.Errorf("failed to create new client. Timeout expected: %s, got: %s", DefaultTimeout.String(), t.Errorf("failed to create new client. Timeout expected: %s, got: %s", DefaultTimeout.String(),
c.connTimeout.String()) c.cto.String())
} }
if c.port != DefaultPort { if c.port != DefaultPort {
t.Errorf("failed to create new client. Port expected: %d, got: %d", DefaultPort, c.port) t.Errorf("failed to create new client. Port expected: %d, got: %d", DefaultPort, c.port)
@ -205,8 +205,8 @@ func TestWithTimeout(t *testing.T) {
t.Errorf("failed to create new client: %s", err) t.Errorf("failed to create new client: %s", err)
return return
} }
if c.connTimeout != tt.want { if c.cto != tt.want {
t.Errorf("failed to set custom timeout. Want: %d, got: %d", tt.want, c.connTimeout) t.Errorf("failed to set custom timeout. Want: %d, got: %d", tt.want, c.cto)
} }
}) })
} }
@ -345,8 +345,8 @@ func TestSetSSL(t *testing.T) {
return return
} }
c.SetSSL(tt.value) c.SetSSL(tt.value)
if c.useSSL != tt.value { if c.ssl != tt.value {
t.Errorf("failed to set SSL setting. Got: %t, want: %t", c.useSSL, tt.value) t.Errorf("failed to set SSL setting. Got: %t, want: %t", c.ssl, tt.value)
} }
}) })
} }
@ -374,8 +374,8 @@ func TestClient_SetSSLPort(t *testing.T) {
return return
} }
c.SetSSLPort(tt.value, tt.fb) c.SetSSLPort(tt.value, tt.fb)
if c.useSSL != tt.value { if c.ssl != tt.value {
t.Errorf("failed to set SSL setting. Got: %t, want: %t", c.useSSL, tt.value) t.Errorf("failed to set SSL setting. Got: %t, want: %t", c.ssl, tt.value)
} }
if c.port != tt.port { if c.port != tt.port {
t.Errorf("failed to set SSLPort, wanted port: %d, got: %d", c.port, tt.port) t.Errorf("failed to set SSLPort, wanted port: %d, got: %d", c.port, tt.port)
@ -460,8 +460,8 @@ func TestSetSMTPAuth(t *testing.T) {
return return
} }
c.SetSMTPAuth(tt.value) c.SetSMTPAuth(tt.value)
if string(c.smtpAuthType) != tt.want { if string(c.satype) != tt.want {
t.Errorf("failed to set SMTP auth type. Expected %s, got: %s", tt.want, string(c.smtpAuthType)) t.Errorf("failed to set SMTP auth type. Expected %s, got: %s", tt.want, string(c.satype))
} }
}) })
} }
@ -590,10 +590,10 @@ func TestSetSMTPAuthCustom(t *testing.T) {
return return
} }
c.SetSMTPAuthCustom(tt.value) c.SetSMTPAuthCustom(tt.value)
if c.smtpAuth == nil { if c.sa == nil {
t.Errorf("failed to set custom SMTP auth method. SMTP Auth method is empty") t.Errorf("failed to set custom SMTP auth method. SMTP Auth method is empty")
} }
p, _, err := c.smtpAuth.Start(&si) p, _, err := c.sa.Start(&si)
if err != nil { if err != nil {
t.Errorf("SMTP Auth Start() method returned error: %s", err) t.Errorf("SMTP Auth Start() method returned error: %s", err)
} }
@ -615,10 +615,10 @@ func TestClient_DialWithContext(t *testing.T) {
t.Errorf("failed to dial with context: %s", err) t.Errorf("failed to dial with context: %s", err)
return return
} }
if c.connection == nil { if c.co == nil {
t.Errorf("DialWithContext didn't fail but no connection found.") t.Errorf("DialWithContext didn't fail but no connection found.")
} }
if c.smtpClient == nil { if c.sc == nil {
t.Errorf("DialWithContext didn't fail but no SMTP client found.") t.Errorf("DialWithContext didn't fail but no SMTP client found.")
} }
if err := c.Close(); err != nil { if err := c.Close(); err != nil {
@ -640,10 +640,10 @@ func TestClient_DialWithContext_Fallback(t *testing.T) {
t.Errorf("failed to dial with context: %s", err) t.Errorf("failed to dial with context: %s", err)
return return
} }
if c.connection == nil { if c.co == nil {
t.Errorf("DialWithContext didn't fail but no connection found.") t.Errorf("DialWithContext didn't fail but no connection found.")
} }
if c.smtpClient == nil { if c.sc == nil {
t.Errorf("DialWithContext didn't fail but no SMTP client found.") t.Errorf("DialWithContext didn't fail but no SMTP client found.")
} }
if err := c.Close(); err != nil { if err := c.Close(); err != nil {
@ -670,10 +670,10 @@ func TestClient_DialWithContext_Debug(t *testing.T) {
t.Errorf("failed to dial with context: %s", err) t.Errorf("failed to dial with context: %s", err)
return return
} }
if c.connection == nil { if c.co == nil {
t.Errorf("DialWithContext didn't fail but no connection found.") t.Errorf("DialWithContext didn't fail but no connection found.")
} }
if c.smtpClient == nil { if c.sc == nil {
t.Errorf("DialWithContext didn't fail but no SMTP client found.") t.Errorf("DialWithContext didn't fail but no SMTP client found.")
} }
c.SetDebugLog(true) c.SetDebugLog(true)
@ -694,10 +694,10 @@ func TestClient_DialWithContext_Debug_custom(t *testing.T) {
t.Errorf("failed to dial with context: %s", err) t.Errorf("failed to dial with context: %s", err)
return return
} }
if c.connection == nil { if c.co == nil {
t.Errorf("DialWithContext didn't fail but no connection found.") t.Errorf("DialWithContext didn't fail but no connection found.")
} }
if c.smtpClient == nil { if c.sc == nil {
t.Errorf("DialWithContext didn't fail but no SMTP client found.") t.Errorf("DialWithContext didn't fail but no SMTP client found.")
} }
c.SetDebugLog(true) c.SetDebugLog(true)
@ -714,7 +714,7 @@ func TestClient_DialWithContextInvalidHost(t *testing.T) {
if err != nil { if err != nil {
t.Skipf("failed to create test client: %s. Skipping tests", err) t.Skipf("failed to create test client: %s. Skipping tests", err)
} }
c.connection = nil c.co = nil
c.host = "invalid.addr" c.host = "invalid.addr"
ctx := context.Background() ctx := context.Background()
if err := c.DialWithContext(ctx); err == nil { if err := c.DialWithContext(ctx); err == nil {
@ -730,7 +730,7 @@ func TestClient_DialWithContextInvalidHELO(t *testing.T) {
if err != nil { if err != nil {
t.Skipf("failed to create test client: %s. Skipping tests", err) t.Skipf("failed to create test client: %s. Skipping tests", err)
} }
c.connection = nil c.co = nil
c.helo = "" c.helo = ""
ctx := context.Background() ctx := context.Background()
if err := c.DialWithContext(ctx); err == nil { if err := c.DialWithContext(ctx); err == nil {
@ -762,7 +762,7 @@ func TestClient_checkConn(t *testing.T) {
if err != nil { if err != nil {
t.Skipf("failed to create test client: %s. Skipping tests", err) t.Skipf("failed to create test client: %s. Skipping tests", err)
} }
c.connection = nil c.co = nil
if err := c.checkConn(); err == nil { if err := c.checkConn(); err == nil {
t.Errorf("connCheck() should fail but succeeded") t.Errorf("connCheck() should fail but succeeded")
} }
@ -799,10 +799,10 @@ func TestClient_DialWithContextOptions(t *testing.T) {
return return
} }
if !tt.sf { if !tt.sf {
if c.connection == nil && !tt.sf { if c.co == nil && !tt.sf {
t.Errorf("DialWithContext didn't fail but no connection found.") t.Errorf("DialWithContext didn't fail but no connection found.")
} }
if c.smtpClient == nil && !tt.sf { if c.sc == nil && !tt.sf {
t.Errorf("DialWithContext didn't fail but no SMTP client found.") t.Errorf("DialWithContext didn't fail but no SMTP client found.")
} }
if err := c.Reset(); err != nil { if err := c.Reset(); err != nil {
@ -1002,16 +1002,16 @@ func TestClient_DialSendCloseBroken(t *testing.T) {
return return
} }
if tt.closestart { if tt.closestart {
_ = c.smtpClient.Close() _ = c.sc.Close()
_ = c.connection.Close() _ = c.co.Close()
} }
if err := c.Send(m); err != nil && !tt.sf { if err := c.Send(m); err != nil && !tt.sf {
t.Errorf("Send() failed: %s", err) t.Errorf("Send() failed: %s", err)
return return
} }
if tt.closeearly { if tt.closeearly {
_ = c.smtpClient.Close() _ = c.sc.Close()
_ = c.connection.Close() _ = c.co.Close()
} }
if err := c.Close(); err != nil && !tt.sf { if err := c.Close(); err != nil && !tt.sf {
t.Errorf("Close() failed: %s", err) t.Errorf("Close() failed: %s", err)
@ -1062,16 +1062,16 @@ func TestClient_DialSendCloseBrokenWithDSN(t *testing.T) {
return return
} }
if tt.closestart { if tt.closestart {
_ = c.smtpClient.Close() _ = c.sc.Close()
_ = c.connection.Close() _ = c.co.Close()
} }
if err := c.Send(m); err != nil && !tt.sf { if err := c.Send(m); err != nil && !tt.sf {
t.Errorf("Send() failed: %s", err) t.Errorf("Send() failed: %s", err)
return return
} }
if tt.closeearly { if tt.closeearly {
_ = c.smtpClient.Close() _ = c.sc.Close()
_ = c.connection.Close() _ = c.co.Close()
} }
if err := c.Close(); err != nil && !tt.sf { if err := c.Close(); err != nil && !tt.sf {
t.Errorf("Close() failed: %s", err) t.Errorf("Close() failed: %s", err)

26
file.go
View file

@ -23,17 +23,17 @@ type File struct {
} }
// WithFileName sets the filename of the File // WithFileName sets the filename of the File
func WithFileName(name string) FileOption { func WithFileName(n string) FileOption {
return func(f *File) { return func(f *File) {
f.Name = name f.Name = n
} }
} }
// WithFileDescription sets an optional file description of the File that will be // WithFileDescription sets an optional file description of the File that will be
// added as Content-Description part // added as Content-Description part
func WithFileDescription(description string) FileOption { func WithFileDescription(d string) FileOption {
return func(f *File) { return func(f *File) {
f.Desc = description f.Desc = d
} }
} }
@ -41,12 +41,12 @@ func WithFileDescription(description string) FileOption {
// Base64 encoding but there might be exceptions, where this might come handy. // Base64 encoding but there might be exceptions, where this might come handy.
// Please note that quoted-printable should never be used for attachments/embeds. If this // Please note that quoted-printable should never be used for attachments/embeds. If this
// is provided as argument, the function will automatically override back to Base64 // is provided as argument, the function will automatically override back to Base64
func WithFileEncoding(encoding Encoding) FileOption { func WithFileEncoding(e Encoding) FileOption {
return func(f *File) { return func(f *File) {
if encoding == EncodingQP { if e == EncodingQP {
return return
} }
f.Enc = encoding f.Enc = e
} }
} }
@ -56,19 +56,19 @@ func WithFileEncoding(encoding Encoding) FileOption {
// could not be guessed. In some cases, however, it might be needed to force // could not be guessed. In some cases, however, it might be needed to force
// this to a specific type. For such situations this override method can // this to a specific type. For such situations this override method can
// be used // be used
func WithFileContentType(contentType ContentType) FileOption { func WithFileContentType(t ContentType) FileOption {
return func(f *File) { return func(f *File) {
f.ContentType = contentType f.ContentType = t
} }
} }
// setHeader sets header fields to a File // setHeader sets header fields to a File
func (f *File) setHeader(header Header, value string) { func (f *File) setHeader(h Header, v string) {
f.Header.Set(string(header), value) f.Header.Set(string(h), v)
} }
// getHeader return header fields of a File // getHeader return header fields of a File
func (f *File) getHeader(header Header) (string, bool) { func (f *File) getHeader(h Header) (string, bool) {
v := f.Header.Get(string(header)) v := f.Header.Get(string(h))
return v, v != "" return v, v != ""
} }

View file

@ -15,68 +15,68 @@ import (
// JSONlog is the default structured JSON logger that satisfies the Logger interface // JSONlog is the default structured JSON logger that satisfies the Logger interface
type JSONlog struct { type JSONlog struct {
level Level l Level
log *slog.Logger log *slog.Logger
} }
// NewJSON returns a new JSONlog type that satisfies the Logger interface // NewJSON returns a new JSONlog type that satisfies the Logger interface
func NewJSON(output io.Writer, level Level) *JSONlog { func NewJSON(o io.Writer, l Level) *JSONlog {
logOpts := slog.HandlerOptions{} lo := slog.HandlerOptions{}
switch level { switch l {
case LevelDebug: case LevelDebug:
logOpts.Level = slog.LevelDebug lo.Level = slog.LevelDebug
case LevelInfo: case LevelInfo:
logOpts.Level = slog.LevelInfo lo.Level = slog.LevelInfo
case LevelWarn: case LevelWarn:
logOpts.Level = slog.LevelWarn lo.Level = slog.LevelWarn
case LevelError: case LevelError:
logOpts.Level = slog.LevelError lo.Level = slog.LevelError
default: default:
logOpts.Level = slog.LevelDebug lo.Level = slog.LevelDebug
} }
logHandler := slog.NewJSONHandler(output, &logOpts) lh := slog.NewJSONHandler(o, &lo)
return &JSONlog{ return &JSONlog{
level: level, l: l,
log: slog.New(logHandler), log: slog.New(lh),
} }
} }
// Debugf logs a debug message via the structured JSON logger // Debugf logs a debug message via the structured JSON logger
func (l *JSONlog) Debugf(log Log) { func (l *JSONlog) Debugf(lo Log) {
if l.level >= LevelDebug { if l.l >= LevelDebug {
l.log.WithGroup(DirString).With( l.log.WithGroup(DirString).With(
slog.String(DirFromString, log.directionFrom()), slog.String(DirFromString, lo.directionFrom()),
slog.String(DirToString, log.directionTo()), slog.String(DirToString, lo.directionTo()),
).Debug(fmt.Sprintf(log.Format, log.Messages...)) ).Debug(fmt.Sprintf(lo.Format, lo.Messages...))
} }
} }
// Infof logs a info message via the structured JSON logger // Infof logs a info message via the structured JSON logger
func (l *JSONlog) Infof(log Log) { func (l *JSONlog) Infof(lo Log) {
if l.level >= LevelInfo { if l.l >= LevelInfo {
l.log.WithGroup(DirString).With( l.log.WithGroup(DirString).With(
slog.String(DirFromString, log.directionFrom()), slog.String(DirFromString, lo.directionFrom()),
slog.String(DirToString, log.directionTo()), slog.String(DirToString, lo.directionTo()),
).Info(fmt.Sprintf(log.Format, log.Messages...)) ).Info(fmt.Sprintf(lo.Format, lo.Messages...))
} }
} }
// Warnf logs a warn message via the structured JSON logger // Warnf logs a warn message via the structured JSON logger
func (l *JSONlog) Warnf(log Log) { func (l *JSONlog) Warnf(lo Log) {
if l.level >= LevelWarn { if l.l >= LevelWarn {
l.log.WithGroup(DirString).With( l.log.WithGroup(DirString).With(
slog.String(DirFromString, log.directionFrom()), slog.String(DirFromString, lo.directionFrom()),
slog.String(DirToString, log.directionTo()), slog.String(DirToString, lo.directionTo()),
).Warn(fmt.Sprintf(log.Format, log.Messages...)) ).Warn(fmt.Sprintf(lo.Format, lo.Messages...))
} }
} }
// Errorf logs a warn message via the structured JSON logger // Errorf logs a warn message via the structured JSON logger
func (l *JSONlog) Errorf(log Log) { func (l *JSONlog) Errorf(lo Log) {
if l.level >= LevelError { if l.l >= LevelError {
l.log.WithGroup(DirString).With( l.log.WithGroup(DirString).With(
slog.String(DirFromString, log.directionFrom()), slog.String(DirFromString, lo.directionFrom()),
slog.String(DirToString, log.directionTo()), slog.String(DirToString, lo.directionTo()),
).Error(fmt.Sprintf(log.Format, log.Messages...)) ).Error(fmt.Sprintf(lo.Format, lo.Messages...))
} }
} }

View file

@ -30,8 +30,8 @@ type jsonDir struct {
func TestNewJSON(t *testing.T) { func TestNewJSON(t *testing.T) {
var b bytes.Buffer var b bytes.Buffer
l := NewJSON(&b, LevelDebug) l := NewJSON(&b, LevelDebug)
if l.level != LevelDebug { if l.l != LevelDebug {
t.Error("Expected level to be LevelDebug, got ", l.level) t.Error("Expected level to be LevelDebug, got ", l.l)
} }
if l.log == nil { if l.log == nil {
t.Error("logger not initialized") t.Error("logger not initialized")
@ -81,7 +81,7 @@ func TestJSONDebugf(t *testing.T) {
} }
b.Reset() b.Reset()
l.level = LevelInfo l.l = LevelInfo
l.Debugf(Log{Direction: DirServerToClient, Format: "test %s", Messages: []interface{}{"foo"}}) l.Debugf(Log{Direction: DirServerToClient, Format: "test %s", Messages: []interface{}{"foo"}})
if b.String() != "" { if b.String() != "" {
t.Error("Debug message was not expected to be logged") t.Error("Debug message was not expected to be logged")
@ -131,7 +131,7 @@ func TestJSONDebugf_WithDefault(t *testing.T) {
} }
b.Reset() b.Reset()
l.level = LevelInfo l.l = LevelInfo
l.Debugf(Log{Direction: DirServerToClient, Format: "test %s", Messages: []interface{}{"foo"}}) l.Debugf(Log{Direction: DirServerToClient, Format: "test %s", Messages: []interface{}{"foo"}})
if b.String() != "" { if b.String() != "" {
t.Error("Debug message was not expected to be logged") t.Error("Debug message was not expected to be logged")
@ -181,7 +181,7 @@ func TestJSONInfof(t *testing.T) {
} }
b.Reset() b.Reset()
l.level = LevelWarn l.l = LevelWarn
l.Infof(Log{Direction: DirServerToClient, Format: "test %s", Messages: []interface{}{"foo"}}) l.Infof(Log{Direction: DirServerToClient, Format: "test %s", Messages: []interface{}{"foo"}})
if b.String() != "" { if b.String() != "" {
t.Error("Info message was not expected to be logged") t.Error("Info message was not expected to be logged")
@ -231,7 +231,7 @@ func TestJSONWarnf(t *testing.T) {
} }
b.Reset() b.Reset()
l.level = LevelError l.l = LevelError
l.Warnf(Log{Direction: DirServerToClient, Format: "test %s", Messages: []interface{}{"foo"}}) l.Warnf(Log{Direction: DirServerToClient, Format: "test %s", Messages: []interface{}{"foo"}})
if b.String() != "" { if b.String() != "" {
t.Error("Warn message was not expected to be logged") t.Error("Warn message was not expected to be logged")
@ -281,7 +281,7 @@ func TestJSONErrorf(t *testing.T) {
} }
b.Reset() b.Reset()
l.level = -99 l.l = -99
l.Errorf(Log{Direction: DirServerToClient, Format: "test %s", Messages: []interface{}{"foo"}}) l.Errorf(Log{Direction: DirServerToClient, Format: "test %s", Messages: []interface{}{"foo"}})
if b.String() != "" { if b.String() != "" {
t.Error("Error message was not expected to be logged") t.Error("Error message was not expected to be logged")

View file

@ -12,7 +12,7 @@ import (
// Stdlog is the default logger that satisfies the Logger interface // Stdlog is the default logger that satisfies the Logger interface
type Stdlog struct { type Stdlog struct {
level Level l Level
err *log.Logger err *log.Logger
warn *log.Logger warn *log.Logger
info *log.Logger info *log.Logger
@ -24,45 +24,45 @@ type Stdlog struct {
const CallDepth = 2 const CallDepth = 2
// New returns a new Stdlog type that satisfies the Logger interface // New returns a new Stdlog type that satisfies the Logger interface
func New(output io.Writer, level Level) *Stdlog { func New(o io.Writer, l Level) *Stdlog {
lf := log.Lmsgprefix | log.LstdFlags lf := log.Lmsgprefix | log.LstdFlags
return &Stdlog{ return &Stdlog{
level: level, l: l,
err: log.New(output, "ERROR: ", lf), err: log.New(o, "ERROR: ", lf),
warn: log.New(output, " WARN: ", lf), warn: log.New(o, " WARN: ", lf),
info: log.New(output, " INFO: ", lf), info: log.New(o, " INFO: ", lf),
debug: log.New(output, "DEBUG: ", lf), debug: log.New(o, "DEBUG: ", lf),
} }
} }
// Debugf performs a Printf() on the debug logger // Debugf performs a Printf() on the debug logger
func (l *Stdlog) Debugf(log Log) { func (l *Stdlog) Debugf(lo Log) {
if l.level >= LevelDebug { if l.l >= LevelDebug {
format := fmt.Sprintf("%s %s", log.directionPrefix(), log.Format) f := fmt.Sprintf("%s %s", lo.directionPrefix(), lo.Format)
_ = l.debug.Output(CallDepth, fmt.Sprintf(format, log.Messages...)) _ = l.debug.Output(CallDepth, fmt.Sprintf(f, lo.Messages...))
} }
} }
// Infof performs a Printf() on the info logger // Infof performs a Printf() on the info logger
func (l *Stdlog) Infof(log Log) { func (l *Stdlog) Infof(lo Log) {
if l.level >= LevelInfo { if l.l >= LevelInfo {
format := fmt.Sprintf("%s %s", log.directionPrefix(), log.Format) f := fmt.Sprintf("%s %s", lo.directionPrefix(), lo.Format)
_ = l.info.Output(CallDepth, fmt.Sprintf(format, log.Messages...)) _ = l.info.Output(CallDepth, fmt.Sprintf(f, lo.Messages...))
} }
} }
// Warnf performs a Printf() on the warn logger // Warnf performs a Printf() on the warn logger
func (l *Stdlog) Warnf(log Log) { func (l *Stdlog) Warnf(lo Log) {
if l.level >= LevelWarn { if l.l >= LevelWarn {
format := fmt.Sprintf("%s %s", log.directionPrefix(), log.Format) f := fmt.Sprintf("%s %s", lo.directionPrefix(), lo.Format)
_ = l.warn.Output(CallDepth, fmt.Sprintf(format, log.Messages...)) _ = l.warn.Output(CallDepth, fmt.Sprintf(f, lo.Messages...))
} }
} }
// Errorf performs a Printf() on the error logger // Errorf performs a Printf() on the error logger
func (l *Stdlog) Errorf(log Log) { func (l *Stdlog) Errorf(lo Log) {
if l.level >= LevelError { if l.l >= LevelError {
format := fmt.Sprintf("%s %s", log.directionPrefix(), log.Format) f := fmt.Sprintf("%s %s", lo.directionPrefix(), lo.Format)
_ = l.err.Output(CallDepth, fmt.Sprintf(format, log.Messages...)) _ = l.err.Output(CallDepth, fmt.Sprintf(f, lo.Messages...))
} }
} }

View file

@ -13,8 +13,8 @@ import (
func TestNew(t *testing.T) { func TestNew(t *testing.T) {
var b bytes.Buffer var b bytes.Buffer
l := New(&b, LevelDebug) l := New(&b, LevelDebug)
if l.level != LevelDebug { if l.l != LevelDebug {
t.Error("Expected level to be LevelDebug, got ", l.level) t.Error("Expected level to be LevelDebug, got ", l.l)
} }
if l.err == nil || l.warn == nil || l.info == nil || l.debug == nil { if l.err == nil || l.warn == nil || l.info == nil || l.debug == nil {
t.Error("Loggers not initialized") t.Error("Loggers not initialized")
@ -37,7 +37,7 @@ func TestDebugf(t *testing.T) {
} }
b.Reset() b.Reset()
l.level = LevelInfo l.l = LevelInfo
l.Debugf(Log{Direction: DirServerToClient, Format: "test %s", Messages: []interface{}{"foo"}}) l.Debugf(Log{Direction: DirServerToClient, Format: "test %s", Messages: []interface{}{"foo"}})
if b.String() != "" { if b.String() != "" {
t.Error("Debug message was not expected to be logged") t.Error("Debug message was not expected to be logged")
@ -60,7 +60,7 @@ func TestInfof(t *testing.T) {
} }
b.Reset() b.Reset()
l.level = LevelWarn l.l = LevelWarn
l.Infof(Log{Direction: DirServerToClient, Format: "test %s", Messages: []interface{}{"foo"}}) l.Infof(Log{Direction: DirServerToClient, Format: "test %s", Messages: []interface{}{"foo"}})
if b.String() != "" { if b.String() != "" {
t.Error("Info message was not expected to be logged") t.Error("Info message was not expected to be logged")
@ -83,7 +83,7 @@ func TestWarnf(t *testing.T) {
} }
b.Reset() b.Reset()
l.level = LevelError l.l = LevelError
l.Warnf(Log{Direction: DirServerToClient, Format: "test %s", Messages: []interface{}{"foo"}}) l.Warnf(Log{Direction: DirServerToClient, Format: "test %s", Messages: []interface{}{"foo"}})
if b.String() != "" { if b.String() != "" {
t.Error("Warn message was not expected to be logged") t.Error("Warn message was not expected to be logged")
@ -106,7 +106,7 @@ func TestErrorf(t *testing.T) {
} }
b.Reset() b.Reset()
l.level = LevelError - 1 l.l = LevelError - 1
l.Errorf(Log{Direction: DirServerToClient, Format: "test %s", Messages: []interface{}{"foo"}}) l.Errorf(Log{Direction: DirServerToClient, Format: "test %s", Messages: []interface{}{"foo"}})
if b.String() != "" { if b.String() != "" {
t.Error("Error message was not expected to be logged") t.Error("Error message was not expected to be logged")

764
msg.go

File diff suppressed because it is too large Load diff

View file

@ -1251,7 +1251,7 @@ func TestMsg_SetBodyString(t *testing.T) {
} }
part := m.parts[0] part := m.parts[0]
res := bytes.Buffer{} res := bytes.Buffer{}
if _, err := part.writeFunc(&res); err != nil && !tt.sf { if _, err := part.w(&res); err != nil && !tt.sf {
t.Errorf("WriteFunc of part failed: %s", err) t.Errorf("WriteFunc of part failed: %s", err)
} }
if res.String() != tt.want { if res.String() != tt.want {
@ -1286,7 +1286,7 @@ func TestMsg_AddAlternativeString(t *testing.T) {
} }
apart := m.parts[1] apart := m.parts[1]
res := bytes.Buffer{} res := bytes.Buffer{}
if _, err := apart.writeFunc(&res); err != nil && !tt.sf { if _, err := apart.w(&res); err != nil && !tt.sf {
t.Errorf("WriteFunc of part failed: %s", err) t.Errorf("WriteFunc of part failed: %s", err)
} }
if res.String() != tt.want { if res.String() != tt.want {
@ -3161,89 +3161,3 @@ func TestMsg_BccFromString(t *testing.T) {
}) })
} }
} }
// TestMsg_checkUserAgent tests the checkUserAgent method of the Msg
func TestMsg_checkUserAgent(t *testing.T) {
tests := []struct {
name string
noDefaultUserAgent bool
genHeader map[Header][]string
wantUserAgent string
sf bool
}{
{
name: "check default user agent",
noDefaultUserAgent: false,
wantUserAgent: fmt.Sprintf("go-mail v%s // https://github.com/wneessen/go-mail", VERSION),
sf: false,
},
{
name: "check no default user agent",
noDefaultUserAgent: true,
wantUserAgent: "",
sf: true,
},
{
name: "check if ua and xm is already set",
noDefaultUserAgent: false,
genHeader: map[Header][]string{
HeaderUserAgent: {"custom UA"},
HeaderXMailer: {"custom XM"},
},
wantUserAgent: "custom UA",
sf: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
msg := &Msg{
noDefaultUserAgent: tt.noDefaultUserAgent,
genHeader: tt.genHeader,
}
msg.checkUserAgent()
gotUserAgent := ""
if val, ok := msg.genHeader[HeaderUserAgent]; ok {
gotUserAgent = val[0] // Assuming the first one is the needed value
}
if gotUserAgent != tt.wantUserAgent && !tt.sf {
t.Errorf("UserAgent got = %v, want = %v", gotUserAgent, tt.wantUserAgent)
}
})
}
}
// TestNewMsgWithMIMEVersion tests WithMIMEVersion and Msg.SetMIMEVersion
func TestNewMsgWithNoDefaultUserAgent(t *testing.T) {
m := NewMsg(WithNoDefaultUserAgent())
if m.noDefaultUserAgent != true {
t.Errorf("WithNoDefaultUserAgent() failed. Expected: %t, got: %t", true, false)
}
}
// Fuzzing tests
func FuzzMsg_Subject(f *testing.F) {
f.Add("Testsubject")
f.Add("")
f.Add("This is a longer test subject.")
f.Add("Let's add some umlauts: üäöß")
f.Add("Or even emojis: ☝️💪👍")
f.Fuzz(func(t *testing.T, data string) {
m := NewMsg()
m.Subject(data)
m.Reset()
})
}
func FuzzMsg_From(f *testing.F) {
f.Add("Toni Tester <toni@tester.com>")
f.Add("<tester@example.com>")
f.Add("mail@server.com")
f.Fuzz(func(t *testing.T, data string) {
m := NewMsg()
if err := m.From(data); err != nil &&
!strings.Contains(err.Error(), "failed to parse mail address") {
t.Errorf("failed set set FROM address: %s", err)
}
m.Reset()
})
}

View file

@ -35,241 +35,237 @@ const DoubleNewLine = "\r\n\r\n"
// msgWriter handles the I/O to the io.WriteCloser of the SMTP client // msgWriter handles the I/O to the io.WriteCloser of the SMTP client
type msgWriter struct { type msgWriter struct {
bytesWritten int64 c Charset
charset Charset d int8
depth int8 en mime.WordEncoder
encoder mime.WordEncoder
err error err error
multiPartWriter [3]*multipart.Writer mpw [3]*multipart.Writer
partWriter io.Writer n int64
writer io.Writer pw io.Writer
w io.Writer
} }
// Write implements the io.Writer interface for msgWriter // Write implements the io.Writer interface for msgWriter
func (mw *msgWriter) Write(payload []byte) (int, error) { func (mw *msgWriter) Write(p []byte) (int, error) {
if mw.err != nil { if mw.err != nil {
return 0, fmt.Errorf("failed to write due to previous error: %w", mw.err) return 0, fmt.Errorf("failed to write due to previous error: %w", mw.err)
} }
var n int var n int
n, mw.err = mw.writer.Write(payload) n, mw.err = mw.w.Write(p)
mw.bytesWritten += int64(n) mw.n += int64(n)
return n, mw.err return n, mw.err
} }
// writeMsg formats the message and sends it to its io.Writer // writeMsg formats the message and sends it to its io.Writer
func (mw *msgWriter) writeMsg(msg *Msg) { func (mw *msgWriter) writeMsg(m *Msg) {
msg.addDefaultHeader() m.addDefaultHeader()
msg.checkUserAgent() m.checkUserAgent()
mw.writeGenHeader(msg) mw.writeGenHeader(m)
mw.writePreformattedGenHeader(msg) mw.writePreformattedGenHeader(m)
// Set the FROM header (or envelope FROM if FROM is empty) // Set the FROM header (or envelope FROM if FROM is empty)
hasFrom := true hf := true
from, ok := msg.addrHeader[HeaderFrom] f, ok := m.addrHeader[HeaderFrom]
if !ok || (len(from) == 0 || from == nil) { if !ok || (len(f) == 0 || f == nil) {
from, ok = msg.addrHeader[HeaderEnvelopeFrom] f, ok = m.addrHeader[HeaderEnvelopeFrom]
if !ok || (len(from) == 0 || from == nil) { if !ok || (len(f) == 0 || f == nil) {
hasFrom = false hf = false
} }
} }
if hasFrom && (len(from) > 0 && from[0] != nil) { if hf && (len(f) > 0 && f[0] != nil) {
mw.writeHeader(Header(HeaderFrom), from[0].String()) mw.writeHeader(Header(HeaderFrom), f[0].String())
} }
// Set the rest of the address headers // Set the rest of the address headers
for _, to := range []AddrHeader{HeaderTo, HeaderCc} { for _, t := range []AddrHeader{HeaderTo, HeaderCc} {
if addresses, ok := msg.addrHeader[to]; ok { if al, ok := m.addrHeader[t]; ok {
var val []string var v []string
for _, addr := range addresses { for _, a := range al {
val = append(val, addr.String()) v = append(v, a.String())
} }
mw.writeHeader(Header(to), val...) mw.writeHeader(Header(t), v...)
} }
} }
if msg.hasMixed() { if m.hasMixed() {
mw.startMP(MIMEMixed, msg.boundary) mw.startMP(MIMEMixed, m.boundary)
mw.writeString(DoubleNewLine) mw.writeString(DoubleNewLine)
} }
if msg.hasRelated() { if m.hasRelated() {
mw.startMP(MIMERelated, msg.boundary) mw.startMP(MIMERelated, m.boundary)
mw.writeString(DoubleNewLine) mw.writeString(DoubleNewLine)
} }
if msg.hasAlt() { if m.hasAlt() {
mw.startMP(MIMEAlternative, msg.boundary) mw.startMP(MIMEAlternative, m.boundary)
mw.writeString(DoubleNewLine) mw.writeString(DoubleNewLine)
} }
if msg.hasPGPType() { if m.hasPGPType() {
switch msg.pgptype { switch m.pgptype {
case PGPEncrypt: case PGPEncrypt:
mw.startMP(`encrypted; protocol="application/pgp-encrypted"`, mw.startMP(`encrypted; protocol="application/pgp-encrypted"`, m.boundary)
msg.boundary)
case PGPSignature: case PGPSignature:
mw.startMP(`signed; protocol="application/pgp-signature";`, mw.startMP(`signed; protocol="application/pgp-signature";`, m.boundary)
msg.boundary)
default:
} }
mw.writeString(DoubleNewLine) mw.writeString(DoubleNewLine)
} }
for _, part := range msg.parts { for _, p := range m.parts {
if !part.isDeleted { if !p.del {
mw.writePart(part, msg.charset) mw.writePart(p, m.charset)
} }
} }
if msg.hasAlt() { if m.hasAlt() {
mw.stopMP() mw.stopMP()
} }
// Add embeds // Add embeds
mw.addFiles(msg.embeds, false) mw.addFiles(m.embeds, false)
if msg.hasRelated() { if m.hasRelated() {
mw.stopMP() mw.stopMP()
} }
// Add attachments // Add attachments
mw.addFiles(msg.attachments, true) mw.addFiles(m.attachments, true)
if msg.hasMixed() { if m.hasMixed() {
mw.stopMP() mw.stopMP()
} }
} }
// writeGenHeader writes out all generic headers to the msgWriter // writeGenHeader writes out all generic headers to the msgWriter
func (mw *msgWriter) writeGenHeader(msg *Msg) { func (mw *msgWriter) writeGenHeader(m *Msg) {
keys := make([]string, 0, len(msg.genHeader)) gk := make([]string, 0, len(m.genHeader))
for key := range msg.genHeader { for k := range m.genHeader {
keys = append(keys, string(key)) gk = append(gk, string(k))
} }
sort.Strings(keys) sort.Strings(gk)
for _, key := range keys { for _, k := range gk {
mw.writeHeader(Header(key), msg.genHeader[Header(key)]...) mw.writeHeader(Header(k), m.genHeader[Header(k)]...)
} }
} }
// writePreformatedHeader writes out all preformated generic headers to the msgWriter // writePreformatedHeader writes out all preformated generic headers to the msgWriter
func (mw *msgWriter) writePreformattedGenHeader(msg *Msg) { func (mw *msgWriter) writePreformattedGenHeader(m *Msg) {
for key, val := range msg.preformHeader { for k, v := range m.preformHeader {
mw.writeString(fmt.Sprintf("%s: %s%s", key, val, SingleNewLine)) mw.writeString(fmt.Sprintf("%s: %s%s", k, v, SingleNewLine))
} }
} }
// startMP writes a multipart beginning // startMP writes a multipart beginning
func (mw *msgWriter) startMP(mimeType MIMEType, boundary string) { func (mw *msgWriter) startMP(mt MIMEType, b string) {
multiPartWriter := multipart.NewWriter(mw) mp := multipart.NewWriter(mw)
if boundary != "" { if b != "" {
mw.err = multiPartWriter.SetBoundary(boundary) mw.err = mp.SetBoundary(b)
} }
contentType := fmt.Sprintf("multipart/%s;\r\n boundary=%s", mimeType, ct := fmt.Sprintf("multipart/%s;\r\n boundary=%s", mt, mp.Boundary())
multiPartWriter.Boundary()) mw.mpw[mw.d] = mp
mw.multiPartWriter[mw.depth] = multiPartWriter
if mw.depth == 0 { if mw.d == 0 {
mw.writeString(fmt.Sprintf("%s: %s", HeaderContentType, contentType)) mw.writeString(fmt.Sprintf("%s: %s", HeaderContentType, ct))
} }
if mw.depth > 0 { if mw.d > 0 {
mw.newPart(map[string][]string{"Content-Type": {contentType}}) mw.newPart(map[string][]string{"Content-Type": {ct}})
} }
mw.depth++ mw.d++
} }
// stopMP closes the multipart // stopMP closes the multipart
func (mw *msgWriter) stopMP() { func (mw *msgWriter) stopMP() {
if mw.depth > 0 { if mw.d > 0 {
mw.err = mw.multiPartWriter[mw.depth-1].Close() mw.err = mw.mpw[mw.d-1].Close()
mw.depth-- mw.d--
} }
} }
// addFiles adds the attachments/embeds file content to the mail body // addFiles adds the attachments/embeds file content to the mail body
func (mw *msgWriter) addFiles(files []*File, isAttachment bool) { func (mw *msgWriter) addFiles(fl []*File, a bool) {
for _, file := range files { for _, f := range fl {
encoding := EncodingB64 e := EncodingB64
if _, ok := file.getHeader(HeaderContentType); !ok { if _, ok := f.getHeader(HeaderContentType); !ok {
mimeType := mime.TypeByExtension(filepath.Ext(file.Name)) mt := mime.TypeByExtension(filepath.Ext(f.Name))
if mimeType == "" { if mt == "" {
mimeType = "application/octet-stream" mt = "application/octet-stream"
} }
if file.ContentType != "" { if f.ContentType != "" {
mimeType = string(file.ContentType) mt = string(f.ContentType)
} }
file.setHeader(HeaderContentType, fmt.Sprintf(`%s; name="%s"`, mimeType, f.setHeader(HeaderContentType, fmt.Sprintf(`%s; name="%s"`, mt,
mw.encoder.Encode(mw.charset.String(), file.Name))) mw.en.Encode(mw.c.String(), f.Name)))
} }
if _, ok := file.getHeader(HeaderContentTransferEnc); !ok { if _, ok := f.getHeader(HeaderContentTransferEnc); !ok {
if file.Enc != "" { if f.Enc != "" {
encoding = file.Enc e = f.Enc
} }
file.setHeader(HeaderContentTransferEnc, string(encoding)) f.setHeader(HeaderContentTransferEnc, string(e))
} }
if file.Desc != "" { if f.Desc != "" {
if _, ok := file.getHeader(HeaderContentDescription); !ok { if _, ok := f.getHeader(HeaderContentDescription); !ok {
file.setHeader(HeaderContentDescription, file.Desc) f.setHeader(HeaderContentDescription, f.Desc)
} }
} }
if _, ok := file.getHeader(HeaderContentDisposition); !ok { if _, ok := f.getHeader(HeaderContentDisposition); !ok {
disposition := "inline" d := "inline"
if isAttachment { if a {
disposition = "attachment" d = "attachment"
} }
file.setHeader(HeaderContentDisposition, fmt.Sprintf(`%s; filename="%s"`, f.setHeader(HeaderContentDisposition, fmt.Sprintf(`%s; filename="%s"`, d,
disposition, mw.encoder.Encode(mw.charset.String(), file.Name))) mw.en.Encode(mw.c.String(), f.Name)))
} }
if !isAttachment { if !a {
if _, ok := file.getHeader(HeaderContentID); !ok { if _, ok := f.getHeader(HeaderContentID); !ok {
file.setHeader(HeaderContentID, fmt.Sprintf("<%s>", file.Name)) f.setHeader(HeaderContentID, fmt.Sprintf("<%s>", f.Name))
} }
} }
if mw.depth == 0 { if mw.d == 0 {
for header, val := range file.Header { for h, v := range f.Header {
mw.writeHeader(Header(header), val...) mw.writeHeader(Header(h), v...)
} }
mw.writeString(SingleNewLine) mw.writeString(SingleNewLine)
} }
if mw.depth > 0 { if mw.d > 0 {
mw.newPart(file.Header) mw.newPart(f.Header)
} }
if mw.err == nil { if mw.err == nil {
mw.writeBody(file.Writer, encoding) mw.writeBody(f.Writer, e)
} }
} }
} }
// newPart creates a new MIME multipart io.Writer and sets the partwriter to it // newPart creates a new MIME multipart io.Writer and sets the partwriter to it
func (mw *msgWriter) newPart(header map[string][]string) { func (mw *msgWriter) newPart(h map[string][]string) {
mw.partWriter, mw.err = mw.multiPartWriter[mw.depth-1].CreatePart(header) mw.pw, mw.err = mw.mpw[mw.d-1].CreatePart(h)
} }
// writePart writes the corresponding part to the Msg body // writePart writes the corresponding part to the Msg body
func (mw *msgWriter) writePart(part *Part, charset Charset) { func (mw *msgWriter) writePart(p *Part, cs Charset) {
partCharset := part.charset pcs := p.cset
if partCharset.String() == "" { if pcs.String() == "" {
partCharset = charset pcs = cs
} }
contentType := fmt.Sprintf("%s; charset=%s", part.contentType, partCharset) ct := fmt.Sprintf("%s; charset=%s", p.ctype, pcs)
contentTransferEnc := part.encoding.String() cte := p.enc.String()
if mw.depth == 0 { if mw.d == 0 {
mw.writeHeader(HeaderContentType, contentType) mw.writeHeader(HeaderContentType, ct)
mw.writeHeader(HeaderContentTransferEnc, contentTransferEnc) mw.writeHeader(HeaderContentTransferEnc, cte)
mw.writeString(SingleNewLine) mw.writeString(SingleNewLine)
} }
if mw.depth > 0 { if mw.d > 0 {
mimeHeader := textproto.MIMEHeader{} mh := textproto.MIMEHeader{}
if part.description != "" { if p.desc != "" {
mimeHeader.Add(string(HeaderContentDescription), part.description) mh.Add(string(HeaderContentDescription), p.desc)
} }
mimeHeader.Add(string(HeaderContentType), contentType) mh.Add(string(HeaderContentType), ct)
mimeHeader.Add(string(HeaderContentTransferEnc), contentTransferEnc) mh.Add(string(HeaderContentTransferEnc), cte)
mw.newPart(mimeHeader) mw.newPart(mh)
} }
mw.writeBody(part.writeFunc, part.encoding) mw.writeBody(p.w, p.enc)
} }
// writeString writes a string into the msgWriter's io.Writer interface // writeString writes a string into the msgWriter's io.Writer interface
@ -278,103 +274,102 @@ func (mw *msgWriter) writeString(s string) {
return return
} }
var n int var n int
n, mw.err = io.WriteString(mw.writer, s) n, mw.err = io.WriteString(mw.w, s)
mw.bytesWritten += int64(n) mw.n += int64(n)
} }
// writeHeader writes a header into the msgWriter's io.Writer // writeHeader writes a header into the msgWriter's io.Writer
func (mw *msgWriter) writeHeader(key Header, values ...string) { func (mw *msgWriter) writeHeader(k Header, vl ...string) {
buffer := strings.Builder{} wbuf := bytes.Buffer{}
charLength := MaxHeaderLength - 2 cl := MaxHeaderLength - 2
buffer.WriteString(string(key)) wbuf.WriteString(string(k))
charLength -= len(key) cl -= len(k)
if len(values) == 0 { if len(vl) == 0 {
buffer.WriteString(":\r\n") wbuf.WriteString(":\r\n")
return return
} }
buffer.WriteString(": ") wbuf.WriteString(": ")
charLength -= 2 cl -= 2
fullValueStr := strings.Join(values, ", ") fs := strings.Join(vl, ", ")
words := strings.Split(fullValueStr, " ") sfs := strings.Split(fs, " ")
for i, val := range words { for i, v := range sfs {
if charLength-len(val) <= 1 { if cl-len(v) <= 1 {
buffer.WriteString(fmt.Sprintf("%s ", SingleNewLine)) wbuf.WriteString(fmt.Sprintf("%s ", SingleNewLine))
charLength = MaxHeaderLength - 3 cl = MaxHeaderLength - 3
} }
buffer.WriteString(val) wbuf.WriteString(v)
if i < len(words)-1 { if i < len(sfs)-1 {
buffer.WriteString(" ") wbuf.WriteString(" ")
charLength -= 1 cl -= 1
} }
charLength -= len(val) cl -= len(v)
} }
bufferString := buffer.String() bufs := wbuf.String()
bufferString = strings.ReplaceAll(bufferString, fmt.Sprintf(" %s", SingleNewLine), bufs = strings.ReplaceAll(bufs, fmt.Sprintf(" %s", SingleNewLine), SingleNewLine)
SingleNewLine) mw.writeString(bufs)
mw.writeString(bufferString)
mw.writeString("\r\n") mw.writeString("\r\n")
} }
// writeBody writes an io.Reader into an io.Writer using provided Encoding // writeBody writes an io.Reader into an io.Writer using provided Encoding
func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encoding Encoding) { func (mw *msgWriter) writeBody(f func(io.Writer) (int64, error), e Encoding) {
var writer io.Writer var w io.Writer
var encodedWriter io.WriteCloser var ew io.WriteCloser
var n int64 var n int64
var err error var err error
if mw.depth == 0 { if mw.d == 0 {
writer = mw.writer w = mw.w
} }
if mw.depth > 0 { if mw.d > 0 {
writer = mw.partWriter w = mw.pw
} }
writeBuffer := bytes.Buffer{} wbuf := bytes.Buffer{}
lineBreaker := Base64LineBreaker{} lb := Base64LineBreaker{}
lineBreaker.out = &writeBuffer lb.out = &wbuf
switch encoding { switch e {
case EncodingQP: case EncodingQP:
encodedWriter = quotedprintable.NewWriter(&writeBuffer) ew = quotedprintable.NewWriter(&wbuf)
case EncodingB64: case EncodingB64:
encodedWriter = base64.NewEncoder(base64.StdEncoding, &lineBreaker) ew = base64.NewEncoder(base64.StdEncoding, &lb)
case NoEncoding: case NoEncoding:
_, err = writeFunc(&writeBuffer) _, err = f(&wbuf)
if err != nil { if err != nil {
mw.err = fmt.Errorf("bodyWriter function: %w", err) mw.err = fmt.Errorf("bodyWriter function: %w", err)
} }
n, err = io.Copy(writer, &writeBuffer) n, err = io.Copy(w, &wbuf)
if err != nil && mw.err == nil { if err != nil && mw.err == nil {
mw.err = fmt.Errorf("bodyWriter io.Copy: %w", err) mw.err = fmt.Errorf("bodyWriter io.Copy: %w", err)
} }
if mw.depth == 0 { if mw.d == 0 {
mw.bytesWritten += n mw.n += n
} }
return return
default: default:
encodedWriter = quotedprintable.NewWriter(writer) ew = quotedprintable.NewWriter(w)
} }
_, err = writeFunc(encodedWriter) _, err = f(ew)
if err != nil { if err != nil {
mw.err = fmt.Errorf("bodyWriter function: %w", err) mw.err = fmt.Errorf("bodyWriter function: %w", err)
} }
err = encodedWriter.Close() err = ew.Close()
if err != nil && mw.err == nil { if err != nil && mw.err == nil {
mw.err = fmt.Errorf("bodyWriter close encoded writer: %w", err) mw.err = fmt.Errorf("bodyWriter close encoded writer: %w", err)
} }
err = lineBreaker.Close() err = lb.Close()
if err != nil && mw.err == nil { if err != nil && mw.err == nil {
mw.err = fmt.Errorf("bodyWriter close linebreaker: %w", err) mw.err = fmt.Errorf("bodyWriter close linebreaker: %w", err)
} }
n, err = io.Copy(writer, &writeBuffer) n, err = io.Copy(w, &wbuf)
if err != nil && mw.err == nil { if err != nil && mw.err == nil {
mw.err = fmt.Errorf("bodyWriter io.Copy: %w", err) mw.err = fmt.Errorf("bodyWriter io.Copy: %w", err)
} }
// Since the part writer uses the WriteTo() method, we don't need to add the // Since the part writer uses the WriteTo() method, we don't need to add the
// bytes twice // bytes twice
if mw.depth == 0 { if mw.d == 0 {
mw.bytesWritten += n mw.n += n
} }
} }

View file

@ -28,7 +28,7 @@ func (bw *brokenWriter) Write([]byte) (int, error) {
// TestMsgWriter_Write tests the WriteTo() method of the msgWriter // TestMsgWriter_Write tests the WriteTo() method of the msgWriter
func TestMsgWriter_Write(t *testing.T) { func TestMsgWriter_Write(t *testing.T) {
bw := &brokenWriter{} bw := &brokenWriter{}
mw := &msgWriter{writer: bw, charset: CharsetUTF8, encoder: mime.QEncoding} mw := &msgWriter{w: bw, c: CharsetUTF8, en: mime.QEncoding}
_, err := mw.Write([]byte("test")) _, err := mw.Write([]byte("test"))
if err == nil { if err == nil {
t.Errorf("msgWriter WriteTo() with brokenWriter should fail, but didn't") t.Errorf("msgWriter WriteTo() with brokenWriter should fail, but didn't")
@ -55,7 +55,7 @@ func TestMsgWriter_writeMsg(t *testing.T) {
m.SetBodyString(TypeTextPlain, "This is the body") m.SetBodyString(TypeTextPlain, "This is the body")
m.AddAlternativeString(TypeTextHTML, "This is the alternative body") m.AddAlternativeString(TypeTextHTML, "This is the alternative body")
buf := bytes.Buffer{} buf := bytes.Buffer{}
mw := &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding} mw := &msgWriter{w: &buf, c: CharsetUTF8, en: mime.QEncoding}
mw.writeMsg(m) mw.writeMsg(m)
ms := buf.String() ms := buf.String()
@ -134,7 +134,7 @@ func TestMsgWriter_writeMsg_PGP(t *testing.T) {
m.Subject("This is a subject") m.Subject("This is a subject")
m.SetBodyString(TypeTextPlain, "This is the body") m.SetBodyString(TypeTextPlain, "This is the body")
buf := bytes.Buffer{} buf := bytes.Buffer{}
mw := &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding} mw := &msgWriter{w: &buf, c: CharsetUTF8, en: mime.QEncoding}
mw.writeMsg(m) mw.writeMsg(m)
ms := buf.String() ms := buf.String()
if !strings.Contains(ms, `encrypted; protocol="application/pgp-encrypted"`) { if !strings.Contains(ms, `encrypted; protocol="application/pgp-encrypted"`) {
@ -147,7 +147,7 @@ func TestMsgWriter_writeMsg_PGP(t *testing.T) {
m.Subject("This is a subject") m.Subject("This is a subject")
m.SetBodyString(TypeTextPlain, "This is the body") m.SetBodyString(TypeTextPlain, "This is the body")
buf = bytes.Buffer{} buf = bytes.Buffer{}
mw = &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding} mw = &msgWriter{w: &buf, c: CharsetUTF8, en: mime.QEncoding}
mw.writeMsg(m) mw.writeMsg(m)
ms = buf.String() ms = buf.String()
if !strings.Contains(ms, `signed; protocol="application/pgp-signature"`) { if !strings.Contains(ms, `signed; protocol="application/pgp-signature"`) {

66
part.go
View file

@ -14,18 +14,18 @@ type PartOption func(*Part)
// Part is a part of the Msg // Part is a part of the Msg
type Part struct { type Part struct {
contentType ContentType ctype ContentType
charset Charset cset Charset
description string desc string
encoding Encoding enc Encoding
isDeleted bool del bool
writeFunc func(io.Writer) (int64, error) w func(io.Writer) (int64, error)
} }
// GetContent executes the WriteFunc of the Part and returns the content as byte slice // GetContent executes the WriteFunc of the Part and returns the content as byte slice
func (p *Part) GetContent() ([]byte, error) { func (p *Part) GetContent() ([]byte, error) {
var b bytes.Buffer var b bytes.Buffer
if _, err := p.writeFunc(&b); err != nil { if _, err := p.w(&b); err != nil {
return nil, err return nil, err
} }
return b.Bytes(), nil return b.Bytes(), nil
@ -33,83 +33,83 @@ func (p *Part) GetContent() ([]byte, error) {
// GetCharset returns the currently set Charset of the Part // GetCharset returns the currently set Charset of the Part
func (p *Part) GetCharset() Charset { func (p *Part) GetCharset() Charset {
return p.charset return p.cset
} }
// GetContentType returns the currently set ContentType of the Part // GetContentType returns the currently set ContentType of the Part
func (p *Part) GetContentType() ContentType { func (p *Part) GetContentType() ContentType {
return p.contentType return p.ctype
} }
// GetEncoding returns the currently set Encoding of the Part // GetEncoding returns the currently set Encoding of the Part
func (p *Part) GetEncoding() Encoding { func (p *Part) GetEncoding() Encoding {
return p.encoding return p.enc
} }
// GetWriteFunc returns the currently set WriterFunc of the Part // GetWriteFunc returns the currently set WriterFunc of the Part
func (p *Part) GetWriteFunc() func(io.Writer) (int64, error) { func (p *Part) GetWriteFunc() func(io.Writer) (int64, error) {
return p.writeFunc return p.w
} }
// GetDescription returns the currently set Content-Description of the Part // GetDescription returns the currently set Content-Description of the Part
func (p *Part) GetDescription() string { func (p *Part) GetDescription() string {
return p.description return p.desc
} }
// SetContent overrides the content of the Part with the given string // SetContent overrides the content of the Part with the given string
func (p *Part) SetContent(content string) { func (p *Part) SetContent(c string) {
buffer := bytes.NewBufferString(content) buf := bytes.NewBufferString(c)
p.writeFunc = writeFuncFromBuffer(buffer) p.w = writeFuncFromBuffer(buf)
} }
// SetContentType overrides the ContentType of the Part // SetContentType overrides the ContentType of the Part
func (p *Part) SetContentType(contentType ContentType) { func (p *Part) SetContentType(c ContentType) {
p.contentType = contentType p.ctype = c
} }
// SetCharset overrides the Charset of the Part // SetCharset overrides the Charset of the Part
func (p *Part) SetCharset(charset Charset) { func (p *Part) SetCharset(c Charset) {
p.charset = charset p.cset = c
} }
// SetEncoding creates a new mime.WordEncoder based on the encoding setting of the message // SetEncoding creates a new mime.WordEncoder based on the encoding setting of the message
func (p *Part) SetEncoding(encoding Encoding) { func (p *Part) SetEncoding(e Encoding) {
p.encoding = encoding p.enc = e
} }
// SetDescription overrides the Content-Description of the Part // SetDescription overrides the Content-Description of the Part
func (p *Part) SetDescription(description string) { func (p *Part) SetDescription(d string) {
p.description = description p.desc = d
} }
// SetWriteFunc overrides the WriteFunc of the Part // SetWriteFunc overrides the WriteFunc of the Part
func (p *Part) SetWriteFunc(writeFunc func(io.Writer) (int64, error)) { func (p *Part) SetWriteFunc(w func(io.Writer) (int64, error)) {
p.writeFunc = writeFunc p.w = w
} }
// Delete removes the current part from the parts list of the Msg by setting the // Delete removes the current part from the parts list of the Msg by setting the
// isDeleted flag to true. The msgWriter will skip it then // del flag to true. The msgWriter will skip it then
func (p *Part) Delete() { func (p *Part) Delete() {
p.isDeleted = true p.del = true
} }
// WithPartCharset overrides the default Part charset // WithPartCharset overrides the default Part charset
func WithPartCharset(charset Charset) PartOption { func WithPartCharset(c Charset) PartOption {
return func(p *Part) { return func(p *Part) {
p.charset = charset p.cset = c
} }
} }
// WithPartEncoding overrides the default Part encoding // WithPartEncoding overrides the default Part encoding
func WithPartEncoding(encoding Encoding) PartOption { func WithPartEncoding(e Encoding) PartOption {
return func(p *Part) { return func(p *Part) {
p.encoding = encoding p.enc = e
} }
} }
// WithPartContentDescription overrides the default Part Content-Description // WithPartContentDescription overrides the default Part Content-Description
func WithPartContentDescription(description string) PartOption { func WithPartContentDescription(d string) PartOption {
return func(p *Part) { return func(p *Part) {
p.description = description p.desc = d
} }
} }

View file

@ -31,15 +31,15 @@ func TestPartEncoding(t *testing.T) {
t.Errorf("newPart() WithPartEncoding() failed: no part returned") t.Errorf("newPart() WithPartEncoding() failed: no part returned")
return return
} }
if part.encoding.String() != tt.want { if part.enc.String() != tt.want {
t.Errorf("newPart() WithPartEncoding() failed: expected encoding: %s, got: %s", tt.want, t.Errorf("newPart() WithPartEncoding() failed: expected encoding: %s, got: %s", tt.want,
part.encoding.String()) part.enc.String())
} }
part.encoding = "" part.enc = ""
part.SetEncoding(tt.enc) part.SetEncoding(tt.enc)
if part.encoding.String() != tt.want { if part.enc.String() != tt.want {
t.Errorf("newPart() SetEncoding() failed: expected encoding: %s, got: %s", tt.want, t.Errorf("newPart() SetEncoding() failed: expected encoding: %s, got: %s", tt.want,
part.encoding.String()) part.enc.String())
} }
}) })
} }
@ -64,9 +64,9 @@ func TestWithPartCharset(t *testing.T) {
t.Errorf("newPart() WithPartCharset() failed: no part returned") t.Errorf("newPart() WithPartCharset() failed: no part returned")
return return
} }
if part.charset.String() != tt.want { if part.cset.String() != tt.want {
t.Errorf("newPart() WithPartCharset() failed: expected charset: %s, got: %s", t.Errorf("newPart() WithPartCharset() failed: expected charset: %s, got: %s",
tt.want, part.charset.String()) tt.want, part.cset.String())
} }
}) })
} }
@ -89,14 +89,14 @@ func TestPart_WithPartContentDescription(t *testing.T) {
t.Errorf("newPart() WithPartContentDescription() failed: no part returned") t.Errorf("newPart() WithPartContentDescription() failed: no part returned")
return return
} }
if part.description != tt.desc { if part.desc != tt.desc {
t.Errorf("newPart() WithPartContentDescription() failed: expected: %s, got: %s", tt.desc, t.Errorf("newPart() WithPartContentDescription() failed: expected: %s, got: %s", tt.desc,
part.description) part.desc)
} }
part.description = "" part.desc = ""
part.SetDescription(tt.desc) part.SetDescription(tt.desc)
if part.description != tt.desc { if part.desc != tt.desc {
t.Errorf("newPart() SetDescription() failed: expected: %s, got: %s", tt.desc, part.description) t.Errorf("newPart() SetDescription() failed: expected: %s, got: %s", tt.desc, part.desc)
} }
}) })
} }
@ -236,7 +236,7 @@ func TestPart_GetContentBroken(t *testing.T) {
t.Errorf("failed: %s", err) t.Errorf("failed: %s", err)
return return
} }
pl[0].writeFunc = func(io.Writer) (int64, error) { pl[0].w = func(io.Writer) (int64, error) {
return 0, fmt.Errorf("broken") return 0, fmt.Errorf("broken")
} }
_, err = pl[0].GetContent() _, err = pl[0].GetContent()
@ -314,8 +314,8 @@ func TestPart_SetDescription(t *testing.T) {
t.Errorf("Part.GetDescription failed. Expected empty description but got: %s", pd) t.Errorf("Part.GetDescription failed. Expected empty description but got: %s", pd)
} }
pl[0].SetDescription(d) pl[0].SetDescription(d)
if pl[0].description != d { if pl[0].desc != d {
t.Errorf("Part.SetDescription failed. Expected description to be: %s, got: %s", d, pd) t.Errorf("Part.SetDescription failed. Expected desc to be: %s, got: %s", d, pd)
} }
pd = pl[0].GetDescription() pd = pl[0].GetDescription()
if pd != d { if pd != d {
@ -334,8 +334,8 @@ func TestPart_Delete(t *testing.T) {
return return
} }
pl[0].Delete() pl[0].Delete()
if !pl[0].isDeleted { if !pl[0].del {
t.Errorf("Delete failed. Expected: %t, got: %t", true, pl[0].isDeleted) t.Errorf("Delete failed. Expected: %t, got: %t", true, pl[0].del)
} }
} }

View file

@ -22,53 +22,54 @@ const (
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
) )
// randomStringSecure returns a random, string of length characters. This method uses the // randomStringSecure returns a random, n long string of characters. The character set is based
// on the s (special chars) and h (human readable) boolean arguments. This method uses the
// crypto/random package and therfore is cryptographically secure // crypto/random package and therfore is cryptographically secure
func randomStringSecure(length int) (string, error) { func randomStringSecure(n int) (string, error) {
randString := strings.Builder{} rs := strings.Builder{}
randString.Grow(length) rs.Grow(n)
charRangeLength := len(cr) crl := len(cr)
randPool := make([]byte, 8) rp := make([]byte, 8)
_, err := rand.Read(randPool) _, err := rand.Read(rp)
if err != nil { if err != nil {
return randString.String(), err return rs.String(), err
} }
for idx, char, rest := length-1, binary.BigEndian.Uint64(randPool), letterIdxMax; idx >= 0; { for i, c, r := n-1, binary.BigEndian.Uint64(rp), letterIdxMax; i >= 0; {
if rest == 0 { if r == 0 {
_, err = rand.Read(randPool) _, err := rand.Read(rp)
if err != nil { if err != nil {
return randString.String(), err return rs.String(), err
} }
char, rest = binary.BigEndian.Uint64(randPool), letterIdxMax c, r = binary.BigEndian.Uint64(rp), letterIdxMax
} }
if i := int(char & letterIdxMask); i < charRangeLength { if idx := int(c & letterIdxMask); idx < crl {
randString.WriteByte(cr[i]) rs.WriteByte(cr[idx])
idx-- i--
} }
char >>= letterIdxBits c >>= letterIdxBits
rest-- r--
} }
return randString.String(), nil return rs.String(), nil
} }
// randNum returns a random number with a maximum value of length // randNum returns a random number with a maximum value of n
func randNum(length int) (int, error) { func randNum(n int) (int, error) {
if length <= 0 { if n <= 0 {
return 0, fmt.Errorf("provided number is <= 0: %d", length) return 0, fmt.Errorf("provided number is <= 0: %d", n)
} }
length64 := big.NewInt(int64(length)) mbi := big.NewInt(int64(n))
if !length64.IsUint64() { if !mbi.IsUint64() {
return 0, fmt.Errorf("big.NewInt() generation returned negative value: %d", length64) return 0, fmt.Errorf("big.NewInt() generation returned negative value: %d", mbi)
} }
randNum64, err := rand.Int(rand.Reader, length64) rn64, err := rand.Int(rand.Reader, mbi)
if err != nil { if err != nil {
return 0, err return 0, err
} }
randomNum := int(randNum64.Int64()) rn := int(rn64.Int64())
if randomNum < 0 { if rn < 0 {
return 0, fmt.Errorf("generated random number does not fit as int64: %d", randNum64) return 0, fmt.Errorf("generated random number does not fit as int64: %d", rn64)
} }
return randomNum, nil return rn, nil
} }

View file

@ -10,8 +10,8 @@ import (
// Reader is a type that implements the io.Reader interface for a Msg // Reader is a type that implements the io.Reader interface for a Msg
type Reader struct { type Reader struct {
buffer []byte // contents are the bytes buffer[offset : len(buffer)] buf []byte // contents are the bytes buf[off : len(buf)]
offset int // read at &buffer[offset], write at &buffer[len(buffer)] off int // read at &buf[off], write at &buf[len(buf)]
err error // initialization error err error // initialization error
} }
@ -21,28 +21,28 @@ func (r *Reader) Error() error {
} }
// Read reads the length of p of the Msg buffer to satisfy the io.Reader interface // Read reads the length of p of the Msg buffer to satisfy the io.Reader interface
func (r *Reader) Read(payload []byte) (n int, err error) { func (r *Reader) Read(p []byte) (n int, err error) {
if r.err != nil { if r.err != nil {
return 0, r.err return 0, r.err
} }
if r.empty() || r.buffer == nil { if r.empty() || r.buf == nil {
r.Reset() r.Reset()
if len(payload) == 0 { if len(p) == 0 {
return 0, nil return 0, nil
} }
return 0, io.EOF return 0, io.EOF
} }
n = copy(payload, r.buffer[r.offset:]) n = copy(p, r.buf[r.off:])
r.offset += n r.off += n
return n, err return n, err
} }
// Reset resets the Reader buffer to be empty, but it retains the underlying storage // Reset resets the Reader buffer to be empty, but it retains the underlying storage
// for use by future writes. // for use by future writes.
func (r *Reader) Reset() { func (r *Reader) Reset() {
r.buffer = r.buffer[:0] r.buf = r.buf[:0]
r.offset = 0 r.off = 0
} }
// empty reports whether the unread portion of the Reader buffer is empty. // empty reports whether the unread portion of the Reader buffer is empty.
func (r *Reader) empty() bool { return len(r.buffer) <= r.offset } func (r *Reader) empty() bool { return len(r.buf) <= r.off }

View file

@ -65,7 +65,7 @@ func TestReader_Read_error(t *testing.T) {
// TestReader_Read_empty tests the Reader.Read method with an empty buffer // TestReader_Read_empty tests the Reader.Read method with an empty buffer
func TestReader_Read_empty(t *testing.T) { func TestReader_Read_empty(t *testing.T) {
r := Reader{buffer: []byte{}} r := Reader{buf: []byte{}}
p := make([]byte, 1) p := make([]byte, 1)
p[0] = 'a' p[0] = 'a'
_, err := r.Read(p) _, err := r.Read(p)
@ -76,7 +76,7 @@ func TestReader_Read_empty(t *testing.T) {
// TestReader_Read_nil tests the Reader.Read method with a nil buffer // TestReader_Read_nil tests the Reader.Read method with a nil buffer
func TestReader_Read_nil(t *testing.T) { func TestReader_Read_nil(t *testing.T) {
r := Reader{buffer: nil, offset: -10} r := Reader{buf: nil, off: -10}
p := make([]byte, 0) p := make([]byte, 0)
_, err := r.Read(p) _, err := r.Read(p)
if err != nil && !errors.Is(err, io.EOF) { if err != nil && !errors.Is(err, io.EOF) {

View file

@ -71,34 +71,34 @@ func (e *SendError) Error() string {
return "unknown reason" return "unknown reason"
} }
var errMessage strings.Builder var em strings.Builder
errMessage.WriteString(e.Reason.String()) em.WriteString(e.Reason.String())
if len(e.errlist) > 0 { if len(e.errlist) > 0 {
errMessage.WriteRune(':') em.WriteRune(':')
for i := range e.errlist { for i := range e.errlist {
errMessage.WriteRune(' ') em.WriteRune(' ')
errMessage.WriteString(e.errlist[i].Error()) em.WriteString(e.errlist[i].Error())
if i != len(e.errlist)-1 { if i != len(e.errlist)-1 {
errMessage.WriteString(", ") em.WriteString(", ")
} }
} }
} }
if len(e.rcpt) > 0 { if len(e.rcpt) > 0 {
errMessage.WriteString(", affected recipient(s): ") em.WriteString(", affected recipient(s): ")
for i := range e.rcpt { for i := range e.rcpt {
errMessage.WriteString(e.rcpt[i]) em.WriteString(e.rcpt[i])
if i != len(e.rcpt)-1 { if i != len(e.rcpt)-1 {
errMessage.WriteString(", ") em.WriteString(", ")
} }
} }
} }
return errMessage.String() return em.String()
} }
// Is implements the errors.Is functionality and compares the SendErrReason // Is implements the errors.Is functionality and compares the SendErrReason
func (e *SendError) Is(errType error) bool { func (e *SendError) Is(et error) bool {
var t *SendError var t *SendError
if errors.As(errType, &t) && t != nil { if errors.As(et, &t) && t != nil {
return e.Reason == t.Reason && e.isTemp == t.isTemp return e.Reason == t.Reason && e.isTemp == t.isTemp
} }
return false return false
@ -143,6 +143,6 @@ func (r SendErrReason) String() string {
// isTempError checks the given SMTP error and returns true if the given error is of temporary nature // isTempError checks the given SMTP error and returns true if the given error is of temporary nature
// and should be retried // and should be retried
func isTempError(err error) bool { func isTempError(e error) bool {
return err.Error()[0] == '4' return e.Error()[0] == '4'
} }

View file

@ -777,9 +777,9 @@ func TestClient_SetLogger(t *testing.T) {
if c.logger == nil { if c.logger == nil {
t.Errorf("Expected Logger to be set but received nil") t.Errorf("Expected Logger to be set but received nil")
} }
c.logger.Debugf(log.Log{Direction: log.DirServerToClient, Format: "%s", Messages: []interface{}{"test"}}) c.logger.Debugf(log.Log{Direction: log.DirServerToClient, Format: "", Messages: []interface{}{"test"}})
c.SetLogger(nil) c.SetLogger(nil)
c.logger.Debugf(log.Log{Direction: log.DirServerToClient, Format: "%s", Messages: []interface{}{"test"}}) c.logger.Debugf(log.Log{Direction: log.DirServerToClient, Format: "", Messages: []interface{}{"test"}})
} }
var newClientServer = `220 hello world var newClientServer = `220 hello world

8
tls.go
View file

@ -8,17 +8,17 @@ package mail
type TLSPolicy int type TLSPolicy int
const ( const (
// TLSMandatory requires that the connection to the server is // TLSMandatory requires that the connection cto the server is
// encrypting using STARTTLS. If the server does not support STARTTLS // encrypting using STARTTLS. If the server does not support STARTTLS
// the connection will be terminated with an error // the connection will be terminated with an error
TLSMandatory TLSPolicy = iota TLSMandatory TLSPolicy = iota
// TLSOpportunistic tries to establish an encrypted connection via the // TLSOpportunistic tries cto establish an encrypted connection via the
// STARTTLS protocol. If the server does not support this, it will fall // STARTTLS protocol. If the server does not support this, it will fall
// back to non-encrypted plaintext transmission // back cto non-encrypted plaintext transmission
TLSOpportunistic TLSOpportunistic
// NoTLS forces the transaction to be not encrypted // NoTLS forces the transaction cto be not encrypted
NoTLS NoTLS
) )