mirror of
https://github.com/wneessen/go-mail.git
synced 2024-12-22 18:50:37 +01:00
Merge branch 'main' into main
This commit is contained in:
commit
e3fb10d897
41 changed files with 5828 additions and 2685 deletions
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
|
@ -1,6 +1,6 @@
|
||||||
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: CC0-1.0
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
github: wneessen
|
github: wneessen
|
||||||
ko_fi: winni
|
ko_fi: winni
|
||||||
|
|
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
@ -1,6 +1,6 @@
|
||||||
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: CC0-1.0
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: Create a report to help us improve
|
description: Create a report to help us improve
|
||||||
|
|
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
|
@ -1,6 +1,6 @@
|
||||||
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: CC0-1.0
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
|
|
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
|
@ -1,6 +1,6 @@
|
||||||
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: CC0-1.0
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
name: Feature request
|
name: Feature request
|
||||||
description: Suggest an idea for this project
|
description: Suggest an idea for this project
|
||||||
|
|
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
|
@ -1,6 +1,6 @@
|
||||||
# SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
# SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: CC0-1.0
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
|
|
36
.github/workflows/ci.yml
vendored
36
.github/workflows/ci.yml
vendored
|
@ -33,12 +33,14 @@ jobs:
|
||||||
PERFORM_ONLINE_TEST: ${{ vars.PERFORM_ONLINE_TEST }}
|
PERFORM_ONLINE_TEST: ${{ vars.PERFORM_ONLINE_TEST }}
|
||||||
PERFORM_UNIX_OPEN_WRITE_TESTS: ${{ vars.PERFORM_UNIX_OPEN_WRITE_TESTS }}
|
PERFORM_UNIX_OPEN_WRITE_TESTS: ${{ vars.PERFORM_UNIX_OPEN_WRITE_TESTS }}
|
||||||
PERFORM_SENDMAIL_TESTS: ${{ vars.PERFORM_SENDMAIL_TESTS }}
|
PERFORM_SENDMAIL_TESTS: ${{ vars.PERFORM_SENDMAIL_TESTS }}
|
||||||
|
TEST_BASEPORT: ${{ vars.TEST_BASEPORT }}
|
||||||
|
TEST_BASEPORT_SMTP: ${{ vars.TEST_BASEPORT_SMTP }}
|
||||||
TEST_HOST: ${{ secrets.TEST_HOST }}
|
TEST_HOST: ${{ secrets.TEST_HOST }}
|
||||||
TEST_USER: ${{ secrets.TEST_USER }}
|
TEST_USER: ${{ secrets.TEST_USER }}
|
||||||
TEST_PASS: ${{ secrets.TEST_PASS }}
|
TEST_PASS: ${{ secrets.TEST_PASS }}
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
|
@ -50,14 +52,14 @@ jobs:
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Install sendmail
|
- name: Install sendmail
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get -y update >/dev/null && sudo apt-get -y upgrade >/dev/null && sudo DEBIAN_FRONTEND=noninteractive apt-get -y install nullmailer >/dev/null && which sendmail
|
sudo apt-get -y update && sudo DEBIAN_FRONTEND=noninteractive apt-get -y install nullmailer && which sendmail
|
||||||
- name: Run go test
|
- name: Run go test
|
||||||
if: success()
|
if: success()
|
||||||
run: |
|
run: |
|
||||||
go test -race -shuffle=on --coverprofile=coverage.coverprofile --covermode=atomic ./...
|
go test -race -shuffle=on --coverprofile=coverage.coverprofile --covermode=atomic ./...
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: success()
|
if: success()
|
||||||
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
|
uses: codecov/codecov-action@7f8b4b4bde536c465e797be725718b88c5d95e0e # v5.1.1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
||||||
lint:
|
lint:
|
||||||
|
@ -71,7 +73,7 @@ jobs:
|
||||||
go: ['1.23']
|
go: ['1.23']
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
- name: Setup go
|
- name: Setup go
|
||||||
|
@ -93,13 +95,13 @@ jobs:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||||
- name: 'Dependency Review'
|
- name: 'Dependency Review'
|
||||||
uses: actions/dependency-review-action@4081bf99e2866ebe428fc0477b69eb4fcda7220a # v4.4.0
|
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0
|
||||||
with:
|
with:
|
||||||
base-ref: ${{ github.event.pull_request.base.sha || 'main' }}
|
base-ref: ${{ github.event.pull_request.base.sha || 'main' }}
|
||||||
head-ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
head-ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||||
|
@ -111,7 +113,7 @@ jobs:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
- name: Run govulncheck
|
- name: Run govulncheck
|
||||||
|
@ -126,9 +128,12 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
go: ['1.19', '1.20', '1.21', '1.22', '1.23']
|
go: ['1.19', '1.20', '1.21', '1.22', '1.23']
|
||||||
|
env:
|
||||||
|
TEST_BASEPORT: ${{ vars.TEST_BASEPORT }}
|
||||||
|
TEST_BASEPORT_SMTP: ${{ vars.TEST_BASEPORT_SMTP }}
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
|
@ -149,11 +154,14 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
osver: ['14.1', '14.0', 13.4']
|
osver: ['14.1', '14.0', 13.4']
|
||||||
|
env:
|
||||||
|
TEST_BASEPORT: ${{ vars.TEST_BASEPORT }}
|
||||||
|
TEST_BASEPORT_SMTP: ${{ vars.TEST_BASEPORT_SMTP }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||||
- name: Run go test on FreeBSD
|
- name: Run go test on FreeBSD
|
||||||
uses: vmactions/freebsd-vm@v1
|
uses: vmactions/freebsd-vm@debf37ca7b7fa40e19c542ef7ba30d6054a706a4 # v1.1.5
|
||||||
with:
|
with:
|
||||||
usesh: true
|
usesh: true
|
||||||
copyback: false
|
copyback: false
|
||||||
|
@ -170,13 +178,13 @@ jobs:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||||
- name: REUSE Compliance Check
|
- name: REUSE Compliance Check
|
||||||
uses: fsfe/reuse-action@3ae3c6bdf1257ab19397fab11fd3312144692083 # v4.0.0
|
uses: fsfe/reuse-action@bb774aa972c2a89ff34781233d275075cbddf542 # v5.0.0
|
||||||
sonarqube:
|
sonarqube:
|
||||||
name: Test with SonarQube review (${{ matrix.os }} / ${{ matrix.go }})
|
name: Test with SonarQube review (${{ matrix.os }} / ${{ matrix.go }})
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
@ -189,12 +197,14 @@ jobs:
|
||||||
go: ['1.23']
|
go: ['1.23']
|
||||||
env:
|
env:
|
||||||
PERFORM_ONLINE_TEST: ${{ vars.PERFORM_ONLINE_TEST }}
|
PERFORM_ONLINE_TEST: ${{ vars.PERFORM_ONLINE_TEST }}
|
||||||
|
TEST_BASEPORT: ${{ vars.TEST_BASEPORT }}
|
||||||
|
TEST_BASEPORT_SMTP: ${{ vars.TEST_BASEPORT_SMTP }}
|
||||||
TEST_HOST: ${{ secrets.TEST_HOST }}
|
TEST_HOST: ${{ secrets.TEST_HOST }}
|
||||||
TEST_USER: ${{ secrets.TEST_USER }}
|
TEST_USER: ${{ secrets.TEST_USER }}
|
||||||
TEST_PASS: ${{ secrets.TEST_PASS }}
|
TEST_PASS: ${{ secrets.TEST_PASS }}
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
|
@ -208,7 +218,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
go test -shuffle=on -race --coverprofile=./cov.out ./...
|
go test -shuffle=on -race --coverprofile=./cov.out ./...
|
||||||
- name: SonarQube scan
|
- name: SonarQube scan
|
||||||
uses: sonarsource/sonarqube-scan-action@884b79409bbd464b2a59edc326a4b77dc56b2195 # master
|
uses: sonarsource/sonarqube-scan-action@1b442ee39ac3fa7c2acdd410208dcb2bcfaae6c4 # master
|
||||||
if: success()
|
if: success()
|
||||||
env:
|
env:
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
|
|
10
.github/workflows/codeql-analysis.yml
vendored
10
.github/workflows/codeql-analysis.yml
vendored
|
@ -1,6 +1,6 @@
|
||||||
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: CC0-1.0
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
# For most projects, this workflow file will not need changing; you simply need
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
# to commit it to your repository.
|
# to commit it to your repository.
|
||||||
|
@ -45,7 +45,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ jobs:
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
uses: github/codeql-action/init@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
@ -65,7 +65,7 @@ jobs:
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
uses: github/codeql-action/autobuild@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
|
@ -79,4 +79,4 @@ jobs:
|
||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
uses: github/codeql-action/analyze@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6
|
||||||
|
|
6
.github/workflows/scorecards.yml
vendored
6
.github/workflows/scorecards.yml
vendored
|
@ -1,6 +1,6 @@
|
||||||
# SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
# SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: CC0-1.0
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
# This workflow uses actions that are not certified by GitHub. They are provided
|
# 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
|
# by a third-party and are governed by separate terms of service, privacy
|
||||||
|
@ -35,7 +35,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
@ -75,6 +75,6 @@ jobs:
|
||||||
|
|
||||||
# Upload the results to GitHub's code scanning dashboard.
|
# Upload the results to GitHub's code scanning dashboard.
|
||||||
- name: "Upload to code-scanning"
|
- name: "Upload to code-scanning"
|
||||||
uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
uses: github/codeql-action/upload-sarif@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,6 +1,6 @@
|
||||||
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: CC0-1.0
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
# Binaries for programs and plugins
|
# Binaries for programs and plugins
|
||||||
*.exe
|
*.exe
|
||||||
|
|
|
@ -9,4 +9,73 @@ exclude-dirs = ["examples"]
|
||||||
|
|
||||||
[linters]
|
[linters]
|
||||||
enable = ["stylecheck", "whitespace", "containedctx", "contextcheck", "decorder",
|
enable = ["stylecheck", "whitespace", "containedctx", "contextcheck", "decorder",
|
||||||
"errname", "errorlint", "gofmt", "gofumpt"]
|
"errname", "errorlint", "gofmt", "gofumpt", "gosec"]
|
||||||
|
|
||||||
|
[issues]
|
||||||
|
|
||||||
|
## An overflow is impossible here
|
||||||
|
[[issues.exclude-rules]]
|
||||||
|
linters = ["gosec"]
|
||||||
|
path = "random.go"
|
||||||
|
text = "G115:"
|
||||||
|
|
||||||
|
## These are tests which intentionally do not need any TLS settings
|
||||||
|
[[issues.exclude-rules]]
|
||||||
|
linters = ["gosec"]
|
||||||
|
path = "client_test.go"
|
||||||
|
text = "G402:"
|
||||||
|
|
||||||
|
## These are tests which intentionally do not need any TLS settings
|
||||||
|
[[issues.exclude-rules]]
|
||||||
|
linters = ["gosec"]
|
||||||
|
path = "smtp/smtp_test.go"
|
||||||
|
text = "G402:"
|
||||||
|
|
||||||
|
## We do not dictate a TLS minimum version in the smtp package. go-mail
|
||||||
|
## itself does set sane defaults
|
||||||
|
[[issues.exclude-rules]]
|
||||||
|
linters = ["gosec"]
|
||||||
|
path = "smtp/smtp.go"
|
||||||
|
text = "G402:"
|
||||||
|
|
||||||
|
## The chance that we write +2 million tests is very low, I think we can
|
||||||
|
## ignore this for the time being
|
||||||
|
[[issues.exclude-rules]]
|
||||||
|
linters = ["gosec"]
|
||||||
|
path = "client_test.go"
|
||||||
|
text = "G109:"
|
||||||
|
|
||||||
|
## The chance that we write +2 million tests is very low, I think we can
|
||||||
|
## ignore this for the time being
|
||||||
|
[[issues.exclude-rules]]
|
||||||
|
linters = ["gosec"]
|
||||||
|
path = "smtp/smtp_test.go"
|
||||||
|
text = "G109:"
|
||||||
|
|
||||||
|
## We inform the user about the deprecated status of CRAM-MD5 and suggest
|
||||||
|
## to use SCRAM-SHA instead
|
||||||
|
[[issues.exclude-rules]]
|
||||||
|
linters = ["gosec"]
|
||||||
|
path = "smtp/auth_cram_md5.go"
|
||||||
|
text = "G501:"
|
||||||
|
|
||||||
|
## Yes, SHA1 is weak, but in the context of SCRAM it is still considered
|
||||||
|
## secure for specific applications. The user is information about this
|
||||||
|
## in the documentation
|
||||||
|
[[issues.exclude-rules]]
|
||||||
|
linters = ["gosec"]
|
||||||
|
path = "smtp/auth_scram.go"
|
||||||
|
text = "G505:"
|
||||||
|
|
||||||
|
## Test code for SCRAM-SHA1. Can be ignored.
|
||||||
|
[[issues.exclude-rules]]
|
||||||
|
linters = ["gosec"]
|
||||||
|
path = "smtp/smtp_test.go"
|
||||||
|
text = "G505:"
|
||||||
|
|
||||||
|
## These are tests which intentionally do not need any TLS settings
|
||||||
|
[[issues.exclude-rules]]
|
||||||
|
linters = ["gosec"]
|
||||||
|
path = "quicksend_test.go"
|
||||||
|
text = "G402:"
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<!--
|
<!--
|
||||||
SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||||
|
|
||||||
SPDX-License-Identifier: CC0-1.0
|
SPDX-License-Identifier: MIT
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# Contributor Covenant Code of Conduct
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<!--
|
<!--
|
||||||
SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||||
|
|
||||||
SPDX-License-Identifier: CC0-1.0
|
SPDX-License-Identifier: MIT
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# How to contribute
|
# How to contribute
|
||||||
|
|
|
@ -1,121 +0,0 @@
|
||||||
Creative Commons Legal Code
|
|
||||||
|
|
||||||
CC0 1.0 Universal
|
|
||||||
|
|
||||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
|
||||||
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
|
||||||
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
|
||||||
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
|
||||||
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
|
||||||
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
|
||||||
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
|
||||||
HEREUNDER.
|
|
||||||
|
|
||||||
Statement of Purpose
|
|
||||||
|
|
||||||
The laws of most jurisdictions throughout the world automatically confer
|
|
||||||
exclusive Copyright and Related Rights (defined below) upon the creator
|
|
||||||
and subsequent owner(s) (each and all, an "owner") of an original work of
|
|
||||||
authorship and/or a database (each, a "Work").
|
|
||||||
|
|
||||||
Certain owners wish to permanently relinquish those rights to a Work for
|
|
||||||
the purpose of contributing to a commons of creative, cultural and
|
|
||||||
scientific works ("Commons") that the public can reliably and without fear
|
|
||||||
of later claims of infringement build upon, modify, incorporate in other
|
|
||||||
works, reuse and redistribute as freely as possible in any form whatsoever
|
|
||||||
and for any purposes, including without limitation commercial purposes.
|
|
||||||
These owners may contribute to the Commons to promote the ideal of a free
|
|
||||||
culture and the further production of creative, cultural and scientific
|
|
||||||
works, or to gain reputation or greater distribution for their Work in
|
|
||||||
part through the use and efforts of others.
|
|
||||||
|
|
||||||
For these and/or other purposes and motivations, and without any
|
|
||||||
expectation of additional consideration or compensation, the person
|
|
||||||
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
|
||||||
is an owner of Copyright and Related Rights in the Work, voluntarily
|
|
||||||
elects to apply CC0 to the Work and publicly distribute the Work under its
|
|
||||||
terms, with knowledge of his or her Copyright and Related Rights in the
|
|
||||||
Work and the meaning and intended legal effect of CC0 on those rights.
|
|
||||||
|
|
||||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
|
||||||
protected by copyright and related or neighboring rights ("Copyright and
|
|
||||||
Related Rights"). Copyright and Related Rights include, but are not
|
|
||||||
limited to, the following:
|
|
||||||
|
|
||||||
i. the right to reproduce, adapt, distribute, perform, display,
|
|
||||||
communicate, and translate a Work;
|
|
||||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
|
||||||
iii. publicity and privacy rights pertaining to a person's image or
|
|
||||||
likeness depicted in a Work;
|
|
||||||
iv. rights protecting against unfair competition in regards to a Work,
|
|
||||||
subject to the limitations in paragraph 4(a), below;
|
|
||||||
v. rights protecting the extraction, dissemination, use and reuse of data
|
|
||||||
in a Work;
|
|
||||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
|
||||||
European Parliament and of the Council of 11 March 1996 on the legal
|
|
||||||
protection of databases, and under any national implementation
|
|
||||||
thereof, including any amended or successor version of such
|
|
||||||
directive); and
|
|
||||||
vii. other similar, equivalent or corresponding rights throughout the
|
|
||||||
world based on applicable law or treaty, and any national
|
|
||||||
implementations thereof.
|
|
||||||
|
|
||||||
2. Waiver. To the greatest extent permitted by, but not in contravention
|
|
||||||
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
|
||||||
irrevocably and unconditionally waives, abandons, and surrenders all of
|
|
||||||
Affirmer's Copyright and Related Rights and associated claims and causes
|
|
||||||
of action, whether now known or unknown (including existing as well as
|
|
||||||
future claims and causes of action), in the Work (i) in all territories
|
|
||||||
worldwide, (ii) for the maximum duration provided by applicable law or
|
|
||||||
treaty (including future time extensions), (iii) in any current or future
|
|
||||||
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
|
||||||
including without limitation commercial, advertising or promotional
|
|
||||||
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
|
||||||
member of the public at large and to the detriment of Affirmer's heirs and
|
|
||||||
successors, fully intending that such Waiver shall not be subject to
|
|
||||||
revocation, rescission, cancellation, termination, or any other legal or
|
|
||||||
equitable action to disrupt the quiet enjoyment of the Work by the public
|
|
||||||
as contemplated by Affirmer's express Statement of Purpose.
|
|
||||||
|
|
||||||
3. Public License Fallback. Should any part of the Waiver for any reason
|
|
||||||
be judged legally invalid or ineffective under applicable law, then the
|
|
||||||
Waiver shall be preserved to the maximum extent permitted taking into
|
|
||||||
account Affirmer's express Statement of Purpose. In addition, to the
|
|
||||||
extent the Waiver is so judged Affirmer hereby grants to each affected
|
|
||||||
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
|
||||||
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
|
||||||
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
|
||||||
maximum duration provided by applicable law or treaty (including future
|
|
||||||
time extensions), (iii) in any current or future medium and for any number
|
|
||||||
of copies, and (iv) for any purpose whatsoever, including without
|
|
||||||
limitation commercial, advertising or promotional purposes (the
|
|
||||||
"License"). The License shall be deemed effective as of the date CC0 was
|
|
||||||
applied by Affirmer to the Work. Should any part of the License for any
|
|
||||||
reason be judged legally invalid or ineffective under applicable law, such
|
|
||||||
partial invalidity or ineffectiveness shall not invalidate the remainder
|
|
||||||
of the License, and in such case Affirmer hereby affirms that he or she
|
|
||||||
will not (i) exercise any of his or her remaining Copyright and Related
|
|
||||||
Rights in the Work or (ii) assert any associated claims and causes of
|
|
||||||
action with respect to the Work, in either case contrary to Affirmer's
|
|
||||||
express Statement of Purpose.
|
|
||||||
|
|
||||||
4. Limitations and Disclaimers.
|
|
||||||
|
|
||||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
|
||||||
surrendered, licensed or otherwise affected by this document.
|
|
||||||
b. Affirmer offers the Work as-is and makes no representations or
|
|
||||||
warranties of any kind concerning the Work, express, implied,
|
|
||||||
statutory or otherwise, including without limitation warranties of
|
|
||||||
title, merchantability, fitness for a particular purpose, non
|
|
||||||
infringement, or the absence of latent or other defects, accuracy, or
|
|
||||||
the present or absence of errors, whether or not discoverable, all to
|
|
||||||
the greatest extent permissible under applicable law.
|
|
||||||
c. Affirmer disclaims responsibility for clearing rights of other persons
|
|
||||||
that may apply to the Work or any use thereof, including without
|
|
||||||
limitation any person's Copyright and Related Rights in the Work.
|
|
||||||
Further, Affirmer disclaims responsibility for obtaining any necessary
|
|
||||||
consents, permissions or other rights required for any use of the
|
|
||||||
Work.
|
|
||||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
|
||||||
party to this document and has no duty or obligation with respect to
|
|
||||||
this CC0 or use of the Work.
|
|
16
README.md
16
README.md
|
@ -1,7 +1,7 @@
|
||||||
<!--
|
<!--
|
||||||
SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||||
|
|
||||||
SPDX-License-Identifier: CC0-1.0
|
SPDX-License-Identifier: MIT
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# go-mail - Easy to use, yet comprehensive library for sending mails with Go
|
# go-mail - Easy to use, yet comprehensive library for sending mails with Go
|
||||||
|
@ -39,14 +39,20 @@ Here are some highlights of go-mail's featureset:
|
||||||
* [X] Very small dependency footprint (mainly Go Stdlib and Go extended packages)
|
* [X] Very small dependency footprint (mainly Go Stdlib and Go extended packages)
|
||||||
* [X] Modern, idiomatic Go
|
* [X] Modern, idiomatic Go
|
||||||
* [X] Sane and secure defaults
|
* [X] Sane and secure defaults
|
||||||
* [X] Explicit SSL/TLS support
|
* [X] Implicit SSL/TLS support
|
||||||
* [X] Implicit StartTLS support with different policies
|
* [X] Explicit STARTTLS support with different policies
|
||||||
* [X] Makes use of contexts for a better control flow and timeout/cancelation handling
|
* [X] Makes use of contexts for a better control flow and timeout/cancelation handling
|
||||||
* [X] SMTP Auth support (LOGIN, PLAIN, CRAM-MD, XOAUTH2, SCRAM-SHA-1(-PLUS), SCRAM-SHA-256(-PLUS))
|
* [X] SMTP Auth support
|
||||||
|
* [X] CRAM-MD5
|
||||||
|
* [X] LOGIN
|
||||||
|
* [X] PLAIN
|
||||||
|
* [X] SCRAM-SHA-1/SCRAM-SHA-1-PLUS
|
||||||
|
* [X] SCRAM-SHA-256/SCRAM-SHA-256-PLUS
|
||||||
|
* [X] XOAUTH2
|
||||||
* [X] RFC5322 compliant mail address validation
|
* [X] RFC5322 compliant mail address validation
|
||||||
* [X] Support for common mail header field generation (Message-ID, Date, Bulk-Precedence, Priority, etc.)
|
* [X] Support for common mail header field generation (Message-ID, Date, Bulk-Precedence, Priority, etc.)
|
||||||
* [X] Concurrency-safe reusing the same SMTP connection to send multiple mails
|
* [X] Concurrency-safe reusing the same SMTP connection to send multiple mails
|
||||||
* [X] Support for attachments and inline embeds (from file system, `io.Reader` or `embed.FS`)
|
* [X] Support for attachments and inline embeds (from file system, `io.Reader`, `embed.FS` or `fs.FS`)
|
||||||
* [X] Support for different encodings
|
* [X] Support for different encodings
|
||||||
* [X] Middleware support for 3rd-party libraries to alter mail messages
|
* [X] Middleware support for 3rd-party libraries to alter mail messages
|
||||||
* [X] Support sending mails via a local sendmail command
|
* [X] Support sending mails via a local sendmail command
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<!--
|
<!--
|
||||||
SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||||
|
|
||||||
SPDX-License-Identifier: CC0-1.0
|
SPDX-License-Identifier: MIT
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# Security Policy
|
# Security Policy
|
||||||
|
|
22
auth.go
22
auth.go
|
@ -136,6 +136,21 @@ const (
|
||||||
//
|
//
|
||||||
// https://datatracker.ietf.org/doc/html/rfc7677
|
// https://datatracker.ietf.org/doc/html/rfc7677
|
||||||
SMTPAuthSCRAMSHA256PLUS SMTPAuthType = "SCRAM-SHA-256-PLUS"
|
SMTPAuthSCRAMSHA256PLUS SMTPAuthType = "SCRAM-SHA-256-PLUS"
|
||||||
|
|
||||||
|
// SMTPAuthAutoDiscover is a mechanism that dynamically discovers all authentication mechanisms
|
||||||
|
// supported by the SMTP server and selects the strongest available one.
|
||||||
|
//
|
||||||
|
// This type simplifies authentication by automatically negotiating the most secure mechanism
|
||||||
|
// offered by the server, based on a predefined security ranking. For instance, mechanisms like
|
||||||
|
// SCRAM-SHA-256(-PLUS) or XOAUTH2 are prioritized over weaker mechanisms such as CRAM-MD5 or PLAIN.
|
||||||
|
//
|
||||||
|
// The negotiation process ensures that mechanisms requiring additional capabilities (e.g.,
|
||||||
|
// SCRAM-SHA-X-PLUS with TLS channel binding) are only selected when the necessary prerequisites
|
||||||
|
// are in place, such as an active TLS-secured connection.
|
||||||
|
//
|
||||||
|
// By automating mechanism selection, SMTPAuthAutoDiscover minimizes configuration effort while
|
||||||
|
// maximizing security and compatibility with a wide range of SMTP servers.
|
||||||
|
SMTPAuthAutoDiscover SMTPAuthType = "AUTODISCOVER"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SMTP Auth related static errors
|
// SMTP Auth related static errors
|
||||||
|
@ -170,12 +185,19 @@ var (
|
||||||
// ErrSCRAMSHA256PLUSAuthNotSupported is returned when the server does not support the "SCRAM-SHA-256-PLUS" SMTP
|
// ErrSCRAMSHA256PLUSAuthNotSupported is returned when the server does not support the "SCRAM-SHA-256-PLUS" SMTP
|
||||||
// authentication type.
|
// authentication type.
|
||||||
ErrSCRAMSHA256PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256-PLUS")
|
ErrSCRAMSHA256PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256-PLUS")
|
||||||
|
|
||||||
|
// ErrNoSupportedAuthDiscovered is returned when the SMTP Auth AutoDiscover process fails to identify
|
||||||
|
// any supported authentication mechanisms offered by the server.
|
||||||
|
ErrNoSupportedAuthDiscovered = errors.New("SMTP Auth autodiscover was not able to detect a supported " +
|
||||||
|
"authentication mechanism")
|
||||||
)
|
)
|
||||||
|
|
||||||
// UnmarshalString satisfies the fig.StringUnmarshaler interface for the SMTPAuthType type
|
// UnmarshalString satisfies the fig.StringUnmarshaler interface for the SMTPAuthType type
|
||||||
// https://pkg.go.dev/github.com/kkyr/fig#StringUnmarshaler
|
// https://pkg.go.dev/github.com/kkyr/fig#StringUnmarshaler
|
||||||
func (sa *SMTPAuthType) UnmarshalString(value string) error {
|
func (sa *SMTPAuthType) UnmarshalString(value string) error {
|
||||||
switch strings.ToLower(value) {
|
switch strings.ToLower(value) {
|
||||||
|
case "auto", "autodiscover", "autodiscovery":
|
||||||
|
*sa = SMTPAuthAutoDiscover
|
||||||
case "cram-md5", "crammd5", "cram":
|
case "cram-md5", "crammd5", "cram":
|
||||||
*sa = SMTPAuthCramMD5
|
*sa = SMTPAuthCramMD5
|
||||||
case "custom":
|
case "custom":
|
||||||
|
|
|
@ -12,6 +12,9 @@ func TestSMTPAuthType_UnmarshalString(t *testing.T) {
|
||||||
authString string
|
authString string
|
||||||
expected SMTPAuthType
|
expected SMTPAuthType
|
||||||
}{
|
}{
|
||||||
|
{"AUTODISCOVER: auto", "auto", SMTPAuthAutoDiscover},
|
||||||
|
{"AUTODISCOVER: autodiscover", "autodiscover", SMTPAuthAutoDiscover},
|
||||||
|
{"AUTODISCOVER: autodiscovery", "autodiscovery", SMTPAuthAutoDiscover},
|
||||||
{"CRAM-MD5: cram-md5", "cram-md5", SMTPAuthCramMD5},
|
{"CRAM-MD5: cram-md5", "cram-md5", SMTPAuthCramMD5},
|
||||||
{"CRAM-MD5: crammd5", "crammd5", SMTPAuthCramMD5},
|
{"CRAM-MD5: crammd5", "crammd5", SMTPAuthCramMD5},
|
||||||
{"CRAM-MD5: cram", "cram", SMTPAuthCramMD5},
|
{"CRAM-MD5: cram", "cram", SMTPAuthCramMD5},
|
||||||
|
|
374
client.go
374
client.go
|
@ -142,9 +142,6 @@ type (
|
||||||
// host is the hostname of the SMTP server we are connecting to.
|
// host is the hostname of the SMTP server we are connecting to.
|
||||||
host string
|
host string
|
||||||
|
|
||||||
// isEncrypted indicates wether the Client connection is encrypted or not.
|
|
||||||
isEncrypted bool
|
|
||||||
|
|
||||||
// logAuthData indicates whether authentication-related data should be logged.
|
// logAuthData indicates whether authentication-related data should be logged.
|
||||||
logAuthData bool
|
logAuthData bool
|
||||||
|
|
||||||
|
@ -170,6 +167,9 @@ type (
|
||||||
// requestDSN indicates wether we want to request DSN (Delivery Status Notifications).
|
// requestDSN indicates wether we want to request DSN (Delivery Status Notifications).
|
||||||
requestDSN bool
|
requestDSN bool
|
||||||
|
|
||||||
|
// sendMutex is used to synchronize access to shared resources during the dial and send methods.
|
||||||
|
sendMutex sync.Mutex
|
||||||
|
|
||||||
// smtpAuth is the authentication type that is used to authenticate the user with SMTP server. It
|
// smtpAuth is the authentication type that is used to authenticate the user with SMTP server. It
|
||||||
// satisfies the smtp.Auth interface.
|
// satisfies the smtp.Auth interface.
|
||||||
//
|
//
|
||||||
|
@ -803,7 +803,7 @@ func (c *Client) SetSSLPort(ssl bool, fallback bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDebugLog sets or overrides whether the Client is using debug logging. The debug logger will log incoming
|
// SetDebugLog sets or overrides whether the Client is using debug logging. The debug logger will log incoming
|
||||||
// and outgoing communication between the Client and the server to os.Stderr.
|
// and outgoing communication between the client and the server to log.Logger that is defined on the Client.
|
||||||
//
|
//
|
||||||
// Note: The SMTP communication might include unencrypted authentication data, depending on whether you are using
|
// Note: The SMTP communication might include unencrypted authentication data, depending on whether you are using
|
||||||
// SMTP authentication and the type of authentication mechanism. This could pose a data protection risk. Use
|
// SMTP authentication and the type of authentication mechanism. This could pose a data protection risk. Use
|
||||||
|
@ -812,9 +812,26 @@ func (c *Client) SetSSLPort(ssl bool, fallback bool) {
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// - val: A boolean value indicating whether to enable (true) or disable (false) debug logging.
|
// - val: A boolean value indicating whether to enable (true) or disable (false) debug logging.
|
||||||
func (c *Client) SetDebugLog(val bool) {
|
func (c *Client) SetDebugLog(val bool) {
|
||||||
|
c.SetDebugLogWithSMTPClient(c.smtpClient, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDebugLogWithSMTPClient sets or overrides whether the provided smtp.Client is using debug logging.
|
||||||
|
// The debug logger will log incoming and outgoing communication between the client and the server to
|
||||||
|
// log.Logger that is defined on the Client.
|
||||||
|
//
|
||||||
|
// Note: The SMTP communication might include unencrypted authentication data, depending on whether you are using
|
||||||
|
// SMTP authentication and the type of authentication mechanism. This could pose a data protection risk. Use
|
||||||
|
// debug logging with caution.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - client: A pointer to the smtp.Client that handles the connection to the server.
|
||||||
|
// - val: A boolean value indicating whether to enable (true) or disable (false) debug logging.
|
||||||
|
func (c *Client) SetDebugLogWithSMTPClient(client *smtp.Client, val bool) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
c.useDebugLog = val
|
c.useDebugLog = val
|
||||||
if c.smtpClient != nil {
|
if client != nil {
|
||||||
c.smtpClient.SetDebugLog(val)
|
client.SetDebugLog(val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -827,9 +844,24 @@ func (c *Client) SetDebugLog(val bool) {
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// - logger: A logger that satisfies the log.Logger interface to be set for the Client.
|
// - logger: A logger that satisfies the log.Logger interface to be set for the Client.
|
||||||
func (c *Client) SetLogger(logger log.Logger) {
|
func (c *Client) SetLogger(logger log.Logger) {
|
||||||
|
c.SetLoggerWithSMTPClient(c.smtpClient, logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLoggerWithSMTPClient sets or overrides the custom logger currently used by the provided smtp.Client.
|
||||||
|
// The logger must satisfy the log.Logger interface and is only utilized when debug logging is enabled on
|
||||||
|
// the provided smtp.Client.
|
||||||
|
//
|
||||||
|
// By default, log.Stdlog is used if no custom logger is provided.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - client: A pointer to the smtp.Client that handles the connection to the server.
|
||||||
|
// - logger: A logger that satisfies the log.Logger interface to be set for the Client.
|
||||||
|
func (c *Client) SetLoggerWithSMTPClient(client *smtp.Client, logger log.Logger) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
c.logger = logger
|
c.logger = logger
|
||||||
if c.smtpClient != nil {
|
if client != nil {
|
||||||
c.smtpClient.SetLogger(logger)
|
client.SetLogger(logger)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -923,67 +955,91 @@ func (c *Client) SetLogAuthData(logAuth bool) {
|
||||||
// SMTP server.
|
// SMTP server.
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// - dialCtx: The context.Context used to control the connection timeout and cancellation.
|
// - ctxDial: The context.Context used to control the connection timeout and cancellation.
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - An error if the connection to the SMTP server fails or any subsequent command fails.
|
// - An error if the connection to the SMTP server fails or any subsequent command fails.
|
||||||
func (c *Client) DialWithContext(dialCtx context.Context) error {
|
func (c *Client) DialWithContext(ctxDial context.Context) error {
|
||||||
c.mutex.Lock()
|
client, err := c.DialToSMTPClientWithContext(ctxDial)
|
||||||
defer c.mutex.Unlock()
|
|
||||||
|
|
||||||
ctx, cancel := context.WithDeadline(dialCtx, time.Now().Add(c.connTimeout))
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if c.dialContextFunc == nil {
|
|
||||||
netDialer := net.Dialer{}
|
|
||||||
c.dialContextFunc = netDialer.DialContext
|
|
||||||
|
|
||||||
if c.useSSL {
|
|
||||||
tlsDialer := tls.Dialer{NetDialer: &netDialer, Config: c.tlsconfig}
|
|
||||||
c.isEncrypted = true
|
|
||||||
c.dialContextFunc = tlsDialer.DialContext
|
|
||||||
}
|
|
||||||
}
|
|
||||||
connection, err := c.dialContextFunc(ctx, "tcp", c.ServerAddr())
|
|
||||||
if err != nil && c.fallbackPort != 0 {
|
|
||||||
// TODO: should we somehow log or append the previous error?
|
|
||||||
connection, err = c.dialContextFunc(ctx, "tcp", c.serverFallbackAddr())
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
c.mutex.Lock()
|
||||||
|
c.smtpClient = client
|
||||||
|
c.mutex.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialToSMTPClientWithContext establishes and configures a smtp.Client connection using
|
||||||
|
// the provided context.
|
||||||
|
//
|
||||||
|
// This function uses the provided context to manage the connection deadline and cancellation.
|
||||||
|
// It dials the SMTP server using the Client's configured DialContextFunc or a default dialer.
|
||||||
|
// If SSL is enabled, it uses a TLS connection. After successfully connecting, it initializes
|
||||||
|
// an smtp.Client, sends the HELO/EHLO command, and optionally performs STARTTLS and SMTP AUTH
|
||||||
|
// based on the Client's configuration. Debug and authentication logging are enabled if
|
||||||
|
// configured.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - ctxDial: The context used to control the connection timeout and cancellation.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A pointer to the initialized smtp.Client.
|
||||||
|
// - An error if the connection fails, the smtp.Client cannot be created, or any subsequent commands fail.
|
||||||
|
func (c *Client) DialToSMTPClientWithContext(ctxDial context.Context) (*smtp.Client, error) {
|
||||||
|
c.mutex.RLock()
|
||||||
|
defer c.mutex.RUnlock()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithDeadline(ctxDial, time.Now().Add(c.connTimeout))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
isEncrypted := false
|
||||||
|
dialContextFunc := c.dialContextFunc
|
||||||
|
if c.dialContextFunc == nil {
|
||||||
|
netDialer := net.Dialer{}
|
||||||
|
dialContextFunc = netDialer.DialContext
|
||||||
|
if c.useSSL {
|
||||||
|
tlsDialer := tls.Dialer{NetDialer: &netDialer, Config: c.tlsconfig}
|
||||||
|
isEncrypted = true
|
||||||
|
dialContextFunc = tlsDialer.DialContext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connection, err := dialContextFunc(ctx, "tcp", c.ServerAddr())
|
||||||
|
if err != nil && c.fallbackPort != 0 {
|
||||||
|
// TODO: should we somehow log or append the previous error?
|
||||||
|
connection, err = dialContextFunc(ctx, "tcp", c.serverFallbackAddr())
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
client, err := smtp.NewClient(connection, c.host)
|
client, err := smtp.NewClient(connection, c.host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if client == nil {
|
|
||||||
return fmt.Errorf("SMTP client is nil")
|
|
||||||
}
|
|
||||||
c.smtpClient = client
|
|
||||||
|
|
||||||
if c.logger != nil {
|
if c.logger != nil {
|
||||||
c.smtpClient.SetLogger(c.logger)
|
client.SetLogger(c.logger)
|
||||||
}
|
}
|
||||||
if c.useDebugLog {
|
if c.useDebugLog {
|
||||||
c.smtpClient.SetDebugLog(true)
|
client.SetDebugLog(true)
|
||||||
}
|
}
|
||||||
if c.logAuthData {
|
if c.logAuthData {
|
||||||
c.smtpClient.SetLogAuthData()
|
client.SetLogAuthData()
|
||||||
}
|
}
|
||||||
if err = c.smtpClient.Hello(c.helo); err != nil {
|
if err = client.Hello(c.helo); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = c.tls(); err != nil {
|
if err = c.tls(client, &isEncrypted); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = c.auth(); err != nil {
|
if err = c.auth(client, isEncrypted); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close terminates the connection to the SMTP server, returning an error if the disconnection
|
// Close terminates the connection to the SMTP server, returning an error if the disconnection
|
||||||
|
@ -996,10 +1052,27 @@ func (c *Client) DialWithContext(dialCtx context.Context) error {
|
||||||
// Returns:
|
// Returns:
|
||||||
// - An error if the disconnection fails; otherwise, returns nil.
|
// - An error if the disconnection fails; otherwise, returns nil.
|
||||||
func (c *Client) Close() error {
|
func (c *Client) Close() error {
|
||||||
if !c.smtpClient.HasConnection() {
|
return c.CloseWithSMTPClient(c.smtpClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseWithSMTPClient terminates the connection of the provided smtp.Client to the SMTP server,
|
||||||
|
// returning an error if the disconnection fails. If the connection is already closed, this
|
||||||
|
// method is a no-op and disregards any error.
|
||||||
|
//
|
||||||
|
// This function checks if the smtp.Client connection is active. If not, it simply returns
|
||||||
|
// without any action. If the connection is active, it attempts to gracefully close the
|
||||||
|
// connection using the Quit method.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - client: A pointer to the smtp.Client that handles the connection to the server.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - An error if the disconnection fails; otherwise, returns nil.
|
||||||
|
func (c *Client) CloseWithSMTPClient(client *smtp.Client) error {
|
||||||
|
if client == nil || !client.HasConnection() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := c.smtpClient.Quit(); err != nil {
|
if err := client.Quit(); err != nil {
|
||||||
return fmt.Errorf("failed to close SMTP client: %w", err)
|
return fmt.Errorf("failed to close SMTP client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1013,12 +1086,29 @@ func (c *Client) Close() error {
|
||||||
// the command fails, an error is returned.
|
// the command fails, an error is returned.
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - An error if the connection check fails or if sending the RSET command fails; otherwise, returns nil.
|
// - An error if the connection check fails or if sending the RSET command fails;
|
||||||
|
// otherwise, returns nil.
|
||||||
func (c *Client) Reset() error {
|
func (c *Client) Reset() error {
|
||||||
if err := c.checkConn(); err != nil {
|
return c.ResetWithSMTPClient(c.smtpClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetWithSMTPClient sends an SMTP RSET command to the provided smtp.Client, to reset
|
||||||
|
// the state of the current SMTP session.
|
||||||
|
//
|
||||||
|
// This method checks the connection to the SMTP server and, if the connection is valid,
|
||||||
|
// it sends an RSET command to reset the session state. If the connection is invalid or
|
||||||
|
// the command fails, an error is returned.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - client: A pointer to the smtp.Client that handles the connection to the server.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - An error if the connection check fails or if sending the RSET command fails; otherwise, returns nil.
|
||||||
|
func (c *Client) ResetWithSMTPClient(client *smtp.Client) error {
|
||||||
|
if err := c.checkConn(client); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := c.smtpClient.Reset(); err != nil {
|
if err := client.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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1058,22 +1148,47 @@ func (c *Client) DialAndSend(messages ...*Msg) error {
|
||||||
// - An error if the connection fails, if sending the messages fails, or if closing the
|
// - An error if the connection fails, if sending the messages fails, or if closing the
|
||||||
// connection fails; otherwise, returns nil.
|
// connection fails; otherwise, returns nil.
|
||||||
func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) error {
|
func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) error {
|
||||||
if err := c.DialWithContext(ctx); err != nil {
|
client, err := c.DialToSMTPClientWithContext(ctx)
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("dial failed: %w", err)
|
return fmt.Errorf("dial failed: %w", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = c.Close()
|
_ = c.CloseWithSMTPClient(client)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err := c.Send(messages...); err != nil {
|
if err = c.SendWithSMTPClient(client, messages...); 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.CloseWithSMTPClient(client); err != nil {
|
||||||
return fmt.Errorf("failed to close connection: %w", err)
|
return fmt.Errorf("failed to close connection: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send attempts to send one or more Msg using the SMTP client that is assigned to the Client.
|
||||||
|
// If the Client has no active connection to the server, Send will fail with an error. For
|
||||||
|
// each of the provided Msg, it will associate a SendError with the Msg in case of a
|
||||||
|
// transmission or delivery error.
|
||||||
|
//
|
||||||
|
// This method first checks for an active connection to the SMTP server. If the connection is
|
||||||
|
// not valid, it returns a SendError. It then iterates over the provided messages, attempting
|
||||||
|
// to send each one. If an error occurs during sending, the method records the error and
|
||||||
|
// associates it with the corresponding Msg. If multiple errors are encountered, it aggregates
|
||||||
|
// them into a single SendError to be returned.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - client: A pointer to the smtp.Client that holds the connection to the SMTP server
|
||||||
|
// - messages: A variadic list of pointers to Msg objects to be sent.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - An error that represents the sending result, which may include multiple SendErrors if
|
||||||
|
// any occurred; otherwise, returns nil.
|
||||||
|
func (c *Client) Send(messages ...*Msg) (returnErr error) {
|
||||||
|
c.sendMutex.Lock()
|
||||||
|
defer c.sendMutex.Unlock()
|
||||||
|
return c.SendWithSMTPClient(c.smtpClient, messages...)
|
||||||
|
}
|
||||||
|
|
||||||
// auth attempts to authenticate the client using SMTP AUTH mechanisms. It checks the connection,
|
// auth attempts to authenticate the client using SMTP AUTH mechanisms. It checks the connection,
|
||||||
// determines the supported authentication methods, and applies the appropriate authentication
|
// determines the supported authentication methods, and applies the appropriate authentication
|
||||||
// type. An error is returned if authentication fails.
|
// type. An error is returned if authentication fails.
|
||||||
|
@ -1093,85 +1208,125 @@ func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) e
|
||||||
// Returns:
|
// Returns:
|
||||||
// - An error if the connection check fails, if no supported authentication method is found,
|
// - An error if the connection check fails, if no supported authentication method is found,
|
||||||
// or if the authentication process fails.
|
// or if the authentication process fails.
|
||||||
func (c *Client) auth() error {
|
func (c *Client) auth(client *smtp.Client, isEnc bool) error {
|
||||||
|
var smtpAuth smtp.Auth
|
||||||
if c.smtpAuth == nil && c.smtpAuthType != SMTPAuthNoAuth {
|
if c.smtpAuth == nil && c.smtpAuthType != SMTPAuthNoAuth {
|
||||||
hasSMTPAuth, smtpAuthType := c.smtpClient.Extension("AUTH")
|
hasSMTPAuth, smtpAuthType := client.Extension("AUTH")
|
||||||
if !hasSMTPAuth {
|
if !hasSMTPAuth {
|
||||||
return fmt.Errorf("server does not support SMTP AUTH")
|
return fmt.Errorf("server does not support SMTP AUTH")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch c.smtpAuthType {
|
authType := c.smtpAuthType
|
||||||
|
if c.smtpAuthType == SMTPAuthAutoDiscover {
|
||||||
|
discoveredType, err := c.authTypeAutoDiscover(smtpAuthType, isEnc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
authType = discoveredType
|
||||||
|
}
|
||||||
|
|
||||||
|
switch authType {
|
||||||
case SMTPAuthPlain:
|
case SMTPAuthPlain:
|
||||||
if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) {
|
if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) {
|
||||||
return ErrPlainAuthNotSupported
|
return ErrPlainAuthNotSupported
|
||||||
}
|
}
|
||||||
c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host, false)
|
smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host, false)
|
||||||
case SMTPAuthPlainNoEnc:
|
case SMTPAuthPlainNoEnc:
|
||||||
if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) {
|
if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) {
|
||||||
return ErrPlainAuthNotSupported
|
return ErrPlainAuthNotSupported
|
||||||
}
|
}
|
||||||
c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host, true)
|
smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host, true)
|
||||||
case SMTPAuthLogin:
|
case SMTPAuthLogin:
|
||||||
if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) {
|
if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) {
|
||||||
return ErrLoginAuthNotSupported
|
return ErrLoginAuthNotSupported
|
||||||
}
|
}
|
||||||
c.smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host, false)
|
smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host, false)
|
||||||
case SMTPAuthLoginNoEnc:
|
case SMTPAuthLoginNoEnc:
|
||||||
if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) {
|
if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) {
|
||||||
return ErrLoginAuthNotSupported
|
return ErrLoginAuthNotSupported
|
||||||
}
|
}
|
||||||
c.smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host, true)
|
smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host, true)
|
||||||
case SMTPAuthCramMD5:
|
case SMTPAuthCramMD5:
|
||||||
if !strings.Contains(smtpAuthType, string(SMTPAuthCramMD5)) {
|
if !strings.Contains(smtpAuthType, string(SMTPAuthCramMD5)) {
|
||||||
return ErrCramMD5AuthNotSupported
|
return ErrCramMD5AuthNotSupported
|
||||||
}
|
}
|
||||||
c.smtpAuth = smtp.CRAMMD5Auth(c.user, c.pass)
|
smtpAuth = smtp.CRAMMD5Auth(c.user, c.pass)
|
||||||
case SMTPAuthXOAUTH2:
|
case SMTPAuthXOAUTH2:
|
||||||
if !strings.Contains(smtpAuthType, string(SMTPAuthXOAUTH2)) {
|
if !strings.Contains(smtpAuthType, string(SMTPAuthXOAUTH2)) {
|
||||||
return ErrXOauth2AuthNotSupported
|
return ErrXOauth2AuthNotSupported
|
||||||
}
|
}
|
||||||
c.smtpAuth = smtp.XOAuth2Auth(c.user, c.pass)
|
smtpAuth = smtp.XOAuth2Auth(c.user, c.pass)
|
||||||
case SMTPAuthSCRAMSHA1:
|
case SMTPAuthSCRAMSHA1:
|
||||||
if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA1)) {
|
if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA1)) {
|
||||||
return ErrSCRAMSHA1AuthNotSupported
|
return ErrSCRAMSHA1AuthNotSupported
|
||||||
}
|
}
|
||||||
c.smtpAuth = smtp.ScramSHA1Auth(c.user, c.pass)
|
smtpAuth = smtp.ScramSHA1Auth(c.user, c.pass)
|
||||||
case SMTPAuthSCRAMSHA256:
|
case SMTPAuthSCRAMSHA256:
|
||||||
if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA256)) {
|
if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA256)) {
|
||||||
return ErrSCRAMSHA256AuthNotSupported
|
return ErrSCRAMSHA256AuthNotSupported
|
||||||
}
|
}
|
||||||
c.smtpAuth = smtp.ScramSHA256Auth(c.user, c.pass)
|
smtpAuth = smtp.ScramSHA256Auth(c.user, c.pass)
|
||||||
case SMTPAuthSCRAMSHA1PLUS:
|
case SMTPAuthSCRAMSHA1PLUS:
|
||||||
if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA1PLUS)) {
|
if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA1PLUS)) {
|
||||||
return ErrSCRAMSHA1PLUSAuthNotSupported
|
return ErrSCRAMSHA1PLUSAuthNotSupported
|
||||||
}
|
}
|
||||||
tlsConnState, err := c.smtpClient.GetTLSConnectionState()
|
tlsConnState, err := client.GetTLSConnectionState()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.smtpAuth = smtp.ScramSHA1PlusAuth(c.user, c.pass, tlsConnState)
|
smtpAuth = smtp.ScramSHA1PlusAuth(c.user, c.pass, tlsConnState)
|
||||||
case SMTPAuthSCRAMSHA256PLUS:
|
case SMTPAuthSCRAMSHA256PLUS:
|
||||||
if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA256PLUS)) {
|
if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA256PLUS)) {
|
||||||
return ErrSCRAMSHA256PLUSAuthNotSupported
|
return ErrSCRAMSHA256PLUSAuthNotSupported
|
||||||
}
|
}
|
||||||
tlsConnState, err := c.smtpClient.GetTLSConnectionState()
|
tlsConnState, err := client.GetTLSConnectionState()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.smtpAuth = smtp.ScramSHA256PlusAuth(c.user, c.pass, tlsConnState)
|
smtpAuth = smtp.ScramSHA256PlusAuth(c.user, c.pass, tlsConnState)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported SMTP AUTH type %q", c.smtpAuthType)
|
return fmt.Errorf("unsupported SMTP AUTH type %q", c.smtpAuthType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.smtpAuth != nil {
|
if smtpAuth != nil {
|
||||||
if err := c.smtpClient.Auth(c.smtpAuth); err != nil {
|
if err := client.Auth(smtpAuth); err != nil {
|
||||||
return fmt.Errorf("SMTP AUTH failed: %w", err)
|
return fmt.Errorf("SMTP AUTH failed: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) authTypeAutoDiscover(supported string, isEnc bool) (SMTPAuthType, error) {
|
||||||
|
if supported == "" {
|
||||||
|
return "", ErrNoSupportedAuthDiscovered
|
||||||
|
}
|
||||||
|
preferList := []SMTPAuthType{
|
||||||
|
SMTPAuthSCRAMSHA256PLUS, SMTPAuthSCRAMSHA256, SMTPAuthSCRAMSHA1PLUS, SMTPAuthSCRAMSHA1,
|
||||||
|
SMTPAuthXOAUTH2, SMTPAuthCramMD5, SMTPAuthPlain, SMTPAuthLogin,
|
||||||
|
}
|
||||||
|
if !isEnc {
|
||||||
|
preferList = []SMTPAuthType{SMTPAuthSCRAMSHA256, SMTPAuthSCRAMSHA1, SMTPAuthXOAUTH2, SMTPAuthCramMD5}
|
||||||
|
}
|
||||||
|
mechs := strings.Split(supported, " ")
|
||||||
|
|
||||||
|
for _, item := range preferList {
|
||||||
|
if sliceContains(mechs, string(item)) {
|
||||||
|
return item, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", ErrNoSupportedAuthDiscovered
|
||||||
|
}
|
||||||
|
|
||||||
|
func sliceContains(slice []string, item string) bool {
|
||||||
|
for _, s := range slice {
|
||||||
|
if s == item {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// sendSingleMsg sends out a single message and returns an error if the transmission or
|
// sendSingleMsg sends out a single message and returns an error if the transmission or
|
||||||
// delivery fails. It is invoked by the public Send methods.
|
// delivery fails. It is invoked by the public Send methods.
|
||||||
//
|
//
|
||||||
|
@ -1187,12 +1342,13 @@ func (c *Client) auth() error {
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - An error if any part of the sending process fails; otherwise, returns nil.
|
// - An error if any part of the sending process fails; otherwise, returns nil.
|
||||||
func (c *Client) sendSingleMsg(message *Msg) error {
|
func (c *Client) sendSingleMsg(client *smtp.Client, message *Msg) error {
|
||||||
c.mutex.Lock()
|
c.mutex.RLock()
|
||||||
defer c.mutex.Unlock()
|
defer c.mutex.RUnlock()
|
||||||
|
escSupport, _ := client.Extension("ENHANCEDSTATUSCODES")
|
||||||
|
|
||||||
if message.encoding == NoEncoding {
|
if message.encoding == NoEncoding {
|
||||||
if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok {
|
if ok, _ := client.Extension("8BITMIME"); !ok {
|
||||||
return &SendError{Reason: ErrNoUnencoded, isTemp: false, affectedMsg: message}
|
return &SendError{Reason: ErrNoUnencoded, isTemp: false, affectedMsg: message}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1200,28 +1356,31 @@ func (c *Client) sendSingleMsg(message *Msg) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &SendError{
|
return &SendError{
|
||||||
Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err),
|
Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err),
|
||||||
affectedMsg: message,
|
affectedMsg: message, errcode: errorCode(err),
|
||||||
|
enhancedStatusCode: enhancedStatusCode(err, escSupport),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rcpts, err := message.GetRecipients()
|
rcpts, err := message.GetRecipients()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &SendError{
|
return &SendError{
|
||||||
Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err),
|
Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err),
|
||||||
affectedMsg: message,
|
affectedMsg: message, errcode: errorCode(err),
|
||||||
|
enhancedStatusCode: enhancedStatusCode(err, escSupport),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.requestDSN {
|
if c.requestDSN {
|
||||||
if c.dsnReturnType != "" {
|
if c.dsnReturnType != "" {
|
||||||
c.smtpClient.SetDSNMailReturnOption(string(c.dsnReturnType))
|
client.SetDSNMailReturnOption(string(c.dsnReturnType))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err = c.smtpClient.Mail(from); err != nil {
|
if err = client.Mail(from); err != nil {
|
||||||
retError := &SendError{
|
retError := &SendError{
|
||||||
Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err),
|
Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err),
|
||||||
affectedMsg: message,
|
affectedMsg: message, errcode: errorCode(err),
|
||||||
|
enhancedStatusCode: enhancedStatusCode(err, escSupport),
|
||||||
}
|
}
|
||||||
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil {
|
if resetSendErr := client.Reset(); resetSendErr != nil {
|
||||||
retError.errlist = append(retError.errlist, resetSendErr)
|
retError.errlist = append(retError.errlist, resetSendErr)
|
||||||
}
|
}
|
||||||
return retError
|
return retError
|
||||||
|
@ -1231,48 +1390,54 @@ func (c *Client) sendSingleMsg(message *Msg) error {
|
||||||
rcptSendErr.errlist = make([]error, 0)
|
rcptSendErr.errlist = make([]error, 0)
|
||||||
rcptSendErr.rcpt = make([]string, 0)
|
rcptSendErr.rcpt = make([]string, 0)
|
||||||
rcptNotifyOpt := strings.Join(c.dsnRcptNotifyType, ",")
|
rcptNotifyOpt := strings.Join(c.dsnRcptNotifyType, ",")
|
||||||
c.smtpClient.SetDSNRcptNotifyOption(rcptNotifyOpt)
|
client.SetDSNRcptNotifyOption(rcptNotifyOpt)
|
||||||
for _, rcpt := range rcpts {
|
for _, rcpt := range rcpts {
|
||||||
if err = c.smtpClient.Rcpt(rcpt); err != nil {
|
if err = client.Rcpt(rcpt); err != nil {
|
||||||
rcptSendErr.Reason = ErrSMTPRcptTo
|
rcptSendErr.Reason = ErrSMTPRcptTo
|
||||||
rcptSendErr.errlist = append(rcptSendErr.errlist, err)
|
rcptSendErr.errlist = append(rcptSendErr.errlist, err)
|
||||||
rcptSendErr.rcpt = append(rcptSendErr.rcpt, rcpt)
|
rcptSendErr.rcpt = append(rcptSendErr.rcpt, rcpt)
|
||||||
rcptSendErr.isTemp = isTempError(err)
|
rcptSendErr.isTemp = isTempError(err)
|
||||||
|
rcptSendErr.errcode = errorCode(err)
|
||||||
|
rcptSendErr.enhancedStatusCode = enhancedStatusCode(err, escSupport)
|
||||||
hasError = true
|
hasError = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if hasError {
|
if hasError {
|
||||||
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil {
|
if resetSendErr := client.Reset(); resetSendErr != nil {
|
||||||
rcptSendErr.errlist = append(rcptSendErr.errlist, resetSendErr)
|
rcptSendErr.errlist = append(rcptSendErr.errlist, resetSendErr)
|
||||||
}
|
}
|
||||||
return rcptSendErr
|
return rcptSendErr
|
||||||
}
|
}
|
||||||
writer, err := c.smtpClient.Data()
|
writer, err := client.Data()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &SendError{
|
return &SendError{
|
||||||
Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err),
|
Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err),
|
||||||
affectedMsg: message,
|
affectedMsg: message, errcode: errorCode(err),
|
||||||
|
enhancedStatusCode: enhancedStatusCode(err, escSupport),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = message.WriteTo(writer)
|
_, err = message.WriteTo(writer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &SendError{
|
return &SendError{
|
||||||
Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err),
|
Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err),
|
||||||
affectedMsg: message,
|
affectedMsg: message, errcode: errorCode(err),
|
||||||
|
enhancedStatusCode: enhancedStatusCode(err, escSupport),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err = writer.Close(); err != nil {
|
if err = writer.Close(); err != nil {
|
||||||
return &SendError{
|
return &SendError{
|
||||||
Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err),
|
Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err),
|
||||||
affectedMsg: message,
|
affectedMsg: message, errcode: errorCode(err),
|
||||||
|
enhancedStatusCode: enhancedStatusCode(err, escSupport),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
message.isDelivered = true
|
message.isDelivered = true
|
||||||
|
|
||||||
if err = c.Reset(); err != nil {
|
if err = c.ResetWithSMTPClient(client); err != nil {
|
||||||
return &SendError{
|
return &SendError{
|
||||||
Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err),
|
Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err),
|
||||||
affectedMsg: message,
|
affectedMsg: message, errcode: errorCode(err),
|
||||||
|
enhancedStatusCode: enhancedStatusCode(err, escSupport),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -1290,21 +1455,24 @@ func (c *Client) sendSingleMsg(message *Msg) error {
|
||||||
// Returns:
|
// Returns:
|
||||||
// - An error if there is no active connection, if the NOOP command fails, or if extending
|
// - An error if there is no active connection, if the NOOP command fails, or if extending
|
||||||
// the deadline fails; otherwise, returns nil.
|
// the deadline fails; otherwise, returns nil.
|
||||||
func (c *Client) checkConn() error {
|
func (c *Client) checkConn(client *smtp.Client) error {
|
||||||
if c.smtpClient == nil {
|
if client == nil {
|
||||||
return ErrNoActiveConnection
|
return ErrNoActiveConnection
|
||||||
}
|
}
|
||||||
if !c.smtpClient.HasConnection() {
|
if !client.HasConnection() {
|
||||||
return ErrNoActiveConnection
|
return ErrNoActiveConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
if !c.noNoop {
|
c.mutex.RLock()
|
||||||
if err := c.smtpClient.Noop(); err != nil {
|
noNoop := c.noNoop
|
||||||
|
c.mutex.RUnlock()
|
||||||
|
if !noNoop {
|
||||||
|
if err := client.Noop(); err != nil {
|
||||||
return ErrNoActiveConnection
|
return ErrNoActiveConnection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.smtpClient.UpdateDeadline(c.connTimeout); err != nil {
|
if err := client.UpdateDeadline(c.connTimeout); err != nil {
|
||||||
return ErrDeadlineExtendFailed
|
return ErrDeadlineExtendFailed
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -1351,10 +1519,10 @@ func (c *Client) setDefaultHelo() error {
|
||||||
// Returns:
|
// Returns:
|
||||||
// - An error if there is no active connection, if STARTTLS is required but not supported,
|
// - An error if there is no active connection, if STARTTLS is required but not supported,
|
||||||
// or if there are issues during the TLS handshake; otherwise, returns nil.
|
// or if there are issues during the TLS handshake; otherwise, returns nil.
|
||||||
func (c *Client) tls() error {
|
func (c *Client) tls(client *smtp.Client, isEnc *bool) error {
|
||||||
if !c.useSSL && c.tlspolicy != NoTLS {
|
if !c.useSSL && c.tlspolicy != NoTLS {
|
||||||
hasStartTLS := false
|
hasStartTLS := false
|
||||||
extension, _ := c.smtpClient.Extension("STARTTLS")
|
extension, _ := client.Extension("STARTTLS")
|
||||||
if c.tlspolicy == TLSMandatory {
|
if c.tlspolicy == TLSMandatory {
|
||||||
hasStartTLS = true
|
hasStartTLS = true
|
||||||
if !extension {
|
if !extension {
|
||||||
|
@ -1368,21 +1536,21 @@ func (c *Client) tls() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if hasStartTLS {
|
if hasStartTLS {
|
||||||
if err := c.smtpClient.StartTLS(c.tlsconfig); err != nil {
|
if err := client.StartTLS(c.tlsconfig); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tlsConnState, err := c.smtpClient.GetTLSConnectionState()
|
tlsConnState, err := client.GetTLSConnectionState()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, smtp.ErrNonTLSConnection):
|
case errors.Is(err, smtp.ErrNonTLSConnection):
|
||||||
c.isEncrypted = false
|
*isEnc = false
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("failed to get TLS connection state: %w", err)
|
return fmt.Errorf("failed to get TLS connection state: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.isEncrypted = tlsConnState.HandshakeComplete
|
*isEnc = tlsConnState.HandshakeComplete
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,16 @@
|
||||||
|
|
||||||
package mail
|
package mail
|
||||||
|
|
||||||
import "errors"
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
// Send attempts to send one or more Msg using the Client connection to the SMTP server.
|
"github.com/wneessen/go-mail/smtp"
|
||||||
// If the Client has no active connection to the server, Send will fail with an error. For each
|
)
|
||||||
// of the provided Msg, it will associate a SendError with the Msg in case of a transmission
|
|
||||||
// or delivery error.
|
// SendWithSMTPClient attempts to send one or more Msg using a provided smtp.Client with an
|
||||||
|
// established connection to the SMTP server. If the smtp.Client has no active connection to
|
||||||
|
// the server, SendWithSMTPClient will fail with an error. For each of the provided Msg, it
|
||||||
|
// will associate a SendError with the Msg in case of a transmission or delivery error.
|
||||||
//
|
//
|
||||||
// This method first checks for an active connection to the SMTP server. If the connection is
|
// This method first checks for an active connection to the SMTP server. If the connection is
|
||||||
// not valid, it returns a SendError. It then iterates over the provided messages, attempting
|
// not valid, it returns a SendError. It then iterates over the provided messages, attempting
|
||||||
|
@ -21,18 +25,26 @@ import "errors"
|
||||||
// them into a single SendError to be returned.
|
// them into a single SendError to be returned.
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
|
// - client: A pointer to the smtp.Client that holds the connection to the SMTP server
|
||||||
// - messages: A variadic list of pointers to Msg objects to be sent.
|
// - messages: A variadic list of pointers to Msg objects to be sent.
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - An error that represents the sending result, which may include multiple SendErrors if
|
// - An error that represents the sending result, which may include multiple SendErrors if
|
||||||
// any occurred; otherwise, returns nil.
|
// any occurred; otherwise, returns nil.
|
||||||
func (c *Client) Send(messages ...*Msg) error {
|
func (c *Client) SendWithSMTPClient(client *smtp.Client, messages ...*Msg) error {
|
||||||
if err := c.checkConn(); err != nil {
|
escSupport := false
|
||||||
return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
if client != nil {
|
||||||
|
escSupport, _ = client.Extension("ENHANCEDSTATUSCODES")
|
||||||
|
}
|
||||||
|
if err := c.checkConn(client); err != nil {
|
||||||
|
return &SendError{
|
||||||
|
Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err),
|
||||||
|
errcode: errorCode(err), enhancedStatusCode: enhancedStatusCode(err, escSupport),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var errs []*SendError
|
var errs []*SendError
|
||||||
for id, message := range messages {
|
for id, message := range messages {
|
||||||
if sendErr := c.sendSingleMsg(message); sendErr != nil {
|
if sendErr := c.sendSingleMsg(client, message); sendErr != nil {
|
||||||
messages[id].sendError = sendErr
|
messages[id].sendError = sendErr
|
||||||
|
|
||||||
var msgSendErr *SendError
|
var msgSendErr *SendError
|
||||||
|
@ -50,9 +62,11 @@ func (c *Client) Send(messages ...*Msg) error {
|
||||||
returnErr.rcpt = append(returnErr.rcpt, errs[i].rcpt...)
|
returnErr.rcpt = append(returnErr.rcpt, errs[i].rcpt...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We assume that the isTemp flag from the last error we received should be the
|
// We assume that the error codes and flags 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
|
returnErr.isTemp = errs[len(errs)-1].isTemp
|
||||||
|
returnErr.errcode = errs[len(errs)-1].errcode
|
||||||
|
returnErr.enhancedStatusCode = errs[len(errs)-1].enhancedStatusCode
|
||||||
|
|
||||||
return returnErr
|
return returnErr
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,26 +9,38 @@ package mail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"github.com/wneessen/go-mail/smtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Send attempts to send one or more Msg using the Client connection to the SMTP server.
|
// SendWithSMTPClient attempts to send one or more Msg using a provided smtp.Client with an
|
||||||
// If the Client has no active connection to the server, Send will fail with an error. For each
|
// established connection to the SMTP server. If the smtp.Client has no active connection to
|
||||||
// of the provided Msg, it will associate a SendError with the Msg in case of a transmission
|
// the server, SendWithSMTPClient will fail with an error. For each of the provided Msg, it
|
||||||
// or delivery error.
|
// will associate a SendError with the Msg in case of a transmission or delivery error.
|
||||||
//
|
//
|
||||||
// This method first checks for an active connection to the SMTP server. If the connection is
|
// This method first checks for an active connection to the SMTP server. If the connection is
|
||||||
// not valid, it returns an error wrapped in a SendError. It then iterates over the provided
|
// not valid, it returns a SendError. It then iterates over the provided messages, attempting
|
||||||
// messages, attempting to send each one. If an error occurs during sending, the method records
|
// to send each one. If an error occurs during sending, the method records the error and
|
||||||
// the error and associates it with the corresponding Msg.
|
// associates it with the corresponding Msg. If multiple errors are encountered, it aggregates
|
||||||
|
// them into a single SendError to be returned.
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
|
// - client: A pointer to the smtp.Client that holds the connection to the SMTP server
|
||||||
// - messages: A variadic list of pointers to Msg objects to be sent.
|
// - messages: A variadic list of pointers to Msg objects to be sent.
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - An error that aggregates any SendErrors encountered during the sending process; otherwise, returns nil.
|
// - An error that represents the sending result, which may include multiple SendErrors if
|
||||||
func (c *Client) Send(messages ...*Msg) (returnErr error) {
|
// any occurred; otherwise, returns nil.
|
||||||
if err := c.checkConn(); err != nil {
|
func (c *Client) SendWithSMTPClient(client *smtp.Client, messages ...*Msg) (returnErr error) {
|
||||||
returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
escSupport := false
|
||||||
|
if client != nil {
|
||||||
|
escSupport, _ = client.Extension("ENHANCEDSTATUSCODES")
|
||||||
|
}
|
||||||
|
if err := c.checkConn(client); err != nil {
|
||||||
|
returnErr = &SendError{
|
||||||
|
Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err),
|
||||||
|
errcode: errorCode(err), enhancedStatusCode: enhancedStatusCode(err, escSupport),
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +50,7 @@ func (c *Client) Send(messages ...*Msg) (returnErr error) {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for id, message := range messages {
|
for id, message := range messages {
|
||||||
if sendErr := c.sendSingleMsg(message); sendErr != nil {
|
if sendErr := c.sendSingleMsg(client, message); sendErr != nil {
|
||||||
messages[id].sendError = sendErr
|
messages[id].sendError = sendErr
|
||||||
errs = append(errs, sendErr)
|
errs = append(errs, sendErr)
|
||||||
}
|
}
|
||||||
|
|
367
client_test.go
367
client_test.go
|
@ -17,6 +17,7 @@ import (
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
@ -34,14 +35,15 @@ const (
|
||||||
TestServerProto = "tcp"
|
TestServerProto = "tcp"
|
||||||
// TestServerAddr is the address the simple SMTP test server listens on
|
// TestServerAddr is the address the simple SMTP test server listens on
|
||||||
TestServerAddr = "127.0.0.1"
|
TestServerAddr = "127.0.0.1"
|
||||||
// TestServerPortBase is the base port for the simple SMTP test server
|
|
||||||
TestServerPortBase = 12025
|
|
||||||
// TestSenderValid is a test sender email address considered valid for sending test emails.
|
// TestSenderValid is a test sender email address considered valid for sending test emails.
|
||||||
TestSenderValid = "valid-from@domain.tld"
|
TestSenderValid = "valid-from@domain.tld"
|
||||||
// TestRcptValid is a test recipient email address considered valid for sending test emails.
|
// TestRcptValid is a test recipient email address considered valid for sending test emails.
|
||||||
TestRcptValid = "valid-to@domain.tld"
|
TestRcptValid = "valid-to@domain.tld"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TestServerPortBase is the base port for the simple SMTP test server
|
||||||
|
var TestServerPortBase int32 = 30025
|
||||||
|
|
||||||
// PortAdder is an atomic counter used to increment port numbers for the test SMTP server instances.
|
// PortAdder is an atomic counter used to increment port numbers for the test SMTP server instances.
|
||||||
var PortAdder atomic.Int32
|
var PortAdder atomic.Int32
|
||||||
|
|
||||||
|
@ -98,6 +100,18 @@ type logData struct {
|
||||||
Lines []logLine `json:"lines"`
|
Lines []logLine `json:"lines"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
testPort := os.Getenv("TEST_BASEPORT")
|
||||||
|
if testPort == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if port, err := strconv.Atoi(testPort); err == nil {
|
||||||
|
if port <= 65000 && port > 1023 {
|
||||||
|
TestServerPortBase = int32(port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewClient(t *testing.T) {
|
func TestNewClient(t *testing.T) {
|
||||||
t.Run("create new Client", func(t *testing.T) {
|
t.Run("create new Client", func(t *testing.T) {
|
||||||
client, err := NewClient(DefaultHost)
|
client, err := NewClient(DefaultHost)
|
||||||
|
@ -1647,6 +1661,15 @@ func TestClient_Close(t *testing.T) {
|
||||||
t.Errorf("close was supposed to fail, but didn't")
|
t.Errorf("close was supposed to fail, but didn't")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
t.Run("close on a nil smtpclient should return nil", func(t *testing.T) {
|
||||||
|
client, err := NewClient(DefaultHost)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create new client: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.Close(); err != nil {
|
||||||
|
t.Errorf("failed to close the client: %s", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClient_DialWithContext(t *testing.T) {
|
func TestClient_DialWithContext(t *testing.T) {
|
||||||
|
@ -1749,11 +1772,8 @@ func TestClient_DialWithContext(t *testing.T) {
|
||||||
t.Errorf("failed to close the client: %s", err)
|
t.Errorf("failed to close the client: %s", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if client.smtpClient == nil {
|
if client.smtpClient != nil {
|
||||||
t.Errorf("client with invalid HELO should still have a smtp client, got nil")
|
t.Error("client with invalid HELO should not have a smtp client")
|
||||||
}
|
|
||||||
if !client.smtpClient.HasConnection() {
|
|
||||||
t.Errorf("client with invalid HELO should still have a smtp client connection, got nil")
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("fail on base port and fallback", func(t *testing.T) {
|
t.Run("fail on base port and fallback", func(t *testing.T) {
|
||||||
|
@ -1802,11 +1822,8 @@ func TestClient_DialWithContext(t *testing.T) {
|
||||||
if err = client.DialWithContext(ctxDial); err == nil {
|
if err = client.DialWithContext(ctxDial); err == nil {
|
||||||
t.Fatalf("connection was supposed to fail, but didn't")
|
t.Fatalf("connection was supposed to fail, but didn't")
|
||||||
}
|
}
|
||||||
if client.smtpClient == nil {
|
if client.smtpClient != nil {
|
||||||
t.Fatalf("client has no smtp client")
|
t.Fatalf("client is not supposed to have a smtp client")
|
||||||
}
|
|
||||||
if !client.smtpClient.HasConnection() {
|
|
||||||
t.Errorf("client has no connection")
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("connect with failing auth", func(t *testing.T) {
|
t.Run("connect with failing auth", func(t *testing.T) {
|
||||||
|
@ -2274,6 +2291,84 @@ func TestClient_DialAndSendWithContext(t *testing.T) {
|
||||||
t.Errorf("client was supposed to fail on dial")
|
t.Errorf("client was supposed to fail on dial")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
// https://github.com/wneessen/go-mail/issues/380
|
||||||
|
t.Run("concurrent sending via DialAndSendWithContext", func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
PortAdder.Add(1)
|
||||||
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
||||||
|
featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
||||||
|
go func() {
|
||||||
|
if err := simpleSMTPServer(ctx, t, &serverProps{
|
||||||
|
FeatureSet: featureSet,
|
||||||
|
ListenPort: serverPort,
|
||||||
|
}); err != nil {
|
||||||
|
t.Errorf("failed to start test server: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
time.Sleep(time.Millisecond * 30)
|
||||||
|
|
||||||
|
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create new client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
msg := testMessage(t)
|
||||||
|
msg.SetMessageIDWithValue("this.is.a.message.id")
|
||||||
|
|
||||||
|
ctxDial, cancelDial := context.WithTimeout(ctx, time.Minute)
|
||||||
|
defer cancelDial()
|
||||||
|
if goroutineErr := client.DialAndSendWithContext(ctxDial, msg); goroutineErr != nil {
|
||||||
|
t.Errorf("failed to dial and send message: %s", goroutineErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
})
|
||||||
|
// https://github.com/wneessen/go-mail/issues/385
|
||||||
|
t.Run("concurrent sending via DialAndSendWithContext on receiver func", func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
PortAdder.Add(1)
|
||||||
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
||||||
|
featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
||||||
|
go func() {
|
||||||
|
if err := simpleSMTPServer(ctx, t, &serverProps{
|
||||||
|
FeatureSet: featureSet,
|
||||||
|
ListenPort: serverPort,
|
||||||
|
}); err != nil {
|
||||||
|
t.Errorf("failed to start test server: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
time.Sleep(time.Millisecond * 30)
|
||||||
|
|
||||||
|
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create new client: %s", err)
|
||||||
|
}
|
||||||
|
sender := testSender{client}
|
||||||
|
|
||||||
|
ctxDial := context.Background()
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
msg := testMessage(t)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
if goroutineErr := sender.Send(ctxDial, msg); goroutineErr != nil {
|
||||||
|
t.Errorf("failed to send message: %s", goroutineErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClient_auth(t *testing.T) {
|
func TestClient_auth(t *testing.T) {
|
||||||
|
@ -2281,6 +2376,11 @@ func TestClient_auth(t *testing.T) {
|
||||||
name string
|
name string
|
||||||
authType SMTPAuthType
|
authType SMTPAuthType
|
||||||
}{
|
}{
|
||||||
|
{"LOGIN via AUTODISCOVER", SMTPAuthAutoDiscover},
|
||||||
|
{"PLAIN via AUTODISCOVER", SMTPAuthAutoDiscover},
|
||||||
|
{"SCRAM-SHA-1 via AUTODISCOVER", SMTPAuthAutoDiscover},
|
||||||
|
{"SCRAM-SHA-256 via AUTODISCOVER", SMTPAuthAutoDiscover},
|
||||||
|
{"XOAUTH2 via AUTODISCOVER", SMTPAuthAutoDiscover},
|
||||||
{"CRAM-MD5", SMTPAuthCramMD5},
|
{"CRAM-MD5", SMTPAuthCramMD5},
|
||||||
{"LOGIN", SMTPAuthLogin},
|
{"LOGIN", SMTPAuthLogin},
|
||||||
{"LOGIN-NOENC", SMTPAuthLoginNoEnc},
|
{"LOGIN-NOENC", SMTPAuthLoginNoEnc},
|
||||||
|
@ -2486,6 +2586,42 @@ func TestClient_auth(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClient_authTypeAutoDiscover(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
supported string
|
||||||
|
tls bool
|
||||||
|
expect SMTPAuthType
|
||||||
|
shouldFail bool
|
||||||
|
}{
|
||||||
|
{"LOGIN SCRAM-SHA-256 SCRAM-SHA-1 SCRAM-SHA-256-PLUS SCRAM-SHA-1-PLUS", true, SMTPAuthSCRAMSHA256PLUS, false},
|
||||||
|
{"LOGIN SCRAM-SHA-256 SCRAM-SHA-1 SCRAM-SHA-256-PLUS SCRAM-SHA-1-PLUS", false, SMTPAuthSCRAMSHA256, false},
|
||||||
|
{"LOGIN PLAIN SCRAM-SHA-1 SCRAM-SHA-1-PLUS", true, SMTPAuthSCRAMSHA1PLUS, false},
|
||||||
|
{"LOGIN PLAIN SCRAM-SHA-1 SCRAM-SHA-1-PLUS", false, SMTPAuthSCRAMSHA1, false},
|
||||||
|
{"LOGIN XOAUTH2 SCRAM-SHA-1-PLUS", false, SMTPAuthXOAUTH2, false},
|
||||||
|
{"PLAIN LOGIN CRAM-MD5", false, SMTPAuthCramMD5, false},
|
||||||
|
{"CRAM-MD5", false, SMTPAuthCramMD5, false},
|
||||||
|
{"PLAIN", true, SMTPAuthPlain, false},
|
||||||
|
{"LOGIN PLAIN", true, SMTPAuthPlain, false},
|
||||||
|
{"LOGIN PLAIN", false, "no secure mechanism", true},
|
||||||
|
{"", false, "supported list empty", true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run("AutoDiscover selects the strongest auth type: "+string(tt.expect), func(t *testing.T) {
|
||||||
|
client := &Client{smtpAuthType: SMTPAuthAutoDiscover}
|
||||||
|
authType, err := client.authTypeAutoDiscover(tt.supported, tt.tls)
|
||||||
|
if err != nil && !tt.shouldFail {
|
||||||
|
t.Fatalf("failed to auto discover auth type: %s", err)
|
||||||
|
}
|
||||||
|
if tt.shouldFail && err == nil {
|
||||||
|
t.Fatal("expected auto discover to fail")
|
||||||
|
}
|
||||||
|
if !tt.shouldFail && authType != tt.expect {
|
||||||
|
t.Errorf("expected strongest auth type: %s, got: %s", tt.expect, authType)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestClient_Send(t *testing.T) {
|
func TestClient_Send(t *testing.T) {
|
||||||
message := testMessage(t)
|
message := testMessage(t)
|
||||||
t.Run("connect and send email", func(t *testing.T) {
|
t.Run("connect and send email", func(t *testing.T) {
|
||||||
|
@ -2606,6 +2742,82 @@ func TestClient_Send(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClient_DialToSMTPClientWithContext(t *testing.T) {
|
||||||
|
t.Run("establish a new client connection", func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
PortAdder.Add(1)
|
||||||
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
||||||
|
featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
||||||
|
go func() {
|
||||||
|
if err := simpleSMTPServer(ctx, t, &serverProps{
|
||||||
|
FeatureSet: featureSet,
|
||||||
|
ListenPort: serverPort,
|
||||||
|
}); err != nil {
|
||||||
|
t.Errorf("failed to start test server: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
time.Sleep(time.Millisecond * 30)
|
||||||
|
|
||||||
|
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
|
||||||
|
t.Cleanup(cancelDial)
|
||||||
|
|
||||||
|
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create new client: %s", err)
|
||||||
|
}
|
||||||
|
smtpClient, err := client.DialToSMTPClientWithContext(ctxDial)
|
||||||
|
if err != nil {
|
||||||
|
var netErr net.Error
|
||||||
|
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||||
|
t.Skip("failed to connect to the test server due to timeout")
|
||||||
|
}
|
||||||
|
t.Fatalf("failed to connect to test server: %s", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := client.CloseWithSMTPClient(smtpClient); err != nil {
|
||||||
|
var netErr net.Error
|
||||||
|
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||||
|
t.Skip("failed to close the test server connection due to timeout")
|
||||||
|
}
|
||||||
|
t.Errorf("failed to close client: %s", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if smtpClient == nil {
|
||||||
|
t.Fatal("expected SMTP client, got nil")
|
||||||
|
}
|
||||||
|
if !smtpClient.HasConnection() {
|
||||||
|
t.Fatal("expected connection on smtp client")
|
||||||
|
}
|
||||||
|
if ok, _ := smtpClient.Extension("DSN"); !ok {
|
||||||
|
t.Error("expected DSN extension but it was not found")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("dial to SMTP server fails on first client writeFile", func(t *testing.T) {
|
||||||
|
var fake faker
|
||||||
|
fake.ReadWriter = struct {
|
||||||
|
io.Reader
|
||||||
|
io.Writer
|
||||||
|
}{
|
||||||
|
failReadWriteSeekCloser{},
|
||||||
|
failReadWriteSeekCloser{},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxDial, cancelDial := context.WithTimeout(context.Background(), time.Millisecond*500)
|
||||||
|
t.Cleanup(cancelDial)
|
||||||
|
|
||||||
|
client, err := NewClient(DefaultHost, WithDialContextFunc(getFakeDialFunc(fake)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create new client: %s", err)
|
||||||
|
}
|
||||||
|
_, err = client.DialToSMTPClientWithContext(ctxDial)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected connection to fake to fail")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestClient_sendSingleMsg(t *testing.T) {
|
func TestClient_sendSingleMsg(t *testing.T) {
|
||||||
t.Run("connect and send email", func(t *testing.T) {
|
t.Run("connect and send email", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
@ -2645,7 +2857,7 @@ func TestClient_sendSingleMsg(t *testing.T) {
|
||||||
t.Errorf("failed to close client: %s", err)
|
t.Errorf("failed to close client: %s", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err = client.sendSingleMsg(message); err != nil {
|
if err = client.sendSingleMsg(client.smtpClient, message); err != nil {
|
||||||
t.Errorf("failed to send message: %s", err)
|
t.Errorf("failed to send message: %s", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -2688,7 +2900,7 @@ func TestClient_sendSingleMsg(t *testing.T) {
|
||||||
t.Errorf("failed to close client: %s", err)
|
t.Errorf("failed to close client: %s", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err = client.sendSingleMsg(message); err == nil {
|
if err = client.sendSingleMsg(client.smtpClient, message); err == nil {
|
||||||
t.Errorf("client should have failed to send message")
|
t.Errorf("client should have failed to send message")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -2733,7 +2945,7 @@ func TestClient_sendSingleMsg(t *testing.T) {
|
||||||
t.Errorf("failed to close client: %s", err)
|
t.Errorf("failed to close client: %s", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err = client.sendSingleMsg(message); err == nil {
|
if err = client.sendSingleMsg(client.smtpClient, message); err == nil {
|
||||||
t.Errorf("client should have failed to send message")
|
t.Errorf("client should have failed to send message")
|
||||||
}
|
}
|
||||||
var sendErr *SendError
|
var sendErr *SendError
|
||||||
|
@ -2783,7 +2995,7 @@ func TestClient_sendSingleMsg(t *testing.T) {
|
||||||
t.Errorf("failed to close client: %s", err)
|
t.Errorf("failed to close client: %s", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err = client.sendSingleMsg(message); err == nil {
|
if err = client.sendSingleMsg(client.smtpClient, message); err == nil {
|
||||||
t.Errorf("client should have failed to send message")
|
t.Errorf("client should have failed to send message")
|
||||||
}
|
}
|
||||||
var sendErr *SendError
|
var sendErr *SendError
|
||||||
|
@ -2794,7 +3006,7 @@ func TestClient_sendSingleMsg(t *testing.T) {
|
||||||
t.Errorf("expected ErrGetSender, got %s", sendErr.Reason)
|
t.Errorf("expected ErrGetSender, got %s", sendErr.Reason)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("fail with no recepient addresses", func(t *testing.T) {
|
t.Run("fail with no recipient addresses", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
PortAdder.Add(1)
|
PortAdder.Add(1)
|
||||||
|
@ -2833,7 +3045,7 @@ func TestClient_sendSingleMsg(t *testing.T) {
|
||||||
t.Errorf("failed to close client: %s", err)
|
t.Errorf("failed to close client: %s", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err = client.sendSingleMsg(message); err == nil {
|
if err = client.sendSingleMsg(client.smtpClient, message); err == nil {
|
||||||
t.Errorf("client should have failed to send message")
|
t.Errorf("client should have failed to send message")
|
||||||
}
|
}
|
||||||
var sendErr *SendError
|
var sendErr *SendError
|
||||||
|
@ -2883,7 +3095,7 @@ func TestClient_sendSingleMsg(t *testing.T) {
|
||||||
t.Errorf("failed to close client: %s", err)
|
t.Errorf("failed to close client: %s", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err = client.sendSingleMsg(message); err != nil {
|
if err = client.sendSingleMsg(client.smtpClient, message); err != nil {
|
||||||
t.Errorf("failed to send message: %s", err)
|
t.Errorf("failed to send message: %s", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -2926,7 +3138,7 @@ func TestClient_sendSingleMsg(t *testing.T) {
|
||||||
t.Errorf("failed to close client: %s", err)
|
t.Errorf("failed to close client: %s", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err = client.sendSingleMsg(message); err == nil {
|
if err = client.sendSingleMsg(client.smtpClient, message); err == nil {
|
||||||
t.Errorf("client should have failed to send message")
|
t.Errorf("client should have failed to send message")
|
||||||
}
|
}
|
||||||
var sendErr *SendError
|
var sendErr *SendError
|
||||||
|
@ -2977,7 +3189,7 @@ func TestClient_sendSingleMsg(t *testing.T) {
|
||||||
t.Errorf("failed to close client: %s", err)
|
t.Errorf("failed to close client: %s", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err = client.sendSingleMsg(message); err == nil {
|
if err = client.sendSingleMsg(client.smtpClient, message); err == nil {
|
||||||
t.Errorf("client should have failed to send message")
|
t.Errorf("client should have failed to send message")
|
||||||
}
|
}
|
||||||
var sendErr *SendError
|
var sendErr *SendError
|
||||||
|
@ -3028,7 +3240,7 @@ func TestClient_sendSingleMsg(t *testing.T) {
|
||||||
t.Errorf("failed to close client: %s", err)
|
t.Errorf("failed to close client: %s", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err = client.sendSingleMsg(message); err == nil {
|
if err = client.sendSingleMsg(client.smtpClient, message); err == nil {
|
||||||
t.Errorf("client should have failed to send message")
|
t.Errorf("client should have failed to send message")
|
||||||
}
|
}
|
||||||
var sendErr *SendError
|
var sendErr *SendError
|
||||||
|
@ -3078,7 +3290,7 @@ func TestClient_sendSingleMsg(t *testing.T) {
|
||||||
t.Errorf("failed to close client: %s", err)
|
t.Errorf("failed to close client: %s", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err = client.sendSingleMsg(message); err == nil {
|
if err = client.sendSingleMsg(client.smtpClient, message); err == nil {
|
||||||
t.Errorf("client should have failed to send message")
|
t.Errorf("client should have failed to send message")
|
||||||
}
|
}
|
||||||
var sendErr *SendError
|
var sendErr *SendError
|
||||||
|
@ -3128,7 +3340,7 @@ func TestClient_sendSingleMsg(t *testing.T) {
|
||||||
t.Errorf("failed to close client: %s", err)
|
t.Errorf("failed to close client: %s", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err = client.sendSingleMsg(message); err == nil {
|
if err = client.sendSingleMsg(client.smtpClient, message); err == nil {
|
||||||
t.Errorf("client should have failed to send message")
|
t.Errorf("client should have failed to send message")
|
||||||
}
|
}
|
||||||
var sendErr *SendError
|
var sendErr *SendError
|
||||||
|
@ -3139,6 +3351,59 @@ func TestClient_sendSingleMsg(t *testing.T) {
|
||||||
t.Errorf("expected ErrSMTPDataClose, got %s", sendErr.Reason)
|
t.Errorf("expected ErrSMTPDataClose, got %s", sendErr.Reason)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
t.Run("error code and enhanced status code support", func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
PortAdder.Add(1)
|
||||||
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
||||||
|
featureSet := "250-ENHANCEDSTATUSCODES\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
||||||
|
go func() {
|
||||||
|
if err := simpleSMTPServer(ctx, t, &serverProps{
|
||||||
|
FailOnMailFrom: true,
|
||||||
|
FeatureSet: featureSet,
|
||||||
|
ListenPort: serverPort,
|
||||||
|
}); err != nil {
|
||||||
|
t.Errorf("failed to start test server: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
time.Sleep(time.Millisecond * 30)
|
||||||
|
|
||||||
|
message := testMessage(t)
|
||||||
|
|
||||||
|
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
|
||||||
|
t.Cleanup(cancelDial)
|
||||||
|
|
||||||
|
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create new client: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.DialWithContext(ctxDial); err != nil {
|
||||||
|
var netErr net.Error
|
||||||
|
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||||
|
t.Skip("failed to connect to the test server due to timeout")
|
||||||
|
}
|
||||||
|
t.Fatalf("failed to connect to test server: %s", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := client.Close(); err != nil {
|
||||||
|
t.Errorf("failed to close client: %s", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err = client.sendSingleMsg(client.smtpClient, message); err == nil {
|
||||||
|
t.Error("expected mail delivery to fail")
|
||||||
|
}
|
||||||
|
var sendErr *SendError
|
||||||
|
if !errors.As(err, &sendErr) {
|
||||||
|
t.Fatalf("expected SendError, got %s", err)
|
||||||
|
}
|
||||||
|
if sendErr.errcode != 500 {
|
||||||
|
t.Errorf("expected error code 500, got %d", sendErr.errcode)
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sendErr.enhancedStatusCode, "5.5.2") {
|
||||||
|
t.Errorf("expected enhanced status code 5.5.2, got %s", sendErr.enhancedStatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClient_checkConn(t *testing.T) {
|
func TestClient_checkConn(t *testing.T) {
|
||||||
|
@ -3178,7 +3443,7 @@ func TestClient_checkConn(t *testing.T) {
|
||||||
t.Errorf("failed to close client: %s", err)
|
t.Errorf("failed to close client: %s", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err = client.checkConn(); err != nil {
|
if err = client.checkConn(client.smtpClient); err != nil {
|
||||||
t.Errorf("failed to check connection: %s", err)
|
t.Errorf("failed to check connection: %s", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -3219,7 +3484,7 @@ func TestClient_checkConn(t *testing.T) {
|
||||||
t.Errorf("failed to close client: %s", err)
|
t.Errorf("failed to close client: %s", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err = client.checkConn(); err == nil {
|
if err = client.checkConn(client.smtpClient); err == nil {
|
||||||
t.Errorf("client should have failed on connection check")
|
t.Errorf("client should have failed on connection check")
|
||||||
}
|
}
|
||||||
if !errors.Is(err, ErrNoActiveConnection) {
|
if !errors.Is(err, ErrNoActiveConnection) {
|
||||||
|
@ -3231,7 +3496,7 @@ func TestClient_checkConn(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create new client: %s", err)
|
t.Fatalf("failed to create new client: %s", err)
|
||||||
}
|
}
|
||||||
if err = client.checkConn(); err == nil {
|
if err = client.checkConn(client.smtpClient); err == nil {
|
||||||
t.Errorf("client should have failed on connection check")
|
t.Errorf("client should have failed on connection check")
|
||||||
}
|
}
|
||||||
if !errors.Is(err, ErrNoActiveConnection) {
|
if !errors.Is(err, ErrNoActiveConnection) {
|
||||||
|
@ -3455,24 +3720,20 @@ func TestClient_XOAuth2OnFaker(t *testing.T) {
|
||||||
}
|
}
|
||||||
if err = c.DialWithContext(context.Background()); err == nil {
|
if err = c.DialWithContext(context.Background()); err == nil {
|
||||||
t.Fatal("expected dial error got nil")
|
t.Fatal("expected dial error got nil")
|
||||||
} else {
|
}
|
||||||
if !errors.Is(err, ErrXOauth2AuthNotSupported) {
|
if !errors.Is(err, ErrXOauth2AuthNotSupported) {
|
||||||
t.Fatalf("expected %v; got %v", ErrXOauth2AuthNotSupported, err)
|
t.Fatalf("expected %v; got %v", ErrXOauth2AuthNotSupported, err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if err = c.Close(); err != nil {
|
if err = c.Close(); err != nil {
|
||||||
t.Fatalf("disconnect from test server failed: %v", err)
|
t.Fatalf("disconnect from test server failed: %v", err)
|
||||||
}
|
}
|
||||||
client := strings.Split(wrote.String(), "\r\n")
|
client := strings.Split(wrote.String(), "\r\n")
|
||||||
if len(client) != 3 {
|
if len(client) != 2 {
|
||||||
t.Fatalf("unexpected number of client requests got %d; want 3", len(client))
|
t.Fatalf("unexpected number of client requests got %d; want 2", len(client))
|
||||||
}
|
}
|
||||||
if !strings.HasPrefix(client[0], "EHLO") {
|
if !strings.HasPrefix(client[0], "EHLO") {
|
||||||
t.Fatalf("expected EHLO, got %q", client[0])
|
t.Fatalf("expected EHLO, got %q", client[0])
|
||||||
}
|
}
|
||||||
if client[1] != "QUIT" {
|
|
||||||
t.Fatalf("expected QUIT, got %q", client[3])
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3496,6 +3757,17 @@ func (f faker) SetDeadline(time.Time) error { return nil }
|
||||||
func (f faker) SetReadDeadline(time.Time) error { return nil }
|
func (f faker) SetReadDeadline(time.Time) error { return nil }
|
||||||
func (f faker) SetWriteDeadline(time.Time) error { return nil }
|
func (f faker) SetWriteDeadline(time.Time) error { return nil }
|
||||||
|
|
||||||
|
type testSender struct {
|
||||||
|
client *Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testSender) Send(ctx context.Context, m *Msg) error {
|
||||||
|
if err := t.client.DialAndSendWithContext(ctx, m); err != nil {
|
||||||
|
return fmt.Errorf("failed to dial and send mail: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// parseJSONLog parses a JSON encoded log from the provided buffer and returns a slice of logLine structs.
|
// parseJSONLog parses a JSON encoded log from the provided buffer and returns a slice of logLine structs.
|
||||||
// In case of a decode error, it reports the error to the testing framework.
|
// In case of a decode error, it reports the error to the testing framework.
|
||||||
func parseJSONLog(t *testing.T, buf *bytes.Buffer) logData {
|
func parseJSONLog(t *testing.T, buf *bytes.Buffer) logData {
|
||||||
|
@ -3548,6 +3820,8 @@ func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "
|
||||||
|
|
||||||
// serverProps represents the configuration properties for the SMTP server.
|
// serverProps represents the configuration properties for the SMTP server.
|
||||||
type serverProps struct {
|
type serverProps struct {
|
||||||
|
BufferMutex sync.RWMutex
|
||||||
|
EchoBuffer io.Writer
|
||||||
FailOnAuth bool
|
FailOnAuth bool
|
||||||
FailOnDataInit bool
|
FailOnDataInit bool
|
||||||
FailOnDataClose bool
|
FailOnDataClose bool
|
||||||
|
@ -3637,6 +3911,13 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Logf("failed to write line: %s", err)
|
t.Logf("failed to write line: %s", err)
|
||||||
}
|
}
|
||||||
|
if props.EchoBuffer != nil {
|
||||||
|
props.BufferMutex.Lock()
|
||||||
|
if _, berr := props.EchoBuffer.Write([]byte(data + "\r\n")); berr != nil {
|
||||||
|
t.Errorf("failed write to echo buffer: %s", berr)
|
||||||
|
}
|
||||||
|
props.BufferMutex.Unlock()
|
||||||
|
}
|
||||||
_ = writer.Flush()
|
_ = writer.Flush()
|
||||||
}
|
}
|
||||||
writeOK := func() {
|
writeOK := func() {
|
||||||
|
@ -3653,6 +3934,13 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
time.Sleep(time.Millisecond)
|
time.Sleep(time.Millisecond)
|
||||||
|
if props.EchoBuffer != nil {
|
||||||
|
props.BufferMutex.Lock()
|
||||||
|
if _, berr := props.EchoBuffer.Write([]byte(data)); berr != nil {
|
||||||
|
t.Errorf("failed write to echo buffer: %s", berr)
|
||||||
|
}
|
||||||
|
props.BufferMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
var datastring string
|
var datastring string
|
||||||
data = strings.TrimSpace(data)
|
data = strings.TrimSpace(data)
|
||||||
|
@ -3713,6 +4001,13 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server
|
||||||
t.Logf("failed to read data from connection: %s", derr)
|
t.Logf("failed to read data from connection: %s", derr)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
if props.EchoBuffer != nil {
|
||||||
|
props.BufferMutex.Lock()
|
||||||
|
if _, berr := props.EchoBuffer.Write([]byte(ddata)); berr != nil {
|
||||||
|
t.Errorf("failed write to echo buffer: %s", berr)
|
||||||
|
}
|
||||||
|
props.BufferMutex.Unlock()
|
||||||
|
}
|
||||||
ddata = strings.TrimSpace(ddata)
|
ddata = strings.TrimSpace(ddata)
|
||||||
if ddata == "." {
|
if ddata == "." {
|
||||||
if props.FailOnDataClose {
|
if props.FailOnDataClose {
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
# SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
# SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: CC0-1.0
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
coverage:
|
coverage:
|
||||||
status:
|
status:
|
||||||
project:
|
project:
|
||||||
default:
|
default:
|
||||||
target: 90%
|
target: 95%
|
||||||
threshold: 2%
|
threshold: 2%
|
||||||
base: auto
|
base: auto
|
||||||
if_ci_failed: error
|
if_ci_failed: error
|
||||||
only_pulls: false
|
only_pulls: false
|
||||||
patch:
|
patch:
|
||||||
default:
|
default:
|
||||||
target: 90%
|
target: 95%
|
||||||
base: auto
|
base: auto
|
||||||
if_ci_failed: error
|
if_ci_failed: error
|
||||||
threshold: 2%
|
threshold: 2%
|
||||||
|
|
2
doc.go
2
doc.go
|
@ -11,4 +11,4 @@ package mail
|
||||||
|
|
||||||
// VERSION indicates the current version of the package. It is also attached to the default user
|
// VERSION indicates the current version of the package. It is also attached to the default user
|
||||||
// agent string.
|
// agent string.
|
||||||
const VERSION = "0.5.1"
|
const VERSION = "0.5.2"
|
||||||
|
|
6
go.mod
6
go.mod
|
@ -7,6 +7,6 @@ module github.com/wneessen/go-mail
|
||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/crypto v0.28.0
|
golang.org/x/crypto v0.30.0
|
||||||
golang.org/x/text v0.19.0
|
golang.org/x/text v0.21.0
|
||||||
)
|
)
|
||||||
|
|
14
go.sum
14
go.sum
|
@ -5,8 +5,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
|
||||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
@ -26,7 +26,7 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
@ -37,7 +37,7 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
@ -46,7 +46,7 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
@ -55,8 +55,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
|
75
msg.go
75
msg.go
|
@ -16,6 +16,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
ht "html/template"
|
ht "html/template"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"mime"
|
"mime"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"os"
|
"os"
|
||||||
|
@ -2033,9 +2034,28 @@ func (m *Msg) AttachTextTemplate(
|
||||||
// - https://datatracker.ietf.org/doc/html/rfc2183
|
// - https://datatracker.ietf.org/doc/html/rfc2183
|
||||||
func (m *Msg) AttachFromEmbedFS(name string, fs *embed.FS, opts ...FileOption) error {
|
func (m *Msg) AttachFromEmbedFS(name string, fs *embed.FS, opts ...FileOption) error {
|
||||||
if fs == nil {
|
if fs == nil {
|
||||||
return fmt.Errorf("embed.FS must not be nil")
|
return errors.New("embed.FS must not be nil")
|
||||||
}
|
}
|
||||||
file, err := fileFromEmbedFS(name, fs)
|
return m.AttachFromIOFS(name, *fs, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttachFromIOFS attaches a file from a generic file system to the message.
|
||||||
|
//
|
||||||
|
// This function retrieves a file by name from an fs.FS instance, processes it, and appends it to the
|
||||||
|
// message's attachment collection. Additional file options can be provided for further customization.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - name: The name of the file to retrieve from the file system.
|
||||||
|
// - iofs: The file system (must not be nil).
|
||||||
|
// - opts: Optional file options to customize the attachment process.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - An error if the file cannot be retrieved, the fs.FS is nil, or any other issue occurs.
|
||||||
|
func (m *Msg) AttachFromIOFS(name string, iofs fs.FS, opts ...FileOption) error {
|
||||||
|
if iofs == nil {
|
||||||
|
return errors.New("fs.FS must not be nil")
|
||||||
|
}
|
||||||
|
file, err := fileFromIOFS(name, iofs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -2179,9 +2199,28 @@ func (m *Msg) EmbedTextTemplate(
|
||||||
// - https://datatracker.ietf.org/doc/html/rfc2183
|
// - https://datatracker.ietf.org/doc/html/rfc2183
|
||||||
func (m *Msg) EmbedFromEmbedFS(name string, fs *embed.FS, opts ...FileOption) error {
|
func (m *Msg) EmbedFromEmbedFS(name string, fs *embed.FS, opts ...FileOption) error {
|
||||||
if fs == nil {
|
if fs == nil {
|
||||||
return fmt.Errorf("embed.FS must not be nil")
|
return errors.New("embed.FS must not be nil")
|
||||||
}
|
}
|
||||||
file, err := fileFromEmbedFS(name, fs)
|
return m.EmbedFromIOFS(name, *fs, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmbedFromIOFS embeds a file from a generic file system into the message.
|
||||||
|
//
|
||||||
|
// This function retrieves a file by name from an fs.FS instance, processes it, and appends it to the
|
||||||
|
// message's embed collection. Additional file options can be provided for further customization.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - name: The name of the file to retrieve from the file system.
|
||||||
|
// - iofs: The file system (must not be nil).
|
||||||
|
// - opts: Optional file options to customize the embedding process.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - An error if the file cannot be retrieved, the fs.FS is nil, or any other issue occurs.
|
||||||
|
func (m *Msg) EmbedFromIOFS(name string, iofs fs.FS, opts ...FileOption) error {
|
||||||
|
if iofs == nil {
|
||||||
|
return errors.New("fs.FS must not be nil")
|
||||||
|
}
|
||||||
|
file, err := fileFromIOFS(name, iofs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -2788,15 +2827,15 @@ func (m *Msg) addDefaultHeader() {
|
||||||
m.SetGenHeader(HeaderMIMEVersion, string(m.mimever))
|
m.SetGenHeader(HeaderMIMEVersion, string(m.mimever))
|
||||||
}
|
}
|
||||||
|
|
||||||
// fileFromEmbedFS returns a File pointer from a given file in the provided embed.FS.
|
// fileFromIOFS returns a File pointer from a given file in the provided fs.FS.
|
||||||
//
|
//
|
||||||
// This method retrieves a file from the embedded filesystem (embed.FS) and returns a File structure
|
// This method retrieves a file from the provided io/fs (fs.FS) and returns a File structure
|
||||||
// that can be used as an attachment or embed in the email message. The file's content is read when
|
// that can be used as an attachment or embed in the email message. The file's content is read when
|
||||||
// writing to an io.Writer, and the file is identified by its base name.
|
// writing to an io.Writer, and the file is identified by its base name.
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// - name: The name of the file to retrieve from the embedded filesystem.
|
// - name: The name of the file to retrieve from the embedded filesystem.
|
||||||
// - fs: A pointer to the embed.FS from which the file will be opened.
|
// - fs: An instance that satisfies the fs.FS interface
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - A pointer to the File structure representing the embedded file.
|
// - A pointer to the File structure representing the embedded file.
|
||||||
|
@ -2804,23 +2843,27 @@ func (m *Msg) addDefaultHeader() {
|
||||||
//
|
//
|
||||||
// References:
|
// References:
|
||||||
// - https://datatracker.ietf.org/doc/html/rfc2183
|
// - https://datatracker.ietf.org/doc/html/rfc2183
|
||||||
func fileFromEmbedFS(name string, fs *embed.FS) (*File, error) {
|
func fileFromIOFS(name string, iofs fs.FS) (*File, error) {
|
||||||
_, err := fs.Open(name)
|
if iofs == nil {
|
||||||
|
return nil, errors.New("fs.FS is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := iofs.Open(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to open file from embed.FS: %w", err)
|
return nil, fmt.Errorf("failed to open file from fs.FS: %w", err)
|
||||||
}
|
}
|
||||||
return &File{
|
return &File{
|
||||||
Name: filepath.Base(name),
|
Name: filepath.Base(name),
|
||||||
Header: make(map[string][]string),
|
Header: make(map[string][]string),
|
||||||
Writer: func(writer io.Writer) (int64, error) {
|
Writer: func(writer io.Writer) (int64, error) {
|
||||||
file, err := fs.Open(name)
|
file, ferr := iofs.Open(name)
|
||||||
if err != nil {
|
if ferr != nil {
|
||||||
return 0, err
|
return 0, fmt.Errorf("failed to open file from fs.FS: %w", ferr)
|
||||||
}
|
}
|
||||||
numBytes, err := io.Copy(writer, file)
|
numBytes, ferr := io.Copy(writer, file)
|
||||||
if err != nil {
|
if ferr != nil {
|
||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
return numBytes, fmt.Errorf("failed to copy file to io.Writer: %w", err)
|
return numBytes, fmt.Errorf("failed to copy file from fs.FS to io.Writer: %w", ferr)
|
||||||
}
|
}
|
||||||
return numBytes, file.Close()
|
return numBytes, file.Close()
|
||||||
},
|
},
|
||||||
|
|
142
msg_test.go
142
msg_test.go
|
@ -126,8 +126,8 @@ var (
|
||||||
{`" "@domain.tld`, true}, // Still valid, since quoted
|
{`" "@domain.tld`, true}, // Still valid, since quoted
|
||||||
{`"<\"@\".!#%$@domain.tld"`, false}, // Quoting with illegal characters is not allowed
|
{`"<\"@\".!#%$@domain.tld"`, false}, // Quoting with illegal characters is not allowed
|
||||||
{`<\"@\\".!#%$@domain.tld`, false}, // Still a bunch of random illegal characters
|
{`<\"@\\".!#%$@domain.tld`, false}, // Still a bunch of random illegal characters
|
||||||
{`hi"@"there@domain.tld`, false}, // Quotes must be dot-seperated
|
{`hi"@"there@domain.tld`, false}, // Quotes must be dot-separated
|
||||||
{`"<\"@\\".!.#%$@domain.tld`, false}, // Quote is escaped and dot-seperated which would be RFC822 compliant, but not RFC5322 compliant
|
{`"<\"@\\".!.#%$@domain.tld`, false}, // Quote is escaped and dot-separated which would be RFC822 compliant, but not RFC5322 compliant
|
||||||
{`hi\ there@domain.tld`, false}, // Spaces must be quoted
|
{`hi\ there@domain.tld`, false}, // Spaces must be quoted
|
||||||
{"hello@tld", true}, // TLD is enough
|
{"hello@tld", true}, // TLD is enough
|
||||||
{`你好@域名.顶级域名`, true}, // We speak RFC6532
|
{`你好@域名.顶级域名`, true}, // We speak RFC6532
|
||||||
|
@ -4527,12 +4527,12 @@ func TestMsg_AttachFile(t *testing.T) {
|
||||||
t.Errorf("expected message body to be %s, got: %s", "This is a test attachment", got)
|
t.Errorf("expected message body to be %s, got: %s", "This is a test attachment", got)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("AttachFile with non-existant file", func(t *testing.T) {
|
t.Run("AttachFile with non-existent file", func(t *testing.T) {
|
||||||
message := NewMsg()
|
message := NewMsg()
|
||||||
if message == nil {
|
if message == nil {
|
||||||
t.Fatal("message is nil")
|
t.Fatal("message is nil")
|
||||||
}
|
}
|
||||||
message.AttachFile("testdata/non-existant-file.txt")
|
message.AttachFile("testdata/non-existent-file.txt")
|
||||||
attachments := message.GetAttachments()
|
attachments := message.GetAttachments()
|
||||||
if len(attachments) != 0 {
|
if len(attachments) != 0 {
|
||||||
t.Fatalf("failed to retrieve attachments list")
|
t.Fatalf("failed to retrieve attachments list")
|
||||||
|
@ -4970,6 +4970,75 @@ func TestMsg_AttachFromEmbedFS(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMsg_AttachFromIOFS(t *testing.T) {
|
||||||
|
t.Run("AttachFromIOFS successful", func(t *testing.T) {
|
||||||
|
message := NewMsg()
|
||||||
|
if message == nil {
|
||||||
|
t.Fatal("message is nil")
|
||||||
|
}
|
||||||
|
if err := message.AttachFromIOFS("testdata/attachment.txt", efs,
|
||||||
|
WithFileName("attachment.txt")); err != nil {
|
||||||
|
t.Fatalf("failed to attach from embed FS: %s", err)
|
||||||
|
}
|
||||||
|
attachments := message.GetAttachments()
|
||||||
|
if len(attachments) != 1 {
|
||||||
|
t.Fatalf("failed to retrieve attachments list")
|
||||||
|
}
|
||||||
|
if attachments[0] == nil {
|
||||||
|
t.Fatal("expected attachment to be not nil")
|
||||||
|
}
|
||||||
|
if attachments[0].Name != "attachment.txt" {
|
||||||
|
t.Errorf("expected attachment name to be %s, got: %s", "attachment.txt", attachments[0].Name)
|
||||||
|
}
|
||||||
|
messageBuf := bytes.NewBuffer(nil)
|
||||||
|
_, err := attachments[0].Writer(messageBuf)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("writer func failed: %s", err)
|
||||||
|
}
|
||||||
|
got := strings.TrimSpace(messageBuf.String())
|
||||||
|
if !strings.EqualFold(got, "This is a test attachment") {
|
||||||
|
t.Errorf("expected message body to be %s, got: %s", "This is a test attachment", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("AttachFromIOFS with invalid path", func(t *testing.T) {
|
||||||
|
message := NewMsg()
|
||||||
|
if message == nil {
|
||||||
|
t.Fatal("message is nil")
|
||||||
|
}
|
||||||
|
err := message.AttachFromIOFS("testdata/invalid.txt", efs, WithFileName("attachment.txt"))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("AttachFromIOFS with nil embed FS", func(t *testing.T) {
|
||||||
|
message := NewMsg()
|
||||||
|
if message == nil {
|
||||||
|
t.Fatal("message is nil")
|
||||||
|
}
|
||||||
|
err := message.AttachFromIOFS("testdata/invalid.txt", nil, WithFileName("attachment.txt"))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("AttachFromIOFS with fs.FS fails on copy", func(t *testing.T) {
|
||||||
|
message := NewMsg()
|
||||||
|
if message == nil {
|
||||||
|
t.Fatal("message is nil")
|
||||||
|
}
|
||||||
|
if err := message.AttachFromIOFS("testdata/attachment.txt", efs); err != nil {
|
||||||
|
t.Fatalf("failed to attach file from fs.FS: %s", err)
|
||||||
|
}
|
||||||
|
attachments := message.GetAttachments()
|
||||||
|
if len(attachments) != 1 {
|
||||||
|
t.Fatalf("failed to get attachments, expected 1, got: %d", len(attachments))
|
||||||
|
}
|
||||||
|
_, err := attachments[0].Writer(failReadWriteSeekCloser{})
|
||||||
|
if err == nil {
|
||||||
|
t.Error("writer func expected to fail, but didn't")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestMsg_EmbedFile(t *testing.T) {
|
func TestMsg_EmbedFile(t *testing.T) {
|
||||||
t.Run("EmbedFile with file", func(t *testing.T) {
|
t.Run("EmbedFile with file", func(t *testing.T) {
|
||||||
message := NewMsg()
|
message := NewMsg()
|
||||||
|
@ -4997,12 +5066,12 @@ func TestMsg_EmbedFile(t *testing.T) {
|
||||||
t.Errorf("expected message body to be %s, got: %s", "This is a test embed", got)
|
t.Errorf("expected message body to be %s, got: %s", "This is a test embed", got)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("EmbedFile with non-existant file", func(t *testing.T) {
|
t.Run("EmbedFile with non-existent file", func(t *testing.T) {
|
||||||
message := NewMsg()
|
message := NewMsg()
|
||||||
if message == nil {
|
if message == nil {
|
||||||
t.Fatal("message is nil")
|
t.Fatal("message is nil")
|
||||||
}
|
}
|
||||||
message.EmbedFile("testdata/non-existant-file.txt")
|
message.EmbedFile("testdata/non-existent-file.txt")
|
||||||
embeds := message.GetEmbeds()
|
embeds := message.GetEmbeds()
|
||||||
if len(embeds) != 0 {
|
if len(embeds) != 0 {
|
||||||
t.Fatalf("failed to retrieve attachments list")
|
t.Fatalf("failed to retrieve attachments list")
|
||||||
|
@ -5435,6 +5504,58 @@ func TestMsg_EmbedFromEmbedFS(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMsg_EmbedFromIOFS(t *testing.T) {
|
||||||
|
t.Run("EmbedFromIOFS successful", func(t *testing.T) {
|
||||||
|
message := NewMsg()
|
||||||
|
if message == nil {
|
||||||
|
t.Fatal("message is nil")
|
||||||
|
}
|
||||||
|
if err := message.EmbedFromIOFS("testdata/embed.txt", efs,
|
||||||
|
WithFileName("embed.txt")); err != nil {
|
||||||
|
t.Fatalf("failed to embed from embed FS: %s", err)
|
||||||
|
}
|
||||||
|
embeds := message.GetEmbeds()
|
||||||
|
if len(embeds) != 1 {
|
||||||
|
t.Fatalf("failed to retrieve embeds list")
|
||||||
|
}
|
||||||
|
if embeds[0] == nil {
|
||||||
|
t.Fatal("expected embed to be not nil")
|
||||||
|
}
|
||||||
|
if embeds[0].Name != "embed.txt" {
|
||||||
|
t.Errorf("expected embed name to be %s, got: %s", "embed.txt", embeds[0].Name)
|
||||||
|
}
|
||||||
|
messageBuf := bytes.NewBuffer(nil)
|
||||||
|
_, err := embeds[0].Writer(messageBuf)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("writer func failed: %s", err)
|
||||||
|
}
|
||||||
|
got := strings.TrimSpace(messageBuf.String())
|
||||||
|
if !strings.EqualFold(got, "This is a test embed") {
|
||||||
|
t.Errorf("expected message body to be %s, got: %s", "This is a test embed", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("EmbedFromIOFS with invalid path", func(t *testing.T) {
|
||||||
|
message := NewMsg()
|
||||||
|
if message == nil {
|
||||||
|
t.Fatal("message is nil")
|
||||||
|
}
|
||||||
|
err := message.EmbedFromIOFS("testdata/invalid.txt", efs, WithFileName("embed.txt"))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("EmbedFromIOFS with nil embed FS", func(t *testing.T) {
|
||||||
|
message := NewMsg()
|
||||||
|
if message == nil {
|
||||||
|
t.Fatal("message is nil")
|
||||||
|
}
|
||||||
|
err := message.EmbedFromIOFS("testdata/invalid.txt", nil, WithFileName("embed.txt"))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestMsg_Reset(t *testing.T) {
|
func TestMsg_Reset(t *testing.T) {
|
||||||
message := NewMsg()
|
message := NewMsg()
|
||||||
if message == nil {
|
if message == nil {
|
||||||
|
@ -6537,6 +6658,15 @@ func TestMsg_addDefaultHeader(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMsg_fileFromIOFS(t *testing.T) {
|
||||||
|
t.Run("file from fs.FS where fs is nil ", func(t *testing.T) {
|
||||||
|
_, err := fileFromIOFS("testfile.txt", nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for fs.FS that is nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// TestSignWithSMime_ValidRSAKeyPair tests WithSMimeSinging with given rsa key pair
|
// TestSignWithSMime_ValidRSAKeyPair tests WithSMimeSinging with given rsa key pair
|
||||||
func TestSignWithSMime_ValidRSAKeyPair(t *testing.T) {
|
func TestSignWithSMime_ValidRSAKeyPair(t *testing.T) {
|
||||||
privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial()
|
privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial()
|
||||||
|
|
38
msgwriter.go
38
msgwriter.go
|
@ -281,7 +281,7 @@ func (mw *msgWriter) addFiles(files []*File, isAttachment bool) {
|
||||||
mimeType = string(file.ContentType)
|
mimeType = string(file.ContentType)
|
||||||
}
|
}
|
||||||
file.setHeader(HeaderContentType, fmt.Sprintf(`%s; name="%s"`, mimeType,
|
file.setHeader(HeaderContentType, fmt.Sprintf(`%s; name="%s"`, mimeType,
|
||||||
mw.encoder.Encode(mw.charset.String(), file.Name)))
|
mw.encoder.Encode(mw.charset.String(), sanitizeFilename(file.Name))))
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := file.getHeader(HeaderContentTransferEnc); !ok {
|
if _, ok := file.getHeader(HeaderContentTransferEnc); !ok {
|
||||||
|
@ -293,7 +293,7 @@ func (mw *msgWriter) addFiles(files []*File, isAttachment bool) {
|
||||||
|
|
||||||
if file.Desc != "" {
|
if file.Desc != "" {
|
||||||
if _, ok := file.getHeader(HeaderContentDescription); !ok {
|
if _, ok := file.getHeader(HeaderContentDescription); !ok {
|
||||||
file.setHeader(HeaderContentDescription, file.Desc)
|
file.setHeader(HeaderContentDescription, mw.encoder.Encode(mw.charset.String(), file.Desc))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -303,12 +303,12 @@ func (mw *msgWriter) addFiles(files []*File, isAttachment bool) {
|
||||||
disposition = "attachment"
|
disposition = "attachment"
|
||||||
}
|
}
|
||||||
file.setHeader(HeaderContentDisposition, fmt.Sprintf(`%s; filename="%s"`,
|
file.setHeader(HeaderContentDisposition, fmt.Sprintf(`%s; filename="%s"`,
|
||||||
disposition, mw.encoder.Encode(mw.charset.String(), file.Name)))
|
disposition, mw.encoder.Encode(mw.charset.String(), sanitizeFilename(file.Name))))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isAttachment {
|
if !isAttachment {
|
||||||
if _, ok := file.getHeader(HeaderContentID); !ok {
|
if _, ok := file.getHeader(HeaderContentID); !ok {
|
||||||
file.setHeader(HeaderContentID, fmt.Sprintf("<%s>", file.Name))
|
file.setHeader(HeaderContentID, fmt.Sprintf("<%s>", sanitizeFilename(file.Name)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if mw.depth == 0 {
|
if mw.depth == 0 {
|
||||||
|
@ -511,3 +511,33 @@ func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encodin
|
||||||
mw.bytesWritten += n
|
mw.bytesWritten += n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sanitizeFilename sanitizes a given filename string by replacing specific unwanted characters with
|
||||||
|
// an underscore ('_').
|
||||||
|
//
|
||||||
|
// This method replaces any control character and any special character that is problematic for
|
||||||
|
// MIME headers and file systems with an underscore ('_') character.
|
||||||
|
//
|
||||||
|
// The following characters are replaced
|
||||||
|
// - Any control character (US-ASCII < 32)
|
||||||
|
// - ", /, :, <, >, ?, \, |, [DEL]
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - input: A string of a filename that is supposed to be sanitized
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A string representing the sanitized version of the filename
|
||||||
|
func sanitizeFilename(input string) string {
|
||||||
|
var sanitized strings.Builder
|
||||||
|
for i := 0; i < len(input); i++ {
|
||||||
|
// We do not allow control characters in file names.
|
||||||
|
if input[i] < 32 || input[i] == 34 || input[i] == 47 || input[i] == 58 ||
|
||||||
|
input[i] == 60 || input[i] == 62 || input[i] == 63 || input[i] == 92 ||
|
||||||
|
input[i] == 124 || input[i] == 127 {
|
||||||
|
sanitized.WriteRune('_')
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sanitized.WriteByte(input[i])
|
||||||
|
}
|
||||||
|
return sanitized.String()
|
||||||
|
}
|
||||||
|
|
|
@ -304,6 +304,65 @@ func TestMsgWriter_addFiles(t *testing.T) {
|
||||||
charset: CharsetUTF8,
|
charset: CharsetUTF8,
|
||||||
encoder: getEncoder(EncodingQP),
|
encoder: getEncoder(EncodingQP),
|
||||||
}
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
filename string
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{"normal US-ASCII filename", "test.txt", "test.txt"},
|
||||||
|
{"normal US-ASCII filename with space", "test file.txt", "test file.txt"},
|
||||||
|
{"filename with new lines", "test\r\n.txt", "test__.txt"},
|
||||||
|
{"filename with disallowed character:\x22", "test\x22.txt", "test_.txt"},
|
||||||
|
{"filename with disallowed character:\x2f", "test\x2f.txt", "test_.txt"},
|
||||||
|
{"filename with disallowed character:\x3a", "test\x3a.txt", "test_.txt"},
|
||||||
|
{"filename with disallowed character:\x3c", "test\x3c.txt", "test_.txt"},
|
||||||
|
{"filename with disallowed character:\x3e", "test\x3e.txt", "test_.txt"},
|
||||||
|
{"filename with disallowed character:\x3f", "test\x3f.txt", "test_.txt"},
|
||||||
|
{"filename with disallowed character:\x5c", "test\x5c.txt", "test_.txt"},
|
||||||
|
{"filename with disallowed character:\x7c", "test\x7c.txt", "test_.txt"},
|
||||||
|
{"filename with disallowed character:\x7f", "test\x7f.txt", "test_.txt"},
|
||||||
|
{
|
||||||
|
"japanese characters filename", "添付ファイル.txt",
|
||||||
|
"=?UTF-8?q?=E6=B7=BB=E4=BB=98=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB.txt?=",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"simplified chinese characters filename", "测试附件文件.txt",
|
||||||
|
"=?UTF-8?q?=E6=B5=8B=E8=AF=95=E9=99=84=E4=BB=B6=E6=96=87=E4=BB=B6.txt?=",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cyrillic characters filename", "Тестовый прикрепленный файл.txt",
|
||||||
|
"=?UTF-8?q?=D0=A2=D0=B5=D1=81=D1=82=D0=BE=D0=B2=D1=8B=D0=B9_=D0=BF=D1=80?= " +
|
||||||
|
"=?UTF-8?q?=D0=B8=D0=BA=D1=80=D0=B5=D0=BF=D0=BB=D0=B5=D0=BD=D0=BD=D1=8B?= " +
|
||||||
|
"=?UTF-8?q?=D0=B9_=D1=84=D0=B0=D0=B9=D0=BB.txt?=",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run("addFile with filename sanitization: "+tt.name, func(t *testing.T) {
|
||||||
|
buffer := bytes.NewBuffer(nil)
|
||||||
|
msgwriter.writer = buffer
|
||||||
|
message := testMessage(t)
|
||||||
|
message.AttachFile("testdata/attachment.txt", WithFileName(tt.filename))
|
||||||
|
msgwriter.writeMsg(message)
|
||||||
|
if msgwriter.err != nil {
|
||||||
|
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ctExpect string
|
||||||
|
cdExpect := fmt.Sprintf(`Content-Disposition: attachment; filename="%s"`, tt.expect)
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "freebsd":
|
||||||
|
ctExpect = fmt.Sprintf(`Content-Type: application/octet-stream; name="%s"`, tt.expect)
|
||||||
|
default:
|
||||||
|
ctExpect = fmt.Sprintf(`Content-Type: text/plain; charset=utf-8; name="%s"`, tt.expect)
|
||||||
|
}
|
||||||
|
if !strings.Contains(buffer.String(), ctExpect) {
|
||||||
|
t.Errorf("expected content-type: %q, got: %q", ctExpect, buffer.String())
|
||||||
|
}
|
||||||
|
if !strings.Contains(buffer.String(), cdExpect) {
|
||||||
|
t.Errorf("expected content-disposition: %q, got: %q", cdExpect, buffer.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
t.Run("message with a single file attached", func(t *testing.T) {
|
t.Run("message with a single file attached", func(t *testing.T) {
|
||||||
buffer := bytes.NewBuffer(nil)
|
buffer := bytes.NewBuffer(nil)
|
||||||
msgwriter.writer = buffer
|
msgwriter.writer = buffer
|
||||||
|
@ -324,7 +383,7 @@ func TestMsgWriter_addFiles(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) {
|
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) {
|
||||||
t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String())
|
t.Errorf("Content-Disposition header not found for attachment. Mail: %s", buffer.String())
|
||||||
}
|
}
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "freebsd":
|
case "freebsd":
|
||||||
|
@ -357,7 +416,7 @@ func TestMsgWriter_addFiles(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment"`) {
|
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment"`) {
|
||||||
t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String())
|
t.Errorf("Content-Disposition header not found for attachment. Mail: %s", buffer.String())
|
||||||
}
|
}
|
||||||
if !strings.Contains(buffer.String(), `Content-Type: application/octet-stream; name="attachment"`) {
|
if !strings.Contains(buffer.String(), `Content-Type: application/octet-stream; name="attachment"`) {
|
||||||
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
|
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
|
||||||
|
@ -383,7 +442,7 @@ func TestMsgWriter_addFiles(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) {
|
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) {
|
||||||
t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String())
|
t.Errorf("Content-Disposition header not found for attachment. Mail: %s", buffer.String())
|
||||||
}
|
}
|
||||||
if !strings.Contains(buffer.String(), `Content-Type: application/octet-stream; name="attachment.txt"`) {
|
if !strings.Contains(buffer.String(), `Content-Type: application/octet-stream; name="attachment.txt"`) {
|
||||||
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
|
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
|
||||||
|
@ -402,7 +461,7 @@ func TestMsgWriter_addFiles(t *testing.T) {
|
||||||
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
|
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
|
||||||
}
|
}
|
||||||
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) {
|
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) {
|
||||||
t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String())
|
t.Errorf("Content-Disposition header not found for attachment. Mail: %s", buffer.String())
|
||||||
}
|
}
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "freebsd":
|
case "freebsd":
|
||||||
|
@ -438,7 +497,7 @@ func TestMsgWriter_addFiles(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) {
|
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) {
|
||||||
t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String())
|
t.Errorf("Content-Disposition header not found for attachment. Mail: %s", buffer.String())
|
||||||
}
|
}
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "freebsd":
|
case "freebsd":
|
||||||
|
@ -478,7 +537,7 @@ func TestMsgWriter_addFiles(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) {
|
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) {
|
||||||
t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String())
|
t.Errorf("Content-Disposition header not found for attachment. Mail: %s", buffer.String())
|
||||||
}
|
}
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "freebsd":
|
case "freebsd":
|
||||||
|
@ -620,7 +679,7 @@ func TestMsgWriter_writeBody(t *testing.T) {
|
||||||
buffer := bytes.NewBuffer(nil)
|
buffer := bytes.NewBuffer(nil)
|
||||||
msgwriter.writer = buffer
|
msgwriter.writer = buffer
|
||||||
message := testMessage(t)
|
message := testMessage(t)
|
||||||
msgwriter.writeBody(message.parts[0].writeFunc, NoEncoding, false)
|
msgwriter.writeBody(message.parts[0].writeFunc, NoEncoding)
|
||||||
if msgwriter.err != nil {
|
if msgwriter.err != nil {
|
||||||
t.Errorf("writeBody failed to write: %s", msgwriter.err)
|
t.Errorf("writeBody failed to write: %s", msgwriter.err)
|
||||||
}
|
}
|
||||||
|
@ -628,7 +687,7 @@ func TestMsgWriter_writeBody(t *testing.T) {
|
||||||
t.Run("writeBody on NoEncoding fails on write", func(t *testing.T) {
|
t.Run("writeBody on NoEncoding fails on write", func(t *testing.T) {
|
||||||
msgwriter.writer = failReadWriteSeekCloser{}
|
msgwriter.writer = failReadWriteSeekCloser{}
|
||||||
message := testMessage(t)
|
message := testMessage(t)
|
||||||
msgwriter.writeBody(message.parts[0].writeFunc, NoEncoding, false)
|
msgwriter.writeBody(message.parts[0].writeFunc, NoEncoding)
|
||||||
if msgwriter.err == nil {
|
if msgwriter.err == nil {
|
||||||
t.Errorf("writeBody succeeded, expected error")
|
t.Errorf("writeBody succeeded, expected error")
|
||||||
}
|
}
|
||||||
|
@ -642,7 +701,7 @@ func TestMsgWriter_writeBody(t *testing.T) {
|
||||||
writeFunc := func(io.Writer) (int64, error) {
|
writeFunc := func(io.Writer) (int64, error) {
|
||||||
return 0, errors.New("intentional write failure")
|
return 0, errors.New("intentional write failure")
|
||||||
}
|
}
|
||||||
msgwriter.writeBody(writeFunc, NoEncoding, false)
|
msgwriter.writeBody(writeFunc, NoEncoding)
|
||||||
if msgwriter.err == nil {
|
if msgwriter.err == nil {
|
||||||
t.Errorf("writeBody succeeded, expected error")
|
t.Errorf("writeBody succeeded, expected error")
|
||||||
}
|
}
|
||||||
|
@ -653,7 +712,7 @@ func TestMsgWriter_writeBody(t *testing.T) {
|
||||||
t.Run("writeBody Quoted-Printable fails on write", func(t *testing.T) {
|
t.Run("writeBody Quoted-Printable fails on write", func(t *testing.T) {
|
||||||
msgwriter.writer = failReadWriteSeekCloser{}
|
msgwriter.writer = failReadWriteSeekCloser{}
|
||||||
message := testMessage(t)
|
message := testMessage(t)
|
||||||
msgwriter.writeBody(message.parts[0].writeFunc, EncodingQP, false)
|
msgwriter.writeBody(message.parts[0].writeFunc, EncodingQP)
|
||||||
if msgwriter.err == nil {
|
if msgwriter.err == nil {
|
||||||
t.Errorf("writeBody succeeded, expected error")
|
t.Errorf("writeBody succeeded, expected error")
|
||||||
}
|
}
|
||||||
|
@ -677,6 +736,36 @@ func TestMsgWriter_writeBody(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMsgWriter_sanitizeFilename(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
given string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"test.txt", "test.txt"},
|
||||||
|
{"test file.txt", "test file.txt"},
|
||||||
|
{"test\\ file.txt", "test_ file.txt"},
|
||||||
|
{`"test" file.txt`, "_test_ file.txt"},
|
||||||
|
{`test file .txt`, "test_file_.txt"},
|
||||||
|
{"test\r\nfile.txt", "test__file.txt"},
|
||||||
|
{"test\x22file.txt", "test_file.txt"},
|
||||||
|
{"test\x2ffile.txt", "test_file.txt"},
|
||||||
|
{"test\x3afile.txt", "test_file.txt"},
|
||||||
|
{"test\x3cfile.txt", "test_file.txt"},
|
||||||
|
{"test\x3efile.txt", "test_file.txt"},
|
||||||
|
{"test\x3ffile.txt", "test_file.txt"},
|
||||||
|
{"test\x5cfile.txt", "test_file.txt"},
|
||||||
|
{"test\x7cfile.txt", "test_file.txt"},
|
||||||
|
{"test\x7ffile.txt", "test_file.txt"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.given+"=>"+tt.want, func(t *testing.T) {
|
||||||
|
if got := sanitizeFilename(tt.given); got != tt.want {
|
||||||
|
t.Errorf("sanitizeFilename failed, expected: %q, got: %q", tt.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestMsgWriter_writeMsg_SMime tests the writeMsg method of the msgWriter with S/MIME types set
|
// TestMsgWriter_writeMsg_SMime tests the writeMsg method of the msgWriter with S/MIME types set
|
||||||
func TestMsgWriter_writeMsg_SMime(t *testing.T) {
|
func TestMsgWriter_writeMsg_SMime(t *testing.T) {
|
||||||
privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial()
|
privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial()
|
||||||
|
@ -715,3 +804,5 @@ func TestMsgWriter_writeMsg_SMime(t *testing.T) {
|
||||||
t.Errorf("writeMsg failed. Unable to find Content-Type")
|
t.Errorf("writeMsg failed. Unable to find Content-Type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
111
quicksend.go
Normal file
111
quicksend.go
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthData struct {
|
||||||
|
Auth bool
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
var testHookTLSConfig func() *tls.Config // nil, except for tests
|
||||||
|
|
||||||
|
// QuickSend is an all-in-one method for quickly sending simple text mails in go-mail.
|
||||||
|
//
|
||||||
|
// This method will create a new client that connects to the server at addr, switches to TLS if possible,
|
||||||
|
// authenticates with the optional AuthData provided in auth and create a new simple Msg with the provided
|
||||||
|
// subject string and message bytes as body. The message will be sent using from as sender address and will
|
||||||
|
// be delivered to every address in rcpts. QuickSend will always send as text/plain ContentType.
|
||||||
|
//
|
||||||
|
// For the SMTP authentication, if auth is not nil and AuthData.Auth is set to true, it will try to
|
||||||
|
// autodiscover the best SMTP authentication mechanism supported by the server. If auth is set to true
|
||||||
|
// but autodiscover is not able to find a suitable authentication mechanism or if the authentication
|
||||||
|
// fails, the mail delivery will fail completely.
|
||||||
|
//
|
||||||
|
// The content parameter should be an RFC 822-style email body. The lines of content should be CRLF terminated.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - addr: The hostname and port of the mail server, it must include a port, as in "mail.example.com:smtp".
|
||||||
|
// - auth: A AuthData pointer. If nil or if AuthData.Auth is set to false, not SMTP authentication will be performed.
|
||||||
|
// - from: The from address of the sender as string.
|
||||||
|
// - rcpts: A slice of strings of receipient addresses.
|
||||||
|
// - subject: The subject line as string.
|
||||||
|
// - content: A byte slice of the mail content
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A pointer to the generated Msg.
|
||||||
|
// - An error if any step in the process of mail generation or delivery failed.
|
||||||
|
func QuickSend(addr string, auth *AuthData, from string, rcpts []string, subject string, content []byte) (*Msg, error) {
|
||||||
|
host, port, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to split host and port from address: %w", err)
|
||||||
|
}
|
||||||
|
portnum, err := strconv.Atoi(port)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert port to int: %w", err)
|
||||||
|
}
|
||||||
|
client, err := NewClient(host, WithPort(portnum), WithTLSPolicy(TLSOpportunistic))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create new client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if auth != nil && auth.Auth {
|
||||||
|
client.SetSMTPAuth(SMTPAuthAutoDiscover)
|
||||||
|
client.SetUsername(auth.Username)
|
||||||
|
client.SetPassword(auth.Password)
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConfig := client.tlsconfig
|
||||||
|
if testHookTLSConfig != nil {
|
||||||
|
tlsConfig = testHookTLSConfig()
|
||||||
|
}
|
||||||
|
if err = client.SetTLSConfig(tlsConfig); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to set TLS config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
message := NewMsg()
|
||||||
|
if err = message.From(from); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to set MAIL FROM address: %w", err)
|
||||||
|
}
|
||||||
|
if err = message.To(rcpts...); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to set RCPT TO address: %w", err)
|
||||||
|
}
|
||||||
|
message.Subject(subject)
|
||||||
|
buffer := bytes.NewBuffer(content)
|
||||||
|
writeFunc := writeFuncFromBuffer(buffer)
|
||||||
|
message.SetBodyWriter(TypeTextPlain, writeFunc)
|
||||||
|
|
||||||
|
if err = client.DialAndSend(message); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to dial and send message: %w", err)
|
||||||
|
}
|
||||||
|
return message, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthData creates a new AuthData instance with the provided username and password.
|
||||||
|
//
|
||||||
|
// This function initializes an AuthData struct with authentication enabled and sets the
|
||||||
|
// username and password fields.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - user: The username for authentication.
|
||||||
|
// - pass: The password for authentication.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A pointer to the initialized AuthData instance.
|
||||||
|
func NewAuthData(user, pass string) *AuthData {
|
||||||
|
return &AuthData{
|
||||||
|
Auth: true,
|
||||||
|
Username: user,
|
||||||
|
Password: pass,
|
||||||
|
}
|
||||||
|
}
|
368
quicksend_test.go
Normal file
368
quicksend_test.go
Normal file
|
@ -0,0 +1,368 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewAuthData(t *testing.T) {
|
||||||
|
t.Run("AuthData with username and password", func(t *testing.T) {
|
||||||
|
auth := NewAuthData("username", "password")
|
||||||
|
if !auth.Auth {
|
||||||
|
t.Fatal("expected auth to be true")
|
||||||
|
}
|
||||||
|
if auth.Username != "username" {
|
||||||
|
t.Fatalf("expected username to be %s, got %s", "username", auth.Username)
|
||||||
|
}
|
||||||
|
if auth.Password != "password" {
|
||||||
|
t.Fatalf("expected password to be %s, got %s", "password", auth.Password)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("AuthData with username and empty password", func(t *testing.T) {
|
||||||
|
auth := NewAuthData("username", "")
|
||||||
|
if !auth.Auth {
|
||||||
|
t.Fatal("expected auth to be true")
|
||||||
|
}
|
||||||
|
if auth.Username != "username" {
|
||||||
|
t.Fatalf("expected username to be %s, got %s", "username", auth.Username)
|
||||||
|
}
|
||||||
|
if auth.Password != "" {
|
||||||
|
t.Fatalf("expected password to be %s, got %s", "", auth.Password)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("AuthData with empty username and set password", func(t *testing.T) {
|
||||||
|
auth := NewAuthData("", "password")
|
||||||
|
if !auth.Auth {
|
||||||
|
t.Fatal("expected auth to be true")
|
||||||
|
}
|
||||||
|
if auth.Username != "" {
|
||||||
|
t.Fatalf("expected username to be %s, got %s", "", auth.Username)
|
||||||
|
}
|
||||||
|
if auth.Password != "password" {
|
||||||
|
t.Fatalf("expected password to be %s, got %s", "password", auth.Password)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("AuthData with empty data", func(t *testing.T) {
|
||||||
|
auth := NewAuthData("", "")
|
||||||
|
if !auth.Auth {
|
||||||
|
t.Fatal("expected auth to be true")
|
||||||
|
}
|
||||||
|
if auth.Username != "" {
|
||||||
|
t.Fatalf("expected username to be %s, got %s", "", auth.Username)
|
||||||
|
}
|
||||||
|
if auth.Password != "" {
|
||||||
|
t.Fatalf("expected password to be %s, got %s", "", auth.Password)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuickSend(t *testing.T) {
|
||||||
|
subject := "This is a test subject"
|
||||||
|
body := []byte("This is a test body\r\nWith multiple lines\r\n\r\nBest,\r\n The go-mail team")
|
||||||
|
sender := TestSenderValid
|
||||||
|
rcpts := []string{TestRcptValid}
|
||||||
|
t.Run("QuickSend with authentication and TLS", func(t *testing.T) {
|
||||||
|
ctxAuth, cancelAuth := context.WithCancel(context.Background())
|
||||||
|
defer cancelAuth()
|
||||||
|
PortAdder.Add(1)
|
||||||
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
||||||
|
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250-STARTTLS\r\n250 SMTPUTF8"
|
||||||
|
echoBuffer := bytes.NewBuffer(nil)
|
||||||
|
props := &serverProps{
|
||||||
|
EchoBuffer: echoBuffer,
|
||||||
|
FeatureSet: featureSet,
|
||||||
|
ListenPort: serverPort,
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
if err := simpleSMTPServer(ctxAuth, t, props); err != nil {
|
||||||
|
t.Errorf("failed to start test server: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
time.Sleep(time.Millisecond * 30)
|
||||||
|
addr := TestServerAddr + ":" + fmt.Sprint(serverPort)
|
||||||
|
testHookTLSConfig = func() *tls.Config { return &tls.Config{InsecureSkipVerify: true} }
|
||||||
|
|
||||||
|
_, err := QuickSend(addr, NewAuthData("username", "password"), sender, rcpts, subject, body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to send email: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
props.BufferMutex.RLock()
|
||||||
|
resp := strings.Split(echoBuffer.String(), "\r\n")
|
||||||
|
props.BufferMutex.RUnlock()
|
||||||
|
|
||||||
|
expects := []struct {
|
||||||
|
line int
|
||||||
|
data string
|
||||||
|
}{
|
||||||
|
{8, "STARTTLS"},
|
||||||
|
{17, "AUTH PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk"},
|
||||||
|
{21, "MAIL FROM:<valid-from@domain.tld> BODY=8BITMIME SMTPUTF8"},
|
||||||
|
{23, "RCPT TO:<valid-to@domain.tld>"},
|
||||||
|
{30, "Subject: " + subject},
|
||||||
|
{33, "From: <valid-from@domain.tld>"},
|
||||||
|
{34, "To: <valid-to@domain.tld>"},
|
||||||
|
{35, "Content-Type: text/plain; charset=UTF-8"},
|
||||||
|
{36, "Content-Transfer-Encoding: quoted-printable"},
|
||||||
|
{38, "This is a test body"},
|
||||||
|
{39, "With multiple lines"},
|
||||||
|
{40, ""},
|
||||||
|
{41, "Best,"},
|
||||||
|
{42, " The go-mail team"},
|
||||||
|
}
|
||||||
|
for _, expect := range expects {
|
||||||
|
if !strings.EqualFold(resp[expect.line], expect.data) {
|
||||||
|
t.Errorf("expected %q at line %d, got: %q", expect.data, expect.line, resp[expect.line])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("QuickSend with authentication and TLS and multiple receipients", func(t *testing.T) {
|
||||||
|
ctxAuth, cancelAuth := context.WithCancel(context.Background())
|
||||||
|
defer cancelAuth()
|
||||||
|
PortAdder.Add(1)
|
||||||
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
||||||
|
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250-STARTTLS\r\n250 SMTPUTF8"
|
||||||
|
echoBuffer := bytes.NewBuffer(nil)
|
||||||
|
props := &serverProps{
|
||||||
|
EchoBuffer: echoBuffer,
|
||||||
|
FeatureSet: featureSet,
|
||||||
|
ListenPort: serverPort,
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
if err := simpleSMTPServer(ctxAuth, t, props); err != nil {
|
||||||
|
t.Errorf("failed to start test server: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
time.Sleep(time.Millisecond * 30)
|
||||||
|
addr := TestServerAddr + ":" + fmt.Sprint(serverPort)
|
||||||
|
testHookTLSConfig = func() *tls.Config { return &tls.Config{InsecureSkipVerify: true} }
|
||||||
|
|
||||||
|
multiRcpts := []string{TestRcptValid, TestRcptValid, TestRcptValid}
|
||||||
|
_, err := QuickSend(addr, NewAuthData("username", "password"), sender, multiRcpts, subject, body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to send email: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
props.BufferMutex.RLock()
|
||||||
|
resp := strings.Split(echoBuffer.String(), "\r\n")
|
||||||
|
props.BufferMutex.RUnlock()
|
||||||
|
|
||||||
|
expects := []struct {
|
||||||
|
line int
|
||||||
|
data string
|
||||||
|
}{
|
||||||
|
{8, "STARTTLS"},
|
||||||
|
{17, "AUTH PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk"},
|
||||||
|
{21, "MAIL FROM:<valid-from@domain.tld> BODY=8BITMIME SMTPUTF8"},
|
||||||
|
{23, "RCPT TO:<valid-to@domain.tld>"},
|
||||||
|
{25, "RCPT TO:<valid-to@domain.tld>"},
|
||||||
|
{27, "RCPT TO:<valid-to@domain.tld>"},
|
||||||
|
{34, "Subject: " + subject},
|
||||||
|
{37, "From: <valid-from@domain.tld>"},
|
||||||
|
{38, "To: <valid-to@domain.tld>, <valid-to@domain.tld>, <valid-to@domain.tld>"},
|
||||||
|
{39, "Content-Type: text/plain; charset=UTF-8"},
|
||||||
|
{40, "Content-Transfer-Encoding: quoted-printable"},
|
||||||
|
{42, "This is a test body"},
|
||||||
|
{43, "With multiple lines"},
|
||||||
|
{44, ""},
|
||||||
|
{45, "Best,"},
|
||||||
|
{46, " The go-mail team"},
|
||||||
|
}
|
||||||
|
for _, expect := range expects {
|
||||||
|
if !strings.EqualFold(resp[expect.line], expect.data) {
|
||||||
|
t.Errorf("expected %q at line %d, got: %q", expect.data, expect.line, resp[expect.line])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("QuickSend uses stronged authentication method", func(t *testing.T) {
|
||||||
|
ctxAuth, cancelAuth := context.WithCancel(context.Background())
|
||||||
|
defer cancelAuth()
|
||||||
|
PortAdder.Add(1)
|
||||||
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
||||||
|
featureSet := "250-AUTH PLAIN CRAM-MD5 SCRAM-SHA-256-PLUS SCRAM-SHA-256\r\n250-8BITMIME\r\n250-DSN\r\n250-STARTTLS\r\n250 SMTPUTF8"
|
||||||
|
echoBuffer := bytes.NewBuffer(nil)
|
||||||
|
props := &serverProps{
|
||||||
|
EchoBuffer: echoBuffer,
|
||||||
|
FeatureSet: featureSet,
|
||||||
|
ListenPort: serverPort,
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
if err := simpleSMTPServer(ctxAuth, t, props); err != nil {
|
||||||
|
t.Errorf("failed to start test server: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
time.Sleep(time.Millisecond * 30)
|
||||||
|
addr := TestServerAddr + ":" + fmt.Sprint(serverPort)
|
||||||
|
testHookTLSConfig = func() *tls.Config { return &tls.Config{InsecureSkipVerify: true} }
|
||||||
|
|
||||||
|
_, err := QuickSend(addr, NewAuthData("username", "password"), sender, rcpts, subject, body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to send email: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
props.BufferMutex.RLock()
|
||||||
|
resp := strings.Split(echoBuffer.String(), "\r\n")
|
||||||
|
props.BufferMutex.RUnlock()
|
||||||
|
|
||||||
|
expects := []struct {
|
||||||
|
line int
|
||||||
|
data string
|
||||||
|
}{
|
||||||
|
{17, "AUTH SCRAM-SHA-256-PLUS"},
|
||||||
|
}
|
||||||
|
for _, expect := range expects {
|
||||||
|
if !strings.EqualFold(resp[expect.line], expect.data) {
|
||||||
|
t.Errorf("expected %q at line %d, got: %q", expect.data, expect.line, resp[expect.line])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("QuickSend uses stronged authentication method without TLS", func(t *testing.T) {
|
||||||
|
ctxAuth, cancelAuth := context.WithCancel(context.Background())
|
||||||
|
defer cancelAuth()
|
||||||
|
PortAdder.Add(1)
|
||||||
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
||||||
|
featureSet := "250-AUTH PLAIN CRAM-MD5 SCRAM-SHA-256-PLUS SCRAM-SHA-256\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
||||||
|
echoBuffer := bytes.NewBuffer(nil)
|
||||||
|
props := &serverProps{
|
||||||
|
EchoBuffer: echoBuffer,
|
||||||
|
FeatureSet: featureSet,
|
||||||
|
ListenPort: serverPort,
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
if err := simpleSMTPServer(ctxAuth, t, props); err != nil {
|
||||||
|
t.Errorf("failed to start test server: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
time.Sleep(time.Millisecond * 30)
|
||||||
|
addr := TestServerAddr + ":" + fmt.Sprint(serverPort)
|
||||||
|
testHookTLSConfig = func() *tls.Config { return &tls.Config{InsecureSkipVerify: true} }
|
||||||
|
|
||||||
|
_, err := QuickSend(addr, NewAuthData("username", "password"), sender, rcpts, subject, body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to send email: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
props.BufferMutex.RLock()
|
||||||
|
resp := strings.Split(echoBuffer.String(), "\r\n")
|
||||||
|
props.BufferMutex.RUnlock()
|
||||||
|
|
||||||
|
expects := []struct {
|
||||||
|
line int
|
||||||
|
data string
|
||||||
|
}{
|
||||||
|
{7, "AUTH SCRAM-SHA-256"},
|
||||||
|
}
|
||||||
|
for _, expect := range expects {
|
||||||
|
if !strings.EqualFold(resp[expect.line], expect.data) {
|
||||||
|
t.Errorf("expected %q at line %d, got: %q", expect.data, expect.line, resp[expect.line])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("QuickSend fails during DialAndSned", func(t *testing.T) {
|
||||||
|
ctxAuth, cancelAuth := context.WithCancel(context.Background())
|
||||||
|
defer cancelAuth()
|
||||||
|
PortAdder.Add(1)
|
||||||
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
||||||
|
featureSet := "250-AUTH PLAIN CRAM-MD5 SCRAM-SHA-256-PLUS SCRAM-SHA-256\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
||||||
|
props := &serverProps{
|
||||||
|
FailOnMailFrom: true,
|
||||||
|
FeatureSet: featureSet,
|
||||||
|
ListenPort: serverPort,
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
if err := simpleSMTPServer(ctxAuth, t, props); err != nil {
|
||||||
|
t.Errorf("failed to start test server: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
time.Sleep(time.Millisecond * 30)
|
||||||
|
addr := TestServerAddr + ":" + fmt.Sprint(serverPort)
|
||||||
|
testHookTLSConfig = func() *tls.Config { return &tls.Config{InsecureSkipVerify: true} }
|
||||||
|
|
||||||
|
_, err := QuickSend(addr, NewAuthData("username", "password"), sender, rcpts, subject, body)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected QuickSend to fail during DialAndSend")
|
||||||
|
}
|
||||||
|
expect := `failed to dial and send message: send failed: sending SMTP MAIL FROM command: 500 ` +
|
||||||
|
`5.5.2 Error: fail on MAIL FROM`
|
||||||
|
if !strings.EqualFold(err.Error(), expect) {
|
||||||
|
t.Errorf("expected error to contain %s, got %s", expect, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("QuickSend fails on server address without port", func(t *testing.T) {
|
||||||
|
addr := TestServerAddr
|
||||||
|
_, err := QuickSend(addr, NewAuthData("username", "password"), sender, rcpts, subject, body)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected QuickSend to fail with invalid server address")
|
||||||
|
}
|
||||||
|
expect := "failed to split host and port from address: address 127.0.0.1: missing port in address"
|
||||||
|
if !strings.Contains(err.Error(), expect) {
|
||||||
|
t.Errorf("expected error to contain %s, got %s", expect, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("QuickSend fails on server address with invalid port", func(t *testing.T) {
|
||||||
|
addr := TestServerAddr + ":invalid"
|
||||||
|
_, err := QuickSend(addr, NewAuthData("username", "password"), sender, rcpts, subject, body)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected QuickSend to fail with invalid server port")
|
||||||
|
}
|
||||||
|
expect := `failed to convert port to int: strconv.Atoi: parsing "invalid": invalid syntax`
|
||||||
|
if !strings.Contains(err.Error(), expect) {
|
||||||
|
t.Errorf("expected error to contain %s, got %s", expect, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("QuickSend fails on nil TLS config (test hook only)", func(t *testing.T) {
|
||||||
|
addr := TestServerAddr + ":587"
|
||||||
|
testHookTLSConfig = func() *tls.Config { return nil }
|
||||||
|
defer func() {
|
||||||
|
testHookTLSConfig = nil
|
||||||
|
}()
|
||||||
|
_, err := QuickSend(addr, NewAuthData("username", "password"), sender, rcpts, subject, body)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected QuickSend to fail with nil-tlsConfig")
|
||||||
|
}
|
||||||
|
expect := `failed to set TLS config: invalid TLS config`
|
||||||
|
if !strings.Contains(err.Error(), expect) {
|
||||||
|
t.Errorf("expected error to contain %s, got %s", expect, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("QuickSend fails with invalid from address", func(t *testing.T) {
|
||||||
|
addr := TestServerAddr + ":587"
|
||||||
|
invalid := "invalid-fromdomain.tld"
|
||||||
|
_, err := QuickSend(addr, NewAuthData("username", "password"), invalid, rcpts, subject, body)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected QuickSend to fail with invalid from address")
|
||||||
|
}
|
||||||
|
expect := `failed to set MAIL FROM address: failed to parse mail address "invalid-fromdomain.tld": ` +
|
||||||
|
`mail: missing '@' or angle-addr`
|
||||||
|
if !strings.Contains(err.Error(), expect) {
|
||||||
|
t.Errorf("expected error to contain %s, got %s", expect, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("QuickSend fails with invalid from address", func(t *testing.T) {
|
||||||
|
addr := TestServerAddr + ":587"
|
||||||
|
invalid := []string{"invalid-todomain.tld"}
|
||||||
|
_, err := QuickSend(addr, NewAuthData("username", "password"), sender, invalid, subject, body)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected QuickSend to fail with invalid to address")
|
||||||
|
}
|
||||||
|
expect := `failed to set RCPT TO address: failed to parse mail address "invalid-todomain.tld": ` +
|
||||||
|
`mail: missing '@' or angle-add`
|
||||||
|
if !strings.Contains(err.Error(), expect) {
|
||||||
|
t.Errorf("expected error to contain %s, got %s", expect, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -5,45 +5,81 @@
|
||||||
package mail
|
package mail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestRandomStringSecure tests the randomStringSecure method
|
// TestRandomStringSecure tests the randomStringSecure method
|
||||||
func TestRandomStringSecure(t *testing.T) {
|
func TestRandomStringSecure(t *testing.T) {
|
||||||
tt := []struct {
|
t.Run("randomStringSecure with varying length", func(t *testing.T) {
|
||||||
testName string
|
tt := []struct {
|
||||||
length int
|
testName string
|
||||||
mustNotMatch string
|
length int
|
||||||
}{
|
mustNotMatch string
|
||||||
{"20 chars", 20, "'"},
|
}{
|
||||||
{"100 chars", 100, "'"},
|
{"20 chars", 20, "'"},
|
||||||
{"1000 chars", 1000, "'"},
|
{"100 chars", 100, "'"},
|
||||||
}
|
{"1000 chars", 1000, "'"},
|
||||||
|
}
|
||||||
|
|
||||||
for _, tc := range tt {
|
for _, tc := range tt {
|
||||||
t.Run(tc.testName, func(t *testing.T) {
|
t.Run(tc.testName, func(t *testing.T) {
|
||||||
rs, err := randomStringSecure(tc.length)
|
rs, err := randomStringSecure(tc.length)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("random string generation failed: %s", err)
|
t.Errorf("random string generation failed: %s", err)
|
||||||
}
|
}
|
||||||
if strings.Contains(rs, tc.mustNotMatch) {
|
if strings.Contains(rs, tc.mustNotMatch) {
|
||||||
t.Errorf("random string contains unexpected character. got: %s, not-expected: %s",
|
t.Errorf("random string contains unexpected character. got: %s, not-expected: %s",
|
||||||
rs, tc.mustNotMatch)
|
rs, tc.mustNotMatch)
|
||||||
}
|
}
|
||||||
if len(rs) != tc.length {
|
if len(rs) != tc.length {
|
||||||
t.Errorf("random string length does not match. expected: %d, got: %d", tc.length, len(rs))
|
t.Errorf("random string length does not match. expected: %d, got: %d", tc.length, len(rs))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
t.Run("randomStringSecure fails on broken rand Reader (first read)", func(t *testing.T) {
|
||||||
|
defaultRandReader := rand.Reader
|
||||||
|
t.Cleanup(func() { rand.Reader = defaultRandReader })
|
||||||
|
rand.Reader = &randReader{failon: 1}
|
||||||
|
if _, err := randomStringSecure(22); err == nil {
|
||||||
|
t.Fatalf("expected failure on broken rand Reader")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("randomStringSecure fails on broken rand Reader (second read)", func(t *testing.T) {
|
||||||
|
defaultRandReader := rand.Reader
|
||||||
|
t.Cleanup(func() { rand.Reader = defaultRandReader })
|
||||||
|
rand.Reader = &randReader{failon: 0}
|
||||||
|
if _, err := randomStringSecure(22); err == nil {
|
||||||
|
t.Fatalf("expected failure on broken rand Reader")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkGenerator_RandomStringSecure(b *testing.B) {
|
func BenchmarkGenerator_RandomStringSecure(b *testing.B) {
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
_, err := randomStringSecure(22)
|
_, err := randomStringSecure(10)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Errorf("RandomStringFromCharRange() failed: %s", err)
|
b.Errorf("RandomStringFromCharRange() failed: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// randReader is type that satisfies the io.Reader interface. It can fail on a specific read
|
||||||
|
// operations and is therefore useful to test consecutive reads with errors
|
||||||
|
type randReader struct {
|
||||||
|
failon uint8
|
||||||
|
call uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read implements the io.Reader interface for the randReader type
|
||||||
|
func (r *randReader) Read(p []byte) (int, error) {
|
||||||
|
if r.call == r.failon {
|
||||||
|
r.call++
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
return 0, errors.New("broken reader")
|
||||||
|
}
|
||||||
|
|
90
senderror.go
90
senderror.go
|
@ -6,6 +6,8 @@ package mail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -60,11 +62,13 @@ const (
|
||||||
// details about the affected message, a list of errors, the recipient list, and whether
|
// details about the affected message, a list of errors, the recipient list, and whether
|
||||||
// the error is temporary or permanent. It also includes a reason code for the error.
|
// the error is temporary or permanent. It also includes a reason code for the error.
|
||||||
type SendError struct {
|
type SendError struct {
|
||||||
affectedMsg *Msg
|
affectedMsg *Msg
|
||||||
errlist []error
|
errcode int
|
||||||
isTemp bool
|
enhancedStatusCode string
|
||||||
rcpt []string
|
errlist []error
|
||||||
Reason SendErrReason
|
isTemp bool
|
||||||
|
rcpt []string
|
||||||
|
Reason SendErrReason
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendErrReason represents a comparable reason on why the delivery failed
|
// SendErrReason represents a comparable reason on why the delivery failed
|
||||||
|
@ -81,7 +85,7 @@ type SendErrReason int
|
||||||
// Returns:
|
// Returns:
|
||||||
// - A string representing the error message.
|
// - A string representing the error message.
|
||||||
func (e *SendError) Error() string {
|
func (e *SendError) Error() string {
|
||||||
if e.Reason > 10 {
|
if e.Reason > ErrAmbiguous {
|
||||||
return "unknown reason"
|
return "unknown reason"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,7 +97,7 @@ func (e *SendError) Error() string {
|
||||||
errMessage.WriteRune(' ')
|
errMessage.WriteRune(' ')
|
||||||
errMessage.WriteString(e.errlist[i].Error())
|
errMessage.WriteString(e.errlist[i].Error())
|
||||||
if i != len(e.errlist)-1 {
|
if i != len(e.errlist)-1 {
|
||||||
errMessage.WriteString(", ")
|
errMessage.WriteString(",")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -175,6 +179,42 @@ func (e *SendError) Msg() *Msg {
|
||||||
return e.affectedMsg
|
return e.affectedMsg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnhancedStatusCode returns the enhanced status code of the server response if the
|
||||||
|
// server supports it, as described in RFC 2034.
|
||||||
|
//
|
||||||
|
// This function retrieves the enhanced status code of an error returned by the server. This
|
||||||
|
// requires that the receiving server supports this SMTP extension as described in RFC 2034.
|
||||||
|
// Since this is the SendError interface, we only collect status codes for error responses,
|
||||||
|
// meaning 4xx or 5xx. If the server does not support the ENHANCEDSTATUSCODES extension or
|
||||||
|
// the error did not include an enhanced status code, it will return an empty string.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - The enhanced status code as returned by the server, or an empty string is not supported.
|
||||||
|
//
|
||||||
|
// References:
|
||||||
|
// - https://datatracker.ietf.org/doc/html/rfc2034
|
||||||
|
func (e *SendError) EnhancedStatusCode() string {
|
||||||
|
if e == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return e.enhancedStatusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorCode returns the error code of the server response.
|
||||||
|
//
|
||||||
|
// This function retrieves the error code the error returned by the server. The error code will
|
||||||
|
// start with 5 on permanent errors and with 4 on a temporary error. If the error is not returned
|
||||||
|
// by the server, but is generated by go-mail, the code will be 0.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - The error code as returned by the server, or 0 if not a server error.
|
||||||
|
func (e *SendError) ErrorCode() int {
|
||||||
|
if e == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return e.errcode
|
||||||
|
}
|
||||||
|
|
||||||
// String satisfies the fmt.Stringer interface for the SendErrReason type.
|
// String satisfies the fmt.Stringer interface for the SendErrReason type.
|
||||||
//
|
//
|
||||||
// This function converts the SendErrReason into a human-readable string representation based
|
// This function converts the SendErrReason into a human-readable string representation based
|
||||||
|
@ -224,3 +264,39 @@ func (r SendErrReason) String() string {
|
||||||
func isTempError(err error) bool {
|
func isTempError(err error) bool {
|
||||||
return err.Error()[0] == '4'
|
return err.Error()[0] == '4'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func errorCode(err error) int {
|
||||||
|
rootErr := errors.Unwrap(err)
|
||||||
|
if rootErr != nil {
|
||||||
|
err = rootErr
|
||||||
|
}
|
||||||
|
firstrune := err.Error()[0]
|
||||||
|
if firstrune < 52 || firstrune > 53 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
code := err.Error()[0:3]
|
||||||
|
errcode, cerr := strconv.Atoi(code)
|
||||||
|
if cerr != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return errcode
|
||||||
|
}
|
||||||
|
|
||||||
|
func enhancedStatusCode(err error, supported bool) string {
|
||||||
|
if err == nil || !supported {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
rootErr := errors.Unwrap(err)
|
||||||
|
if rootErr != nil {
|
||||||
|
err = rootErr
|
||||||
|
}
|
||||||
|
firstrune := err.Error()[0]
|
||||||
|
if firstrune != 50 && firstrune != 52 && firstrune != 53 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
re, rerr := regexp.Compile(`\b([245])\.\d{1,3}\.\d{1,3}\b`)
|
||||||
|
if rerr != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return re.FindString(err.Error())
|
||||||
|
}
|
||||||
|
|
|
@ -13,156 +13,354 @@ import (
|
||||||
|
|
||||||
// TestSendError_Error tests the SendError and SendErrReason error handling methods
|
// TestSendError_Error tests the SendError and SendErrReason error handling methods
|
||||||
func TestSendError_Error(t *testing.T) {
|
func TestSendError_Error(t *testing.T) {
|
||||||
tl := []struct {
|
t.Run("TestSendError_Error with various reasons", func(t *testing.T) {
|
||||||
n string
|
tests := []struct {
|
||||||
r SendErrReason
|
name string
|
||||||
te bool
|
reason SendErrReason
|
||||||
}{
|
isTemp bool
|
||||||
{"ErrGetSender/temp", ErrGetSender, true},
|
}{
|
||||||
{"ErrGetSender/perm", ErrGetSender, false},
|
{"ErrGetSender/temp", ErrGetSender, true},
|
||||||
{"ErrGetRcpts/temp", ErrGetRcpts, true},
|
{"ErrGetSender/perm", ErrGetSender, false},
|
||||||
{"ErrGetRcpts/perm", ErrGetRcpts, false},
|
{"ErrGetRcpts/temp", ErrGetRcpts, true},
|
||||||
{"ErrSMTPMailFrom/temp", ErrSMTPMailFrom, true},
|
{"ErrGetRcpts/perm", ErrGetRcpts, false},
|
||||||
{"ErrSMTPMailFrom/perm", ErrSMTPMailFrom, false},
|
{"ErrSMTPMailFrom/temp", ErrSMTPMailFrom, true},
|
||||||
{"ErrSMTPRcptTo/temp", ErrSMTPRcptTo, true},
|
{"ErrSMTPMailFrom/perm", ErrSMTPMailFrom, false},
|
||||||
{"ErrSMTPRcptTo/perm", ErrSMTPRcptTo, false},
|
{"ErrSMTPRcptTo/temp", ErrSMTPRcptTo, true},
|
||||||
{"ErrSMTPData/temp", ErrSMTPData, true},
|
{"ErrSMTPRcptTo/perm", ErrSMTPRcptTo, false},
|
||||||
{"ErrSMTPData/perm", ErrSMTPData, false},
|
{"ErrSMTPData/temp", ErrSMTPData, true},
|
||||||
{"ErrSMTPDataClose/temp", ErrSMTPDataClose, true},
|
{"ErrSMTPData/perm", ErrSMTPData, false},
|
||||||
{"ErrSMTPDataClose/perm", ErrSMTPDataClose, false},
|
{"ErrSMTPDataClose/temp", ErrSMTPDataClose, true},
|
||||||
{"ErrSMTPReset/temp", ErrSMTPReset, true},
|
{"ErrSMTPDataClose/perm", ErrSMTPDataClose, false},
|
||||||
{"ErrSMTPReset/perm", ErrSMTPReset, false},
|
{"ErrSMTPReset/temp", ErrSMTPReset, true},
|
||||||
{"ErrWriteContent/temp", ErrWriteContent, true},
|
{"ErrSMTPReset/perm", ErrSMTPReset, false},
|
||||||
{"ErrWriteContent/perm", ErrWriteContent, false},
|
{"ErrWriteContent/temp", ErrWriteContent, true},
|
||||||
{"ErrConnCheck/temp", ErrConnCheck, true},
|
{"ErrWriteContent/perm", ErrWriteContent, false},
|
||||||
{"ErrConnCheck/perm", ErrConnCheck, false},
|
{"ErrConnCheck/temp", ErrConnCheck, true},
|
||||||
{"ErrNoUnencoded/temp", ErrNoUnencoded, true},
|
{"ErrConnCheck/perm", ErrConnCheck, false},
|
||||||
{"ErrNoUnencoded/perm", ErrNoUnencoded, false},
|
{"ErrNoUnencoded/temp", ErrNoUnencoded, true},
|
||||||
{"ErrAmbiguous/temp", ErrAmbiguous, true},
|
{"ErrNoUnencoded/perm", ErrNoUnencoded, false},
|
||||||
{"ErrAmbiguous/perm", ErrAmbiguous, false},
|
{"ErrAmbiguous/temp", ErrAmbiguous, true},
|
||||||
{"Unknown/temp", 9999, true},
|
{"ErrAmbiguous/perm", ErrAmbiguous, false},
|
||||||
{"Unknown/perm", 9999, false},
|
{"Unknown/temp", 9999, true},
|
||||||
}
|
{"Unknown/perm", 9999, false},
|
||||||
|
}
|
||||||
for _, tt := range tl {
|
for _, tt := range tests {
|
||||||
t.Run(tt.n, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if err := returnSendError(tt.r, tt.te); err != nil {
|
err := returnSendError(tt.reason, tt.isTemp)
|
||||||
exp := &SendError{Reason: tt.r, isTemp: tt.te}
|
if err == nil {
|
||||||
if !errors.Is(err, exp) {
|
t.Fatalf("error expected, got nil")
|
||||||
t.Errorf("error mismatch, expected: %s (temp: %t), got: %s (temp: %t)", tt.r, tt.te,
|
|
||||||
exp.Error(), exp.isTemp)
|
|
||||||
}
|
}
|
||||||
if !strings.Contains(fmt.Sprintf("%s", err), tt.r.String()) {
|
want := &SendError{Reason: tt.reason, isTemp: tt.isTemp}
|
||||||
|
if !errors.Is(err, want) {
|
||||||
|
t.Errorf("error mismatch, expected: %s (temp: %t), got: %s (temp: %t)",
|
||||||
|
tt.reason, tt.isTemp, want.Error(), want.isTemp)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), tt.reason.String()) {
|
||||||
t.Errorf("error string mismatch, expected: %s, got: %s",
|
t.Errorf("error string mismatch, expected: %s, got: %s",
|
||||||
tt.r.String(), fmt.Sprintf("%s", err))
|
tt.reason.String(), err.Error())
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
|
t.Run("TestSendError_Error with multiple errors", func(t *testing.T) {
|
||||||
|
message := testMessage(t)
|
||||||
|
err := &SendError{
|
||||||
|
affectedMsg: message,
|
||||||
|
errlist: []error{ErrNoRcptAddresses, ErrNoFromAddress},
|
||||||
|
rcpt: []string{"<toni.tester@domain.tld>", "<tina.tester@domain.tld>"},
|
||||||
|
Reason: ErrAmbiguous,
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "ambiguous reason, check Msg.SendError for message specific reasons") {
|
||||||
|
t.Errorf("error string mismatch, expected: ambiguous reason, check Msg.SendError for message "+
|
||||||
|
"specific reasons, got: %s", err.Error())
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "no recipient addresses set, no FROM address set") {
|
||||||
|
t.Errorf("error string mismatch, expected: no recipient addresses set, no FROM address set, got: %s",
|
||||||
|
err.Error())
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "affected recipient(s): <toni.tester@domain.tld>, "+
|
||||||
|
"<tina.tester@domain.tld>") {
|
||||||
|
t.Errorf("error string mismatch, expected: affected recipient(s): <toni.tester@domain.tld>, "+
|
||||||
|
"<tina.tester@domain.tld>, got: %s", err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendError_Is(t *testing.T) {
|
||||||
|
t.Run("TestSendError_Is errors match", func(t *testing.T) {
|
||||||
|
err1 := returnSendError(ErrAmbiguous, false)
|
||||||
|
err2 := returnSendError(ErrAmbiguous, false)
|
||||||
|
if !errors.Is(err1, err2) {
|
||||||
|
t.Error("error mismatch, expected ErrAmbiguous to be equal to ErrAmbiguous")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("TestSendError_Is errors mismatch", func(t *testing.T) {
|
||||||
|
err1 := returnSendError(ErrAmbiguous, false)
|
||||||
|
err2 := returnSendError(ErrSMTPMailFrom, false)
|
||||||
|
if errors.Is(err1, err2) {
|
||||||
|
t.Error("error mismatch, ErrAmbiguous should not be equal to ErrSMTPMailFrom")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("TestSendError_Is on nil", func(t *testing.T) {
|
||||||
|
var err *SendError
|
||||||
|
if err.Is(ErrNoFromAddress) {
|
||||||
|
t.Error("expected false on nil-senderror")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSendError_IsTemp(t *testing.T) {
|
func TestSendError_IsTemp(t *testing.T) {
|
||||||
var se *SendError
|
t.Run("TestSendError_IsTemp is true", func(t *testing.T) {
|
||||||
err1 := returnSendError(ErrAmbiguous, true)
|
err := returnSendError(ErrAmbiguous, true)
|
||||||
if !errors.As(err1, &se) {
|
if err == nil {
|
||||||
t.Errorf("error mismatch, expected error to be of type *SendError")
|
t.Fatalf("error expected, got nil")
|
||||||
return
|
}
|
||||||
}
|
var sendErr *SendError
|
||||||
if errors.As(err1, &se) && !se.IsTemp() {
|
if !errors.As(err, &sendErr) {
|
||||||
t.Errorf("error mismatch, expected temporary error")
|
t.Fatal("error expected to be of type *SendError")
|
||||||
return
|
}
|
||||||
}
|
if !sendErr.IsTemp() {
|
||||||
err2 := returnSendError(ErrAmbiguous, false)
|
t.Errorf("expected temporary error, got: temperr: %t", sendErr.IsTemp())
|
||||||
if !errors.As(err2, &se) {
|
}
|
||||||
t.Errorf("error mismatch, expected error to be of type *SendError")
|
})
|
||||||
return
|
t.Run("TestSendError_IsTemp is false", func(t *testing.T) {
|
||||||
}
|
err := returnSendError(ErrAmbiguous, false)
|
||||||
if errors.As(err2, &se) && se.IsTemp() {
|
if err == nil {
|
||||||
t.Errorf("error mismatch, expected non-temporary error")
|
t.Fatalf("error expected, got nil")
|
||||||
return
|
}
|
||||||
}
|
var sendErr *SendError
|
||||||
}
|
if !errors.As(err, &sendErr) {
|
||||||
|
t.Fatal("error expected to be of type *SendError")
|
||||||
func TestSendError_IsTempNil(t *testing.T) {
|
}
|
||||||
var se *SendError
|
if sendErr.IsTemp() {
|
||||||
if se.IsTemp() {
|
t.Errorf("expected permanent error, got: temperr: %t", sendErr.IsTemp())
|
||||||
t.Error("expected false on nil-senderror")
|
}
|
||||||
}
|
})
|
||||||
|
t.Run("TestSendError_IsTemp is nil", func(t *testing.T) {
|
||||||
|
var se *SendError
|
||||||
|
if se.IsTemp() {
|
||||||
|
t.Error("expected false on nil-senderror")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSendError_MessageID(t *testing.T) {
|
func TestSendError_MessageID(t *testing.T) {
|
||||||
var se *SendError
|
t.Run("TestSendError_MessageID message ID is set", func(t *testing.T) {
|
||||||
err := returnSendError(ErrAmbiguous, false)
|
var sendErr *SendError
|
||||||
if !errors.As(err, &se) {
|
err := returnSendError(ErrAmbiguous, false)
|
||||||
t.Errorf("error mismatch, expected error to be of type *SendError")
|
if !errors.As(err, &sendErr) {
|
||||||
return
|
t.Fatal("error mismatch, expected error to be of type *SendError")
|
||||||
}
|
|
||||||
if errors.As(err, &se) {
|
|
||||||
if se.MessageID() == "" {
|
|
||||||
t.Errorf("sendError expected message-id, but got empty string")
|
|
||||||
}
|
}
|
||||||
if !strings.EqualFold(se.MessageID(), "<this.is.a.message.id>") {
|
if sendErr.MessageID() == "" {
|
||||||
|
t.Error("sendError expected message-id, but got empty string")
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sendErr.MessageID(), "<this.is.a.message.id>") {
|
||||||
t.Errorf("sendError message-id expected: %s, but got: %s", "<this.is.a.message.id>",
|
t.Errorf("sendError message-id expected: %s, but got: %s", "<this.is.a.message.id>",
|
||||||
se.MessageID())
|
sendErr.MessageID())
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
t.Run("TestSendError_MessageID message ID is not set", func(t *testing.T) {
|
||||||
|
var sendErr *SendError
|
||||||
func TestSendError_MessageIDNil(t *testing.T) {
|
message := testMessage(t)
|
||||||
var se *SendError
|
err := &SendError{
|
||||||
if se.MessageID() != "" {
|
affectedMsg: message,
|
||||||
t.Error("expected empty string on nil-senderror")
|
errlist: []error{ErrNoRcptAddresses},
|
||||||
}
|
rcpt: []string{"<toni.tester@domain.tld>", "<tina.tester@domain.tld>"},
|
||||||
|
Reason: ErrAmbiguous,
|
||||||
|
}
|
||||||
|
if !errors.As(err, &sendErr) {
|
||||||
|
t.Fatal("error mismatch, expected error to be of type *SendError")
|
||||||
|
}
|
||||||
|
if sendErr.MessageID() != "" {
|
||||||
|
t.Errorf("sendError expected empty message-id, got: %s", sendErr.MessageID())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("TestSendError_MessageID on nil error should return empty", func(t *testing.T) {
|
||||||
|
var sendErr *SendError
|
||||||
|
if sendErr.MessageID() != "" {
|
||||||
|
t.Error("expected empty message-id on nil-senderror")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSendError_Msg(t *testing.T) {
|
func TestSendError_Msg(t *testing.T) {
|
||||||
var se *SendError
|
t.Run("TestSendError_Msg message is set", func(t *testing.T) {
|
||||||
err := returnSendError(ErrAmbiguous, false)
|
var sendErr *SendError
|
||||||
if !errors.As(err, &se) {
|
err := returnSendError(ErrAmbiguous, false)
|
||||||
t.Errorf("error mismatch, expected error to be of type *SendError")
|
if !errors.As(err, &sendErr) {
|
||||||
return
|
t.Fatal("error mismatch, expected error to be of type *SendError")
|
||||||
}
|
|
||||||
if errors.As(err, &se) {
|
|
||||||
if se.Msg() == nil {
|
|
||||||
t.Errorf("sendError expected msg pointer, but got nil")
|
|
||||||
}
|
}
|
||||||
from := se.Msg().GetFromString()
|
msg := sendErr.Msg()
|
||||||
|
if msg == nil {
|
||||||
|
t.Fatalf("sendError expected msg pointer, but got nil")
|
||||||
|
}
|
||||||
|
from := msg.GetFromString()
|
||||||
if len(from) == 0 {
|
if len(from) == 0 {
|
||||||
t.Errorf("sendError expected msg from, but got empty string")
|
t.Fatal("sendError expected msg from, but got empty string")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if !strings.EqualFold(from[0], "<toni.tester@domain.tld>") {
|
if !strings.EqualFold(from[0], "<toni.tester@domain.tld>") {
|
||||||
t.Errorf("sendError message from expected: %s, but got: %s", "<toni.tester@domain.tld>",
|
t.Errorf("sendError message from expected: %s, but got: %s", "<toni.tester@domain.tld>",
|
||||||
from[0])
|
from[0])
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
t.Run("TestSendError_Msg message is not set", func(t *testing.T) {
|
||||||
|
var sendErr *SendError
|
||||||
|
err := &SendError{
|
||||||
|
errlist: []error{ErrNoRcptAddresses},
|
||||||
|
rcpt: []string{"<toni.tester@domain.tld>", "<tina.tester@domain.tld>"},
|
||||||
|
Reason: ErrAmbiguous,
|
||||||
|
}
|
||||||
|
if !errors.As(err, &sendErr) {
|
||||||
|
t.Fatal("error mismatch, expected error to be of type *SendError")
|
||||||
|
}
|
||||||
|
if sendErr.Msg() != nil {
|
||||||
|
t.Errorf("sendError expected nil msg pointer, got: %v", sendErr.Msg())
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSendError_MsgNil(t *testing.T) {
|
func TestSendError_EnhancedStatusCode(t *testing.T) {
|
||||||
var se *SendError
|
t.Run("SendError with no enhanced status code", func(t *testing.T) {
|
||||||
if se.Msg() != nil {
|
err := &SendError{
|
||||||
t.Error("expected nil on nil-senderror")
|
errlist: []error{ErrNoRcptAddresses},
|
||||||
}
|
rcpt: []string{"<toni.tester@domain.tld>", "<tina.tester@domain.tld>"},
|
||||||
|
Reason: ErrAmbiguous,
|
||||||
|
}
|
||||||
|
if err.EnhancedStatusCode() != "" {
|
||||||
|
t.Errorf("expected empty enhanced status code, got: %s", err.EnhancedStatusCode())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("SendError with enhanced status code", func(t *testing.T) {
|
||||||
|
err := &SendError{
|
||||||
|
errlist: []error{ErrNoRcptAddresses},
|
||||||
|
rcpt: []string{"<toni.tester@domain.tld>", "<tina.tester@domain.tld>"},
|
||||||
|
Reason: ErrAmbiguous,
|
||||||
|
enhancedStatusCode: "5.7.1",
|
||||||
|
}
|
||||||
|
if err.EnhancedStatusCode() != "5.7.1" {
|
||||||
|
t.Errorf("expected enhanced status code: %s, got: %s", "5.7.1", err.EnhancedStatusCode())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("enhanced status code on nil error should return empty string", func(t *testing.T) {
|
||||||
|
var err *SendError
|
||||||
|
if err.EnhancedStatusCode() != "" {
|
||||||
|
t.Error("expected empty enhanced status code on nil-senderror")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSendError_IsFail(t *testing.T) {
|
func TestSendError_ErrorCode(t *testing.T) {
|
||||||
err1 := returnSendError(ErrAmbiguous, false)
|
t.Run("ErrorCode with a go-mail error should return 0", func(t *testing.T) {
|
||||||
err2 := returnSendError(ErrSMTPMailFrom, false)
|
err := &SendError{
|
||||||
if errors.Is(err1, err2) {
|
errlist: []error{ErrNoRcptAddresses},
|
||||||
t.Errorf("error mismatch, ErrAmbiguous should not be equal to ErrSMTPMailFrom")
|
rcpt: []string{"<toni.tester@domain.tld>", "<tina.tester@domain.tld>"},
|
||||||
}
|
Reason: ErrAmbiguous,
|
||||||
|
errcode: errorCode(ErrNoRcptAddresses),
|
||||||
|
}
|
||||||
|
if err.ErrorCode() != 0 {
|
||||||
|
t.Errorf("expected error code: %d, got: %d", 0, err.ErrorCode())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("SendError with permanent error", func(t *testing.T) {
|
||||||
|
err := &SendError{
|
||||||
|
errlist: []error{ErrNoRcptAddresses},
|
||||||
|
rcpt: []string{"<toni.tester@domain.tld>", "<tina.tester@domain.tld>"},
|
||||||
|
Reason: ErrAmbiguous,
|
||||||
|
errcode: errorCode(errors.New("535 5.7.8 Error: authentication failed")),
|
||||||
|
}
|
||||||
|
if err.ErrorCode() != 535 {
|
||||||
|
t.Errorf("expected error code: %d, got: %d", 535, err.ErrorCode())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("SendError with temporary error", func(t *testing.T) {
|
||||||
|
err := &SendError{
|
||||||
|
errlist: []error{ErrNoRcptAddresses},
|
||||||
|
rcpt: []string{"<toni.tester@domain.tld>", "<tina.tester@domain.tld>"},
|
||||||
|
Reason: ErrAmbiguous,
|
||||||
|
errcode: errorCode(errors.New("441 4.1.0 Server currently unavailable")),
|
||||||
|
}
|
||||||
|
if err.ErrorCode() != 441 {
|
||||||
|
t.Errorf("expected error code: %d, got: %d", 441, err.ErrorCode())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("error code on nil error should return 0", func(t *testing.T) {
|
||||||
|
var err *SendError
|
||||||
|
if err.ErrorCode() != 0 {
|
||||||
|
t.Error("expected 0 error code on nil-senderror")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSendError_ErrorMulti(t *testing.T) {
|
func TestSendError_errorCode(t *testing.T) {
|
||||||
expected := `ambiguous reason, check Msg.SendError for message specific reasons, ` +
|
t.Run("errorCode with a go-mail error should return 0", func(t *testing.T) {
|
||||||
`affected recipient(s): <email1@domain.tld>, <email2@domain.tld>`
|
code := errorCode(ErrNoRcptAddresses)
|
||||||
err := &SendError{
|
if code != 0 {
|
||||||
Reason: ErrAmbiguous, isTemp: false, affectedMsg: nil,
|
t.Errorf("expected error code: %d, got: %d", 0, code)
|
||||||
rcpt: []string{"<email1@domain.tld>", "<email2@domain.tld>"},
|
}
|
||||||
}
|
})
|
||||||
if err.Error() != expected {
|
t.Run("errorCode with permanent error", func(t *testing.T) {
|
||||||
t.Errorf("error mismatch, expected: %s, got: %s", expected, err.Error())
|
code := errorCode(errors.New("535 5.7.8 Error: authentication failed"))
|
||||||
}
|
if code != 535 {
|
||||||
|
t.Errorf("expected error code: %d, got: %d", 535, code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("errorCode with temporary error", func(t *testing.T) {
|
||||||
|
code := errorCode(errors.New("443 4.1.0 Server currently unavailable"))
|
||||||
|
if code != 443 {
|
||||||
|
t.Errorf("expected error code: %d, got: %d", 443, code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("errorCode with wrapper error", func(t *testing.T) {
|
||||||
|
code := errorCode(fmt.Errorf("an error occured: %w", errors.New("443 4.1.0 Server currently unavailable")))
|
||||||
|
if code != 443 {
|
||||||
|
t.Errorf("expected error code: %d, got: %d", 443, code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("errorCode with non-4xx and non-5xx error", func(t *testing.T) {
|
||||||
|
code := errorCode(errors.New("220 2.1.0 This is not an error"))
|
||||||
|
if code != 0 {
|
||||||
|
t.Errorf("expected error code: %d, got: %d", 0, code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("errorCode with non 3-digit code", func(t *testing.T) {
|
||||||
|
code := errorCode(errors.New("4xx 4.1.0 The status code is invalid"))
|
||||||
|
if code != 0 {
|
||||||
|
t.Errorf("expected error code: %d, got: %d", 0, code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendError_enhancedStatusCode(t *testing.T) {
|
||||||
|
t.Run("enhancedStatusCode with nil error should return empty string", func(t *testing.T) {
|
||||||
|
code := enhancedStatusCode(nil, true)
|
||||||
|
if code != "" {
|
||||||
|
t.Errorf("expected empty enhanced status code, got: %s", code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("enhancedStatusCode with error but no support should return empty string", func(t *testing.T) {
|
||||||
|
code := enhancedStatusCode(errors.New("553 5.5.3 something went wrong"), false)
|
||||||
|
if code != "" {
|
||||||
|
t.Errorf("expected empty enhanced status code, got: %s", code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("enhancedStatusCode with error and support", func(t *testing.T) {
|
||||||
|
code := enhancedStatusCode(errors.New("553 5.5.3 something went wrong"), true)
|
||||||
|
if code != "5.5.3" {
|
||||||
|
t.Errorf("expected enhanced status code: %s, got: %s", "5.5.3", code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("enhancedStatusCode with wrapped error and support", func(t *testing.T) {
|
||||||
|
code := enhancedStatusCode(fmt.Errorf("this error is wrapped: %w", errors.New("553 5.5.3 something went wrong")), true)
|
||||||
|
if code != "5.5.3" {
|
||||||
|
t.Errorf("expected enhanced status code: %s, got: %s", "5.5.3", code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("enhancedStatusCode with 3xx error", func(t *testing.T) {
|
||||||
|
code := enhancedStatusCode(errors.New("300 3.0.0 i don't know what i'm doing"), true)
|
||||||
|
if code != "" {
|
||||||
|
t.Errorf("expected enhanced status code to be empty, got: %s", code)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// returnSendError is a helper method to retunr a SendError with a specific reason
|
// returnSendError is a helper method to retunr a SendError with a specific reason
|
||||||
|
@ -173,6 +371,5 @@ func returnSendError(r SendErrReason, t bool) error {
|
||||||
message.Subject("This is the subject")
|
message.Subject("This is the subject")
|
||||||
message.SetBodyString(TypeTextPlain, "This is the message body")
|
message.SetBodyString(TypeTextPlain, "This is the message body")
|
||||||
message.SetMessageIDWithValue("this.is.a.message.id")
|
message.SetMessageIDWithValue("this.is.a.message.id")
|
||||||
|
|
||||||
return &SendError{Reason: r, isTemp: t, affectedMsg: message}
|
return &SendError{Reason: r, isTemp: t, affectedMsg: message}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,8 +36,8 @@ type loginAuth struct {
|
||||||
// LoginAuth will only send the credentials if the connection is using TLS
|
// LoginAuth will only send the credentials if the connection is using TLS
|
||||||
// or is connected to localhost. Otherwise authentication will fail with an
|
// or is connected to localhost. Otherwise authentication will fail with an
|
||||||
// error, without sending the credentials.
|
// error, without sending the credentials.
|
||||||
func LoginAuth(username, password, host string, allowUnEnc bool) Auth {
|
func LoginAuth(username, password, host string, allowUnenc bool) Auth {
|
||||||
return &loginAuth{username, password, host, 0, allowUnEnc}
|
return &loginAuth{username, password, host, 0, allowUnenc}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start begins the SMTP authentication process by validating server's TLS status and hostname.
|
// Start begins the SMTP authentication process by validating server's TLS status and hostname.
|
||||||
|
|
|
@ -28,8 +28,8 @@ type plainAuth struct {
|
||||||
// PlainAuth will only send the credentials if the connection is using TLS
|
// PlainAuth will only send the credentials if the connection is using TLS
|
||||||
// or is connected to localhost. Otherwise authentication will fail with an
|
// or is connected to localhost. Otherwise authentication will fail with an
|
||||||
// error, without sending the credentials.
|
// error, without sending the credentials.
|
||||||
func PlainAuth(identity, username, password, host string, allowUnEnc bool) Auth {
|
func PlainAuth(identity, username, password, host string, allowUnenc bool) Auth {
|
||||||
return &plainAuth{identity, username, password, host, allowUnEnc}
|
return &plainAuth{identity, username, password, host, allowUnenc}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
|
func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
|
||||||
|
|
|
@ -154,7 +154,7 @@ func (a *scramAuth) initialClientMessage() ([]byte, error) {
|
||||||
connState := a.tlsConnState
|
connState := a.tlsConnState
|
||||||
bindData := connState.TLSUnique
|
bindData := connState.TLSUnique
|
||||||
|
|
||||||
// crypto/tl: no tls-unique channel binding value for this tls connection, possibly due to missing
|
// crypto/tls: no tls-unique channel binding value for this tls connection, possibly due to missing
|
||||||
// extended master key support and/or resumed connection
|
// extended master key support and/or resumed connection
|
||||||
// RFC9266:122 tls-unique not defined for tls 1.3 and later
|
// RFC9266:122 tls-unique not defined for tls 1.3 and later
|
||||||
if bindData == nil || connState.Version >= tls.VersionTLS13 {
|
if bindData == nil || connState.Version >= tls.VersionTLS13 {
|
||||||
|
@ -308,10 +308,7 @@ func (a *scramAuth) normalizeUsername() (string, error) {
|
||||||
func (a *scramAuth) normalizeString(s string) (string, error) {
|
func (a *scramAuth) normalizeString(s string) (string, error) {
|
||||||
s, err := precis.OpaqueString.String(s)
|
s, err := precis.OpaqueString.String(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failled to normalize string: %w", err)
|
return "", fmt.Errorf("failed to normalize string: %w", err)
|
||||||
}
|
|
||||||
if s == "" {
|
|
||||||
return "", errors.New("normalized string is empty")
|
|
||||||
}
|
}
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -587,7 +587,9 @@ func (c *Client) SetLogger(l log.Logger) {
|
||||||
if l == nil {
|
if l == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
c.mutex.Lock()
|
||||||
c.logger = l
|
c.logger = l
|
||||||
|
c.mutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLogAuthData enables logging of authentication data in the Client.
|
// SetLogAuthData enables logging of authentication data in the Client.
|
||||||
|
@ -599,12 +601,16 @@ func (c *Client) SetLogAuthData() {
|
||||||
|
|
||||||
// SetDSNMailReturnOption sets the DSN mail return option for the Mail method
|
// SetDSNMailReturnOption sets the DSN mail return option for the Mail method
|
||||||
func (c *Client) SetDSNMailReturnOption(d string) {
|
func (c *Client) SetDSNMailReturnOption(d string) {
|
||||||
|
c.mutex.Lock()
|
||||||
c.dsnmrtype = d
|
c.dsnmrtype = d
|
||||||
|
c.mutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDSNRcptNotifyOption sets the DSN recipient notify option for the Mail method
|
// SetDSNRcptNotifyOption sets the DSN recipient notify option for the Mail method
|
||||||
func (c *Client) SetDSNRcptNotifyOption(d string) {
|
func (c *Client) SetDSNRcptNotifyOption(d string) {
|
||||||
|
c.mutex.Lock()
|
||||||
c.dsnrntype = d
|
c.dsnrntype = d
|
||||||
|
c.mutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasConnection checks if the client has an active connection.
|
// HasConnection checks if the client has an active connection.
|
||||||
|
@ -620,6 +626,9 @@ func (c *Client) HasConnection() bool {
|
||||||
func (c *Client) UpdateDeadline(timeout time.Duration) error {
|
func (c *Client) UpdateDeadline(timeout time.Duration) error {
|
||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
|
if c.conn == nil {
|
||||||
|
return errors.New("smtp: client has no connection")
|
||||||
|
}
|
||||||
if err := c.conn.SetDeadline(time.Now().Add(timeout)); err != nil {
|
if err := c.conn.SetDeadline(time.Now().Add(timeout)); err != nil {
|
||||||
return fmt.Errorf("smtp: failed to update deadline: %w", err)
|
return fmt.Errorf("smtp: failed to update deadline: %w", err)
|
||||||
}
|
}
|
||||||
|
|
59
smtp/smtp_121_test.go
Normal file
59
smtp/smtp_121_test.go
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
|
||||||
|
// SPDX-FileCopyrightText: Copyright (c) 2022-2023 The go-mail Authors
|
||||||
|
//
|
||||||
|
// Original net/smtp code from the Go stdlib by the Go Authors.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// LICENSE file that can be found in this directory.
|
||||||
|
//
|
||||||
|
// go-mail specific modifications by the go-mail Authors.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
// See [PROJECT ROOT]/LICENSES directory for more information.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause AND MIT
|
||||||
|
|
||||||
|
//go:build go1.21
|
||||||
|
// +build go1.21
|
||||||
|
|
||||||
|
package smtp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/wneessen/go-mail/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClient_SetDebugLog_JSON(t *testing.T) {
|
||||||
|
t.Run("set debug loggging to on should not override logger", func(t *testing.T) {
|
||||||
|
client := &Client{logger: log.NewJSON(os.Stderr, log.LevelDebug)}
|
||||||
|
client.SetDebugLog(true)
|
||||||
|
if !client.debug {
|
||||||
|
t.Fatalf("expected debug log to be true")
|
||||||
|
}
|
||||||
|
if client.logger == nil {
|
||||||
|
t.Fatalf("expected logger to be defined")
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(fmt.Sprintf("%T", client.logger), "*log.JSONlog") {
|
||||||
|
t.Errorf("expected logger to be of type *log.JSONlog, got: %T", client.logger)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_SetLogger_JSON(t *testing.T) {
|
||||||
|
t.Run("set logger to JSONlog logger", func(t *testing.T) {
|
||||||
|
client := &Client{}
|
||||||
|
client.SetLogger(log.NewJSON(os.Stderr, log.LevelDebug))
|
||||||
|
if !strings.EqualFold(fmt.Sprintf("%T", client.logger), "*log.JSONlog") {
|
||||||
|
t.Errorf("expected logger to be of type *log.JSONlog, got: %T", client.logger)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("nil logger should just return and not set/override", func(t *testing.T) {
|
||||||
|
client := &Client{logger: log.NewJSON(os.Stderr, log.LevelDebug)}
|
||||||
|
client.SetLogger(nil)
|
||||||
|
if !strings.EqualFold(fmt.Sprintf("%T", client.logger), "*log.JSONlog") {
|
||||||
|
t.Errorf("expected logger to be of type *log.JSONlog, got: %T", client.logger)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
5822
smtp/smtp_test.go
5822
smtp/smtp_test.go
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
||||||
# SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
# SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: CC0-1.0
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
sonar.projectKey=go-mail
|
sonar.projectKey=go-mail
|
||||||
sonar.go.coverage.reportPaths=cov.out
|
sonar.go.coverage.reportPaths=cov.out
|
||||||
|
|
Loading…
Reference in a new issue