Compare commits

..

1 commit

Author SHA1 Message Date
Michael Fuchs
7da89ac3e1
Merge 46cf2ed498 into 627216425f 2024-10-01 06:26:51 +02:00
50 changed files with 1076 additions and 5820 deletions

View file

@ -14,10 +14,12 @@ 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 -race -cover -shuffle=on ./...
- go test -v -race -cover -shuffle=on ./...

View file

@ -10,7 +10,7 @@ on:
paths:
- '**.go'
- 'go.*'
- '.github/workflows/codecov.yml'
- '.github/**'
- 'codecov.yml'
pull_request:
branches:
@ -18,7 +18,7 @@ on:
paths:
- '**.go'
- 'go.*'
- '.github/workflows/codecov.yml'
- '.github/**'
- 'codecov.yml'
env:
TEST_HOST: ${{ secrets.TEST_HOST }}
@ -27,10 +27,6 @@ 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
@ -40,7 +36,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
go: ['1.23']
go: ['1.19', '1.20', '1.23']
steps:
- name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
@ -59,9 +55,9 @@ jobs:
sudo apt-get -y install sendmail; which sendmail
- name: Run Tests
run: |
go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./...
go test -v -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@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
with:
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos

View file

@ -54,7 +54,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
uses: github/codeql-action/init@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10
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@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
uses: github/codeql-action/autobuild@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10
# 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@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
uses: github/codeql-action/analyze@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10

View file

@ -29,7 +29,7 @@ jobs:
go-version: '1.23'
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- name: golangci-lint
uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1
uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0
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

@ -18,4 +18,4 @@ jobs:
with:
egress-policy: audit
- name: Run govulncheck
uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4
uses: golang/govulncheck-action@dd0578b371c987f96d1185abb54344b44352bd58 # v1.0.3

View file

@ -1,45 +0,0 @@
# 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

@ -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@84480863f228bb9747b473957fcc9e309aa96097 # v4.4.2
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
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@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
uses: github/codeql-action/upload-sarif@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10
with:
sarif_file: results.sarif

View file

@ -10,18 +10,17 @@ permissions:
on:
push:
branches:
- main
paths:
- '**.go'
- 'go.*'
- '.github/workflows/sonarqube.yml'
- main # or the name of your main branch
pull_request:
branches:
- main
paths:
- '**.go'
- 'go.*'
- '.github/workflows/sonarqube.yml'
- 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"
jobs:
build:
name: Build
@ -39,11 +38,11 @@ jobs:
- name: Setup Go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
go-version: '1.23'
go-version: '1.23.x'
- name: Run unit Tests
run: |
go test -shuffle=on -race --coverprofile=./cov.out ./...
go test -v -race --coverprofile=./cov.out ./...
- uses: sonarsource/sonarqube-scan-action@884b79409bbd464b2a59edc326a4b77dc56b2195 # master
env:

10
.reuse/dep5 Normal file
View file

