Merge branch 'main' into main

This commit is contained in:
Michael Fuchs 2024-10-09 16:25:29 +02:00 committed by theexiile1305
commit 7f7bf80e39
No known key found for this signature in database
GPG key ID: A1BDDE98F2BF6E40
48 changed files with 6785 additions and 1245 deletions

View file

@ -14,12 +14,10 @@ freebsd_task:
image_family: freebsd-14-0
env:
TEST_ALLOW_SEND: 0
TEST_SKIP_SENDMAIL: 1
pkginstall_script:
- pkg update -f
- pkg install -y go
test_script:
- go test -v -race -cover -shuffle=on ./...
- go test -race -cover -shuffle=on ./...

View file

@ -10,7 +10,7 @@ on:
paths:
- '**.go'
- 'go.*'
- '.github/**'
- '.github/workflows/codecov.yml'
- 'codecov.yml'
pull_request:
branches:
@ -18,7 +18,7 @@ on:
paths:
- '**.go'
- 'go.*'
- '.github/**'
- '.github/workflows/codecov.yml'
- 'codecov.yml'
env:
TEST_HOST: ${{ secrets.TEST_HOST }}
@ -27,6 +27,10 @@ env:
TEST_SMTPAUTH_USER: ${{ secrets.TEST_USER }}
TEST_SMTPAUTH_PASS: ${{ secrets.TEST_PASS }}
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:
contents: read
@ -36,10 +40,10 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
go: ['1.20', '1.21', '1.22', '1.23']
go: ['1.23']
steps:
- name: Harden Runner
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with:
egress-policy: audit
@ -55,9 +59,9 @@ jobs:
sudo apt-get -y install sendmail; which sendmail
- name: Run Tests
run: |
go test -v -race --coverprofile=coverage.coverprofile --covermode=atomic ./...
go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./...
- name: Upload coverage to Codecov
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:
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos

View file

@ -45,7 +45,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with:
egress-policy: audit
@ -54,7 +54,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
uses: github/codeql-action/init@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
with:
languages: ${{ matrix.language }}
# 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).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
uses: github/codeql-action/autobuild@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -79,4 +79,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
uses: github/codeql-action/analyze@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12

View file

@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with:
egress-policy: audit

View file

@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with:
egress-policy: audit
@ -29,7 +29,7 @@ jobs:
go-version: '1.23'
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- name: golangci-lint
uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0
uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1
with:
# 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

View file

@ -14,8 +14,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with:
egress-policy: audit
- 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
View 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 ./...

View file

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with:
egress-policy: audit

View file

@ -35,7 +35,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with:
egress-policy: audit
@ -67,7 +67,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4.4.1
with:
name: SARIF file
path: results.sarif
@ -75,6 +75,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
uses: github/codeql-action/upload-sarif@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
with:
sarif_file: results.sarif

View file

@ -10,24 +10,25 @@ permissions:
on:
push:
branches:
- main # or the name of your main branch
- main
paths:
- '**.go'
- 'go.*'
- '.github/workflows/sonarqube.yml'
pull_request:
branches:
- main # or the name of your main branch
env:
TEST_HOST: ${{ secrets.TEST_HOST }}
TEST_FROM: ${{ secrets.TEST_USER }}
TEST_ALLOW_SEND: "1"
TEST_SMTPAUTH_USER: ${{ secrets.TEST_USER }}
TEST_SMTPAUTH_PASS: ${{ secrets.TEST_PASS }}
TEST_SMTPAUTH_TYPE: "LOGIN"
- main
paths:
- '**.go'
- 'go.*'
- '.github/workflows/sonarqube.yml'
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with:
egress-policy: audit
@ -38,13 +39,13 @@ jobs:
- name: Setup Go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
go-version: '1.23.x'
go-version: '1.23'
- name: Run unit Tests
run: |
go test -v -race --coverprofile=./cov.out ./...
go test -shuffle=on -race --coverprofile=./cov.out ./...
- uses: sonarsource/sonarqube-scan-action@0c0f3958d90fc466625f1d1af1f47bddd4cc6bd1 # master
- uses: sonarsource/sonarqube-scan-action@884b79409bbd464b2a59edc326a4b77dc56b2195 # master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}

View file

@ -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: ...

View file

