mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-22 22:00:49 +01:00
Compare commits
155 commits
7da89ac3e1
...
dda48b395d
Author | SHA1 | Date | |
---|---|---|---|
|
dda48b395d | ||
7b315e5fe9 | |||
|
7f7bf80e39 | ||
|
12076cf64a | ||
|
295390999e | ||
|
43ba8e3af2 | ||
|
5913fc1540 | ||
|
6ea0974156 | ||
|
2c2ee4c1fb | ||
|
4700691380 | ||
|
b4370ded12 | ||
0ee1449850 | |||
72a9f68444 | |||
|
44d6a3333f | ||
|
4d0e3e2215 | ||
8faac3d101 | |||
5874911c91 | |||
5c8b2fc371 | |||
cdb9463ec8 | |||
a94e721161 | |||
46ca42e1b7 | |||
f0388ec600 | |||
6ce5c2a860 | |||
5d79ff69c3 | |||
cd90c3ddf3 | |||
e640f2df46 | |||
295155ba67 | |||
0b105048e6 | |||
3333c784a6 | |||
ac7fa5771a | |||
dab9cc947a | |||
2c1082fe42 | |||
3e8706d52e | |||
756269644e | |||
d6426063ba | |||
b4197a136e | |||
01278ccb30 | |||
eafb9cb17e | |||
864c593208 | |||
4890d9130b | |||
b37f8995da | |||
c520925457 | |||
3d5435c138 | |||
1dcdad9da1 | |||
78e2857782 | |||
c186cba2c2 | |||
682f7a6ca5 | |||
cd4c0194dc | |||
a820ba3cee | |||
96466facdd | |||
493f8fc657 | |||
94f47d4369 | |||
476130d6e3 | |||
a0a7f74121 | |||
ecd0bff5ad | |||
5653df373b | |||
869e8db6c5 | |||
fa3c6f956e | |||
159c1bf850 | |||
9163943684 | |||
fbbf17acd0 | |||
48b469faf7 | |||
adcb8ac41d | |||
dfdadc5da2 | |||
8942b08424 | |||
972a3c51c7 | |||
d900f5403e | |||
eeaee3f60a | |||
bae0ac6cde | |||
3e5c93a418 | |||
a34f400a05 | |||
779a3f3942 | |||
6cd3cfd2f7 | |||
aab04672f8 | |||
f7c12d412b | |||
ef3da39840 | |||
92c411454b | |||
59e91eb936 | |||
6a9c8bb56b | |||
ea90352ef4 | |||
e8739b88b0 | |||
b9888929f8 | |||
711ce2ac65 | |||
84f562554c | |||
d931050a6f | |||
04023a1a25 | |||
|
fe36f3b294 | ||
28103ede26 | |||
19dcba620a | |||
6f10892d0b | |||
8f596ffae7 | |||
f80b4dd8ac | |||
|
94ed5646c5 | ||
ff5454a61f | |||
4c8c0d855e | |||
03062c5183 | |||
a8e89a1258 | |||
e4dd62475a | |||
580981b158 | |||
a41639ec07 | |||
d19c2fd87d | |||
c8a8e9772a | |||
2bde374d2c | |||
97ad132965 | |||
b7fa04e0cb | |||
0c3bf239f1 | |||
cbba4d83d1 | |||
|
3f3b21348f | ||
|
e037df43a7 | ||
c8e45477bb | |||
761e205049 | |||
9d70283af9 | |||
93752280aa | |||
547f78dbee | |||
9bafa969b8 | |||
72b3f53eb7 | |||
986a988c5d | |||
f823112a4d | |||
15b9ddf067 | |||
5058fd5222 | |||
8838414c38 | |||
b69ad27de3 | |||
7499bae3eb | |||
324be9d032 | |||
bcf7084982 | |||
abab0af2a3 | |||
687843ee53 | |||
cace4890bc | |||
e5b87db448 | |||
5b5991f17d | |||
e8f3c444e6 | |||
27838f5b1f | |||
3013975c6a | |||
c797f0be17 | |||
b96badbd59 | |||
738f43e289 | |||
ebd171005d | |||
4f1a60760d | |||
e8fc6cd78f | |||
9069c9cdff | |||
|
46cf2ed498 | ||
|
79f22fb722 | ||
|
4b60557518 | ||
|
9bdb741e05 | ||
|
2368113872 | ||
|
12edb2724b | ||
|
3154649420 | ||
|
65b9fd07da | ||
|
894936092e | ||
|
41a81966d8 | ||
|
94942ed383 | ||
|
e0a59dba6d | ||
|
6fda661dc7 | ||
|
158c1b0458 | ||
|
07d9654ce7 |
53 changed files with 6379 additions and 1043 deletions
|
@ -14,12 +14,10 @@ freebsd_task:
|
||||||
image_family: freebsd-14-0
|
image_family: freebsd-14-0
|
||||||
|
|
||||||
env:
|
env:
|
||||||
TEST_ALLOW_SEND: 0
|
|
||||||
TEST_SKIP_SENDMAIL: 1
|
TEST_SKIP_SENDMAIL: 1
|
||||||
|
|
||||||
pkginstall_script:
|
pkginstall_script:
|
||||||
- pkg update -f
|
|
||||||
- pkg install -y go
|
- pkg install -y go
|
||||||
|
|
||||||
test_script:
|
test_script:
|
||||||
- go test -v -race -cover -shuffle=on ./...
|
- go test -race -cover -shuffle=on ./...
|
14
.github/workflows/codecov.yml
vendored
14
.github/workflows/codecov.yml
vendored
|
@ -10,7 +10,7 @@ on:
|
||||||
paths:
|
paths:
|
||||||
- '**.go'
|
- '**.go'
|
||||||
- 'go.*'
|
- 'go.*'
|
||||||
- '.github/**'
|
- '.github/workflows/codecov.yml'
|
||||||
- 'codecov.yml'
|
- 'codecov.yml'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
|
@ -18,7 +18,7 @@ on:
|
||||||
paths:
|
paths:
|
||||||
- '**.go'
|
- '**.go'
|
||||||
- 'go.*'
|
- 'go.*'
|
||||||
- '.github/**'
|
- '.github/workflows/codecov.yml'
|
||||||
- 'codecov.yml'
|
- 'codecov.yml'
|
||||||
env:
|
env:
|
||||||
TEST_HOST: ${{ secrets.TEST_HOST }}
|
TEST_HOST: ${{ secrets.TEST_HOST }}
|
||||||
|
@ -27,6 +27,10 @@ env:
|
||||||
TEST_SMTPAUTH_USER: ${{ secrets.TEST_USER }}
|
TEST_SMTPAUTH_USER: ${{ secrets.TEST_USER }}
|
||||||
TEST_SMTPAUTH_PASS: ${{ secrets.TEST_PASS }}
|
TEST_SMTPAUTH_PASS: ${{ secrets.TEST_PASS }}
|
||||||
TEST_SMTPAUTH_TYPE: "LOGIN"
|
TEST_SMTPAUTH_TYPE: "LOGIN"
|
||||||
|
TEST_ONLINE_SCRAM: "1"
|
||||||
|
TEST_HOST_SCRAM: ${{ secrets.TEST_HOST_SCRAM }}
|
||||||
|
TEST_USER_SCRAM: ${{ secrets.TEST_USER_SCRAM }}
|
||||||
|
TEST_PASS_SCRAM: ${{ secrets.TEST_PASS_SCRAM }}
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
|
@ -36,7 +40,7 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
go: ['1.19', '1.20', '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@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
|
@ -55,9 +59,9 @@ jobs:
|
||||||
sudo apt-get -y install sendmail; which sendmail
|
sudo apt-get -y install sendmail; which sendmail
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: |
|
run: |
|
||||||
go test -v -race --coverprofile=coverage.coverprofile --covermode=atomic ./...
|
go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./...
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: success() && matrix.go == '1.23' && matrix.os == 'ubuntu-latest'
|
if: success() && matrix.go == '1.23' && matrix.os == 'ubuntu-latest'
|
||||||
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
|
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
||||||
|
|
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
|
@ -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@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10
|
uses: github/codeql-action/init@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
|
||||||
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@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10
|
uses: github/codeql-action/autobuild@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
|
||||||
|
|
||||||
# ℹ️ 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@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10
|
uses: github/codeql-action/analyze@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
|
||||||
|
|
2
.github/workflows/golangci-lint.yml
vendored
2
.github/workflows/golangci-lint.yml
vendored
|
@ -29,7 +29,7 @@ jobs:
|
||||||
go-version: '1.23'
|
go-version: '1.23'
|
||||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0
|
uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1
|
||||||
with:
|
with:
|
||||||
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
|
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
|
||||||
version: latest
|
version: latest
|
||||||
|
|
2
.github/workflows/govulncheck.yml
vendored
2
.github/workflows/govulncheck.yml
vendored
|
@ -18,4 +18,4 @@ jobs:
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
- name: Run govulncheck
|
- name: Run govulncheck
|
||||||
uses: golang/govulncheck-action@dd0578b371c987f96d1185abb54344b44352bd58 # v1.0.3
|
uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4
|
45
.github/workflows/offline-tests.yml
vendored
Normal file
45
.github/workflows/offline-tests.yml
vendored
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
|
name: Offline tests workflow
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- '**.go'
|
||||||
|
- 'go.*'
|
||||||
|
- '.github/workflows/offline-tests.yml'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- '**.go'
|
||||||
|
- 'go.*'
|
||||||
|
- '.github/workflows/offline-tests.yml'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
go: ['1.19', '1.20', '1.21', '1.22', '1.23']
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||||
|
- name: Setup go
|
||||||
|
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go }}
|
||||||
|
- name: Run Tests
|
||||||
|
run: |
|
||||||
|
go test -race -shuffle=on ./...
|
4
.github/workflows/scorecards.yml
vendored
4
.github/workflows/scorecards.yml
vendored
|
@ -67,7 +67,7 @@ jobs:
|
||||||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||||
# format to the repository Actions tab.
|
# format to the repository Actions tab.
|
||||||
- name: "Upload artifact"
|
- name: "Upload artifact"
|
||||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
uses: actions/upload-artifact@84480863f228bb9747b473957fcc9e309aa96097 # v4.4.2
|
||||||
with:
|
with:
|
||||||
name: SARIF file
|
name: SARIF file
|
||||||
path: results.sarif
|
path: results.sarif
|
||||||
|
@ -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@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10
|
uses: github/codeql-action/upload-sarif@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
|
23
.github/workflows/sonarqube.yml
vendored
23
.github/workflows/sonarqube.yml
vendored
|
@ -10,17 +10,18 @@ permissions:
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main # or the name of your main branch
|
- main
|
||||||
|
paths:
|
||||||
|
- '**.go'
|
||||||
|
- 'go.*'
|
||||||
|
- '.github/workflows/sonarqube.yml'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main # or the name of your main branch
|
- main
|
||||||
env:
|
paths:
|
||||||
TEST_HOST: ${{ secrets.TEST_HOST }}
|
- '**.go'
|
||||||
TEST_FROM: ${{ secrets.TEST_USER }}
|
- 'go.*'
|
||||||
TEST_ALLOW_SEND: "1"
|
- '.github/workflows/sonarqube.yml'
|
||||||
TEST_SMTPAUTH_USER: ${{ secrets.TEST_USER }}
|
|
||||||
TEST_SMTPAUTH_PASS: ${{ secrets.TEST_PASS }}
|
|
||||||
TEST_SMTPAUTH_TYPE: "LOGIN"
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build
|
name: Build
|
||||||
|
@ -38,11 +39,11 @@ jobs:
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
|
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
|
||||||
with:
|
with:
|
||||||
go-version: '1.23.x'
|
go-version: '1.23'
|
||||||
|
|
||||||
- name: Run unit Tests
|
- name: Run unit Tests
|
||||||
run: |
|
run: |
|
||||||
go test -v -race --coverprofile=./cov.out ./...
|
go test -shuffle=on -race --coverprofile=./cov.out ./...
|
||||||
|
|
||||||
- uses: sonarsource/sonarqube-scan-action@884b79409bbd464b2a59edc326a4b77dc56b2195 # master
|
- uses: sonarsource/sonarqube-scan-action@884b79409bbd464b2a59edc326a4b77dc56b2195 # master
|
||||||
env:
|
env:
|
||||||
|
|
10
.reuse/dep5
10
.reuse/dep5
|
@ -1,10 +0,0 @@
|
||||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
|
||||||
Upstream-Name: go-mail
|
|
||||||
Upstream-Contact: Winni Neessen <winni@neessen.dev>
|
|
||||||
Source: https://github.com/wneessen/go-mail
|
|
||||||
|
|
||||||
# Sample paragraph, commented out:
|
|
||||||
#
|
|
||||||
# Files: src/*
|
|
||||||
# Copyright: $YEAR $NAME <$CONTACT>
|
|
||||||
# License: ...
|
|
55
README.md
55
README.md
|
@ -18,40 +18,41 @@ SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
<p align="center"><img src="./assets/gopher2.svg" width="250" alt="go-mail logo"/></p>
|
<p align="center"><img src="./assets/gopher2.svg" width="250" alt="go-mail logo"/></p>
|
||||||
|
|
||||||
The main idea of this library was to provide a simple interface to sending mails for
|
The main idea of this library was to provide a simple interface for sending mails to
|
||||||
my [JS-Mailer](https://github.com/wneessen/js-mailer) project. It quickly evolved into a full-fledged mail library.
|
my [JS-Mailer](https://github.com/wneessen/js-mailer) project. It quickly evolved into a full-fledged mail library.
|
||||||
|
|
||||||
go-mail follows idiomatic Go style and best practice. It's only dependency is the Go Standard Library. It combines a lot
|
go-mail follows idiomatic Go style and best practice. It has a small dependency footprint by mainly relying on the
|
||||||
of functionality from the standard library to give easy and convenient access to mail and SMTP related tasks.
|
Go Standard Library and the Go extended packages. It combines a lot of functionality from the standard library to
|
||||||
|
give easy and convenient access to mail and SMTP related tasks.
|
||||||
|
|
||||||
Parts of this library (especially some parts of [msgwriter.go](msgwriter.go)) have been forked/ported from the
|
In the early days, parts of this library (especially some parts of [msgwriter.go](msgwriter.go)) had been
|
||||||
[go-mail/mail](https://github.com/go-mail/mail) respectively [go-gomail/gomail](https://github.com/go-gomail/gomail)
|
forked/ported from [go-mail/mail](https://github.com/go-mail/mail) and respectively [go-gomail/gomail](https://github.com/go-gomail/gomail). Today
|
||||||
which both seems to not be maintained anymore.
|
most of the ported code has been refactored.
|
||||||
|
|
||||||
The smtp package of go-mail is forked from the original Go stdlib's `net/smtp` and then extended by the go-mail
|
The `smtp` package of go-mail has been forked from the original Go stdlib's `net/smtp` package and has then been extended
|
||||||
team.
|
by the go-mail team to fit the packages needs (more SMTP Auth methods, logging, concurrency-safety, etc.).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
Some of the features of this library:
|
Here are some highlights of go-mail's featureset:
|
||||||
|
|
||||||
* [X] Only Standard Library dependant
|
* [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] Explicit SSL/TLS support
|
||||||
* [X] Implicit StartTLS support with different policies
|
* [X] Implicit 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)
|
* [X] SMTP Auth support (LOGIN, PLAIN, CRAM-MD, XOAUTH2, SCRAM-SHA-1(-PLUS), SCRAM-SHA-256(-PLUS))
|
||||||
* [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] 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` or `embed.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
|
||||||
* [X] Support for requestng MDNs (RFC 8098) and DSNs (RFC 1891)
|
* [X] Support for requestng MDNs (RFC 8098) and DSNs (RFC 1891)
|
||||||
* [X] DKIM signature support via [go-mail-middlware](https://github.com/wneessen/go-mail-middleware)
|
* [X] DKIM signature support via [go-mail-middlware](https://github.com/wneessen/go-mail-middleware)
|
||||||
* [X] Message object satisfies `io.WriteTo` and `io.Reader` interfaces
|
* [X] Message object satisfies `io.WriterTo` and `io.Reader` interfaces
|
||||||
* [X] Support for Go's `html/template` and `text/template` (as message body, alternative part or attachment/emebed)
|
* [X] Support for Go's `html/template` and `text/template` (as message body, alternative part or attachment/emebed)
|
||||||
* [X] Output to file support which allows storing mail messages as e. g. `.eml` files to disk to open them in a MUA
|
* [X] Output to file support which allows storing mail messages as e. g. `.eml` files to disk to open them in a MUA
|
||||||
* [X] Debug logging of SMTP traffic
|
* [X] Debug logging of SMTP traffic
|
||||||
|
@ -76,7 +77,8 @@ We guarantee that go-mail will always support the last four releases of Go. With
|
||||||
the user a timeframe of two years to update to the next or even the latest version of Go.
|
the user a timeframe of two years to update to the next or even the latest version of Go.
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
We have a support and general discussion channel on Discord. Find us at: [#go-mail](https://discord.gg/dbfQyC4s)
|
We have a support and general discussion channel on Discord. Find us at: [#go-mail](https://discord.gg/dbfQyC4s) alternatively find us
|
||||||
|
on the [Gophers Slack](https://gophers.slack.com) in #go-mail
|
||||||
|
|
||||||
## Middleware
|
## Middleware
|
||||||
The goal of go-mail is to keep it free from 3rd party dependencies and only focus on things a mail library should
|
The goal of go-mail is to keep it free from 3rd party dependencies and only focus on things a mail library should
|
||||||
|
@ -99,15 +101,18 @@ We provide example code in both our GoDocs as well as on our official Website (s
|
||||||
check out our [Getting started](https://go-mail.dev/getting-started/introduction/) guide.
|
check out our [Getting started](https://go-mail.dev/getting-started/introduction/) guide.
|
||||||
|
|
||||||
## Authors/Contributors
|
## Authors/Contributors
|
||||||
go-mail was initially authored and developed by [Winni Neessen](https://github.com/wneessen/).
|
go-mail was initially created and developed by [Winni Neessen](https://github.com/wneessen/), but over time a lot of amazing people
|
||||||
|
contributed ot the project. Big thanks to all of them for improving the go-mail project (be it writing code, testing
|
||||||
|
code, reviewing code, writing documenation or helping to translate the website):
|
||||||
|
|
||||||
Big thanks to the following people, for contributing to the go-mail project (either in form of code or by
|
<a href="https://github.com/wneessen/go-mail/graphs/contributors">
|
||||||
reviewing code, writing documenation or helping to translate the website):
|
<img src="https://contrib.rocks/image?repo=wneessen/go-mail" />
|
||||||
* [Christian Vette](https://github.com/cvette)
|
</a>
|
||||||
* [Dhia Gharsallaoui](https://github.com/dhia-gharsallaoui)
|
|
||||||
* [inliquid](https://github.com/inliquid)
|
A huge thank you also goes to [Maria Letta](https://github.com/MariaLetta) for designing our super cool go-mail logo!
|
||||||
* [iwittkau](https://github.com/iwittkau)
|
|
||||||
* [James Elliott](https://github.com/james-d-elliott)
|
## Sponsors
|
||||||
* [Maria Letta](https://github.com/MariaLetta) (designed the go-mail logo)
|
We sincerely thank our amazing sponsors for their generous support! Your contributions do not go unnoticed and helps
|
||||||
* [Nicola Murino](https://github.com/drakkan)
|
keeping up the project!
|
||||||
* [sters](https://github.com/sters)
|
|
||||||
|
* [kolaente](https://github.com/kolaente)
|
||||||
|
|
9
REUSE.toml
Normal file
9
REUSE.toml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
version = 1
|
||||||
|
SPDX-PackageName = "go-mail"
|
||||||
|
SPDX-PackageSupplier = "Winni Neessen <winni@neessen.dev>"
|
||||||
|
SPDX-PackageDownloadLocation = "https://github.com/wneessen/go-mail"
|
||||||
|
annotations = []
|
110
auth.go
110
auth.go
|
@ -6,41 +6,131 @@ package mail
|
||||||
|
|
||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
// SMTPAuthType represents a string to any SMTP AUTH type
|
// SMTPAuthType is a type wrapper for a string type. It represents the type of SMTP authentication
|
||||||
|
// mechanism to be used.
|
||||||
type SMTPAuthType string
|
type SMTPAuthType string
|
||||||
|
|
||||||
// Supported SMTP AUTH types
|
|
||||||
const (
|
const (
|
||||||
// SMTPAuthCramMD5 is the "CRAM-MD5" SASL authentication mechanism as described in RFC 4954
|
// SMTPAuthCramMD5 is the "CRAM-MD5" SASL authentication mechanism as described in RFC 4954.
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc4954/
|
||||||
|
//
|
||||||
|
// CRAM-MD5 is not secure by modern standards. The vulnerabilities of MD5 and the lack of
|
||||||
|
// advanced security features make it inappropriate for protecting sensitive communications
|
||||||
|
// today.
|
||||||
|
//
|
||||||
|
// It was recommended to deprecate the standard in 20 November 2008. As an alternative it
|
||||||
|
// recommends e.g. SCRAM or SASL Plain protected by TLS instead.
|
||||||
|
//
|
||||||
|
// https://datatracker.ietf.org/doc/html/draft-ietf-sasl-crammd5-to-historic-00.html
|
||||||
SMTPAuthCramMD5 SMTPAuthType = "CRAM-MD5"
|
SMTPAuthCramMD5 SMTPAuthType = "CRAM-MD5"
|
||||||
|
|
||||||
// SMTPAuthLogin is the "LOGIN" SASL authentication mechanism
|
// SMTPAuthCustom is a custom SMTP AUTH mechanism provided by the user. If a user provides
|
||||||
|
// a custom smtp.Auth function to the Client, the Client will its smtpAuthType to this type.
|
||||||
|
//
|
||||||
|
// Do not use this SMTPAuthType without setting a custom smtp.Auth function on the Client.
|
||||||
|
SMTPAuthCustom SMTPAuthType = "CUSTOM"
|
||||||
|
|
||||||
|
// SMTPAuthLogin is the "LOGIN" SASL authentication mechanism. This authentication mechanism
|
||||||
|
// does not have an official RFC that could be followed. There is a spec by Microsoft and an
|
||||||
|
// IETF draft. The IETF draft is more lax than the MS spec, therefore we follow the I-D, which
|
||||||
|
// automatically matches the MS spec.
|
||||||
|
//
|
||||||
|
// Since the "LOGIN" SASL authentication mechansim transmits the username and password in
|
||||||
|
// plaintext over the internet connection, we only allow this mechanism over a TLS secured
|
||||||
|
// connection.
|
||||||
|
//
|
||||||
|
// https://msopenspecs.azureedge.net/files/MS-XLOGIN/%5bMS-XLOGIN%5d.pdf
|
||||||
|
//
|
||||||
|
// https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
|
||||||
SMTPAuthLogin SMTPAuthType = "LOGIN"
|
SMTPAuthLogin SMTPAuthType = "LOGIN"
|
||||||
|
|
||||||
// SMTPAuthNoAuth is equivalent to performing no authentication at all. It is a convenience
|
// SMTPAuthNoAuth is equivalent to performing no authentication at all. It is a convenience
|
||||||
// option and should not be used. Instead, for mail servers that do no support/require
|
// option and should not be used. Instead, for mail servers that do no support/require
|
||||||
// authentication, the Client should not be used with the WithSMTPAuth option
|
// authentication, the Client should not be passed the WithSMTPAuth option at all.
|
||||||
SMTPAuthNoAuth SMTPAuthType = ""
|
SMTPAuthNoAuth SMTPAuthType = ""
|
||||||
|
|
||||||
// SMTPAuthPlain is the "PLAIN" authentication mechanism as described in RFC 4616
|
// SMTPAuthPlain is the "PLAIN" authentication mechanism as described in RFC 4616.
|
||||||
|
//
|
||||||
|
// Since the "PLAIN" SASL authentication mechansim transmits the username and password in
|
||||||
|
// plaintext over the internet connection, we only allow this mechanism over a TLS secured
|
||||||
|
// connection.
|
||||||
|
//
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc4616/
|
||||||
SMTPAuthPlain SMTPAuthType = "PLAIN"
|
SMTPAuthPlain SMTPAuthType = "PLAIN"
|
||||||
|
|
||||||
// SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism.
|
// SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism.
|
||||||
// https://developers.google.com/gmail/imap/xoauth2-protocol
|
// https://developers.google.com/gmail/imap/xoauth2-protocol
|
||||||
SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2"
|
SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2"
|
||||||
|
|
||||||
|
// SMTPAuthSCRAMSHA1 is the "SCRAM-SHA-1" SASL authentication mechanism as described in RFC 5802.
|
||||||
|
//
|
||||||
|
// SCRAM-SHA-1 is still considered secure for certain applications, particularly when used as part
|
||||||
|
// of a challenge-response authentication mechanism (as we use it). However, it is generally
|
||||||
|
// recommended to prefer stronger alternatives like SCRAM-SHA-256(-PLUS), as SHA-1 has known
|
||||||
|
// vulnerabilities in other contexts, although it remains effective in HMAC constructions.
|
||||||
|
//
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc5802
|
||||||
|
SMTPAuthSCRAMSHA1 SMTPAuthType = "SCRAM-SHA-1"
|
||||||
|
|
||||||
|
// SMTPAuthSCRAMSHA1PLUS is the "SCRAM-SHA-1-PLUS" SASL authentication mechanism as described in RFC 5802.
|
||||||
|
//
|
||||||
|
// SCRAM-SHA-X-PLUS authentication require TLS channel bindings to protect against MitM attacks and
|
||||||
|
// to guarantee that the integrity of the transport layer is preserved throughout the authentication
|
||||||
|
// process. Therefore we only allow this mechansim over a TLS secured connection.
|
||||||
|
//
|
||||||
|
// SCRAM-SHA-1-PLUS is still considered secure for certain applications, particularly when used as part
|
||||||
|
// of a challenge-response authentication mechanism (as we use it). However, it is generally
|
||||||
|
// recommended to prefer stronger alternatives like SCRAM-SHA-256(-PLUS), as SHA-1 has known
|
||||||
|
// vulnerabilities in other contexts, although it remains effective in HMAC constructions.
|
||||||
|
//
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc5802
|
||||||
|
SMTPAuthSCRAMSHA1PLUS SMTPAuthType = "SCRAM-SHA-1-PLUS"
|
||||||
|
|
||||||
|
// SMTPAuthSCRAMSHA256 is the "SCRAM-SHA-256" SASL authentication mechanism as described in RFC 7677.
|
||||||
|
//
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc7677
|
||||||
|
SMTPAuthSCRAMSHA256 SMTPAuthType = "SCRAM-SHA-256"
|
||||||
|
|
||||||
|
// SMTPAuthSCRAMSHA256PLUS is the "SCRAM-SHA-256-PLUS" SASL authentication mechanism as described in RFC 7677.
|
||||||
|
//
|
||||||
|
// SCRAM-SHA-X-PLUS authentication require TLS channel bindings to protect against MitM attacks and
|
||||||
|
// to guarantee that the integrity of the transport layer is preserved throughout the authentication
|
||||||
|
// process. Therefore we only allow this mechansim over a TLS secured connection.
|
||||||
|
//
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc7677
|
||||||
|
SMTPAuthSCRAMSHA256PLUS SMTPAuthType = "SCRAM-SHA-256-PLUS"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SMTP Auth related static errors
|
// SMTP Auth related static errors
|
||||||
var (
|
var (
|
||||||
// ErrPlainAuthNotSupported should be used if the target server does not support the "PLAIN" schema
|
// ErrPlainAuthNotSupported is returned when the server does not support the "PLAIN" SMTP
|
||||||
|
// authentication type.
|
||||||
ErrPlainAuthNotSupported = errors.New("server does not support SMTP AUTH type: PLAIN")
|
ErrPlainAuthNotSupported = errors.New("server does not support SMTP AUTH type: PLAIN")
|
||||||
|
|
||||||
// ErrLoginAuthNotSupported should be used if the target server does not support the "LOGIN" schema
|
// ErrLoginAuthNotSupported is returned when the server does not support the "LOGIN" SMTP
|
||||||
|
// authentication type.
|
||||||
ErrLoginAuthNotSupported = errors.New("server does not support SMTP AUTH type: LOGIN")
|
ErrLoginAuthNotSupported = errors.New("server does not support SMTP AUTH type: LOGIN")
|
||||||
|
|
||||||
// ErrCramMD5AuthNotSupported should be used if the target server does not support the "CRAM-MD5" schema
|
// ErrCramMD5AuthNotSupported is returned when the server does not support the "CRAM-MD5" SMTP
|
||||||
|
// authentication type.
|
||||||
ErrCramMD5AuthNotSupported = errors.New("server does not support SMTP AUTH type: CRAM-MD5")
|
ErrCramMD5AuthNotSupported = errors.New("server does not support SMTP AUTH type: CRAM-MD5")
|
||||||
|
|
||||||
// ErrXOauth2AuthNotSupported should be used if the target server does not support the "XOAUTH2" schema
|
// ErrXOauth2AuthNotSupported is returned when the server does not support the "XOAUTH2" schema.
|
||||||
ErrXOauth2AuthNotSupported = errors.New("server does not support SMTP AUTH type: XOAUTH2")
|
ErrXOauth2AuthNotSupported = errors.New("server does not support SMTP AUTH type: XOAUTH2")
|
||||||
|
|
||||||
|
// ErrSCRAMSHA1AuthNotSupported is returned when the server does not support the "SCRAM-SHA-1" SMTP
|
||||||
|
// authentication type.
|
||||||
|
ErrSCRAMSHA1AuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-1")
|
||||||
|
|
||||||
|
// ErrSCRAMSHA1PLUSAuthNotSupported is returned when the server does not support the "SCRAM-SHA-1-PLUS" SMTP
|
||||||
|
// authentication type.
|
||||||
|
ErrSCRAMSHA1PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-1-PLUS")
|
||||||
|
|
||||||
|
// ErrSCRAMSHA256AuthNotSupported is returned when the server does not support the "SCRAM-SHA-256" SMTP
|
||||||
|
// authentication type.
|
||||||
|
ErrSCRAMSHA256AuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256")
|
||||||
|
|
||||||
|
// ErrSCRAMSHA256PLUSAuthNotSupported is returned when the server does not support the "SCRAM-SHA-256-PLUS" SMTP
|
||||||
|
// authentication type.
|
||||||
|
ErrSCRAMSHA256PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256-PLUS")
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,21 +9,39 @@ import (
|
||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrNoOutWriter is an error message that should be used if a Base64LineBreaker has no out io.Writer set
|
// newlineBytes is a byte slice representation of the SingleNewLine constant used for line breaking
|
||||||
|
// in encoding processes.
|
||||||
|
var newlineBytes = []byte(SingleNewLine)
|
||||||
|
|
||||||
|
// ErrNoOutWriter is the error message returned when no io.Writer is set for Base64LineBreaker.
|
||||||
const ErrNoOutWriter = "no io.Writer set for Base64LineBreaker"
|
const ErrNoOutWriter = "no io.Writer set for Base64LineBreaker"
|
||||||
|
|
||||||
// Base64LineBreaker is a io.WriteCloser that writes Base64 encoded data streams
|
// Base64LineBreaker handles base64 encoding with the insertion of new lines after a certain number
|
||||||
// with line breaks at a given line length
|
// of characters.
|
||||||
|
//
|
||||||
|
// This struct is used to manage base64 encoding while ensuring that new lines are inserted after
|
||||||
|
// reaching a specific line length. It satisfies the io.WriteCloser interface.
|
||||||
|
//
|
||||||
|
// References:
|
||||||
|
// - https://datatracker.ietf.org/doc/html/rfc2045 (Base64 and line length limitations)
|
||||||
type Base64LineBreaker struct {
|
type Base64LineBreaker struct {
|
||||||
line [MaxBodyLength]byte
|
line [MaxBodyLength]byte
|
||||||
used int
|
used int
|
||||||
out io.Writer
|
out io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
var newlineBytes = []byte(SingleNewLine)
|
// Write writes data to the Base64LineBreaker, ensuring lines do not exceed MaxBodyLength.
|
||||||
|
//
|
||||||
// Write writes the data stream and inserts a SingleNewLine when the maximum
|
// This method writes the provided data to the Base64LineBreaker. It ensures that the written
|
||||||
// line length is reached
|
// lines do not exceed the MaxBodyLength. If the data exceeds the limit, it handles the
|
||||||
|
// continuation by splitting the data and writing new lines as necessary.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - data: A byte slice containing the data to be written.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - numBytes: The number of bytes written.
|
||||||
|
// - err: An error if one occurred during the write operation.
|
||||||
func (l *Base64LineBreaker) Write(data []byte) (numBytes int, err error) {
|
func (l *Base64LineBreaker) Write(data []byte) (numBytes int, err error) {
|
||||||
if l.out == nil {
|
if l.out == nil {
|
||||||
err = errors.New(ErrNoOutWriter)
|
err = errors.New(ErrNoOutWriter)
|
||||||
|
@ -55,8 +73,14 @@ func (l *Base64LineBreaker) Write(data []byte) (numBytes int, err error) {
|
||||||
return l.Write(data[excess:])
|
return l.Write(data[excess:])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close closes the Base64LineBreaker and writes any access data that is still
|
// Close finalizes the Base64LineBreaker, writing any remaining buffered data and appending a newline.
|
||||||
// unwritten in memory
|
//
|
||||||
|
// This method ensures that any remaining data in the buffer is written to the output and appends
|
||||||
|
// a newline. It is used to finalize the Base64LineBreaker and should be called when no more data
|
||||||
|
// is expected to be written.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - err: An error if one occurred during the final write operation.
|
||||||
func (l *Base64LineBreaker) Close() (err error) {
|
func (l *Base64LineBreaker) Close() (err error) {
|
||||||
if l.used > 0 {
|
if l.used > 0 {
|
||||||
_, err = l.out.Write(l.line[0:l.used])
|
_, err = l.out.Write(l.line[0:l.used])
|
||||||
|
|
|
@ -9,7 +9,23 @@ package mail
|
||||||
|
|
||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
// Send sends out the mail message
|
// Send attempts to send one or more Msg using the Client connection to the SMTP server.
|
||||||
|
// 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:
|
||||||
|
// - 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) error {
|
func (c *Client) Send(messages ...*Msg) error {
|
||||||
if err := c.checkConn(); err != nil {
|
if err := c.checkConn(); err != nil {
|
||||||
return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
||||||
|
|
|
@ -11,7 +11,21 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Send sends out the mail message
|
// Send attempts to send one or more Msg using the Client connection to the SMTP server.
|
||||||
|
// 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 an error wrapped in 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.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - messages: A variadic list of pointers to Msg objects to be sent.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - An error that aggregates any SendErrors encountered during the sending process; otherwise, returns nil.
|
||||||
func (c *Client) Send(messages ...*Msg) (returnErr error) {
|
func (c *Client) Send(messages ...*Msg) (returnErr error) {
|
||||||
if err := c.checkConn(); err != nil {
|
if err := c.checkConn(); err != nil {
|
||||||
returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
||||||
|
|
425
client_test.go
425
client_test.go
|
@ -27,7 +27,7 @@ const (
|
||||||
// DefaultHost is used as default hostname for the Client
|
// DefaultHost is used as default hostname for the Client
|
||||||
DefaultHost = "localhost"
|
DefaultHost = "localhost"
|
||||||
// TestRcpt is a trash mail address to send test mails to
|
// TestRcpt is a trash mail address to send test mails to
|
||||||
TestRcpt = "go-mail@mytrashmailer.com"
|
TestRcpt = "couttifaddebro-1473@yopmail.com"
|
||||||
// TestServerProto is the protocol used for the simple SMTP test server
|
// TestServerProto is the protocol used for the simple SMTP test server
|
||||||
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
|
||||||
|
@ -483,20 +483,20 @@ func TestWithDSN(t *testing.T) {
|
||||||
t.Errorf("failed to create new client: %s", err)
|
t.Errorf("failed to create new client: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !c.dsn {
|
if !c.requestDSN {
|
||||||
t.Errorf("WithDSN failed. c.dsn expected to be: %t, got: %t", true, c.dsn)
|
t.Errorf("WithDSN failed. c.requestDSN expected to be: %t, got: %t", true, c.requestDSN)
|
||||||
}
|
}
|
||||||
if c.dsnmrtype != DSNMailReturnFull {
|
if c.dsnReturnType != DSNMailReturnFull {
|
||||||
t.Errorf("WithDSN failed. c.dsnmrtype expected to be: %s, got: %s", DSNMailReturnFull,
|
t.Errorf("WithDSN failed. c.dsnReturnType expected to be: %s, got: %s", DSNMailReturnFull,
|
||||||
c.dsnmrtype)
|
c.dsnReturnType)
|
||||||
}
|
}
|
||||||
if c.dsnrntype[0] != string(DSNRcptNotifyFailure) {
|
if c.dsnRcptNotifyType[0] != string(DSNRcptNotifyFailure) {
|
||||||
t.Errorf("WithDSN failed. c.dsnrntype[0] expected to be: %s, got: %s", DSNRcptNotifyFailure,
|
t.Errorf("WithDSN failed. c.dsnRcptNotifyType[0] expected to be: %s, got: %s", DSNRcptNotifyFailure,
|
||||||
c.dsnrntype[0])
|
c.dsnRcptNotifyType[0])
|
||||||
}
|
}
|
||||||
if c.dsnrntype[1] != string(DSNRcptNotifySuccess) {
|
if c.dsnRcptNotifyType[1] != string(DSNRcptNotifySuccess) {
|
||||||
t.Errorf("WithDSN failed. c.dsnrntype[1] expected to be: %s, got: %s", DSNRcptNotifySuccess,
|
t.Errorf("WithDSN failed. c.dsnRcptNotifyType[1] expected to be: %s, got: %s", DSNRcptNotifySuccess,
|
||||||
c.dsnrntype[1])
|
c.dsnRcptNotifyType[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -519,8 +519,8 @@ func TestWithDSNMailReturnType(t *testing.T) {
|
||||||
t.Errorf("failed to create new client: %s", err)
|
t.Errorf("failed to create new client: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if string(c.dsnmrtype) != tt.want {
|
if string(c.dsnReturnType) != tt.want {
|
||||||
t.Errorf("WithDSNMailReturnType failed. Expected %s, got: %s", tt.want, string(c.dsnmrtype))
|
t.Errorf("WithDSNMailReturnType failed. Expected %s, got: %s", tt.want, string(c.dsnReturnType))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -547,11 +547,11 @@ func TestWithDSNRcptNotifyType(t *testing.T) {
|
||||||
t.Errorf("failed to create new client: %s", err)
|
t.Errorf("failed to create new client: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(c.dsnrntype) <= 0 && !tt.sf {
|
if len(c.dsnRcptNotifyType) <= 0 && !tt.sf {
|
||||||
t.Errorf("WithDSNRcptNotifyType failed. Expected at least one DSNRNType but got none")
|
t.Errorf("WithDSNRcptNotifyType failed. Expected at least one DSNRNType but got none")
|
||||||
}
|
}
|
||||||
if !tt.sf && c.dsnrntype[0] != tt.want {
|
if !tt.sf && c.dsnRcptNotifyType[0] != tt.want {
|
||||||
t.Errorf("WithDSNRcptNotifyType failed. Expected %s, got: %s", tt.want, c.dsnrntype[0])
|
t.Errorf("WithDSNRcptNotifyType failed. Expected %s, got: %s", tt.want, c.dsnRcptNotifyType[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -602,6 +602,10 @@ func TestSetSMTPAuthCustom(t *testing.T) {
|
||||||
if c.smtpAuth == nil {
|
if c.smtpAuth == nil {
|
||||||
t.Errorf("failed to set custom SMTP auth method. SMTP Auth method is empty")
|
t.Errorf("failed to set custom SMTP auth method. SMTP Auth method is empty")
|
||||||
}
|
}
|
||||||
|
if c.smtpAuthType != SMTPAuthCustom {
|
||||||
|
t.Errorf("failed to set custom SMTP auth method. SMTP Auth type is not custom: %s",
|
||||||
|
c.smtpAuthType)
|
||||||
|
}
|
||||||
p, _, err := c.smtpAuth.Start(&si)
|
p, _, err := c.smtpAuth.Start(&si)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("SMTP Auth Start() method returned error: %s", err)
|
t.Errorf("SMTP Auth Start() method returned error: %s", err)
|
||||||
|
@ -613,6 +617,32 @@ func TestSetSMTPAuthCustom(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestClient_Close_double tests if a close on an already closed connection causes an error.
|
||||||
|
func TestClient_Close_double(t *testing.T) {
|
||||||
|
c, err := getTestConnection(true)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
if err = c.DialWithContext(ctx); err != nil {
|
||||||
|
t.Errorf("failed to dial with context: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c.smtpClient == nil {
|
||||||
|
t.Errorf("DialWithContext didn't fail but no SMTP client found.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !c.smtpClient.HasConnection() {
|
||||||
|
t.Errorf("DialWithContext didn't fail but no connection found.")
|
||||||
|
}
|
||||||
|
if err = c.Close(); err != nil {
|
||||||
|
t.Errorf("failed to close connection: %s", err)
|
||||||
|
}
|
||||||
|
if err = c.Close(); err != nil {
|
||||||
|
t.Errorf("failed 2nd close connection: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestClient_DialWithContext tests the DialWithContext method for the Client object
|
// TestClient_DialWithContext tests the DialWithContext method for the Client object
|
||||||
func TestClient_DialWithContext(t *testing.T) {
|
func TestClient_DialWithContext(t *testing.T) {
|
||||||
c, err := getTestConnection(true)
|
c, err := getTestConnection(true)
|
||||||
|
@ -620,7 +650,7 @@ func TestClient_DialWithContext(t *testing.T) {
|
||||||
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if err := c.DialWithContext(ctx); err != nil {
|
if err = c.DialWithContext(ctx); err != nil {
|
||||||
t.Errorf("failed to dial with context: %s", err)
|
t.Errorf("failed to dial with context: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1836,6 +1866,299 @@ func TestClient_DialSendConcurrent_local(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClient_AuthSCRAMSHAX(t *testing.T) {
|
||||||
|
if os.Getenv("TEST_ONLINE_SCRAM") == "" {
|
||||||
|
t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests")
|
||||||
|
}
|
||||||
|
hostname := os.Getenv("TEST_HOST_SCRAM")
|
||||||
|
username := os.Getenv("TEST_USER_SCRAM")
|
||||||
|
password := os.Getenv("TEST_PASS_SCRAM")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
authtype SMTPAuthType
|
||||||
|
}{
|
||||||
|
{"SCRAM-SHA-1", SMTPAuthSCRAMSHA1},
|
||||||
|
{"SCRAM-SHA-256", SMTPAuthSCRAMSHA256},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
client, err := NewClient(hostname,
|
||||||
|
WithTLSPortPolicy(TLSMandatory),
|
||||||
|
WithSMTPAuth(tt.authtype),
|
||||||
|
WithUsername(username), WithPassword(password))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to create new client: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = client.DialWithContext(context.Background()); err != nil {
|
||||||
|
t.Errorf("failed to dial to test server: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.Close(); err != nil {
|
||||||
|
t.Errorf("failed to close server connection: %s", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_AuthLoginSuccess(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
featureSet string
|
||||||
|
}{
|
||||||
|
{"default", "250-AUTH LOGIN\r\n250-X-DEFAULT-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"},
|
||||||
|
{"mox server", "250-AUTH LOGIN\r\n250-X-MOX-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"},
|
||||||
|
{"null byte", "250-AUTH LOGIN\r\n250-X-NULLBYTE-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"},
|
||||||
|
{"bogus responses", "250-AUTH LOGIN\r\n250-X-BOGUS-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"},
|
||||||
|
{"empty responses", "250-AUTH LOGIN\r\n250-X-EMPTY-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
serverPort := TestServerPortBase + 40 + i
|
||||||
|
go func() {
|
||||||
|
if err := simpleSMTPServer(ctx, tt.featureSet, true, serverPort); err != nil {
|
||||||
|
t.Errorf("failed to start test server: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
time.Sleep(time.Millisecond * 300)
|
||||||
|
|
||||||
|
client, err := NewClient(TestServerAddr,
|
||||||
|
WithPort(serverPort),
|
||||||
|
WithTLSPortPolicy(NoTLS),
|
||||||
|
WithSMTPAuth(SMTPAuthLogin),
|
||||||
|
WithUsername("toni@tester.com"),
|
||||||
|
WithPassword("V3ryS3cr3t+"))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to create new client: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = client.DialWithContext(context.Background()); err != nil {
|
||||||
|
t.Errorf("failed to dial to test server: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.Close(); err != nil {
|
||||||
|
t.Errorf("failed to close server connection: %s", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_AuthLoginFail(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
serverPort := TestServerPortBase + 50
|
||||||
|
featureSet := "250-AUTH LOGIN\r\n250-X-DEFAULT-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
||||||
|
go func() {
|
||||||
|
if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil {
|
||||||
|
t.Errorf("failed to start test server: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
time.Sleep(time.Millisecond * 300)
|
||||||
|
|
||||||
|
client, err := NewClient(TestServerAddr,
|
||||||
|
WithPort(serverPort),
|
||||||
|
WithTLSPortPolicy(NoTLS),
|
||||||
|
WithSMTPAuth(SMTPAuthLogin),
|
||||||
|
WithUsername("toni@tester.com"),
|
||||||
|
WithPassword("InvalidPassword"))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to create new client: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = client.DialWithContext(context.Background()); err == nil {
|
||||||
|
t.Error("expected to fail to dial to test server, but it succeeded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_AuthLoginFail_noTLS(t *testing.T) {
|
||||||
|
if os.Getenv("TEST_SKIP_ONLINE") != "" {
|
||||||
|
t.Skipf("env variable TEST_SKIP_ONLINE is set. Skipping online tests")
|
||||||
|
}
|
||||||
|
th := os.Getenv("TEST_HOST")
|
||||||
|
if th == "" {
|
||||||
|
t.Skipf("no host set. Skipping online tests")
|
||||||
|
}
|
||||||
|
tp := 587
|
||||||
|
if tps := os.Getenv("TEST_PORT"); tps != "" {
|
||||||
|
tpi, err := strconv.Atoi(tps)
|
||||||
|
if err == nil {
|
||||||
|
tp = tpi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client, err := NewClient(th, WithPort(tp), WithSMTPAuth(SMTPAuthLogin), WithTLSPolicy(NoTLS))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to create new client: %s", err)
|
||||||
|
}
|
||||||
|
u := os.Getenv("TEST_SMTPAUTH_USER")
|
||||||
|
if u != "" {
|
||||||
|
client.SetUsername(u)
|
||||||
|
}
|
||||||
|
p := os.Getenv("TEST_SMTPAUTH_PASS")
|
||||||
|
if p != "" {
|
||||||
|
client.SetPassword(p)
|
||||||
|
}
|
||||||
|
// We don't want to log authentication data in tests
|
||||||
|
client.SetDebugLog(false)
|
||||||
|
|
||||||
|
if err = client.DialWithContext(context.Background()); err == nil {
|
||||||
|
t.Error("expected to fail to dial to test server, but it succeeded")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, smtp.ErrUnencrypted) {
|
||||||
|
t.Errorf("expected error to be %s, but got %s", smtp.ErrUnencrypted, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_AuthSCRAMSHAX_fail(t *testing.T) {
|
||||||
|
if os.Getenv("TEST_ONLINE_SCRAM") == "" {
|
||||||
|
t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests")
|
||||||
|
}
|
||||||
|
hostname := os.Getenv("TEST_HOST_SCRAM")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
authtype SMTPAuthType
|
||||||
|
}{
|
||||||
|
{"SCRAM-SHA-1", SMTPAuthSCRAMSHA1},
|
||||||
|
{"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS},
|
||||||
|
{"SCRAM-SHA-256", SMTPAuthSCRAMSHA256},
|
||||||
|
{"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
client, err := NewClient(hostname,
|
||||||
|
WithTLSPortPolicy(TLSMandatory),
|
||||||
|
WithSMTPAuth(tt.authtype),
|
||||||
|
WithUsername("invalid"), WithPassword("invalid"))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to create new client: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = client.DialWithContext(context.Background()); err == nil {
|
||||||
|
t.Errorf("expected error but got nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_AuthSCRAMSHAX_unsupported(t *testing.T) {
|
||||||
|
if os.Getenv("TEST_ALLOW_SEND") == "" {
|
||||||
|
t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test")
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := getTestConnection(true)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
authtype SMTPAuthType
|
||||||
|
expErr error
|
||||||
|
}{
|
||||||
|
{"SCRAM-SHA-1", SMTPAuthSCRAMSHA1, ErrSCRAMSHA1AuthNotSupported},
|
||||||
|
{"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS, ErrSCRAMSHA1PLUSAuthNotSupported},
|
||||||
|
{"SCRAM-SHA-256", SMTPAuthSCRAMSHA256, ErrSCRAMSHA256AuthNotSupported},
|
||||||
|
{"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS, ErrSCRAMSHA256PLUSAuthNotSupported},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
client.SetSMTPAuth(tt.authtype)
|
||||||
|
client.SetTLSPolicy(TLSMandatory)
|
||||||
|
if err = client.DialWithContext(context.Background()); err == nil {
|
||||||
|
t.Errorf("expected error but got nil")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, tt.expErr) {
|
||||||
|
t.Errorf("expected error %s, but got %s", tt.expErr, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_AuthSCRAMSHAXPLUS_tlsexporter(t *testing.T) {
|
||||||
|
if os.Getenv("TEST_ONLINE_SCRAM") == "" {
|
||||||
|
t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests")
|
||||||
|
}
|
||||||
|
hostname := os.Getenv("TEST_HOST_SCRAM")
|
||||||
|
username := os.Getenv("TEST_USER_SCRAM")
|
||||||
|
password := os.Getenv("TEST_PASS_SCRAM")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
authtype SMTPAuthType
|
||||||
|
}{
|
||||||
|
{"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS},
|
||||||
|
{"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
client, err := NewClient(hostname,
|
||||||
|
WithTLSPortPolicy(TLSMandatory),
|
||||||
|
WithSMTPAuth(tt.authtype),
|
||||||
|
WithUsername(username), WithPassword(password))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to create new client: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = client.DialWithContext(context.Background()); err != nil {
|
||||||
|
t.Errorf("failed to dial to test server: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.Close(); err != nil {
|
||||||
|
t.Errorf("failed to close server connection: %s", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_AuthSCRAMSHAXPLUS_tlsunique(t *testing.T) {
|
||||||
|
if os.Getenv("TEST_ONLINE_SCRAM") == "" {
|
||||||
|
t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests")
|
||||||
|
}
|
||||||
|
hostname := os.Getenv("TEST_HOST_SCRAM")
|
||||||
|
username := os.Getenv("TEST_USER_SCRAM")
|
||||||
|
password := os.Getenv("TEST_PASS_SCRAM")
|
||||||
|
tlsConfig := &tls.Config{}
|
||||||
|
tlsConfig.MaxVersion = tls.VersionTLS12
|
||||||
|
tlsConfig.ServerName = hostname
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
authtype SMTPAuthType
|
||||||
|
}{
|
||||||
|
{"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS},
|
||||||
|
{"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
client, err := NewClient(hostname,
|
||||||
|
WithTLSPortPolicy(TLSMandatory),
|
||||||
|
WithTLSConfig(tlsConfig),
|
||||||
|
WithSMTPAuth(tt.authtype),
|
||||||
|
WithUsername(username), WithPassword(password))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to create new client: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = client.DialWithContext(context.Background()); err != nil {
|
||||||
|
t.Errorf("failed to dial to test server: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.Close(); err != nil {
|
||||||
|
t.Errorf("failed to close server connection: %s", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// getTestConnection takes environment variables to establish a connection to a real
|
// getTestConnection takes environment variables to establish a connection to a real
|
||||||
// SMTP server to test all functionality that requires a connection
|
// SMTP server to test all functionality that requires a connection
|
||||||
func getTestConnection(auth bool) (*Client, error) {
|
func getTestConnection(auth bool) (*Client, error) {
|
||||||
|
@ -1878,10 +2201,10 @@ func getTestConnection(auth bool) (*Client, error) {
|
||||||
// We don't want to log authentication data in tests
|
// We don't want to log authentication data in tests
|
||||||
c.SetDebugLog(false)
|
c.SetDebugLog(false)
|
||||||
}
|
}
|
||||||
if err := c.DialWithContext(context.Background()); err != nil {
|
if err = c.DialWithContext(context.Background()); err != nil {
|
||||||
return c, fmt.Errorf("connection to test server failed: %w", err)
|
return c, fmt.Errorf("connection to test server failed: %w", err)
|
||||||
}
|
}
|
||||||
if err := c.Close(); err != nil {
|
if err = c.Close(); err != nil {
|
||||||
return c, fmt.Errorf("disconnect from test server failed: %w", err)
|
return c, fmt.Errorf("disconnect from test server failed: %w", err)
|
||||||
}
|
}
|
||||||
return c, nil
|
return c, nil
|
||||||
|
@ -2094,7 +2417,6 @@ func TestXOAuth2OK_faker(t *testing.T) {
|
||||||
"250 8BITMIME",
|
"250 8BITMIME",
|
||||||
"250 OK",
|
"250 OK",
|
||||||
"235 2.7.0 Accepted",
|
"235 2.7.0 Accepted",
|
||||||
"250 OK",
|
|
||||||
"221 OK",
|
"221 OK",
|
||||||
}
|
}
|
||||||
var wrote strings.Builder
|
var wrote strings.Builder
|
||||||
|
@ -2115,10 +2437,10 @@ func TestXOAuth2OK_faker(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to create new client: %v", err)
|
t.Fatalf("unable to create new client: %v", err)
|
||||||
}
|
}
|
||||||
if err := c.DialWithContext(context.Background()); err != nil {
|
if err = c.DialWithContext(context.Background()); err != nil {
|
||||||
t.Fatalf("unexpected dial error: %v", err)
|
t.Fatalf("unexpected dial error: %v", 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)
|
||||||
}
|
}
|
||||||
if !strings.Contains(wrote.String(), "AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n") {
|
if !strings.Contains(wrote.String(), "AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n") {
|
||||||
|
@ -2133,7 +2455,6 @@ func TestXOAuth2Unsupported_faker(t *testing.T) {
|
||||||
"250-AUTH LOGIN PLAIN",
|
"250-AUTH LOGIN PLAIN",
|
||||||
"250 8BITMIME",
|
"250 8BITMIME",
|
||||||
"250 OK",
|
"250 OK",
|
||||||
"250 OK",
|
|
||||||
"221 OK",
|
"221 OK",
|
||||||
}
|
}
|
||||||
var wrote strings.Builder
|
var wrote strings.Builder
|
||||||
|
@ -2152,18 +2473,18 @@ func TestXOAuth2Unsupported_faker(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to create new client: %v", err)
|
t.Fatalf("unable to create new client: %v", err)
|
||||||
}
|
}
|
||||||
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 {
|
} 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) != 5 {
|
if len(client) != 4 {
|
||||||
t.Fatalf("unexpected number of client requests got %d; want 5", len(client))
|
t.Fatalf("unexpected number of client requests got %d; want 5", len(client))
|
||||||
}
|
}
|
||||||
if !strings.HasPrefix(client[0], "EHLO") {
|
if !strings.HasPrefix(client[0], "EHLO") {
|
||||||
|
@ -2172,10 +2493,7 @@ func TestXOAuth2Unsupported_faker(t *testing.T) {
|
||||||
if client[1] != "NOOP" {
|
if client[1] != "NOOP" {
|
||||||
t.Fatalf("expected NOOP, got %q", client[1])
|
t.Fatalf("expected NOOP, got %q", client[1])
|
||||||
}
|
}
|
||||||
if client[2] != "NOOP" {
|
if client[2] != "QUIT" {
|
||||||
t.Fatalf("expected NOOP, got %q", client[2])
|
|
||||||
}
|
|
||||||
if client[3] != "QUIT" {
|
|
||||||
t.Fatalf("expected QUIT, got %q", client[3])
|
t.Fatalf("expected QUIT, got %q", client[3])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2311,6 +2629,51 @@ func handleTestServerConnection(connection net.Conn, featureSet string, failRese
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
_ = writeLine("235 2.7.0 Authentication successful")
|
_ = writeLine("235 2.7.0 Authentication successful")
|
||||||
|
case strings.HasPrefix(data, "AUTH LOGIN"):
|
||||||
|
var username, password string
|
||||||
|
userResp := "VXNlcm5hbWU6"
|
||||||
|
passResp := "UGFzc3dvcmQ6"
|
||||||
|
if strings.Contains(featureSet, "250-X-MOX-LOGIN") {
|
||||||
|
userResp = ""
|
||||||
|
passResp = "UGFzc3dvcmQ="
|
||||||
|
}
|
||||||
|
if strings.Contains(featureSet, "250-X-NULLBYTE-LOGIN") {
|
||||||
|
userResp = "VXNlciBuYW1lAA=="
|
||||||
|
passResp = "UGFzc3dvcmQA"
|
||||||
|
}
|
||||||
|
if strings.Contains(featureSet, "250-X-BOGUS-LOGIN") {
|
||||||
|
userResp = "Qm9ndXM="
|
||||||
|
passResp = "Qm9ndXM="
|
||||||
|
}
|
||||||
|
if strings.Contains(featureSet, "250-X-EMPTY-LOGIN") {
|
||||||
|
userResp = ""
|
||||||
|
passResp = ""
|
||||||
|
}
|
||||||
|
_ = writeLine("334 " + userResp)
|
||||||
|
|
||||||
|
ddata, derr := reader.ReadString('\n')
|
||||||
|
if derr != nil {
|
||||||
|
fmt.Printf("failed to read username data from connection: %s\n", derr)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ddata = strings.TrimSpace(ddata)
|
||||||
|
username = ddata
|
||||||
|
_ = writeLine("334 " + passResp)
|
||||||
|
|
||||||
|
ddata, derr = reader.ReadString('\n')
|
||||||
|
if derr != nil {
|
||||||
|
fmt.Printf("failed to read password data from connection: %s\n", derr)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ddata = strings.TrimSpace(ddata)
|
||||||
|
password = ddata
|
||||||
|
|
||||||
|
if !strings.EqualFold(username, "dG9uaUB0ZXN0ZXIuY29t") ||
|
||||||
|
!strings.EqualFold(password, "VjNyeVMzY3IzdCs=") {
|
||||||
|
_ = writeLine("535 5.7.8 Error: authentication failed")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
_ = writeLine("235 2.7.0 Authentication successful")
|
||||||
case strings.EqualFold(data, "DATA"):
|
case strings.EqualFold(data, "DATA"):
|
||||||
_ = writeLine("354 End data with <CR><LF>.<CR><LF>")
|
_ = writeLine("354 End data with <CR><LF>.<CR><LF>")
|
||||||
for {
|
for {
|
||||||
|
|
11
doc.go
11
doc.go
|
@ -2,8 +2,13 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
// Package mail provides a simple and easy way to send mails with Go
|
// Package mail provides an easy to use interface for formating and sending mails. go-mail follows idiomatic Go style
|
||||||
|
// and best practice. It has a small dependency footprint by mainly relying on the Go Standard Library and the Go
|
||||||
|
// extended packages. It combines a lot of functionality from the standard library to give easy and convenient access
|
||||||
|
// to mail and SMTP related tasks. It works like a programatic email client and provides lots of methods and
|
||||||
|
// functionalities you would consider standard in a MUA.
|
||||||
package mail
|
package mail
|
||||||
|
|
||||||
// VERSION is used in the default user agent string
|
// VERSION indicates the current version of the package. It is also attached to the default user
|
||||||
const VERSION = "0.4.4"
|
// agent string.
|
||||||
|
const VERSION = "0.5.0"
|
||||||
|
|
62
dummy-chain-cert.pem
Normal file
62
dummy-chain-cert.pem
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFWjCCA0KgAwIBAgIUAi7P4JOR4g8b5DMERUtZQEtw+igwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
||||||
|
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDA5MjYxNDU0MDlaFw0yNTA5
|
||||||
|
MjYxNDU0MDlaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
|
||||||
|
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB
|
||||||
|
AQUAA4ICDwAwggIKAoICAQCiLh0JJTRRBhmUyiMKALHtTOK7T20Bwy+fG0SO6RlB
|
||||||
|
c+hSuuX/n6znXcNgOBlQ2Gg3+p1on/bmcKnGN/SCiVBLpROiwxg3blQbZ7B7Jors
|
||||||
|
/MopGk0LIBOXHPtAuYbF4J6ND5Ol6sgeGjMnomwRjZlfeuBlHY345MqvcwH/lPhO
|
||||||
|
lKme+tWD+bsFh08NGS+3NdQGP6dA2bRVrPXhLXStHEmqfKO9EMVLWv+77tYhZESD
|
||||||
|
6XgnA8pWjbdr9jajCsrQWrCG3jqHtzHNxtwf7xfRwwgoUhLEvue6SBVokZGVmDhv
|
||||||
|
WdRt2sjtLcWWJCI3p7M+NXRt5qf6iu24wLBdzIDuWfgooWu5vBzNZjSTh2if6R1a
|
||||||
|
s9BdwASwy1n2HMvqpzgA+f/rXDFbvVc7WIKiuGzfWApBrL0qTCQBuSyepH0G4rQ2
|
||||||
|
sJtI5U8QOKBO76nQJq5WDQgefBX4GDI8aJ6qX+teQq1AqERUmLWx4WlTwxSo5X+d
|
||||||
|
1jY9I8f61CRKVfIRgMvAZUhm2h6RnoVIgq7G7W3HdC3RT/f758njI7oIv5bhiyqp
|
||||||
|
gyKr3cYhmn0enjP+YtjY85m51q005qzRLfaTYiwMR4qyJW4ZOEPntPs20CD+e+Pi
|
||||||
|
2JLONpRdcsSrkqZusVjm5PFy2e5RyNFXTupUH2KVrgTRHL3GG2KWF5PmBdkhQfGG
|
||||||
|
1QIDAQABo0IwQDAdBgNVHQ4EFgQUS8+HouVQ94SdgI3kV3L8Jm53P6YwHwYDVR0j
|
||||||
|
BBgwFoAUY1u7KerT8m01BvAg77PUaot2S0EwDQYJKoZIhvcNAQELBQADggIBAE14
|
||||||
|
YBa/stYwrsy/1iQ44NeQyYMPMdOC8TI1xrbW/9u1FllipECnFEGDK1N6mZ7xDEfG
|
||||||
|
un5dQ3jXQ7156Ge672374yUsN7FQ37mTyZos3Q2N/mOpVOnYJt5mIukx2MXBU3r3
|
||||||
|
UP1Jpnf9rB4kdtWXa7b1CSTkM4kraige3wZhPELwESnm4t8C34MIzHBWPbHpft05
|
||||||
|
WheDv9Zizfw+0pbJ+WNGnHF4PjR/wq9ymkLf89cqsbS9mOdPpWva8i0e7pqKnxzo
|
||||||
|
iz2ueQB4Z2Tbgp0G9ResA+2Zxk1iIQPbhtqNUZv6ROPiLAWdiVRysFJJf/19V+nZ
|
||||||
|
LIC0xw+amF6P51/fA95EGqElO4OLJTIGY27H761g7+FhTwfryLMHKknSxcfk7xoq
|
||||||
|
BMyBr7ARYnmpjee7yKOBUgSdpxb6YUcdGZwjCUIiIHlBII83DzcNILa5QvUkzMCh
|
||||||
|
xHYmPvvftJOjF8hwMfjA9MDFML9yWVm+CBNraNPh25U5uMOuIuyUBtSB5yEdPRhY
|
||||||
|
BJGrZEew0lLAWAqqASmGPDaWNBaA0HYqO70g4IyqBwIGNnaHSLVr/vT23BFRMyXf
|
||||||
|
wh5mtJmyyR3+c0po3vDX39mkIAZ2gZWprWa3Jw0dVs6cEujcVNdqeZlQw0RCa9wm
|
||||||
|
xioBxb1md2AplUQ2fG/KHnu2ZuxHA6MNYcwMJNgv
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFazCCA1OgAwIBAgIUKU+ta2rnJE/79L2Uxg4vFoF0RxYwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
||||||
|
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDA5MjYxNDUzMzlaFw0zNDA5
|
||||||
|
MjQxNDUzMzlaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
|
||||||
|
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB
|
||||||
|
AQUAA4ICDwAwggIKAoICAQDSOuYqxNsP4Gm/EDauon/SAVWj8PIKFB1OpuguHm13
|
||||||
|
b2l2G6hRuNnAmR6ewP22H/YoyYz1qRchH2qw1uizwnnSS0OY74CsJhd0PV6f4XRR
|
||||||
|
Y+6PotGDPu1fJJM4XI3HjWGdBkJSawZNWjP1dQRJPUHNRttSOrPsG3XT7TjfLjoK
|
||||||
|
jOelTqgwHUGE2n0AtQP7ZFQVn7LLBrukve8zMgivwEL1JFSlKppWFf0SUgpmQVE6
|
||||||
|
3jTAQPPrI/B5z5Ys1j2jv7mJt3/UATcTGmvPTNv94SUrO6nC3TJxKHtR30MALteo
|
||||||
|
EgH/s2O0Ax44iDENgm9p6eb+GCyTWS/eHBAJ6SU76PRymiE57/0GOqyYewuEOuIU
|
||||||
|
FYd6+gglCMe+ayfhI20njHP6RTiQpRjFy+DM8+bkcS89q0sfFSFHR5oFNbAgUgyI
|
||||||
|
bGiaWb+DmUCwSFnS0HusSU2AECqzuwiyObD3rkoqBQMj+xl6SnJU6TTcB+WD/Z5G
|
||||||
|
tqu1zTMXpo3VRts3AQSGUuSaqbeG/S+38LX+fbjeTLa6SEGJfB7/H2s64vCO/0hR
|
||||||
|
M0KEXAaTyjx0PnNKYSlCIJyA9lYea21oByNc31tkUXQjmUQpSXYayrDwzR2JAXVY
|
||||||
|
rFJLNu1Q6sZ0WqT9fj06oTas7g1g3gcl18tIapeael2jJohth0RizBxKYuLYc6Pv
|
||||||
|
1QIDAQABo1MwUTAdBgNVHQ4EFgQUY1u7KerT8m01BvAg77PUaot2S0EwHwYDVR0j
|
||||||
|
BBgwFoAUY1u7KerT8m01BvAg77PUaot2S0EwDwYDVR0TAQH/BAUwAwEB/zANBgkq
|
||||||
|
hkiG9w0BAQsFAAOCAgEAB8BpmI3v2cNlXUjhf+rldaqDfq/IJ7Ri23kCBJW4VaoW
|
||||||
|
c0UrtvLC+K5m61I1iWSUYEK85/bPw2K5dn1e8w3Q2J460Yvc7/ZT7mucZlXQxfl3
|
||||||
|
V7yqqQI7OMsY6FochYUL3+c32WQg5jllsLPlHAHBJlagf3uEqmVrvSExHNBQOVyE
|
||||||
|
/cs1i9DcTJF2A8JNPKilIObvRT103Qp2eFnW+EY9OUBb+TdQvPjxroLfK1SuOAe6
|
||||||
|
bLPBxdgvA/0raHuXeDTNsNRICIU1X5eBfZwCXKe9lRVJpIsKTYeHDN/rEmfTtehB
|
||||||
|
vz8/KkCWqwPDn/YFkNAdg3TRjqW4oW2wZ+XqbTlR2qA7szE7oMAfHxNkintxMnNm
|
||||||
|
vD2/AAP6RUw16HZk0najFWPIG9gc+O1gSks6hwn9JilAPy8mn40H2D7cedU6Ew+T
|
||||||
|
CQ02+dw2+2FLKYr1eiYPlIELsAu8kmbrjwvwy2sCf3L4fxLtPRqXFuXB2Uer9zvy
|
||||||
|
tn+RK5hJkKo/YY37I9Y9x57rpCqUfFIeYWBub07x1620ujRkL1pJPxfRNBfyh42t
|
||||||
|
beuk/XQGIvPcIkbPnmsb4gGaiRMuw+mZ7isJDoQwHUmfqL1EpOYb5mLYHkIqKaCz
|
||||||
|
8t8wTdkimIVIFSxedy7cJCCWdQ/BCyTJoQpXD69PLPzxEi/YK9pB9S8qBtfefu4=
|
||||||
|
-----END CERTIFICATE-----
|
52
dummy-child-key.pem
Normal file
52
dummy-child-key.pem
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIJQAIBADANBgkqhkiG9w0BAQEFAASCCSowggkmAgEAAoICAQCiLh0JJTRRBhmU
|
||||||
|
yiMKALHtTOK7T20Bwy+fG0SO6RlBc+hSuuX/n6znXcNgOBlQ2Gg3+p1on/bmcKnG
|
||||||
|
N/SCiVBLpROiwxg3blQbZ7B7Jors/MopGk0LIBOXHPtAuYbF4J6ND5Ol6sgeGjMn
|
||||||
|
omwRjZlfeuBlHY345MqvcwH/lPhOlKme+tWD+bsFh08NGS+3NdQGP6dA2bRVrPXh
|
||||||
|
LXStHEmqfKO9EMVLWv+77tYhZESD6XgnA8pWjbdr9jajCsrQWrCG3jqHtzHNxtwf
|
||||||
|
7xfRwwgoUhLEvue6SBVokZGVmDhvWdRt2sjtLcWWJCI3p7M+NXRt5qf6iu24wLBd
|
||||||
|
zIDuWfgooWu5vBzNZjSTh2if6R1as9BdwASwy1n2HMvqpzgA+f/rXDFbvVc7WIKi
|
||||||
|
uGzfWApBrL0qTCQBuSyepH0G4rQ2sJtI5U8QOKBO76nQJq5WDQgefBX4GDI8aJ6q
|
||||||
|
X+teQq1AqERUmLWx4WlTwxSo5X+d1jY9I8f61CRKVfIRgMvAZUhm2h6RnoVIgq7G
|
||||||
|
7W3HdC3RT/f758njI7oIv5bhiyqpgyKr3cYhmn0enjP+YtjY85m51q005qzRLfaT
|
||||||
|
YiwMR4qyJW4ZOEPntPs20CD+e+Pi2JLONpRdcsSrkqZusVjm5PFy2e5RyNFXTupU
|
||||||
|
H2KVrgTRHL3GG2KWF5PmBdkhQfGG1QIDAQABAoICAAL3IruL6/zP5DPZ9RkiL1m/
|
||||||
|
lHPhP7/sWKTfWMPf4ChX6XAHWeYraqNPvx8/bESdstLAvx3piyvcapRupN9DsVTu
|
||||||
|
SrKytjXft6OEVWFLEveHk3F8B+9ewbMY4BmsQOjpVR9J7+6SZmpUB2MdD3lpdY07
|
||||||
|
YSuamff0dcNdV2+NEZth6bit++iJFc9rTI/OwBZLMTVsp+oVpKh7w72h1DfboaF/
|
||||||
|
oU9tYHWFWTRUfSzoqm7Q4POKmII4BhA+1QWIUIX1OLQocepzgfw72Tj+GlRQ7WAu
|
||||||
|
IToIBqbRxfsNflaWDSv15UrE7OCDLUdlXILOd0GgtJaYTsX7X7ZMU1mIoqX0IBsk
|
||||||
|
KRCep+7BTA8VYXOlW26tPEcsj2Vp7tdghptTaEtdx05delaYaX18rxdprNz+VV0Z
|
||||||
|
jNVIgShJC4vMEVQtOSOyznavF9OONScBG4e0E9rSHbYudtvrp8WOoaDK6mzAwo6w
|
||||||
|
wShYwwzFmf0Y2FbsENvNiNw5cqKT4WQoCXM6WS9BjPfJqz4M7V8pYa3pCE4M+oXP
|
||||||
|
sfQDgkpyXg88Dez1N78cbpr4GKsI9odFmphivQngXF1H3N8UKLCjW/EQNKHoydTn
|
||||||
|
nTtIfSY1G5hlDS0nCqTn6LvI2W887Da+ASWGtgGn0opLUW5Kg5fPurFjoCEP4mNg
|
||||||
|
JQ8nX9q2N5AogHld0h1ZAoIBAQDVRSty2Q7+oQh0qWGI0eDESeAHd/OKnwELAsMx
|
||||||
|
pfXABhB/CjO0/Iy4MgBL1dMb1S64gHhHi816CfxoSQuaUNjOcAWxhLTbnPEx8eaS
|
||||||
|
SFvdnd3itEC0T6Cg1r8mklHXrz6WH8vtLfu3pc8svGp+xJ5Rmlg1G3rhYFce6csg
|
||||||
|
lgeP4n4vjxOZtds3Jr8oHiW0/KYrLUK8/TgFOwYvWZQ8Lk92Oz+Fcd+RkegSkzIB
|
||||||
|
hHpqIMQz4T5uirjmxKX9573X17cVO3LuCBSite1uiyXRAIrGEOOyHwv3xC2h/bV1
|
||||||
|
6IYSiuwJOxDxZAmYuWYBtlvfWrOypEuWogZQv8C/zxDpkDSLAoIBAQDCrHxmh5NI
|
||||||
|
ksWQg0W6176uktwyypFBh8REE1VfCgoxJ84TVOQzEBiNQpRj/FUCe4e7ZElIAks+
|
||||||
|
N9mh8smJLvHDVo7033NIfAZCfCbLve8uWSGAm5x+aKS7kG5gynojKT8vl0y88m5o
|
||||||
|
Nm+BfvpQKjb6n6jibZZRsQsyz5KBct+Gb9gyA81jjrFudBmYy9WZeEm6pjzQtkuq
|
||||||
|
I0xCbQFwB+It/utjz7okuAWk5fPU0LRqOvvEMJjf2DyvIK45FDsBF3zGlG8Zunnh
|
||||||
|
q3o28zXdQCvYxY5Ik8zRuSTTAQnaJ3/zUvqR2g3PoSF8d7/WSkzNK+ZhTANqPkq1
|
||||||
|
SOg515Na9L4fAoH/Vc5+rLaoUcp4nHeJxoKq7E7M1DRuyFcxFD0IS/F57siB2ptA
|
||||||
|
MpFqDLIRbHGbfpdHNPR7cE3PXkqmQ08gW/YrROPNZp7+JV3/rRimrDRwwbnCjHP5
|
||||||
|
lJJ1DkFYpyw3wY/AnqYsZkEaBcmwkU89icOR70MqOjPUPNmGM+nc0D+My1dVbc0j
|
||||||
|
FbUVfhsYzgtTIH6GXNjZATDgWTpmQqbH/W6kie1MoWQvj2Ik/VQ7ymCC4DBOwJDf
|
||||||
|
jZpCypZUMtQKjc083E4O77ZQlyabYN6bWHvfWdFxyziyl/1WXta1K7tiNhOu5Aff
|
||||||
|
yT92nPv7DrVQQY08v6NaxkBqShLcek/VfiOHAoIBAAtzd/HUAb7gG0zv29csv6On
|
||||||
|
Mdqu/bJcGRhkBr6LaaQQkleiw7WZOch9ZRsoiZuWxpooQQNCV0i2ok+bZ21xXHlA
|
||||||
|
CzKuPirCWN/qS6HqbzpLtePJw3/QCfiae1OoNV0CHRxgivwGSqZIpXB5lqHGiete
|
||||||
|
HuIKzi/J+T2o5hZFOo6+33m5rYgwqZE0tRi+zLa1U6juBF/GiVbdsqupm88KN6y6
|
||||||
|
9P+vBWUJihN0D06yZBpnk82riiKIprEqe/URkpLy3b0UmCBsTqUOoCbBUabNEocy
|
||||||
|
v7bXMtIXUOo0gm7ZqfYXKHQR3oQbF0wqAxfI0RG0hl2syfqi5WQagMZ+PsW35cMC
|
||||||
|
ggEBAM6kIM/zUnNBFla+oTiPCyoII7nGmGz8dT9IhH0+6T5nNzG4O6D28A3MJcs1
|
||||||
|
C3pfukCCeWOnDNCIQ7C9Hx5XcCDoI4eD6zCRy+7zXxn0FxYS+O6lczB2mXGE69Yp
|
||||||
|
n3qb7P4XqZYex3dT72czJUlY6nFB2e0FmyvSoez8fsH9Ws78c4JGO953klam/emA
|
||||||
|
Hc8nB3CyM8rb2JlM3WeQbmo+Sbi0Yvj+MWM2AnTXx0xyaFXKP4WGD8hAxTvf9tSX
|
||||||
|
3NAPVBXku4zRpvoXyyrcBVd9vwE0qBr3eWCuci8aDD6RAAJgb8HHX9RjRF/phFpY
|
||||||
|
S++xGnGHEUSEl7cWnO8k0RyJzzc=
|
||||||
|
-----END PRIVATE KEY-----
|
205
eml.go
205
eml.go
|
@ -18,14 +18,35 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EMLToMsgFromString will parse a given EML string and returns a pre-filled Msg pointer
|
// EMLToMsgFromString parses a given EML string and returns a pre-filled Msg pointer.
|
||||||
|
//
|
||||||
|
// This function takes an EML formatted string, converts it into a bytes buffer, and then
|
||||||
|
// calls EMLToMsgFromReader to parse the buffer and create a Msg object. This provides a
|
||||||
|
// convenient way to convert EML strings directly into Msg objects.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - emlString: A string containing the EML formatted message.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A pointer to the Msg object populated with the parsed data, and an error if parsing
|
||||||
|
// fails.
|
||||||
func EMLToMsgFromString(emlString string) (*Msg, error) {
|
func EMLToMsgFromString(emlString string) (*Msg, error) {
|
||||||
eb := bytes.NewBufferString(emlString)
|
eb := bytes.NewBufferString(emlString)
|
||||||
return EMLToMsgFromReader(eb)
|
return EMLToMsgFromReader(eb)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EMLToMsgFromReader will parse a reader that holds EML content and returns a pre-filled
|
// EMLToMsgFromReader parses a reader that holds EML content and returns a pre-filled Msg pointer.
|
||||||
// Msg pointer
|
//
|
||||||
|
// This function reads EML content from the provided io.Reader and populates a Msg object
|
||||||
|
// with the parsed data. It initializes the Msg and extracts headers and body parts from
|
||||||
|
// the EML content. Any errors encountered during parsing are returned.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - reader: An io.Reader containing the EML formatted message.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A pointer to the Msg object populated with the parsed data, and an error if parsing
|
||||||
|
// fails.
|
||||||
func EMLToMsgFromReader(reader io.Reader) (*Msg, error) {
|
func EMLToMsgFromReader(reader io.Reader) (*Msg, error) {
|
||||||
msg := &Msg{
|
msg := &Msg{
|
||||||
addrHeader: make(map[AddrHeader][]*netmail.Address),
|
addrHeader: make(map[AddrHeader][]*netmail.Address),
|
||||||
|
@ -46,8 +67,19 @@ func EMLToMsgFromReader(reader io.Reader) (*Msg, error) {
|
||||||
return msg, nil
|
return msg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// EMLToMsgFromFile will open and parse a .eml file at a provided file path and returns a
|
// EMLToMsgFromFile opens and parses a .eml file at a provided file path and returns a
|
||||||
// pre-filled Msg pointer
|
// pre-filled Msg pointer.
|
||||||
|
//
|
||||||
|
// This function attempts to read and parse an EML file located at the specified file path.
|
||||||
|
// It initializes a Msg object and populates it with the parsed headers and body. Any errors
|
||||||
|
// encountered during the file operations or parsing are returned.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - filePath: The path to the .eml file to be parsed.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A pointer to the Msg object populated with the parsed data, and an error if parsing
|
||||||
|
// fails.
|
||||||
func EMLToMsgFromFile(filePath string) (*Msg, error) {
|
func EMLToMsgFromFile(filePath string) (*Msg, error) {
|
||||||
msg := &Msg{
|
msg := &Msg{
|
||||||
addrHeader: make(map[AddrHeader][]*netmail.Address),
|
addrHeader: make(map[AddrHeader][]*netmail.Address),
|
||||||
|
@ -68,7 +100,19 @@ func EMLToMsgFromFile(filePath string) (*Msg, error) {
|
||||||
return msg, nil
|
return msg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseEML parses the EML's headers and body and inserts the parsed values into the Msg
|
// parseEML parses the EML's headers and body and inserts the parsed values into the Msg.
|
||||||
|
//
|
||||||
|
// This function extracts relevant header fields and body content from the parsed EML message
|
||||||
|
// and stores them in the provided Msg object. It handles various header types and body
|
||||||
|
// parts, ensuring that the Msg is correctly populated with all necessary information.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - parsedMsg: A pointer to the netmail.Message containing the parsed EML data.
|
||||||
|
// - bodybuf: A bytes.Buffer containing the body content of the EML message.
|
||||||
|
// - msg: A pointer to the Msg object to be populated with the parsed data.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - An error if any issues occur during the parsing process; otherwise, returns nil.
|
||||||
func parseEML(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
|
func parseEML(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
|
||||||
if err := parseEMLHeaders(&parsedMsg.Header, msg); err != nil {
|
if err := parseEMLHeaders(&parsedMsg.Header, msg); err != nil {
|
||||||
return fmt.Errorf("failed to parse EML headers: %w", err)
|
return fmt.Errorf("failed to parse EML headers: %w", err)
|
||||||
|
@ -79,7 +123,18 @@ func parseEML(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// readEML opens an EML file and uses net/mail to parse the header and body
|
// readEML opens an EML file and uses net/mail to parse the header and body.
|
||||||
|
//
|
||||||
|
// This function opens the specified EML file for reading and utilizes the net/mail package
|
||||||
|
// to parse the message's headers and body. It returns the parsed message and a buffer
|
||||||
|
// containing the body content, along with any errors encountered during the process.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - filePath: The path to the EML file to be opened and parsed.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A pointer to the parsed netmail.Message, a bytes.Buffer containing the body, and an
|
||||||
|
// error if any issues occur during file operations or parsing.
|
||||||
func readEML(filePath string) (*netmail.Message, *bytes.Buffer, error) {
|
func readEML(filePath string) (*netmail.Message, *bytes.Buffer, error) {
|
||||||
fileHandle, err := os.Open(filePath)
|
fileHandle, err := os.Open(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -91,7 +146,19 @@ func readEML(filePath string) (*netmail.Message, *bytes.Buffer, error) {
|
||||||
return readEMLFromReader(fileHandle)
|
return readEMLFromReader(fileHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
// readEMLFromReader uses net/mail to parse the header and body from a given io.Reader
|
// readEMLFromReader uses net/mail to parse the header and body from a given io.Reader.
|
||||||
|
//
|
||||||
|
// This function reads the EML content from the provided io.Reader and uses the net/mail
|
||||||
|
// package to parse the message's headers and body. It returns the parsed netmail.Message
|
||||||
|
// along with a bytes.Buffer containing the body content. Any errors encountered during
|
||||||
|
// the parsing process are returned.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - reader: An io.Reader containing the EML formatted message.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A pointer to the parsed netmail.Message, a bytes.Buffer containing the body, and an
|
||||||
|
// error if any issues occur during parsing.
|
||||||
func readEMLFromReader(reader io.Reader) (*netmail.Message, *bytes.Buffer, error) {
|
func readEMLFromReader(reader io.Reader) (*netmail.Message, *bytes.Buffer, error) {
|
||||||
parsedMsg, err := netmail.ReadMessage(reader)
|
parsedMsg, err := netmail.ReadMessage(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -106,8 +173,18 @@ func readEMLFromReader(reader io.Reader) (*netmail.Message, *bytes.Buffer, error
|
||||||
return parsedMsg, &buf, nil
|
return parsedMsg, &buf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseEMLHeaders will check the EML headers for the most common headers and set the
|
// parseEMLHeaders parses the EML's headers and populates the Msg with relevant information.
|
||||||
// according settings in the Msg
|
//
|
||||||
|
// This function checks the EML headers for common headers and sets the corresponding fields
|
||||||
|
// in the Msg object. It extracts address headers, content types, and other relevant data
|
||||||
|
// for further processing.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - mailHeader: A pointer to the netmail.Header containing the EML headers.
|
||||||
|
// - msg: A pointer to the Msg object to be populated with parsed header information.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - An error if parsing the headers fails; otherwise, returns nil.
|
||||||
func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error {
|
func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error {
|
||||||
commonHeaders := []Header{
|
commonHeaders := []Header{
|
||||||
HeaderContentType, HeaderImportance, HeaderInReplyTo, HeaderListUnsubscribe,
|
HeaderContentType, HeaderImportance, HeaderInReplyTo, HeaderListUnsubscribe,
|
||||||
|
@ -175,7 +252,19 @@ func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseEMLBodyParts parses the body of a EML based on the different content types and encodings
|
// parseEMLBodyParts parses the body of an EML based on the different content types and encodings.
|
||||||
|
//
|
||||||
|
// This function examines the content type of the parsed EML message and processes the body
|
||||||
|
// parts accordingly. It handles both plain text and multipart types, ensuring that the
|
||||||
|
// Msg object is populated with the appropriate body content.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - parsedMsg: A pointer to the netmail.Message containing the parsed EML data.
|
||||||
|
// - bodybuf: A bytes.Buffer containing the body content of the EML message.
|
||||||
|
// - msg: A pointer to the Msg object to be populated with the parsed body content.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - An error if any issues occur during the body parsing process; otherwise, returns nil.
|
||||||
func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
|
func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
|
||||||
// Extract the transfer encoding of the body
|
// Extract the transfer encoding of the body
|
||||||
mediatype, params, err := mime.ParseMediaType(parsedMsg.Header.Get(HeaderContentType.String()))
|
mediatype, params, err := mime.ParseMediaType(parsedMsg.Header.Get(HeaderContentType.String()))
|
||||||
|
@ -212,10 +301,24 @@ func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *M
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseEMLBodyPlain parses the mail body of plain type mails
|
// parseEMLBodyPlain parses the mail body of plain type messages.
|
||||||
|
//
|
||||||
|
// This function handles the parsing of plain text messages based on their encoding. It
|
||||||
|
// identifies the content transfer encoding and decodes the body content accordingly,
|
||||||
|
// storing the result in the provided Msg object.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - mediatype: The media type of the message (e.g., text/plain).
|
||||||
|
// - parsedMsg: A pointer to the netmail.Message containing the parsed EML data.
|
||||||
|
// - bodybuf: A bytes.Buffer containing the body content of the EML message.
|
||||||
|
// - msg: A pointer to the Msg object to be populated with the parsed body content.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - An error if any issues occur during the parsing of the plain body; otherwise, returns nil.
|
||||||
func parseEMLBodyPlain(mediatype string, parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
|
func parseEMLBodyPlain(mediatype string, parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
|
||||||
contentTransferEnc := parsedMsg.Header.Get(HeaderContentTransferEnc.String())
|
contentTransferEnc := parsedMsg.Header.Get(HeaderContentTransferEnc.String())
|
||||||
// According to RFC2045, if no Content-Transfer-Encoding is set, we can imply 7bit US-ASCII encoding
|
// If no Content-Transfer-Encoding is set, we can imply 7bit US-ASCII encoding
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2045#section-6.1
|
||||||
if contentTransferEnc == "" || strings.EqualFold(contentTransferEnc, EncodingUSASCII.String()) {
|
if contentTransferEnc == "" || strings.EqualFold(contentTransferEnc, EncodingUSASCII.String()) {
|
||||||
msg.SetEncoding(EncodingUSASCII)
|
msg.SetEncoding(EncodingUSASCII)
|
||||||
msg.SetBodyString(ContentType(mediatype), bodybuf.String())
|
msg.SetBodyString(ContentType(mediatype), bodybuf.String())
|
||||||
|
@ -249,7 +352,20 @@ func parseEMLBodyPlain(mediatype string, parsedMsg *netmail.Message, bodybuf *by
|
||||||
return fmt.Errorf("unsupported Content-Transfer-Encoding")
|
return fmt.Errorf("unsupported Content-Transfer-Encoding")
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseEMLMultipart parses a multipart body part of a EML
|
// parseEMLMultipart parses a multipart body part of an EML message.
|
||||||
|
//
|
||||||
|
// This function handles the parsing of multipart messages, extracting the individual parts
|
||||||
|
// and determining their content types. It processes each part according to its content type
|
||||||
|
// and ensures that all relevant data is stored in the Msg object.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - params: A map containing the parameters from the multipart content type.
|
||||||
|
// - bodybuf: A bytes.Buffer containing the body content of the EML message.
|
||||||
|
// - msg: A pointer to the Msg object to be populated with the parsed body parts.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - An error if any issues occur during the parsing of the multipart body; otherwise,
|
||||||
|
// returns nil.
|
||||||
func parseEMLMultipart(params map[string]string, bodybuf *bytes.Buffer, msg *Msg) error {
|
func parseEMLMultipart(params map[string]string, bodybuf *bytes.Buffer, msg *Msg) error {
|
||||||
boundary, ok := params["boundary"]
|
boundary, ok := params["boundary"]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -349,7 +465,15 @@ ReadNextPart:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseEMLEncoding parses and determines the encoding of the message
|
// parseEMLEncoding parses and determines the encoding of the message.
|
||||||
|
//
|
||||||
|
// This function extracts the content transfer encoding from the EML headers and sets the
|
||||||
|
// corresponding encoding in the Msg object. It ensures that the correct encoding is used
|
||||||
|
// for further processing of the message content.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - mailHeader: A pointer to the netmail.Header containing the EML headers.
|
||||||
|
// - msg: A pointer to the Msg object to be updated with the encoding information.
|
||||||
func parseEMLEncoding(mailHeader *netmail.Header, msg *Msg) {
|
func parseEMLEncoding(mailHeader *netmail.Header, msg *Msg) {
|
||||||
if value := mailHeader.Get(HeaderContentTransferEnc.String()); value != "" {
|
if value := mailHeader.Get(HeaderContentTransferEnc.String()); value != "" {
|
||||||
switch {
|
switch {
|
||||||
|
@ -363,7 +487,15 @@ func parseEMLEncoding(mailHeader *netmail.Header, msg *Msg) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseEMLContentTypeCharset parses and determines the charset and content type of the message
|
// parseEMLContentTypeCharset parses and determines the charset and content type of the message.
|
||||||
|
//
|
||||||
|
// This function extracts the content type and charset from the EML headers, setting them
|
||||||
|
// appropriately in the Msg object. It ensures that the Msg object is configured with the
|
||||||
|
// correct content type for further processing.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - mailHeader: A pointer to the netmail.Header containing the EML headers.
|
||||||
|
// - msg: A pointer to the Msg object to be updated with content type and charset information.
|
||||||
func parseEMLContentTypeCharset(mailHeader *netmail.Header, msg *Msg) {
|
func parseEMLContentTypeCharset(mailHeader *netmail.Header, msg *Msg) {
|
||||||
if value := mailHeader.Get(HeaderContentType.String()); value != "" {
|
if value := mailHeader.Get(HeaderContentType.String()); value != "" {
|
||||||
contentType, optional := parseMultiPartHeader(value)
|
contentType, optional := parseMultiPartHeader(value)
|
||||||
|
@ -377,7 +509,18 @@ func parseEMLContentTypeCharset(mailHeader *netmail.Header, msg *Msg) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleEMLMultiPartBase64Encoding sets the content body of a base64 encoded Part
|
// handleEMLMultiPartBase64Encoding sets the content body of a base64 encoded Part.
|
||||||
|
//
|
||||||
|
// This function decodes the base64 encoded content of a multipart part and stores the
|
||||||
|
// resulting content in the provided Part object. It handles any errors that occur during
|
||||||
|
// the decoding process.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - multiPartData: A byte slice containing the base64 encoded data.
|
||||||
|
// - part: A pointer to the Part object where the decoded content will be stored.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - An error if the base64 decoding fails; otherwise, returns nil.
|
||||||
func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error {
|
func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error {
|
||||||
part.SetEncoding(EncodingB64)
|
part.SetEncoding(EncodingB64)
|
||||||
content, err := base64.StdEncoding.DecodeString(string(multiPartData))
|
content, err := base64.StdEncoding.DecodeString(string(multiPartData))
|
||||||
|
@ -388,8 +531,17 @@ func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseMultiPartHeader parses a multipart header and returns the value and optional parts as
|
// parseMultiPartHeader parses a multipart header and returns the value and optional parts as a map.
|
||||||
// separate map
|
//
|
||||||
|
// This function splits a multipart header into its main value and any optional parameters,
|
||||||
|
// returning them separately. It helps in processing multipart messages by extracting
|
||||||
|
// relevant information from headers.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - multiPartHeader: A string representing the multipart header to be parsed.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - The main header value as a string and a map of optional parameters.
|
||||||
func parseMultiPartHeader(multiPartHeader string) (header string, optional map[string]string) {
|
func parseMultiPartHeader(multiPartHeader string) (header string, optional map[string]string) {
|
||||||
optional = make(map[string]string)
|
optional = make(map[string]string)
|
||||||
headerSplit := strings.SplitN(multiPartHeader, ";", 2)
|
headerSplit := strings.SplitN(multiPartHeader, ";", 2)
|
||||||
|
@ -404,7 +556,20 @@ func parseMultiPartHeader(multiPartHeader string) (header string, optional map[s
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseEMLAttachmentEmbed parses a multipart that is an attachment or embed
|
// parseEMLAttachmentEmbed parses a multipart that is an attachment or embed.
|
||||||
|
//
|
||||||
|
// This function handles the parsing of multipart sections that are marked as attachments or
|
||||||
|
// embedded content. It processes the content disposition and sets the appropriate fields in
|
||||||
|
// the Msg object based on the parsed data.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - contentDisposition: A slice of strings containing the content disposition header.
|
||||||
|
// - multiPart: A pointer to the multipart.Part to be parsed.
|
||||||
|
// - msg: A pointer to the Msg object to be populated with the attachment or embed data.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - An error if any issues occur during the parsing of attachments or embeds; otherwise,
|
||||||
|
// returns nil.
|
||||||
func parseEMLAttachmentEmbed(contentDisposition []string, multiPart *multipart.Part, msg *Msg) error {
|
func parseEMLAttachmentEmbed(contentDisposition []string, multiPart *multipart.Part, msg *Msg) error {
|
||||||
cdType, optional := parseMultiPartHeader(contentDisposition[0])
|
cdType, optional := parseMultiPartHeader(contentDisposition[0])
|
||||||
filename := "generic.attachment"
|
filename := "generic.attachment"
|
||||||
|
|
155
encoding.go
155
encoding.go
|
@ -4,171 +4,226 @@
|
||||||
|
|
||||||
package mail
|
package mail
|
||||||
|
|
||||||
// Charset represents a character set for the encoding
|
// Charset is a type wrapper for a string representing different character encodings.
|
||||||
type Charset string
|
type Charset string
|
||||||
|
|
||||||
// ContentType represents a content type for the Msg
|
// ContentType is a type wrapper for a string and represents the MIME type of the content being handled.
|
||||||
type ContentType string
|
type ContentType string
|
||||||
|
|
||||||
// Encoding represents a MIME encoding scheme like quoted-printable or Base64.
|
// Encoding is a type wrapper for a string and represents the type of encoding used for email messages
|
||||||
|
// and/or parts.
|
||||||
type Encoding string
|
type Encoding string
|
||||||
|
|
||||||
// MIMEVersion represents the MIME version for the mail
|
// MIMEVersion is a type wrapper for a string nad represents the MIME version used in email messages.
|
||||||
type MIMEVersion string
|
type MIMEVersion string
|
||||||
|
|
||||||
// MIMEType represents the MIME type for the mail
|
// MIMEType is a type wrapper for a string and represents the MIME type for the Msg content or parts.
|
||||||
type MIMEType string
|
type MIMEType string
|
||||||
|
|
||||||
// List of supported encodings
|
|
||||||
const (
|
const (
|
||||||
// EncodingB64 represents the Base64 encoding as specified in RFC 2045.
|
// EncodingB64 represents the Base64 encoding as specified in RFC 2045.
|
||||||
|
//
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2045#section-6.8
|
||||||
EncodingB64 Encoding = "base64"
|
EncodingB64 Encoding = "base64"
|
||||||
|
|
||||||
// EncodingQP represents the "quoted-printable" encoding as specified in RFC 2045.
|
// EncodingQP represents the "quoted-printable" encoding as specified in RFC 2045.
|
||||||
|
//
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2045#section-6.7
|
||||||
EncodingQP Encoding = "quoted-printable"
|
EncodingQP Encoding = "quoted-printable"
|
||||||
|
|
||||||
// EncodingUSASCII represents encoding with only US-ASCII characters (aka 7Bit)
|
// EncodingUSASCII represents encoding with only US-ASCII characters (aka 7Bit)
|
||||||
|
//
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2045#section-2.7
|
||||||
EncodingUSASCII Encoding = "7bit"
|
EncodingUSASCII Encoding = "7bit"
|
||||||
|
|
||||||
// NoEncoding avoids any character encoding (except of the mail headers)
|
// NoEncoding represents 8-bit encoding for email messages as specified in RFC 6152.
|
||||||
|
//
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2045#section-2.8
|
||||||
|
//
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc6152
|
||||||
NoEncoding Encoding = "8bit"
|
NoEncoding Encoding = "8bit"
|
||||||
)
|
)
|
||||||
|
|
||||||
// List of common charsets
|
|
||||||
const (
|
const (
|
||||||
// CharsetUTF7 represents the "UTF-7" charset
|
// CharsetUTF7 represents the "UTF-7" charset.
|
||||||
CharsetUTF7 Charset = "UTF-7"
|
CharsetUTF7 Charset = "UTF-7"
|
||||||
|
|
||||||
// CharsetUTF8 represents the "UTF-8" charset
|
// CharsetUTF8 represents the "UTF-8" charset.
|
||||||
CharsetUTF8 Charset = "UTF-8"
|
CharsetUTF8 Charset = "UTF-8"
|
||||||
|
|
||||||
// CharsetASCII represents the "US-ASCII" charset
|
// CharsetASCII represents the "US-ASCII" charset.
|
||||||
CharsetASCII Charset = "US-ASCII"
|
CharsetASCII Charset = "US-ASCII"
|
||||||
|
|
||||||
// CharsetISO88591 represents the "ISO-8859-1" charset
|
// CharsetISO88591 represents the "ISO-8859-1" charset.
|
||||||
CharsetISO88591 Charset = "ISO-8859-1"
|
CharsetISO88591 Charset = "ISO-8859-1"
|
||||||
|
|
||||||
// CharsetISO88592 represents the "ISO-8859-2" charset
|
// CharsetISO88592 represents the "ISO-8859-2" charset.
|
||||||
CharsetISO88592 Charset = "ISO-8859-2"
|
CharsetISO88592 Charset = "ISO-8859-2"
|
||||||
|
|
||||||
// CharsetISO88593 represents the "ISO-8859-3" charset
|
// CharsetISO88593 represents the "ISO-8859-3" charset.
|
||||||
CharsetISO88593 Charset = "ISO-8859-3"
|
CharsetISO88593 Charset = "ISO-8859-3"
|
||||||
|
|
||||||
// CharsetISO88594 represents the "ISO-8859-4" charset
|
// CharsetISO88594 represents the "ISO-8859-4" charset.
|
||||||
CharsetISO88594 Charset = "ISO-8859-4"
|
CharsetISO88594 Charset = "ISO-8859-4"
|
||||||
|
|
||||||
// CharsetISO88595 represents the "ISO-8859-5" charset
|
// CharsetISO88595 represents the "ISO-8859-5" charset.
|
||||||
CharsetISO88595 Charset = "ISO-8859-5"
|
CharsetISO88595 Charset = "ISO-8859-5"
|
||||||
|
|
||||||
// CharsetISO88596 represents the "ISO-8859-6" charset
|
// CharsetISO88596 represents the "ISO-8859-6" charset.
|
||||||
CharsetISO88596 Charset = "ISO-8859-6"
|
CharsetISO88596 Charset = "ISO-8859-6"
|
||||||
|
|
||||||
// CharsetISO88597 represents the "ISO-8859-7" charset
|
// CharsetISO88597 represents the "ISO-8859-7" charset.
|
||||||
CharsetISO88597 Charset = "ISO-8859-7"
|
CharsetISO88597 Charset = "ISO-8859-7"
|
||||||
|
|
||||||
// CharsetISO88599 represents the "ISO-8859-9" charset
|
// CharsetISO88599 represents the "ISO-8859-9" charset.
|
||||||
CharsetISO88599 Charset = "ISO-8859-9"
|
CharsetISO88599 Charset = "ISO-8859-9"
|
||||||
|
|
||||||
// CharsetISO885913 represents the "ISO-8859-13" charset
|
// CharsetISO885913 represents the "ISO-8859-13" charset.
|
||||||
CharsetISO885913 Charset = "ISO-8859-13"
|
CharsetISO885913 Charset = "ISO-8859-13"
|
||||||
|
|
||||||
// CharsetISO885914 represents the "ISO-8859-14" charset
|
// CharsetISO885914 represents the "ISO-8859-14" charset.
|
||||||
CharsetISO885914 Charset = "ISO-8859-14"
|
CharsetISO885914 Charset = "ISO-8859-14"
|
||||||
|
|
||||||
// CharsetISO885915 represents the "ISO-8859-15" charset
|
// CharsetISO885915 represents the "ISO-8859-15" charset.
|
||||||
CharsetISO885915 Charset = "ISO-8859-15"
|
CharsetISO885915 Charset = "ISO-8859-15"
|
||||||
|
|
||||||
// CharsetISO885916 represents the "ISO-8859-16" charset
|
// CharsetISO885916 represents the "ISO-8859-16" charset.
|
||||||
CharsetISO885916 Charset = "ISO-8859-16"
|
CharsetISO885916 Charset = "ISO-8859-16"
|
||||||
|
|
||||||
// CharsetISO2022JP represents the "ISO-2022-JP" charset
|
// CharsetISO2022JP represents the "ISO-2022-JP" charset.
|
||||||
CharsetISO2022JP Charset = "ISO-2022-JP"
|
CharsetISO2022JP Charset = "ISO-2022-JP"
|
||||||
|
|
||||||
// CharsetISO2022KR represents the "ISO-2022-KR" charset
|
// CharsetISO2022KR represents the "ISO-2022-KR" charset.
|
||||||
CharsetISO2022KR Charset = "ISO-2022-KR"
|
CharsetISO2022KR Charset = "ISO-2022-KR"
|
||||||
|
|
||||||
// CharsetWindows1250 represents the "windows-1250" charset
|
// CharsetWindows1250 represents the "windows-1250" charset.
|
||||||
CharsetWindows1250 Charset = "windows-1250"
|
CharsetWindows1250 Charset = "windows-1250"
|
||||||
|
|
||||||
// CharsetWindows1251 represents the "windows-1251" charset
|
// CharsetWindows1251 represents the "windows-1251" charset.
|
||||||
CharsetWindows1251 Charset = "windows-1251"
|
CharsetWindows1251 Charset = "windows-1251"
|
||||||
|
|
||||||
// CharsetWindows1252 represents the "windows-1252" charset
|
// CharsetWindows1252 represents the "windows-1252" charset.
|
||||||
CharsetWindows1252 Charset = "windows-1252"
|
CharsetWindows1252 Charset = "windows-1252"
|
||||||
|
|
||||||
// CharsetWindows1255 represents the "windows-1255" charset
|
// CharsetWindows1255 represents the "windows-1255" charset.
|
||||||
CharsetWindows1255 Charset = "windows-1255"
|
CharsetWindows1255 Charset = "windows-1255"
|
||||||
|
|
||||||
// CharsetWindows1256 represents the "windows-1256" charset
|
// CharsetWindows1256 represents the "windows-1256" charset.
|
||||||
CharsetWindows1256 Charset = "windows-1256"
|
CharsetWindows1256 Charset = "windows-1256"
|
||||||
|
|
||||||
// CharsetKOI8R represents the "KOI8-R" charset
|
// CharsetKOI8R represents the "KOI8-R" charset.
|
||||||
CharsetKOI8R Charset = "KOI8-R"
|
CharsetKOI8R Charset = "KOI8-R"
|
||||||
|
|
||||||
// CharsetKOI8U represents the "KOI8-U" charset
|
// CharsetKOI8U represents the "KOI8-U" charset.
|
||||||
CharsetKOI8U Charset = "KOI8-U"
|
CharsetKOI8U Charset = "KOI8-U"
|
||||||
|
|
||||||
// CharsetBig5 represents the "Big5" charset
|
// CharsetBig5 represents the "Big5" charset.
|
||||||
CharsetBig5 Charset = "Big5"
|
CharsetBig5 Charset = "Big5"
|
||||||
|
|
||||||
// CharsetGB18030 represents the "GB18030" charset
|
// CharsetGB18030 represents the "GB18030" charset.
|
||||||
CharsetGB18030 Charset = "GB18030"
|
CharsetGB18030 Charset = "GB18030"
|
||||||
|
|
||||||
// CharsetGB2312 represents the "GB2312" charset
|
// CharsetGB2312 represents the "GB2312" charset.
|
||||||
CharsetGB2312 Charset = "GB2312"
|
CharsetGB2312 Charset = "GB2312"
|
||||||
|
|
||||||
// CharsetTIS620 represents the "TIS-620" charset
|
// CharsetTIS620 represents the "TIS-620" charset.
|
||||||
CharsetTIS620 Charset = "TIS-620"
|
CharsetTIS620 Charset = "TIS-620"
|
||||||
|
|
||||||
// CharsetEUCKR represents the "EUC-KR" charset
|
// CharsetEUCKR represents the "EUC-KR" charset.
|
||||||
CharsetEUCKR Charset = "EUC-KR"
|
CharsetEUCKR Charset = "EUC-KR"
|
||||||
|
|
||||||
// CharsetShiftJIS represents the "Shift_JIS" charset
|
// CharsetShiftJIS represents the "Shift_JIS" charset.
|
||||||
CharsetShiftJIS Charset = "Shift_JIS"
|
CharsetShiftJIS Charset = "Shift_JIS"
|
||||||
|
|
||||||
// CharsetUnknown represents the "Unknown" charset
|
// CharsetUnknown represents the "Unknown" charset.
|
||||||
CharsetUnknown Charset = "Unknown"
|
CharsetUnknown Charset = "Unknown"
|
||||||
|
|
||||||
// CharsetGBK represents the "GBK" charset
|
// CharsetGBK represents the "GBK" charset.
|
||||||
CharsetGBK Charset = "GBK"
|
CharsetGBK Charset = "GBK"
|
||||||
)
|
)
|
||||||
|
|
||||||
// List of MIME versions
|
// MIME10 represents the MIME version "1.0" used in email messages.
|
||||||
const (
|
const MIME10 MIMEVersion = "1.0"
|
||||||
// MIME10 is the MIME Version 1.0
|
|
||||||
MIME10 MIMEVersion = "1.0"
|
|
||||||
)
|
|
||||||
|
|
||||||
// List of common content types
|
|
||||||
const (
|
const (
|
||||||
|
// TypeAppOctetStream represents the MIME type for arbitrary binary data.
|
||||||
TypeAppOctetStream ContentType = "application/octet-stream"
|
TypeAppOctetStream ContentType = "application/octet-stream"
|
||||||
|
|
||||||
|
// TypeMultipartAlternative represents the MIME type for a message body that can contain multiple alternative
|
||||||
|
// formats.
|
||||||
TypeMultipartAlternative ContentType = "multipart/alternative"
|
TypeMultipartAlternative ContentType = "multipart/alternative"
|
||||||
|
|
||||||
|
// TypeMultipartMixed represents the MIME type for a multipart message containing different parts.
|
||||||
TypeMultipartMixed ContentType = "multipart/mixed"
|
TypeMultipartMixed ContentType = "multipart/mixed"
|
||||||
|
|
||||||
|
// TypeMultipartRelated represents the MIME type for a multipart message where each part is a related file
|
||||||
|
// or resource.
|
||||||
TypeMultipartRelated ContentType = "multipart/related"
|
TypeMultipartRelated ContentType = "multipart/related"
|
||||||
|
|
||||||
|
// TypePGPSignature represents the MIME type for PGP signed messages.
|
||||||
TypePGPSignature ContentType = "application/pgp-signature"
|
TypePGPSignature ContentType = "application/pgp-signature"
|
||||||
|
|
||||||
|
// TypePGPEncrypted represents the MIME type for PGP encrypted messages.
|
||||||
TypePGPEncrypted ContentType = "application/pgp-encrypted"
|
TypePGPEncrypted ContentType = "application/pgp-encrypted"
|
||||||
|
|
||||||
|
// TypeTextHTML represents the MIME type for HTML text content.
|
||||||
TypeTextHTML ContentType = "text/html"
|
TypeTextHTML ContentType = "text/html"
|
||||||
|
|
||||||
|
// TypeTextPlain represents the MIME type for plain text content.
|
||||||
TypeTextPlain ContentType = "text/plain"
|
TypeTextPlain ContentType = "text/plain"
|
||||||
|
|
||||||
|
// typeSMimeSigned represents the MIME type for S/MIME singed messages.
|
||||||
|
typeSMimeSigned ContentType = `application/pkcs7-signature; name="smime.p7s"`
|
||||||
)
|
)
|
||||||
|
|
||||||
// List of MIMETypes
|
|
||||||
const (
|
const (
|
||||||
|
// MIMEAlternative MIMEType represents a MIME multipart/alternative type, used for emails with multiple versions.
|
||||||
MIMEAlternative MIMEType = "alternative"
|
MIMEAlternative MIMEType = "alternative"
|
||||||
|
// MIMEMixed MIMEType represents a MIME multipart/mixed type used for emails containing different types of content.
|
||||||
MIMEMixed MIMEType = "mixed"
|
MIMEMixed MIMEType = "mixed"
|
||||||
|
// MIMERelated MIMEType represents a MIME multipart/related type, used for emails with related content entities.
|
||||||
MIMERelated MIMEType = "related"
|
MIMERelated MIMEType = "related"
|
||||||
|
// MIMESMime MIMEType represents a MIME multipart/signed type, used for siging emails with S/MIME.
|
||||||
|
MIMESMime MIMEType = `signed; protocol="application/pkcs7-signature"; micalg=sha-256`
|
||||||
)
|
)
|
||||||
|
|
||||||
// String is a standard method to convert an Charset into a printable format
|
// String satisfies the fmt.Stringer interface for the Charset type.
|
||||||
|
// It converts a Charset into a printable format.
|
||||||
|
//
|
||||||
|
// This method returns the string representation of the Charset, allowing it to be easily
|
||||||
|
// printed or logged.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A string representation of the Charset.
|
||||||
func (c Charset) String() string {
|
func (c Charset) String() string {
|
||||||
return string(c)
|
return string(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// String is a standard method to convert an ContentType into a printable format
|
// String satisfies the fmt.Stringer interface for the ContentType type.
|
||||||
|
// It converts a ContentType into a printable format.
|
||||||
|
//
|
||||||
|
// This method returns the string representation of the ContentType, enabling its use
|
||||||
|
// in formatted output such as logging or displaying information to the user.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A string representation of the ContentType.
|
||||||
func (c ContentType) String() string {
|
func (c ContentType) String() string {
|
||||||
return string(c)
|
return string(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// String is a standard method to convert an Encoding into a printable format
|
// String satisfies the fmt.Stringer interface for the Encoding type.
|
||||||
|
// It converts an Encoding into a printable format.
|
||||||
|
//
|
||||||
|
// This method returns the string representation of the Encoding, which can be used
|
||||||
|
// for displaying or logging purposes.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A string representation of the Encoding.
|
||||||
func (e Encoding) String() string {
|
func (e Encoding) String() string {
|
||||||
return string(e)
|
return string(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String is a standard method to convert an MIMEType into a printable format
|
||||||
|
func (e MIMEType) String() string {
|
||||||
|
return string(e)
|
||||||
|
}
|
||||||
|
|
|
@ -61,6 +61,11 @@ func TestContentType_String(t *testing.T) {
|
||||||
"ContentType: application/pgp-encrypted", TypePGPEncrypted,
|
"ContentType: application/pgp-encrypted", TypePGPEncrypted,
|
||||||
"application/pgp-encrypted",
|
"application/pgp-encrypted",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"ContentType: pkcs7-signature", typeSMimeSigned,
|
||||||
|
`application/pkcs7-signature; name="smime.p7s"`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
@ -121,3 +126,24 @@ func TestCharset_String(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestContentType_String tests the mime type method of the MIMEType object
|
||||||
|
func TestMimeType_String(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
mt MIMEType
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{MIMEAlternative, "alternative"},
|
||||||
|
{MIMEMixed, "mixed"},
|
||||||
|
{MIMERelated, "related"},
|
||||||
|
{MIMESMime, `signed; protocol="application/pkcs7-signature"; micalg=sha-256`},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.mt.String(), func(t *testing.T) {
|
||||||
|
if tt.mt.String() != tt.want {
|
||||||
|
t.Errorf("wrong string for mime type returned. Expected: %s, got: %s",
|
||||||
|
tt.want, tt.mt.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
97
file.go
97
file.go
|
@ -9,10 +9,15 @@ import (
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FileOption returns a function that can be used for grouping File options
|
// FileOption is a function type used to modify properties of a File
|
||||||
type FileOption func(*File)
|
type FileOption func(*File)
|
||||||
|
|
||||||
// File is an attachment or embedded file of the Msg
|
// File represents a file with properties such as content type, description, encoding, headers, name, and
|
||||||
|
// a writer function.
|
||||||
|
//
|
||||||
|
// This struct can represent either an attachment or an embedded file in a Msg, and it stores relevant
|
||||||
|
// metadata such as content type and encoding, as well as a function to write the file's content to an
|
||||||
|
// io.Writer.
|
||||||
type File struct {
|
type File struct {
|
||||||
ContentType ContentType
|
ContentType ContentType
|
||||||
Desc string
|
Desc string
|
||||||
|
@ -22,32 +27,68 @@ type File struct {
|
||||||
Writer func(w io.Writer) (int64, error)
|
Writer func(w io.Writer) (int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithFileContentID sets the Content-ID header for the File
|
// WithFileContentID sets the "Content-ID" header in the File's MIME headers to the specified ID.
|
||||||
|
//
|
||||||
|
// This function updates the File's MIME headers by setting the "Content-ID" to the provided string value,
|
||||||
|
// allowing the file to be referenced by this ID within the MIME structure.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - id: A string representing the content ID to be set in the "Content-ID" header.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A FileOption function that updates the File's "Content-ID" header.
|
||||||
func WithFileContentID(id string) FileOption {
|
func WithFileContentID(id string) FileOption {
|
||||||
return func(f *File) {
|
return func(f *File) {
|
||||||
f.Header.Set(HeaderContentID.String(), id)
|
f.Header.Set(HeaderContentID.String(), id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithFileName sets the filename of the File
|
// WithFileName sets the name of a File to the provided value.
|
||||||
|
//
|
||||||
|
// This function assigns the specified name to the File, updating its Name field.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - name: A string representing the name to be assigned to the File.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A FileOption function that sets the File's name.
|
||||||
func WithFileName(name string) FileOption {
|
func WithFileName(name string) FileOption {
|
||||||
return func(f *File) {
|
return func(f *File) {
|
||||||
f.Name = name
|
f.Name = name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithFileDescription sets an optional file description of the File that will be
|
// WithFileDescription sets an optional description for the File, which is used in the Content-Description
|
||||||
// added as Content-Description part
|
// header of the MIME output.
|
||||||
|
//
|
||||||
|
// This function updates the File's description, allowing an additional text description to be added to
|
||||||
|
// the MIME headers for the file.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - description: A string representing the description to be set in the Content-Description header.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A FileOption function that sets the File's description.
|
||||||
func WithFileDescription(description string) FileOption {
|
func WithFileDescription(description string) FileOption {
|
||||||
return func(f *File) {
|
return func(f *File) {
|
||||||
f.Desc = description
|
f.Desc = description
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithFileEncoding sets the encoding of the File. By default we should always use
|
// WithFileEncoding sets the encoding type for a File.
|
||||||
// Base64 encoding but there might be exceptions, where this might come handy.
|
//
|
||||||
// Please note that quoted-printable should never be used for attachments/embeds. If this
|
// This function allows the specification of an encoding type for the file, typically used for attachments
|
||||||
// is provided as argument, the function will automatically override back to Base64
|
// or embedded files. By default, Base64 encoding should be used, but this function can override the
|
||||||
|
// default if needed.
|
||||||
|
//
|
||||||
|
// Note: Quoted-printable encoding (EncodingQP) must never be used for attachments or embeds. If EncodingQP
|
||||||
|
// is passed to this function, it will be ignored and the encoding will remain unchanged.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - encoding: The Encoding type to be assigned to the File, unless it's EncodingQP.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A FileOption function that sets the File's encoding.
|
||||||
func WithFileEncoding(encoding Encoding) FileOption {
|
func WithFileEncoding(encoding Encoding) FileOption {
|
||||||
return func(f *File) {
|
return func(f *File) {
|
||||||
if encoding == EncodingQP {
|
if encoding == EncodingQP {
|
||||||
|
@ -58,23 +99,45 @@ func WithFileEncoding(encoding Encoding) FileOption {
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithFileContentType sets the content type of the File.
|
// WithFileContentType sets the content type of the File.
|
||||||
// By default go-mail will try to guess the file type and its corresponding
|
//
|
||||||
// content type and fall back to application/octet-stream if the file type
|
// By default, the content type is guessed based on the file type, and if no matching type is identified,
|
||||||
// could not be guessed. In some cases, however, it might be needed to force
|
// the default "application/octet-stream" is used. This FileOption allows overriding the guessed content
|
||||||
// this to a specific type. For such situations this override method can
|
// type with a specific one if required.
|
||||||
// be used
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - contentType: The ContentType to be assigned to the File.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A FileOption function that sets the File's content type.
|
||||||
func WithFileContentType(contentType ContentType) FileOption {
|
func WithFileContentType(contentType ContentType) FileOption {
|
||||||
return func(f *File) {
|
return func(f *File) {
|
||||||
f.ContentType = contentType
|
f.ContentType = contentType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// setHeader sets header fields to a File
|
// setHeader sets the value of a specified MIME header field for the File.
|
||||||
|
//
|
||||||
|
// This method updates the MIME headers of the File by assigning the provided value to the specified
|
||||||
|
// header field.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - header: The Header field to be updated.
|
||||||
|
// - value: A string representing the value to be set for the given header.
|
||||||
func (f *File) setHeader(header Header, value string) {
|
func (f *File) setHeader(header Header, value string) {
|
||||||
f.Header.Set(string(header), value)
|
f.Header.Set(string(header), value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getHeader return header fields of a File
|
// getHeader retrieves the value of the specified MIME header field.
|
||||||
|
//
|
||||||
|
// This method returns the value of the given header and a boolean indicating whether the header was found
|
||||||
|
// in the File's MIME headers.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - header: The Header field whose value is to be retrieved.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A string containing the value of the header.
|
||||||
|
// - A boolean indicating whether the header was present (true) or not (false).
|
||||||
func (f *File) getHeader(header Header) (string, bool) {
|
func (f *File) getHeader(header Header) (string, bool) {
|
||||||
v := f.Header.Get(string(header))
|
v := f.Header.Get(string(header))
|
||||||
return v, v != ""
|
return v, v != ""
|
||||||
|
|
6
go.mod
6
go.mod
|
@ -5,3 +5,9 @@
|
||||||
module github.com/wneessen/go-mail
|
module github.com/wneessen/go-mail
|
||||||
|
|
||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
|
require (
|
||||||
|
go.mozilla.org/pkcs7 v0.9.0
|
||||||
|
golang.org/x/crypto v0.28.0
|
||||||
|
golang.org/x/text v0.19.0
|
||||||
|
)
|
||||||
|
|
68
go.sum
68
go.sum
|
@ -0,0 +1,68 @@
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
|
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||||
|
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||||
|
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.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI=
|
||||||
|
go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
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.6.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/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
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.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.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
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.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
|
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/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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
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.14.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.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
|
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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
3
go.sum.license
Normal file
3
go.sum.license
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
133
header.go
133
header.go
|
@ -4,129 +4,146 @@
|
||||||
|
|
||||||
package mail
|
package mail
|
||||||
|
|
||||||
// Header represents a generic mail header field name
|
// Header is a type wrapper for a string and represents email header fields in a Msg.
|
||||||
type Header string
|
type Header string
|
||||||
|
|
||||||
// AddrHeader represents a address related mail Header field name
|
// AddrHeader is a type wrapper for a string and represents email address headers fields in a Msg.
|
||||||
type AddrHeader string
|
type AddrHeader string
|
||||||
|
|
||||||
// Importance represents a Importance/Priority value string
|
// Importance is a type wrapper for an int and represents the level of importance or priority for a Msg.
|
||||||
type Importance int
|
type Importance int
|
||||||
|
|
||||||
// List of common generic header field names
|
|
||||||
const (
|
const (
|
||||||
// HeaderContentDescription is the "Content-Description" header
|
// HeaderContentDescription is the "Content-Description" header.
|
||||||
HeaderContentDescription Header = "Content-Description"
|
HeaderContentDescription Header = "Content-Description"
|
||||||
|
|
||||||
// HeaderContentDisposition is the "Content-Disposition" header
|
// HeaderContentDisposition is the "Content-Disposition" header.
|
||||||
HeaderContentDisposition Header = "Content-Disposition"
|
HeaderContentDisposition Header = "Content-Disposition"
|
||||||
|
|
||||||
// HeaderContentID is the "Content-ID" header
|
// HeaderContentID is the "Content-ID" header.
|
||||||
HeaderContentID Header = "Content-ID"
|
HeaderContentID Header = "Content-ID"
|
||||||
|
|
||||||
// HeaderContentLang is the "Content-Language" header
|
// HeaderContentLang is the "Content-Language" header.
|
||||||
HeaderContentLang Header = "Content-Language"
|
HeaderContentLang Header = "Content-Language"
|
||||||
|
|
||||||
// HeaderContentLocation is the "Content-Location" header (RFC 2110)
|
// HeaderContentLocation is the "Content-Location" header (RFC 2110).
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2110#section-4.3
|
||||||
HeaderContentLocation Header = "Content-Location"
|
HeaderContentLocation Header = "Content-Location"
|
||||||
|
|
||||||
// HeaderContentTransferEnc is the "Content-Transfer-Encoding" header
|
// HeaderContentTransferEnc is the "Content-Transfer-Encoding" header.
|
||||||
HeaderContentTransferEnc Header = "Content-Transfer-Encoding"
|
HeaderContentTransferEnc Header = "Content-Transfer-Encoding"
|
||||||
|
|
||||||
// HeaderContentType is the "Content-Type" header
|
// HeaderContentType is the "Content-Type" header.
|
||||||
HeaderContentType Header = "Content-Type"
|
HeaderContentType Header = "Content-Type"
|
||||||
|
|
||||||
// HeaderDate represents the "Date" field
|
// HeaderDate represents the "Date" field.
|
||||||
// See: https://www.rfc-editor.org/rfc/rfc822#section-5.1
|
// https://datatracker.ietf.org/doc/html/rfc822#section-5.1
|
||||||
HeaderDate Header = "Date"
|
HeaderDate Header = "Date"
|
||||||
|
|
||||||
// HeaderDispositionNotificationTo is the MDN header as described in RFC8098
|
// HeaderDispositionNotificationTo is the MDN header as described in RFC 8098.
|
||||||
// See: https://www.rfc-editor.org/rfc/rfc8098.html#section-2.1
|
// https://datatracker.ietf.org/doc/html/rfc8098#section-2.1
|
||||||
HeaderDispositionNotificationTo Header = "Disposition-Notification-To"
|
HeaderDispositionNotificationTo Header = "Disposition-Notification-To"
|
||||||
|
|
||||||
// HeaderImportance represents the "Importance" field
|
// HeaderImportance represents the "Importance" field.
|
||||||
HeaderImportance Header = "Importance"
|
HeaderImportance Header = "Importance"
|
||||||
|
|
||||||
// HeaderInReplyTo represents the "In-Reply-To" field
|
// HeaderInReplyTo represents the "In-Reply-To" field.
|
||||||
HeaderInReplyTo Header = "In-Reply-To"
|
HeaderInReplyTo Header = "In-Reply-To"
|
||||||
|
|
||||||
// HeaderListUnsubscribe is the "List-Unsubscribe" header field
|
// HeaderListUnsubscribe is the "List-Unsubscribe" header field.
|
||||||
HeaderListUnsubscribe Header = "List-Unsubscribe"
|
HeaderListUnsubscribe Header = "List-Unsubscribe"
|
||||||
|
|
||||||
// HeaderListUnsubscribePost is the "List-Unsubscribe-Post" header field
|
// HeaderListUnsubscribePost is the "List-Unsubscribe-Post" header field.
|
||||||
HeaderListUnsubscribePost Header = "List-Unsubscribe-Post"
|
HeaderListUnsubscribePost Header = "List-Unsubscribe-Post"
|
||||||
|
|
||||||
// HeaderMessageID represents the "Message-ID" field for message identification
|
// HeaderMessageID represents the "Message-ID" field for message identification.
|
||||||
// See: https://www.rfc-editor.org/rfc/rfc1036#section-2.1.5
|
// https://datatracker.ietf.org/doc/html/rfc1036#section-2.1.5
|
||||||
HeaderMessageID Header = "Message-ID"
|
HeaderMessageID Header = "Message-ID"
|
||||||
|
|
||||||
// HeaderMIMEVersion represents the "MIME-Version" field as per RFC 2045
|
// HeaderMIMEVersion represents the "MIME-Version" field as per RFC 2045.
|
||||||
// See: https://datatracker.ietf.org/doc/html/rfc2045#section-4
|
// https://datatracker.ietf.org/doc/html/rfc2045#section-4
|
||||||
HeaderMIMEVersion Header = "MIME-Version"
|
HeaderMIMEVersion Header = "MIME-Version"
|
||||||
|
|
||||||
// HeaderOrganization is the "Organization" header field
|
// HeaderOrganization is the "Organization" header field.
|
||||||
HeaderOrganization Header = "Organization"
|
HeaderOrganization Header = "Organization"
|
||||||
|
|
||||||
// HeaderPrecedence is the "Precedence" header field
|
// HeaderPrecedence is the "Precedence" header field.
|
||||||
HeaderPrecedence Header = "Precedence"
|
HeaderPrecedence Header = "Precedence"
|
||||||
|
|
||||||
// HeaderPriority represents the "Priority" field
|
// HeaderPriority represents the "Priority" field.
|
||||||
HeaderPriority Header = "Priority"
|
HeaderPriority Header = "Priority"
|
||||||
|
|
||||||
// HeaderReferences is the "References" header field
|
// HeaderReferences is the "References" header field.
|
||||||
HeaderReferences Header = "References"
|
HeaderReferences Header = "References"
|
||||||
|
|
||||||
// HeaderReplyTo is the "Reply-To" header field
|
// HeaderReplyTo is the "Reply-To" header field.
|
||||||
HeaderReplyTo Header = "Reply-To"
|
HeaderReplyTo Header = "Reply-To"
|
||||||
|
|
||||||
// HeaderSubject is the "Subject" header field
|
// HeaderSubject is the "Subject" header field.
|
||||||
HeaderSubject Header = "Subject"
|
HeaderSubject Header = "Subject"
|
||||||
|
|
||||||
// HeaderUserAgent is the "User-Agent" header field
|
// HeaderUserAgent is the "User-Agent" header field.
|
||||||
HeaderUserAgent Header = "User-Agent"
|
HeaderUserAgent Header = "User-Agent"
|
||||||
|
|
||||||
// HeaderXAutoResponseSuppress is the "X-Auto-Response-Suppress" header field
|
// HeaderXAutoResponseSuppress is the "X-Auto-Response-Suppress" header field.
|
||||||
HeaderXAutoResponseSuppress Header = "X-Auto-Response-Suppress"
|
HeaderXAutoResponseSuppress Header = "X-Auto-Response-Suppress"
|
||||||
|
|
||||||
// HeaderXMailer is the "X-Mailer" header field
|
// HeaderXMailer is the "X-Mailer" header field.
|
||||||
HeaderXMailer Header = "X-Mailer"
|
HeaderXMailer Header = "X-Mailer"
|
||||||
|
|
||||||
// HeaderXMSMailPriority is the "X-MSMail-Priority" header field
|
// HeaderXMSMailPriority is the "X-MSMail-Priority" header field.
|
||||||
HeaderXMSMailPriority Header = "X-MSMail-Priority"
|
HeaderXMSMailPriority Header = "X-MSMail-Priority"
|
||||||
|
|
||||||
// HeaderXPriority is the "X-Priority" header field
|
// HeaderXPriority is the "X-Priority" header field.
|
||||||
HeaderXPriority Header = "X-Priority"
|
HeaderXPriority Header = "X-Priority"
|
||||||
)
|
)
|
||||||
|
|
||||||
// List of common address header field names
|
|
||||||
const (
|
const (
|
||||||
// HeaderBcc is the "Blind Carbon Copy" header field
|
// HeaderBcc is the "Blind Carbon Copy" header field.
|
||||||
HeaderBcc AddrHeader = "Bcc"
|
HeaderBcc AddrHeader = "Bcc"
|
||||||
|
|
||||||
// HeaderCc is the "Carbon Copy" header field
|
// HeaderCc is the "Carbon Copy" header field.
|
||||||
HeaderCc AddrHeader = "Cc"
|
HeaderCc AddrHeader = "Cc"
|
||||||
|
|
||||||
// HeaderEnvelopeFrom is the envelope FROM header field
|
// HeaderEnvelopeFrom is the envelope FROM header field.
|
||||||
// It's not included in the mail body but only used by the Client for the envelope
|
//
|
||||||
|
// It is generally not included in the mail body but only used by the Client for the communication with the
|
||||||
|
// SMTP server. If the Msg has no "FROM" address set in the mail body, the msgWriter will try to use the
|
||||||
|
// envelope from address, if this has been set for the Msg.
|
||||||
HeaderEnvelopeFrom AddrHeader = "EnvelopeFrom"
|
HeaderEnvelopeFrom AddrHeader = "EnvelopeFrom"
|
||||||
|
|
||||||
// HeaderFrom is the "From" header field
|
// HeaderFrom is the "From" header field.
|
||||||
HeaderFrom AddrHeader = "From"
|
HeaderFrom AddrHeader = "From"
|
||||||
|
|
||||||
// HeaderTo is the "Receipient" header field
|
// HeaderTo is the "Receipient" header field.
|
||||||
HeaderTo AddrHeader = "To"
|
HeaderTo AddrHeader = "To"
|
||||||
)
|
)
|
||||||
|
|
||||||
// List of Importance values
|
|
||||||
const (
|
const (
|
||||||
|
// ImportanceLow indicates a low level of importance or priority in a Msg.
|
||||||
ImportanceLow Importance = iota
|
ImportanceLow Importance = iota
|
||||||
|
|
||||||
|
// ImportanceNormal indicates a standard level of importance or priority for a Msg.
|
||||||
ImportanceNormal
|
ImportanceNormal
|
||||||
|
|
||||||
|
// ImportanceHigh indicates a high level of importance or priority in a Msg.
|
||||||
ImportanceHigh
|
ImportanceHigh
|
||||||
|
|
||||||
|
// ImportanceNonUrgent indicates a non-urgent level of importance or priority in a Msg.
|
||||||
ImportanceNonUrgent
|
ImportanceNonUrgent
|
||||||
|
|
||||||
|
// ImportanceUrgent indicates an urgent level of importance or priority in a Msg.
|
||||||
ImportanceUrgent
|
ImportanceUrgent
|
||||||
)
|
)
|
||||||
|
|
||||||
// NumString returns the importance number string based on the Importance
|
// NumString returns a numerical string representation of the Importance level.
|
||||||
|
//
|
||||||
|
// This method maps ImportanceHigh and ImportanceUrgent to "1", while ImportanceNonUrgent and ImportanceLow
|
||||||
|
// are mapped to "0". Other values return an empty string.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A string representing the numerical value of the Importance level ("1" or "0"), or an empty string
|
||||||
|
// if the Importance level is unrecognized.
|
||||||
func (i Importance) NumString() string {
|
func (i Importance) NumString() string {
|
||||||
switch i {
|
switch i {
|
||||||
case ImportanceNonUrgent:
|
case ImportanceNonUrgent:
|
||||||
|
@ -142,7 +159,14 @@ func (i Importance) NumString() string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// XPrioString returns the X-Priority number string based on the Importance
|
// XPrioString returns the X-Priority string representation of the Importance level.
|
||||||
|
//
|
||||||
|
// This method maps ImportanceHigh and ImportanceUrgent to "1", while ImportanceNonUrgent and ImportanceLow
|
||||||
|
// are mapped to "5". Other values return an empty string.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A string representing the X-Priority value of the Importance level ("1" or "5"), or an empty string
|
||||||
|
// if the Importance level is unrecognized.
|
||||||
func (i Importance) XPrioString() string {
|
func (i Importance) XPrioString() string {
|
||||||
switch i {
|
switch i {
|
||||||
case ImportanceNonUrgent:
|
case ImportanceNonUrgent:
|
||||||
|
@ -158,7 +182,14 @@ func (i Importance) XPrioString() string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns the importance string based on the Importance
|
// String satisfies the fmt.Stringer interface for the Importance type and returns the string
|
||||||
|
// representation of the Importance level.
|
||||||
|
//
|
||||||
|
// This method provides a human-readable string for each Importance level.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A string representing the Importance level ("non-urgent", "low", "high", or "urgent"), or an empty
|
||||||
|
// string if the Importance level is unrecognized.
|
||||||
func (i Importance) String() string {
|
func (i Importance) String() string {
|
||||||
switch i {
|
switch i {
|
||||||
case ImportanceNonUrgent:
|
case ImportanceNonUrgent:
|
||||||
|
@ -174,12 +205,20 @@ func (i Importance) String() string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns the header string based on the given Header
|
// String satisfies the fmt.Stringer interface for the Header type and returns the string
|
||||||
|
// representation of the Header.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A string representing the Header.
|
||||||
func (h Header) String() string {
|
func (h Header) String() string {
|
||||||
return string(h)
|
return string(h)
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns the address header string based on the given AddrHeader
|
// String satisfies the fmt.Stringer interface for the AddrHeader type and returns the string
|
||||||
|
// representation of the AddrHeader.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A string representing the AddrHeader.
|
||||||
func (a AddrHeader) String() string {
|
func (a AddrHeader) String() string {
|
||||||
return string(a)
|
return string(a)
|
||||||
}
|
}
|
||||||
|
|
208
msg_test.go
208
msg_test.go
|
@ -786,8 +786,8 @@ func TestMsg_SetMessageIDWithValue(t *testing.T) {
|
||||||
// TestMsg_SetMessageIDRandomness tests the randomness of Msg.SetMessageID methods
|
// TestMsg_SetMessageIDRandomness tests the randomness of Msg.SetMessageID methods
|
||||||
func TestMsg_SetMessageIDRandomness(t *testing.T) {
|
func TestMsg_SetMessageIDRandomness(t *testing.T) {
|
||||||
var mids []string
|
var mids []string
|
||||||
for i := 0; i < 50_000; i++ {
|
|
||||||
m := NewMsg()
|
m := NewMsg()
|
||||||
|
for i := 0; i < 50_000; i++ {
|
||||||
m.SetMessageID()
|
m.SetMessageID()
|
||||||
mid := m.GetMessageID()
|
mid := m.GetMessageID()
|
||||||
mids = append(mids, mid)
|
mids = append(mids, mid)
|
||||||
|
@ -1907,6 +1907,39 @@ func TestMsg_hasAlt(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMsg_hasAlt tests the hasAlt() method of the Msg with active S/MIME
|
||||||
|
func TestMsg_hasAltWithSMime(t *testing.T) {
|
||||||
|
keyPair, err := getDummyCertificate()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to load dummy certificate. Cause: %v", err)
|
||||||
|
}
|
||||||
|
m := NewMsg()
|
||||||
|
m.SetBodyString(TypeTextPlain, "Plain")
|
||||||
|
m.AddAlternativeString(TypeTextHTML, "<b>HTML</b>")
|
||||||
|
if err := m.SignWithSMime(keyPair); err != nil {
|
||||||
|
t.Errorf("set of certificate was not successful")
|
||||||
|
}
|
||||||
|
if m.hasAlt() {
|
||||||
|
t.Errorf("mail has alternative parts and S/MIME is active, but hasAlt() returned true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMsg_hasSMime tests the hasSMime() method of the Msg
|
||||||
|
func TestMsg_hasSMime(t *testing.T) {
|
||||||
|
keyPair, err := getDummyCertificate()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to load dummy certificate. Cause: %v", err)
|
||||||
|
}
|
||||||
|
m := NewMsg()
|
||||||
|
if err := m.SignWithSMime(keyPair); err != nil {
|
||||||
|
t.Errorf("set of certificate was not successful")
|
||||||
|
}
|
||||||
|
m.SetBodyString(TypeTextPlain, "Plain")
|
||||||
|
if !m.hasSMime() {
|
||||||
|
t.Errorf("mail has smime configured but hasSMime() returned true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestMsg_hasRelated tests the hasRelated() method of the Msg
|
// TestMsg_hasRelated tests the hasRelated() method of the Msg
|
||||||
func TestMsg_hasRelated(t *testing.T) {
|
func TestMsg_hasRelated(t *testing.T) {
|
||||||
m := NewMsg()
|
m := NewMsg()
|
||||||
|
@ -1974,6 +2007,70 @@ func TestMsg_WriteToSkipMiddleware(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMsg_WriteToWithSMIME tests the WriteTo() method of the Msg
|
||||||
|
func TestMsg_WriteToWithSMIME(t *testing.T) {
|
||||||
|
keyPair, err := getDummyCertificate()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to load dummy certificate. Cause: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := NewMsg()
|
||||||
|
m.Subject("This is a test")
|
||||||
|
m.SetBodyString(TypeTextPlain, "Plain")
|
||||||
|
if err := m.SignWithSMime(keyPair); err != nil {
|
||||||
|
t.Errorf("set of certificate was not successful")
|
||||||
|
}
|
||||||
|
|
||||||
|
wbuf := bytes.Buffer{}
|
||||||
|
if _, err = m.WriteTo(&wbuf); err != nil {
|
||||||
|
t.Errorf("WriteTo() failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := wbuf.String()
|
||||||
|
boundary := result[strings.LastIndex(result, "--")-60 : strings.LastIndex(result, "--")]
|
||||||
|
if strings.Count(result, boundary) != 4 {
|
||||||
|
t.Errorf("WriteTo() failed. False number of boundaries found")
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(result, fmt.Sprintf("--%s", boundary))
|
||||||
|
if len(parts) != 4 {
|
||||||
|
t.Errorf("WriteTo() failed. False number of parts found")
|
||||||
|
}
|
||||||
|
|
||||||
|
preamble := parts[0]
|
||||||
|
if !strings.Contains(preamble, "MIME-Version: 1.0") {
|
||||||
|
t.Errorf("WriteTo() failed. Unable to find MIME-Version")
|
||||||
|
}
|
||||||
|
if !strings.Contains(preamble, "Subject: This is a test") {
|
||||||
|
t.Errorf("WriteTo() failed. Unable to find subject")
|
||||||
|
}
|
||||||
|
if !strings.Contains(preamble, fmt.Sprintf("Content-Type: multipart/signed; protocol=\"application/pkcs7-signature\"; micalg=sha-256;\r\n boundary=%s", boundary)) {
|
||||||
|
t.Errorf("WriteTo() failed. Unable to find Content-Type")
|
||||||
|
}
|
||||||
|
|
||||||
|
signedData := parts[1]
|
||||||
|
if !strings.Contains(signedData, "Content-Transfer-Encoding: quoted-printable") {
|
||||||
|
t.Errorf("WriteTo() failed. Unable to find Content-Transfer-Encoding")
|
||||||
|
}
|
||||||
|
if !strings.Contains(signedData, "Content-Type: text/plain; charset=UTF-8") {
|
||||||
|
t.Errorf("WriteTo() failed. Unable to find Content-Type")
|
||||||
|
}
|
||||||
|
if !strings.Contains(signedData, "Plain") {
|
||||||
|
t.Errorf("WriteTo() failed. Unable to find Content")
|
||||||
|
}
|
||||||
|
|
||||||
|
signature := parts[2]
|
||||||
|
if !strings.Contains(signature, "Content-Transfer-Encoding: base64") {
|
||||||
|
t.Errorf("WriteTo() failed. Unable to find Content-Transfer-Encoding")
|
||||||
|
}
|
||||||
|
if !strings.Contains(signature, `application/pkcs7-signature; name="smime.p7s"`) {
|
||||||
|
t.Errorf("WriteTo() failed. Unable to find Content-Type")
|
||||||
|
}
|
||||||
|
if strings.Contains(signature, "Plain") {
|
||||||
|
t.Errorf("WriteTo() failed. Unable to find signature")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestMsg_WriteTo_fails tests the WriteTo() method of the Msg but with a failing body writer function
|
// TestMsg_WriteTo_fails tests the WriteTo() method of the Msg but with a failing body writer function
|
||||||
func TestMsg_WriteTo_fails(t *testing.T) {
|
func TestMsg_WriteTo_fails(t *testing.T) {
|
||||||
m := NewMsg()
|
m := NewMsg()
|
||||||
|
@ -3246,6 +3343,34 @@ func TestNewMsgWithNoDefaultUserAgent(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestSignWithSMime_ValidKeyPair tests WithSMimeSinging with given key pair
|
||||||
|
func TestSignWithSMime_ValidKeyPair(t *testing.T) {
|
||||||
|
keyPair, err := getDummyCertificate()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to load dummy certificate. Cause: %v", err)
|
||||||
|
}
|
||||||
|
m := NewMsg()
|
||||||
|
if err := m.SignWithSMime(keyPair); err != nil {
|
||||||
|
t.Errorf("failed to set sMime. Cause: %v", err)
|
||||||
|
}
|
||||||
|
if m.sMime.privateKey == nil {
|
||||||
|
t.Errorf("WithSMimeSinging() - no private key is given")
|
||||||
|
}
|
||||||
|
if m.sMime.certificate == nil {
|
||||||
|
t.Errorf("WithSMimeSinging() - no certificate is given")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSignWithSMime_InvalidKeyPair tests WithSMimeSinging with given invalid key pair
|
||||||
|
func TestSignWithSMime_InvalidKeyPair(t *testing.T) {
|
||||||
|
m := NewMsg()
|
||||||
|
|
||||||
|
err := m.SignWithSMime(nil)
|
||||||
|
if !errors.Is(err, ErrInvalidKeyPair) {
|
||||||
|
t.Errorf("failed to check sMimeAuthConfig values correctly: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fuzzing tests
|
// Fuzzing tests
|
||||||
func FuzzMsg_Subject(f *testing.F) {
|
func FuzzMsg_Subject(f *testing.F) {
|
||||||
f.Add("Testsubject")
|
f.Add("Testsubject")
|
||||||
|
@ -3273,3 +3398,84 @@ func FuzzMsg_From(f *testing.F) {
|
||||||
m.Reset()
|
m.Reset()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMsg_createSignaturePart tests the Msg.createSignaturePart method
|
||||||
|
func TestMsg_createSignaturePart(t *testing.T) {
|
||||||
|
keyPair, err := getDummyCertificate()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to load dummy certificate. Cause: %v", err)
|
||||||
|
}
|
||||||
|
m := NewMsg()
|
||||||
|
if err := m.SignWithSMime(keyPair); err != nil {
|
||||||
|
t.Errorf("set of certificate was not successful")
|
||||||
|
}
|
||||||
|
body := []byte("This is the body")
|
||||||
|
part, err := m.createSignaturePart(EncodingQP, TypeTextPlain, CharsetUTF7, body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("createSignaturePart() method failed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if part.GetEncoding() != EncodingB64 {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected encoding: %s, got: %s", EncodingB64, part.GetEncoding())
|
||||||
|
}
|
||||||
|
if part.GetContentType() != typeSMimeSigned {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected content type: %s, got: %s", typeSMimeSigned, part.GetContentType())
|
||||||
|
}
|
||||||
|
if part.GetCharset() != CharsetUTF8 {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected charset: %s, got: %s", CharsetUTF8, part.GetCharset())
|
||||||
|
}
|
||||||
|
if content, err := part.GetContent(); err != nil || len(content) == len(body) {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected content should not be equal: %s, got: %s", body, part.GetEncoding())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMsg_signMessage tests the Msg.signMessage method
|
||||||
|
func TestMsg_signMessage(t *testing.T) {
|
||||||
|
keyPair, err := getDummyCertificate()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to load dummy certificate. Cause: %v", err)
|
||||||
|
}
|
||||||
|
body := []byte("This is the body")
|
||||||
|
m := NewMsg()
|
||||||
|
m.SetBodyString(TypeTextPlain, string(body))
|
||||||
|
if err := m.SignWithSMime(keyPair); err != nil {
|
||||||
|
t.Errorf("set of certificate was not successful")
|
||||||
|
}
|
||||||
|
msg, err := m.signMessage(m)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("createSignaturePart() method failed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := msg.GetParts()
|
||||||
|
if len(parts) != 2 {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected 2 parts, got: %d", len(parts))
|
||||||
|
}
|
||||||
|
|
||||||
|
signedPart := parts[0]
|
||||||
|
if signedPart.GetEncoding() != EncodingQP {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected encoding: %s, got: %s", EncodingB64, signedPart.GetEncoding())
|
||||||
|
}
|
||||||
|
if signedPart.GetContentType() != TypeTextPlain {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected content type: %s, got: %s", typeSMimeSigned, signedPart.GetContentType())
|
||||||
|
}
|
||||||
|
if signedPart.GetCharset() != CharsetUTF8 {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected charset: %s, got: %s", CharsetUTF8, signedPart.GetCharset())
|
||||||
|
}
|
||||||
|
if content, err := signedPart.GetContent(); err != nil || len(content) != len(body) {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected content should be equal: %s, got: %s", body, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
signaturePart := parts[1]
|
||||||
|
if signaturePart.GetEncoding() != EncodingB64 {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected encoding: %s, got: %s", EncodingB64, signaturePart.GetEncoding())
|
||||||
|
}
|
||||||
|
if signaturePart.GetContentType() != typeSMimeSigned {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected content type: %s, got: %s", typeSMimeSigned, signaturePart.GetContentType())
|
||||||
|
}
|
||||||
|
if signaturePart.GetCharset() != CharsetUTF8 {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected charset: %s, got: %s", CharsetUTF8, signaturePart.GetCharset())
|
||||||
|
}
|
||||||
|
if content, err := signaturePart.GetContent(); err != nil || len(content) == len(body) {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected content should not be equal: %s, got: %s", body, signaturePart.GetEncoding())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -12,8 +12,14 @@ import (
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WriteToTempFile will create a temporary file and output the Msg to this file
|
// WriteToTempFile creates a temporary file and writes the Msg content to this file.
|
||||||
// The method will return the filename of the temporary file
|
//
|
||||||
|
// This method generates a temporary file with a ".eml" extension, writes the Msg to it, and returns the
|
||||||
|
// filename of the created temporary file.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A string representing the filename of the temporary file.
|
||||||
|
// - An error if the file creation or writing process fails.
|
||||||
func (m *Msg) WriteToTempFile() (string, error) {
|
func (m *Msg) WriteToTempFile() (string, error) {
|
||||||
f, err := os.CreateTemp("", "go-mail_*.eml")
|
f, err := os.CreateTemp("", "go-mail_*.eml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -12,8 +12,14 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WriteToTempFile will create a temporary file and output the Msg to this file
|
// WriteToTempFile creates a temporary file and writes the Msg content to this file.
|
||||||
// The method will return the filename of the temporary file
|
//
|
||||||
|
// This method generates a temporary file with a ".eml" extension, writes the Msg to it, and returns the
|
||||||
|
// filename of the created temporary file.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A string representing the filename of the temporary file.
|
||||||
|
// - An error if the file creation or writing process fails.
|
||||||
func (m *Msg) WriteToTempFile() (string, error) {
|
func (m *Msg) WriteToTempFile() (string, error) {
|
||||||
f, err := ioutil.TempFile("", "go-mail_*.eml")
|
f, err := ioutil.TempFile("", "go-mail_*.eml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
199
msgwriter.go
199
msgwriter.go
|
@ -18,22 +18,39 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MaxHeaderLength defines the maximum line length for a mail header
|
const (
|
||||||
// RFC 2047 suggests 76 characters
|
// MaxHeaderLength defines the maximum line length for a mail header.
|
||||||
const MaxHeaderLength = 76
|
//
|
||||||
|
// This constant follows the recommendation of RFC 2047, which suggests a maximum length of 76 characters.
|
||||||
|
//
|
||||||
|
// References:
|
||||||
|
// - https://datatracker.ietf.org/doc/html/rfc2047
|
||||||
|
MaxHeaderLength = 76
|
||||||
|
|
||||||
// MaxBodyLength defines the maximum line length for the mail body
|
// MaxBodyLength defines the maximum line length for the mail body.
|
||||||
// RFC 2047 suggests 76 characters
|
//
|
||||||
const MaxBodyLength = 76
|
// This constant follows the recommendation of RFC 2047, which suggests a maximum length of 76 characters.
|
||||||
|
//
|
||||||
|
// References:
|
||||||
|
// - https://datatracker.ietf.org/doc/html/rfc2047
|
||||||
|
MaxBodyLength = 76
|
||||||
|
|
||||||
// SingleNewLine represents a new line that can be used by the msgWriter to issue a carriage return
|
// SingleNewLine represents a single newline character sequence ("\r\n").
|
||||||
const SingleNewLine = "\r\n"
|
//
|
||||||
|
// This constant can be used by the msgWriter to issue a carriage return when writing mail content.
|
||||||
|
SingleNewLine = "\r\n"
|
||||||
|
|
||||||
// DoubleNewLine represents a double new line that can be used by the msgWriter to
|
// DoubleNewLine represents a double newline character sequence ("\r\n\r\n").
|
||||||
// indicate a new segement of the mail
|
//
|
||||||
const DoubleNewLine = "\r\n\r\n"
|
// This constant can be used by the msgWriter to indicate a new segment of the mail when writing mail content.
|
||||||
|
DoubleNewLine = "\r\n\r\n"
|
||||||
|
)
|
||||||
|
|
||||||
// msgWriter handles the I/O to the io.WriteCloser of the SMTP client
|
// msgWriter handles the I/O operations for writing to the io.WriteCloser of the SMTP client.
|
||||||
|
//
|
||||||
|
// This struct keeps track of the number of bytes written, the character set used, and the depth of the
|
||||||
|
// current multipart section. It also handles encoding, error tracking, and managing multipart and part
|
||||||
|
// writers for constructing the email message body.
|
||||||
type msgWriter struct {
|
type msgWriter struct {
|
||||||
bytesWritten int64
|
bytesWritten int64
|
||||||
charset Charset
|
charset Charset
|
||||||
|
@ -45,7 +62,18 @@ type msgWriter struct {
|
||||||
writer io.Writer
|
writer io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write implements the io.Writer interface for msgWriter
|
// Write implements the io.Writer interface for msgWriter.
|
||||||
|
//
|
||||||
|
// This method writes the provided payload to the underlying writer. It keeps track of the number of bytes
|
||||||
|
// written and handles any errors encountered during the writing process. If a previous error exists, it
|
||||||
|
// prevents further writing and returns the error.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - payload: A byte slice containing the data to be written.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - The number of bytes successfully written.
|
||||||
|
// - An error if the writing process fails, or if a previous error was encountered.
|
||||||
func (mw *msgWriter) Write(payload []byte) (int, error) {
|
func (mw *msgWriter) Write(payload []byte) (int, error) {
|
||||||
if mw.err != nil {
|
if mw.err != nil {
|
||||||
return 0, fmt.Errorf("failed to write due to previous error: %w", mw.err)
|
return 0, fmt.Errorf("failed to write due to previous error: %w", mw.err)
|
||||||
|
@ -57,7 +85,19 @@ func (mw *msgWriter) Write(payload []byte) (int, error) {
|
||||||
return n, mw.err
|
return n, mw.err
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeMsg formats the message and sends it to its io.Writer
|
// writeMsg formats the message and writes it to the msgWriter's io.Writer.
|
||||||
|
//
|
||||||
|
// This method handles the process of writing the message headers and body content, including handling
|
||||||
|
// multipart structures (e.g., mixed, related, alternative), PGP types, and attachments/embeds. It sets the
|
||||||
|
// required headers (e.g., "From", "To", "Cc") and iterates over the message parts, writing them to the
|
||||||
|
// output writer.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - msg: A pointer to the Msg struct containing the message data and headers to be written.
|
||||||
|
//
|
||||||
|
// References:
|
||||||
|
// - https://datatracker.ietf.org/doc/html/rfc2045 (Multipurpose Internet Mail Extensions - MIME)
|
||||||
|
// - https://datatracker.ietf.org/doc/html/rfc5322 (Internet Message Format)
|
||||||
func (mw *msgWriter) writeMsg(msg *Msg) {
|
func (mw *msgWriter) writeMsg(msg *Msg) {
|
||||||
msg.addDefaultHeader()
|
msg.addDefaultHeader()
|
||||||
msg.checkUserAgent()
|
msg.checkUserAgent()
|
||||||
|
@ -88,6 +128,10 @@ func (mw *msgWriter) writeMsg(msg *Msg) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if msg.hasSMime() {
|
||||||
|
mw.startMP(MIMESMime, msg.boundary)
|
||||||
|
mw.writeString(DoubleNewLine)
|
||||||
|
}
|
||||||
if msg.hasMixed() {
|
if msg.hasMixed() {
|
||||||
mw.startMP(MIMEMixed, msg.boundary)
|
mw.startMP(MIMEMixed, msg.boundary)
|
||||||
mw.writeString(DoubleNewLine)
|
mw.writeString(DoubleNewLine)
|
||||||
|
@ -134,9 +178,19 @@ func (mw *msgWriter) writeMsg(msg *Msg) {
|
||||||
if msg.hasMixed() {
|
if msg.hasMixed() {
|
||||||
mw.stopMP()
|
mw.stopMP()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if msg.hasSMime() {
|
||||||
|
mw.stopMP()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeGenHeader writes out all generic headers to the msgWriter
|
// writeGenHeader writes out all generic headers to the msgWriter.
|
||||||
|
//
|
||||||
|
// This function extracts all generic headers from the provided Msg object, sorts them, and writes them
|
||||||
|
// to the msgWriter in alphabetical order.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - msg: The Msg object containing the headers to be written.
|
||||||
func (mw *msgWriter) writeGenHeader(msg *Msg) {
|
func (mw *msgWriter) writeGenHeader(msg *Msg) {
|
||||||
keys := make([]string, 0, len(msg.genHeader))
|
keys := make([]string, 0, len(msg.genHeader))
|
||||||
for key := range msg.genHeader {
|
for key := range msg.genHeader {
|
||||||
|
@ -148,14 +202,32 @@ func (mw *msgWriter) writeGenHeader(msg *Msg) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// writePreformatedHeader writes out all preformated generic headers to the msgWriter
|
// writePreformattedGenHeader writes out all preformatted generic headers to the msgWriter.
|
||||||
|
//
|
||||||
|
// This function iterates over all preformatted generic headers from the provided Msg object and writes
|
||||||
|
// them to the msgWriter in the format "key: value" followed by a newline.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - msg: The Msg object containing the preformatted headers to be written.
|
||||||
func (mw *msgWriter) writePreformattedGenHeader(msg *Msg) {
|
func (mw *msgWriter) writePreformattedGenHeader(msg *Msg) {
|
||||||
for key, val := range msg.preformHeader {
|
for key, val := range msg.preformHeader {
|
||||||
mw.writeString(fmt.Sprintf("%s: %s%s", key, val, SingleNewLine))
|
mw.writeString(fmt.Sprintf("%s: %s%s", key, val, SingleNewLine))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// startMP writes a multipart beginning
|
// startMP writes a multipart beginning.
|
||||||
|
//
|
||||||
|
// This function initializes a multipart writer for the msgWriter using the specified MIME type and
|
||||||
|
// boundary. It sets the Content-Type header to indicate the multipart type and writes the boundary
|
||||||
|
// information. If a boundary is provided, it is set explicitly; otherwise, a default boundary is
|
||||||
|
// generated. It also handles writing a new part when nested multipart structures are used.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - mimeType: The MIME type of the multipart content (e.g., "mixed", "alternative").
|
||||||
|
// - boundary: The boundary string separating different parts of the multipart message.
|
||||||
|
//
|
||||||
|
// References:
|
||||||
|
// - https://datatracker.ietf.org/doc/html/rfc2046
|
||||||
func (mw *msgWriter) startMP(mimeType MIMEType, boundary string) {
|
func (mw *msgWriter) startMP(mimeType MIMEType, boundary string) {
|
||||||
multiPartWriter := multipart.NewWriter(mw)
|
multiPartWriter := multipart.NewWriter(mw)
|
||||||
if boundary != "" {
|
if boundary != "" {
|
||||||
|
@ -175,7 +247,10 @@ func (mw *msgWriter) startMP(mimeType MIMEType, boundary string) {
|
||||||
mw.depth++
|
mw.depth++
|
||||||
}
|
}
|
||||||
|
|
||||||
// stopMP closes the multipart
|
// stopMP closes the multipart.
|
||||||
|
//
|
||||||
|
// This function closes the current multipart writer if there is an active multipart structure.
|
||||||
|
// It decreases the depth level of multipart nesting.
|
||||||
func (mw *msgWriter) stopMP() {
|
func (mw *msgWriter) stopMP() {
|
||||||
if mw.depth > 0 {
|
if mw.depth > 0 {
|
||||||
mw.err = mw.multiPartWriter[mw.depth-1].Close()
|
mw.err = mw.multiPartWriter[mw.depth-1].Close()
|
||||||
|
@ -183,7 +258,17 @@ func (mw *msgWriter) stopMP() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// addFiles adds the attachments/embeds file content to the mail body
|
// addFiles adds the attachments/embeds file content to the mail body.
|
||||||
|
//
|
||||||
|
// This function iterates through the list of files, setting necessary headers for each file,
|
||||||
|
// including Content-Type, Content-Transfer-Encoding, Content-Disposition, and Content-ID
|
||||||
|
// (if the file is an embed). It determines the appropriate MIME type for each file based on
|
||||||
|
// its extension or the provided ContentType. It writes file headers and file content
|
||||||
|
// to the mail body using the appropriate encoding.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - files: A slice of File objects to be added to the mail body.
|
||||||
|
// - isAttachment: A boolean indicating whether the files are attachments (true) or embeds (false).
|
||||||
func (mw *msgWriter) addFiles(files []*File, isAttachment bool) {
|
func (mw *msgWriter) addFiles(files []*File, isAttachment bool) {
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
encoding := EncodingB64
|
encoding := EncodingB64
|
||||||
|
@ -237,23 +322,45 @@ func (mw *msgWriter) addFiles(files []*File, isAttachment bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if mw.err == nil {
|
if mw.err == nil {
|
||||||
mw.writeBody(file.Writer, encoding)
|
mw.writeBody(file.Writer, encoding, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// newPart creates a new MIME multipart io.Writer and sets the partwriter to it
|
// newPart creates a new MIME multipart io.Writer and sets the partWriter to it.
|
||||||
|
//
|
||||||
|
// This function creates a new MIME part using the provided header information and assigns it
|
||||||
|
// to the partWriter. It interacts with the current multipart writer at the specified depth
|
||||||
|
// to create the part.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - header: A map containing the header fields and their corresponding values for the new part.
|
||||||
func (mw *msgWriter) newPart(header map[string][]string) {
|
func (mw *msgWriter) newPart(header map[string][]string) {
|
||||||
mw.partWriter, mw.err = mw.multiPartWriter[mw.depth-1].CreatePart(header)
|
mw.partWriter, mw.err = mw.multiPartWriter[mw.depth-1].CreatePart(header)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writePart writes the corresponding part to the Msg body
|
// writePart writes the corresponding part to the Msg body.
|
||||||
|
//
|
||||||
|
// This function writes a MIME part to the message body, setting the appropriate headers such
|
||||||
|
// as Content-Type and Content-Transfer-Encoding. It determines the charset for the part,
|
||||||
|
// either using the part's own charset or a fallback charset if none is specified. If the part
|
||||||
|
// is at the top level (depth 0), headers are written directly. For nested parts, it creates
|
||||||
|
// a new MIME part with the provided headers.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - part: The Part object containing the data to be written.
|
||||||
|
// - charset: The Charset used as a fallback if the part does not specify one.
|
||||||
func (mw *msgWriter) writePart(part *Part, charset Charset) {
|
func (mw *msgWriter) writePart(part *Part, charset Charset) {
|
||||||
partCharset := part.charset
|
partCharset := part.charset
|
||||||
if partCharset.String() == "" {
|
if partCharset.String() == "" {
|
||||||
partCharset = charset
|
partCharset = charset
|
||||||
}
|
}
|
||||||
contentType := fmt.Sprintf("%s; charset=%s", part.contentType, partCharset)
|
|
||||||
|
contentType := part.contentType.String()
|
||||||
|
if !part.IsSMimeSigned() {
|
||||||
|
contentType = strings.Join([]string{contentType, "; charset=", partCharset.String()}, "")
|
||||||
|
}
|
||||||
|
|
||||||
contentTransferEnc := part.encoding.String()
|
contentTransferEnc := part.encoding.String()
|
||||||
if mw.depth == 0 {
|
if mw.depth == 0 {
|
||||||
mw.writeHeader(HeaderContentType, contentType)
|
mw.writeHeader(HeaderContentType, contentType)
|
||||||
|
@ -269,10 +376,17 @@ func (mw *msgWriter) writePart(part *Part, charset Charset) {
|
||||||
mimeHeader.Add(string(HeaderContentTransferEnc), contentTransferEnc)
|
mimeHeader.Add(string(HeaderContentTransferEnc), contentTransferEnc)
|
||||||
mw.newPart(mimeHeader)
|
mw.newPart(mimeHeader)
|
||||||
}
|
}
|
||||||
mw.writeBody(part.writeFunc, part.encoding)
|
mw.writeBody(part.writeFunc, part.encoding, part.smime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeString writes a string into the msgWriter's io.Writer interface
|
// writeString writes a string into the msgWriter's io.Writer interface.
|
||||||
|
//
|
||||||
|
// This function writes the given string to the msgWriter's underlying writer. It checks for
|
||||||
|
// existing errors before performing the write operation. It also tracks the number of bytes
|
||||||
|
// written and updates the bytesWritten field accordingly.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - s: The string to be written.
|
||||||
func (mw *msgWriter) writeString(s string) {
|
func (mw *msgWriter) writeString(s string) {
|
||||||
if mw.err != nil {
|
if mw.err != nil {
|
||||||
return
|
return
|
||||||
|
@ -282,7 +396,16 @@ func (mw *msgWriter) writeString(s string) {
|
||||||
mw.bytesWritten += int64(n)
|
mw.bytesWritten += int64(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeHeader writes a header into the msgWriter's io.Writer
|
// writeHeader writes a header into the msgWriter's io.Writer.
|
||||||
|
//
|
||||||
|
// This function writes a header key and its associated values to the msgWriter. It ensures
|
||||||
|
// proper formatting of long headers by inserting line breaks as needed. The header values
|
||||||
|
// are joined and split into words to ensure compliance with the maximum header length
|
||||||
|
// (MaxHeaderLength). After processing the header, it is written to the underlying writer.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - key: The Header key to be written.
|
||||||
|
// - values: A variadic parameter representing the values associated with the header.
|
||||||
func (mw *msgWriter) writeHeader(key Header, values ...string) {
|
func (mw *msgWriter) writeHeader(key Header, values ...string) {
|
||||||
buffer := strings.Builder{}
|
buffer := strings.Builder{}
|
||||||
charLength := MaxHeaderLength - 2
|
charLength := MaxHeaderLength - 2
|
||||||
|
@ -317,8 +440,19 @@ func (mw *msgWriter) writeHeader(key Header, values ...string) {
|
||||||
mw.writeString("\r\n")
|
mw.writeString("\r\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeBody writes an io.Reader into an io.Writer using provided Encoding
|
// writeBody writes an io.Reader into an io.Writer using the provided Encoding.
|
||||||
func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encoding Encoding) {
|
//
|
||||||
|
// This function writes data from an io.Reader to the underlying writer using a specified
|
||||||
|
// encoding (quoted-printable, base64, or no encoding). It handles encoding of the content
|
||||||
|
// and manages writing the encoded data to the appropriate writer, depending on the depth
|
||||||
|
// (whether the data is part of a multipart structure or not). It also tracks the number
|
||||||
|
// of bytes written and manages any errors encountered during the process.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - writeFunc: A function that writes the body content to the given io.Writer.
|
||||||
|
// - encoding: The encoding type to use when writing the content (e.g., base64, quoted-printable).
|
||||||
|
// - singingWithSMime: Whether the msg should be signed with S/MIME or not.
|
||||||
|
func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encoding Encoding, singingWithSMime bool) {
|
||||||
var writer io.Writer
|
var writer io.Writer
|
||||||
var encodedWriter io.WriteCloser
|
var encodedWriter io.WriteCloser
|
||||||
var n int64
|
var n int64
|
||||||
|
@ -333,12 +467,11 @@ func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encodin
|
||||||
lineBreaker := Base64LineBreaker{}
|
lineBreaker := Base64LineBreaker{}
|
||||||
lineBreaker.out = &writeBuffer
|
lineBreaker.out = &writeBuffer
|
||||||
|
|
||||||
switch encoding {
|
if encoding == EncodingQP {
|
||||||
case EncodingQP:
|
|
||||||
encodedWriter = quotedprintable.NewWriter(&writeBuffer)
|
encodedWriter = quotedprintable.NewWriter(&writeBuffer)
|
||||||
case EncodingB64:
|
} else if encoding == EncodingB64 && !singingWithSMime {
|
||||||
encodedWriter = base64.NewEncoder(base64.StdEncoding, &lineBreaker)
|
encodedWriter = base64.NewEncoder(base64.StdEncoding, &lineBreaker)
|
||||||
case NoEncoding:
|
} else if encoding == NoEncoding || singingWithSMime {
|
||||||
_, err = writeFunc(&writeBuffer)
|
_, err = writeFunc(&writeBuffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mw.err = fmt.Errorf("bodyWriter function: %w", err)
|
mw.err = fmt.Errorf("bodyWriter function: %w", err)
|
||||||
|
@ -351,7 +484,7 @@ func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encodin
|
||||||
mw.bytesWritten += n
|
mw.bytesWritten += n
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
default:
|
} else {
|
||||||
encodedWriter = quotedprintable.NewWriter(writer)
|
encodedWriter = quotedprintable.NewWriter(writer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -154,3 +154,42 @@ func TestMsgWriter_writeMsg_PGP(t *testing.T) {
|
||||||
t.Errorf("writeMsg failed. Expected PGP encoding header but didn't find it in message output")
|
t.Errorf("writeMsg failed. Expected PGP encoding header but didn't find it in message output")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMsgWriter_writeMsg_SMime tests the writeMsg method of the msgWriter with S/MIME types set
|
||||||
|
func TestMsgWriter_writeMsg_SMime(t *testing.T) {
|
||||||
|
keyPair, err := getDummyCertificate()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to load dummy certificate. Cause: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := NewMsg()
|
||||||
|
if err := m.SignWithSMime(keyPair); err != nil {
|
||||||
|
t.Errorf("set of certificate was not successful")
|
||||||
|
}
|
||||||
|
_ = m.From(`"Toni Tester" <test@example.com>`)
|
||||||
|
_ = m.To(`"Toni Receiver" <receiver@example.com>`)
|
||||||
|
m.Subject("This is a subject")
|
||||||
|
m.SetBodyString(TypeTextPlain, "This is the body")
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
mw := &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding}
|
||||||
|
mw.writeMsg(m)
|
||||||
|
ms := buf.String()
|
||||||
|
|
||||||
|
if !strings.Contains(ms, "MIME-Version: 1.0") {
|
||||||
|
t.Errorf("writeMsg failed. Unable to find MIME-Version")
|
||||||
|
}
|
||||||
|
if !strings.Contains(ms, "Subject: This is a subject") {
|
||||||
|
t.Errorf("writeMsg failed. Unable to find subject")
|
||||||
|
}
|
||||||
|
if !strings.Contains(ms, "From: \"Toni Tester\" <test@example.com>") {
|
||||||
|
t.Errorf("writeMsg failed. Unable to find transmitter")
|
||||||
|
}
|
||||||
|
if !strings.Contains(ms, "To: \"Toni Receiver\" <receiver@example.com>") {
|
||||||
|
t.Errorf("writeMsg failed. Unable to find receiver")
|
||||||
|
}
|
||||||
|
|
||||||
|
boundary := ms[strings.LastIndex(ms, "--")-60 : strings.LastIndex(ms, "--")]
|
||||||
|
if !strings.Contains(ms, fmt.Sprintf("Content-Type: multipart/signed; protocol=\"application/pkcs7-signature\"; micalg=sha-256;\r\n boundary=%s", boundary)) {
|
||||||
|
t.Errorf("writeMsg failed. Unable to find Content-Type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
155
part.go
155
part.go
|
@ -12,7 +12,11 @@ import (
|
||||||
// PartOption returns a function that can be used for grouping Part options
|
// PartOption returns a function that can be used for grouping Part options
|
||||||
type PartOption func(*Part)
|
type PartOption func(*Part)
|
||||||
|
|
||||||
// Part is a part of the Msg
|
// Part is a part of the Msg.
|
||||||
|
//
|
||||||
|
// This struct represents a single part of a multipart message. Each part has a content type,
|
||||||
|
// charset, optional description, encoding, and a function to write its content to an io.Writer.
|
||||||
|
// It also includes a flag to mark the part as deleted.
|
||||||
type Part struct {
|
type Part struct {
|
||||||
contentType ContentType
|
contentType ContentType
|
||||||
charset Charset
|
charset Charset
|
||||||
|
@ -20,9 +24,17 @@ type Part struct {
|
||||||
encoding Encoding
|
encoding Encoding
|
||||||
isDeleted bool
|
isDeleted bool
|
||||||
writeFunc func(io.Writer) (int64, error)
|
writeFunc func(io.Writer) (int64, error)
|
||||||
|
smime bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetContent executes the WriteFunc of the Part and returns the content as byte slice
|
// GetContent executes the WriteFunc of the Part and returns the content as a byte slice.
|
||||||
|
//
|
||||||
|
// This function runs the part's writeFunc to write its content into a buffer and then returns
|
||||||
|
// the content as a byte slice. If an error occurs during the writing process, it is returned.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A byte slice containing the part's content.
|
||||||
|
// - An error if the writeFunc encounters an issue.
|
||||||
func (p *Part) GetContent() ([]byte, error) {
|
func (p *Part) GetContent() ([]byte, error) {
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
if _, err := p.writeFunc(&b); err != nil {
|
if _, err := p.writeFunc(&b); err != nil {
|
||||||
|
@ -31,85 +43,192 @@ func (p *Part) GetContent() ([]byte, error) {
|
||||||
return b.Bytes(), nil
|
return b.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCharset returns the currently set Charset of the Part
|
// GetCharset returns the currently set Charset of the Part.
|
||||||
|
//
|
||||||
|
// This function returns the Charset that is currently set for the Part.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - The Charset of the Part.
|
||||||
func (p *Part) GetCharset() Charset {
|
func (p *Part) GetCharset() Charset {
|
||||||
return p.charset
|
return p.charset
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetContentType returns the currently set ContentType of the Part
|
// GetContentType returns the currently set ContentType of the Part.
|
||||||
|
//
|
||||||
|
// This function returns the ContentType that is currently set for the Part.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - The ContentType of the Part.
|
||||||
func (p *Part) GetContentType() ContentType {
|
func (p *Part) GetContentType() ContentType {
|
||||||
return p.contentType
|
return p.contentType
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEncoding returns the currently set Encoding of the Part
|
// GetEncoding returns the currently set Encoding of the Part.
|
||||||
|
//
|
||||||
|
// This function returns the Encoding that is currently set for the Part.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - The Encoding of the Part.
|
||||||
func (p *Part) GetEncoding() Encoding {
|
func (p *Part) GetEncoding() Encoding {
|
||||||
return p.encoding
|
return p.encoding
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWriteFunc returns the currently set WriterFunc of the Part
|
// GetWriteFunc returns the currently set WriteFunc of the Part.
|
||||||
|
//
|
||||||
|
// This function returns the WriteFunc that is currently set for the Part, which writes
|
||||||
|
// the part's content to an io.Writer.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - The WriteFunc of the Part, which is a function that takes an io.Writer and returns
|
||||||
|
// the number of bytes written and an error (if any).
|
||||||
func (p *Part) GetWriteFunc() func(io.Writer) (int64, error) {
|
func (p *Part) GetWriteFunc() func(io.Writer) (int64, error) {
|
||||||
return p.writeFunc
|
return p.writeFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDescription returns the currently set Content-Description of the Part
|
// GetDescription returns the currently set Content-Description of the Part.
|
||||||
|
//
|
||||||
|
// This function returns the Content-Description that is currently set for the Part.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - The Content-Description of the Part as a string.
|
||||||
func (p *Part) GetDescription() string {
|
func (p *Part) GetDescription() string {
|
||||||
return p.description
|
return p.description
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetContent overrides the content of the Part with the given string
|
// IsSMimeSigned returns true if the Part should be singed with S/MIME
|
||||||
|
func (p *Part) IsSMimeSigned() bool {
|
||||||
|
return p.smime
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContent overrides the content of the Part with the given string.
|
||||||
|
//
|
||||||
|
// This function sets the content of the Part by creating a new writeFunc that writes the
|
||||||
|
// provided string content to an io.Writer.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - content: The string that will replace the current content of the Part.
|
||||||
func (p *Part) SetContent(content string) {
|
func (p *Part) SetContent(content string) {
|
||||||
buffer := bytes.NewBufferString(content)
|
buffer := bytes.NewBufferString(content)
|
||||||
p.writeFunc = writeFuncFromBuffer(buffer)
|
p.writeFunc = writeFuncFromBuffer(buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetContentType overrides the ContentType of the Part
|
// SetContentType overrides the ContentType of the Part.
|
||||||
|
//
|
||||||
|
// This function sets a new ContentType for the Part, replacing the existing one.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - contentType: The new ContentType to be set for the Part.
|
||||||
func (p *Part) SetContentType(contentType ContentType) {
|
func (p *Part) SetContentType(contentType ContentType) {
|
||||||
p.contentType = contentType
|
p.contentType = contentType
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetCharset overrides the Charset of the Part
|
// SetCharset overrides the Charset of the Part.
|
||||||
|
//
|
||||||
|
// This function sets a new Charset for the Part, replacing the existing one.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - charset: The new Charset to be set for the Part.
|
||||||
func (p *Part) SetCharset(charset Charset) {
|
func (p *Part) SetCharset(charset Charset) {
|
||||||
p.charset = charset
|
p.charset = charset
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetEncoding creates a new mime.WordEncoder based on the encoding setting of the message
|
// SetEncoding creates a new mime.WordEncoder based on the encoding setting of the message.
|
||||||
|
//
|
||||||
|
// This function sets a new Encoding for the Part, replacing the existing one.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - encoding: The new Encoding to be set for the Part.
|
||||||
func (p *Part) SetEncoding(encoding Encoding) {
|
func (p *Part) SetEncoding(encoding Encoding) {
|
||||||
p.encoding = encoding
|
p.encoding = encoding
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDescription overrides the Content-Description of the Part
|
// SetDescription overrides the Content-Description of the Part.
|
||||||
|
//
|
||||||
|
// This function sets a new Content-Description for the Part, replacing the existing one.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - description: The new Content-Description to be set for the Part.
|
||||||
func (p *Part) SetDescription(description string) {
|
func (p *Part) SetDescription(description string) {
|
||||||
p.description = description
|
p.description = description
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetWriteFunc overrides the WriteFunc of the Part
|
|
||||||
|
// SetIsSMimeSigned sets the flag for signing the Part with S/MIME
|
||||||
|
func (p *Part) SetIsSMimeSigned(smime bool) {
|
||||||
|
p.smime = smime
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetWriteFunc overrides the WriteFunc of the Part.
|
||||||
|
//
|
||||||
|
// This function sets a new WriteFunc for the Part, replacing the existing one. The WriteFunc
|
||||||
|
// is responsible for writing the Part's content to an io.Writer.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - writeFunc: A function that writes the Part's content to an io.Writer and returns
|
||||||
|
// the number of bytes written and an error (if any).
|
||||||
func (p *Part) SetWriteFunc(writeFunc func(io.Writer) (int64, error)) {
|
func (p *Part) SetWriteFunc(writeFunc func(io.Writer) (int64, error)) {
|
||||||
p.writeFunc = writeFunc
|
p.writeFunc = writeFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes the current part from the parts list of the Msg by setting the
|
// Delete removes the current part from the parts list of the Msg by setting the isDeleted flag to true.
|
||||||
// isDeleted flag to true. The msgWriter will skip it then
|
//
|
||||||
|
// This function marks the Part as deleted by setting the isDeleted flag to true. The msgWriter
|
||||||
|
// will skip over this Part during processing.
|
||||||
func (p *Part) Delete() {
|
func (p *Part) Delete() {
|
||||||
p.isDeleted = true
|
p.isDeleted = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithPartCharset overrides the default Part charset
|
// WithPartCharset overrides the default Part charset.
|
||||||
|
//
|
||||||
|
// This function returns a PartOption that allows the charset of a Part to be overridden
|
||||||
|
// with the specified Charset.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - charset: The Charset to be set for the Part.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A PartOption function that sets the Part's charset.
|
||||||
func WithPartCharset(charset Charset) PartOption {
|
func WithPartCharset(charset Charset) PartOption {
|
||||||
return func(p *Part) {
|
return func(p *Part) {
|
||||||
p.charset = charset
|
p.charset = charset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithPartEncoding overrides the default Part encoding
|
// WithPartEncoding overrides the default Part encoding.
|
||||||
|
//
|
||||||
|
// This function returns a PartOption that allows the encoding of a Part to be overridden
|
||||||
|
// with the specified Encoding.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - encoding: The Encoding to be set for the Part.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A PartOption function that sets the Part's encoding.
|
||||||
func WithPartEncoding(encoding Encoding) PartOption {
|
func WithPartEncoding(encoding Encoding) PartOption {
|
||||||
return func(p *Part) {
|
return func(p *Part) {
|
||||||
p.encoding = encoding
|
p.encoding = encoding
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithPartContentDescription overrides the default Part Content-Description
|
// WithPartContentDescription overrides the default Part Content-Description.
|
||||||
|
//
|
||||||
|
// This function returns a PartOption that allows the Content-Description of a Part
|
||||||
|
// to be overridden with the specified description.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - description: The Content-Description to be set for the Part.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A PartOption function that sets the Part's Content-Description.
|
||||||
func WithPartContentDescription(description string) PartOption {
|
func WithPartContentDescription(description string) PartOption {
|
||||||
return func(p *Part) {
|
return func(p *Part) {
|
||||||
p.description = description
|
p.description = description
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithSMimeSinging overrides the flag for signing the Part with S/MIME
|
||||||
|
func WithSMimeSinging() PartOption {
|
||||||
|
return func(p *Part) {
|
||||||
|
p.smime = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
45
part_test.go
45
part_test.go
|
@ -102,6 +102,24 @@ func TestPart_WithPartContentDescription(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestPart_WithSMimeSinging tests the WithSMimeSinging method
|
||||||
|
func TestPart_WithSMimeSinging(t *testing.T) {
|
||||||
|
m := NewMsg()
|
||||||
|
part := m.newPart(TypeTextPlain, WithSMimeSinging())
|
||||||
|
if part == nil {
|
||||||
|
t.Errorf("newPart() WithSMimeSinging() failed: no part returned")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if part.smime != true {
|
||||||
|
t.Errorf("newPart() WithSMimeSinging() failed: expected: %v, got: %v", true, part.smime)
|
||||||
|
}
|
||||||
|
part.smime = true
|
||||||
|
part.SetIsSMimeSigned(false)
|
||||||
|
if part.smime != false {
|
||||||
|
t.Errorf("newPart() SetIsSMimeSigned() failed: expected: %v, got: %v", false, part.smime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestPartContentType tests Part.SetContentType
|
// TestPartContentType tests Part.SetContentType
|
||||||
func TestPart_SetContentType(t *testing.T) {
|
func TestPart_SetContentType(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
@ -245,6 +263,33 @@ func TestPart_GetContentBroken(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestPart_IsSMimeSigned tests Part.IsSMimeSigned
|
||||||
|
func TestPart_IsSMimeSigned(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"smime:", true},
|
||||||
|
{"smime:", false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
m := NewMsg()
|
||||||
|
m.SetBodyString(TypeTextPlain, "This is a body!")
|
||||||
|
pl, err := getPartList(m)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pl[0].SetIsSMimeSigned(tt.want)
|
||||||
|
smime := pl[0].IsSMimeSigned()
|
||||||
|
if smime != tt.want {
|
||||||
|
t.Errorf("SetContentType failed. Got: %v, expected: %v", smime, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestPart_SetWriteFunc tests Part.SetWriteFunc
|
// TestPart_SetWriteFunc tests Part.SetWriteFunc
|
||||||
func TestPart_SetWriteFunc(t *testing.T) {
|
func TestPart_SetWriteFunc(t *testing.T) {
|
||||||
c := "This is a test with ümläutß"
|
c := "This is a test with ümläutß"
|
||||||
|
|
32
random.go
32
random.go
|
@ -11,17 +11,37 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Range of characters for the secure string generation
|
// Range of characters for the secure string generation
|
||||||
const cr = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
|
const cr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-"
|
||||||
|
|
||||||
// Bitmask sizes for the string generators (based on 93 chars total)
|
// Bitmask sizes for the string generators (based on 93 chars total)
|
||||||
|
//
|
||||||
|
// These constants define bitmask-related values used for efficient random string generation.
|
||||||
|
// The bitmask operates over 66 possible characters, and the constants help determine the
|
||||||
|
// number of bits and indices used in the process.
|
||||||
const (
|
const (
|
||||||
letterIdxBits = 7 // 7 bits to represent a letter index
|
// letterIdxBits: Number of bits needed to represent a letter index. We have 64 possible characters
|
||||||
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
// which fit into 6 bits.
|
||||||
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
letterIdxBits = 6
|
||||||
|
// letterIdxMask: Bitmask to extract letter indices (all 1-bits for letterIdxBits).
|
||||||
|
letterIdxMask = 1<<letterIdxBits - 1
|
||||||
|
// letterIdxMax: The maximum number of letter indices that fit in 63 bits.
|
||||||
|
letterIdxMax = 63 / letterIdxBits
|
||||||
)
|
)
|
||||||
|
|
||||||
// randomStringSecure returns a random, string of length characters. This method uses the
|
// randomStringSecure returns a random string of the specified length.
|
||||||
// crypto/random package and therfore is cryptographically secure
|
//
|
||||||
|
// This function generates a cryptographically secure random string of the given length using
|
||||||
|
// the crypto/rand package. It ensures that the randomness is secure and suitable for
|
||||||
|
// cryptographic purposes. The function reads random bytes, converts them to indices within
|
||||||
|
// a character range, and builds the string. If an error occurs while reading from the random
|
||||||
|
// pool, it returns the error.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - length: The length of the random string to be generated.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A randomly generated string.
|
||||||
|
// - An error if the random generation fails.
|
||||||
func randomStringSecure(length int) (string, error) {
|
func randomStringSecure(length int) (string, error) {
|
||||||
randString := strings.Builder{}
|
randString := strings.Builder{}
|
||||||
randString.Grow(length)
|
randString.Grow(length)
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build !go1.20
|
|
||||||
// +build !go1.20
|
|
||||||
|
|
||||||
package mail
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/rand"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// randNum returns a random number with a maximum value of length
|
|
||||||
func randNum(maxval int) int {
|
|
||||||
if maxval <= 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
rand.Seed(time.Now().UnixNano())
|
|
||||||
return rand.Intn(maxval)
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build go1.20 && !go1.22
|
|
||||||
// +build go1.20,!go1.22
|
|
||||||
|
|
||||||
package mail
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/rand"
|
|
||||||
)
|
|
||||||
|
|
||||||
// randNum returns a random number with a maximum value of length
|
|
||||||
func randNum(maxval int) int {
|
|
||||||
if maxval <= 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return rand.Intn(maxval)
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build go1.22
|
|
||||||
// +build go1.22
|
|
||||||
|
|
||||||
package mail
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/rand/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// randNum returns a random number with a maximum value of maxval.
|
|
||||||
// go-mail compiled with Go 1.22+ will make use of the novel math/rand/v2 interface
|
|
||||||
// Older versions of Go will use math/rand
|
|
||||||
func randNum(maxval int) int {
|
|
||||||
if maxval <= 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return rand.IntN(maxval)
|
|
||||||
}
|
|
|
@ -38,34 +38,12 @@ func TestRandomStringSecure(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRandomNum tests the randomNum method
|
func BenchmarkGenerator_RandomStringSecure(b *testing.B) {
|
||||||
func TestRandomNum(t *testing.T) {
|
b.ReportAllocs()
|
||||||
tt := []struct {
|
for i := 0; i < b.N; i++ {
|
||||||
testName string
|
_, err := randomStringSecure(22)
|
||||||
max int
|
if err != nil {
|
||||||
}{
|
b.Errorf("RandomStringFromCharRange() failed: %s", err)
|
||||||
{"Max: 1", 1},
|
|
||||||
{"Max: 20", 20},
|
|
||||||
{"Max: 50", 50},
|
|
||||||
{"Max: 100", 100},
|
|
||||||
{"Max: 1000", 1000},
|
|
||||||
{"Max: 10000", 10000},
|
|
||||||
{"Max: 100000000", 100000000},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tt {
|
|
||||||
t.Run(tc.testName, func(t *testing.T) {
|
|
||||||
rn := randNum(tc.max)
|
|
||||||
if rn > tc.max {
|
|
||||||
t.Errorf("random number generation failed: %d is bigger than given value %d", rn, tc.max)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRandomNumZero(t *testing.T) {
|
|
||||||
rn := randNum(0)
|
|
||||||
if rn != 0 {
|
|
||||||
t.Errorf("random number generation failed: %d is not zero", rn)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
40
reader.go
40
reader.go
|
@ -8,19 +8,41 @@ import (
|
||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Reader is a type that implements the io.Reader interface for a Msg
|
// Reader is a type that implements the io.Reader interface for a Msg.
|
||||||
|
//
|
||||||
|
// This struct represents a reader that reads from a byte slice buffer. It keeps track of the
|
||||||
|
// current read position (offset) and any initialization error. The buffer holds the data to be
|
||||||
|
// read from the message.
|
||||||
type Reader struct {
|
type Reader struct {
|
||||||
buffer []byte // contents are the bytes buffer[offset : len(buffer)]
|
buffer []byte // contents are the bytes buffer[offset : len(buffer)]
|
||||||
offset int // read at &buffer[offset], write at &buffer[len(buffer)]
|
offset int // read at &buffer[offset], write at &buffer[len(buffer)]
|
||||||
err error // initialization error
|
err error // initialization error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error returns an error if the Reader err field is not nil
|
// Error returns an error if the Reader err field is not nil.
|
||||||
|
//
|
||||||
|
// This function checks the Reader's err field and returns it if it is not nil. If no error
|
||||||
|
// occurred during initialization, it returns nil.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - The error stored in the err field, or nil if no error is present.
|
||||||
func (r *Reader) Error() error {
|
func (r *Reader) Error() error {
|
||||||
return r.err
|
return r.err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read reads the length of p of the Msg buffer to satisfy the io.Reader interface
|
// Read reads the content of the Msg buffer into the provided payload to satisfy the io.Reader interface.
|
||||||
|
//
|
||||||
|
// This function reads data from the Reader's buffer into the provided byte slice (payload).
|
||||||
|
// It checks for errors or an empty buffer and resets the Reader if necessary. If no data is available,
|
||||||
|
// it returns io.EOF. Otherwise, it copies the content from the buffer into the payload and updates
|
||||||
|
// the read offset.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - payload: A byte slice where the data will be copied.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - n: The number of bytes copied into the payload.
|
||||||
|
// - err: An error if any issues occurred during the read operation or io.EOF if the buffer is empty.
|
||||||
func (r *Reader) Read(payload []byte) (n int, err error) {
|
func (r *Reader) Read(payload []byte) (n int, err error) {
|
||||||
if r.err != nil {
|
if r.err != nil {
|
||||||
return 0, r.err
|
return 0, r.err
|
||||||
|
@ -37,12 +59,20 @@ func (r *Reader) Read(payload []byte) (n int, err error) {
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset resets the Reader buffer to be empty, but it retains the underlying storage
|
// Reset resets the Reader buffer to be empty, but it retains the underlying storage for future use.
|
||||||
// for use by future writes.
|
//
|
||||||
|
// This function clears the Reader's buffer by setting its length to 0 and resets the read offset
|
||||||
|
// to the beginning. The underlying storage is retained, allowing future writes to reuse the buffer.
|
||||||
func (r *Reader) Reset() {
|
func (r *Reader) Reset() {
|
||||||
r.buffer = r.buffer[:0]
|
r.buffer = r.buffer[:0]
|
||||||
r.offset = 0
|
r.offset = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// empty reports whether the unread portion of the Reader buffer is empty.
|
// empty reports whether the unread portion of the Reader buffer is empty.
|
||||||
|
//
|
||||||
|
// This function checks if the unread portion of the Reader's buffer is empty by comparing
|
||||||
|
// the length of the buffer to the current read offset.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - true if the unread portion is empty, false otherwise.
|
||||||
func (r *Reader) empty() bool { return len(r.buffer) <= r.offset }
|
func (r *Reader) empty() bool { return len(r.buffer) <= r.offset }
|
||||||
|
|
75
senderror.go
75
senderror.go
|
@ -54,7 +54,11 @@ const (
|
||||||
ErrAmbiguous
|
ErrAmbiguous
|
||||||
)
|
)
|
||||||
|
|
||||||
// SendError is an error wrapper for delivery errors of the Msg
|
// SendError is an error wrapper for delivery errors of the Msg.
|
||||||
|
//
|
||||||
|
// This struct represents an error that occurs during the delivery of a message. It holds
|
||||||
|
// 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.
|
||||||
type SendError struct {
|
type SendError struct {
|
||||||
affectedMsg *Msg
|
affectedMsg *Msg
|
||||||
errlist []error
|
errlist []error
|
||||||
|
@ -66,7 +70,16 @@ type SendError struct {
|
||||||
// SendErrReason represents a comparable reason on why the delivery failed
|
// SendErrReason represents a comparable reason on why the delivery failed
|
||||||
type SendErrReason int
|
type SendErrReason int
|
||||||
|
|
||||||
// Error implements the error interface for the SendError type
|
// Error implements the error interface for the SendError type.
|
||||||
|
//
|
||||||
|
// This function returns a detailed error message string for the SendError, including the
|
||||||
|
// reason for failure, list of errors, affected recipients, and the message ID of the
|
||||||
|
// affected message (if available). If the reason is unknown (greater than 10), it returns
|
||||||
|
// "unknown reason". The error message is built dynamically based on the content of the
|
||||||
|
// error list, recipient list, and message ID.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A string representing the error message.
|
||||||
func (e *SendError) Error() string {
|
func (e *SendError) Error() string {
|
||||||
if e.Reason > 10 {
|
if e.Reason > 10 {
|
||||||
return "unknown reason"
|
return "unknown reason"
|
||||||
|
@ -101,7 +114,17 @@ func (e *SendError) Error() string {
|
||||||
return errMessage.String()
|
return errMessage.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is implements the errors.Is functionality and compares the SendErrReason
|
// Is implements the errors.Is functionality and compares the SendErrReason.
|
||||||
|
//
|
||||||
|
// This function allows for comparison between two errors by checking if the provided
|
||||||
|
// error matches the SendError type and, if so, compares the SendErrReason and the
|
||||||
|
// temporary status (isTemp) of both errors.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - errType: The error to compare against the current SendError.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - true if the errors have the same reason and temporary status, false otherwise.
|
||||||
func (e *SendError) Is(errType error) bool {
|
func (e *SendError) Is(errType error) bool {
|
||||||
var t *SendError
|
var t *SendError
|
||||||
if errors.As(errType, &t) && t != nil {
|
if errors.As(errType, &t) && t != nil {
|
||||||
|
@ -110,7 +133,13 @@ func (e *SendError) Is(errType error) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTemp returns true if the delivery error is of temporary nature and can be retried
|
// IsTemp returns true if the delivery error is of a temporary nature and can be retried.
|
||||||
|
//
|
||||||
|
// This function checks whether the SendError indicates a temporary error, which suggests
|
||||||
|
// that the delivery can be retried. If the SendError is nil, it returns false.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - true if the error is temporary, false otherwise.
|
||||||
func (e *SendError) IsTemp() bool {
|
func (e *SendError) IsTemp() bool {
|
||||||
if e == nil {
|
if e == nil {
|
||||||
return false
|
return false
|
||||||
|
@ -118,8 +147,13 @@ func (e *SendError) IsTemp() bool {
|
||||||
return e.isTemp
|
return e.isTemp
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessageID returns the message ID of the affected Msg that caused the error
|
// MessageID returns the message ID of the affected Msg that caused the error.
|
||||||
// If no message ID was set for the Msg, an empty string will be returned
|
//
|
||||||
|
// This function retrieves the message ID of the Msg associated with the SendError.
|
||||||
|
// If no message ID was set or if the SendError or Msg is nil, it returns an empty string.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - The message ID as a string, or an empty string if no ID is available.
|
||||||
func (e *SendError) MessageID() string {
|
func (e *SendError) MessageID() string {
|
||||||
if e == nil || e.affectedMsg == nil {
|
if e == nil || e.affectedMsg == nil {
|
||||||
return ""
|
return ""
|
||||||
|
@ -127,7 +161,13 @@ func (e *SendError) MessageID() string {
|
||||||
return e.affectedMsg.GetMessageID()
|
return e.affectedMsg.GetMessageID()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Msg returns the pointer to the affected message that caused the error
|
// Msg returns the pointer to the affected message that caused the error.
|
||||||
|
//
|
||||||
|
// This function retrieves the Msg associated with the SendError. If the SendError or
|
||||||
|
// the affectedMsg is nil, it returns nil.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A pointer to the Msg that caused the error, or nil if not available.
|
||||||
func (e *SendError) Msg() *Msg {
|
func (e *SendError) Msg() *Msg {
|
||||||
if e == nil || e.affectedMsg == nil {
|
if e == nil || e.affectedMsg == nil {
|
||||||
return nil
|
return nil
|
||||||
|
@ -135,7 +175,14 @@ func (e *SendError) Msg() *Msg {
|
||||||
return e.affectedMsg
|
return e.affectedMsg
|
||||||
}
|
}
|
||||||
|
|
||||||
// String implements the Stringer interface for the SendErrReason
|
// String satisfies the fmt.Stringer interface for the SendErrReason type.
|
||||||
|
//
|
||||||
|
// This function converts the SendErrReason into a human-readable string representation based
|
||||||
|
// on the error type. If the error reason does not match any predefined case, it returns
|
||||||
|
// "unknown reason".
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A string representation of the SendErrReason.
|
||||||
func (r SendErrReason) String() string {
|
func (r SendErrReason) String() string {
|
||||||
switch r {
|
switch r {
|
||||||
case ErrGetSender:
|
case ErrGetSender:
|
||||||
|
@ -164,8 +211,16 @@ func (r SendErrReason) String() string {
|
||||||
return "unknown reason"
|
return "unknown reason"
|
||||||
}
|
}
|
||||||
|
|
||||||
// isTempError checks the given SMTP error and returns true if the given error is of temporary nature
|
// isTempError checks if the given SMTP error is of a temporary nature and should be retried.
|
||||||
// and should be retried
|
//
|
||||||
|
// This function inspects the error message and returns true if the first character of the
|
||||||
|
// error message is '4', indicating a temporary SMTP error that can be retried.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - err: The error to check.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - true if the error is temporary, false otherwise.
|
||||||
func isTempError(err error) bool {
|
func isTempError(err error) bool {
|
||||||
return err.Error()[0] == '4'
|
return err.Error()[0] == '4'
|
||||||
}
|
}
|
||||||
|
|
151
smime.go
Normal file
151
smime.go
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"go.mozilla.org/pkcs7"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrInvalidKeyPair should be used if key pair is invalid
|
||||||
|
ErrInvalidKeyPair = errors.New("invalid key pair")
|
||||||
|
|
||||||
|
// ErrCouldNotInitialize should be used if the signed data could not initialize
|
||||||
|
ErrCouldNotInitialize = errors.New("could not initialize signed data")
|
||||||
|
|
||||||
|
// ErrCouldNotAddSigner should be used if the signer could not be added
|
||||||
|
ErrCouldNotAddSigner = errors.New("could not add signer message")
|
||||||
|
|
||||||
|
// ErrCouldNotFinishSigning should be used if the signing could not be finished
|
||||||
|
ErrCouldNotFinishSigning = errors.New("could not finish signing")
|
||||||
|
|
||||||
|
// ErrCouldNoEncodeToPEM should be used if the signature could not be encoded to PEM
|
||||||
|
ErrCouldNoEncodeToPEM = errors.New("could not encode to PEM")
|
||||||
|
)
|
||||||
|
|
||||||
|
// SMime is used to sign messages with S/MIME
|
||||||
|
type SMime struct {
|
||||||
|
privateKey *rsa.PrivateKey
|
||||||
|
certificate *x509.Certificate
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSMime construct a new instance of SMime with a provided *tls.Certificate
|
||||||
|
func newSMime(keyPair *tls.Certificate) (*SMime, error) {
|
||||||
|
if keyPair == nil {
|
||||||
|
return nil, ErrInvalidKeyPair
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SMime{
|
||||||
|
privateKey: keyPair.PrivateKey.(*rsa.PrivateKey),
|
||||||
|
certificate: keyPair.Leaf,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// signMessage signs the message with S/MIME
|
||||||
|
func (sm *SMime) signMessage(message string) (*string, error) {
|
||||||
|
lines := parseLines([]byte(message))
|
||||||
|
toBeSigned := lines.bytesFromLines([]byte("\r\n"))
|
||||||
|
|
||||||
|
signedData, err := pkcs7.NewSignedData(toBeSigned)
|
||||||
|
signedData.SetDigestAlgorithm(pkcs7.OIDDigestAlgorithmSHA256)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrCouldNotInitialize
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = signedData.AddSigner(sm.certificate, sm.privateKey, pkcs7.SignerInfoConfig{}); err != nil {
|
||||||
|
return nil, ErrCouldNotAddSigner
|
||||||
|
}
|
||||||
|
|
||||||
|
signedData.Detach()
|
||||||
|
|
||||||
|
signatureDER, err := signedData.Finish()
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrCouldNotFinishSigning
|
||||||
|
}
|
||||||
|
|
||||||
|
pemMsg, err := encodeToPEM(signatureDER)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrCouldNoEncodeToPEM
|
||||||
|
}
|
||||||
|
|
||||||
|
return pemMsg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createMessage prepares the message that will be used for the sign method later
|
||||||
|
func (sm *SMime) prepareMessage(encoding Encoding, contentType ContentType, charset Charset, body []byte) string {
|
||||||
|
return fmt.Sprintf("Content-Transfer-Encoding: %v\r\nContent-Type: %v; charset=%v\r\n\r\n%v", encoding, contentType, charset, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeToPEM uses the method pem.Encode from the standard library but cuts the typical PEM preamble
|
||||||
|
func encodeToPEM(msg []byte) (*string, error) {
|
||||||
|
block := &pem.Block{Bytes: msg}
|
||||||
|
|
||||||
|
var arrayBuffer bytes.Buffer
|
||||||
|
if err := pem.Encode(&arrayBuffer, block); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r := arrayBuffer.String()
|
||||||
|
r = strings.TrimPrefix(r, "-----BEGIN -----")
|
||||||
|
r = strings.Trim(r, "\n")
|
||||||
|
r = strings.TrimSuffix(r, "-----END -----")
|
||||||
|
r = strings.Trim(r, "\n")
|
||||||
|
|
||||||
|
return &r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// line is the representation of one line of the message that will be used for signing purposes
|
||||||
|
type line struct {
|
||||||
|
line []byte
|
||||||
|
endOfLine []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// lines is the representation of a message that will be used for signing purposes
|
||||||
|
type lines []line
|
||||||
|
|
||||||
|
// bytesFromLines creates the line representation with the given endOfLine char
|
||||||
|
func (ls lines) bytesFromLines(sep []byte) []byte {
|
||||||
|
var raw []byte
|
||||||
|
for i := range ls {
|
||||||
|
raw = append(raw, ls[i].line...)
|
||||||
|
if len(ls[i].endOfLine) != 0 && sep != nil {
|
||||||
|
raw = append(raw, sep...)
|
||||||
|
} else {
|
||||||
|
raw = append(raw, ls[i].endOfLine...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseLines constructs the lines representation of a given message
|
||||||
|
func parseLines(raw []byte) lines {
|
||||||
|
oneLine := line{raw, nil}
|
||||||
|
lines := lines{oneLine}
|
||||||
|
lines = lines.splitLine([]byte("\r\n"))
|
||||||
|
lines = lines.splitLine([]byte("\r"))
|
||||||
|
lines = lines.splitLine([]byte("\n"))
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitLine uses the given endOfLine to split the given line
|
||||||
|
func (ls lines) splitLine(sep []byte) lines {
|
||||||
|
nl := lines{}
|
||||||
|
for _, l := range ls {
|
||||||
|
split := bytes.Split(l.line, sep)
|
||||||
|
if len(split) > 1 {
|
||||||
|
for i := 0; i < len(split)-1; i++ {
|
||||||
|
nl = append(nl, line{split[i], sep})
|
||||||
|
}
|
||||||
|
nl = append(nl, line{split[len(split)-1], l.endOfLine})
|
||||||
|
} else {
|
||||||
|
nl = append(nl, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nl
|
||||||
|
}
|
190
smime_test.go
Normal file
190
smime_test.go
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestNewSMime tests the newSMime method
|
||||||
|
func TestNewSMime(t *testing.T) {
|
||||||
|
keyPair, err := getDummyCertificate()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error getting dummy certificate: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sMime, err := newSMime(keyPair)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error creating new SMime from keyPair: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sMime.privateKey != keyPair.PrivateKey {
|
||||||
|
t.Errorf("NewSMime() did not return the same private key")
|
||||||
|
}
|
||||||
|
if sMime.certificate != keyPair.Leaf {
|
||||||
|
t.Errorf("NewSMime() did not return the same leaf certificate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSign tests the sign method
|
||||||
|
func TestSign(t *testing.T) {
|
||||||
|
keyPair, err := getDummyCertificate()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error getting dummy certificate: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sMime, err := newSMime(keyPair)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error creating new SMime from keyPair: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
message := "This is a test message"
|
||||||
|
singedMessage, err := sMime.signMessage(message)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error creating singed message: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *singedMessage == message {
|
||||||
|
t.Errorf("Sign() did not work")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPrepareMessage tests the createMessage method
|
||||||
|
func TestPrepareMessage(t *testing.T) {
|
||||||
|
keyPair, err := getDummyCertificate()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error getting dummy certificate: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sMime, err := newSMime(keyPair)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error creating new SMime from keyPair: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
encoding := EncodingB64
|
||||||
|
contentType := TypeTextPlain
|
||||||
|
charset := CharsetUTF8
|
||||||
|
body := []byte("This is the body!")
|
||||||
|
result := sMime.prepareMessage(encoding, contentType, charset, body)
|
||||||
|
|
||||||
|
if !strings.Contains(result, encoding.String()) {
|
||||||
|
t.Errorf("createMessage() did not return the correct encoding")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result, contentType.String()) {
|
||||||
|
t.Errorf("createMessage() did not return the correct contentType")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result, string(body)) {
|
||||||
|
t.Errorf("createMessage() did not return the correct body")
|
||||||
|
}
|
||||||
|
if result != fmt.Sprintf("Content-Transfer-Encoding: %s\r\nContent-Type: %s; charset=%s\r\n\r\n%s", encoding, contentType, charset, string(body)) {
|
||||||
|
t.Errorf("createMessage() did not sucessfully create the message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEncodeToPEM tests the encodeToPEM method
|
||||||
|
func TestEncodeToPEM(t *testing.T) {
|
||||||
|
message := []byte("This is a test message")
|
||||||
|
|
||||||
|
pemMessage, err := encodeToPEM(message)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error encoding message: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
base64Encoded := base64.StdEncoding.EncodeToString(message)
|
||||||
|
if *pemMessage != base64Encoded {
|
||||||
|
t.Errorf("encodeToPEM() did not work")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBytesFromLines tests the bytesFromLines method
|
||||||
|
func TestBytesFromLines(t *testing.T) {
|
||||||
|
ls := lines{
|
||||||
|
{line: []byte("Hello"), endOfLine: []byte("\n")},
|
||||||
|
{line: []byte("World"), endOfLine: []byte("\n")},
|
||||||
|
}
|
||||||
|
expected := []byte("Hello\nWorld\n")
|
||||||
|
|
||||||
|
result := ls.bytesFromLines([]byte("\n"))
|
||||||
|
if !bytes.Equal(result, expected) {
|
||||||
|
t.Errorf("Expected %s, but got %s", expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FuzzBytesFromLines tests the bytesFromLines method with fuzzing
|
||||||
|
func FuzzBytesFromLines(f *testing.F) {
|
||||||
|
f.Add([]byte("Hello"), []byte("\n"))
|
||||||
|
f.Fuzz(func(t *testing.T, lineData, sep []byte) {
|
||||||
|
ls := lines{
|
||||||
|
{line: lineData, endOfLine: sep},
|
||||||
|
}
|
||||||
|
_ = ls.bytesFromLines(sep)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestParseLines tests the parseLines method
|
||||||
|
func TestParseLines(t *testing.T) {
|
||||||
|
input := []byte("Hello\r\nWorld\nHello\rWorld")
|
||||||
|
expected := lines{
|
||||||
|
{line: []byte("Hello"), endOfLine: []byte("\r\n")},
|
||||||
|
{line: []byte("World"), endOfLine: []byte("\n")},
|
||||||
|
{line: []byte("Hello"), endOfLine: []byte("\r")},
|
||||||
|
{line: []byte("World"), endOfLine: []byte("")},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := parseLines(input)
|
||||||
|
if len(result) != len(expected) {
|
||||||
|
t.Errorf("Expected %d lines, but got %d", len(expected), len(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range result {
|
||||||
|
if !bytes.Equal(result[i].line, expected[i].line) || !bytes.Equal(result[i].endOfLine, expected[i].endOfLine) {
|
||||||
|
t.Errorf("Line %d mismatch. Expected line: %s, endOfLine: %s, got line: %s, endOfLine: %s",
|
||||||
|
i, expected[i].line, expected[i].endOfLine, result[i].line, result[i].endOfLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FuzzParseLines tests the parseLines method with fuzzing
|
||||||
|
func FuzzParseLines(f *testing.F) {
|
||||||
|
f.Add([]byte("Hello\nWorld\r\nAnother\rLine"))
|
||||||
|
f.Fuzz(func(t *testing.T, input []byte) {
|
||||||
|
_ = parseLines(input)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSplitLine tests the splitLine method
|
||||||
|
func TestSplitLine(t *testing.T) {
|
||||||
|
ls := lines{
|
||||||
|
{line: []byte("Hello\r\nWorld\r\nAnotherLine"), endOfLine: []byte("")},
|
||||||
|
}
|
||||||
|
expected := lines{
|
||||||
|
{line: []byte("Hello"), endOfLine: []byte("\r\n")},
|
||||||
|
{line: []byte("World"), endOfLine: []byte("\r\n")},
|
||||||
|
{line: []byte("AnotherLine"), endOfLine: []byte("")},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := ls.splitLine([]byte("\r\n"))
|
||||||
|
if len(result) != len(expected) {
|
||||||
|
t.Errorf("Expected %d lines, but got %d", len(expected), len(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range result {
|
||||||
|
if !bytes.Equal(result[i].line, expected[i].line) || !bytes.Equal(result[i].endOfLine, expected[i].endOfLine) {
|
||||||
|
t.Errorf("Line %d mismatch. Expected line: %s, endOfLine: %s, got line: %s, endOfLine: %s",
|
||||||
|
i, expected[i].line, expected[i].endOfLine, result[i].line, result[i].endOfLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FuzzSplitLine tests the parseLsplitLineines method with fuzzing
|
||||||
|
func FuzzSplitLine(f *testing.F) {
|
||||||
|
f.Add([]byte("Hello\r\nWorld"), []byte("\r\n"))
|
||||||
|
f.Fuzz(func(t *testing.T, input, sep []byte) {
|
||||||
|
ls := lines{
|
||||||
|
{line: input, endOfLine: []byte("")},
|
||||||
|
}
|
||||||
|
_ = ls.splitLine(sep)
|
||||||
|
})
|
||||||
|
}
|
13
smtp/auth.go
13
smtp/auth.go
|
@ -13,6 +13,19 @@
|
||||||
|
|
||||||
package smtp
|
package smtp
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrUnencrypted is an error indicating that the connection is not encrypted.
|
||||||
|
ErrUnencrypted = errors.New("unencrypted connection")
|
||||||
|
// ErrUnexpectedServerChallange is an error indicating that the server issued an unexpected challenge.
|
||||||
|
ErrUnexpectedServerChallange = errors.New("unexpected server challenge")
|
||||||
|
// ErrUnexpectedServerResponse is an error indicating that the server issued an unexpected response.
|
||||||
|
ErrUnexpectedServerResponse = errors.New("unexpected server response")
|
||||||
|
// ErrWrongHostname is an error indicating that the provided hostname does not match the expected value.
|
||||||
|
ErrWrongHostname = errors.New("wrong host name")
|
||||||
|
)
|
||||||
|
|
||||||
// Auth is implemented by an SMTP authentication mechanism.
|
// Auth is implemented by an SMTP authentication mechanism.
|
||||||
type Auth interface {
|
type Auth interface {
|
||||||
// Start begins an authentication with a server.
|
// Start begins an authentication with a server.
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
// SPDX-FileCopyrightText: Copyright (c) 2022-2023 The go-mail Authors
|
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package smtp
|
package smtp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -13,53 +12,35 @@ import (
|
||||||
type loginAuth struct {
|
type loginAuth struct {
|
||||||
username, password string
|
username, password string
|
||||||
host string
|
host string
|
||||||
|
respStep uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
// LoginXUsernameChallenge represents the Username Challenge response sent by the SMTP server per the AUTH LOGIN
|
|
||||||
// extension.
|
|
||||||
//
|
|
||||||
// See: https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-xlogin/.
|
|
||||||
LoginXUsernameChallenge = "Username:"
|
|
||||||
LoginXUsernameLowerChallenge = "username:"
|
|
||||||
|
|
||||||
// LoginXPasswordChallenge represents the Password Challenge response sent by the SMTP server per the AUTH LOGIN
|
|
||||||
// extension.
|
|
||||||
//
|
|
||||||
// See: https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-xlogin/.
|
|
||||||
LoginXPasswordChallenge = "Password:"
|
|
||||||
LoginXPasswordLowerChallenge = "password:"
|
|
||||||
|
|
||||||
// LoginXDraftUsernameChallenge represents the Username Challenge response sent by the SMTP server per the IETF
|
|
||||||
// draft AUTH LOGIN extension. It should be noted this extension is an expired draft which was never formally
|
|
||||||
// published and was deprecated in favor of the AUTH PLAIN extension.
|
|
||||||
//
|
|
||||||
// See: https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00.
|
|
||||||
LoginXDraftUsernameChallenge = "User Name\x00"
|
|
||||||
|
|
||||||
// LoginXDraftPasswordChallenge represents the Password Challenge response sent by the SMTP server per the IETF
|
|
||||||
// draft AUTH LOGIN extension. It should be noted this extension is an expired draft which was never formally
|
|
||||||
// published and was deprecated in favor of the AUTH PLAIN extension.
|
|
||||||
//
|
|
||||||
// See: https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00.
|
|
||||||
LoginXDraftPasswordChallenge = "Password\x00"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LoginAuth returns an [Auth] that implements the LOGIN authentication
|
// LoginAuth returns an [Auth] that implements the LOGIN authentication
|
||||||
// mechanism as it is used by MS Outlook. The Auth works similar to PLAIN
|
// mechanism as it is used by MS Outlook. The Auth works similar to PLAIN
|
||||||
// but instead of sending all in one response, the login is handled within
|
// but instead of sending all in one response, the login is handled within
|
||||||
// 3 steps:
|
// 3 steps:
|
||||||
// - Sending AUTH LOGIN (server responds with "Username:")
|
// - Sending AUTH LOGIN (server might responds with "Username:")
|
||||||
// - Sending the username (server responds with "Password:")
|
// - Sending the username (server might responds with "Password:")
|
||||||
// - Sending the password (server authenticates)
|
// - Sending the password (server authenticates)
|
||||||
|
// This is the common approach as specified by Microsoft in their MS-XLOGIN spec.
|
||||||
|
// See: https://msopenspecs.azureedge.net/files/MS-XLOGIN/%5bMS-XLOGIN%5d.pdf
|
||||||
|
// Yet, there is also an old IETF draft for SMTP AUTH LOGIN that states for clients:
|
||||||
|
// "The contents of both challenges SHOULD be ignored.".
|
||||||
|
// See: https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
|
||||||
|
// Since there is no official standard RFC and we've seen different implementations
|
||||||
|
// of this mechanism (sending "Username:", "Username", "username", "User name", etc.)
|
||||||
|
// we follow the IETF-Draft and ignore any server challange to allow compatiblity
|
||||||
|
// with most mail servers/providers.
|
||||||
//
|
//
|
||||||
// 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) Auth {
|
func LoginAuth(username, password, host string) Auth {
|
||||||
return &loginAuth{username, password, host}
|
return &loginAuth{username, password, host, 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start begins the SMTP authentication process by validating server's TLS status and hostname.
|
||||||
|
// Returns "LOGIN" on success.
|
||||||
func (a *loginAuth) Start(server *ServerInfo) (string, []byte, error) {
|
func (a *loginAuth) Start(server *ServerInfo) (string, []byte, error) {
|
||||||
// Must have TLS, or else localhost server.
|
// Must have TLS, or else localhost server.
|
||||||
// Note: If TLS is not true, then we can't trust ANYTHING in ServerInfo.
|
// Note: If TLS is not true, then we can't trust ANYTHING in ServerInfo.
|
||||||
|
@ -67,23 +48,28 @@ func (a *loginAuth) Start(server *ServerInfo) (string, []byte, error) {
|
||||||
// That might just be the attacker saying
|
// That might just be the attacker saying
|
||||||
// "it's ok, you can trust me with your password."
|
// "it's ok, you can trust me with your password."
|
||||||
if !server.TLS && !isLocalhost(server.Name) {
|
if !server.TLS && !isLocalhost(server.Name) {
|
||||||
return "", nil, errors.New("unencrypted connection")
|
return "", nil, ErrUnencrypted
|
||||||
}
|
}
|
||||||
if server.Name != a.host {
|
if server.Name != a.host {
|
||||||
return "", nil, errors.New("wrong host name")
|
return "", nil, ErrWrongHostname
|
||||||
}
|
}
|
||||||
|
a.respStep = 0
|
||||||
return "LOGIN", nil, nil
|
return "LOGIN", nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Next processes responses from the server during the SMTP authentication exchange, sending the
|
||||||
|
// username and password.
|
||||||
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||||
if more {
|
if more {
|
||||||
switch string(fromServer) {
|
switch a.respStep {
|
||||||
case LoginXUsernameChallenge, LoginXUsernameLowerChallenge, LoginXDraftUsernameChallenge:
|
case 0:
|
||||||
|
a.respStep++
|
||||||
return []byte(a.username), nil
|
return []byte(a.username), nil
|
||||||
case LoginXPasswordChallenge, LoginXPasswordLowerChallenge, LoginXDraftPasswordChallenge:
|
case 1:
|
||||||
|
a.respStep++
|
||||||
return []byte(a.password), nil
|
return []byte(a.password), nil
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unexpected server response: %s", string(fromServer))
|
return nil, fmt.Errorf("%w: %s", ErrUnexpectedServerResponse, string(fromServer))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|
|
@ -13,10 +13,6 @@
|
||||||
|
|
||||||
package smtp
|
package smtp
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// plainAuth is the type that satisfies the Auth interface for the "SMTP PLAIN" auth
|
// plainAuth is the type that satisfies the Auth interface for the "SMTP PLAIN" auth
|
||||||
type plainAuth struct {
|
type plainAuth struct {
|
||||||
identity, username, password string
|
identity, username, password string
|
||||||
|
@ -42,10 +38,10 @@ func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
|
||||||
// That might just be the attacker saying
|
// That might just be the attacker saying
|
||||||
// "it's ok, you can trust me with your password."
|
// "it's ok, you can trust me with your password."
|
||||||
if !server.TLS && !isLocalhost(server.Name) {
|
if !server.TLS && !isLocalhost(server.Name) {
|
||||||
return "", nil, errors.New("unencrypted connection")
|
return "", nil, ErrUnencrypted
|
||||||
}
|
}
|
||||||
if server.Name != a.host {
|
if server.Name != a.host {
|
||||||
return "", nil, errors.New("wrong host name")
|
return "", nil, ErrWrongHostname
|
||||||
}
|
}
|
||||||
resp := []byte(a.identity + "\x00" + a.username + "\x00" + a.password)
|
resp := []byte(a.identity + "\x00" + a.username + "\x00" + a.password)
|
||||||
return "PLAIN", resp, nil
|
return "PLAIN", resp, nil
|
||||||
|
@ -54,7 +50,7 @@ func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
|
||||||
func (a *plainAuth) Next(_ []byte, more bool) ([]byte, error) {
|
func (a *plainAuth) Next(_ []byte, more bool) ([]byte, error) {
|
||||||
if more {
|
if more {
|
||||||
// We've already sent everything.
|
// We've already sent everything.
|
||||||
return nil, errors.New("unexpected server challenge")
|
return nil, ErrUnexpectedServerChallange
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
317
smtp/auth_scram.go
Normal file
317
smtp/auth_scram.go
Normal file
|
@ -0,0 +1,317 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright (c) 2024 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package smtp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"hash"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/pbkdf2"
|
||||||
|
"golang.org/x/text/secure/precis"
|
||||||
|
)
|
||||||
|
|
||||||
|
// scramAuth represents a SCRAM (Salted Challenge Response Authentication Mechanism) client and
|
||||||
|
// satisfies the smtp.Auth interface.
|
||||||
|
type scramAuth struct {
|
||||||
|
username, password, algorithm string
|
||||||
|
firstBareMsg, nonce, saltedPwd, authMessage []byte
|
||||||
|
iterations int
|
||||||
|
h func() hash.Hash
|
||||||
|
isPlus bool
|
||||||
|
tlsConnState *tls.ConnectionState
|
||||||
|
bindData []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScramSHA1Auth creates and returns a new SCRAM-SHA-1 authentication mechanism with the given
|
||||||
|
// username and password.
|
||||||
|
func ScramSHA1Auth(username, password string) Auth {
|
||||||
|
return &scramAuth{
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
algorithm: "SCRAM-SHA-1",
|
||||||
|
h: sha1.New,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScramSHA256Auth creates and returns a new SCRAM-SHA-256 authentication mechanism with the given
|
||||||
|
// username and password.
|
||||||
|
func ScramSHA256Auth(username, password string) Auth {
|
||||||
|
return &scramAuth{
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
algorithm: "SCRAM-SHA-256",
|
||||||
|
h: sha256.New,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScramSHA1PlusAuth returns an Auth instance configured for SCRAM-SHA-1-PLUS authentication with
|
||||||
|
// the provided username, password, and TLS connection state.
|
||||||
|
func ScramSHA1PlusAuth(username, password string, tlsConnState *tls.ConnectionState) Auth {
|
||||||
|
return &scramAuth{
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
algorithm: "SCRAM-SHA-1-PLUS",
|
||||||
|
h: sha1.New,
|
||||||
|
isPlus: true,
|
||||||
|
tlsConnState: tlsConnState,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScramSHA256PlusAuth returns an Auth instance configured for SCRAM-SHA-256-PLUS authentication with
|
||||||
|
// the provided username, password, and TLS connection state.
|
||||||
|
func ScramSHA256PlusAuth(username, password string, tlsConnState *tls.ConnectionState) Auth {
|
||||||
|
return &scramAuth{
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
algorithm: "SCRAM-SHA-256-PLUS",
|
||||||
|
h: sha256.New,
|
||||||
|
isPlus: true,
|
||||||
|
tlsConnState: tlsConnState,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start initializes the SCRAM authentication process and returns the selected algorithm, nil data, and no error.
|
||||||
|
func (a *scramAuth) Start(_ *ServerInfo) (string, []byte, error) {
|
||||||
|
return a.algorithm, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next processes the server's challenge and returns the client's response for SCRAM authentication.
|
||||||
|
func (a *scramAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||||
|
if more {
|
||||||
|
if len(fromServer) == 0 {
|
||||||
|
a.reset()
|
||||||
|
return a.initialClientMessage()
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case bytes.HasPrefix(fromServer, []byte("r=")):
|
||||||
|
resp, err := a.handleServerFirstResponse(fromServer)
|
||||||
|
if err != nil {
|
||||||
|
a.reset()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
case bytes.HasPrefix(fromServer, []byte("v=")):
|
||||||
|
resp, err := a.handleServerValidationMessage(fromServer)
|
||||||
|
if err != nil {
|
||||||
|
a.reset()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
default:
|
||||||
|
a.reset()
|
||||||
|
return nil, fmt.Errorf("%w: %s", ErrUnexpectedServerResponse, string(fromServer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset clears all authentication-related properties in the scramAuth instance, effectively resetting its state.
|
||||||
|
func (a *scramAuth) reset() {
|
||||||
|
a.nonce = nil
|
||||||
|
a.firstBareMsg = nil
|
||||||
|
a.saltedPwd = nil
|
||||||
|
a.authMessage = nil
|
||||||
|
a.iterations = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialClientMessage generates the initial message for SCRAM authentication, including a nonce and
|
||||||
|
// optional channel binding.
|
||||||
|
func (a *scramAuth) initialClientMessage() ([]byte, error) {
|
||||||
|
username, err := a.normalizeUsername()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("username normalization failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nonceBuffer := make([]byte, 24)
|
||||||
|
if _, err := io.ReadFull(rand.Reader, nonceBuffer); err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to generate client secret: %w", err)
|
||||||
|
}
|
||||||
|
a.nonce = make([]byte, base64.StdEncoding.EncodedLen(len(nonceBuffer)))
|
||||||
|
base64.StdEncoding.Encode(a.nonce, nonceBuffer)
|
||||||
|
|
||||||
|
a.firstBareMsg = []byte("n=" + username + ",r=" + string(a.nonce))
|
||||||
|
returnBytes := []byte("n,," + string(a.firstBareMsg))
|
||||||
|
|
||||||
|
// SCRAM-SHA-X-PLUS auth requires channel binding
|
||||||
|
if a.isPlus {
|
||||||
|
if a.tlsConnState == nil {
|
||||||
|
return nil, errors.New("tls connection state is required for SCRAM-SHA-X-PLUS")
|
||||||
|
}
|
||||||
|
bindType := "tls-unique"
|
||||||
|
connState := a.tlsConnState
|
||||||
|
bindData := connState.TLSUnique
|
||||||
|
|
||||||
|
// crypto/tl: no tls-unique channel binding value for this tls connection, possibly due to missing
|
||||||
|
// extended master key support and/or resumed connection
|
||||||
|
// RFC9266:122 tls-unique not defined for tls 1.3 and later
|
||||||
|
if bindData == nil || connState.Version >= tls.VersionTLS13 {
|
||||||
|
bindType = "tls-exporter"
|
||||||
|
bindData, err = connState.ExportKeyingMaterial("EXPORTER-Channel-Binding", []byte{}, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to export keying material: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bindData = []byte("p=" + bindType + ",," + string(bindData))
|
||||||
|
a.bindData = make([]byte, base64.StdEncoding.EncodedLen(len(bindData)))
|
||||||
|
base64.StdEncoding.Encode(a.bindData, bindData)
|
||||||
|
returnBytes = []byte("p=" + bindType + ",," + string(a.firstBareMsg))
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleServerFirstResponse processes the first response from the server in SCRAM authentication.
|
||||||
|
func (a *scramAuth) handleServerFirstResponse(fromServer []byte) ([]byte, error) {
|
||||||
|
parts := bytes.Split(fromServer, []byte(","))
|
||||||
|
if len(parts) < 3 {
|
||||||
|
return nil, errors.New("not enough fields in the first server response")
|
||||||
|
}
|
||||||
|
if !bytes.HasPrefix(parts[0], []byte("r=")) {
|
||||||
|
return nil, errors.New("first part of the server response does not start with r=")
|
||||||
|
}
|
||||||
|
if !bytes.HasPrefix(parts[1], []byte("s=")) {
|
||||||
|
return nil, errors.New("second part of the server response does not start with s=")
|
||||||
|
}
|
||||||
|
if !bytes.HasPrefix(parts[2], []byte("i=")) {
|
||||||
|
return nil, errors.New("third part of the server response does not start with i=")
|
||||||
|
}
|
||||||
|
|
||||||
|
combinedNonce := parts[0][2:]
|
||||||
|
if len(a.nonce) == 0 || !bytes.HasPrefix(combinedNonce, a.nonce) {
|
||||||
|
return nil, errors.New("server nonce does not start with our nonce")
|
||||||
|
}
|
||||||
|
a.nonce = combinedNonce
|
||||||
|
|
||||||
|
encodedSalt := parts[1][2:]
|
||||||
|
salt := make([]byte, base64.StdEncoding.DecodedLen(len(encodedSalt)))
|
||||||
|
n, err := base64.StdEncoding.Decode(salt, encodedSalt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid encoded salt: %w", err)
|
||||||
|
}
|
||||||
|
salt = salt[:n]
|
||||||
|
|
||||||
|
iterations, err := strconv.Atoi(string(parts[2][2:]))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid iterations: %w", err)
|
||||||
|
}
|
||||||
|
a.iterations = iterations
|
||||||
|
|
||||||
|
password, err := a.normalizeString(a.password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to normalize password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.saltedPwd = pbkdf2.Key([]byte(password), salt, a.iterations, a.h().Size(), a.h)
|
||||||
|
|
||||||
|
msgWithoutProof := []byte("c=biws,r=" + string(a.nonce))
|
||||||
|
|
||||||
|
// A PLUS authentication requires the channel binding data
|
||||||
|
if a.isPlus {
|
||||||
|
msgWithoutProof = []byte("c=" + string(a.bindData) + ",r=" + string(a.nonce))
|
||||||
|
}
|
||||||
|
|
||||||
|
a.authMessage = []byte(string(a.firstBareMsg) + "," + string(fromServer) + "," + string(msgWithoutProof))
|
||||||
|
clientProof := a.computeClientProof()
|
||||||
|
|
||||||
|
return []byte(string(msgWithoutProof) + ",p=" + string(clientProof)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleServerValidationMessage verifies the server's signature during the SCRAM authentication process.
|
||||||
|
func (a *scramAuth) handleServerValidationMessage(fromServer []byte) ([]byte, error) {
|
||||||
|
serverSignature := fromServer[2:]
|
||||||
|
computedServerSignature := a.computeServerSignature()
|
||||||
|
|
||||||
|
if !hmac.Equal(serverSignature, computedServerSignature) {
|
||||||
|
return nil, errors.New("invalid server signature")
|
||||||
|
}
|
||||||
|
return []byte(""), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// computeHMAC generates a Hash-based Message Authentication Code (HMAC) using the specified key and message.
|
||||||
|
func (a *scramAuth) computeHMAC(key, msg []byte) []byte {
|
||||||
|
mac := hmac.New(a.h, key)
|
||||||
|
mac.Write(msg)
|
||||||
|
return mac.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// computeHash generates a hash of the given key using the configured hashing algorithm.
|
||||||
|
func (a *scramAuth) computeHash(key []byte) []byte {
|
||||||
|
hasher := a.h()
|
||||||
|
hasher.Write(key)
|
||||||
|
return hasher.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// computeClientProof generates the client proof as part of the SCRAM authentication process.
|
||||||
|
func (a *scramAuth) computeClientProof() []byte {
|
||||||
|
clientKey := a.computeHMAC(a.saltedPwd, []byte("Client Key"))
|
||||||
|
storedKey := a.computeHash(clientKey)
|
||||||
|
clientSignature := a.computeHMAC(storedKey[:], a.authMessage)
|
||||||
|
clientProof := make([]byte, len(clientSignature))
|
||||||
|
for i := 0; i < len(clientSignature); i++ {
|
||||||
|
clientProof[i] = clientKey[i] ^ clientSignature[i]
|
||||||
|
}
|
||||||
|
buf := make([]byte, base64.StdEncoding.EncodedLen(len(clientProof)))
|
||||||
|
base64.StdEncoding.Encode(buf, clientProof)
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// computeServerSignature returns the computed base64-encoded server signature in the SCRAM
|
||||||
|
// authentication process.
|
||||||
|
func (a *scramAuth) computeServerSignature() []byte {
|
||||||
|
serverKey := a.computeHMAC(a.saltedPwd, []byte("Server Key"))
|
||||||
|
serverSignature := a.computeHMAC(serverKey, a.authMessage)
|
||||||
|
buf := make([]byte, base64.StdEncoding.EncodedLen(len(serverSignature)))
|
||||||
|
base64.StdEncoding.Encode(buf, serverSignature)
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeUsername replaces special characters in the username for SCRAM authentication
|
||||||
|
// and prepares it using the SASLprep profile as per RFC 8265, returning the normalized
|
||||||
|
// username or an error.
|
||||||
|
func (a *scramAuth) normalizeUsername() (string, error) {
|
||||||
|
// RFC 5802 section 5.1: the characters ',' or '=' in usernames are
|
||||||
|
// sent as '=2C' and '=3D' respectively.
|
||||||
|
replacer := strings.NewReplacer("=", "=3D", ",", "=2C")
|
||||||
|
username := replacer.Replace(a.username)
|
||||||
|
// RFC 5802 section 5.1: before sending the username to the server,
|
||||||
|
// the client SHOULD prepare the username using the "SASLprep"
|
||||||
|
// profile [RFC4013] of the "stringprep" algorithm [RFC3454]
|
||||||
|
// treating it as a query string (i.e., unassigned Unicode code
|
||||||
|
// points are allowed). If the preparation of the username fails or
|
||||||
|
// results in an empty string, the client SHOULD abort the
|
||||||
|
// authentication exchange.
|
||||||
|
//
|
||||||
|
// Since RFC 8265 obsoletes RFC 4013 we use it instead.
|
||||||
|
username, err := a.normalizeString(username)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("unable to normalize username: %w", err)
|
||||||
|
}
|
||||||
|
return username, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeString normalizes the input string according to the OpaqueString profile of the
|
||||||
|
// precis framework. It returns the normalized string or an error if normalization fails or
|
||||||
|
// results in an empty string.
|
||||||
|
func (a *scramAuth) normalizeString(s string) (string, error) {
|
||||||
|
s, err := precis.OpaqueString.String(s)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failled to normalize string: %w", err)
|
||||||
|
}
|
||||||
|
if s == "" {
|
||||||
|
return "", errors.New("normalized string is empty")
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
43
smtp/smtp.go
43
smtp/smtp.go
|
@ -36,6 +36,16 @@ import (
|
||||||
"github.com/wneessen/go-mail/log"
|
"github.com/wneessen/go-mail/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
|
||||||
|
// ErrNonTLSConnection is returned when an attempt is made to retrieve TLS state on a non-TLS connection.
|
||||||
|
ErrNonTLSConnection = errors.New("connection is not using TLS")
|
||||||
|
|
||||||
|
// ErrNoConnection is returned when attempting to perform an operation that requires an established
|
||||||
|
// connection but none exists.
|
||||||
|
ErrNoConnection = errors.New("connection is not established")
|
||||||
|
)
|
||||||
|
|
||||||
// A Client represents a client connection to an SMTP server.
|
// A Client represents a client connection to an SMTP server.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
// Text is the textproto.Conn used by the Client. It is exported to allow for clients to add extensions.
|
// Text is the textproto.Conn used by the Client. It is exported to allow for clients to add extensions.
|
||||||
|
@ -65,6 +75,9 @@ type Client struct {
|
||||||
// helloError is the error from the hello
|
// helloError is the error from the hello
|
||||||
helloError error
|
helloError error
|
||||||
|
|
||||||
|
// isConnected indicates if the Client has an active connection
|
||||||
|
isConnected bool
|
||||||
|
|
||||||
// localName is the name to use in HELO/EHLO
|
// localName is the name to use in HELO/EHLO
|
||||||
localName string // the name to use in HELO/EHLO
|
localName string // the name to use in HELO/EHLO
|
||||||
|
|
||||||
|
@ -111,6 +124,7 @@ func NewClient(conn net.Conn, host string) (*Client, error) {
|
||||||
}
|
}
|
||||||
c := &Client{Text: text, conn: conn, serverName: host, localName: "localhost"}
|
c := &Client{Text: text, conn: conn, serverName: host, localName: "localhost"}
|
||||||
_, c.tls = conn.(*tls.Conn)
|
_, c.tls = conn.(*tls.Conn)
|
||||||
|
c.isConnected = true
|
||||||
|
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
@ -119,6 +133,7 @@ func NewClient(conn net.Conn, host string) (*Client, error) {
|
||||||
func (c *Client) Close() error {
|
func (c *Client) Close() error {
|
||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
err := c.Text.Close()
|
err := c.Text.Close()
|
||||||
|
c.isConnected = false
|
||||||
c.mutex.Unlock()
|
c.mutex.Unlock()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -514,6 +529,7 @@ func (c *Client) Quit() error {
|
||||||
}
|
}
|
||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
err = c.Text.Close()
|
err = c.Text.Close()
|
||||||
|
c.isConnected = false
|
||||||
c.mutex.Unlock()
|
c.mutex.Unlock()
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
@ -553,18 +569,41 @@ func (c *Client) SetDSNRcptNotifyOption(d string) {
|
||||||
// HasConnection checks if the client has an active connection.
|
// HasConnection checks if the client has an active connection.
|
||||||
// Returns true if the `conn` field is not nil, indicating an active connection.
|
// Returns true if the `conn` field is not nil, indicating an active connection.
|
||||||
func (c *Client) HasConnection() bool {
|
func (c *Client) HasConnection() bool {
|
||||||
return c.conn != nil
|
c.mutex.RLock()
|
||||||
|
isConn := c.isConnected
|
||||||
|
c.mutex.RUnlock()
|
||||||
|
return isConn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateDeadline sets a new deadline on the SMTP connection with the specified timeout duration.
|
||||||
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()
|
||||||
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)
|
||||||
}
|
}
|
||||||
c.mutex.Unlock()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTLSConnectionState retrieves the TLS connection state of the client's current connection.
|
||||||
|
// Returns an error if the connection is not using TLS or if the connection is not established.
|
||||||
|
func (c *Client) GetTLSConnectionState() (*tls.ConnectionState, error) {
|
||||||
|
c.mutex.RLock()
|
||||||
|
defer c.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !c.isConnected {
|
||||||
|
return nil, ErrNoConnection
|
||||||
|
}
|
||||||
|
if !c.tls {
|
||||||
|
return nil, ErrNonTLSConnection
|
||||||
|
}
|
||||||
|
if conn, ok := c.conn.(*tls.Conn); ok {
|
||||||
|
cstate := conn.ConnectionState()
|
||||||
|
return &cstate, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("unable to retrieve TLS connection state")
|
||||||
|
}
|
||||||
|
|
||||||
// debugLog checks if the debug flag is set and if so logs the provided message to
|
// debugLog checks if the debug flag is set and if so logs the provided message to
|
||||||
// the log.Logger interface
|
// the log.Logger interface
|
||||||
func (c *Client) debugLog(d log.Direction, f string, a ...interface{}) {
|
func (c *Client) debugLog(d log.Direction, f string, a ...interface{}) {
|
||||||
|
|
File diff suppressed because it is too large
Load diff
17
tls.go
17
tls.go
|
@ -4,25 +4,32 @@
|
||||||
|
|
||||||
package mail
|
package mail
|
||||||
|
|
||||||
// TLSPolicy type describes a int alias for the different TLS policies we allow
|
// TLSPolicy is a type wrapper for an int type and describes the different TLS policies we allow.
|
||||||
type TLSPolicy int
|
type TLSPolicy int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// TLSMandatory requires that the connection to the server is
|
// TLSMandatory requires that the connection to the server is
|
||||||
// encrypting using STARTTLS. If the server does not support STARTTLS
|
// encrypting using STARTTLS. If the server does not support STARTTLS
|
||||||
// the connection will be terminated with an error
|
// the connection will be terminated with an error.
|
||||||
TLSMandatory TLSPolicy = iota
|
TLSMandatory TLSPolicy = iota
|
||||||
|
|
||||||
// TLSOpportunistic tries to establish an encrypted connection via the
|
// TLSOpportunistic tries to establish an encrypted connection via the
|
||||||
// STARTTLS protocol. If the server does not support this, it will fall
|
// STARTTLS protocol. If the server does not support this, it will fall
|
||||||
// back to non-encrypted plaintext transmission
|
// back to non-encrypted plaintext transmission.
|
||||||
TLSOpportunistic
|
TLSOpportunistic
|
||||||
|
|
||||||
// NoTLS forces the transaction to be not encrypted
|
// NoTLS forces the transaction to be not encrypted.
|
||||||
NoTLS
|
NoTLS
|
||||||
)
|
)
|
||||||
|
|
||||||
// String is a standard method to convert a TLSPolicy into a printable format
|
// String satisfies the fmt.Stringer interface for the TLSPolicy type.
|
||||||
|
//
|
||||||
|
// This function returns a string representation of the TLSPolicy. It matches the policy
|
||||||
|
// value to predefined constants and returns the corresponding string. If the policy does
|
||||||
|
// not match any known values, it returns "UnknownPolicy".
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A string representing the TLSPolicy.
|
||||||
func (p TLSPolicy) String() string {
|
func (p TLSPolicy) String() string {
|
||||||
switch p {
|
switch p {
|
||||||
case TLSMandatory:
|
case TLSMandatory:
|
||||||
|
|
23
util_test.go
Normal file
23
util_test.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
certFilePath = "dummy-chain-cert.pem"
|
||||||
|
keyFilePath = "dummy-child-key.pem"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getDummyCertificate loads a certificate and a private key form local disk for testing purposes
|
||||||
|
func getDummyCertificate() (*tls.Certificate, error) {
|
||||||
|
keyPair, err := tls.LoadX509KeyPair(certFilePath, keyFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0])
|
||||||
|
|
||||||
|
return &keyPair, nil
|
||||||
|
}
|
Loading…
Reference in a new issue