@ -0,0 +1,10 @@
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,41 +18,40 @@ 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 for sending mails to
The main idea of this library was to provide a simple interface to sending mails for
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 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.
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.
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.
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.
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.).
The smtp package of go-mail is forked from the original Go stdlib's `net/smtp` and then extended by the go-mail
team.
## Features
Here are some highlights of go-mail's featureset:
Some of the features of this library:
* [X] Very small dependency footprint (mainly Go Stdlib and Go extended packages)
* [X] Only Standard Library dependant
* [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, SCRAM-SHA-1(-PLUS), SCRAM-SHA-256(-PLUS))
* [X] SMTP Auth support (LOGIN, PLAIN, CRAM-MD, XOAUTH2)
* [X] RFC5322 compliant mail address validation
* [X] Support for common mail header field generation (Message-ID, Date, Bulk-Precedence, Priority, etc.)
* [X] Concurrency-safe reusing the same SMTP connection to send multiple mails
* [X] 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.WriterTo` and `io.Reader` interfaces
* [X] Message object satisfies `io.WriteTo` 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
@ -77,8 +76,7 @@ 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) alternatively find us
on the [Gophers Slack](https://gophers.slack.com) in #go-mail
We have a support and general discussion channel on Discord. Find us at: [#go-mail](https://discord.gg/dbfQyC4s)
## Middleware
The goal of go-mail is to keep it free from 3rd party dependencies and only focus on things a mail library should
@ -101,18 +99,15 @@ 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 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):
go-mail was initially authored and developed by [Winni Neessen](https://github.com/wneessen/).
<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)
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)

View file

@ -1,9 +0,0 @@
# 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,131 +6,41 @@ package mail
import "errors"
// SMTPAuthType is a type wrapper for a string type. It represents the type of SMTP authentication
// mechanism to be used.
// SMTPAuthType represents a string to any SMTP AUTH type
type SMTPAuthType string
// Supported SMTP AUTH types
const (
// 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 is the "CRAM-MD5" SASL authentication mechanism as described in RFC 4954
SMTPAuthCramMD5 SMTPAuthType = "CRAM-MD5"
// 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 is the "LOGIN" SASL authentication mechanism
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 passed the WithSMTPAuth option at all.
// authentication, the Client should not be used with the WithSMTPAuth option
SMTPAuthNoAuth SMTPAuthType = ""
// 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 is the "PLAIN" authentication mechanism as described in RFC 4616
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 is returned when the server does not support the "PLAIN" SMTP
// authentication type.
// ErrPlainAuthNotSupported should be used if the target server does not support the "PLAIN" schema
ErrPlainAuthNotSupported = errors.New("server does not support SMTP AUTH type: PLAIN")
// ErrLoginAuthNotSupported is returned when the server does not support the "LOGIN" SMTP
// authentication type.
// ErrLoginAuthNotSupported should be used if the target server does not support the "LOGIN" schema
ErrLoginAuthNotSupported = errors.New("server does not support SMTP AUTH type: LOGIN")
// ErrCramMD5AuthNotSupported is returned when the server does not support the "CRAM-MD5" SMTP
// authentication type.
// ErrCramMD5AuthNotSupported should be used if the target server does not support the "CRAM-MD5" schema
ErrCramMD5AuthNotSupported = errors.New("server does not support SMTP AUTH type: CRAM-MD5")
// ErrXOauth2AuthNotSupported is returned when the server does not support the "XOAUTH2" schema.
// ErrXOauth2AuthNotSupported should be used if the target 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,39 +9,21 @@ import (
"io"
)
// 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.
// ErrNoOutWriter is an error message that should be used if a Base64LineBreaker has no out io.Writer set
const ErrNoOutWriter = "no io.Writer set for Base64LineBreaker"
// 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)
// Base64LineBreaker is a io.WriteCloser that writes Base64 encoded data streams
// with line breaks at a given line length
type Base64LineBreaker struct {
line [MaxBodyLength]byte
used int
out io.Writer
}
// 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.
var newlineBytes = []byte(SingleNewLine)
// Write writes the data stream and inserts a SingleNewLine when the maximum
// line length is reached
func (l *Base64LineBreaker) Write(data []byte) (numBytes int, err error) {
if l.out == nil {
err = errors.New(ErrNoOutWriter)
@ -73,14 +55,8 @@ func (l *Base64LineBreaker) Write(data []byte) (numBytes int, err error) {
return l.Write(data[excess:])
}
// 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.
// Close closes the Base64LineBreaker and writes any access data that is still
// unwritten in memory
func (l *Base64LineBreaker) Close() (err error) {
if l.used > 0 {
_, err = l.out.Write(l.line[0:l.used])

974
client.go

File diff suppressed because it is too large Load diff

View file

@ -9,23 +9,7 @@ package mail
import "errors"
// 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.
// Send sends out the mail message
func (c *Client) Send(messages ...*Msg) error {
if err := c.checkConn(); err != nil {
return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}

View file

@ -11,21 +11,7 @@ import (
"errors"
)
// 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.
// Send sends out the mail message
func (c *Client) Send(messages ...*Msg) (returnErr error) {
if err := c.checkConn(); err != nil {
returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}

View file

@ -27,7 +27,7 @@ const (
// DefaultHost is used as default hostname for the Client
DefaultHost = "localhost"
// TestRcpt is a trash mail address to send test mails to
TestRcpt = "couttifaddebro-1473@yopmail.com"
TestRcpt = "go-mail@mytrashmailer.com"
// TestServerProto is the protocol used for the simple SMTP test server
TestServerProto = "tcp"
// TestServerAddr is the address the simple SMTP test server listens on
@ -483,20 +483,20 @@ func TestWithDSN(t *testing.T) {
t.Errorf("failed to create new client: %s", err)
return
}
if !c.requestDSN {
t.Errorf("WithDSN failed. c.requestDSN expected to be: %t, got: %t", true, c.requestDSN)
if !c.dsn {
t.Errorf("WithDSN failed. c.dsn expected to be: %t, got: %t", true, c.dsn)
}
if c.dsnReturnType != DSNMailReturnFull {
t.Errorf("WithDSN failed. c.dsnReturnType expected to be: %s, got: %s", DSNMailReturnFull,
c.dsnReturnType)
if c.dsnmrtype != DSNMailReturnFull {
t.Errorf("WithDSN failed. c.dsnmrtype expected to be: %s, got: %s", DSNMailReturnFull,
c.dsnmrtype)
}
if c.dsnRcptNotifyType[0] != string(DSNRcptNotifyFailure) {
t.Errorf("WithDSN failed. c.dsnRcptNotifyType[0] expected to be: %s, got: %s", DSNRcptNotifyFailure,
c.dsnRcptNotifyType[0])
if c.dsnrntype[0] != string(DSNRcptNotifyFailure) {
t.Errorf("WithDSN failed. c.dsnrntype[0] expected to be: %s, got: %s", DSNRcptNotifyFailure,
c.dsnrntype[0])
}
if c.dsnRcptNotifyType[1] != string(DSNRcptNotifySuccess) {
t.Errorf("WithDSN failed. c.dsnRcptNotifyType[1] expected to be: %s, got: %s", DSNRcptNotifySuccess,
c.dsnRcptNotifyType[1])
if c.dsnrntype[1] != string(DSNRcptNotifySuccess) {
t.Errorf("WithDSN failed. c.dsnrntype[1] expected to be: %s, got: %s", DSNRcptNotifySuccess,
c.dsnrntype[1])
}
}
@ -519,8 +519,8 @@ func TestWithDSNMailReturnType(t *testing.T) {
t.Errorf("failed to create new client: %s", err)
return
}
if string(c.dsnReturnType) != tt.want {
t.Errorf("WithDSNMailReturnType failed. Expected %s, got: %s", tt.want, string(c.dsnReturnType))
if string(c.dsnmrtype) != tt.want {
t.Errorf("WithDSNMailReturnType failed. Expected %s, got: %s", tt.want, string(c.dsnmrtype))
}
})
}
@ -547,11 +547,11 @@ func TestWithDSNRcptNotifyType(t *testing.T) {
t.Errorf("failed to create new client: %s", err)
return
}
if len(c.dsnRcptNotifyType) <= 0 && !tt.sf {
if len(c.dsnrntype) <= 0 && !tt.sf {
t.Errorf("WithDSNRcptNotifyType failed. Expected at least one DSNRNType but got none")
}
if !tt.sf && c.dsnRcptNotifyType[0] != tt.want {
t.Errorf("WithDSNRcptNotifyType failed. Expected %s, got: %s", tt.want, c.dsnRcptNotifyType[0])
if !tt.sf && c.dsnrntype[0] != tt.want {
t.Errorf("WithDSNRcptNotifyType failed. Expected %s, got: %s", tt.want, c.dsnrntype[0])
}
})
}
@ -602,10 +602,6 @@ func TestSetSMTPAuthCustom(t *testing.T) {
if c.smtpAuth == nil {
t.Errorf("failed to set custom SMTP auth method. SMTP Auth method is empty")
}
if c.smtpAuthType != SMTPAuthCustom {
t.Errorf("failed to set custom SMTP auth method. SMTP Auth type is not custom: %s",
c.smtpAuthType)
}
p, _, err := c.smtpAuth.Start(&si)
if err != nil {
t.Errorf("SMTP Auth Start() method returned error: %s", err)
@ -617,32 +613,6 @@ func TestSetSMTPAuthCustom(t *testing.T) {
}
}
// TestClient_Close_double tests if a close on an already closed connection causes an error.
func TestClient_Close_double(t *testing.T) {
c, err := getTestConnection(true)
if err != nil {
t.Skipf("failed to create test client: %s. Skipping tests", err)
}
ctx := context.Background()
if err = c.DialWithContext(ctx); err != nil {
t.Errorf("failed to dial with context: %s", err)
return
}
if c.smtpClient == nil {
t.Errorf("DialWithContext didn't fail but no SMTP client found.")
return
}
if !c.smtpClient.HasConnection() {
t.Errorf("DialWithContext didn't fail but no connection found.")
}
if err = c.Close(); err != nil {
t.Errorf("failed to close connection: %s", err)
}
if err = c.Close(); err != nil {
t.Errorf("failed 2nd close connection: %s", err)
}
}
// TestClient_DialWithContext tests the DialWithContext method for the Client object
func TestClient_DialWithContext(t *testing.T) {
c, err := getTestConnection(true)
@ -650,7 +620,7 @@ func TestClient_DialWithContext(t *testing.T) {
t.Skipf("failed to create test client: %s. Skipping tests", err)
}
ctx := context.Background()
if err = c.DialWithContext(ctx); err != nil {
if err := c.DialWithContext(ctx); err != nil {
t.Errorf("failed to dial with context: %s", err)
return
}
@ -1866,299 +1836,6 @@ func TestClient_DialSendConcurrent_local(t *testing.T) {
}
}
func TestClient_AuthSCRAMSHAX(t *testing.T) {
if os.Getenv("TEST_ONLINE_SCRAM") == "" {
t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests")
}
hostname := os.Getenv("TEST_HOST_SCRAM")
username := os.Getenv("TEST_USER_SCRAM")
password := os.Getenv("TEST_PASS_SCRAM")
tests := []struct {
name string
authtype SMTPAuthType
}{
{"SCRAM-SHA-1", SMTPAuthSCRAMSHA1},
{"SCRAM-SHA-256", SMTPAuthSCRAMSHA256},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, err := NewClient(hostname,
WithTLSPortPolicy(TLSMandatory),
WithSMTPAuth(tt.authtype),
WithUsername(username), WithPassword(password))
if err != nil {
t.Errorf("unable to create new client: %s", err)
return
}
if err = client.DialWithContext(context.Background()); err != nil {
t.Errorf("failed to dial to test server: %s", err)
}
if err = client.Close(); err != nil {
t.Errorf("failed to close server connection: %s", err)
}
})
}
}
func TestClient_AuthLoginSuccess(t *testing.T) {
tests := []struct {
name string
featureSet string
}{
{"default", "250-AUTH LOGIN\r\n250-X-DEFAULT-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"},
{"mox server", "250-AUTH LOGIN\r\n250-X-MOX-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"},
{"null byte", "250-AUTH LOGIN\r\n250-X-NULLBYTE-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"},
{"bogus responses", "250-AUTH LOGIN\r\n250-X-BOGUS-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"},
{"empty responses", "250-AUTH LOGIN\r\n250-X-EMPTY-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"},
}
for i, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
serverPort := TestServerPortBase + 40 + i
go func() {
if err := simpleSMTPServer(ctx, tt.featureSet, true, serverPort); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 300)
client, err := NewClient(TestServerAddr,
WithPort(serverPort),
WithTLSPortPolicy(NoTLS),
WithSMTPAuth(SMTPAuthLogin),
WithUsername("toni@tester.com"),
WithPassword("V3ryS3cr3t+"))
if err != nil {
t.Errorf("unable to create new client: %s", err)
return
}
if err = client.DialWithContext(context.Background()); err != nil {
t.Errorf("failed to dial to test server: %s", err)
}
if err = client.Close(); err != nil {
t.Errorf("failed to close server connection: %s", err)
}
})
}
}
func TestClient_AuthLoginFail(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
serverPort := TestServerPortBase + 50
featureSet := "250-AUTH LOGIN\r\n250-X-DEFAULT-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
go func() {
if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 300)
client, err := NewClient(TestServerAddr,
WithPort(serverPort),
WithTLSPortPolicy(NoTLS),
WithSMTPAuth(SMTPAuthLogin),
WithUsername("toni@tester.com"),
WithPassword("InvalidPassword"))
if err != nil {
t.Errorf("unable to create new client: %s", err)
return
}
if err = client.DialWithContext(context.Background()); err == nil {
t.Error("expected to fail to dial to test server, but it succeeded")
}
}
func TestClient_AuthLoginFail_noTLS(t *testing.T) {
if os.Getenv("TEST_SKIP_ONLINE") != "" {
t.Skipf("env variable TEST_SKIP_ONLINE is set. Skipping online tests")
}
th := os.Getenv("TEST_HOST")
if th == "" {
t.Skipf("no host set. Skipping online tests")
}
tp := 587
if tps := os.Getenv("TEST_PORT"); tps != "" {
tpi, err := strconv.Atoi(tps)
if err == nil {
tp = tpi
}
}
client, err := NewClient(th, WithPort(tp), WithSMTPAuth(SMTPAuthLogin), WithTLSPolicy(NoTLS))
if err != nil {
t.Errorf("failed to create new client: %s", err)
}
u := os.Getenv("TEST_SMTPAUTH_USER")
if u != "" {
client.SetUsername(u)
}
p := os.Getenv("TEST_SMTPAUTH_PASS")
if p != "" {
client.SetPassword(p)
}
// We don't want to log authentication data in tests
client.SetDebugLog(false)
if err = client.DialWithContext(context.Background()); err == nil {
t.Error("expected to fail to dial to test server, but it succeeded")
}
if !errors.Is(err, smtp.ErrUnencrypted) {
t.Errorf("expected error to be %s, but got %s", smtp.ErrUnencrypted, err)
}
}
func TestClient_AuthSCRAMSHAX_fail(t *testing.T) {
if os.Getenv("TEST_ONLINE_SCRAM") == "" {
t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests")
}
hostname := os.Getenv("TEST_HOST_SCRAM")
tests := []struct {
name string
authtype SMTPAuthType
}{
{"SCRAM-SHA-1", SMTPAuthSCRAMSHA1},
{"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS},
{"SCRAM-SHA-256", SMTPAuthSCRAMSHA256},
{"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, err := NewClient(hostname,
WithTLSPortPolicy(TLSMandatory),
WithSMTPAuth(tt.authtype),
WithUsername("invalid"), WithPassword("invalid"))
if err != nil {
t.Errorf("unable to create new client: %s", err)
return
}
if err = client.DialWithContext(context.Background()); err == nil {
t.Errorf("expected error but got nil")
}
})
}
}
func TestClient_AuthSCRAMSHAX_unsupported(t *testing.T) {
if os.Getenv("TEST_ALLOW_SEND") == "" {
t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test")
}
client, err := getTestConnection(true)
if err != nil {
t.Skipf("failed to create test client: %s. Skipping tests", err)
}
tests := []struct {
name string
authtype SMTPAuthType
expErr error
}{
{"SCRAM-SHA-1", SMTPAuthSCRAMSHA1, ErrSCRAMSHA1AuthNotSupported},
{"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS, ErrSCRAMSHA1PLUSAuthNotSupported},
{"SCRAM-SHA-256", SMTPAuthSCRAMSHA256, ErrSCRAMSHA256AuthNotSupported},
{"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS, ErrSCRAMSHA256PLUSAuthNotSupported},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client.SetSMTPAuth(tt.authtype)
client.SetTLSPolicy(TLSMandatory)
if err = client.DialWithContext(context.Background()); err == nil {
t.Errorf("expected error but got nil")
}
if !errors.Is(err, tt.expErr) {
t.Errorf("expected error %s, but got %s", tt.expErr, err)
}
})
}
}
func TestClient_AuthSCRAMSHAXPLUS_tlsexporter(t *testing.T) {
if os.Getenv("TEST_ONLINE_SCRAM") == "" {
t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests")
}
hostname := os.Getenv("TEST_HOST_SCRAM")
username := os.Getenv("TEST_USER_SCRAM")
password := os.Getenv("TEST_PASS_SCRAM")
tests := []struct {
name string
authtype SMTPAuthType
}{
{"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS},
{"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, err := NewClient(hostname,
WithTLSPortPolicy(TLSMandatory),
WithSMTPAuth(tt.authtype),
WithUsername(username), WithPassword(password))
if err != nil {
t.Errorf("unable to create new client: %s", err)
return
}
if err = client.DialWithContext(context.Background()); err != nil {
t.Errorf("failed to dial to test server: %s", err)
}
if err = client.Close(); err != nil {
t.Errorf("failed to close server connection: %s", err)
}
})
}
}
func TestClient_AuthSCRAMSHAXPLUS_tlsunique(t *testing.T) {
if os.Getenv("TEST_ONLINE_SCRAM") == "" {
t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests")
}
hostname := os.Getenv("TEST_HOST_SCRAM")
username := os.Getenv("TEST_USER_SCRAM")
password := os.Getenv("TEST_PASS_SCRAM")
tlsConfig := &tls.Config{}
tlsConfig.MaxVersion = tls.VersionTLS12
tlsConfig.ServerName = hostname
tests := []struct {
name string
authtype SMTPAuthType
}{
{"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS},
{"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, err := NewClient(hostname,
WithTLSPortPolicy(TLSMandatory),
WithTLSConfig(tlsConfig),
WithSMTPAuth(tt.authtype),
WithUsername(username), WithPassword(password))
if err != nil {
t.Errorf("unable to create new client: %s", err)
return
}
if err = client.DialWithContext(context.Background()); err != nil {
t.Errorf("failed to dial to test server: %s", err)
}
if err = client.Close(); err != nil {
t.Errorf("failed to close server connection: %s", err)
}
})
}
}
// getTestConnection takes environment variables to establish a connection to a real
// SMTP server to test all functionality that requires a connection
func getTestConnection(auth bool) (*Client, error) {
@ -2201,10 +1878,10 @@ func getTestConnection(auth bool) (*Client, error) {
// We don't want to log authentication data in tests
c.SetDebugLog(false)
}
if err = c.DialWithContext(context.Background()); err != nil {
if err := c.DialWithContext(context.Background()); err != nil {
return c, fmt.Errorf("connection to test server failed: %w", err)
}
if err = c.Close(); err != nil {
if err := c.Close(); err != nil {
return c, fmt.Errorf("disconnect from test server failed: %w", err)
}
return c, nil
@ -2417,6 +2094,7 @@ func TestXOAuth2OK_faker(t *testing.T) {
"250 8BITMIME",
"250 OK",
"235 2.7.0 Accepted",
"250 OK",
"221 OK",
}
var wrote strings.Builder
@ -2437,10 +2115,10 @@ func TestXOAuth2OK_faker(t *testing.T) {
if err != nil {
t.Fatalf("unable to create new client: %v", err)
}
if err = c.DialWithContext(context.Background()); err != nil {
if err := c.DialWithContext(context.Background()); err != nil {
t.Fatalf("unexpected dial error: %v", err)
}
if err = c.Close(); err != nil {
if err := c.Close(); err != nil {
t.Fatalf("disconnect from test server failed: %v", err)
}
if !strings.Contains(wrote.String(), "AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n") {
@ -2455,6 +2133,7 @@ func TestXOAuth2Unsupported_faker(t *testing.T) {
"250-AUTH LOGIN PLAIN",
"250 8BITMIME",
"250 OK",
"250 OK",
"221 OK",
}
var wrote strings.Builder
@ -2473,18 +2152,18 @@ func TestXOAuth2Unsupported_faker(t *testing.T) {
if err != nil {
t.Fatalf("unable to create new client: %v", err)
}
if err = c.DialWithContext(context.Background()); err == nil {
if err := c.DialWithContext(context.Background()); err == nil {
t.Fatal("expected dial error got nil")
} else {
if !errors.Is(err, ErrXOauth2AuthNotSupported) {
t.Fatalf("expected %v; got %v", ErrXOauth2AuthNotSupported, err)
}
}
if err = c.Close(); err != nil {
if err := c.Close(); err != nil {
t.Fatalf("disconnect from test server failed: %v", err)
}
client := strings.Split(wrote.String(), "\r\n")
if len(client) != 4 {
if len(client) != 5 {
t.Fatalf("unexpected number of client requests got %d; want 5", len(client))
}
if !strings.HasPrefix(client[0], "EHLO") {
@ -2493,7 +2172,10 @@ func TestXOAuth2Unsupported_faker(t *testing.T) {
if client[1] != "NOOP" {
t.Fatalf("expected NOOP, got %q", client[1])
}
if client[2] != "QUIT" {
if client[2] != "NOOP" {
t.Fatalf("expected NOOP, got %q", client[2])
}
if client[3] != "QUIT" {
t.Fatalf("expected QUIT, got %q", client[3])
}
}
@ -2629,51 +2311,6 @@ func handleTestServerConnection(connection net.Conn, featureSet string, failRese
break
}
_ = writeLine("235 2.7.0 Authentication successful")
case strings.HasPrefix(data, "AUTH LOGIN"):
var username, password string
userResp := "VXNlcm5hbWU6"
passResp := "UGFzc3dvcmQ6"
if strings.Contains(featureSet, "250-X-MOX-LOGIN") {
userResp = ""
passResp = "UGFzc3dvcmQ="
}
if strings.Contains(featureSet, "250-X-NULLBYTE-LOGIN") {
userResp = "VXNlciBuYW1lAA=="
passResp = "UGFzc3dvcmQA"
}
if strings.Contains(featureSet, "250-X-BOGUS-LOGIN") {
userResp = "Qm9ndXM="
passResp = "Qm9ndXM="
}
if strings.Contains(featureSet, "250-X-EMPTY-LOGIN") {
userResp = ""
passResp = ""
}
_ = writeLine("334 " + userResp)
ddata, derr := reader.ReadString('\n')
if derr != nil {
fmt.Printf("failed to read username data from connection: %s\n", derr)
break
}
ddata = strings.TrimSpace(ddata)
username = ddata
_ = writeLine("334 " + passResp)
ddata, derr = reader.ReadString('\n')
if derr != nil {
fmt.Printf("failed to read password data from connection: %s\n", derr)
break
}
ddata = strings.TrimSpace(ddata)
password = ddata
if !strings.EqualFold(username, "dG9uaUB0ZXN0ZXIuY29t") ||
!strings.EqualFold(password, "VjNyeVMzY3IzdCs=") {
_ = writeLine("535 5.7.8 Error: authentication failed")
break
}
_ = writeLine("235 2.7.0 Authentication successful")
case strings.EqualFold(data, "DATA"):
_ = writeLine("354 End data with <CR><LF>.<CR><LF>")
for {

11
doc.go
View file

@ -2,13 +2,8 @@
//
// SPDX-License-Identifier: MIT
// 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 provides a simple and easy way to send mails with Go
package mail
// VERSION indicates the current version of the package. It is also attached to the default user
// agent string.
const VERSION = "0.5.0"
// VERSION is used in the default user agent string
const VERSION = "0.4.4"

205
eml.go
View file

@ -18,35 +18,14 @@ import (
"strings"
)
// 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.
// EMLToMsgFromString will parse a given EML string and returns a pre-filled Msg pointer
func EMLToMsgFromString(emlString string) (*Msg, error) {
eb := bytes.NewBufferString(emlString)
return EMLToMsgFromReader(eb)
}
// 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.
// EMLToMsgFromReader will parse a reader that holds EML content and returns a pre-filled
// Msg pointer
func EMLToMsgFromReader(reader io.Reader) (*Msg, error) {
msg := &Msg{
addrHeader: make(map[AddrHeader][]*netmail.Address),
@ -67,19 +46,8 @@ func EMLToMsgFromReader(reader io.Reader) (*Msg, error) {
return msg, nil
}
// 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.
// EMLToMsgFromFile will open and parse a .eml file at a provided file path and returns a
// pre-filled Msg pointer
func EMLToMsgFromFile(filePath string) (*Msg, error) {
msg := &Msg{
addrHeader: make(map[AddrHeader][]*netmail.Address),
@ -100,19 +68,7 @@ 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.
//
// 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.
// parseEML parses the EML's headers and body and inserts the parsed values into the Msg
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)
@ -123,18 +79,7 @@ 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.
//
// 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.
// readEML opens an EML file and uses net/mail to parse the header and body
func readEML(filePath string) (*netmail.Message, *bytes.Buffer, error) {
fileHandle, err := os.Open(filePath)
if err != nil {
@ -146,19 +91,7 @@ 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.
//
// 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.
// readEMLFromReader uses net/mail to parse the header and body from a given io.Reader
func readEMLFromReader(reader io.Reader) (*netmail.Message, *bytes.Buffer, error) {
parsedMsg, err := netmail.ReadMessage(reader)
if err != nil {
@ -173,18 +106,8 @@ func readEMLFromReader(reader io.Reader) (*netmail.Message, *bytes.Buffer, error
return parsedMsg, &buf, nil
}
// 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.
// parseEMLHeaders will check the EML headers for the most common headers and set the
// according settings in the Msg
func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error {
commonHeaders := []Header{
HeaderContentType, HeaderImportance, HeaderInReplyTo, HeaderListUnsubscribe,
@ -252,19 +175,7 @@ func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error {
return nil
}
// 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.
// parseEMLBodyParts parses the body of a EML based on the different content types and encodings
func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
// Extract the transfer encoding of the body
mediatype, params, err := mime.ParseMediaType(parsedMsg.Header.Get(HeaderContentType.String()))
@ -301,24 +212,10 @@ func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *M
return nil
}
// 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.
// parseEMLBodyPlain parses the mail body of plain type mails
func parseEMLBodyPlain(mediatype string, parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
contentTransferEnc := parsedMsg.Header.Get(HeaderContentTransferEnc.String())
// If no Content-Transfer-Encoding is set, we can imply 7bit US-ASCII encoding
// https://datatracker.ietf.org/doc/html/rfc2045#section-6.1
// According to RFC2045, if no Content-Transfer-Encoding is set, we can imply 7bit US-ASCII encoding
if contentTransferEnc == "" || strings.EqualFold(contentTransferEnc, EncodingUSASCII.String()) {
msg.SetEncoding(EncodingUSASCII)
msg.SetBodyString(ContentType(mediatype), bodybuf.String())
@ -352,20 +249,7 @@ func parseEMLBodyPlain(mediatype string, parsedMsg *netmail.Message, bodybuf *by
return fmt.Errorf("unsupported Content-Transfer-Encoding")
}
// 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.
// parseEMLMultipart parses a multipart body part of a EML
func parseEMLMultipart(params map[string]string, bodybuf *bytes.Buffer, msg *Msg) error {
boundary, ok := params["boundary"]
if !ok {
@ -465,15 +349,7 @@ ReadNextPart:
return nil
}
// 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.
// parseEMLEncoding parses and determines the encoding of the message
func parseEMLEncoding(mailHeader *netmail.Header, msg *Msg) {
if value := mailHeader.Get(HeaderContentTransferEnc.String()); value != "" {
switch {
@ -487,15 +363,7 @@ func parseEMLEncoding(mailHeader *netmail.Header, msg *Msg) {
}
}
// 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.
// parseEMLContentTypeCharset parses and determines the charset and content type of the message
func parseEMLContentTypeCharset(mailHeader *netmail.Header, msg *Msg) {
if value := mailHeader.Get(HeaderContentType.String()); value != "" {
contentType, optional := parseMultiPartHeader(value)
@ -509,18 +377,7 @@ func parseEMLContentTypeCharset(mailHeader *netmail.Header, msg *Msg) {
}
}
// 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.
// handleEMLMultiPartBase64Encoding sets the content body of a base64 encoded Part
func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error {
part.SetEncoding(EncodingB64)
content, err := base64.StdEncoding.DecodeString(string(multiPartData))
@ -531,17 +388,8 @@ func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error {
return nil
}
// 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.
// parseMultiPartHeader parses a multipart header and returns the value and optional parts as
// separate map
func parseMultiPartHeader(multiPartHeader string) (header string, optional map[string]string) {
optional = make(map[string]string)
headerSplit := strings.SplitN(multiPartHeader, ";", 2)
@ -556,20 +404,7 @@ func parseMultiPartHeader(multiPartHeader string) (header string, optional map[s
return
}
// 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.
// parseEMLAttachmentEmbed parses a multipart that is an attachment or embed
func parseEMLAttachmentEmbed(contentDisposition []string, multiPart *multipart.Part, msg *Msg) error {
cdType, optional := parseMultiPartHeader(contentDisposition[0])
filename := "generic.attachment"

View file

@ -4,226 +4,173 @@
package mail
// Charset is a type wrapper for a string representing different character encodings.
// Charset represents a character set for the encoding
type Charset string
// ContentType is a type wrapper for a string and represents the MIME type of the content being handled.
// ContentType represents a content type for the Msg
type ContentType string
// Encoding is a type wrapper for a string and represents the type of encoding used for email messages
// and/or parts.
// Encoding represents a MIME encoding scheme like quoted-printable or Base64.
type Encoding string
// MIMEVersion is a type wrapper for a string nad represents the MIME version used in email messages.
// MIMEVersion represents the MIME version for the mail
type MIMEVersion string
// MIMEType is a type wrapper for a string and represents the MIME type for the Msg content or parts.
// MIMEType represents the MIME type for the mail
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 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 avoids any character encoding (except of the mail headers)
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"
)
// MIME10 represents the MIME version "1.0" used in email messages.
const MIME10 MIMEVersion = "1.0"
// List of MIME versions
const (
// TypeAppOctetStream represents the MIME type for arbitrary binary data.
TypeAppOctetStream ContentType = "application/octet-stream"
// MIME10 is the MIME Version 1.0
MIME10 MIMEVersion = "1.0"
)
// TypeMultipartAlternative represents the MIME type for a message body that can contain multiple alternative
// formats.
// List of common content types
const (
TypeAppOctetStream ContentType = "application/octet-stream"
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"`
TypeMultipartMixed ContentType = "multipart/mixed"
TypeMultipartRelated ContentType = "multipart/related"
TypePGPSignature ContentType = "application/pgp-signature"
TypePGPEncrypted ContentType = "application/pgp-encrypted"
TypeTextHTML ContentType = "text/html"
TypeTextPlain ContentType = "text/plain"
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`
MIMESMime MIMEType = `signed; protocol="application/pkcs7-signature"; micalg=sha256`
)
// 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.
// String is a standard method to convert an Charset into a printable format
func (c Charset) String() string {
return string(c)
}
// 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.
// String is a standard method to convert an ContentType into a printable format
func (c ContentType) String() string {
return string(c)
}
// 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.
// String is a standard method to convert an Encoding into a printable format
func (e Encoding) String() string {
return string(e)
}
// String is a standard method to convert an MIMEType into a printable format
func (e MIMEType) String() string {
return string(e)
}

View file

@ -126,24 +126,3 @@ func TestCharset_String(t *testing.T) {
})
}
}
// TestContentType_String tests the mime type method of the MIMEType object
func TestMimeType_String(t *testing.T) {
tests := []struct {
mt MIMEType
want string
}{
{MIMEAlternative, "alternative"},
{MIMEMixed, "mixed"},
{MIMERelated, "related"},
{MIMESMime, `signed; protocol="application/pkcs7-signature"; micalg=sha-256`},
}
for _, tt := range tests {
t.Run(tt.mt.String(), func(t *testing.T) {
if tt.mt.String() != tt.want {
t.Errorf("wrong string for mime type returned. Expected: %s, got: %s",
tt.want, tt.mt.String())
}
})
}
}

97
file.go
View file

@ -9,15 +9,10 @@ import (
"net/textproto"
)
// FileOption is a function type used to modify properties of a File
// FileOption returns a function that can be used for grouping File options
type FileOption func(*File)
// 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.
// File is an attachment or embedded file of the Msg
type File struct {
ContentType ContentType
Desc string
@ -27,68 +22,32 @@ type File struct {
Writer func(w io.Writer) (int64, error)
}
// 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.
// WithFileContentID sets the Content-ID header for the File
func WithFileContentID(id string) FileOption {
return func(f *File) {
f.Header.Set(HeaderContentID.String(), id)
}
}
// 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.
// WithFileName sets the filename of the File
func WithFileName(name string) FileOption {
return func(f *File) {
f.Name = name
}
}
// 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.
// WithFileDescription sets an optional file description of the File that will be
// added as Content-Description part
func WithFileDescription(description string) FileOption {
return func(f *File) {
f.Desc = description
}
}
// 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.
// 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
func WithFileEncoding(encoding Encoding) FileOption {
return func(f *File) {
if encoding == EncodingQP {
@ -99,45 +58,23 @@ func WithFileEncoding(encoding Encoding) FileOption {
}
// WithFileContentType sets the content type of the File.
//
// 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.
// 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
func WithFileContentType(contentType ContentType) FileOption {
return func(f *File) {
f.ContentType = contentType
}
}
// 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.
// setHeader sets header fields to a File
func (f *File) setHeader(header Header, value string) {
f.Header.Set(string(header), value)
}
// 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).
// getHeader return header fields of a File
func (f *File) getHeader(header Header) (string, bool) {
v := f.Header.Get(string(header))
return v, v != ""

6
go.mod
View file

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

66
go.sum
View file

@ -1,68 +1,2 @@
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=

View file

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

133
header.go
View file

@ -4,146 +4,129 @@
package mail
// Header is a type wrapper for a string and represents email header fields in a Msg.
// Header represents a generic mail header field name
type Header string
// AddrHeader is a type wrapper for a string and represents email address headers fields in a Msg.
// AddrHeader represents a address related mail Header field name
type AddrHeader string
// Importance is a type wrapper for an int and represents the level of importance or priority for a Msg.
// Importance represents a Importance/Priority value string
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).
// https://datatracker.ietf.org/doc/html/rfc2110#section-4.3
// HeaderContentLocation is the "Content-Location" header (RFC 2110)
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.
// https://datatracker.ietf.org/doc/html/rfc822#section-5.1
// HeaderDate represents the "Date" field
// See: https://www.rfc-editor.org/rfc/rfc822#section-5.1
HeaderDate Header = "Date"
// HeaderDispositionNotificationTo is the MDN header as described in RFC 8098.
// https://datatracker.ietf.org/doc/html/rfc8098#section-2.1
// HeaderDispositionNotificationTo is the MDN header as described in RFC8098
// See: https://www.rfc-editor.org/rfc/rfc8098.html#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.
// https://datatracker.ietf.org/doc/html/rfc1036#section-2.1.5
// HeaderMessageID represents the "Message-ID" field for message identification
// See: https://www.rfc-editor.org/rfc/rfc1036#section-2.1.5
HeaderMessageID Header = "Message-ID"
// HeaderMIMEVersion represents the "MIME-Version" field as per RFC 2045.
// https://datatracker.ietf.org/doc/html/rfc2045#section-4
// HeaderMIMEVersion represents the "MIME-Version" field as per RFC 2045
// See: 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 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 is the envelope FROM header field
// It's not included in the mail body but only used by the Client for the envelope
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 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.
// NumString returns the importance number string based on the Importance
func (i Importance) NumString() string {
switch i {
case ImportanceNonUrgent:
@ -159,14 +142,7 @@ func (i Importance) NumString() string {
}
}
// 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.
// XPrioString returns the X-Priority number string based on the Importance
func (i Importance) XPrioString() string {
switch i {
case ImportanceNonUrgent:
@ -182,14 +158,7 @@ func (i Importance) XPrioString() string {
}
}
// 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.
// String returns the importance string based on the Importance
func (i Importance) String() string {
switch i {
case ImportanceNonUrgent:
@ -205,20 +174,12 @@ func (i Importance) String() string {
}
}
// String satisfies the fmt.Stringer interface for the Header type and returns the string
// representation of the Header.
//
// Returns:
// - A string representing the Header.
// String returns the header string based on the given Header
func (h Header) String() string {
return string(h)
}
// String satisfies the fmt.Stringer interface for the AddrHeader type and returns the string
// representation of the AddrHeader.
//
// Returns:
// - A string representing the AddrHeader.
// String returns the address header string based on the given AddrHeader
func (a AddrHeader) String() string {
return string(a)
}

2087
msg.go

File diff suppressed because it is too large Load diff

View file

@ -786,8 +786,8 @@ func TestMsg_SetMessageIDWithValue(t *testing.T) {
// TestMsg_SetMessageIDRandomness tests the randomness of Msg.SetMessageID methods
func TestMsg_SetMessageIDRandomness(t *testing.T) {
var mids []string
m := NewMsg()
for i := 0; i < 50_000; i++ {
m := NewMsg()
m.SetMessageID()
mid := m.GetMessageID()
mids = append(mids, mid)
@ -1924,22 +1924,6 @@ func TestMsg_hasAltWithSMime(t *testing.T) {
}
}
// TestMsg_hasSMime tests the hasSMime() method of the Msg
func TestMsg_hasSMime(t *testing.T) {
keyPair, err := getDummyCertificate()
if err != nil {
t.Errorf("failed to load dummy certificate. Cause: %v", err)
}
m := NewMsg()
if err := m.SignWithSMime(keyPair); err != nil {
t.Errorf("set of certificate was not successful")
}
m.SetBodyString(TypeTextPlain, "Plain")
if !m.hasSMime() {
t.Errorf("mail has smime configured but hasSMime() returned true")
}
}
// TestMsg_hasRelated tests the hasRelated() method of the Msg
func TestMsg_hasRelated(t *testing.T) {
m := NewMsg()
@ -2007,70 +1991,6 @@ func TestMsg_WriteToSkipMiddleware(t *testing.T) {
}
}
// TestMsg_WriteToWithSMIME tests the WriteTo() method of the Msg
func TestMsg_WriteToWithSMIME(t *testing.T) {
keyPair, err := getDummyCertificate()
if err != nil {
t.Errorf("failed to load dummy certificate. Cause: %v", err)
}
m := NewMsg()
m.Subject("This is a test")
m.SetBodyString(TypeTextPlain, "Plain")
if err := m.SignWithSMime(keyPair); err != nil {
t.Errorf("set of certificate was not successful")
}
wbuf := bytes.Buffer{}
if _, err = m.WriteTo(&wbuf); err != nil {
t.Errorf("WriteTo() failed: %s", err)
}
result := wbuf.String()
boundary := result[strings.LastIndex(result, "--")-60 : strings.LastIndex(result, "--")]
if strings.Count(result, boundary) != 4 {
t.Errorf("WriteTo() failed. False number of boundaries found")
}
parts := strings.Split(result, fmt.Sprintf("--%s", boundary))
if len(parts) != 4 {
t.Errorf("WriteTo() failed. False number of parts found")
}
preamble := parts[0]
if !strings.Contains(preamble, "MIME-Version: 1.0") {
t.Errorf("WriteTo() failed. Unable to find MIME-Version")
}
if !strings.Contains(preamble, "Subject: This is a test") {
t.Errorf("WriteTo() failed. Unable to find subject")
}
if !strings.Contains(preamble, fmt.Sprintf("Content-Type: multipart/signed; protocol=\"application/pkcs7-signature\"; micalg=sha-256;\r\n boundary=%s", boundary)) {
t.Errorf("WriteTo() failed. Unable to find Content-Type")
}
signedData := parts[1]
if !strings.Contains(signedData, "Content-Transfer-Encoding: quoted-printable") {
t.Errorf("WriteTo() failed. Unable to find Content-Transfer-Encoding")
}
if !strings.Contains(signedData, "Content-Type: text/plain; charset=UTF-8") {
t.Errorf("WriteTo() failed. Unable to find Content-Type")
}
if !strings.Contains(signedData, "Plain") {
t.Errorf("WriteTo() failed. Unable to find Content")
}
signature := parts[2]
if !strings.Contains(signature, "Content-Transfer-Encoding: base64") {
t.Errorf("WriteTo() failed. Unable to find Content-Transfer-Encoding")
}
if !strings.Contains(signature, `application/pkcs7-signature; name="smime.p7s"`) {
t.Errorf("WriteTo() failed. Unable to find Content-Type")
}
if strings.Contains(signature, "Plain") {
t.Errorf("WriteTo() failed. Unable to find signature")
}
}
// TestMsg_WriteTo_fails tests the WriteTo() method of the Msg but with a failing body writer function
func TestMsg_WriteTo_fails(t *testing.T) {
m := NewMsg()
@ -3359,6 +3279,9 @@ func TestSignWithSMime_ValidKeyPair(t *testing.T) {
if m.sMime.certificate == nil {
t.Errorf("WithSMimeSinging() - no certificate is given")
}
if len(m.sMime.parentCertificates) != len(keyPair.Certificate[:1]) {
t.Errorf("WithSMimeSinging() - no certificate is given")
}
}
// TestSignWithSMime_InvalidKeyPair tests WithSMimeSinging with given invalid key pair

View file

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

View file

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

View file

@ -18,39 +18,22 @@ import (
"strings"
)
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
// MaxHeaderLength defines the maximum line length for a mail header
// RFC 2047 suggests 76 characters
const MaxHeaderLength = 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
// MaxBodyLength defines the maximum line length for the mail body
// RFC 2047 suggests 76 characters
const MaxBodyLength = 76
// 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"
// SingleNewLine represents a new line that can be used by the msgWriter to issue a carriage return
const SingleNewLine = "\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"
)
// 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"
// 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.
// msgWriter handles the I/O to the io.WriteCloser of the SMTP client
type msgWriter struct {
bytesWritten int64
charset Charset
@ -62,18 +45,7 @@ type msgWriter struct {
writer io.Writer
}
// 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.
// Write implements the io.Writer interface for msgWriter
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)
@ -85,19 +57,7 @@ func (mw *msgWriter) Write(payload []byte) (int, error) {
return n, mw.err
}
// 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)
// writeMsg formats the message and sends it to its io.Writer
func (mw *msgWriter) writeMsg(msg *Msg) {
msg.addDefaultHeader()
msg.checkUserAgent()
@ -184,13 +144,7 @@ func (mw *msgWriter) writeMsg(msg *Msg) {
}
}
// 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.
// writeGenHeader writes out all generic headers to the msgWriter
func (mw *msgWriter) writeGenHeader(msg *Msg) {
keys := make([]string, 0, len(msg.genHeader))
for key := range msg.genHeader {
@ -202,32 +156,14 @@ func (mw *msgWriter) writeGenHeader(msg *Msg) {
}
}
// 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.
// writePreformatedHeader writes out all preformated generic headers to the msgWriter
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.
//
// 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
// startMP writes a multipart beginning
func (mw *msgWriter) startMP(mimeType MIMEType, boundary string) {
multiPartWriter := multipart.NewWriter(mw)
if boundary != "" {
@ -247,10 +183,7 @@ func (mw *msgWriter) startMP(mimeType MIMEType, boundary string) {
mw.depth++
}
// 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.
// stopMP closes the multipart
func (mw *msgWriter) stopMP() {
if mw.depth > 0 {
mw.err = mw.multiPartWriter[mw.depth-1].Close()
@ -258,17 +191,7 @@ func (mw *msgWriter) stopMP() {
}
}
// 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).
// addFiles adds the attachments/embeds file content to the mail body
func (mw *msgWriter) addFiles(files []*File, isAttachment bool) {
for _, file := range files {
encoding := EncodingB64
@ -327,40 +250,18 @@ func (mw *msgWriter) addFiles(files []*File, isAttachment bool) {
}
}
// 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.
// newPart creates a new MIME multipart io.Writer and sets the partwriter to it
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.
//
// 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.
// writePart writes the corresponding part to the Msg body
func (mw *msgWriter) writePart(part *Part, charset Charset) {
partCharset := part.charset
if partCharset.String() == "" {
partCharset = charset
}
contentType := part.contentType.String()
if !part.IsSMimeSigned() {
contentType = strings.Join([]string{contentType, "; charset=", partCharset.String()}, "")
}
contentType := fmt.Sprintf("%s; charset=%s", part.contentType, partCharset)
contentTransferEnc := part.encoding.String()
if mw.depth == 0 {
mw.writeHeader(HeaderContentType, contentType)
@ -379,14 +280,7 @@ 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.
//
// 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.
// writeString writes a string into the msgWriter's io.Writer interface
func (mw *msgWriter) writeString(s string) {
if mw.err != nil {
return
@ -396,16 +290,7 @@ func (mw *msgWriter) writeString(s string) {
mw.bytesWritten += int64(n)
}
// 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.
// writeHeader writes a header into the msgWriter's io.Writer
func (mw *msgWriter) writeHeader(key Header, values ...string) {
buffer := strings.Builder{}
charLength := MaxHeaderLength - 2
@ -440,18 +325,7 @@ func (mw *msgWriter) writeHeader(key Header, values ...string) {
mw.writeString("\r\n")
}
// 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.
// writeBody writes an io.Reader into an io.Writer using provided Encoding
func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encoding Encoding, singingWithSMime bool) {
var writer io.Writer
var encodedWriter io.WriteCloser

View file

@ -161,7 +161,6 @@ func TestMsgWriter_writeMsg_SMime(t *testing.T) {
if err != nil {
t.Errorf("failed to load dummy certificate. Cause: %v", err)
}
m := NewMsg()
if err := m.SignWithSMime(keyPair); err != nil {
t.Errorf("set of certificate was not successful")
@ -174,22 +173,7 @@ func TestMsgWriter_writeMsg_SMime(t *testing.T) {
mw := &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding}
mw.writeMsg(m)
ms := buf.String()
if !strings.Contains(ms, "MIME-Version: 1.0") {
t.Errorf("writeMsg failed. Unable to find MIME-Version")
}
if !strings.Contains(ms, "Subject: This is a subject") {
t.Errorf("writeMsg failed. Unable to find subject")
}
if !strings.Contains(ms, "From: \"Toni Tester\" <test@example.com>") {
t.Errorf("writeMsg failed. Unable to find transmitter")
}
if !strings.Contains(ms, "To: \"Toni Receiver\" <receiver@example.com>") {
t.Errorf("writeMsg failed. Unable to find receiver")
}
boundary := ms[strings.LastIndex(ms, "--")-60 : strings.LastIndex(ms, "--")]
if !strings.Contains(ms, fmt.Sprintf("Content-Type: multipart/signed; protocol=\"application/pkcs7-signature\"; micalg=sha-256;\r\n boundary=%s", boundary)) {
t.Errorf("writeMsg failed. Unable to find Content-Type")
if !strings.Contains(ms, `multipart/signed; protocol="application/pkcs7-signature"; micalg=sha256;`) {
t.Errorf("writeMsg failed. Expected PGP encoding header but didn't find it in message output")
}
}

137
part.go
View file

@ -12,11 +12,7 @@ import (
// PartOption returns a function that can be used for grouping Part options
type PartOption func(*Part)
// 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.
// Part is a part of the Msg
type Part struct {
contentType ContentType
charset Charset
@ -27,14 +23,7 @@ type Part struct {
smime bool
}
// 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.
// GetContent executes the WriteFunc of the Part and returns the content as byte slice
func (p *Part) GetContent() ([]byte, error) {
var b bytes.Buffer
if _, err := p.writeFunc(&b); err != nil {
@ -43,54 +32,27 @@ func (p *Part) GetContent() ([]byte, error) {
return b.Bytes(), nil
}
// 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.
// GetCharset returns the currently set Charset of the Part
func (p *Part) GetCharset() Charset {
return p.charset
}
// 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.
// GetContentType returns the currently set ContentType of the Part
func (p *Part) GetContentType() ContentType {
return p.contentType
}
// 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.
// GetEncoding returns the currently set Encoding of the Part
func (p *Part) GetEncoding() Encoding {
return p.encoding
}
// 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).
// GetWriteFunc returns the currently set WriterFunc of the Part
func (p *Part) GetWriteFunc() func(io.Writer) (int64, error) {
return p.writeFunc
}
// 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.
// GetDescription returns the currently set Content-Description of the Part
func (p *Part) GetDescription() string {
return p.description
}
@ -100,126 +62,63 @@ func (p *Part) IsSMimeSigned() bool {
return p.smime
}
// SetContent overrides the content of the Part with the given string.
//
// This function sets the content of the Part by creating a new writeFunc that writes the
// provided string content to an io.Writer.
//
// Parameters:
// - content: The string that will replace the current content of the Part.
// SetContent overrides the content of the Part with the given string
func (p *Part) SetContent(content string) {
buffer := bytes.NewBufferString(content)
p.writeFunc = writeFuncFromBuffer(buffer)
}
// 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.
// SetContentType overrides the ContentType of the Part
func (p *Part) SetContentType(contentType ContentType) {
p.contentType = contentType
}
// 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.
// SetCharset overrides the Charset of 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.
//
// This function sets a new Encoding for the Part, replacing the existing one.
//
// Parameters:
// - encoding: The new Encoding to be set for the Part.
// SetEncoding creates a new mime.WordEncoder based on the encoding setting of the message
func (p *Part) SetEncoding(encoding Encoding) {
p.encoding = encoding
}
// 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.
// SetDescription overrides the Content-Description of 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.
//
// 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).
// SetWriteFunc overrides the WriteFunc of the Part
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.
//
// This function marks the Part as deleted by setting the isDeleted flag to true. The msgWriter
// will skip over this Part during processing.
// 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
func (p *Part) Delete() {
p.isDeleted = true
}
// 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.
// WithPartCharset overrides the default Part charset
func WithPartCharset(charset Charset) PartOption {
return func(p *Part) {
p.charset = charset
}
}
// 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.
// WithPartEncoding overrides the default Part encoding
func WithPartEncoding(encoding Encoding) PartOption {
return func(p *Part) {
p.encoding = encoding
}
}
// 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.
// WithPartContentDescription overrides the default Part Content-Description
func WithPartContentDescription(description string) PartOption {
return func(p *Part) {
p.description = description

View file

@ -11,37 +11,17 @@ import (
)
// Range of characters for the secure string generation
const cr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-"
const cr = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
// 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: 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
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
)
// 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.
// randomStringSecure returns a random, string of length characters. This method uses the
// crypto/random package and therfore is cryptographically secure
func randomStringSecure(length int) (string, error) {
randString := strings.Builder{}
randString.Grow(length)

22
random_119.go Normal file
View file

@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
//
// SPDX-License-Identifier: MIT
//go:build !go1.20
// +build !go1.20
package mail
import (
"math/rand"
"time"
)
// randNum returns a random number with a maximum value of length
func randNum(maxval int) int {
if maxval <= 0 {
return 0
}
rand.Seed(time.Now().UnixNano())
return rand.Intn(maxval)
}

20
random_121.go Normal file
View file

@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
//
// SPDX-License-Identifier: MIT
//go:build go1.20 && !go1.22
// +build go1.20,!go1.22
package mail
import (
"math/rand"
)
// randNum returns a random number with a maximum value of length
func randNum(maxval int) int {
if maxval <= 0 {
return 0
}
return rand.Intn(maxval)
}

22
random_122.go Normal file
View file

@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
//
// SPDX-License-Identifier: MIT
//go:build go1.22
// +build go1.22
package mail
import (
"math/rand/v2"
)
// randNum returns a random number with a maximum value of maxval.
// go-mail compiled with Go 1.22+ will make use of the novel math/rand/v2 interface
// Older versions of Go will use math/rand
func randNum(maxval int) int {
if maxval <= 0 {
return 0
}
return rand.IntN(maxval)
}

View file

@ -38,12 +38,34 @@ func TestRandomStringSecure(t *testing.T) {
}
}
func BenchmarkGenerator_RandomStringSecure(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, err := randomStringSecure(22)
if err != nil {
b.Errorf("RandomStringFromCharRange() failed: %s", err)
}
// 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 := randNum(tc.max)
if rn > tc.max {
t.Errorf("random number generation failed: %d is bigger than given value %d", rn, tc.max)
}
})
}
}
func TestRandomNumZero(t *testing.T) {
rn := randNum(0)
if rn != 0 {
t.Errorf("random number generation failed: %d is not zero", rn)
}
}

View file

@ -8,41 +8,19 @@ import (
"io"
)
// 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.
// Reader is a type that implements the io.Reader interface for a Msg
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.
//
// 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.
// Error returns an error if the Reader err field is not nil
func (r *Reader) Error() error {
return r.err
}
// 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.
// Read reads the length of p of the Msg buffer to satisfy the io.Reader interface
func (r *Reader) Read(payload []byte) (n int, err error) {
if r.err != nil {
return 0, r.err
@ -59,20 +37,12 @@ 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 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.
// Reset resets the Reader buffer to be empty, but it retains the underlying storage
// for use by future writes.
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,11 +54,7 @@ const (
ErrAmbiguous
)
// 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.
// SendError is an error wrapper for delivery errors of the Msg
type SendError struct {
affectedMsg *Msg
errlist []error
@ -70,16 +66,7 @@ type SendError struct {
// SendErrReason represents a comparable reason on why the delivery failed
type SendErrReason int
// 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.
// Error implements the error interface for the SendError type
func (e *SendError) Error() string {
if e.Reason > 10 {
return "unknown reason"
@ -114,17 +101,7 @@ func (e *SendError) Error() string {
return errMessage.String()
}
// 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.
// Is implements the errors.Is functionality and compares the SendErrReason
func (e *SendError) Is(errType error) bool {
var t *SendError
if errors.As(errType, &t) && t != nil {
@ -133,13 +110,7 @@ func (e *SendError) Is(errType error) bool {
return false
}
// 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.
// IsTemp returns true if the delivery error is of temporary nature and can be retried
func (e *SendError) IsTemp() bool {
if e == nil {
return false
@ -147,13 +118,8 @@ func (e *SendError) IsTemp() bool {
return e.isTemp
}
// 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.
// MessageID returns the message ID of the affected Msg that caused the error
// If no message ID was set for the Msg, an empty string will be returned
func (e *SendError) MessageID() string {
if e == nil || e.affectedMsg == nil {
return ""
@ -161,13 +127,7 @@ func (e *SendError) MessageID() string {
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.
// Msg returns the pointer to the affected message that caused the error
func (e *SendError) Msg() *Msg {
if e == nil || e.affectedMsg == nil {
return nil
@ -175,14 +135,7 @@ func (e *SendError) Msg() *Msg {
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.
// String implements the Stringer interface for the SendErrReason
func (r SendErrReason) String() string {
switch r {
case ErrGetSender:
@ -211,16 +164,8 @@ func (r SendErrReason) String() string {
return "unknown reason"
}
// 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.
// isTempError checks the given SMTP error and returns true if the given error is of temporary nature
// and should be retried
func isTempError(err error) bool {
return err.Error()[0] == '4'
}

View file

@ -16,6 +16,9 @@ var (
// ErrInvalidKeyPair should be used if key pair is invalid
ErrInvalidKeyPair = errors.New("invalid key pair")
// ErrInvalidCertificate should be used if a certificate is invalid
ErrInvalidCertificate = errors.New("invalid certificate")
// ErrCouldNotInitialize should be used if the signed data could not initialize
ErrCouldNotInitialize = errors.New("could not initialize signed data")
@ -31,8 +34,9 @@ var (
// SMime is used to sign messages with S/MIME
type SMime struct {
privateKey *rsa.PrivateKey
certificate *x509.Certificate
privateKey *rsa.PrivateKey
certificate *x509.Certificate
parentCertificates []*x509.Certificate
}
// NewSMime construct a new instance of SMime with a provided *tls.Certificate
@ -41,44 +45,53 @@ func newSMime(keyPair *tls.Certificate) (*SMime, error) {
return nil, ErrInvalidKeyPair
}
parentCertificates := make([]*x509.Certificate, 0)
for _, cert := range keyPair.Certificate[1:] {
c, err := x509.ParseCertificate(cert)
if err != nil {
return nil, ErrInvalidCertificate
}
parentCertificates = append(parentCertificates, c)
}
return &SMime{
privateKey: keyPair.PrivateKey.(*rsa.PrivateKey),
certificate: keyPair.Leaf,
privateKey: keyPair.PrivateKey.(*rsa.PrivateKey),
certificate: keyPair.Leaf,
parentCertificates: parentCertificates,
}, nil
}
// signMessage signs the message with S/MIME
func (sm *SMime) signMessage(message string) (*string, error) {
// sign with the S/MIME method the message of the actual *Part
func (sm *SMime) sign(signaturePart *Part, message string) error {
lines := parseLines([]byte(message))
toBeSigned := lines.bytesFromLines([]byte("\r\n"))
signedData, err := pkcs7.NewSignedData(toBeSigned)
signedData.SetDigestAlgorithm(pkcs7.OIDDigestAlgorithmSHA256)
tmp, err := pkcs7.NewSignedData(toBeSigned)
tmp.SetDigestAlgorithm(pkcs7.OIDDigestAlgorithmSHA256)
if err != nil {
return nil, ErrCouldNotInitialize
return ErrCouldNotInitialize
}
if err = signedData.AddSigner(sm.certificate, sm.privateKey, pkcs7.SignerInfoConfig{}); err != nil {
return nil, ErrCouldNotAddSigner
if err = tmp.AddSignerChain(sm.certificate, sm.privateKey, sm.parentCertificates, pkcs7.SignerInfoConfig{}); err != nil {
return ErrCouldNotAddSigner
}
signedData.Detach()
signatureDER, err := signedData.Finish()
signatureDER, err := tmp.Finish()
if err != nil {
return nil, ErrCouldNotFinishSigning
return ErrCouldNotFinishSigning
}
pemMsg, err := encodeToPEM(signatureDER)
if err != nil {
return nil, ErrCouldNoEncodeToPEM
return ErrCouldNoEncodeToPEM
}
signaturePart.SetContent(*pemMsg)
return pemMsg, nil
return nil
}
// createMessage prepares the message that will be used for the sign method later
func (sm *SMime) prepareMessage(encoding Encoding, contentType ContentType, charset Charset, body []byte) string {
func (sm *SMime) createMessage(encoding Encoding, contentType ContentType, charset Charset, body []byte) string {
return fmt.Sprintf("Content-Transfer-Encoding: %v\r\nContent-Type: %v; charset=%v\r\n\r\n%v", encoding, contentType, charset, string(body))
}

View file

@ -1,190 +0,0 @@
package mail
import (
"bytes"
"encoding/base64"
"fmt"
"strings"
"testing"
)
// TestNewSMime tests the newSMime method
func TestNewSMime(t *testing.T) {
keyPair, err := getDummyCertificate()
if err != nil {
t.Errorf("Error getting dummy certificate: %s", err)
}
sMime, err := newSMime(keyPair)
if err != nil {
t.Errorf("Error creating new SMime from keyPair: %s", err)
}
if sMime.privateKey != keyPair.PrivateKey {
t.Errorf("NewSMime() did not return the same private key")
}
if sMime.certificate != keyPair.Leaf {
t.Errorf("NewSMime() did not return the same leaf certificate")
}
}
// TestSign tests the sign method
func TestSign(t *testing.T) {
keyPair, err := getDummyCertificate()
if err != nil {
t.Errorf("Error getting dummy certificate: %s", err)
}
sMime, err := newSMime(keyPair)
if err != nil {
t.Errorf("Error creating new SMime from keyPair: %s", err)
}
message := "This is a test message"
singedMessage, err := sMime.signMessage(message)
if err != nil {
t.Errorf("Error creating singed message: %s", err)
}
if *singedMessage == message {
t.Errorf("Sign() did not work")
}
}
// TestPrepareMessage tests the createMessage method
func TestPrepareMessage(t *testing.T) {
keyPair, err := getDummyCertificate()
if err != nil {
t.Errorf("Error getting dummy certificate: %s", err)
}
sMime, err := newSMime(keyPair)
if err != nil {
t.Errorf("Error creating new SMime from keyPair: %s", err)
}
encoding := EncodingB64
contentType := TypeTextPlain
charset := CharsetUTF8
body := []byte("This is the body!")
result := sMime.prepareMessage(encoding, contentType, charset, body)
if !strings.Contains(result, encoding.String()) {
t.Errorf("createMessage() did not return the correct encoding")
}
if !strings.Contains(result, contentType.String()) {
t.Errorf("createMessage() did not return the correct contentType")
}
if !strings.Contains(result, string(body)) {
t.Errorf("createMessage() did not return the correct body")
}
if result != fmt.Sprintf("Content-Transfer-Encoding: %s\r\nContent-Type: %s; charset=%s\r\n\r\n%s", encoding, contentType, charset, string(body)) {
t.Errorf("createMessage() did not sucessfully create the message")
}
}
// TestEncodeToPEM tests the encodeToPEM method
func TestEncodeToPEM(t *testing.T) {
message := []byte("This is a test message")
pemMessage, err := encodeToPEM(message)
if err != nil {
t.Errorf("Error encoding message: %s", err)
}
base64Encoded := base64.StdEncoding.EncodeToString(message)
if *pemMessage != base64Encoded {
t.Errorf("encodeToPEM() did not work")
}
}
// TestBytesFromLines tests the bytesFromLines method
func TestBytesFromLines(t *testing.T) {
ls := lines{
{line: []byte("Hello"), endOfLine: []byte("\n")},
{line: []byte("World"), endOfLine: []byte("\n")},
}
expected := []byte("Hello\nWorld\n")
result := ls.bytesFromLines([]byte("\n"))
if !bytes.Equal(result, expected) {
t.Errorf("Expected %s, but got %s", expected, result)
}
}
// FuzzBytesFromLines tests the bytesFromLines method with fuzzing
func FuzzBytesFromLines(f *testing.F) {
f.Add([]byte("Hello"), []byte("\n"))
f.Fuzz(func(t *testing.T, lineData, sep []byte) {
ls := lines{
{line: lineData, endOfLine: sep},
}
_ = ls.bytesFromLines(sep)
})
}
// TestParseLines tests the parseLines method
func TestParseLines(t *testing.T) {
input := []byte("Hello\r\nWorld\nHello\rWorld")
expected := lines{
{line: []byte("Hello"), endOfLine: []byte("\r\n")},
{line: []byte("World"), endOfLine: []byte("\n")},
{line: []byte("Hello"), endOfLine: []byte("\r")},
{line: []byte("World"), endOfLine: []byte("")},
}
result := parseLines(input)
if len(result) != len(expected) {
t.Errorf("Expected %d lines, but got %d", len(expected), len(result))
}
for i := range result {
if !bytes.Equal(result[i].line, expected[i].line) || !bytes.Equal(result[i].endOfLine, expected[i].endOfLine) {
t.Errorf("Line %d mismatch. Expected line: %s, endOfLine: %s, got line: %s, endOfLine: %s",
i, expected[i].line, expected[i].endOfLine, result[i].line, result[i].endOfLine)
}
}
}
// FuzzParseLines tests the parseLines method with fuzzing
func FuzzParseLines(f *testing.F) {
f.Add([]byte("Hello\nWorld\r\nAnother\rLine"))
f.Fuzz(func(t *testing.T, input []byte) {
_ = parseLines(input)
})
}
// TestSplitLine tests the splitLine method
func TestSplitLine(t *testing.T) {
ls := lines{
{line: []byte("Hello\r\nWorld\r\nAnotherLine"), endOfLine: []byte("")},
}
expected := lines{
{line: []byte("Hello"), endOfLine: []byte("\r\n")},
{line: []byte("World"), endOfLine: []byte("\r\n")},
{line: []byte("AnotherLine"), endOfLine: []byte("")},
}
result := ls.splitLine([]byte("\r\n"))
if len(result) != len(expected) {
t.Errorf("Expected %d lines, but got %d", len(expected), len(result))
}
for i := range result {
if !bytes.Equal(result[i].line, expected[i].line) || !bytes.Equal(result[i].endOfLine, expected[i].endOfLine) {
t.Errorf("Line %d mismatch. Expected line: %s, endOfLine: %s, got line: %s, endOfLine: %s",
i, expected[i].line, expected[i].endOfLine, result[i].line, result[i].endOfLine)
}
}
}
// FuzzSplitLine tests the parseLsplitLineines method with fuzzing
func FuzzSplitLine(f *testing.F) {
f.Add([]byte("Hello\r\nWorld"), []byte("\r\n"))
f.Fuzz(func(t *testing.T, input, sep []byte) {
ls := lines{
{line: input, endOfLine: []byte("")},
}
_ = ls.splitLine(sep)
})
}

View file

@ -13,19 +13,6 @@
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,10 +1,11 @@
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
// SPDX-FileCopyrightText: Copyright (c) 2022-2023 The go-mail Authors
//
// SPDX-License-Identifier: MIT
package smtp
import (
"errors"
"fmt"
)
@ -12,35 +13,53 @@ 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 might responds with "Username:")
// - Sending the username (server might responds with "Password:")
// - Sending AUTH LOGIN (server responds with "Username:")
// - Sending the username (server 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, 0}
return &loginAuth{username, password, host}
}
// 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.
@ -48,28 +67,23 @@ 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, ErrUnencrypted
return "", nil, errors.New("unencrypted connection")
}
if server.Name != a.host {
return "", nil, ErrWrongHostname
return "", nil, errors.New("wrong host name")
}
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 a.respStep {
case 0:
a.respStep++
switch string(fromServer) {
case LoginXUsernameChallenge, LoginXUsernameLowerChallenge, LoginXDraftUsernameChallenge:
return []byte(a.username), nil
case 1:
a.respStep++
case LoginXPasswordChallenge, LoginXPasswordLowerChallenge, LoginXDraftPasswordChallenge:
return []byte(a.password), nil
default:
return nil, fmt.Errorf("%w: %s", ErrUnexpectedServerResponse, string(fromServer))
return nil, fmt.Errorf("unexpected server response: %s", string(fromServer))
}
}
return nil, nil

View file

@ -13,6 +13,10 @@
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
@ -38,10 +42,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, ErrUnencrypted
return "", nil, errors.New("unencrypted connection")
}
if server.Name != a.host {
return "", nil, ErrWrongHostname
return "", nil, errors.New("wrong host name")
}
resp := []byte(a.identity + "\x00" + a.username + "\x00" + a.password)
return "PLAIN", resp, nil
@ -50,7 +54,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, ErrUnexpectedServerChallange
return nil, errors.New("unexpected server challenge")
}
return nil, nil
}

View file

@ -1,317 +0,0 @@
// 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

@ -36,16 +36,6 @@ import (
"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.
@ -75,9 +65,6 @@ type Client struct {
// 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
@ -124,7 +111,6 @@ 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
}
@ -133,7 +119,6 @@ func NewClient(conn net.Conn, host string) (*Client, error) {
func (c *Client) Close() error {
c.mutex.Lock()
err := c.Text.Close()
c.isConnected = false
c.mutex.Unlock()
return err
}
@ -529,7 +514,6 @@ func (c *Client) Quit() error {
}
c.mutex.Lock()
err = c.Text.Close()
c.isConnected = false
c.mutex.Unlock()
return err
@ -569,41 +553,18 @@ func (c *Client) SetDSNRcptNotifyOption(d string) {
// 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
return c.conn != nil
}
// 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)
}
c.mutex.Unlock()
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{}) {

File diff suppressed because it is too large Load diff

17
tls.go
View file

@ -4,32 +4,25 @@
package mail
// TLSPolicy is a type wrapper for an int type and describes the different TLS policies we allow.
// TLSPolicy type describes a int alias for 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 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.
// String is a standard method to convert a TLSPolicy into a printable format
func (p TLSPolicy) String() string {
switch p {
case TLSMandatory:

View file

@ -10,7 +10,6 @@ const (
keyFilePath = "dummy-child-key.pem"
)
// getDummyCertificate loads a certificate and a private key form local disk for testing purposes
func getDummyCertificate() (*tls.Certificate, error) {
keyPair, err := tls.LoadX509KeyPair(certFilePath, keyFilePath)
if err != nil {