@ -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>
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.
go-mail follows idiomatic Go style and best practice. It's only dependency is the Go Standard Library. It combines a lot
of functionality from the standard library to give easy and convenient access to mail and SMTP related tasks.
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.
Parts of this library (especially some parts of [msgwriter.go](msgwriter.go)) have been forked/ported from the
[go-mail/mail](https://github.com/go-mail/mail) respectively [go-gomail/gomail](https://github.com/go-gomail/gomail)
which both seems to not be maintained anymore.
In the early days, parts of this library (especially some parts of [msgwriter.go](msgwriter.go)) had been
forked/ported from [go-mail/mail](https://github.com/go-mail/mail) and respectively [go-gomail/gomail](https://github.com/go-gomail/gomail). Today
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
team.
The `smtp` package of go-mail has been forked from the original Go stdlib's `net/smtp` package and has then been extended
by the go-mail team to fit the packages needs (more SMTP Auth methods, logging, concurrency-safety, etc.).
## 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] Sane and secure defaults
* [X] Explicit SSL/TLS support
* [X] Implicit StartTLS support with different policies
* [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] 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 different encodings
* [X] Middleware support for 3rd-party libraries to alter mail messages
* [X] Support sending mails via a local sendmail command
* [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] 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] 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
@ -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.
## 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
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.
## 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
reviewing code, writing documenation or helping to translate the website):
* [Christian Vette](https://github.com/cvette)
* [Dhia Gharsallaoui](https://github.com/dhia-gharsallaoui)
* [inliquid](https://github.com/inliquid)
* [iwittkau](https://github.com/iwittkau)
* [James Elliott](https://github.com/james-d-elliott)
* [Maria Letta](https://github.com/MariaLetta) (designed the go-mail logo)
* [Nicola Murino](https://github.com/drakkan)
* [sters](https://github.com/sters)
<a href="https://github.com/wneessen/go-mail/graphs/contributors">
<img src="https://contrib.rocks/image?repo=wneessen/go-mail" />
</a>
A huge thank you also goes to [Maria Letta](https://github.com/MariaLetta) for designing our super cool go-mail logo!
## Sponsors
We sincerely thank our amazing sponsors for their generous support! Your contributions do not go unnoticed and helps
keeping up the project!
* [kolaente](https://github.com/kolaente)

9
REUSE.toml Normal file
View 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
View file

@ -6,41 +6,131 @@ package mail
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
// Supported SMTP AUTH types
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"
// 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"
// 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
// 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 = ""
// 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"
// SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism.
// https://developers.google.com/gmail/imap/xoauth2-protocol
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
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")
// 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")
// 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")
// 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")
// 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")
)

View file

@ -9,21 +9,39 @@ import (
"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"
// Base64LineBreaker is a io.WriteCloser that writes Base64 encoded data streams
// with line breaks at a given line length
// Base64LineBreaker handles base64 encoding with the insertion of new lines after a certain number
// 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 {
line [MaxBodyLength]byte
used int
out io.Writer
}
var newlineBytes = []byte(SingleNewLine)
// Write writes the data stream and inserts a SingleNewLine when the maximum
// line length is reached
// Write writes data to the Base64LineBreaker, ensuring lines do not exceed MaxBodyLength.
//
// This method writes the provided data to the Base64LineBreaker. It ensures that the written
// 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) {
if l.out == nil {
err = errors.New(ErrNoOutWriter)
@ -55,8 +73,14 @@ func (l *Base64LineBreaker) Write(data []byte) (numBytes int, err error) {
return l.Write(data[excess:])
}
// Close closes the Base64LineBreaker and writes any access data that is still
// unwritten in memory
// Close finalizes the Base64LineBreaker, writing any remaining buffered data and appending a newline.
//
// 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) {
if l.used > 0 {
_, err = l.out.Write(l.line[0:l.used])

1082
client.go

File diff suppressed because it is too large Load diff

View file

@ -7,111 +7,39 @@
package mail
import "strings"
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 {
if cerr := c.checkConn(); cerr != nil {
return &SendError{Reason: ErrConnCheck, errlist: []error{cerr}, isTemp: isTempError(cerr)}
if err := c.checkConn(); err != nil {
return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
}
var errs []*SendError
for _, message := range messages {
message.sendError = nil
if message.encoding == NoEncoding {
if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok {
sendErr := &SendError{Reason: ErrNoUnencoded, isTemp: false}
message.sendError = sendErr
errs = append(errs, sendErr)
continue
}
}
from, err := message.GetSender(false)
if err != nil {
sendErr := &SendError{Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err)}
message.sendError = sendErr
errs = append(errs, sendErr)
continue
}
rcpts, err := message.GetRecipients()
if err != nil {
sendErr := &SendError{Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err)}
message.sendError = sendErr
errs = append(errs, sendErr)
continue
}
for id, message := range messages {
if sendErr := c.sendSingleMsg(message); sendErr != nil {
messages[id].sendError = sendErr
if c.dsn {
if c.dsnmrtype != "" {
c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype))
var msgSendErr *SendError
if errors.As(sendErr, &msgSendErr) {
errs = append(errs, msgSendErr)
}
}
if err = c.smtpClient.Mail(from); err != nil {
sendErr := &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)}
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil {
sendErr.errlist = append(sendErr.errlist, resetSendErr)
}
message.sendError = sendErr
errs = append(errs, sendErr)
continue
}
failed := false
rcptSendErr := &SendError{}
rcptSendErr.errlist = make([]error, 0)
rcptSendErr.rcpt = make([]string, 0)
rcptNotifyOpt := strings.Join(c.dsnrntype, ",")
c.smtpClient.SetDSNRcptNotifyOption(rcptNotifyOpt)
for _, rcpt := range rcpts {
if err = c.smtpClient.Rcpt(rcpt); err != nil {
rcptSendErr.Reason = ErrSMTPRcptTo
rcptSendErr.errlist = append(rcptSendErr.errlist, err)
rcptSendErr.rcpt = append(rcptSendErr.rcpt, rcpt)
rcptSendErr.isTemp = isTempError(err)
failed = true
}
}
if failed {
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil {
rcptSendErr.errlist = append(rcptSendErr.errlist, err)
}
message.sendError = rcptSendErr
errs = append(errs, rcptSendErr)
continue
}
writer, err := c.smtpClient.Data()
if err != nil {
sendErr := &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)}
message.sendError = sendErr
errs = append(errs, sendErr)
continue
}
_, err = message.WriteTo(writer)
if err != nil {
sendErr := &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err)}
message.sendError = sendErr
errs = append(errs, sendErr)
continue
}
message.isDelivered = true
if err = writer.Close(); err != nil {
sendErr := &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)}
message.sendError = sendErr
errs = append(errs, sendErr)
continue
}
if err = c.Reset(); err != nil {
sendErr := &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err)}
message.sendError = sendErr
errs = append(errs, sendErr)
continue
}
if err = c.checkConn(); err != nil {
sendErr := &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
message.sendError = sendErr
errs = append(errs, sendErr)
continue
}
}
if len(errs) > 0 {

View file

@ -9,101 +9,38 @@ package mail
import (
"errors"
"strings"
)
// 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) {
if err := c.checkConn(); err != nil {
returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
return
}
for _, message := range messages {
message.sendError = nil
if message.encoding == NoEncoding {
if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok {
message.sendError = &SendError{Reason: ErrNoUnencoded, isTemp: false}
returnErr = errors.Join(returnErr, message.sendError)
continue
}
}
from, err := message.GetSender(false)
if err != nil {
message.sendError = &SendError{Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err)}
returnErr = errors.Join(returnErr, message.sendError)
continue
}
rcpts, err := message.GetRecipients()
if err != nil {
message.sendError = &SendError{Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err)}
returnErr = errors.Join(returnErr, message.sendError)
continue
}
if c.dsn {
if c.dsnmrtype != "" {
c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype))
}
}
if err = c.smtpClient.Mail(from); err != nil {
message.sendError = &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)}
returnErr = errors.Join(returnErr, message.sendError)
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil {
returnErr = errors.Join(returnErr, resetSendErr)
}
continue
}
failed := false
rcptSendErr := &SendError{}
rcptSendErr.errlist = make([]error, 0)
rcptSendErr.rcpt = make([]string, 0)
rcptNotifyOpt := strings.Join(c.dsnrntype, ",")
c.smtpClient.SetDSNRcptNotifyOption(rcptNotifyOpt)
for _, rcpt := range rcpts {
if err = c.smtpClient.Rcpt(rcpt); err != nil {
rcptSendErr.Reason = ErrSMTPRcptTo
rcptSendErr.errlist = append(rcptSendErr.errlist, err)
rcptSendErr.rcpt = append(rcptSendErr.rcpt, rcpt)
rcptSendErr.isTemp = isTempError(err)
failed = true
}
}
if failed {
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil {
returnErr = errors.Join(returnErr, resetSendErr)
}
message.sendError = rcptSendErr
returnErr = errors.Join(returnErr, message.sendError)
continue
}
writer, err := c.smtpClient.Data()
if err != nil {
message.sendError = &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)}
returnErr = errors.Join(returnErr, message.sendError)
continue
}
_, err = message.WriteTo(writer)
if err != nil {
message.sendError = &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err)}
returnErr = errors.Join(returnErr, message.sendError)
continue
}
message.isDelivered = true
var errs []error
defer func() {
returnErr = errors.Join(errs...)
}()
if err = writer.Close(); err != nil {
message.sendError = &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)}
returnErr = errors.Join(returnErr, message.sendError)
continue
}
if err = c.Reset(); err != nil {
message.sendError = &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err)}
returnErr = errors.Join(returnErr, message.sendError)
continue
}
if err = c.checkConn(); err != nil {
message.sendError = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
returnErr = errors.Join(returnErr, message.sendError)
for id, message := range messages {
if sendErr := c.sendSingleMsg(message); sendErr != nil {
messages[id].sendError = sendErr
errs = append(errs, sendErr)
}
}

File diff suppressed because it is too large Load diff

11
doc.go
View file

@ -2,8 +2,13 @@
//
// 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
// VERSION is used in the default user agent string
const VERSION = "0.4.4"
// VERSION indicates the current version of the package. It is also attached to the default user
// agent string.
const VERSION = "0.5.0"

205
eml.go
View file

@ -18,14 +18,35 @@ import (
"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) {
eb := bytes.NewBufferString(emlString)
return EMLToMsgFromReader(eb)
}
// EMLToMsgFromReader will parse a reader that holds EML content and returns a pre-filled
// Msg pointer
// EMLToMsgFromReader parses a reader that holds EML content and returns a pre-filled 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) {
msg := &Msg{
addrHeader: make(map[AddrHeader][]*netmail.Address),
@ -46,8 +67,19 @@ func EMLToMsgFromReader(reader io.Reader) (*Msg, error) {
return msg, nil
}
// EMLToMsgFromFile will open and parse a .eml file at a provided file path and returns a
// pre-filled Msg pointer
// EMLToMsgFromFile opens and parses a .eml file at a provided file path and returns a
// 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) {
msg := &Msg{
addrHeader: make(map[AddrHeader][]*netmail.Address),
@ -68,7 +100,19 @@ func EMLToMsgFromFile(filePath string) (*Msg, error) {
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 {
if err := parseEMLHeaders(&parsedMsg.Header, msg); err != nil {
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
}
// 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) {
fileHandle, err := os.Open(filePath)
if err != nil {
@ -91,7 +146,19 @@ func readEML(filePath string) (*netmail.Message, *bytes.Buffer, error) {
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) {
parsedMsg, err := netmail.ReadMessage(reader)
if err != nil {
@ -106,8 +173,18 @@ func readEMLFromReader(reader io.Reader) (*netmail.Message, *bytes.Buffer, error
return parsedMsg, &buf, nil
}
// parseEMLHeaders will check the EML headers for the most common headers and set the
// according settings in the Msg
// parseEMLHeaders parses the EML's headers and populates the Msg with relevant information.
//
// 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 {
commonHeaders := []Header{
HeaderContentType, HeaderImportance, HeaderInReplyTo, HeaderListUnsubscribe,
@ -175,7 +252,19 @@ func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error {
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 {
// Extract the transfer encoding of the body
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
}
// 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 {
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()) {
msg.SetEncoding(EncodingUSASCII)
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")
}
// 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 {
boundary, ok := params["boundary"]
if !ok {
@ -349,7 +465,15 @@ ReadNextPart:
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) {
if value := mailHeader.Get(HeaderContentTransferEnc.String()); value != "" {
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) {
if value := mailHeader.Get(HeaderContentType.String()); 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 {
part.SetEncoding(EncodingB64)
content, err := base64.StdEncoding.DecodeString(string(multiPartData))
@ -388,8 +531,17 @@ func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error {
return nil
}
// parseMultiPartHeader parses a multipart header and returns the value and optional parts as
// separate map
// parseMultiPartHeader parses a multipart header and returns the value and optional parts as a 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) {
optional = make(map[string]string)
headerSplit := strings.SplitN(multiPartHeader, ";", 2)
@ -404,7 +556,20 @@ func parseMultiPartHeader(multiPartHeader string) (header string, optional map[s
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 {
cdType, optional := parseMultiPartHeader(contentDisposition[0])
filename := "generic.attachment"

View file

@ -4,173 +4,221 @@
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
// 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
// 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
// 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
// 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
// List of supported encodings
const (
// EncodingB64 represents the Base64 encoding as specified in RFC 2045.
//
// https://datatracker.ietf.org/doc/html/rfc2045#section-6.8
EncodingB64 Encoding = "base64"
// 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"
// EncodingUSASCII represents encoding with only US-ASCII characters (aka 7Bit)
//
// https://datatracker.ietf.org/doc/html/rfc2045#section-2.7
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"
)
// List of common charsets
const (
// CharsetUTF7 represents the "UTF-7" charset
// CharsetUTF7 represents the "UTF-7" charset.
CharsetUTF7 Charset = "UTF-7"
// CharsetUTF8 represents the "UTF-8" charset
// CharsetUTF8 represents the "UTF-8" charset.
CharsetUTF8 Charset = "UTF-8"
// CharsetASCII represents the "US-ASCII" charset
// CharsetASCII represents the "US-ASCII" charset.
CharsetASCII Charset = "US-ASCII"
// CharsetISO88591 represents the "ISO-8859-1" charset
// CharsetISO88591 represents the "ISO-8859-1" charset.
CharsetISO88591 Charset = "ISO-8859-1"
// CharsetISO88592 represents the "ISO-8859-2" charset
// CharsetISO88592 represents the "ISO-8859-2" charset.
CharsetISO88592 Charset = "ISO-8859-2"
// CharsetISO88593 represents the "ISO-8859-3" charset
// CharsetISO88593 represents the "ISO-8859-3" charset.
CharsetISO88593 Charset = "ISO-8859-3"
// CharsetISO88594 represents the "ISO-8859-4" charset
// CharsetISO88594 represents the "ISO-8859-4" charset.
CharsetISO88594 Charset = "ISO-8859-4"
// CharsetISO88595 represents the "ISO-8859-5" charset
// CharsetISO88595 represents the "ISO-8859-5" charset.
CharsetISO88595 Charset = "ISO-8859-5"
// CharsetISO88596 represents the "ISO-8859-6" charset
// CharsetISO88596 represents the "ISO-8859-6" charset.
CharsetISO88596 Charset = "ISO-8859-6"
// CharsetISO88597 represents the "ISO-8859-7" charset
// CharsetISO88597 represents the "ISO-8859-7" charset.
CharsetISO88597 Charset = "ISO-8859-7"
// CharsetISO88599 represents the "ISO-8859-9" charset
// CharsetISO88599 represents the "ISO-8859-9" charset.
CharsetISO88599 Charset = "ISO-8859-9"
// CharsetISO885913 represents the "ISO-8859-13" charset
// CharsetISO885913 represents the "ISO-8859-13" charset.
CharsetISO885913 Charset = "ISO-8859-13"
// CharsetISO885914 represents the "ISO-8859-14" charset
// CharsetISO885914 represents the "ISO-8859-14" charset.
CharsetISO885914 Charset = "ISO-8859-14"
// CharsetISO885915 represents the "ISO-8859-15" charset
// CharsetISO885915 represents the "ISO-8859-15" charset.
CharsetISO885915 Charset = "ISO-8859-15"
// CharsetISO885916 represents the "ISO-8859-16" charset
// CharsetISO885916 represents the "ISO-8859-16" charset.
CharsetISO885916 Charset = "ISO-8859-16"
// CharsetISO2022JP represents the "ISO-2022-JP" charset
// CharsetISO2022JP represents the "ISO-2022-JP" charset.
CharsetISO2022JP Charset = "ISO-2022-JP"
// CharsetISO2022KR represents the "ISO-2022-KR" charset
// CharsetISO2022KR represents the "ISO-2022-KR" charset.
CharsetISO2022KR Charset = "ISO-2022-KR"
// CharsetWindows1250 represents the "windows-1250" charset
// CharsetWindows1250 represents the "windows-1250" charset.
CharsetWindows1250 Charset = "windows-1250"
// CharsetWindows1251 represents the "windows-1251" charset
// CharsetWindows1251 represents the "windows-1251" charset.
CharsetWindows1251 Charset = "windows-1251"
// CharsetWindows1252 represents the "windows-1252" charset
// CharsetWindows1252 represents the "windows-1252" charset.
CharsetWindows1252 Charset = "windows-1252"
// CharsetWindows1255 represents the "windows-1255" charset
// CharsetWindows1255 represents the "windows-1255" charset.
CharsetWindows1255 Charset = "windows-1255"
// CharsetWindows1256 represents the "windows-1256" charset
// CharsetWindows1256 represents the "windows-1256" charset.
CharsetWindows1256 Charset = "windows-1256"
// CharsetKOI8R represents the "KOI8-R" charset
// CharsetKOI8R represents the "KOI8-R" charset.
CharsetKOI8R Charset = "KOI8-R"
// CharsetKOI8U represents the "KOI8-U" charset
// CharsetKOI8U represents the "KOI8-U" charset.
CharsetKOI8U Charset = "KOI8-U"
// CharsetBig5 represents the "Big5" charset
// CharsetBig5 represents the "Big5" charset.
CharsetBig5 Charset = "Big5"
// CharsetGB18030 represents the "GB18030" charset
// CharsetGB18030 represents the "GB18030" charset.
CharsetGB18030 Charset = "GB18030"
// CharsetGB2312 represents the "GB2312" charset
// CharsetGB2312 represents the "GB2312" charset.
CharsetGB2312 Charset = "GB2312"
// CharsetTIS620 represents the "TIS-620" charset
// CharsetTIS620 represents the "TIS-620" charset.
CharsetTIS620 Charset = "TIS-620"
// CharsetEUCKR represents the "EUC-KR" charset
// CharsetEUCKR represents the "EUC-KR" charset.
CharsetEUCKR Charset = "EUC-KR"
// CharsetShiftJIS represents the "Shift_JIS" charset
// CharsetShiftJIS represents the "Shift_JIS" charset.
CharsetShiftJIS Charset = "Shift_JIS"
// CharsetUnknown represents the "Unknown" charset
// CharsetUnknown represents the "Unknown" charset.
CharsetUnknown Charset = "Unknown"
// CharsetGBK represents the "GBK" charset
// CharsetGBK represents the "GBK" charset.
CharsetGBK Charset = "GBK"
)
// List of MIME versions
const (
// MIME10 is the MIME Version 1.0
MIME10 MIMEVersion = "1.0"
)
// MIME10 represents the MIME version "1.0" used in email messages.
const MIME10 MIMEVersion = "1.0"
// List of common content types
const (
// TypeAppOctetStream represents the MIME type for arbitrary binary data.
TypeAppOctetStream ContentType = "application/octet-stream"
// TypeMultipartAlternative represents the MIME type for a message body that can contain multiple alternative
// formats.
TypeMultipartAlternative ContentType = "multipart/alternative"
// TypeMultipartMixed represents the MIME type for a multipart message containing different parts.
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"
// TypePGPSignature represents the MIME type for PGP signed messages.
TypePGPSignature ContentType = "application/pgp-signature"
// TypePGPEncrypted represents the MIME type for PGP encrypted messages.
TypePGPEncrypted ContentType = "application/pgp-encrypted"
// TypeTextHTML represents the MIME type for HTML text content.
TypeTextHTML ContentType = "text/html"
// TypeTextPlain represents the MIME type for plain text content.
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 (
// MIMEAlternative MIMEType represents a MIME multipart/alternative type, used for emails with multiple versions.
MIMEAlternative MIMEType = "alternative"
// MIMEMixed MIMEType represents a MIME multipart/mixed type used for emails containing different types of content.
MIMEMixed MIMEType = "mixed"
// MIMERelated MIMEType represents a MIME multipart/related type, used for emails with related content entities.
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 {
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 {
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 {
return string(e)
}

97
file.go
View file

@ -9,10 +9,15 @@ import (
"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)
// 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 {
ContentType ContentType
Desc string
@ -22,32 +27,68 @@ type File struct {
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 {
return func(f *File) {
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 {
return func(f *File) {
f.Name = name
}
}
// WithFileDescription sets an optional file description of the File that will be
// added as Content-Description part
// WithFileDescription sets an optional description for the File, which is used in the Content-Description
// 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 {
return func(f *File) {
f.Desc = description
}
}
// WithFileEncoding sets the encoding of the File. By default we should always use
// 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
// is provided as argument, the function will automatically override back to Base64
// WithFileEncoding sets the encoding type for a File.
//
// This function allows the specification of an encoding type for the file, typically used for attachments
// 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 {
return func(f *File) {
if encoding == EncodingQP {
@ -58,23 +99,45 @@ func WithFileEncoding(encoding Encoding) FileOption {
}
// 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
// could not be guessed. In some cases, however, it might be needed to force
// this to a specific type. For such situations this override method can
// be used
//
// By default, the content type is guessed based on the file type, and if no matching type is identified,
// the default "application/octet-stream" is used. This FileOption allows overriding the guessed content
// type with a specific one if required.
//
// 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 {
return func(f *File) {
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) {
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) {
v := f.Header.Get(string(header))
return v, v != ""

6
go.mod
View file

@ -6,4 +6,8 @@ module github.com/wneessen/go-mail
go 1.16
require go.mozilla.org/pkcs7 v0.9.0
require (
go.mozilla.org/pkcs7 v0.9.0
golang.org/x/crypto v0.28.0
golang.org/x/text v0.19.0
)

66
go.sum
View file

@ -1,2 +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
View file

@ -0,0 +1,3 @@
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
//
// SPDX-License-Identifier: MIT

133
header.go
View file

@ -4,129 +4,146 @@
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
// 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
// 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
// List of common generic header field names
const (
// HeaderContentDescription is the "Content-Description" header
// HeaderContentDescription is the "Content-Description" header.
HeaderContentDescription Header = "Content-Description"
// HeaderContentDisposition is the "Content-Disposition" header
// HeaderContentDisposition is the "Content-Disposition" header.
HeaderContentDisposition Header = "Content-Disposition"
// HeaderContentID is the "Content-ID" header
// HeaderContentID is the "Content-ID" header.
HeaderContentID Header = "Content-ID"
// HeaderContentLang is the "Content-Language" header
// HeaderContentLang is the "Content-Language" header.
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"
// HeaderContentTransferEnc is the "Content-Transfer-Encoding" header
// HeaderContentTransferEnc is the "Content-Transfer-Encoding" header.
HeaderContentTransferEnc Header = "Content-Transfer-Encoding"
// HeaderContentType is the "Content-Type" header
// HeaderContentType is the "Content-Type" header.
HeaderContentType Header = "Content-Type"
// HeaderDate represents the "Date" field
// See: https://www.rfc-editor.org/rfc/rfc822#section-5.1
// HeaderDate represents the "Date" field.
// https://datatracker.ietf.org/doc/html/rfc822#section-5.1
HeaderDate Header = "Date"
// HeaderDispositionNotificationTo is the MDN header as described in RFC8098
// See: https://www.rfc-editor.org/rfc/rfc8098.html#section-2.1
// HeaderDispositionNotificationTo is the MDN header as described in RFC 8098.
// https://datatracker.ietf.org/doc/html/rfc8098#section-2.1
HeaderDispositionNotificationTo Header = "Disposition-Notification-To"
// HeaderImportance represents the "Importance" field
// HeaderImportance represents the "Importance" field.
HeaderImportance Header = "Importance"
// HeaderInReplyTo represents the "In-Reply-To" field
// HeaderInReplyTo represents the "In-Reply-To" field.
HeaderInReplyTo Header = "In-Reply-To"
// HeaderListUnsubscribe is the "List-Unsubscribe" header field
// HeaderListUnsubscribe is the "List-Unsubscribe" header field.
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"
// HeaderMessageID represents the "Message-ID" field for message identification
// See: https://www.rfc-editor.org/rfc/rfc1036#section-2.1.5
// HeaderMessageID represents the "Message-ID" field for message identification.
// https://datatracker.ietf.org/doc/html/rfc1036#section-2.1.5
HeaderMessageID Header = "Message-ID"
// HeaderMIMEVersion represents the "MIME-Version" field as per RFC 2045
// See: https://datatracker.ietf.org/doc/html/rfc2045#section-4
// HeaderMIMEVersion represents the "MIME-Version" field as per RFC 2045.
// https://datatracker.ietf.org/doc/html/rfc2045#section-4
HeaderMIMEVersion Header = "MIME-Version"
// HeaderOrganization is the "Organization" header field
// HeaderOrganization is the "Organization" header field.
HeaderOrganization Header = "Organization"
// HeaderPrecedence is the "Precedence" header field
// HeaderPrecedence is the "Precedence" header field.
HeaderPrecedence Header = "Precedence"
// HeaderPriority represents the "Priority" field
// HeaderPriority represents the "Priority" field.
HeaderPriority Header = "Priority"
// HeaderReferences is the "References" header field
// HeaderReferences is the "References" header field.
HeaderReferences Header = "References"
// HeaderReplyTo is the "Reply-To" header field
// HeaderReplyTo is the "Reply-To" header field.
HeaderReplyTo Header = "Reply-To"
// HeaderSubject is the "Subject" header field
// HeaderSubject is the "Subject" header field.
HeaderSubject Header = "Subject"
// HeaderUserAgent is the "User-Agent" header field
// HeaderUserAgent is the "User-Agent" header field.
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"
// HeaderXMailer is the "X-Mailer" header field
// HeaderXMailer is the "X-Mailer" header field.
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"
// HeaderXPriority is the "X-Priority" header field
// HeaderXPriority is the "X-Priority" header field.
HeaderXPriority Header = "X-Priority"
)
// List of common address header field names
const (
// HeaderBcc is the "Blind Carbon Copy" header field
// HeaderBcc is the "Blind Carbon Copy" header field.
HeaderBcc AddrHeader = "Bcc"
// HeaderCc is the "Carbon Copy" header field
// HeaderCc is the "Carbon Copy" header field.
HeaderCc AddrHeader = "Cc"
// HeaderEnvelopeFrom is the envelope FROM header field
// It's not included in the mail body but only used by the Client for the envelope
// HeaderEnvelopeFrom is the envelope FROM header field.
//
// 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"
// HeaderFrom is the "From" header field
// HeaderFrom is the "From" header field.
HeaderFrom AddrHeader = "From"
// HeaderTo is the "Receipient" header field
// HeaderTo is the "Receipient" header field.
HeaderTo AddrHeader = "To"
)
// List of Importance values
const (
// ImportanceLow indicates a low level of importance or priority in a Msg.
ImportanceLow Importance = iota
// ImportanceNormal indicates a standard level of importance or priority for a Msg.
ImportanceNormal
// ImportanceHigh indicates a high level of importance or priority in a Msg.
ImportanceHigh
// ImportanceNonUrgent indicates a non-urgent level of importance or priority in a Msg.
ImportanceNonUrgent
// ImportanceUrgent indicates an urgent level of importance or priority in a Msg.
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 {
switch i {
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 {
switch i {
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 {
switch i {
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 {
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 {
return string(a)
}

2081
msg.go

File diff suppressed because it is too large Load diff

View file

@ -61,3 +61,25 @@ func TestMsg_WriteToSendmail(t *testing.T) {
t.Errorf("WriteToSendmail failed: %s", err)
}
}
func TestMsg_WriteToTempFileFailed(t *testing.T) {
m := NewMsg()
_ = m.From("Toni Tester <tester@example.com>")
_ = m.To("Ellenor Tester <ellinor@example.com>")
m.SetBodyString(TypeTextPlain, "This is a test")
curTmpDir := os.Getenv("TMPDIR")
defer func() {
if err := os.Setenv("TMPDIR", curTmpDir); err != nil {
t.Errorf("failed to set TMPDIR environment variable: %s", err)
}
}()
if err := os.Setenv("TMPDIR", "/invalid/directory/that/does/not/exist"); err != nil {
t.Errorf("failed to set TMPDIR environment variable: %s", err)
}
_, err := m.WriteToTempFile()
if err == nil {
t.Errorf("WriteToTempFile() did not fail as expected")
}
}

View file

@ -786,13 +786,11 @@ func TestMsg_SetMessageIDWithValue(t *testing.T) {
// TestMsg_SetMessageIDRandomness tests the randomness of Msg.SetMessageID methods
func TestMsg_SetMessageIDRandomness(t *testing.T) {
var mids []string
for i := 0; i < 100; i++ {
m := NewMsg()
for i := 0; i < 50_000; i++ {
m.SetMessageID()
mid := m.GetGenHeader(HeaderMessageID)
if len(mid) > 0 {
mids = append(mids, mid[0])
}
mid := m.GetMessageID()
mids = append(mids, mid)
}
c := make(map[string]int)
for i := range mids {
@ -805,6 +803,21 @@ func TestMsg_SetMessageIDRandomness(t *testing.T) {
}
}
func TestMsg_GetMessageID(t *testing.T) {
expected := "this.is.a.message.id"
msg := NewMsg()
msg.SetMessageIDWithValue(expected)
val := msg.GetMessageID()
if !strings.EqualFold(val, fmt.Sprintf("<%s>", expected)) {
t.Errorf("GetMessageID() failed. Expected: %s, got: %s", fmt.Sprintf("<%s>", expected), val)
}
msg.genHeader[HeaderMessageID] = nil
val = msg.GetMessageID()
if val != "" {
t.Errorf("GetMessageID() failed. Expected empty string, got: %s", val)
}
}
// TestMsg_FromFormat tests the FromFormat and EnvelopeFrom methods for the Msg object
func TestMsg_FromFormat(t *testing.T) {
tests := []struct {

View file

@ -12,8 +12,14 @@ import (
"os"
)
// WriteToTempFile will create a temporary file and output the Msg to this file
// The method will return the filename of the temporary file
// WriteToTempFile creates a temporary file and writes the Msg content to this 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) {
f, err := os.CreateTemp("", "go-mail_*.eml")
if err != nil {

View file

@ -12,8 +12,14 @@ import (
"io/ioutil"
)
// WriteToTempFile will create a temporary file and output the Msg to this file
// The method will return the filename of the temporary file
// WriteToTempFile creates a temporary file and writes the Msg content to this 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) {
f, err := ioutil.TempFile("", "go-mail_*.eml")
if err != nil {

View file

@ -18,22 +18,39 @@ import (
"strings"
)
// MaxHeaderLength defines the maximum line length for a mail header
// RFC 2047 suggests 76 characters
const MaxHeaderLength = 76
const (
// MaxHeaderLength defines the maximum line length for a mail header.
//
// 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
// RFC 2047 suggests 76 characters
const MaxBodyLength = 76
// MaxBodyLength defines the maximum line length for the mail body.
//
// 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
const SingleNewLine = "\r\n"
// SingleNewLine represents a single newline character sequence ("\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
// indicate a new segement of the mail
const DoubleNewLine = "\r\n\r\n"
// DoubleNewLine represents a double newline character sequence ("\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 {
bytesWritten int64
charset Charset
@ -45,7 +62,18 @@ type msgWriter struct {
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) {
if mw.err != nil {
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
}
// 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) {
msg.addDefaultHeader()
msg.checkUserAgent()
@ -144,7 +184,13 @@ func (mw *msgWriter) writeMsg(msg *Msg) {
}
}
// 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) {
keys := make([]string, 0, len(msg.genHeader))
for key := range msg.genHeader {
@ -156,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) {
for key, val := range msg.preformHeader {
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) {
multiPartWriter := multipart.NewWriter(mw)
if boundary != "" {
@ -183,7 +247,10 @@ func (mw *msgWriter) startMP(mimeType MIMEType, boundary string) {
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() {
if mw.depth > 0 {
mw.err = mw.multiPartWriter[mw.depth-1].Close()
@ -191,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) {
for _, file := range files {
encoding := EncodingB64
@ -250,12 +327,29 @@ func (mw *msgWriter) addFiles(files []*File, isAttachment bool) {
}
}
// 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) {
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) {
partCharset := part.charset
if partCharset.String() == "" {
@ -285,7 +379,14 @@ func (mw *msgWriter) writePart(part *Part, charset Charset) {
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) {
if mw.err != nil {
return
@ -295,7 +396,16 @@ func (mw *msgWriter) writeString(s string) {
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) {
buffer := strings.Builder{}
charLength := MaxHeaderLength - 2
@ -330,7 +440,18 @@ func (mw *msgWriter) writeHeader(key Header, values ...string) {
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.
//
// 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 encodedWriter io.WriteCloser

137
part.go
View file

@ -12,7 +12,11 @@ import (
// PartOption returns a function that can be used for grouping Part options
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 {
contentType ContentType
charset Charset
@ -23,7 +27,14 @@ type Part struct {
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) {
var b bytes.Buffer
if _, err := p.writeFunc(&b); err != nil {
@ -32,27 +43,54 @@ func (p *Part) GetContent() ([]byte, error) {
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 {
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 {
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 {
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) {
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 {
return p.description
}
@ -62,63 +100,126 @@ func (p *Part) IsSMimeSigned() bool {
return p.smime
}
// SetContent overrides the content of the Part with the given string
// 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) {
buffer := bytes.NewBufferString(content)
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) {
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) {
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) {
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) {
p.description = description
}
// 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
// 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)) {
p.writeFunc = writeFunc
}
// Delete removes the current part from the parts list of the Msg by setting the
// isDeleted flag to true. The msgWriter will skip it then
// Delete removes the current part from the parts list of the Msg by setting the isDeleted flag to true.
//
// 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() {
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 {
return func(p *Part) {
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 {
return func(p *Part) {
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 {
return func(p *Part) {
p.description = description

View file

@ -7,23 +7,41 @@ package mail
import (
"crypto/rand"
"encoding/binary"
"fmt"
"math/big"
"strings"
)
// 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)
//
// 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 (
letterIdxBits = 7 // 7 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
// letterIdxBits: Number of bits needed to represent a letter index. We have 64 possible characters
// which fit into 6 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
// crypto/random package and therfore is cryptographically secure
// randomStringSecure returns a random string of the specified length.
//
// 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) {
randString := strings.Builder{}
randString.Grow(length)
@ -52,23 +70,3 @@ func randomStringSecure(length int) (string, error) {
return randString.String(), nil
}
// randNum returns a random number with a maximum value of length
func randNum(length int) (int, error) {
if length <= 0 {
return 0, fmt.Errorf("provided number is <= 0: %d", length)
}
length64 := big.NewInt(int64(length))
if !length64.IsUint64() {
return 0, fmt.Errorf("big.NewInt() generation returned negative value: %d", length64)
}
randNum64, err := rand.Int(rand.Reader, length64)
if err != nil {
return 0, err
}
randomNum := int(randNum64.Int64())
if randomNum < 0 {
return 0, fmt.Errorf("generated random number does not fit as int64: %d", randNum64)
}
return randomNum, nil
}

View file

@ -38,33 +38,12 @@ func TestRandomStringSecure(t *testing.T) {
}
}
// TestRandomNum tests the randomNum method
func TestRandomNum(t *testing.T) {
tt := []struct {
testName string
max int
}{
{"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, err := randNum(tc.max)
func BenchmarkGenerator_RandomStringSecure(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, err := randomStringSecure(22)
if err != nil {
t.Errorf("random number generation failed: %s", err)
b.Errorf("RandomStringFromCharRange() failed: %s", err)
}
if rn < 0 {
t.Errorf("random number generation failed: %d is smaller than zero", rn)
}
if rn > tc.max {
t.Errorf("random number generation failed: %d is bigger than given value %d", rn, tc.max)
}
})
}
}

View file

@ -8,19 +8,41 @@ import (
"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 {
buffer []byte // contents are the bytes buffer[offset : len(buffer)]
offset int // read at &buffer[offset], write at &buffer[len(buffer)]
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 {
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) {
if r.err != nil {
return 0, r.err
@ -37,12 +59,20 @@ func (r *Reader) Read(payload []byte) (n int, err error) {
return n, err
}
// Reset resets the Reader buffer to be empty, but it retains the underlying storage
// for use by future writes.
// Reset resets the Reader buffer to be empty, but it retains the underlying storage for future use.
//
// 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() {
r.buffer = r.buffer[:0]
r.offset = 0
}
// 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 }

View file

@ -54,18 +54,32 @@ const (
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 {
Reason SendErrReason
isTemp bool
affectedMsg *Msg
errlist []error
isTemp bool
rcpt []string
Reason SendErrReason
}
// SendErrReason represents a comparable reason on why the delivery failed
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 {
if e.Reason > 10 {
return "unknown reason"
@ -92,10 +106,25 @@ func (e *SendError) Error() string {
}
}
}
if e.affectedMsg != nil && e.affectedMsg.GetMessageID() != "" {
errMessage.WriteString(", affected message ID: ")
errMessage.WriteString(e.affectedMsg.GetMessageID())
}
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 {
var t *SendError
if errors.As(errType, &t) && t != nil {
@ -104,7 +133,13 @@ func (e *SendError) Is(errType error) bool {
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 {
if e == nil {
return false
@ -112,7 +147,42 @@ func (e *SendError) IsTemp() bool {
return e.isTemp
}
// String implements the Stringer interface for the SendErrReason
// MessageID returns the message ID of the affected Msg that caused the error.
//
// 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 {
if e == nil || e.affectedMsg == nil {
return ""
}
return e.affectedMsg.GetMessageID()
}
// 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 {
if e == nil || e.affectedMsg == nil {
return nil
}
return e.affectedMsg
}
// 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 {
switch r {
case ErrGetSender:
@ -141,8 +211,16 @@ func (r SendErrReason) String() string {
return "unknown reason"
}
// isTempError checks the given SMTP error and returns true if the given error is of temporary nature
// and should be retried
// isTempError checks if the given SMTP error is of a temporary nature 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 {
return err.Error()[0] == '4'
}

View file

@ -83,7 +83,96 @@ func TestSendError_IsTemp(t *testing.T) {
}
}
func TestSendError_IsTempNil(t *testing.T) {
var se *SendError
if se.IsTemp() {
t.Error("expected false on nil-senderror")
}
}
func TestSendError_MessageID(t *testing.T) {
var se *SendError
err := returnSendError(ErrAmbiguous, false)
if !errors.As(err, &se) {
t.Errorf("error mismatch, expected error to be of type *SendError")
return
}
if errors.As(err, &se) {
if se.MessageID() == "" {
t.Errorf("sendError expected message-id, but got empty string")
}
if !strings.EqualFold(se.MessageID(), "<this.is.a.message.id>") {
t.Errorf("sendError message-id expected: %s, but got: %s", "<this.is.a.message.id>",
se.MessageID())
}
}
}
func TestSendError_MessageIDNil(t *testing.T) {
var se *SendError
if se.MessageID() != "" {
t.Error("expected empty string on nil-senderror")
}
}
func TestSendError_Msg(t *testing.T) {
var se *SendError
err := returnSendError(ErrAmbiguous, false)
if !errors.As(err, &se) {
t.Errorf("error mismatch, expected error to be of type *SendError")
return
}
if errors.As(err, &se) {
if se.Msg() == nil {
t.Errorf("sendError expected msg pointer, but got nil")
}
from := se.Msg().GetFromString()
if len(from) == 0 {
t.Errorf("sendError expected msg from, but got empty string")
return
}
if !strings.EqualFold(from[0], "<toni.tester@domain.tld>") {
t.Errorf("sendError message from expected: %s, but got: %s", "<toni.tester@domain.tld>",
from[0])
}
}
}
func TestSendError_MsgNil(t *testing.T) {
var se *SendError
if se.Msg() != nil {
t.Error("expected nil on nil-senderror")
}
}
func TestSendError_IsFail(t *testing.T) {
err1 := returnSendError(ErrAmbiguous, false)
err2 := returnSendError(ErrSMTPMailFrom, false)
if errors.Is(err1, err2) {
t.Errorf("error mismatch, ErrAmbiguous should not be equal to ErrSMTPMailFrom")
}
}
func TestSendError_ErrorMulti(t *testing.T) {
expected := `ambiguous reason, check Msg.SendError for message specific reasons, ` +
`affected recipient(s): <email1@domain.tld>, <email2@domain.tld>`
err := &SendError{
Reason: ErrAmbiguous, isTemp: false, affectedMsg: nil,
rcpt: []string{"<email1@domain.tld>", "<email2@domain.tld>"},
}
if err.Error() != expected {
t.Errorf("error mismatch, expected: %s, got: %s", expected, err.Error())
}
}
// returnSendError is a helper method to retunr a SendError with a specific reason
func returnSendError(r SendErrReason, t bool) error {
return &SendError{Reason: r, isTemp: t}
message := NewMsg()
_ = message.From("toni.tester@domain.tld")
_ = message.To("tina.tester@domain.tld")
message.Subject("This is the subject")
message.SetBodyString(TypeTextPlain, "This is the message body")
message.SetMessageIDWithValue("this.is.a.message.id")
return &SendError{Reason: r, isTemp: t, affectedMsg: message}
}

View file

@ -13,6 +13,19 @@
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.
type Auth interface {
// Start begins an authentication with a server.

View file

@ -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
package smtp
import (
"errors"
"fmt"
)
@ -13,53 +12,35 @@ import (
type loginAuth struct {
username, password 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
// 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
// 3 steps:
// - Sending AUTH LOGIN (server responds with "Username:")
// - Sending the username (server responds with "Password:")
// - Sending AUTH LOGIN (server might responds with "Username:")
// - Sending the username (server might responds with "Password:")
// - 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
// or is connected to localhost. Otherwise authentication will fail with an
// error, without sending the credentials.
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) {
// Must have TLS, or else localhost server.
// 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
// "it's ok, you can trust me with your password."
if !server.TLS && !isLocalhost(server.Name) {
return "", nil, errors.New("unencrypted connection")
return "", nil, ErrUnencrypted
}
if server.Name != a.host {
return "", nil, errors.New("wrong host name")
return "", nil, ErrWrongHostname
}
a.respStep = 0
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) {
if more {
switch string(fromServer) {
case LoginXUsernameChallenge, LoginXUsernameLowerChallenge, LoginXDraftUsernameChallenge:
switch a.respStep {
case 0:
a.respStep++
return []byte(a.username), nil
case LoginXPasswordChallenge, LoginXPasswordLowerChallenge, LoginXDraftPasswordChallenge:
case 1:
a.respStep++
return []byte(a.password), nil
default:
return nil, fmt.Errorf("unexpected server response: %s", string(fromServer))
return nil, fmt.Errorf("%w: %s", ErrUnexpectedServerResponse, string(fromServer))
}
}
return nil, nil

View file

@ -13,10 +13,6 @@
package smtp
import (
"errors"
)
// plainAuth is the type that satisfies the Auth interface for the "SMTP PLAIN" auth
type plainAuth struct {
identity, username, password string
@ -42,10 +38,10 @@ func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
// That might just be the attacker saying
// "it's ok, you can trust me with your password."
if !server.TLS && !isLocalhost(server.Name) {
return "", nil, errors.New("unencrypted connection")
return "", nil, ErrUnencrypted
}
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)
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) {
if more {
// We've already sent everything.
return nil, errors.New("unexpected server challenge")
return nil, ErrUnexpectedServerChallange
}
return nil, nil
}

317
smtp/auth_scram.go Normal file
View 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
}

View file

@ -30,34 +30,70 @@ import (
"net/textproto"
"os"
"strings"
"sync"
"time"
"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.
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.
Text *textproto.Conn
// keep a reference to the connection so it can be used to create a TLS
// connection later
conn net.Conn
// whether the Client is using TLS
tls bool
serverName string
// map of supported extensions
ext map[string]string
// supported auth mechanisms
// auth supported auth mechanisms
auth []string
// keep a reference to the connection so it can be used to create a TLS connection later
conn net.Conn
// debug logging is enabled
debug bool
// didHello indicates whether we've said HELO/EHLO
didHello bool
// dsnmrtype defines the mail return option in case DSN is enabled
dsnmrtype string
// dsnrntype defines the recipient notify option in case DSN is enabled
dsnrntype string
// ext is a map of supported extensions
ext map[string]string
// helloError is the error from the hello
helloError error
// isConnected indicates if the Client has an active connection
isConnected bool
// localName is the name to use in HELO/EHLO
localName string // the name to use in HELO/EHLO
didHello bool // whether we've said HELO/EHLO
helloError error // the error from the hello
// debug logging
debug bool // debug logging is enabled
logger log.Logger // logger will be used for debug logging
// DSN support
dsnmrtype string // dsnmrtype defines the mail return option in case DSN is enabled
dsnrntype string // dsnrntype defines the recipient notify option in case DSN is enabled
// logger will be used for debug logging
logger log.Logger
// mutex is used to synchronize access to shared resources, ensuring that only one goroutine can access
// the resource at a time.
mutex sync.RWMutex
// tls indicates whether the Client is using TLS
tls bool
// serverName denotes the name of the server to which the application will connect. Used for
// identification and routing.
serverName string
}
// Dial returns a new [Client] connected to an SMTP server at addr.
@ -88,13 +124,18 @@ func NewClient(conn net.Conn, host string) (*Client, error) {
}
c := &Client{Text: text, conn: conn, serverName: host, localName: "localhost"}
_, c.tls = conn.(*tls.Conn)
c.isConnected = true
return c, nil
}
// Close closes the connection.
func (c *Client) Close() error {
return c.Text.Close()
c.mutex.Lock()
err := c.Text.Close()
c.isConnected = false
c.mutex.Unlock()
return err
}
// hello runs a hello exchange if needed.
@ -121,28 +162,39 @@ func (c *Client) Hello(localName string) error {
if c.didHello {
return errors.New("smtp: Hello called after other methods")
}
c.mutex.Lock()
c.localName = localName
c.mutex.Unlock()
return c.hello()
}
// cmd is a convenience function that sends a command and returns the response
func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
c.mutex.Lock()
c.debugLog(log.DirClientToServer, format, args...)
id, err := c.Text.Cmd(format, args...)
if err != nil {
c.mutex.Unlock()
return 0, "", err
}
c.Text.StartResponse(id)
defer c.Text.EndResponse(id)
code, msg, err := c.Text.ReadResponse(expectCode)
c.debugLog(log.DirServerToClient, "%d %s", code, msg)
c.Text.EndResponse(id)
c.mutex.Unlock()
return code, msg, err
}
// helo sends the HELO greeting to the server. It should be used only when the
// server does not support ehlo.
func (c *Client) helo() error {
c.mutex.Lock()
c.ext = nil
c.mutex.Unlock()
_, _, err := c.cmd(250, "HELO %s", c.localName)
return err
}
@ -157,9 +209,13 @@ func (c *Client) StartTLS(config *tls.Config) error {
if err != nil {
return err
}
c.mutex.Lock()
c.conn = tls.Client(c.conn, config)
c.Text = textproto.NewConn(c.conn)
c.tls = true
c.mutex.Unlock()
return c.ehlo()
}
@ -167,11 +223,15 @@ func (c *Client) StartTLS(config *tls.Config) error {
// The return values are their zero values if [Client.StartTLS] did
// not succeed.
func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) {
c.mutex.RLock()
defer c.mutex.RUnlock()
tc, ok := c.conn.(*tls.Conn)
if !ok {
return
}
return tc.ConnectionState(), true
state, ok = tc.ConnectionState(), true
return
}
// Verify checks the validity of an email address on the server.
@ -257,6 +317,8 @@ func (c *Client) Mail(from string) error {
return err
}
cmdStr := "MAIL FROM:<%s>"
c.mutex.RLock()
if c.ext != nil {
if _, ok := c.ext["8BITMIME"]; ok {
cmdStr += " BODY=8BITMIME"
@ -269,6 +331,8 @@ func (c *Client) Mail(from string) error {
cmdStr += fmt.Sprintf(" RET=%s", c.dsnmrtype)
}
}
c.mutex.RUnlock()
_, _, err := c.cmd(250, cmdStr, from)
return err
}
@ -280,7 +344,11 @@ func (c *Client) Rcpt(to string) error {
if err := validateLine(to); err != nil {
return err
}
c.mutex.RLock()
_, ok := c.ext["DSN"]
c.mutex.RUnlock()
if ok && c.dsnrntype != "" {
_, _, err := c.cmd(25, "RCPT TO:<%s> NOTIFY=%s", to, c.dsnrntype)
return err
@ -294,12 +362,23 @@ type dataCloser struct {
io.WriteCloser
}
// Close releases the lock, closes the WriteCloser, waits for a response, and then returns any error encountered.
func (d *dataCloser) Close() error {
d.c.mutex.Lock()
_ = d.WriteCloser.Close()
_, _, err := d.c.Text.ReadResponse(250)
d.c.mutex.Unlock()
return err
}
// Write writes data to the underlying WriteCloser while ensuring thread-safety by locking and unlocking a mutex.
func (d *dataCloser) Write(p []byte) (n int, err error) {
d.c.mutex.Lock()
n, err = d.WriteCloser.Write(p)
d.c.mutex.Unlock()
return
}
// Data issues a DATA command to the server and returns a writer that
// can be used to write the mail headers and body. The caller should
// close the writer before calling any more methods on c. A call to
@ -309,7 +388,14 @@ func (c *Client) Data() (io.WriteCloser, error) {
if err != nil {
return nil, err
}
return &dataCloser{c, c.Text.DotWriter()}, nil
datacloser := &dataCloser{}
c.mutex.Lock()
datacloser.c = c
datacloser.WriteCloser = c.Text.DotWriter()
c.mutex.Unlock()
return datacloser, nil
}
var testHookStartTLS func(*tls.Config) // nil, except for tests
@ -405,7 +491,10 @@ func (c *Client) Extension(ext string) (bool, string) {
return false, ""
}
ext = strings.ToUpper(ext)
c.mutex.RLock()
param, ok := c.ext[ext]
c.mutex.RUnlock()
return ok, param
}
@ -438,7 +527,12 @@ func (c *Client) Quit() error {
if err != nil {
return err
}
return c.Text.Close()
c.mutex.Lock()
err = c.Text.Close()
c.isConnected = false
c.mutex.Unlock()
return err
}
// SetDebugLog enables the debug logging for incoming and outgoing SMTP messages
@ -472,6 +566,44 @@ func (c *Client) SetDSNRcptNotifyOption(d string) {
c.dsnrntype = d
}
// HasConnection checks if the client has an active connection.
// Returns true if the `conn` field is not nil, indicating an active connection.
func (c *Client) HasConnection() bool {
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 {
c.mutex.Lock()
defer c.mutex.Unlock()
if err := c.conn.SetDeadline(time.Now().Add(timeout)); err != nil {
return fmt.Errorf("smtp: failed to update deadline: %w", err)
}
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
// the log.Logger interface
func (c *Client) debugLog(d log.Direction, f string, a ...interface{}) {

View file

@ -25,6 +25,9 @@ func (c *Client) ehlo() error {
if err != nil {
return err
}
c.mutex.Lock()
defer c.mutex.Unlock()
ext := make(map[string]string)
extList := strings.Split(msg, "\n")
if len(extList) > 1 {

View file

@ -22,12 +22,15 @@ import "strings"
// should be the preferred greeting for servers that support it.
//
// Backport of: https://github.com/golang/go/commit/4d8db00641cc9ff4f44de7df9b8c4f4a4f9416ee#diff-4f6f6bdb9891d4dd271f9f31430420a2e44018fe4ee539576faf458bebb3cee4
// to guarantee backwards compatibility with Go 1.16/1.17:w
// to guarantee backwards compatibility with Go 1.16/1.17
func (c *Client) ehlo() error {
_, msg, err := c.cmd(250, "EHLO %s", c.localName)
if err != nil {
return err
}
c.mutex.Lock()
defer c.mutex.Unlock()
ext := make(map[string]string)
extList := strings.Split(msg, "\n")
if len(extList) > 1 {

File diff suppressed because it is too large Load diff

17
tls.go
View file

@ -4,25 +4,32 @@
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
const (
// TLSMandatory requires that the connection to the server is
// 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
// TLSOpportunistic tries to establish an encrypted connection via the
// 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
// NoTLS forces the transaction to be not encrypted
// NoTLS forces the transaction to be not encrypted.
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 {
switch p {
case TLSMandatory: