vendored
This commit is contained in:
1 parent
ffa5cd9cd7
commit
f380dfe58d
238 files changed
+151817
-41
No files matched your search
+61
@@ -0,0 +1,61 @@
|
||||
# SPDX-FileCopyrightText: The go-mail Authors
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Local testfiles and auth data
|
||||
.auth
|
||||
examples/*
|
||||
|
||||
# SonarQube
|
||||
.scannerwork/
|
||||
|
||||
# IDEA specific ignores
|
||||
# Source: https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
.idea/
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# SonarLint plugin
|
||||
.idea/sonarlint/
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
## Coverage data
|
||||
coverage.coverprofile
|
||||
coverage.html
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
## SPDX-FileCopyrightText: The go-mail Authors
|
||||
##
|
||||
## SPDX-License-Identifier: MIT
|
||||
|
||||
version = '2'
|
||||
|
||||
[run]
|
||||
go = '1.24'
|
||||
tests = true
|
||||
|
||||
[linters]
|
||||
enable = [
|
||||
'containedctx',
|
||||
'contextcheck',
|
||||
'decorder',
|
||||
'errname',
|
||||
'errorlint',
|
||||
'gosec',
|
||||
'staticcheck',
|
||||
'whitespace'
|
||||
]
|
||||
|
||||
[linters.exclusions]
|
||||
generated = 'lax'
|
||||
presets = [
|
||||
'comments',
|
||||
'common-false-positives',
|
||||
'legacy',
|
||||
'std-error-handling'
|
||||
]
|
||||
paths = [
|
||||
'examples',
|
||||
'third_party$',
|
||||
'builtin$',
|
||||
'examples$'
|
||||
]
|
||||
|
||||
## An overflow is impossible here
|
||||
[[linters.exclusions.rules]]
|
||||
linters = [
|
||||
'gosec'
|
||||
]
|
||||
path = 'random.go'
|
||||
text = 'G115:'
|
||||
|
||||
## These are tests which intentionally do not need any TLS settings
|
||||
[[linters.exclusions.rules]]
|
||||
linters = [
|
||||
'gosec'
|
||||
]
|
||||
path = 'client_test.go'
|
||||
text = 'G402:'
|
||||
|
||||
## These are tests which intentionally do not need any TLS settings
|
||||
[[linters.exclusions.rules]]
|
||||
linters = [
|
||||
'gosec'
|
||||
]
|
||||
path = 'smtp/smtp_test.go'
|
||||
text = 'G402:'
|
||||
|
||||
## We do not dictate a TLS minimum version in the smtp package. go-mail
|
||||
## itself does set sane defaults
|
||||
[[linters.exclusions.rules]]
|
||||
linters = [
|
||||
'gosec'
|
||||
]
|
||||
path = 'smtp/smtp.go'
|
||||
text = 'G402:'
|
||||
|
||||
## The chance that we write +2 million tests is very low, I think we can
|
||||
## ignore this for the time being
|
||||
[[linters.exclusions.rules]]
|
||||
linters = [
|
||||
'gosec'
|
||||
]
|
||||
path = 'client_test.go'
|
||||
text = 'G109:'
|
||||
|
||||
## The chance that we write +2 million tests is very low, I think we can
|
||||
## ignore this for the time being
|
||||
[[linters.exclusions.rules]]
|
||||
linters = [
|
||||
'gosec'
|
||||
]
|
||||
path = 'smtp/smtp_test.go'
|
||||
text = 'G109:'
|
||||
|
||||
## We inform the user about the deprecated status of CRAM-MD5 and suggest
|
||||
## to use SCRAM-SHA instead
|
||||
[[linters.exclusions.rules]]
|
||||
linters = [
|
||||
'gosec'
|
||||
]
|
||||
path = 'smtp/auth_cram_md5.go'
|
||||
text = 'G501:'
|
||||
|
||||
## Yes, SHA1 is weak, but in the context of SCRAM it is still considered
|
||||
## secure for specific applications. The user is information about this
|
||||
## in the documentation
|
||||
[[linters.exclusions.rules]]
|
||||
linters = [
|
||||
'gosec'
|
||||
]
|
||||
path = 'smtp/auth_scram.go'
|
||||
text = 'G505:'
|
||||
|
||||
## Test code for SCRAM-SHA1. Can be ignored.
|
||||
[[linters.exclusions.rules]]
|
||||
linters = [
|
||||
'gosec'
|
||||
]
|
||||
path = 'smtp/smtp_test.go'
|
||||
text = 'G505:'
|
||||
|
||||
## These are tests which intentionally do not need any TLS settings
|
||||
[[linters.exclusions.rules]]
|
||||
linters = [
|
||||
'gosec'
|
||||
]
|
||||
path = 'quicksend_test.go'
|
||||
text = 'G402:'
|
||||
|
||||
## These are tests which intentionally test SHA1 and SHA256
|
||||
[[linters.exclusions.rules]]
|
||||
linters = [
|
||||
'gosec'
|
||||
]
|
||||
path = 'internal/pbkdf2/pbkdf2_test.go'
|
||||
text = 'G505:'
|
||||
|
||||
[formatters]
|
||||
enable = [
|
||||
'gofmt',
|
||||
'gofumpt'
|
||||
]
|
||||
|
||||
[formatters.exclusions]
|
||||
generated = 'lax'
|
||||
paths = [
|
||||
'examples',
|
||||
'third_party$',
|
||||
'builtin$',
|
||||
'examples$'
|
||||
]
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: Winni Neessen <winni@neessen.dev>
|
||||
|
||||
SPDX-License-Identifier: MIT
|
||||
-->
|
||||
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
github+coc@neessen.net.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: The go-mail Authors
|
||||
|
||||
SPDX-License-Identifier: MIT
|
||||
-->
|
||||
|
||||
# How to contribute
|
||||
|
||||
**Working on your first Pull Request?** You can learn how from this *free* series [How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github)
|
||||
|
||||
## Guidelines for Pull Requests
|
||||
|
||||
How to get your contributions merged smoothly and quickly.
|
||||
|
||||
* Create **small PRs** that are narrowly focused on **addressing a single concern**. We often times receive PRs that are trying to fix several things at a time, but only one fix is considered acceptable, nothing gets merged and both author's & review's time is wasted. Create more PRs to address different concerns and everyone will be happy.
|
||||
|
||||
* For speculative changes, consider opening an issue and discussing it first.
|
||||
|
||||
* Provide a good **PR description** as a record of **what** change is being made and **why** it was made. Link to a github issue if it exists.
|
||||
|
||||
* Unless your PR is trivial, you should expect there will be reviewer comments that you'll need to address before merging. We expect you to be reasonably responsive to those comments, otherwise the PR will be closed after 2-3 weeks of inactivity.
|
||||
|
||||
* Maintain **clean commit history** and use **meaningful commit messages**. PRs with messy commit history are difficult to review and won't be merged. Use `rebase -i upstream/main` to curate your commit history and/or to bring in latest changes from main (but avoid rebasing in the middle of a code review).
|
||||
|
||||
* Keep your PR up to date with upstream/main (if there are merge conflicts, we can't really merge your change).
|
||||
|
||||
* Exceptions to the rules can be made if there's a compelling reason for doing so.
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022-2025 The go-mail Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: The go-mail Authors
|
||||
|
||||
SPDX-License-Identifier: MIT
|
||||
-->
|
||||
|
||||
# go-mail - Easy to use, yet comprehensive library for sending mails with Go
|
||||
|
||||
[](https://pkg.go.dev/github.com/wneessen/go-mail)
|
||||
[](https://codecov.io/gh/wneessen/go-mail)
|
||||
[](https://goreportcard.com/report/github.com/wneessen/go-mail)
|
||||
[](https://github.com/avelino/awesome-go)
|
||||
[](https://discord.gg/ysQXkaccXk)
|
||||
[](https://api.reuse.software/info/github.com/wneessen/go-mail)
|
||||
[](https://www.bestpractices.dev/projects/8701)
|
||||
[](https://securityscorecards.dev/viewer/?uri=github.com/wneessen/go-mail)
|
||||
<a href="https://ko-fi.com/D1D24V9IX"><img src="https://uploads-ssl.webflow.com/5c14e387dab576fe667689cf/5cbed8a4ae2b88347c06c923_BuyMeACoffee_blue.png" height="20" alt="buy ma a coffee"></a>
|
||||
|
||||
<p style="text-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
|
||||
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.
|
||||
|
||||
In the early days, parts of this library (especially some parts of [msgwriter.go](msgwriter.go)) had been
|
||||
forked/ported from [go-mail/mail](https://github.com/go-mail/mail) and respectively [go-gomail/gomail](https://github.com/go-gomail/gomail). Today
|
||||
most of the ported code has been refactored.
|
||||
|
||||
The `smtp` package of go-mail has been forked from the original Go stdlib's `net/smtp` package and has then been extended
|
||||
by the go-mail team to fit the packages needs (more SMTP Auth methods, logging, concurrency-safety, etc.).
|
||||
|
||||
## Features
|
||||
|
||||
Here are some highlights of go-mail's featureset:
|
||||
|
||||
* [X] Very small dependency footprint (mainly Go Stdlib and Go extended packages)
|
||||
* [X] Modern, idiomatic Go
|
||||
* [X] Sane and secure defaults
|
||||
* [X] Implicit SSL/TLS support
|
||||
* [X] Explicit STARTTLS support with different policies
|
||||
* [X] Makes use of contexts for a better control flow and timeout/cancelation handling
|
||||
* [X] SMTP Auth support
|
||||
* [X] CRAM-MD5
|
||||
* [X] LOGIN
|
||||
* [X] PLAIN
|
||||
* [X] SCRAM-SHA-1/SCRAM-SHA-1-PLUS
|
||||
* [X] SCRAM-SHA-256/SCRAM-SHA-256-PLUS
|
||||
* [X] XOAUTH2
|
||||
* [X] RFC5322 compliant mail address validation
|
||||
* [X] 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] Support for attachments and inline embeds (from file system, `io.Reader`, `embed.FS` or `fs.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] 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
|
||||
* [X] Custom error types for delivery errors
|
||||
* [X] Custom dial-context functions for more control over the connection (proxing, DNS hooking, etc.)
|
||||
* [X] Output a go-mail message as EML file and parse EML file into a go-mail message
|
||||
* [X] S/MIME message signing support (RSA and ECDSA)
|
||||
* [X] UNIX domain socket support
|
||||
* [X] Pluggable SMTP error registry for advanced handling of non-RFC-conforming servers
|
||||
|
||||
go-mail works like a programatic email client and provides lots of methods and functionalities you would consider
|
||||
standard in a MUA.
|
||||
|
||||
## Documentation
|
||||
We aim for good GoDoc documenation in our library which gives you a full API reference. We also provide a more in-depth
|
||||
documentation website at [go-mail.dev](https://go-mail.dev)
|
||||
|
||||
## Compatibility
|
||||
|
||||
Go evolves quickly and introduces valuable improvements with each release. To balance adopting new features with
|
||||
ensuring stability for our users, we align our support with the official Go release policy. In practice, go-mail
|
||||
will always support the same set of Go versions that the Go team actively maintains.
|
||||
|
||||
Since Go provides two releases per year, this translates into roughly one year of guaranteed compatibility for
|
||||
any given Go version. We encourage users to stay current with supported Go versions to benefit from security
|
||||
updates, performance improvements, and new language features.
|
||||
|
||||
## 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
|
||||
|
||||
## Middleware
|
||||
The goal of go-mail is to keep it free from 3rd party dependencies and only focus on things a mail library should
|
||||
fulfill. Yet, since version v0.2.8 we've added support for middleware on the `Msg` object, allowing 3rd parties to
|
||||
alter a given mail message to their needs without relying on `go-mail` to support their specific need.
|
||||
|
||||
To get our users started with message middleware, we've created a collection of useful middlewares. It can be
|
||||
found in a seperate repository: [go-mail-middlware](https://github.com/wneessen/go-mail-middleware).
|
||||
|
||||
## Merch
|
||||
Thanks to our wonderful friends at [HelloTux](https://www.hellotux.com) we can offer great go-mail merchandising. All merch articles are embroidery
|
||||
to provide the best and most long-lasting quality possible.
|
||||
|
||||
If you want to support the open source community and represent your favourite Go mail library with some cool drip, check out our merch shop at:
|
||||
[https://www.hellotux.com/go-mail](https://www.hellotux.com/go-mail).
|
||||
|
||||
## Examples
|
||||
|
||||
We provide example code in both our GoDocs as well as on our official Website (see [Documentation](#documentation)). For a quick start into go-mail
|
||||
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):
|
||||
|
||||
<a href="https://github.com/wneessen/go-mail/graphs/contributors">
|
||||
<img alt="image of contributors" 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)
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) The go-mail Authors
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
version = 1
|
||||
SPDX-PackageName = "go-mail"
|
||||
SPDX-PackageSupplier = "The go-mail Authors"
|
||||
SPDX-PackageDownloadLocation = "https://github.com/wneessen/go-mail"
|
||||
|
||||
[[annotations]]
|
||||
path = ["testdata/**"]
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "The go-mail Authors"
|
||||
SPDX-License-Identifier = "MIT"
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: The go-mail Authors
|
||||
|
||||
SPDX-License-Identifier: MIT
|
||||
-->
|
||||
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report (possible) security issues in go-mail, please either send a mail to
|
||||
[security@go-mail.dev](mailto:security@go-mail.dev) or use Github's
|
||||
[private reporting feature](https://github.com/wneessen/go-mail/security/advisories/new).
|
||||
Reports are always welcome. Even if you are not 100% certain that a specific issue you found
|
||||
counts as a security issue, we'd love to hear the details, so we can figure out together if
|
||||
the issue in question needds to be addressed.
|
||||
|
||||
Typically, you will receive an answer within a day or even within a few hours.
|
||||
|
||||
## Encryption
|
||||
You can send OpenPGP/GPG encrpyted mails to the [security@go-mail.dev](mailto:security@go-mail.dev) address.
|
||||
|
||||
OpenPGP/GPG public key:
|
||||
```
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
xjMEY8RwPBYJKwYBBAHaRw8BAQdAiLsW7pv+CCMq5Ol0hbIB1HnJI97u3zJw
|
||||
Wslr7GJzgOzNK3NlY3VyaXR5QGdvLW1haWwuZGV2IDxzZWN1cml0eUBnby1t
|
||||
YWlsLmRldj7CjAQQFgoAPgUCY8RwPAQLCQcICRCgTBOxf8keAAMVCAoEFgAC
|
||||
AQIZAQIbAwIeARYhBAoWEB7Y0bE7zcIOuaBME7F/yR4AAAByugD9HabWXsyD
|
||||
aPIDrIS97OBA1OLltB4NPT5ba9whKRxTEmMBALBiB2ML4ZTrjLqI6UbGkhJq
|
||||
mWeMtvmU0chZT7WNBO0PzjgEY8RwPBIKKwYBBAGXVQEFAQEHQGDEccz6gvl5
|
||||
t8cMMb/Dy2l0elRZL+Nd0gOhnbWMWlArAwEIB8J4BBgWCAAqBQJjxHA8CRCg
|
||||
TBOxf8keAAIbDBYhBAoWEB7Y0bE7zcIOuaBME7F/yR4AAADaMwD9EvEA3NSN
|
||||
NtdSaeL/euh6oRRiCjKzh5bIqZiQXqMlIOoBAJvPE2facs8MISwTtDoHW0sD
|
||||
WdOs3yBpGlGCs5WEqvQH
|
||||
=zn96
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
```
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
// SPDX-FileCopyrightText: The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SMTPAuthType is a type wrapper for a string type. It represents the type of SMTP authentication
|
||||
// mechanism to be used.
|
||||
type SMTPAuthType string
|
||||
|
||||
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 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 mechanism transmits the username and password in
|
||||
// plaintext over the internet connection, we only allow this mechanism over a TLS secured
|
||||
// connection.
|
||||
//
|
||||
// https://msopenspecs.azureedge.net/files/MS-XLOGIN/%5bMS-XLOGIN%5d.pdf
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
|
||||
SMTPAuthLogin SMTPAuthType = "LOGIN"
|
||||
|
||||
// SMTPAuthLoginNoEnc 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 mechanism transmits the username and password in
|
||||
// plaintext over the internet connection, by default we only allow this mechanism over
|
||||
// a TLS secured connection. This authentiation mechanism overrides this default and will
|
||||
// allow LOGIN authentication via an unencrypted channel. This can be useful if the
|
||||
// connection has already been secured in a different way (e. g. a SSH tunnel)
|
||||
//
|
||||
// Note: Use this authentication method with caution. If used in the wrong way, you might
|
||||
// expose your authentication information over unencrypted channels!
|
||||
//
|
||||
// https://msopenspecs.azureedge.net/files/MS-XLOGIN/%5bMS-XLOGIN%5d.pdf
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
|
||||
SMTPAuthLoginNoEnc SMTPAuthType = "LOGIN-NOENC"
|
||||
|
||||
// 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.
|
||||
SMTPAuthNoAuth SMTPAuthType = "NOAUTH"
|
||||
|
||||
// SMTPAuthPlain is the "PLAIN" authentication mechanism as described in RFC 4616.
|
||||
//
|
||||
// Since the "PLAIN" SASL authentication mechanism transmits the username and password in
|
||||
// plaintext over the internet connection, we only allow this mechanism over a TLS secured
|
||||
// connection.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc4616/
|
||||
SMTPAuthPlain SMTPAuthType = "PLAIN"
|
||||
|
||||
// SMTPAuthPlainNoEnc is the "PLAIN" authentication mechanism as described in RFC 4616.
|
||||
//
|
||||
// Since the "PLAIN" SASL authentication mechanism transmits the username and password in
|
||||
// plaintext over the internet connection, by default we only allow this mechanism over
|
||||
// a TLS secured connection. This authentiation mechanism overrides this default and will
|
||||
// allow PLAIN authentication via an unencrypted channel. This can be useful if the
|
||||
// connection has already been secured in a different way (e. g. a SSH tunnel)
|
||||
//
|
||||
// Note: Use this authentication method with caution. If used in the wrong way, you might
|
||||
// expose your authentication information over unencrypted channels!
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc4616/
|
||||
SMTPAuthPlainNoEnc SMTPAuthType = "PLAIN-NOENC"
|
||||
|
||||
// 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 mechanism 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 mechanism over a TLS secured connection.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc7677
|
||||
SMTPAuthSCRAMSHA256PLUS SMTPAuthType = "SCRAM-SHA-256-PLUS"
|
||||
|
||||
// SMTPAuthAutoDiscover is a mechanism that dynamically discovers all authentication mechanisms
|
||||
// supported by the SMTP server and selects the strongest available one.
|
||||
//
|
||||
// This type simplifies authentication by automatically negotiating the most secure mechanism
|
||||
// offered by the server, based on a predefined security ranking. For instance, mechanisms like
|
||||
// SCRAM-SHA-256(-PLUS) or XOAUTH2 are prioritized over weaker mechanisms such as CRAM-MD5 or PLAIN.
|
||||
//
|
||||
// The negotiation process ensures that mechanisms requiring additional capabilities (e.g.,
|
||||
// SCRAM-SHA-X-PLUS with TLS channel binding) are only selected when the necessary prerequisites
|
||||
// are in place, such as an active TLS-secured connection.
|
||||
//
|
||||
// By automating mechanism selection, SMTPAuthAutoDiscover minimizes configuration effort while
|
||||
// maximizing security and compatibility with a wide range of SMTP servers.
|
||||
SMTPAuthAutoDiscover SMTPAuthType = "AUTODISCOVER"
|
||||
)
|
||||
|
||||
// SMTP Auth related static errors
|
||||
var (
|
||||
// ErrPlainAuthNotSupported is returned when the server does not support the "PLAIN" SMTP
|
||||
// authentication type.
|
||||
ErrPlainAuthNotSupported = errors.New("server does not support SMTP AUTH type: PLAIN")
|
||||
|
||||
// ErrLoginAuthNotSupported is returned when the server does not support the "LOGIN" SMTP
|
||||
// authentication type.
|
||||
ErrLoginAuthNotSupported = errors.New("server does not support SMTP AUTH type: LOGIN")
|
||||
|
||||
// ErrCramMD5AuthNotSupported is returned when the server does not support the "CRAM-MD5" SMTP
|
||||
// authentication type.
|
||||
ErrCramMD5AuthNotSupported = errors.New("server does not support SMTP AUTH type: CRAM-MD5")
|
||||
|
||||
// ErrXOauth2AuthNotSupported is returned when the server does not support the "XOAUTH2" schema.
|
||||
ErrXOauth2AuthNotSupported = errors.New("server does not support SMTP AUTH type: XOAUTH2")
|
||||
|
||||
// ErrSCRAMSHA1AuthNotSupported is returned when the server does not support the "SCRAM-SHA-1" SMTP
|
||||
// authentication type.
|
||||
ErrSCRAMSHA1AuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-1")
|
||||
|
||||
// ErrSCRAMSHA1PLUSAuthNotSupported is returned when the server does not support the "SCRAM-SHA-1-PLUS" SMTP
|
||||
// authentication type.
|
||||
ErrSCRAMSHA1PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-1-PLUS")
|
||||
|
||||
// ErrSCRAMSHA256AuthNotSupported is returned when the server does not support the "SCRAM-SHA-256" SMTP
|
||||
// authentication type.
|
||||
ErrSCRAMSHA256AuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256")
|
||||
|
||||
// ErrSCRAMSHA256PLUSAuthNotSupported is returned when the server does not support the "SCRAM-SHA-256-PLUS" SMTP
|
||||
// authentication type.
|
||||
ErrSCRAMSHA256PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256-PLUS")
|
||||
|
||||
// ErrNoSupportedAuthDiscovered is returned when the SMTP Auth AutoDiscover process fails to identify
|
||||
// any supported authentication mechanisms offered by the server.
|
||||
ErrNoSupportedAuthDiscovered = errors.New("SMTP Auth autodiscover was not able to detect a supported " +
|
||||
"authentication mechanism")
|
||||
)
|
||||
|
||||
// UnmarshalString satisfies the fig.StringUnmarshaler interface for the SMTPAuthType type
|
||||
// https://pkg.go.dev/github.com/kkyr/fig#StringUnmarshaler
|
||||
func (sa *SMTPAuthType) UnmarshalString(value string) error {
|
||||
switch strings.ToLower(value) {
|
||||
case "auto", "autodiscover", "autodiscovery":
|
||||
*sa = SMTPAuthAutoDiscover
|
||||
case "cram-md5", "crammd5", "cram":
|
||||
*sa = SMTPAuthCramMD5
|
||||
case "custom":
|
||||
*sa = SMTPAuthCustom
|
||||
case "login":
|
||||
*sa = SMTPAuthLogin
|
||||
case "login-noenc":
|
||||
*sa = SMTPAuthLoginNoEnc
|
||||
case "none", "noauth", "no":
|
||||
*sa = SMTPAuthNoAuth
|
||||
case "plain":
|
||||
*sa = SMTPAuthPlain
|
||||
case "plain-noenc":
|
||||
*sa = SMTPAuthPlainNoEnc
|
||||
case "scram-sha-1", "scram-sha1", "scramsha1":
|
||||
*sa = SMTPAuthSCRAMSHA1
|
||||
case "scram-sha-1-plus", "scram-sha1-plus", "scramsha1plus":
|
||||
*sa = SMTPAuthSCRAMSHA1PLUS
|
||||
case "scram-sha-256", "scram-sha256", "scramsha256":
|
||||
*sa = SMTPAuthSCRAMSHA256
|
||||
case "scram-sha-256-plus", "scram-sha256-plus", "scramsha256plus":
|
||||
*sa = SMTPAuthSCRAMSHA256PLUS
|
||||
case "xoauth2", "oauth2":
|
||||
*sa = SMTPAuthXOAUTH2
|
||||
default:
|
||||
return fmt.Errorf("unsupported SMTP auth type: %s", value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
// SPDX-FileCopyrightText: The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
// newlineBytes is a byte slice representation of the SingleNewLine constant used for line breaking
|
||||
// in encoding processes.
|
||||
var newlineBytes = []byte(SingleNewLine)
|
||||
|
||||
// base64LineBreaker handles base64 encoding with the insertion of new lines after a certain number
|
||||
// of characters.
|
||||
//
|
||||
// This struct is used to manage base64 encoding while ensuring that new lines are inserted after
|
||||
// reaching a specific line length. It satisfies the io.WriteCloser interface.
|
||||
//
|
||||
// References:
|
||||
// - https://datatracker.ietf.org/doc/html/rfc2045 (Base64 and line length limitations)
|
||||
type base64LineBreaker struct {
|
||||
line [MaxBodyLength]byte
|
||||
used int
|
||||
out io.Writer
|
||||
}
|
||||
|
||||
// Write writes data to the base64LineBreaker, ensuring lines do not exceed MaxBodyLength.
|
||||
//
|
||||
// This method writes the provided data to the base64LineBreaker. It ensures that the written
|
||||
// lines do not exceed the MaxBodyLength. If the data exceeds the limit, it handles the
|
||||
// continuation by splitting the data and writing new lines as necessary.
|
||||
//
|
||||
// Parameters:
|
||||
// - data: A byte slice containing the data to be written.
|
||||
//
|
||||
// Returns:
|
||||
// - numBytes: The number of bytes written.
|
||||
// - err: An error if one occurred during the write operation.
|
||||
func (l *base64LineBreaker) Write(data []byte) (numBytes int, err error) {
|
||||
if l.out == nil {
|
||||
err = errors.New("no io.Writer set for base64LineBreaker")
|
||||
return numBytes, err
|
||||
}
|
||||
if l.used+len(data) < MaxBodyLength {
|
||||
copy(l.line[l.used:], data)
|
||||
l.used += len(data)
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
_, err = l.out.Write(l.line[0:l.used])
|
||||
if err != nil {
|
||||
return numBytes, err
|
||||
}
|
||||
excess := MaxBodyLength - l.used
|
||||
l.used = 0
|
||||
|
||||
numBytes, err = l.out.Write(data[0:excess])
|
||||
if err != nil {
|
||||
return numBytes, err
|
||||
}
|
||||
|
||||
_, err = l.out.Write(newlineBytes)
|
||||
if err != nil {
|
||||
return numBytes, err
|
||||
}
|
||||
|
||||
var n int
|
||||
n, err = l.Write(data[excess:]) // recurse
|
||||
numBytes += n
|
||||
return numBytes, err
|
||||
}
|
||||
|
||||
// Close finalizes the base64LineBreaker, writing any remaining buffered data and appending a newline.
|
||||
//
|
||||
// This method ensures that any remaining data in the buffer is written to the output and appends
|
||||
// a newline. It is used to finalize the base64LineBreaker and should be called when no more data
|
||||
// is expected to be written.
|
||||
//
|
||||
// Returns:
|
||||
// - err: An error if one occurred during the final write operation.
|
||||
func (l *base64LineBreaker) Close() (err error) {
|
||||
if l.used > 0 {
|
||||
_, err = l.out.Write(l.line[0:l.used])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = l.out.Write(newlineBytes)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
+1655
File diff suppressed because it is too large.
Load diff
+22
@@ -0,0 +1,22 @@
|
||||
# SPDX-FileCopyrightText: The go-mail Authors
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: 95%
|
||||
threshold: 2%
|
||||
base: auto
|
||||
if_ci_failed: error
|
||||
only_pulls: false
|
||||
patch:
|
||||
default:
|
||||
target: 95%
|
||||
base: auto
|
||||
if_ci_failed: error
|
||||
threshold: 2%
|
||||
|
||||
comment:
|
||||
require_changes: true
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
// SPDX-FileCopyrightText: The go-mail Authors
|
||||
//
|
||||
// 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 programmatic email client and provides lots of methods and
|
||||
// functionalities you would consider standard in a MUA.
|
||||
package mail
|
||||
|
||||
// VERSION indicates the current version of the package. It is also attached to the default user
|
||||
// agent string.
|
||||
const VERSION = "0.7.2"
|
||||
+601
@@ -0,0 +1,601 @@
|
||||
// SPDX-FileCopyrightText: The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"mime/quotedprintable"
|
||||
netmail "net/mail"
|
||||
"os"
|
||||
"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.
|
||||
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.
|
||||
func EMLToMsgFromReader(reader io.Reader) (*Msg, error) {
|
||||
msg := NewMsg()
|
||||
parsedMsg, bodybuf, err := readEMLFromReader(reader)
|
||||
if err != nil || parsedMsg == nil {
|
||||
return msg, fmt.Errorf("failed to parse EML from reader: %w", err)
|
||||
}
|
||||
|
||||
if err = parseEML(parsedMsg, bodybuf, msg); err != nil {
|
||||
return msg, fmt.Errorf("failed to parse EML contents: %w", err)
|
||||
}
|
||||
|
||||
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.
|
||||
func EMLToMsgFromFile(filePath string) (*Msg, error) {
|
||||
msg := NewMsg()
|
||||
parsedMsg, bodybuf, err := readEML(filePath)
|
||||
if err != nil || parsedMsg == nil {
|
||||
return msg, fmt.Errorf("failed to parse EML file: %w", err)
|
||||
}
|
||||
|
||||
if err = parseEML(parsedMsg, bodybuf, msg); err != nil {
|
||||
return msg, fmt.Errorf("failed to parse EML contents: %w", err)
|
||||
}
|
||||
|
||||
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.
|
||||
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)
|
||||
}
|
||||
if err := parseEMLBodyParts(parsedMsg, bodybuf, msg); err != nil {
|
||||
return fmt.Errorf("failed to parse EML body parts: %w", err)
|
||||
}
|
||||
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.
|
||||
func readEML(filePath string) (*netmail.Message, *bytes.Buffer, error) {
|
||||
fileHandle, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to open EML file: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = fileHandle.Close()
|
||||
}()
|
||||
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.
|
||||
func readEMLFromReader(reader io.Reader) (*netmail.Message, *bytes.Buffer, error) {
|
||||
parsedMsg, err := netmail.ReadMessage(reader)
|
||||
if err != nil {
|
||||
return parsedMsg, nil, fmt.Errorf("failed to parse EML: %w", err)
|
||||
}
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
if _, err = buf.ReadFrom(parsedMsg.Body); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
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.
|
||||
func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error {
|
||||
commonHeaders := []Header{
|
||||
HeaderContentType, HeaderImportance, HeaderInReplyTo, HeaderListUnsubscribe,
|
||||
HeaderListUnsubscribePost, HeaderMessageID, HeaderMIMEVersion, HeaderOrganization,
|
||||
HeaderPrecedence, HeaderPriority, HeaderReferences, HeaderSubject, HeaderUserAgent,
|
||||
HeaderXMailer, HeaderXMSMailPriority, HeaderXPriority,
|
||||
}
|
||||
|
||||
// Extract content type, charset and encoding first
|
||||
parseEMLEncoding(mailHeader, msg)
|
||||
parseEMLContentTypeCharset(mailHeader, msg)
|
||||
|
||||
// Extract address headers
|
||||
if value := mailHeader.Get(HeaderFrom.String()); value != "" {
|
||||
if err := msg.From(value); err != nil {
|
||||
return fmt.Errorf(`failed to parse %q header: %w`, HeaderFrom, err)
|
||||
}
|
||||
}
|
||||
addrHeaders := map[AddrHeader]func(...string) error{
|
||||
HeaderTo: msg.To,
|
||||
HeaderCc: msg.Cc,
|
||||
HeaderBcc: msg.Bcc,
|
||||
}
|
||||
for addrHeader, addrFunc := range addrHeaders {
|
||||
if v := mailHeader.Get(addrHeader.String()); v != "" {
|
||||
var addrStrings []string
|
||||
parsedAddrs, err := netmail.ParseAddressList(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to parse address list: %w`, err)
|
||||
}
|
||||
for _, addr := range parsedAddrs {
|
||||
addrStrings = append(addrStrings, addr.String())
|
||||
}
|
||||
// We can skip the error checking here since netmail.ParseAddressList already performed the
|
||||
// same address checking that the msg methods do.
|
||||
_ = addrFunc(addrStrings...)
|
||||
}
|
||||
}
|
||||
|
||||
// Extract date from message
|
||||
date, err := mailHeader.Date()
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, netmail.ErrHeaderNotPresent):
|
||||
msg.SetDate()
|
||||
default:
|
||||
return fmt.Errorf("failed to parse EML date: %w", err)
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
msg.SetDateWithValue(date)
|
||||
}
|
||||
|
||||
// Extract common headers
|
||||
for _, header := range commonHeaders {
|
||||
if value := mailHeader.Get(header.String()); value != "" {
|
||||
if strings.EqualFold(header.String(), HeaderContentType.String()) &&
|
||||
strings.HasPrefix(value, TypeMultipartMixed.String()) {
|
||||
continue
|
||||
}
|
||||
msg.SetGenHeader(header, value)
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
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()))
|
||||
if err != nil {
|
||||
switch {
|
||||
// If no Content-Type header is found, we assume that this is a plain text, 7bit, US-ASCII mail
|
||||
case strings.EqualFold(err.Error(), "mime: no media type"):
|
||||
mediatype = TypeTextPlain.String()
|
||||
params = make(map[string]string)
|
||||
params["charset"] = CharsetASCII.String()
|
||||
default:
|
||||
return fmt.Errorf("failed to extract content type: %w", err)
|
||||
}
|
||||
}
|
||||
if value, ok := params["charset"]; ok {
|
||||
msg.SetCharset(Charset(value))
|
||||
}
|
||||
if value, ok := params["boundary"]; ok {
|
||||
msg.SetBoundary(value)
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.EqualFold(mediatype, TypeTextPlain.String()),
|
||||
strings.EqualFold(mediatype, TypeTextHTML.String()):
|
||||
if err = parseEMLBodyPlain(mediatype, parsedMsg, bodybuf, msg); err != nil {
|
||||
return fmt.Errorf("failed to parse plain body: %w", err)
|
||||
}
|
||||
case strings.EqualFold(mediatype, TypeMultipartAlternative.String()),
|
||||
strings.EqualFold(mediatype, TypeMultipartMixed.String()),
|
||||
strings.EqualFold(mediatype, TypeMultipartRelated.String()):
|
||||
if err = parseEMLMultipart(params, bodybuf, msg); err != nil {
|
||||
return fmt.Errorf("failed to parse multipart body: %w", err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("failed to parse body, unknown content type: %s", mediatype)
|
||||
}
|
||||
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.
|
||||
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
|
||||
if contentTransferEnc == "" || strings.EqualFold(contentTransferEnc, EncodingUSASCII.String()) {
|
||||
msg.SetEncoding(EncodingUSASCII)
|
||||
msg.SetBodyString(ContentType(mediatype), bodybuf.String())
|
||||
return nil
|
||||
}
|
||||
if strings.EqualFold(contentTransferEnc, NoEncoding.String()) {
|
||||
msg.SetEncoding(NoEncoding)
|
||||
msg.SetBodyString(ContentType(mediatype), bodybuf.String())
|
||||
return nil
|
||||
}
|
||||
if strings.EqualFold(contentTransferEnc, EncodingQP.String()) {
|
||||
msg.SetEncoding(EncodingQP)
|
||||
qpReader := quotedprintable.NewReader(bodybuf)
|
||||
qpBuffer := bytes.Buffer{}
|
||||
if _, err := qpBuffer.ReadFrom(qpReader); err != nil {
|
||||
return fmt.Errorf("failed to read quoted-printable body: %w", err)
|
||||
}
|
||||
msg.SetBodyString(ContentType(mediatype), qpBuffer.String())
|
||||
return nil
|
||||
}
|
||||
if strings.EqualFold(contentTransferEnc, EncodingB64.String()) {
|
||||
msg.SetEncoding(EncodingB64)
|
||||
b64Decoder := base64.NewDecoder(base64.StdEncoding, bodybuf)
|
||||
b64Buffer := bytes.Buffer{}
|
||||
if _, err := b64Buffer.ReadFrom(b64Decoder); err != nil {
|
||||
return fmt.Errorf("failed to read base64 body: %w", err)
|
||||
}
|
||||
msg.SetBodyString(ContentType(mediatype), b64Buffer.String())
|
||||
return nil
|
||||
}
|
||||
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.
|
||||
func parseEMLMultipart(params map[string]string, bodybuf *bytes.Buffer, msg *Msg) error {
|
||||
boundary, ok := params["boundary"]
|
||||
if !ok {
|
||||
return fmt.Errorf("no boundary tag found in multipart body")
|
||||
}
|
||||
multipartReader := multipart.NewReader(bodybuf, boundary)
|
||||
ReadNextPart:
|
||||
multiPart, err := multipartReader.NextPart()
|
||||
defer func() {
|
||||
if multiPart != nil {
|
||||
_ = multiPart.Close()
|
||||
}
|
||||
}()
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return fmt.Errorf("failed to get next part of multipart message: %w", err)
|
||||
}
|
||||
for err == nil {
|
||||
// Multipart/related and Multipart/alternative parts need to be parsed separately
|
||||
if contentTypeSlice, ok := multiPart.Header[HeaderContentType.String()]; ok && len(contentTypeSlice) == 1 {
|
||||
contentType, _ := parseMultiPartHeader(contentTypeSlice[0])
|
||||
if strings.EqualFold(contentType, TypeMultipartRelated.String()) ||
|
||||
strings.EqualFold(contentType, TypeMultipartAlternative.String()) {
|
||||
relatedPart := &netmail.Message{
|
||||
Header: netmail.Header(multiPart.Header),
|
||||
Body: multiPart,
|
||||
}
|
||||
relatedBuf := &bytes.Buffer{}
|
||||
if _, err = relatedBuf.ReadFrom(multiPart); err != nil {
|
||||
return fmt.Errorf("failed to read related multipart message to buffer: %w", err)
|
||||
}
|
||||
if err := parseEMLBodyParts(relatedPart, relatedBuf, msg); err != nil {
|
||||
return fmt.Errorf("failed to parse related multipart body: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Content-Disposition header means we have an attachment or embed
|
||||
if contentDisposition, ok := multiPart.Header[HeaderContentDisposition.String()]; ok {
|
||||
if err = parseEMLAttachmentEmbed(contentDisposition, multiPart, msg); err != nil {
|
||||
return fmt.Errorf("failed to parse attachment/embed: %w", err)
|
||||
}
|
||||
goto ReadNextPart
|
||||
}
|
||||
|
||||
multiPartData, mperr := io.ReadAll(multiPart)
|
||||
if mperr != nil {
|
||||
_ = multiPart.Close()
|
||||
return fmt.Errorf("failed to read multipart: %w", err)
|
||||
}
|
||||
|
||||
multiPartContentType, ok := multiPart.Header[HeaderContentType.String()]
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to get content-type from part")
|
||||
}
|
||||
contentType, optional := parseMultiPartHeader(multiPartContentType[0])
|
||||
if strings.EqualFold(contentType, TypeMultipartRelated.String()) {
|
||||
goto ReadNextPart
|
||||
}
|
||||
part := msg.newPart(ContentType(contentType))
|
||||
if charset, ok := optional["charset"]; ok {
|
||||
part.SetCharset(Charset(charset))
|
||||
}
|
||||
|
||||
mutliPartTransferEnc, ok := multiPart.Header[HeaderContentTransferEnc.String()]
|
||||
if !ok {
|
||||
// If CTE is empty we can assume that it's a quoted-printable CTE since the
|
||||
// GO stdlib multipart packages deletes that header
|
||||
// See: https://cs.opensource.google/go/go/+/refs/tags/go1.22.0:src/mime/multipart/multipart.go;l=161
|
||||
mutliPartTransferEnc = []string{EncodingQP.String()}
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.EqualFold(mutliPartTransferEnc[0], EncodingUSASCII.String()):
|
||||
part.SetEncoding(EncodingUSASCII)
|
||||
part.SetContent(string(multiPartData))
|
||||
case strings.EqualFold(mutliPartTransferEnc[0], NoEncoding.String()):
|
||||
part.SetEncoding(NoEncoding)
|
||||
part.SetContent(string(multiPartData))
|
||||
case strings.EqualFold(mutliPartTransferEnc[0], EncodingB64.String()):
|
||||
part.SetEncoding(EncodingB64)
|
||||
if err = handleEMLMultiPartBase64Encoding(multiPartData, part); err != nil {
|
||||
return fmt.Errorf("failed to handle multipart base64 transfer-encoding: %w", err)
|
||||
}
|
||||
case strings.EqualFold(mutliPartTransferEnc[0], EncodingQP.String()):
|
||||
part.SetEncoding(EncodingQP)
|
||||
part.SetContent(string(multiPartData))
|
||||
default:
|
||||
return fmt.Errorf("unsupported Content-Transfer-Encoding: %s", mutliPartTransferEnc[0])
|
||||
}
|
||||
|
||||
msg.parts = append(msg.parts, part)
|
||||
multiPart, err = multipartReader.NextPart()
|
||||
}
|
||||
if !errors.Is(err, io.EOF) {
|
||||
return fmt.Errorf("failed to read multipart: %w", err)
|
||||
}
|
||||
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.
|
||||
func parseEMLEncoding(mailHeader *netmail.Header, msg *Msg) {
|
||||
if value := mailHeader.Get(HeaderContentTransferEnc.String()); value != "" {
|
||||
switch {
|
||||
case strings.EqualFold(value, EncodingQP.String()):
|
||||
msg.SetEncoding(EncodingQP)
|
||||
case strings.EqualFold(value, EncodingB64.String()):
|
||||
msg.SetEncoding(EncodingB64)
|
||||
default:
|
||||
msg.SetEncoding(NoEncoding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseEMLContentTypeCharset parses and determines the charset and content type of the message.
|
||||
//
|
||||
// This function extracts the content type and charset from the EML headers, setting them
|
||||
// appropriately in the Msg object. It ensures that the Msg object is configured with the
|
||||
// correct content type for further processing.
|
||||
//
|
||||
// Parameters:
|
||||
// - mailHeader: A pointer to the netmail.Header containing the EML headers.
|
||||
// - msg: A pointer to the Msg object to be updated with content type and charset information.
|
||||
func parseEMLContentTypeCharset(mailHeader *netmail.Header, msg *Msg) {
|
||||
if value := mailHeader.Get(HeaderContentType.String()); value != "" {
|
||||
contentType, optional := parseMultiPartHeader(value)
|
||||
if charset, ok := optional["charset"]; ok {
|
||||
msg.SetCharset(Charset(charset))
|
||||
}
|
||||
msg.setEncoder()
|
||||
if contentType != "" && !strings.EqualFold(contentType, TypeMultipartMixed.String()) {
|
||||
msg.SetGenHeader(HeaderContentType, contentType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleEMLMultiPartBase64Encoding sets the content body of a base64 encoded Part.
|
||||
//
|
||||
// This function decodes the base64 encoded content of a multipart part and stores the
|
||||
// resulting content in the provided Part object. It handles any errors that occur during
|
||||
// the decoding process.
|
||||
//
|
||||
// Parameters:
|
||||
// - multiPartData: A byte slice containing the base64 encoded data.
|
||||
// - part: A pointer to the Part object where the decoded content will be stored.
|
||||
//
|
||||
// Returns:
|
||||
// - An error if the base64 decoding fails; otherwise, returns nil.
|
||||
func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error {
|
||||
part.SetEncoding(EncodingB64)
|
||||
content, err := base64.StdEncoding.DecodeString(string(multiPartData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode base64 part: %w", err)
|
||||
}
|
||||
part.SetContent(string(content))
|
||||
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.
|
||||
func parseMultiPartHeader(multiPartHeader string) (header string, optional map[string]string) {
|
||||
optional = make(map[string]string)
|
||||
headerSplit := strings.Split(multiPartHeader, ";")
|
||||
header = headerSplit[0]
|
||||
if len(headerSplit) == 1 {
|
||||
return header, optional
|
||||
}
|
||||
for _, opt := range headerSplit[1:] {
|
||||
optString := strings.TrimLeft(opt, " ")
|
||||
optSplit := strings.SplitN(optString, "=", 2)
|
||||
if len(optSplit) == 2 {
|
||||
optional[optSplit[0]] = optSplit[1]
|
||||
}
|
||||
}
|
||||
return header, optional
|
||||
}
|
||||
|
||||
// parseEMLAttachmentEmbed parses a multipart that is an attachment or embed.
|
||||
//
|
||||
// This function handles the parsing of multipart sections that are marked as attachments or
|
||||
// embedded content. It processes the content disposition and sets the appropriate fields in
|
||||
// the Msg object based on the parsed data.
|
||||
//
|
||||
// Parameters:
|
||||
// - contentDisposition: A slice of strings containing the content disposition header.
|
||||
// - multiPart: A pointer to the multipart.Part to be parsed.
|
||||
// - msg: A pointer to the Msg object to be populated with the attachment or embed data.
|
||||
//
|
||||
// Returns:
|
||||
// - An error if any issues occur during the parsing of attachments or embeds; otherwise,
|
||||
// returns nil.
|
||||
func parseEMLAttachmentEmbed(contentDisposition []string, multiPart *multipart.Part, msg *Msg) error {
|
||||
cdType, optional := parseMultiPartHeader(contentDisposition[0])
|
||||
filename := "generic.attachment"
|
||||
if name, ok := optional["filename"]; ok {
|
||||
filename = name[1 : len(name)-1]
|
||||
}
|
||||
|
||||
var dataReader io.Reader
|
||||
dataReader = multiPart
|
||||
contentTransferEnc, _ := parseMultiPartHeader(multiPart.Header.Get(HeaderContentTransferEnc.String()))
|
||||
b64Decoder := base64.NewDecoder(base64.StdEncoding, multiPart)
|
||||
if strings.EqualFold(contentTransferEnc, EncodingB64.String()) {
|
||||
dataReader = b64Decoder
|
||||
}
|
||||
|
||||
switch strings.ToLower(cdType) {
|
||||
case "attachment":
|
||||
if err := msg.AttachReader(filename, dataReader); err != nil {
|
||||
return fmt.Errorf("failed to attach multipart body: %w", err)
|
||||
}
|
||||
case "inline":
|
||||
if contentID, _ := parseMultiPartHeader(multiPart.Header.Get(HeaderContentID.String())); contentID != "" {
|
||||
if err := msg.EmbedReader(filename, dataReader, WithFileContentID(contentID)); err != nil {
|
||||
return fmt.Errorf("failed to embed multipart body: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := msg.EmbedReader(filename, dataReader); err != nil {
|
||||
return fmt.Errorf("failed to embed multipart body: %w", err)
|
||||
}
|
||||
default:
|
||||
return errors.New("unsupported content disposition type")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
// SPDX-FileCopyrightText: The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mail
|
||||
|
||||
// Charset is a type wrapper for a string representing different character encodings.
|
||||
type Charset string
|
||||
|
||||
// ContentType is a type wrapper for a string and represents the MIME type of the content being handled.
|
||||
type ContentType string
|
||||
|
||||
// Encoding is a type wrapper for a string and represents the type of encoding used for email messages
|
||||
// and/or parts.
|
||||
type Encoding string
|
||||
|
||||
// MIMEVersion is a type wrapper for a string nad represents the MIME version used in email messages.
|
||||
type MIMEVersion string
|
||||
|
||||
// MIMEType is a type wrapper for a string and represents the MIME type for the Msg content or parts.
|
||||
type MIMEType string
|
||||
|
||||
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 Encoding = "8bit"
|
||||
)
|
||||
|
||||
const (
|
||||
// CharsetUTF7 represents the "UTF-7" charset.
|
||||
CharsetUTF7 Charset = "UTF-7"
|
||||
|
||||
// CharsetUTF8 represents the "UTF-8" charset.
|
||||
CharsetUTF8 Charset = "UTF-8"
|
||||
|
||||
// CharsetASCII represents the "US-ASCII" charset.
|
||||
CharsetASCII Charset = "US-ASCII"
|
||||
|
||||
// CharsetISO88591 represents the "ISO-8859-1" charset.
|
||||
CharsetISO88591 Charset = "ISO-8859-1"
|
||||
|
||||
// CharsetISO88592 represents the "ISO-8859-2" charset.
|
||||
CharsetISO88592 Charset = "ISO-8859-2"
|
||||
|
||||
// CharsetISO88593 represents the "ISO-8859-3" charset.
|
||||
CharsetISO88593 Charset = "ISO-8859-3"
|
||||
|
||||
// CharsetISO88594 represents the "ISO-8859-4" charset.
|
||||
CharsetISO88594 Charset = "ISO-8859-4"
|
||||
|
||||
// CharsetISO88595 represents the "ISO-8859-5" charset.
|
||||
CharsetISO88595 Charset = "ISO-8859-5"
|
||||
|
||||
// CharsetISO88596 represents the "ISO-8859-6" charset.
|
||||
CharsetISO88596 Charset = "ISO-8859-6"
|
||||
|
||||
// CharsetISO88597 represents the "ISO-8859-7" charset.
|
||||
CharsetISO88597 Charset = "ISO-8859-7"
|
||||
|
||||
// CharsetISO88599 represents the "ISO-8859-9" charset.
|
||||
CharsetISO88599 Charset = "ISO-8859-9"
|
||||
|
||||
// CharsetISO885913 represents the "ISO-8859-13" charset.
|
||||
CharsetISO885913 Charset = "ISO-8859-13"
|
||||
|
||||
// CharsetISO885914 represents the "ISO-8859-14" charset.
|
||||
CharsetISO885914 Charset = "ISO-8859-14"
|
||||
|
||||
// CharsetISO885915 represents the "ISO-8859-15" charset.
|
||||
CharsetISO885915 Charset = "ISO-8859-15"
|
||||
|
||||
// CharsetISO885916 represents the "ISO-8859-16" charset.
|
||||
CharsetISO885916 Charset = "ISO-8859-16"
|
||||
|
||||
// CharsetISO2022JP represents the "ISO-2022-JP" charset.
|
||||
CharsetISO2022JP Charset = "ISO-2022-JP"
|
||||
|
||||
// CharsetISO2022KR represents the "ISO-2022-KR" charset.
|
||||
CharsetISO2022KR Charset = "ISO-2022-KR"
|
||||
|
||||
// CharsetWindows1250 represents the "windows-1250" charset.
|
||||
CharsetWindows1250 Charset = "windows-1250"
|
||||
|
||||
// CharsetWindows1251 represents the "windows-1251" charset.
|
||||
CharsetWindows1251 Charset = "windows-1251"
|
||||
|
||||
// CharsetWindows1252 represents the "windows-1252" charset.
|
||||
CharsetWindows1252 Charset = "windows-1252"
|
||||
|
||||
// CharsetWindows1255 represents the "windows-1255" charset.
|
||||
CharsetWindows1255 Charset = "windows-1255"
|
||||
|
||||
// CharsetWindows1256 represents the "windows-1256" charset.
|
||||
CharsetWindows1256 Charset = "windows-1256"
|
||||
|
||||
// CharsetKOI8R represents the "KOI8-R" charset.
|
||||
CharsetKOI8R Charset = "KOI8-R"
|
||||
|
||||
// CharsetKOI8U represents the "KOI8-U" charset.
|
||||
CharsetKOI8U Charset = "KOI8-U"
|
||||
|
||||
// CharsetBig5 represents the "Big5" charset.
|
||||
CharsetBig5 Charset = "Big5"
|
||||
|
||||
// CharsetGB18030 represents the "GB18030" charset.
|
||||
CharsetGB18030 Charset = "GB18030"
|
||||
|
||||
// CharsetGB2312 represents the "GB2312" charset.
|
||||
CharsetGB2312 Charset = "GB2312"
|
||||
|
||||
// CharsetTIS620 represents the "TIS-620" charset.
|
||||
CharsetTIS620 Charset = "TIS-620"
|
||||
|
||||
// CharsetEUCKR represents the "EUC-KR" charset.
|
||||
CharsetEUCKR Charset = "EUC-KR"
|
||||
|
||||
// CharsetShiftJIS represents the "Shift_JIS" charset.
|
||||
CharsetShiftJIS Charset = "Shift_JIS"
|
||||
|
||||
// CharsetUnknown represents the "Unknown" charset.
|
||||
CharsetUnknown Charset = "Unknown"
|
||||
|
||||
// CharsetGBK represents the "GBK" charset.
|
||||
CharsetGBK Charset = "GBK"
|
||||
)
|
||||
|
||||
// MIME10 represents the MIME version "1.0" used in email messages.
|
||||
const MIME10 MIMEVersion = "1.0"
|
||||
|
||||
const (
|
||||
// TypeAppOctetStream represents the MIME type for arbitrary binary data.
|
||||
TypeAppOctetStream ContentType = "application/octet-stream"
|
||||
|
||||
// TypeMultipartAlternative represents the MIME type for a message body that can contain multiple alternative
|
||||
// formats.
|
||||
TypeMultipartAlternative ContentType = "multipart/alternative"
|
||||
|
||||
// TypeMultipartMixed represents the MIME type for a multipart message containing different parts.
|
||||
TypeMultipartMixed ContentType = "multipart/mixed"
|
||||
|
||||
// TypeMultipartRelated represents the MIME type for a multipart message where each part is a related file
|
||||
// or resource.
|
||||
TypeMultipartRelated ContentType = "multipart/related"
|
||||
|
||||
// TypePGPSignature represents the MIME type for PGP signed messages.
|
||||
TypePGPSignature ContentType = "application/pgp-signature"
|
||||
|
||||
// TypePGPEncrypted represents the MIME type for PGP encrypted messages.
|
||||
TypePGPEncrypted ContentType = "application/pgp-encrypted"
|
||||
|
||||
// TypeTextHTML represents the MIME type for HTML text content.
|
||||
TypeTextHTML ContentType = "text/html"
|
||||
|
||||
// TypeTextPlain represents the MIME type for plain text content.
|
||||
TypeTextPlain ContentType = "text/plain"
|
||||
|
||||
// TypeSMIMESigned represents the MIME type for S/MIME singed messages.
|
||||
TypeSMIMESigned ContentType = `application/pkcs7-signature; name="smime.p7s"`
|
||||
)
|
||||
|
||||
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 fork 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"
|
||||
|
||||
// MIMESMIMESigned MIMEType represents a MIME multipart/signed type, used for siging emails with S/MIME.
|
||||
MIMESMIMESigned MIMEType = `signed; protocol="application/pkcs7-signature"; micalg=sha-256`
|
||||
)
|
||||
|
||||
// String satisfies the fmt.Stringer interface for the Charset type.
|
||||
// It converts a Charset into a printable format.
|
||||
//
|
||||
// This method returns the string representation of the Charset, allowing it to be easily
|
||||
// printed or logged.
|
||||
//
|
||||
// Returns:
|
||||
// - A string representation of the Charset.
|
||||
func (c Charset) String() string {
|
||||
return string(c)
|
||||
}
|
||||
|
||||
// String satisfies the fmt.Stringer interface for the ContentType type.
|
||||
// It converts a ContentType into a printable format.
|
||||
//
|
||||
// This method returns the string representation of the ContentType, enabling its use
|
||||
// in formatted output such as logging or displaying information to the user.
|
||||
//
|
||||
// Returns:
|
||||
// - A string representation of the ContentType.
|
||||
func (c ContentType) String() string {
|
||||
return string(c)
|
||||
}
|
||||
|
||||
// String satisfies the fmt.Stringer interface for the Encoding type.
|
||||
// It converts an Encoding into a printable format.
|
||||
//
|
||||
// This method returns the string representation of the Encoding, which can be used
|
||||
// for displaying or logging purposes.
|
||||
//
|
||||
// Returns:
|
||||
// - A string representation of the Encoding.
|
||||
func (e Encoding) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
// String satisfies the fmt.Stringer interface for the MIMEType type.
|
||||
// It converts an MIMEType into a printable format.
|
||||
//
|
||||
// This method returns the string representation of the MIMEType, which can be used
|
||||
// for displaying or logging purposes.
|
||||
//
|
||||
// Returns:
|
||||
// - A string representation of the MIMEType.
|
||||
func (e MIMEType) String() string {
|
||||
return string(e)
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
// SPDX-FileCopyrightText: The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/textproto"
|
||||
)
|
||||
|
||||
// FileOption is a function type used to modify properties of a File
|
||||
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.
|
||||
type File struct {
|
||||
ContentType ContentType
|
||||
Desc string
|
||||
Enc Encoding
|
||||
Header textproto.MIMEHeader
|
||||
Name string
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
func WithFileEncoding(encoding Encoding) FileOption {
|
||||
return func(f *File) {
|
||||
if encoding == EncodingQP {
|
||||
return
|
||||
}
|
||||
f.Enc = encoding
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
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.
|
||||
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).
|
||||
func (f *File) getHeader(header Header) (string, bool) {
|
||||
v := f.Header.Get(string(header))
|
||||
return v, v != ""
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
// SPDX-FileCopyrightText: The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mail
|
||||
|
||||
import "strings"
|
||||
|
||||
// Header is a type wrapper for a string and represents email header fields in a Msg.
|
||||
type Header string
|
||||
|
||||
// AddrHeader is a type wrapper for a string and represents email address headers fields in a Msg.
|
||||
type AddrHeader string
|
||||
|
||||
// Importance is a type wrapper for an int and represents the level of importance or priority for a Msg.
|
||||
type Importance int
|
||||
|
||||
const (
|
||||
// HeaderContentDescription is the "Content-Description" header.
|
||||
HeaderContentDescription Header = "Content-Description"
|
||||
|
||||
// HeaderContentDisposition is the "Content-Disposition" header.
|
||||
HeaderContentDisposition Header = "Content-Disposition"
|
||||
|
||||
// HeaderContentID is the "Content-ID" header.
|
||||
HeaderContentID Header = "Content-ID"
|
||||
|
||||
// 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 Header = "Content-Location"
|
||||
|
||||
// HeaderContentTransferEnc is the "Content-Transfer-Encoding" header.
|
||||
HeaderContentTransferEnc Header = "Content-Transfer-Encoding"
|
||||
|
||||
// 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 Header = "Date"
|
||||
|
||||
// HeaderDispositionNotificationTo is the MDN header as described in RFC 8098.
|
||||
// https://datatracker.ietf.org/doc/html/rfc8098#section-2.1
|
||||
HeaderDispositionNotificationTo Header = "Disposition-Notification-To"
|
||||
|
||||
// HeaderImportance represents the "Importance" field.
|
||||
HeaderImportance Header = "Importance"
|
||||
|
||||
// HeaderInReplyTo represents the "In-Reply-To" field.
|
||||
HeaderInReplyTo Header = "In-Reply-To"
|
||||
|
||||
// HeaderListUnsubscribe is the "List-Unsubscribe" header field.
|
||||
HeaderListUnsubscribe Header = "List-Unsubscribe"
|
||||
|
||||
// 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 Header = "Message-ID"
|
||||
|
||||
// HeaderMIMEVersion represents the "MIME-Version" field as per RFC 2045.
|
||||
// https://datatracker.ietf.org/doc/html/rfc2045#section-4
|
||||
HeaderMIMEVersion Header = "MIME-Version"
|
||||
|
||||
// HeaderOrganization is the "Organization" header field.
|
||||
HeaderOrganization Header = "Organization"
|
||||
|
||||
// HeaderPrecedence is the "Precedence" header field.
|
||||
HeaderPrecedence Header = "Precedence"
|
||||
|
||||
// HeaderPriority represents the "Priority" field.
|
||||
HeaderPriority Header = "Priority"
|
||||
|
||||
// HeaderReferences is the "References" header field.
|
||||
HeaderReferences Header = "References"
|
||||
|
||||
// HeaderSubject is the "Subject" header field.
|
||||
HeaderSubject Header = "Subject"
|
||||
|
||||
// HeaderUserAgent is the "User-Agent" header field.
|
||||
HeaderUserAgent Header = "User-Agent"
|
||||
|
||||
// HeaderXAutoResponseSuppress is the "X-Auto-Response-Suppress" header field.
|
||||
HeaderXAutoResponseSuppress Header = "X-Auto-Response-Suppress"
|
||||
|
||||
// HeaderXMailer is the "X-Mailer" header field.
|
||||
HeaderXMailer Header = "X-Mailer"
|
||||
|
||||
// HeaderXMSMailPriority is the "X-MSMail-Priority" header field.
|
||||
HeaderXMSMailPriority Header = "X-MSMail-Priority"
|
||||
|
||||
// HeaderXPriority is the "X-Priority" header field.
|
||||
HeaderXPriority Header = "X-Priority"
|
||||
)
|
||||
|
||||
const (
|
||||
// HeaderBcc is the "Blind Carbon Copy" header field.
|
||||
HeaderBcc AddrHeader = "Bcc"
|
||||
|
||||
// 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 AddrHeader = "EnvelopeFrom"
|
||||
|
||||
// HeaderFrom is the "From" header field.
|
||||
HeaderFrom AddrHeader = "From"
|
||||
|
||||
// HeaderReplyTo is the "Reply-To" header field.
|
||||
HeaderReplyTo AddrHeader = "Reply-To"
|
||||
|
||||
// HeaderTo is the "Recipient" header field.
|
||||
HeaderTo AddrHeader = "To"
|
||||
)
|
||||
|
||||
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.
|
||||
func (i Importance) NumString() string {
|
||||
switch i {
|
||||
case ImportanceNonUrgent:
|
||||
return "0"
|
||||
case ImportanceLow:
|
||||
return "0"
|
||||
case ImportanceHigh:
|
||||
return "1"
|
||||
case ImportanceUrgent:
|
||||
return "1"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// XPrioString returns the X-Priority string representation of the Importance level.
|
||||
//
|
||||
// This method maps ImportanceHigh and ImportanceUrgent to "1", while ImportanceNonUrgent and ImportanceLow
|
||||
// are mapped to "5". Other values return an empty string.
|
||||
//
|
||||
// Returns:
|
||||
// - A string representing the X-Priority value of the Importance level ("1" or "5"), or an empty string
|
||||
// if the Importance level is unrecognized.
|
||||
func (i Importance) XPrioString() string {
|
||||
switch i {
|
||||
case ImportanceNonUrgent:
|
||||
return "5"
|
||||
case ImportanceLow:
|
||||
return "5"
|
||||
case ImportanceHigh:
|
||||
return "1"
|
||||
case ImportanceUrgent:
|
||||
return "1"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// String satisfies the fmt.Stringer interface for the Importance type and returns the string
|
||||
// representation of the Importance level.
|
||||
//
|
||||
// This method provides a human-readable string for each Importance level.
|
||||
//
|
||||
// Returns:
|
||||
// - A string representing the Importance level ("non-urgent", "low", "high", or "urgent"), or an empty
|
||||
// string if the Importance level is unrecognized.
|
||||
func (i Importance) String() string {
|
||||
switch i {
|
||||
case ImportanceNonUrgent:
|
||||
return "non-urgent"
|
||||
case ImportanceLow:
|
||||
return "low"
|
||||
case ImportanceHigh:
|
||||
return "high"
|
||||
case ImportanceUrgent:
|
||||
return "urgent"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// String satisfies the fmt.Stringer interface for the Header type and returns the string
|
||||
// representation of the Header.
|
||||
//
|
||||
// Returns:
|
||||
// - A string representing the Header.
|
||||
func (h Header) String() string {
|
||||
return string(h)
|
||||
}
|
||||
|
||||
// String satisfies the fmt.Stringer interface for the AddrHeader type and returns the string
|
||||
// representation of the AddrHeader.
|
||||
//
|
||||
// Returns:
|
||||
// - A string representing the AddrHeader.
|
||||
func (a AddrHeader) String() string {
|
||||
return string(a)
|
||||
}
|
||||
|
||||
// IsAddrHeader checks if the provided string is an address header. It returns true on a valid AddrHeader
|
||||
// and false for any other string.
|
||||
//
|
||||
// Parameters:
|
||||
// - header: The string to check.
|
||||
func IsAddrHeader(header string) bool {
|
||||
addrHeaderList := []string{"bcc", "cc", "envelopefrom", "from", "reply-to", "to"}
|
||||
for _, addrHeader := range addrHeaderList {
|
||||
if addrHeader == strings.ToLower(header) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
+412
@@ -0,0 +1,412 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2015 Andrew Smith
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2017-2024 The mozilla services project (https://github.com/mozilla-services)
|
||||
// SPDX-FileCopyrightText: Copyright (c) The go-mail Authors
|
||||
//
|
||||
// Partially forked from https://github.com/mozilla-services/pkcs7, which in turn is also a fork
|
||||
// of https://github.com/fullsailor/pkcs7.
|
||||
// Use of the forked source code is, same as go-mail, governed by a MIT license.
|
||||
//
|
||||
// go-mail specific modifications by the go-mail Authors.
|
||||
// Licensed under the MIT License.
|
||||
// See [PROJECT ROOT]/LICENSES directory for more information.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pkcs7
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
_ "crypto/sha256" // for crypto.SHA256
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
OIDData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 1}
|
||||
OIDSignedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2}
|
||||
OIDAttributeContentType = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 3}
|
||||
OIDAttributeMessageDigest = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 4}
|
||||
OIDAttributeSigningTime = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 5}
|
||||
|
||||
OIDDigestAlgorithmSHA256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1}
|
||||
OIDDigestAlgorithmECDSASHA256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 2}
|
||||
|
||||
OIDEncryptionAlgorithmRSASHA256 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 11}
|
||||
)
|
||||
|
||||
// PKCS7 Represents a PKCS7 structure
|
||||
type PKCS7 struct {
|
||||
Content []byte
|
||||
Certificates []*x509.Certificate
|
||||
CRLs []x509.RevocationList
|
||||
Signers []signerInfo
|
||||
}
|
||||
|
||||
type contentInfo struct {
|
||||
ContentType asn1.ObjectIdentifier
|
||||
Content asn1.RawValue `asn1:"explicit,optional,tag:0"`
|
||||
}
|
||||
|
||||
type signedData struct {
|
||||
Version int `asn1:"default:1"`
|
||||
DigestAlgorithmIdentifiers []pkix.AlgorithmIdentifier `asn1:"set"`
|
||||
ContentInfo contentInfo
|
||||
Certificates rawCertificates `asn1:"optional,tag:0"`
|
||||
CRLs []x509.RevocationList `asn1:"optional,tag:1"`
|
||||
SignerInfos []signerInfo `asn1:"set"`
|
||||
}
|
||||
|
||||
type rawCertificates struct {
|
||||
Raw asn1.RawContent
|
||||
}
|
||||
|
||||
func (raw rawCertificates) Parse() ([]*x509.Certificate, error) {
|
||||
if len(raw.Raw) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var val asn1.RawValue
|
||||
if _, err := asn1.Unmarshal(raw.Raw, &val); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return x509.ParseCertificates(val.Bytes)
|
||||
}
|
||||
|
||||
type attribute struct {
|
||||
Type asn1.ObjectIdentifier
|
||||
Value asn1.RawValue `asn1:"set"`
|
||||
}
|
||||
|
||||
type issuerAndSerial struct {
|
||||
IssuerName asn1.RawValue
|
||||
SerialNumber *big.Int
|
||||
}
|
||||
|
||||
// MessageDigestMismatchError is returned when the signer data digest does not
|
||||
// match the computed digest for the contained content
|
||||
type MessageDigestMismatchError struct {
|
||||
ExpectedDigest []byte
|
||||
ActualDigest []byte
|
||||
}
|
||||
|
||||
func (err *MessageDigestMismatchError) Error() string {
|
||||
return fmt.Sprintf("pkcs7: Message digest mismatch\n\tExpected: %X\n\tActual : %X", err.ExpectedDigest, err.ActualDigest)
|
||||
}
|
||||
|
||||
type signerInfo struct {
|
||||
Version int `asn1:"default:1"`
|
||||
IssuerAndSerialNumber issuerAndSerial
|
||||
DigestAlgorithm pkix.AlgorithmIdentifier
|
||||
AuthenticatedAttributes []attribute `asn1:"optional,tag:0"`
|
||||
DigestEncryptionAlgorithm pkix.AlgorithmIdentifier
|
||||
EncryptedDigest []byte
|
||||
UnauthenticatedAttributes []attribute `asn1:"optional,tag:1"`
|
||||
}
|
||||
|
||||
// Attribute represents a key value pair attribute. Value must be marshalable byte
|
||||
// `encoding/asn1`
|
||||
type Attribute struct {
|
||||
Type asn1.ObjectIdentifier
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
// SignerInfoConfig are optional values to include when adding a signer
|
||||
type SignerInfoConfig struct {
|
||||
ExtraSignedAttributes []Attribute
|
||||
ExtraUnsignedAttributes []Attribute
|
||||
}
|
||||
|
||||
type attributes struct {
|
||||
types []asn1.ObjectIdentifier
|
||||
values []interface{}
|
||||
}
|
||||
|
||||
// Add adds the attribute, maintaining insertion order
|
||||
func (as *attributes) Add(attrType asn1.ObjectIdentifier, value interface{}) {
|
||||
as.types = append(as.types, attrType)
|
||||
as.values = append(as.values, value)
|
||||
}
|
||||
|
||||
type sortableAttribute struct {
|
||||
SortKey []byte
|
||||
Attribute attribute
|
||||
}
|
||||
|
||||
type attributeSet []sortableAttribute
|
||||
|
||||
func (as attributeSet) Len() int {
|
||||
return len(as)
|
||||
}
|
||||
|
||||
func (as attributeSet) Less(i, j int) bool {
|
||||
return bytes.Compare(as[i].SortKey, as[j].SortKey) < 0
|
||||
}
|
||||
|
||||
func (as attributeSet) Swap(i, j int) {
|
||||
as[i], as[j] = as[j], as[i]
|
||||
}
|
||||
|
||||
func (as attributeSet) attributes() []attribute {
|
||||
attrs := make([]attribute, len(as))
|
||||
for i, attr := range as {
|
||||
attrs[i] = attr.Attribute
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
|
||||
func (as *attributes) forMarshaling() ([]attribute, error) {
|
||||
sortables := make(attributeSet, len(as.types))
|
||||
for i := range sortables {
|
||||
attrType := as.types[i]
|
||||
attrValue := as.values[i]
|
||||
asn1Value, err := asn1.Marshal(attrValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
attr := attribute{
|
||||
Type: attrType,
|
||||
Value: asn1.RawValue{Tag: 17, IsCompound: true, Bytes: asn1Value}, // 17 == SET tag
|
||||
}
|
||||
encoded, err := asn1.Marshal(attr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sortables[i] = sortableAttribute{
|
||||
SortKey: encoded,
|
||||
Attribute: attr,
|
||||
}
|
||||
}
|
||||
sort.Sort(sortables)
|
||||
return sortables.attributes(), nil
|
||||
}
|
||||
|
||||
// SignedData is an opaque data structure for creating signed data payloads
|
||||
type SignedData struct {
|
||||
sd signedData
|
||||
certs []*x509.Certificate
|
||||
data []byte
|
||||
messageDigest []byte
|
||||
digestOid asn1.ObjectIdentifier
|
||||
}
|
||||
|
||||
// NewSignedData initializes a SignedData with content
|
||||
func NewSignedData(data []byte) (*SignedData, error) {
|
||||
content, err := asn1.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ci := contentInfo{
|
||||
ContentType: OIDData,
|
||||
Content: asn1.RawValue{Class: 2, Tag: 0, Bytes: content, IsCompound: true},
|
||||
}
|
||||
sd := signedData{
|
||||
ContentInfo: ci,
|
||||
Version: 1,
|
||||
}
|
||||
return &SignedData{sd: sd, data: data, digestOid: OIDDigestAlgorithmSHA256}, nil
|
||||
}
|
||||
|
||||
// AddSigner is a wrapper around AddSignerChain() that adds a signer without any parent.
|
||||
func (sd *SignedData) AddSigner(cert *x509.Certificate, pkey crypto.PrivateKey, config SignerInfoConfig) error {
|
||||
var parents []*x509.Certificate
|
||||
return sd.addSignerChain(cert, pkey, parents, config)
|
||||
}
|
||||
|
||||
// addSignerChain signs attributes about the content and adds certificates
|
||||
// and signers infos to the Signed Data. The certificate and private key
|
||||
// of the end-entity signer are used to issue the signature, and any
|
||||
// parent of that end-entity that need to be added to the list of
|
||||
// certifications can be specified in the parents slice.
|
||||
//
|
||||
// The signature algorithm used to hash the data is the one of the end-entity
|
||||
// certificate.
|
||||
func (sd *SignedData) addSignerChain(ee *x509.Certificate, pkey crypto.PrivateKey, parents []*x509.Certificate, config SignerInfoConfig) error {
|
||||
// Following RFC 2315, 9.2 SignerInfo type, the distinguished name of
|
||||
// the issuer of the end-entity signer is stored in the issuerAndSerialNumber
|
||||
// section of the SignedData.SignerInfo, alongside the serial number of
|
||||
// the end-entity.
|
||||
var ias issuerAndSerial
|
||||
ias.SerialNumber = ee.SerialNumber
|
||||
if len(parents) == 0 {
|
||||
// no parent, the issuer is the end-entity cert itself
|
||||
ias.IssuerName = asn1.RawValue{FullBytes: ee.RawIssuer}
|
||||
} else {
|
||||
err := verifyPartialChain(ee, parents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// the first parent is the issuer
|
||||
ias.IssuerName = asn1.RawValue{FullBytes: parents[0].RawSubject}
|
||||
}
|
||||
sd.sd.DigestAlgorithmIdentifiers = append(sd.sd.DigestAlgorithmIdentifiers,
|
||||
pkix.AlgorithmIdentifier{Algorithm: sd.digestOid},
|
||||
)
|
||||
h := crypto.SHA256.New()
|
||||
h.Write(sd.data)
|
||||
sd.messageDigest = h.Sum(nil)
|
||||
encryptionOid, err := getOIDForEncryptionAlgorithm(pkey, sd.digestOid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
attrs := &attributes{}
|
||||
attrs.Add(OIDAttributeContentType, sd.sd.ContentInfo.ContentType)
|
||||
attrs.Add(OIDAttributeMessageDigest, sd.messageDigest)
|
||||
attrs.Add(OIDAttributeSigningTime, time.Now().UTC())
|
||||
for _, attr := range config.ExtraSignedAttributes {
|
||||
attrs.Add(attr.Type, attr.Value)
|
||||
}
|
||||
finalAttrs, err := attrs.forMarshaling()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
unsignedAttrs := &attributes{}
|
||||
for _, attr := range config.ExtraUnsignedAttributes {
|
||||
unsignedAttrs.Add(attr.Type, attr.Value)
|
||||
}
|
||||
finalUnsignedAttrs, err := unsignedAttrs.forMarshaling()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// create signature of signed attributes
|
||||
signature, err := signAttributes(finalAttrs, pkey, crypto.SHA256)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
signer := signerInfo{
|
||||
AuthenticatedAttributes: finalAttrs,
|
||||
UnauthenticatedAttributes: finalUnsignedAttrs,
|
||||
DigestAlgorithm: pkix.AlgorithmIdentifier{Algorithm: sd.digestOid},
|
||||
DigestEncryptionAlgorithm: pkix.AlgorithmIdentifier{Algorithm: encryptionOid},
|
||||
IssuerAndSerialNumber: ias,
|
||||
EncryptedDigest: signature,
|
||||
Version: 1,
|
||||
}
|
||||
sd.certs = append(sd.certs, ee)
|
||||
if len(parents) > 0 {
|
||||
sd.certs = append(sd.certs, parents...)
|
||||
}
|
||||
sd.sd.SignerInfos = append(sd.sd.SignerInfos, signer)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddCertificate adds the certificate to the payload. Useful for parent certificates
|
||||
func (sd *SignedData) AddCertificate(cert *x509.Certificate) {
|
||||
sd.certs = append(sd.certs, cert)
|
||||
}
|
||||
|
||||
// Detach removes content from the signed data struct to make it a detached signature.
|
||||
// This must be called right before Finish()
|
||||
func (sd *SignedData) Detach() {
|
||||
sd.sd.ContentInfo = contentInfo{ContentType: OIDData}
|
||||
}
|
||||
|
||||
// Finish marshals the content and its signers
|
||||
func (sd *SignedData) Finish() ([]byte, error) {
|
||||
sd.sd.Certificates = marshalCertificates(sd.certs)
|
||||
inner, err := asn1.Marshal(sd.sd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outer := contentInfo{
|
||||
ContentType: OIDSignedData,
|
||||
Content: asn1.RawValue{Class: 2, Tag: 0, Bytes: inner, IsCompound: true},
|
||||
}
|
||||
return asn1.Marshal(outer)
|
||||
}
|
||||
|
||||
// verifyPartialChain checks that a given cert is issued by the first parent in the list,
|
||||
// then continue down the path. It doesn't require the last parent to be a root CA,
|
||||
// or to be trusted in any truststore. It simply verifies that the chain provided, albeit
|
||||
// partial, makes sense.
|
||||
func verifyPartialChain(cert *x509.Certificate, parents []*x509.Certificate) error {
|
||||
if len(parents) == 0 {
|
||||
return fmt.Errorf("pkcs7: zero parents provided to verify the signature of certificate %q", cert.Subject.CommonName)
|
||||
}
|
||||
err := cert.CheckSignatureFrom(parents[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("pkcs7: certificate signature from parent is invalid: %w", err)
|
||||
}
|
||||
if len(parents) == 1 {
|
||||
// there is no more parent to check, return
|
||||
return nil
|
||||
}
|
||||
return verifyPartialChain(parents[0], parents[1:])
|
||||
}
|
||||
|
||||
// getOIDForEncryptionAlgorithm takes the private key type of the signer and
|
||||
// the OID of a digest algorithm to return the appropriate signerInfo.DigestEncryptionAlgorithm
|
||||
func getOIDForEncryptionAlgorithm(pkey crypto.PrivateKey, _ asn1.ObjectIdentifier) (asn1.ObjectIdentifier, error) {
|
||||
switch pkey.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return OIDEncryptionAlgorithmRSASHA256, nil
|
||||
case *ecdsa.PrivateKey:
|
||||
return OIDDigestAlgorithmECDSASHA256, nil
|
||||
}
|
||||
return nil, fmt.Errorf("pkcs7: cannot convert encryption algorithm to oid, unknown private key type %T", pkey)
|
||||
}
|
||||
|
||||
// signs the DER encoded form of the attributes with the private key
|
||||
func signAttributes(attrs []attribute, pkey crypto.PrivateKey, hash crypto.Hash) ([]byte, error) {
|
||||
attrBytes, err := marshalAttributes(attrs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h := hash.New()
|
||||
h.Write(attrBytes)
|
||||
hashed := h.Sum(nil)
|
||||
|
||||
key, ok := pkey.(crypto.Signer)
|
||||
if !ok {
|
||||
return nil, errors.New("pkcs7: private key does not implement crypto.Signer")
|
||||
}
|
||||
return key.Sign(rand.Reader, hashed, hash)
|
||||
}
|
||||
|
||||
// concat and wraps the certificates in the RawValue structure
|
||||
func marshalCertificates(certs []*x509.Certificate) rawCertificates {
|
||||
var buf bytes.Buffer
|
||||
for _, cert := range certs {
|
||||
buf.Write(cert.Raw)
|
||||
}
|
||||
rawCerts, _ := marshalCertificateBytes(buf.Bytes())
|
||||
return rawCerts
|
||||
}
|
||||
|
||||
// Even though, the tag & length are stripped out during marshalling the
|
||||
// RawContent, we have to encode it into the RawContent. If It's missing,
|
||||
// then `asn1.Marshal()` will strip out the certificate wrapper instead.
|
||||
func marshalCertificateBytes(certs []byte) (rawCertificates, error) {
|
||||
val := asn1.RawValue{Bytes: certs, Class: 2, Tag: 0, IsCompound: true}
|
||||
b, err := asn1.Marshal(val)
|
||||
if err != nil {
|
||||
return rawCertificates{}, err
|
||||
}
|
||||
return rawCertificates{Raw: b}, nil
|
||||
}
|
||||
|
||||
func marshalAttributes(attrs []attribute) ([]byte, error) {
|
||||
encodedAttributes, err := asn1.Marshal(struct {
|
||||
A []attribute `asn1:"set"`
|
||||
}{A: attrs})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Remove the leading sequence octets
|
||||
var raw asn1.RawValue
|
||||
if _, err := asn1.Unmarshal(encodedAttributes, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return raw.Bytes, nil
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// JSONlog is the default structured JSON logger that satisfies the Logger interface
|
||||
type JSONlog struct {
|
||||
level Level
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// NewJSON returns a new JSONlog type that satisfies the Logger interface
|
||||
func NewJSON(output io.Writer, level Level) *JSONlog {
|
||||
logOpts := slog.HandlerOptions{}
|
||||
switch level {
|
||||
case LevelDebug:
|
||||
logOpts.Level = slog.LevelDebug
|
||||
case LevelInfo:
|
||||
logOpts.Level = slog.LevelInfo
|
||||
case LevelWarn:
|
||||
logOpts.Level = slog.LevelWarn
|
||||
case LevelError:
|
||||
logOpts.Level = slog.LevelError
|
||||
default:
|
||||
logOpts.Level = slog.LevelDebug
|
||||
}
|
||||
logHandler := slog.NewJSONHandler(output, &logOpts)
|
||||
return &JSONlog{
|
||||
level: level,
|
||||
log: slog.New(logHandler),
|
||||
}
|
||||
}
|
||||
|
||||
// logMessage is a helper function to handle different log levels and formats.
|
||||
func logMessage(level Level, log *slog.Logger, logData Log, formatFunc func(string, ...interface{}) string) {
|
||||
lGroup := log.WithGroup(DirString).With(
|
||||
slog.String(DirFromString, logData.directionFrom()),
|
||||
slog.String(DirToString, logData.directionTo()),
|
||||
)
|
||||
switch level {
|
||||
case LevelDebug:
|
||||
lGroup.Debug(formatFunc(logData.Format, logData.Messages...))
|
||||
case LevelInfo:
|
||||
lGroup.Info(formatFunc(logData.Format, logData.Messages...))
|
||||
case LevelWarn:
|
||||
lGroup.Warn(formatFunc(logData.Format, logData.Messages...))
|
||||
case LevelError:
|
||||
lGroup.Error(formatFunc(logData.Format, logData.Messages...))
|
||||
}
|
||||
}
|
||||
|
||||
// Debugf logs a debug message via the structured JSON logger
|
||||
func (l *JSONlog) Debugf(log Log) {
|
||||
if l.level >= LevelDebug {
|
||||
logMessage(LevelDebug, l.log, log, fmt.Sprintf)
|
||||
}
|
||||
}
|
||||
|
||||
// Infof logs a info message via the structured JSON logger
|
||||
func (l *JSONlog) Infof(log Log) {
|
||||
if l.level >= LevelInfo {
|
||||
logMessage(LevelInfo, l.log, log, fmt.Sprintf)
|
||||
}
|
||||
}
|
||||
|
||||
// Warnf logs a warn message via the structured JSON logger
|
||||
func (l *JSONlog) Warnf(log Log) {
|
||||
if l.level >= LevelWarn {
|
||||
logMessage(LevelWarn, l.log, log, fmt.Sprintf)
|
||||
}
|
||||
}
|
||||
|
||||
// Errorf logs a warn message via the structured JSON logger
|
||||
func (l *JSONlog) Errorf(log Log) {
|
||||
if l.level >= LevelError {
|
||||
logMessage(LevelError, l.log, log, fmt.Sprintf)
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package log implements a logger interface that can be used within the go-mail package
|
||||
package log
|
||||
|
||||
const (
|
||||
DirServerToClient Direction = iota // Server to Client communication
|
||||
DirClientToServer // Client to Server communication
|
||||
)
|
||||
|
||||
const (
|
||||
// LevelError is the Level for only ERROR log messages
|
||||
LevelError Level = iota
|
||||
// LevelWarn is the Level for WARN and higher log messages
|
||||
LevelWarn
|
||||
// LevelInfo is the Level for INFO and higher log messages
|
||||
LevelInfo
|
||||
// LevelDebug is the Level for DEBUG and higher log messages
|
||||
LevelDebug
|
||||
)
|
||||
|
||||
const (
|
||||
// DirString is a constant used for the structured logger
|
||||
DirString = "direction"
|
||||
// DirFromString is a constant used for the structured logger
|
||||
DirFromString = "from"
|
||||
// DirToString is a constant used for the structured logger
|
||||
DirToString = "to"
|
||||
)
|
||||
|
||||
// Direction is a type wrapper for the direction a debug log message goes
|
||||
type Direction int
|
||||
|
||||
// Level is a type wrapper for an int
|
||||
type Level int
|
||||
|
||||
// Log represents a log message type that holds a log Direction, a Format string
|
||||
// and a slice of Messages
|
||||
type Log struct {
|
||||
Direction Direction
|
||||
Format string
|
||||
Messages []interface{}
|
||||
}
|
||||
|
||||
// Logger is the log interface for go-mail
|
||||
type Logger interface {
|
||||
Debugf(Log)
|
||||
Infof(Log)
|
||||
Warnf(Log)
|
||||
Errorf(Log)
|
||||
}
|
||||
|
||||
// directionPrefix will return a prefix string depending on the Direction.
|
||||
func (l Log) directionPrefix() string {
|
||||
p := "C <-- S:"
|
||||
if l.Direction == DirClientToServer {
|
||||
p = "C --> S:"
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// directionFrom will return a from direction string depending on the Direction.
|
||||
func (l Log) directionFrom() string {
|
||||
p := "server"
|
||||
if l.Direction == DirClientToServer {
|
||||
p = "client"
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// directionTo will return a to direction string depending on the Direction.
|
||||
func (l Log) directionTo() string {
|
||||
p := "client"
|
||||
if l.Direction == DirClientToServer {
|
||||
p = "server"
|
||||
}
|
||||
return p
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
)
|
||||
|
||||
// Stdlog is the default logger that satisfies the Logger interface
|
||||
type Stdlog struct {
|
||||
level Level
|
||||
err *log.Logger
|
||||
warn *log.Logger
|
||||
info *log.Logger
|
||||
debug *log.Logger
|
||||
}
|
||||
|
||||
// CallDepth is the call depth value for the log.Logger's Output method
|
||||
// This defaults to 2 and is only here for better readablity of the code
|
||||
const CallDepth = 2
|
||||
|
||||
// New returns a new Stdlog type that satisfies the Logger interface
|
||||
func New(output io.Writer, level Level) *Stdlog {
|
||||
lf := log.Lmsgprefix | log.LstdFlags
|
||||
return &Stdlog{
|
||||
level: level,
|
||||
err: log.New(output, "ERROR: ", lf),
|
||||
warn: log.New(output, " WARN: ", lf),
|
||||
info: log.New(output, " INFO: ", lf),
|
||||
debug: log.New(output, "DEBUG: ", lf),
|
||||
}
|
||||
}
|
||||
|
||||
// logStdMessage is a helper function to handle different log levels and formats for Stdlog.
|
||||
func logStdMessage(logger *log.Logger, logData Log, callDepth int) {
|
||||
format := fmt.Sprintf("%s %s", logData.directionPrefix(), logData.Format)
|
||||
_ = logger.Output(callDepth, fmt.Sprintf(format, logData.Messages...))
|
||||
}
|
||||
|
||||
// Debugf performs a Printf() on the debug logger
|
||||
func (l *Stdlog) Debugf(log Log) {
|
||||
if l.level >= LevelDebug {
|
||||
logStdMessage(l.debug, log, CallDepth)
|
||||
}
|
||||
}
|
||||
|
||||
// Infof performs a Printf() on the info logger
|
||||
func (l *Stdlog) Infof(log Log) {
|
||||
if l.level >= LevelInfo {
|
||||
logStdMessage(l.info, log, CallDepth)
|
||||
}
|
||||
}
|
||||
|
||||
// Warnf performs a Printf() on the warn logger
|
||||
func (l *Stdlog) Warnf(log Log) {
|
||||
if l.level >= LevelWarn {
|
||||
logStdMessage(l.warn, log, CallDepth)
|
||||
}
|
||||
}
|
||||
|
||||
// Errorf performs a Printf() on the error logger
|
||||
func (l *Stdlog) Errorf(log Log) {
|
||||
if l.level >= LevelError {
|
||||
logStdMessage(l.err, log, CallDepth)
|
||||
}
|
||||
}
|
||||
+3333
File diff suppressed because it is too large.
Load diff
+27
@@ -0,0 +1,27 @@
|
||||
// SPDX-FileCopyrightText: The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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.
|
||||
func (m *Msg) WriteToTempFile() (string, error) {
|
||||
f, err := os.CreateTemp("", "go-mail_*.eml")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create output file: %w", err)
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
return f.Name(), m.WriteToFile(f.Name())
|
||||
}
|
||||
+606
@@ -0,0 +1,606 @@
|
||||
// SPDX-FileCopyrightText: The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"mime/quotedprintable"
|
||||
"net/textproto"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"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
|
||||
|
||||
// MaxBodyLength defines the maximum line length for the mail body.
|
||||
//
|
||||
// This constant follows the recommendation of RFC 2047, which suggests a maximum length of 76 characters.
|
||||
//
|
||||
// References:
|
||||
// - https://datatracker.ietf.org/doc/html/rfc2047
|
||||
MaxBodyLength = 76
|
||||
|
||||
// SingleNewLine represents a single newline character sequence ("\r\n").
|
||||
//
|
||||
// This constant can be used by the msgWriter to issue a carriage return when writing mail content.
|
||||
SingleNewLine = "\r\n"
|
||||
|
||||
// DoubleNewLine represents a double newline character sequence ("\r\n\r\n").
|
||||
//
|
||||
// This constant can be used by the msgWriter to indicate a new segment of the mail when writing mail content.
|
||||
DoubleNewLine = "\r\n\r\n"
|
||||
)
|
||||
|
||||
// msgWriter handles the I/O operations for writing to the io.WriteCloser of the SMTP client.
|
||||
//
|
||||
// This struct keeps track of the number of bytes written, the character set used, and the depth of the
|
||||
// current multipart section. It also handles encoding, error tracking, and managing multipart and part
|
||||
// writers for constructing the email message body.
|
||||
type msgWriter struct {
|
||||
bytesWritten int64
|
||||
charset Charset
|
||||
depth int8
|
||||
encoder mime.WordEncoder
|
||||
err error
|
||||
multiPartWriter [4]*multipart.Writer
|
||||
partWriter io.Writer
|
||||
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.
|
||||
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)
|
||||
}
|
||||
|
||||
var n int
|
||||
n, mw.err = mw.writer.Write(payload)
|
||||
mw.bytesWritten += int64(n)
|
||||
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)
|
||||
func (mw *msgWriter) writeMsg(msg *Msg) {
|
||||
msg.addDefaultHeader()
|
||||
msg.checkUserAgent()
|
||||
mw.writeGenHeader(msg)
|
||||
mw.writePreformattedGenHeader(msg)
|
||||
|
||||
// Set the FROM header (or envelope FROM if FROM is empty)
|
||||
hasFrom := true
|
||||
from, ok := msg.addrHeader[HeaderFrom]
|
||||
if !ok || (len(from) == 0 || from == nil) {
|
||||
from, ok = msg.addrHeader[HeaderEnvelopeFrom]
|
||||
if !ok || (len(from) == 0 || from == nil) {
|
||||
hasFrom = false
|
||||
}
|
||||
}
|
||||
if hasFrom && (len(from) > 0 && from[0] != nil) {
|
||||
msg.headerCount += mw.writeHeader(Header(HeaderFrom), from[0].String())
|
||||
}
|
||||
|
||||
// Set the rest of the address headers
|
||||
for _, to := range []AddrHeader{HeaderTo, HeaderCc, HeaderReplyTo} {
|
||||
if addresses, ok := msg.addrHeader[to]; ok {
|
||||
var val []string
|
||||
for _, addr := range addresses {
|
||||
if addr == nil {
|
||||
continue
|
||||
}
|
||||
val = append(val, addr.String())
|
||||
}
|
||||
msg.headerCount += mw.writeHeader(Header(to), val...)
|
||||
}
|
||||
}
|
||||
|
||||
if msg.hasSMIME() && !msg.isSMIMEInProgress() {
|
||||
boundary, err := randomBoundary()
|
||||
if err != nil {
|
||||
mw.err = err
|
||||
return
|
||||
}
|
||||
mw.startMP(msg, MIMESMIMESigned, boundary)
|
||||
mw.writeString(DoubleNewLine)
|
||||
}
|
||||
if msg.hasMixed() {
|
||||
boundary := mw.getMultipartBoundary(msg, MIMEMixed)
|
||||
boundary = mw.startMP(msg, MIMEMixed, boundary)
|
||||
msg.multiPartBoundary[MIMEMixed] = boundary
|
||||
if mw.depth == 1 {
|
||||
mw.writeString(DoubleNewLine)
|
||||
}
|
||||
}
|
||||
if msg.hasRelated() {
|
||||
boundary := mw.getMultipartBoundary(msg, MIMERelated)
|
||||
boundary = mw.startMP(msg, MIMERelated, boundary)
|
||||
msg.multiPartBoundary[MIMERelated] = boundary
|
||||
if mw.depth == 1 {
|
||||
mw.writeString(DoubleNewLine)
|
||||
}
|
||||
}
|
||||
if msg.hasAlt() {
|
||||
boundary := mw.getMultipartBoundary(msg, MIMEAlternative)
|
||||
boundary = mw.startMP(msg, MIMEAlternative, boundary)
|
||||
msg.multiPartBoundary[MIMEAlternative] = boundary
|
||||
if mw.depth == 1 {
|
||||
mw.writeString(DoubleNewLine)
|
||||
}
|
||||
}
|
||||
if msg.hasPGPType() {
|
||||
switch msg.pgptype {
|
||||
case PGPEncrypt:
|
||||
mw.startMP(msg, `encrypted; protocol="application/pgp-encrypted"`,
|
||||
msg.boundary)
|
||||
case PGPSignature:
|
||||
mw.startMP(msg, `signed; protocol="application/pgp-signature";`,
|
||||
msg.boundary)
|
||||
default:
|
||||
}
|
||||
mw.writeString(DoubleNewLine)
|
||||
}
|
||||
|
||||
for _, part := range msg.parts {
|
||||
if !part.isDeleted && !part.smime {
|
||||
mw.writePart(part, msg.charset)
|
||||
}
|
||||
}
|
||||
|
||||
if msg.hasAlt() {
|
||||
mw.stopMP()
|
||||
}
|
||||
|
||||
// Add embeds
|
||||
mw.addFiles(msg.embeds, false)
|
||||
if msg.hasRelated() {
|
||||
mw.stopMP()
|
||||
}
|
||||
|
||||
// Add attachments
|
||||
mw.addFiles(msg.attachments, true)
|
||||
if msg.hasMixed() {
|
||||
mw.stopMP()
|
||||
}
|
||||
|
||||
if msg.hasSMIME() && !msg.isSMIMEInProgress() {
|
||||
for _, part := range msg.parts {
|
||||
if part.smime {
|
||||
mw.writePart(part, msg.charset)
|
||||
}
|
||||
}
|
||||
mw.stopMP()
|
||||
}
|
||||
}
|
||||
|
||||
// writeGenHeader writes out all generic headers to the msgWriter.
|
||||
//
|
||||
// This function extracts all generic headers from the provided Msg object, sorts them, and writes them
|
||||
// to the msgWriter in alphabetical order.
|
||||
//
|
||||
// Parameters:
|
||||
// - msg: The Msg object containing the headers to be written.
|
||||
func (mw *msgWriter) writeGenHeader(msg *Msg) {
|
||||
keys := make([]string, 0, len(msg.genHeader))
|
||||
for key := range msg.genHeader {
|
||||
keys = append(keys, string(key))
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
msg.headerCount += mw.writeHeader(Header(key), msg.genHeader[Header(key)]...)
|
||||
}
|
||||
}
|
||||
|
||||
// writePreformattedGenHeader writes out all preformatted generic headers to the msgWriter.
|
||||
//
|
||||
// This function iterates over all preformatted generic headers from the provided Msg object and writes
|
||||
// them to the msgWriter in the format "key: value" followed by a newline.
|
||||
//
|
||||
// Parameters:
|
||||
// - msg: The Msg object containing the preformatted headers to be written.
|
||||
func (mw *msgWriter) writePreformattedGenHeader(msg *Msg) {
|
||||
for key, val := range msg.preformHeader {
|
||||
line := fmt.Sprintf("%s: %s%s", key, val, SingleNewLine)
|
||||
mw.writeString(line)
|
||||
msg.headerCount += strings.Count(line, 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:
|
||||
// - msg: The message whose multipart headers are being written.
|
||||
// - mimeType: The MIME type of the multipart content (e.g., "mixed", "alternative").
|
||||
// - boundary: The boundary string separating different parts of the multipart message.
|
||||
//
|
||||
// Returns:
|
||||
// - The multipart boundary string
|
||||
//
|
||||
// References:
|
||||
// - https://datatracker.ietf.org/doc/html/rfc2046
|
||||
func (mw *msgWriter) startMP(msg *Msg, mimeType MIMEType, boundary string) string {
|
||||
multiPartWriter := multipart.NewWriter(mw)
|
||||
if boundary != "" {
|
||||
mw.err = multiPartWriter.SetBoundary(boundary)
|
||||
}
|
||||
|
||||
contentType := fmt.Sprintf("multipart/%s;\r\n boundary=%s", mimeType,
|
||||
multiPartWriter.Boundary())
|
||||
mw.multiPartWriter[mw.depth] = multiPartWriter
|
||||
|
||||
// Do not write Content-Type if the header was already written as part of genHeaders.
|
||||
if _, ok := msg.genHeader[HeaderContentType]; !ok {
|
||||
if mw.depth == 0 {
|
||||
mw.writeString(fmt.Sprintf("%s: %s", HeaderContentType, contentType))
|
||||
}
|
||||
if mw.depth > 0 {
|
||||
mw.newPart(map[string][]string{"Content-Type": {contentType}})
|
||||
}
|
||||
}
|
||||
|
||||
mw.depth++
|
||||
return multiPartWriter.Boundary()
|
||||
}
|
||||
|
||||
// stopMP closes the multipart.
|
||||
//
|
||||
// This function closes the current multipart writer if there is an active multipart structure.
|
||||
// It decreases the depth level of multipart nesting.
|
||||
func (mw *msgWriter) stopMP() {
|
||||
if mw.depth > 0 {
|
||||
mw.err = mw.multiPartWriter[mw.depth-1].Close()
|
||||
mw.depth--
|
||||
}
|
||||
}
|
||||
|
||||
// getMultipartBoundary returns the appropriate multipart boundary for the given MIME type.
|
||||
//
|
||||
// If the Msg has a predefined boundary, it is returned. Otherwise, the function checks
|
||||
// for a MIME type-specific boundary in the Msg's multiPartBoundary map. If no boundary
|
||||
// is found, an empty string is returned.
|
||||
//
|
||||
// Parameters:
|
||||
// - msg: A pointer to the Msg containing the boundary and MIME type-specific mappings.
|
||||
// - mimetype: The MIMEType for which the boundary is being determined.
|
||||
//
|
||||
// Returns:
|
||||
// - A string representing the multipart boundary, or an empty string if none is found.
|
||||
func (mw *msgWriter) getMultipartBoundary(msg *Msg, mimetype MIMEType) string {
|
||||
if msg.boundary != "" {
|
||||
return msg.boundary
|
||||
}
|
||||
if msg.multiPartBoundary[mimetype] != "" {
|
||||
return msg.multiPartBoundary[mimetype]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// addFiles adds the attachments/embeds file content to the mail body.
|
||||
//
|
||||
// This function iterates through the list of files, setting necessary headers for each file,
|
||||
// including Content-Type, Content-Transfer-Encoding, Content-Disposition, and Content-ID
|
||||
// (if the file is an embed). It determines the appropriate MIME type for each file based on
|
||||
// its extension or the provided ContentType. It writes file headers and file content
|
||||
// to the mail body using the appropriate encoding.
|
||||
//
|
||||
// Parameters:
|
||||
// - files: A slice of File objects to be added to the mail body.
|
||||
// - isAttachment: A boolean indicating whether the files are attachments (true) or embeds (false).
|
||||
func (mw *msgWriter) addFiles(files []*File, isAttachment bool) {
|
||||
for _, file := range files {
|
||||
encoding := EncodingB64
|
||||
if _, ok := file.getHeader(HeaderContentType); !ok {
|
||||
mimeType := mime.TypeByExtension(filepath.Ext(file.Name))
|
||||
if mimeType == "" {
|
||||
mimeType = "application/octet-stream"
|
||||
}
|
||||
if file.ContentType != "" {
|
||||
mimeType = string(file.ContentType)
|
||||
}
|
||||
file.setHeader(HeaderContentType, fmt.Sprintf(`%s; name="%s"`, mimeType,
|
||||
mw.encoder.Encode(mw.charset.String(), sanitizeFilename(file.Name))))
|
||||
}
|
||||
|
||||
if _, ok := file.getHeader(HeaderContentTransferEnc); !ok {
|
||||
if file.Enc != "" {
|
||||
encoding = file.Enc
|
||||
}
|
||||
file.setHeader(HeaderContentTransferEnc, string(encoding))
|
||||
}
|
||||
|
||||
if file.Desc != "" {
|
||||
if _, ok := file.getHeader(HeaderContentDescription); !ok {
|
||||
file.setHeader(HeaderContentDescription, mw.encoder.Encode(mw.charset.String(), file.Desc))
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := file.getHeader(HeaderContentDisposition); !ok {
|
||||
disposition := "inline"
|
||||
if isAttachment {
|
||||
disposition = "attachment"
|
||||
}
|
||||
file.setHeader(HeaderContentDisposition, fmt.Sprintf(`%s; filename="%s"`,
|
||||
disposition, mw.encoder.Encode(mw.charset.String(), sanitizeFilename(file.Name))))
|
||||
}
|
||||
|
||||
if !isAttachment {
|
||||
if _, ok := file.getHeader(HeaderContentID); !ok {
|
||||
file.setHeader(HeaderContentID, fmt.Sprintf("<%s>", sanitizeFilename(file.Name)))
|
||||
}
|
||||
}
|
||||
if mw.depth == 0 {
|
||||
for header, val := range file.Header {
|
||||
mw.writeHeader(Header(header), val...)
|
||||
}
|
||||
mw.writeString(SingleNewLine)
|
||||
}
|
||||
if mw.depth > 0 {
|
||||
mw.newPart(file.Header)
|
||||
}
|
||||
|
||||
if mw.err == nil {
|
||||
mw.writeBody(file.Writer, encoding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// newPart creates a new MIME multipart io.Writer and sets the partWriter to it.
|
||||
//
|
||||
// This function creates a new MIME part using the provided header information and assigns it
|
||||
// to the partWriter. It interacts with the current multipart writer at the specified depth
|
||||
// to create the part.
|
||||
//
|
||||
// Parameters:
|
||||
// - header: A map containing the header fields and their corresponding values for the new part.
|
||||
func (mw *msgWriter) newPart(header map[string][]string) {
|
||||
mw.partWriter, mw.err = mw.multiPartWriter[mw.depth-1].CreatePart(header)
|
||||
}
|
||||
|
||||
// writePart writes the corresponding part to the Msg body.
|
||||
//
|
||||
// This function writes a MIME part to the message body, setting the appropriate headers such
|
||||
// as Content-Type and Content-Transfer-Encoding. It determines the charset for the part,
|
||||
// either using the part's own charset or a fallback charset if none is specified. If the part
|
||||
// is at the top level (depth 0), headers are written directly. For nested parts, it creates
|
||||
// a new MIME part with the provided headers.
|
||||
//
|
||||
// Parameters:
|
||||
// - part: The Part object containing the data to be written.
|
||||
// - charset: The Charset used as a fallback if the part does not specify one.
|
||||
func (mw *msgWriter) writePart(part *Part, charset Charset) {
|
||||
partCharset := part.charset
|
||||
if partCharset.String() == "" {
|
||||
partCharset = charset
|
||||
}
|
||||
|
||||
contentType := fmt.Sprintf("%s; charset=%s", part.contentType, partCharset)
|
||||
if part.smime {
|
||||
contentType = part.contentType.String()
|
||||
}
|
||||
contentTransferEnc := part.encoding.String()
|
||||
|
||||
if mw.depth == 0 {
|
||||
mw.writeHeader(HeaderContentTransferEnc, contentTransferEnc)
|
||||
mw.writeHeader(HeaderContentType, contentType)
|
||||
mw.writeString(SingleNewLine)
|
||||
}
|
||||
if mw.depth > 0 {
|
||||
mimeHeader := textproto.MIMEHeader{}
|
||||
if part.description != "" {
|
||||
mimeHeader.Add(string(HeaderContentDescription), part.description)
|
||||
}
|
||||
mimeHeader.Add(string(HeaderContentTransferEnc), contentTransferEnc)
|
||||
mimeHeader.Add(string(HeaderContentType), contentType)
|
||||
mw.newPart(mimeHeader)
|
||||
}
|
||||
mw.writeBody(part.writeFunc, part.encoding)
|
||||
}
|
||||
|
||||
// writeString writes a string into the msgWriter's io.Writer interface.
|
||||
//
|
||||
// This function writes the given string to the msgWriter's underlying writer. It checks for
|
||||
// existing errors before performing the write operation. It also tracks the number of bytes
|
||||
// written and updates the bytesWritten field accordingly.
|
||||
//
|
||||
// Parameters:
|
||||
// - s: The string to be written.
|
||||
func (mw *msgWriter) writeString(s string) {
|
||||
if mw.err != nil {
|
||||
return
|
||||
}
|
||||
var n int
|
||||
n, mw.err = io.WriteString(mw.writer, s)
|
||||
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.
|
||||
func (mw *msgWriter) writeHeader(key Header, values ...string) int {
|
||||
lines := 0
|
||||
buffer := strings.Builder{}
|
||||
charLength := MaxHeaderLength - 2
|
||||
buffer.WriteString(string(key))
|
||||
charLength -= len(key)
|
||||
if len(values) == 0 {
|
||||
buffer.WriteString(":\r\n")
|
||||
return lines + 1
|
||||
}
|
||||
buffer.WriteString(": ")
|
||||
charLength -= 2
|
||||
|
||||
fullValueStr := strings.Join(values, ", ")
|
||||
words := strings.Split(fullValueStr, " ")
|
||||
for i, val := range words {
|
||||
if charLength-len(val) <= 1 {
|
||||
buffer.WriteString(fmt.Sprintf("%s ", SingleNewLine))
|
||||
charLength = MaxHeaderLength - 3
|
||||
}
|
||||
buffer.WriteString(val)
|
||||
if i < len(words)-1 {
|
||||
buffer.WriteString(" ")
|
||||
charLength -= 1
|
||||
}
|
||||
charLength -= len(val)
|
||||
}
|
||||
|
||||
bufferString := buffer.String()
|
||||
bufferString = strings.ReplaceAll(bufferString, fmt.Sprintf(" %s", SingleNewLine),
|
||||
SingleNewLine)
|
||||
mw.writeString(bufferString)
|
||||
mw.writeString("\r\n")
|
||||
|
||||
lines += strings.Count(bufferString, SingleNewLine) + 1
|
||||
return lines
|
||||
}
|
||||
|
||||
// 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).
|
||||
func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encoding Encoding) {
|
||||
var writer io.Writer
|
||||
var encodedWriter io.WriteCloser
|
||||
var n int64
|
||||
var err error
|
||||
if mw.depth == 0 {
|
||||
writer = mw.writer
|
||||
}
|
||||
if mw.depth > 0 {
|
||||
writer = mw.partWriter
|
||||
}
|
||||
writeBuffer := bytes.Buffer{}
|
||||
lineBreaker := base64LineBreaker{}
|
||||
lineBreaker.out = &writeBuffer
|
||||
|
||||
switch encoding {
|
||||
case EncodingQP:
|
||||
encodedWriter = quotedprintable.NewWriter(&writeBuffer)
|
||||
case EncodingB64:
|
||||
encodedWriter = base64.NewEncoder(base64.StdEncoding, &lineBreaker)
|
||||
case NoEncoding:
|
||||
_, err = writeFunc(&writeBuffer)
|
||||
if err != nil {
|
||||
mw.err = fmt.Errorf("bodyWriter function: %w", err)
|
||||
}
|
||||
n, err = io.Copy(writer, &writeBuffer)
|
||||
if err != nil && mw.err == nil {
|
||||
mw.err = fmt.Errorf("bodyWriter io.Copy: %w", err)
|
||||
}
|
||||
if mw.depth == 0 {
|
||||
mw.bytesWritten += n
|
||||
}
|
||||
return
|
||||
default:
|
||||
encodedWriter = quotedprintable.NewWriter(writer)
|
||||
}
|
||||
|
||||
_, err = writeFunc(encodedWriter)
|
||||
if err != nil {
|
||||
mw.err = fmt.Errorf("bodyWriter function: %w", err)
|
||||
}
|
||||
err = encodedWriter.Close()
|
||||
if err != nil && mw.err == nil {
|
||||
mw.err = fmt.Errorf("bodyWriter close encoded writer: %w", err)
|
||||
}
|
||||
err = lineBreaker.Close()
|
||||
if err != nil && mw.err == nil {
|
||||
mw.err = fmt.Errorf("bodyWriter close linebreaker: %w", err)
|
||||
}
|
||||
n, err = io.Copy(writer, &writeBuffer)
|
||||
if err != nil && mw.err == nil {
|
||||
mw.err = fmt.Errorf("bodyWriter io.Copy: %w", err)
|
||||
}
|
||||
|
||||
// Since the part writer uses the WriteTo() method, we don't need to add the
|
||||
// bytes twice
|
||||
if mw.depth == 0 {
|
||||
mw.bytesWritten += n
|
||||
}
|
||||
}
|
||||
|
||||
// sanitizeFilename sanitizes a given filename string by replacing specific unwanted characters with
|
||||
// an underscore ('_').
|
||||
//
|
||||
// This method replaces any control character and any special character that is problematic for
|
||||
// MIME headers and file systems with an underscore ('_') character.
|
||||
//
|
||||
// The following characters are replaced
|
||||
// - Any control character (US-ASCII < 32)
|
||||
// - ", /, :, <, >, ?, \, |, [DEL]
|
||||
//
|
||||
// Parameters:
|
||||
// - input: A string of a filename that is supposed to be sanitized
|
||||
//
|
||||
// Returns:
|
||||
// - A string representing the sanitized version of the filename
|
||||
func sanitizeFilename(input string) string {
|
||||
var sanitized strings.Builder
|
||||
for i := 0; i < len(input); i++ {
|
||||
// We do not allow control characters in file names.
|
||||
if input[i] < 32 || input[i] == 34 || input[i] == 47 || input[i] == 58 ||
|
||||
input[i] == 60 || input[i] == 62 || input[i] == 63 || input[i] == 92 ||
|
||||
input[i] == 124 || input[i] == 127 {
|
||||
sanitized.WriteRune('_')
|
||||
continue
|
||||
}
|
||||
sanitized.WriteByte(input[i])
|
||||
}
|
||||
return sanitized.String()
|
||||
}
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
// SPDX-FileCopyrightText: The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
)
|
||||
|
||||
// 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.
|
||||
type Part struct {
|
||||
contentType ContentType
|
||||
charset Charset
|
||||
description string
|
||||
encoding Encoding
|
||||
isDeleted bool
|
||||
writeFunc func(io.Writer) (int64, error)
|
||||
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.
|
||||
func (p *Part) GetContent() ([]byte, error) {
|
||||
var b bytes.Buffer
|
||||
if _, err := p.writeFunc(&b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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).
|
||||
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.
|
||||
func (p *Part) GetDescription() string {
|
||||
return p.description
|
||||
}
|
||||
|
||||
// SetContent overrides the content of the Part with the given string.
|
||||
//
|
||||
// This function sets the content of the Part by creating a new writeFunc that writes the
|
||||
// provided string content to an io.Writer.
|
||||
//
|
||||
// Parameters:
|
||||
// - content: The string that will replace the current content of the Part.
|
||||
func (p *Part) SetContent(content string) {
|
||||
buffer := bytes.NewBufferString(content)
|
||||
p.writeFunc = writeFuncFromBuffer(buffer)
|
||||
}
|
||||
|
||||
// SetContentType overrides the ContentType of the Part.
|
||||
//
|
||||
// This function sets a new ContentType for the Part, replacing the existing one.
|
||||
//
|
||||
// Parameters:
|
||||
// - contentType: The new ContentType to be set for the Part.
|
||||
func (p *Part) SetContentType(contentType ContentType) {
|
||||
p.contentType = contentType
|
||||
}
|
||||
|
||||
// SetCharset overrides the Charset of the Part.
|
||||
//
|
||||
// This function sets a new Charset for the Part, replacing the existing one.
|
||||
//
|
||||
// Parameters:
|
||||
// - charset: The new Charset to be set for the Part.
|
||||
func (p *Part) SetCharset(charset Charset) {
|
||||
p.charset = charset
|
||||
}
|
||||
|
||||
// SetEncoding creates a new mime.WordEncoder based on the encoding setting of the message.
|
||||
//
|
||||
// This function sets a new Encoding for the Part, replacing the existing one.
|
||||
//
|
||||
// Parameters:
|
||||
// - encoding: The new Encoding to be set for the Part.
|
||||
func (p *Part) SetEncoding(encoding Encoding) {
|
||||
p.encoding = encoding
|
||||
}
|
||||
|
||||
// SetDescription overrides the Content-Description of the Part.
|
||||
//
|
||||
// This function sets a new Content-Description for the Part, replacing the existing one.
|
||||
//
|
||||
// Parameters:
|
||||
// - description: The new Content-Description to be set for the Part.
|
||||
func (p *Part) SetDescription(description string) {
|
||||
p.description = description
|
||||
}
|
||||
|
||||
// SetIsSMIMESigned sets the flag for signing the Part with S/MIME.
|
||||
//
|
||||
// This function updates the S/MIME signing flag for the Part.
|
||||
//
|
||||
// Parameters:
|
||||
// - smime: A boolean indicating whether the Part should be signed with S/MIME.
|
||||
func (p *Part) SetIsSMIMESigned(smime bool) {
|
||||
p.smime = smime
|
||||
}
|
||||
|
||||
// SetWriteFunc overrides the WriteFunc of the Part.
|
||||
//
|
||||
// This function sets a new WriteFunc for the Part, replacing the existing one. The WriteFunc
|
||||
// is responsible for writing the Part's content to an io.Writer.
|
||||
//
|
||||
// Parameters:
|
||||
// - writeFunc: A function that writes the Part's content to an io.Writer and returns
|
||||
// the number of bytes written and an error (if any).
|
||||
func (p *Part) SetWriteFunc(writeFunc func(io.Writer) (int64, error)) {
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
func WithPartContentDescription(description string) PartOption {
|
||||
return func(p *Part) {
|
||||
p.description = description
|
||||
}
|
||||
}
|
||||
|
||||
// WithSMIMESigning enables the S/MIME signing flag for a Part.
|
||||
//
|
||||
// This function provides a PartOption that overrides the S/MIME signing flag to enable signing.
|
||||
//
|
||||
// Returns:
|
||||
// - A PartOption that sets the S/MIME signing flag to true.
|
||||
func WithSMIMESigning() PartOption {
|
||||
return func(p *Part) {
|
||||
p.smime = true
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
// SPDX-FileCopyrightText: The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type AuthData struct {
|
||||
Auth bool
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
var testHookTLSConfig func() *tls.Config // nil, except for tests
|
||||
|
||||
// QuickSend is an all-in-one method for quickly sending simple text mails in go-mail.
|
||||
//
|
||||
// This method will create a new client that connects to the server at addr, switches to TLS if possible,
|
||||
// authenticates with the optional AuthData provided in auth and create a new simple Msg with the provided
|
||||
// subject string and message bytes as body. The message will be sent using from as sender address and will
|
||||
// be delivered to every address in rcpts. QuickSend will always send as text/plain ContentType.
|
||||
//
|
||||
// For the SMTP authentication, if auth is not nil and AuthData.Auth is set to true, it will try to
|
||||
// autodiscover the best SMTP authentication mechanism supported by the server. If auth is set to true
|
||||
// but autodiscover is not able to find a suitable authentication mechanism or if the authentication
|
||||
// fails, the mail delivery will fail completely.
|
||||
//
|
||||
// The content parameter should be an RFC 822-style email body. The lines of content should be CRLF terminated.
|
||||
//
|
||||
// Parameters:
|
||||
// - addr: The hostname and port of the mail server, it must include a port, as in "mail.example.com:smtp".
|
||||
// - auth: A AuthData pointer. If nil or if AuthData.Auth is set to false, not SMTP authentication will be performed.
|
||||
// - from: The from address of the sender as string.
|
||||
// - rcpts: A slice of strings of receipient addresses.
|
||||
// - subject: The subject line as string.
|
||||
// - content: A byte slice of the mail content
|
||||
//
|
||||
// Returns:
|
||||
// - A pointer to the generated Msg.
|
||||
// - An error if any step in the process of mail generation or delivery failed.
|
||||
func QuickSend(addr string, auth *AuthData, from string, rcpts []string, subject string, content []byte) (*Msg, error) {
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to split host and port from address: %w", err)
|
||||
}
|
||||
portnum, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert port to int: %w", err)
|
||||
}
|
||||
client, err := NewClient(host, WithPort(portnum), WithTLSPolicy(TLSOpportunistic))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create new client: %w", err)
|
||||
}
|
||||
|
||||
if auth != nil && auth.Auth {
|
||||
client.SetSMTPAuth(SMTPAuthAutoDiscover)
|
||||
client.SetUsername(auth.Username)
|
||||
client.SetPassword(auth.Password)
|
||||
}
|
||||
|
||||
tlsConfig := client.tlsconfig
|
||||
if testHookTLSConfig != nil {
|
||||
tlsConfig = testHookTLSConfig()
|
||||
}
|
||||
if err = client.SetTLSConfig(tlsConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to set TLS config: %w", err)
|
||||
}
|
||||
|
||||
message := NewMsg()
|
||||
if err = message.From(from); err != nil {
|
||||
return nil, fmt.Errorf("failed to set MAIL FROM address: %w", err)
|
||||
}
|
||||
if err = message.To(rcpts...); err != nil {
|
||||
return nil, fmt.Errorf("failed to set RCPT TO address: %w", err)
|
||||
}
|
||||
message.Subject(subject)
|
||||
buffer := bytes.NewBuffer(content)
|
||||
writeFunc := writeFuncFromBuffer(buffer)
|
||||
message.SetBodyWriter(TypeTextPlain, writeFunc)
|
||||
|
||||
if err = client.DialAndSend(message); err != nil {
|
||||
return nil, fmt.Errorf("failed to dial and send message: %w", err)
|
||||
}
|
||||
return message, nil
|
||||
}
|
||||
|
||||
// NewAuthData creates a new AuthData instance with the provided username and password.
|
||||
//
|
||||
// This function initializes an AuthData struct with authentication enabled and sets the
|
||||
// username and password fields.
|
||||
//
|
||||
// Parameters:
|
||||
// - user: The username for authentication.
|
||||
// - pass: The password for authentication.
|
||||
//
|
||||
// Returns:
|
||||
// - A pointer to the initialized AuthData instance.
|
||||
func NewAuthData(user, pass string) *AuthData {
|
||||
return &AuthData{
|
||||
Auth: true,
|
||||
Username: user,
|
||||
Password: pass,
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
// SPDX-FileCopyrightText: The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Range of characters for the secure string generation
|
||||
const cr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-"
|
||||
|
||||
// Bitmask sizes for the string generators (based on 93 chars total)
|
||||
//
|
||||
// These constants define bitmask-related values used for efficient random string generation.
|
||||
// The bitmask operates over 66 possible characters, and the constants help determine the
|
||||
// number of bits and indices used in the process.
|
||||
const (
|
||||
// letterIdxBits: Number of bits needed to represent a letter index. We have 64 possible characters
|
||||
// which fit into 6 bits.
|
||||
letterIdxBits = 6
|
||||
// letterIdxMask: Bitmask to extract letter indices (all 1-bits for letterIdxBits).
|
||||
letterIdxMask = 1<<letterIdxBits - 1
|
||||
// letterIdxMax: The maximum number of letter indices that fit in 63 bits.
|
||||
letterIdxMax = 63 / letterIdxBits
|
||||
)
|
||||
|
||||
// randomStringSecure returns a random string of the specified length.
|
||||
//
|
||||
// This function generates a cryptographically secure random string of the given length using
|
||||
// the crypto/rand package. It ensures that the randomness is secure and suitable for
|
||||
// cryptographic purposes. The function reads random bytes, converts them to indices within
|
||||
// a character range, and builds the string. If an error occurs while reading from the random
|
||||
// pool, it returns the error.
|
||||
//
|
||||
// Parameters:
|
||||
// - length: The length of the random string to be generated.
|
||||
//
|
||||
// Returns:
|
||||
// - A randomly generated string.
|
||||
// - An error if the random generation fails.
|
||||
func randomStringSecure(length int) (string, error) {
|
||||
randString := strings.Builder{}
|
||||
randString.Grow(length)
|
||||
charRangeLength := len(cr)
|
||||
|
||||
randPool := make([]byte, 8)
|
||||
_, err := rand.Read(randPool)
|
||||
if err != nil {
|
||||
return randString.String(), err
|
||||
}
|
||||
for idx, char, rest := length-1, binary.BigEndian.Uint64(randPool), letterIdxMax; idx >= 0; {
|
||||
if rest == 0 {
|
||||
_, err = rand.Read(randPool)
|
||||
if err != nil {
|
||||
return randString.String(), err
|
||||
}
|
||||
char, rest = binary.BigEndian.Uint64(randPool), letterIdxMax
|
||||
}
|
||||
if i := int(char & letterIdxMask); i < charRangeLength {
|
||||
randString.WriteByte(cr[i])
|
||||
idx--
|
||||
}
|
||||
char >>= letterIdxBits
|
||||
rest--
|
||||
}
|
||||
|
||||
return randString.String(), nil
|
||||
}
|
||||
|
||||
// randomBoundary generates a random boundary string for use in MIME messages.
|
||||
//
|
||||
// This function creates a 30-byte random value using a cryptographic random number generator
|
||||
// and formats it as a hexadecimal string.
|
||||
//
|
||||
// Returns:
|
||||
// - A string containing the generated random boundary.
|
||||
// - An error if reading from the random number generator fails.
|
||||
func randomBoundary() (string, error) {
|
||||
var buf [30]byte
|
||||
_, err := io.ReadFull(rand.Reader, buf[:])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read from rand.Reader: %w", err)
|
||||
}
|
||||
return fmt.Sprintf("%x", buf[:]), nil
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
// SPDX-FileCopyrightText: The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mail
|
||||
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
func (r *Reader) Read(payload []byte) (n int, err error) {
|
||||
if r.err != nil {
|
||||
return 0, r.err
|
||||
}
|
||||
if r.empty() || r.buffer == nil {
|
||||
r.Reset()
|
||||
if len(payload) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, io.EOF
|
||||
}
|
||||
n = copy(payload, r.buffer[r.offset:])
|
||||
r.offset += n
|
||||
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.
|
||||
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 }
|
||||
+302
@@ -0,0 +1,302 @@
|
||||
// SPDX-FileCopyrightText: The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// List of SendError reasons
|
||||
const (
|
||||
// ErrGetSender is returned if the Msg.GetSender method fails during a Client.Send
|
||||
ErrGetSender SendErrReason = iota
|
||||
|
||||
// ErrGetRcpts is returned if the Msg.GetRecipients method fails during a Client.Send
|
||||
ErrGetRcpts
|
||||
|
||||
// ErrSMTPMailFrom is returned if the Msg delivery failed when sending the MAIL FROM command
|
||||
// to the sending SMTP server
|
||||
ErrSMTPMailFrom
|
||||
|
||||
// ErrSMTPRcptTo is returned if the Msg delivery failed when sending the RCPT TO command
|
||||
// to the sending SMTP server
|
||||
ErrSMTPRcptTo
|
||||
|
||||
// ErrSMTPData is returned if the Msg delivery failed when sending the DATA command
|
||||
// to the sending SMTP server
|
||||
ErrSMTPData
|
||||
|
||||
// ErrSMTPDataClose is returned if the Msg delivery failed when trying to close the
|
||||
// Client data writer
|
||||
ErrSMTPDataClose
|
||||
|
||||
// ErrSMTPReset is returned if the Msg delivery failed when sending the RSET command
|
||||
// to the sending SMTP server
|
||||
ErrSMTPReset
|
||||
|
||||
// ErrWriteContent is returned if the Msg delivery failed when sending Msg content
|
||||
// to the Client writer
|
||||
ErrWriteContent
|
||||
|
||||
// ErrConnCheck is returned if the Msg delivery failed when checking if the SMTP
|
||||
// server connection is still working
|
||||
ErrConnCheck
|
||||
|
||||
// ErrNoUnencoded is returned if the Msg delivery failed when the Msg is configured for
|
||||
// unencoded delivery but the server does not support this
|
||||
ErrNoUnencoded
|
||||
|
||||
// ErrAmbiguous is a generalized delivery error for the SendError type that is
|
||||
// returned if the exact reason for the delivery failure is ambiguous
|
||||
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.
|
||||
type SendError struct {
|
||||
affectedMsg *Msg
|
||||
errcode int
|
||||
enhancedStatusCode string
|
||||
errlist []error
|
||||
isTemp bool
|
||||
rcpt []string
|
||||
Reason SendErrReason
|
||||
}
|
||||
|
||||
// SendErrReason represents a comparable reason on why the delivery failed
|
||||
type SendErrReason int
|
||||
|
||||
// Error implements the error interface for the SendError type.
|
||||
//
|
||||
// This function returns a detailed error message string for the SendError, including the
|
||||
// reason for failure, list of errors, affected recipients, and the message ID of the
|
||||
// affected message (if available). If the reason is unknown (greater than 10), it returns
|
||||
// "unknown reason". The error message is built dynamically based on the content of the
|
||||
// error list, recipient list, and message ID.
|
||||
//
|
||||
// Returns:
|
||||
// - A string representing the error message.
|
||||
func (e *SendError) Error() string {
|
||||
if e.Reason > ErrAmbiguous {
|
||||
return "unknown reason"
|
||||
}
|
||||
|
||||
var errMessage strings.Builder
|
||||
errMessage.WriteString(e.Reason.String())
|
||||
if len(e.errlist) > 0 {
|
||||
errMessage.WriteRune(':')
|
||||
for i := range e.errlist {
|
||||
errMessage.WriteRune(' ')
|
||||
errMessage.WriteString(e.errlist[i].Error())
|
||||
if i != len(e.errlist)-1 {
|
||||
errMessage.WriteString(",")
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(e.rcpt) > 0 {
|
||||
errMessage.WriteString(", affected recipient(s): ")
|
||||
for i := range e.rcpt {
|
||||
errMessage.WriteString(e.rcpt[i])
|
||||
if i != len(e.rcpt)-1 {
|
||||
errMessage.WriteString(", ")
|
||||
}
|
||||
}
|
||||
}
|
||||
if e.affectedMsg != nil && e.affectedMsg.GetMessageID() != "" {
|
||||
errMessage.WriteString(", affected message ID: ")
|
||||
errMessage.WriteString(e.affectedMsg.GetMessageID())
|
||||
}
|
||||
|
||||
return errMessage.String()
|
||||
}
|
||||
|
||||
// Is implements the errors.Is functionality and compares the SendErrReason.
|
||||
//
|
||||
// This function allows for comparison between two errors by checking if the provided
|
||||
// error matches the SendError type and, if so, compares the SendErrReason and the
|
||||
// temporary status (isTemp) of both errors.
|
||||
//
|
||||
// Parameters:
|
||||
// - errType: The error to compare against the current SendError.
|
||||
//
|
||||
// Returns:
|
||||
// - true if the errors have the same reason and temporary status, false otherwise.
|
||||
func (e *SendError) Is(errType error) bool {
|
||||
var t *SendError
|
||||
if errors.As(errType, &t) && t != nil {
|
||||
return e.Reason == t.Reason && e.isTemp == t.isTemp
|
||||
}
|
||||
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.
|
||||
func (e *SendError) IsTemp() bool {
|
||||
if e == nil {
|
||||
return false
|
||||
}
|
||||
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.
|
||||
func (e *SendError) MessageID() string {
|
||||
if e == nil || e.affectedMsg == nil {
|
||||
return ""
|
||||
}
|
||||
return e.affectedMsg.GetMessageID()
|
||||
}
|
||||
|
||||
// Msg returns the pointer to the affected message that caused the error.
|
||||
//
|
||||
// This function retrieves the Msg associated with the SendError. If the SendError or
|
||||
// the affectedMsg is nil, it returns nil.
|
||||
//
|
||||
// Returns:
|
||||
// - A pointer to the Msg that caused the error, or nil if not available.
|
||||
func (e *SendError) Msg() *Msg {
|
||||
if e == nil || e.affectedMsg == nil {
|
||||
return nil
|
||||
}
|
||||
return e.affectedMsg
|
||||
}
|
||||
|
||||
// EnhancedStatusCode returns the enhanced status code of the server response if the
|
||||
// server supports it, as described in RFC 2034.
|
||||
//
|
||||
// This function retrieves the enhanced status code of an error returned by the server. This
|
||||
// requires that the receiving server supports this SMTP extension as described in RFC 2034.
|
||||
// Since this is the SendError interface, we only collect status codes for error responses,
|
||||
// meaning 4xx or 5xx. If the server does not support the ENHANCEDSTATUSCODES extension or
|
||||
// the error did not include an enhanced status code, it will return an empty string.
|
||||
//
|
||||
// Returns:
|
||||
// - The enhanced status code as returned by the server, or an empty string is not supported.
|
||||
//
|
||||
// References:
|
||||
// - https://datatracker.ietf.org/doc/html/rfc2034
|
||||
func (e *SendError) EnhancedStatusCode() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
return e.enhancedStatusCode
|
||||
}
|
||||
|
||||
// ErrorCode returns the error code of the server response.
|
||||
//
|
||||
// This function retrieves the error code the error returned by the server. The error code will
|
||||
// start with 5 on permanent errors and with 4 on a temporary error. If the error is not returned
|
||||
// by the server, but is generated by go-mail, the code will be 0.
|
||||
//
|
||||
// Returns:
|
||||
// - The error code as returned by the server, or 0 if not a server error.
|
||||
func (e *SendError) ErrorCode() int {
|
||||
if e == nil {
|
||||
return 0
|
||||
}
|
||||
return e.errcode
|
||||
}
|
||||
|
||||
// String satisfies the fmt.Stringer interface for the SendErrReason type.
|
||||
//
|
||||
// This function converts the SendErrReason into a human-readable string representation based
|
||||
// on the error type. If the error reason does not match any predefined case, it returns
|
||||
// "unknown reason".
|
||||
//
|
||||
// Returns:
|
||||
// - A string representation of the SendErrReason.
|
||||
func (r SendErrReason) String() string {
|
||||
switch r {
|
||||
case ErrGetSender:
|
||||
return "getting sender address"
|
||||
case ErrGetRcpts:
|
||||
return "getting recipient addresses"
|
||||
case ErrSMTPMailFrom:
|
||||
return "sending SMTP MAIL FROM command"
|
||||
case ErrSMTPRcptTo:
|
||||
return "sending SMTP RCPT TO command"
|
||||
case ErrSMTPData:
|
||||
return "sending SMTP DATA command"
|
||||
case ErrSMTPDataClose:
|
||||
return "closing SMTP DATA writer"
|
||||
case ErrSMTPReset:
|
||||
return "sending SMTP RESET command"
|
||||
case ErrWriteContent:
|
||||
return "sending message content"
|
||||
case ErrConnCheck:
|
||||
return "checking SMTP connection"
|
||||
case ErrNoUnencoded:
|
||||
return ErrServerNoUnencoded.Error()
|
||||
case ErrAmbiguous:
|
||||
return "ambiguous reason, check Msg.SendError for message specific reasons"
|
||||
}
|
||||
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.
|
||||
func isTempError(err error) bool {
|
||||
return err.Error()[0] == '4'
|
||||
}
|
||||
|
||||
func errorCode(err error) int {
|
||||
rootErr := errors.Unwrap(err)
|
||||
if rootErr != nil {
|
||||
err = rootErr
|
||||
}
|
||||
firstrune := err.Error()[0]
|
||||
if firstrune < 52 || firstrune > 53 {
|
||||
return 0
|
||||
}
|
||||
code := err.Error()[0:3]
|
||||
errcode, cerr := strconv.Atoi(code)
|
||||
if cerr != nil {
|
||||
return 0
|
||||
}
|
||||
return errcode
|
||||
}
|
||||
|
||||
func enhancedStatusCode(err error, supported bool) string {
|
||||
if err == nil || !supported {
|
||||
return ""
|
||||
}
|
||||
rootErr := errors.Unwrap(err)
|
||||
if rootErr != nil {
|
||||
err = rootErr
|
||||
}
|
||||
firstrune := err.Error()[0]
|
||||
if firstrune != 50 && firstrune != 52 && firstrune != 53 {
|
||||
return ""
|
||||
}
|
||||
re, rerr := regexp.Compile(`\b([245])\.\d{1,3}\.\d{1,3}\b`)
|
||||
if rerr != nil {
|
||||
return ""
|
||||
}
|
||||
return re.FindString(err.Error())
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
// SPDX-FileCopyrightText: The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/wneessen/go-mail/internal/pkcs7"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrPrivateKeyMissing should be used if private key is invalid
|
||||
ErrPrivateKeyMissing = errors.New("private key is missing")
|
||||
|
||||
// ErrCertificateMissing should be used if the certificate is invalid
|
||||
ErrCertificateMissing = errors.New("certificate is missing")
|
||||
)
|
||||
|
||||
// SMIME represents the configuration and state for S/MIME signing.
|
||||
//
|
||||
// This struct encapsulates the private key, certificate, optional intermediate certificate, and
|
||||
// a flag indicating whether a signing process is currently in progress.
|
||||
//
|
||||
// Fields:
|
||||
// - privateKey: The private key used for signing (implements crypto.PrivateKey).
|
||||
// - certificate: The x509 certificate associated with the private key.
|
||||
// - intermediateCert: An optional x509 intermediate certificate for chain validation.
|
||||
// - inProgress: A boolean flag indicating if a signing operation is currently active.
|
||||
type SMIME struct {
|
||||
privateKey crypto.PrivateKey
|
||||
certificate *x509.Certificate
|
||||
intermediateCert *x509.Certificate
|
||||
inProgress bool
|
||||
}
|
||||
|
||||
// newSMIME constructs a new instance of SMIME with the provided parameters.
|
||||
//
|
||||
// This function initializes an SMIME object with a private key, certificate, and an optional
|
||||
// intermediate certificate.
|
||||
//
|
||||
// Parameters:
|
||||
// - privateKey: The private key used for signing (must implement crypto.PrivateKey).
|
||||
// - certificate: The x509 certificate associated with the private key.
|
||||
// - intermediateCert: An optional x509 intermediate certificate for chain validation.
|
||||
//
|
||||
// Returns:
|
||||
// - An SMIME instance configured with the provided parameters.
|
||||
// - An error if the private key or certificate is missing.
|
||||
func newSMIME(privateKey crypto.PrivateKey, certificate *x509.Certificate,
|
||||
intermediateCertificate *x509.Certificate,
|
||||
) (*SMIME, error) {
|
||||
if privateKey == nil {
|
||||
return nil, ErrPrivateKeyMissing
|
||||
}
|
||||
if certificate == nil {
|
||||
return nil, ErrCertificateMissing
|
||||
}
|
||||
|
||||
return &SMIME{
|
||||
privateKey: privateKey,
|
||||
certificate: certificate,
|
||||
intermediateCert: intermediateCertificate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// signMessage signs the provided message with S/MIME and returns the signature in DER format.
|
||||
//
|
||||
// This function creates S/MIME signed data using the configured private key and certificate.
|
||||
// It optionally includes an intermediate certificate for chain validation and detaches the signature.
|
||||
//
|
||||
// Parameters:
|
||||
// - message: The byte slice representing the message to be signed.
|
||||
//
|
||||
// Returns:
|
||||
// - A string containing the S/MIME signature in DER format.
|
||||
// - An error if initializing signed data, adding the signer, or finishing the signature fails.
|
||||
func (s *SMIME) signMessage(message []byte) (string, error) {
|
||||
signedData, err := pkcs7.NewSignedData(message)
|
||||
if err != nil || signedData == nil {
|
||||
return "", fmt.Errorf("failed to initialize signed data: %w", err)
|
||||
}
|
||||
|
||||
if err = signedData.AddSigner(s.certificate, s.privateKey, pkcs7.SignerInfoConfig{}); err != nil {
|
||||
return "", fmt.Errorf("could not add signer message: %w", err)
|
||||
}
|
||||
|
||||
if s.intermediateCert != nil {
|
||||
signedData.AddCertificate(s.intermediateCert)
|
||||
}
|
||||
|
||||
signedData.Detach()
|
||||
signatureDER, err := signedData.Finish()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to finish signature: %w", err)
|
||||
}
|
||||
|
||||
return string(signatureDER), nil
|
||||
}
|
||||
|
||||
// getLeafCertificate retrieves the leaf certificate from a tls.Certificate.
|
||||
//
|
||||
// This function returns the parsed leaf certificate from the provided TLS certificate. If the Leaf field
|
||||
// is nil, it parses and returns the first certificate in the chain.
|
||||
//
|
||||
// PLEASE NOTE: In Go versions prior to 1.23, the Certificate.Leaf field was left nil, and the parsed
|
||||
// certificate was discarded. This behavior can be re-enabled by setting "x509keypairleaf=0" in the
|
||||
// GODEBUG environment variable.
|
||||
//
|
||||
// Parameters:
|
||||
// - keyPairTlS: The *tls.Certificate containing the certificate chain.
|
||||
//
|
||||
// Returns:
|
||||
// - The parsed leaf x509 certificate.
|
||||
// - An error if the certificate could not be parsed.
|
||||
func getLeafCertificate(keyPairTLS *tls.Certificate) (*x509.Certificate, error) {
|
||||
if keyPairTLS == nil {
|
||||
return nil, errors.New("provided certificate is nil")
|
||||
}
|
||||
if keyPairTLS.Leaf != nil {
|
||||
return keyPairTLS.Leaf, nil
|
||||
}
|
||||
|
||||
if len(keyPairTLS.Certificate) == 0 {
|
||||
return nil, errors.New("certificate chain is empty")
|
||||
}
|
||||
cert, err := x509.ParseCertificate(keyPairTLS.Certificate[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
|
||||
// SPDX-FileCopyrightText: Copyright (c) The go-mail Authors
|
||||
//
|
||||
// Original net/smtp code from the Go stdlib by the Go Authors.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// LICENSE file that can be found in this directory.
|
||||
//
|
||||
// go-mail specific modifications by the go-mail Authors.
|
||||
// Licensed under the MIT License.
|
||||
// See [PROJECT ROOT]/LICENSES directory for more information.
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-3-Clause AND MIT
|
||||
|
||||
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.
|
||||
// It returns the name of the authentication protocol
|
||||
// and optionally data to include in the initial AUTH message
|
||||
// sent to the server.
|
||||
// If it returns a non-nil error, the SMTP client aborts
|
||||
// the authentication attempt and closes the connection.
|
||||
Start(server *ServerInfo) (proto string, toServer []byte, err error)
|
||||
|
||||
// Next continues the authentication. The server has just sent
|
||||
// the fromServer data. If more is true, the server expects a
|
||||
// response, which Next should return as toServer; otherwise
|
||||
// Next should return toServer == nil.
|
||||
// If Next returns a non-nil error, the SMTP client aborts
|
||||
// the authentication attempt and closes the connection.
|
||||
Next(fromServer []byte, more bool) (toServer []byte, err error)
|
||||
}
|
||||
|
||||
// ServerInfo records information about an SMTP server.
|
||||
type ServerInfo struct {
|
||||
Name string // SMTP server name
|
||||
TLS bool // using TLS, with valid certificate for Name
|
||||
Auth []string // advertised authentication mechanisms
|
||||
}
|
||||
|
||||
func isLocalhost(name string) bool {
|
||||
return name == "localhost" || name == "127.0.0.1" || name == "::1"
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
|
||||
// SPDX-FileCopyrightText: Copyright (c) The go-mail Authors
|
||||
//
|
||||
// Original net/smtp code from the Go stdlib by the Go Authors.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// LICENSE file that can be found in this directory.
|
||||
//
|
||||
// go-mail specific modifications by the go-mail Authors.
|
||||
// Licensed under the MIT License.
|
||||
// See [PROJECT ROOT]/LICENSES directory for more information.
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-3-Clause AND MIT
|
||||
|
||||
package smtp
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// cramMD5Auth is the type that satisfies the Auth interface for the "SMTP CRAM_MD5" auth
|
||||
type cramMD5Auth struct {
|
||||
username, secret string
|
||||
}
|
||||
|
||||
// CRAMMD5Auth returns an [Auth] that implements the CRAM-MD5 authentication
|
||||
// mechanism as defined in RFC 2195.
|
||||
// The returned Auth uses the given username and secret to authenticate
|
||||
// to the server using the challenge-response mechanism.
|
||||
func CRAMMD5Auth(username, secret string) Auth {
|
||||
return &cramMD5Auth{username, secret}
|
||||
}
|
||||
|
||||
func (a *cramMD5Auth) Start(_ *ServerInfo) (string, []byte, error) {
|
||||
return "CRAM-MD5", nil, nil
|
||||
}
|
||||
|
||||
func (a *cramMD5Auth) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||
if more {
|
||||
d := hmac.New(md5.New, []byte(a.secret))
|
||||
d.Write(fromServer)
|
||||
s := make([]byte, 0, d.Size())
|
||||
return fmt.Appendf(nil, "%s %x", a.username, d.Sum(s)), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package smtp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// loginAuth is the type that satisfies the Auth interface for the "SMTP LOGIN" auth
|
||||
type loginAuth struct {
|
||||
username, password string
|
||||
host string
|
||||
respStep uint8
|
||||
allowUnencryptedAuth bool
|
||||
}
|
||||
|
||||
// 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 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 challenge to allow compatibility
|
||||
// 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, allowUnenc bool) Auth {
|
||||
return &loginAuth{username, password, host, 0, allowUnenc}
|
||||
}
|
||||
|
||||
// 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.
|
||||
// In particular, it doesn't matter if the server advertises LOGIN auth.
|
||||
// That might just be the attacker saying
|
||||
// "it's ok, you can trust me with your password."
|
||||
if !a.allowUnencryptedAuth && !server.TLS && !isLocalhost(server.Name) {
|
||||
return "", nil, ErrUnencrypted
|
||||
}
|
||||
if server.Name != a.host {
|
||||
return "", nil, ErrWrongHostname
|
||||
}
|
||||
a.respStep = 0
|
||||
return "LOGIN", nil, nil
|
||||
}
|
||||
|
||||
// Next processes responses from the server during the SMTP authentication exchange, sending the
|
||||
// username and password.
|
||||
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||
if more {
|
||||
switch a.respStep {
|
||||
case 0:
|
||||
a.respStep++
|
||||
return []byte(a.username), nil
|
||||
case 1:
|
||||
a.respStep++
|
||||
return []byte(a.password), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnexpectedServerResponse, string(fromServer))
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
|
||||
// SPDX-FileCopyrightText: Copyright (c) The go-mail Authors
|
||||
//
|
||||
// Original net/smtp code from the Go stdlib by the Go Authors.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// LICENSE file that can be found in this directory.
|
||||
//
|
||||
// go-mail specific modifications by the go-mail Authors.
|
||||
// Licensed under the MIT License.
|
||||
// See [PROJECT ROOT]/LICENSES directory for more information.
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-3-Clause AND MIT
|
||||
|
||||
package smtp
|
||||
|
||||
// plainAuth is the type that satisfies the Auth interface for the "SMTP PLAIN" auth
|
||||
type plainAuth struct {
|
||||
identity, username, password string
|
||||
host string
|
||||
allowUnencryptedAuth bool
|
||||
}
|
||||
|
||||
// PlainAuth returns an [Auth] that implements the PLAIN authentication
|
||||
// mechanism as defined in RFC 4616. The returned Auth uses the given
|
||||
// username and password to authenticate to host and act as identity.
|
||||
// Usually identity should be the empty string, to act as username.
|
||||
//
|
||||
// PlainAuth 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 PlainAuth(identity, username, password, host string, allowUnenc bool) Auth {
|
||||
return &plainAuth{identity, username, password, host, allowUnenc}
|
||||
}
|
||||
|
||||
func (a *plainAuth) 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.
|
||||
// In particular, it doesn't matter if the server advertises PLAIN auth.
|
||||
// That might just be the attacker saying
|
||||
// "it's ok, you can trust me with your password."
|
||||
if !a.allowUnencryptedAuth && !server.TLS && !isLocalhost(server.Name) {
|
||||
return "", nil, ErrUnencrypted
|
||||
}
|
||||
if server.Name != a.host {
|
||||
return "", nil, ErrWrongHostname
|
||||
}
|
||||
resp := []byte(a.identity + "\x00" + a.username + "\x00" + a.password)
|
||||
return "PLAIN", resp, nil
|
||||
}
|
||||
|
||||
func (a *plainAuth) Next(_ []byte, more bool) ([]byte, error) {
|
||||
if more {
|
||||
// We've already sent everything.
|
||||
return nil, ErrUnexpectedServerChallange
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
+317
@@ -0,0 +1,317 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package smtp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/pbkdf2"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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/tls: 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, err = pbkdf2.Key(a.h, password, salt, a.iterations, a.h().Size())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to derive key: %w", err)
|
||||
}
|
||||
|
||||
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("failed to normalize string: %w", err)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package smtp
|
||||
|
||||
type xoauth2Auth struct {
|
||||
username, token string
|
||||
}
|
||||
|
||||
// XOAuth2Auth returns an [Auth] that implements the XOAuth2 authentication
|
||||
// mechanism as defined in the following specs:
|
||||
//
|
||||
// https://developers.google.com/gmail/imap/xoauth2-protocol
|
||||
// https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth
|
||||
func XOAuth2Auth(username, token string) Auth {
|
||||
return &xoauth2Auth{username, token}
|
||||
}
|
||||
|
||||
func (a *xoauth2Auth) Start(_ *ServerInfo) (string, []byte, error) {
|
||||
return "XOAUTH2", []byte("user=" + a.username + "\x01" + "auth=Bearer " + a.token + "\x01\x01"), nil
|
||||
}
|
||||
|
||||
func (a *xoauth2Auth) Next(_ []byte, more bool) ([]byte, error) {
|
||||
if more {
|
||||
return []byte(""), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package smtp
|
||||
|
||||
import (
|
||||
"net/textproto"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ResponseErrorHandler provides custom handling for SMTP response errors.
|
||||
//
|
||||
// This interface defines a method for handling SMTP responses that do not comply with expected
|
||||
// formats or behaviors. It is useful for implementing retry logic, logging, or protocol-specific
|
||||
// error handling logic. It injects itself into the smtp.Client and is called whenever a server
|
||||
// response does fail.
|
||||
//
|
||||
// Parameters:
|
||||
// - host: The hostname of the SMTP server that this ResponseErrorHandler should handle
|
||||
// - command: The SMTP command that triggered the error.
|
||||
// - conn: The textproto.Conn to the SMTP server.
|
||||
// - err: The error received from the SMTP server.
|
||||
//
|
||||
// Returns:
|
||||
// - An error indicating the outcome of the error handling logic.
|
||||
type ResponseErrorHandler interface {
|
||||
HandleError(host, command string, conn *textproto.Conn, err error) error
|
||||
}
|
||||
|
||||
// DefaultErrorHandler implements ResponseErrorHandler by returning the original error.
|
||||
//
|
||||
// This handler provides a default implementation of the ResponseErrorHandler interface,
|
||||
// where no custom error processing is performed. It simply returns the original error
|
||||
// received from the SMTP server.
|
||||
//
|
||||
// Returns:
|
||||
// - The original error passed to the handler.
|
||||
type DefaultErrorHandler struct{}
|
||||
|
||||
// HandleError satisfies the ResponseErrorHandler interface for the DefaultErrorHandler type
|
||||
func (d *DefaultErrorHandler) HandleError(_, _ string, _ *textproto.Conn, err error) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// HandlerKey uniquely identifies a host-command pair for handler mapping.
|
||||
//
|
||||
// This struct is used to associate a specific SMTP host and command combination with
|
||||
// a ResponseErrorHandler. It enables mapping and retrieval of handlers based on
|
||||
// the source of the error.
|
||||
type HandlerKey struct {
|
||||
Host string
|
||||
Command string
|
||||
}
|
||||
|
||||
// ErrorHandlerRegistry manages custom error handlers for SMTP host-command pairs.
|
||||
//
|
||||
// This struct stores mappings between HandlerKey values and corresponding
|
||||
// ResponseErrorHandler implementations. It supports concurrent access and provides
|
||||
// a fallback default handler when no specific match is found.
|
||||
type ErrorHandlerRegistry struct {
|
||||
mu sync.RWMutex
|
||||
handlers map[HandlerKey]ResponseErrorHandler
|
||||
defaultHandler ResponseErrorHandler
|
||||
}
|
||||
|
||||
// NewErrorHandlerRegistry creates a new ErrorHandlerRegistry instance.
|
||||
//
|
||||
// This function initializes an ErrorHandlerRegistry with an empty handler map and
|
||||
// assigns a DefaultErrorHandler as the fallback handler.
|
||||
//
|
||||
// Returns:
|
||||
// - A pointer to the newly constructed ErrorHandlerRegistry.
|
||||
func NewErrorHandlerRegistry() *ErrorHandlerRegistry {
|
||||
return &ErrorHandlerRegistry{
|
||||
handlers: make(map[HandlerKey]ResponseErrorHandler),
|
||||
defaultHandler: &DefaultErrorHandler{}, // Set the default handler
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterHandler associates a custom handler with a specific host and command.
|
||||
//
|
||||
// This method registers a ResponseErrorHandler for the given SMTP host and command
|
||||
// combination. It ensures thread-safe access to the internal handler map.
|
||||
//
|
||||
// Parameters:
|
||||
// - host: The SMTP server hostname.
|
||||
// - command: The SMTP command to associate with the handler.
|
||||
// - handler: The ResponseErrorHandler to register.
|
||||
func (r *ErrorHandlerRegistry) RegisterHandler(host, command string, handler ResponseErrorHandler) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.handlers[HandlerKey{Host: strings.ToLower(host), Command: strings.ToLower(command)}] = handler
|
||||
}
|
||||
|
||||
// GetHandler retrieves the handler for a given host and command.
|
||||
//
|
||||
// This method returns the ResponseErrorHandler registered for the specified SMTP host
|
||||
// and command. If no handler is found, it returns the default handler.
|
||||
//
|
||||
// Parameters:
|
||||
// - host: The SMTP server hostname.
|
||||
// - command: The SMTP command to look up.
|
||||
//
|
||||
// Returns:
|
||||
// - The corresponding ResponseErrorHandler, or the default handler if none is registered.
|
||||
func (r *ErrorHandlerRegistry) GetHandler(host, command string) ResponseErrorHandler {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
if handler, ok := r.handlers[HandlerKey{Host: strings.ToLower(host), Command: strings.ToLower(command)}]; ok {
|
||||
return handler
|
||||
}
|
||||
return r.defaultHandler
|
||||
}
|
||||
|
||||
// SetDefaultHandler overrides the default ResponseErrorHandler.
|
||||
//
|
||||
// This method sets a new default handler to be used when no specific handler is
|
||||
// registered for a host and command combination. It ensures thread-safe access.
|
||||
//
|
||||
// Parameters:
|
||||
// - handler: The new default ResponseErrorHandler to assign.
|
||||
func (r *ErrorHandlerRegistry) SetDefaultHandler(handler ResponseErrorHandler) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.defaultHandler = handler
|
||||
}
|
||||
+702
@@ -0,0 +1,702 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
|
||||
// SPDX-FileCopyrightText: Copyright (c) The go-mail Authors
|
||||
//
|
||||
// Original net/smtp code from the Go stdlib by the Go Authors.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// LICENSE file that can be found in this directory.
|
||||
//
|
||||
// go-mail specific modifications by the go-mail Authors.
|
||||
// Licensed under the MIT License.
|
||||
// See [PROJECT ROOT]/LICENSES directory for more information.
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-3-Clause AND MIT
|
||||
|
||||
// Package smtp implements the Simple Mail Transfer Protocol as defined in RFC 5321.
|
||||
// It also implements the following extensions:
|
||||
//
|
||||
// 8BITMIME RFC 1652
|
||||
// AUTH RFC 2554
|
||||
// STARTTLS RFC 3207
|
||||
// DSN RFC 1891
|
||||
package smtp
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/textproto"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/wneessen/go-mail/log"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
// ErrNonTLSConnection is returned when an attempt is made to retrieve TLS state on a non-TLS connection.
|
||||
ErrNonTLSConnection = errors.New("connection is not using TLS")
|
||||
|
||||
// ErrNoConnection is returned when attempting to perform an operation that requires an established
|
||||
// connection but none exists.
|
||||
ErrNoConnection = errors.New("connection is not established")
|
||||
)
|
||||
|
||||
// A Client represents a client connection to an SMTP server.
|
||||
type Client struct {
|
||||
// Text is the textproto.Conn used by the Client. It is exported to allow for clients to add extensions.
|
||||
Text *textproto.Conn
|
||||
|
||||
// ErrorHandlerRegistry manages custom error handlers for SMTP host-command pairs.
|
||||
ErrorHandlerRegistry *ErrorHandlerRegistry
|
||||
|
||||
// auth supported auth mechanisms
|
||||
auth []string
|
||||
|
||||
// authIsActive indicates that the Client is currently during SMTP authentication
|
||||
authIsActive bool
|
||||
|
||||
// keep a reference to the connection so it can be used to create a TLS connection later
|
||||
conn net.Conn
|
||||
|
||||
// debug logging is enabled
|
||||
debug bool
|
||||
|
||||
// didHello indicates whether we've said HELO/EHLO
|
||||
didHello bool
|
||||
|
||||
// dsnmrtype defines the mail return option in case DSN is enabled
|
||||
dsnmrtype string
|
||||
|
||||
// dsnrntype defines the recipient notify option in case DSN is enabled
|
||||
dsnrntype string
|
||||
|
||||
// ext is a map of supported extensions
|
||||
ext map[string]string
|
||||
|
||||
// helloError is the error from the hello
|
||||
helloError error
|
||||
|
||||
// isConnected indicates if the Client has an active connection
|
||||
isConnected bool
|
||||
|
||||
// logAuthData indicates if the Client should include SMTP authentication data in the logs
|
||||
logAuthData bool
|
||||
|
||||
// localName is the name to use in HELO/EHLO
|
||||
localName string // the name to use in HELO/EHLO
|
||||
|
||||
// logger will be used for debug logging
|
||||
logger log.Logger
|
||||
|
||||
// mutex is used to synchronize access to shared resources, ensuring that only one goroutine can access
|
||||
// the resource at a time.
|
||||
mutex sync.RWMutex
|
||||
|
||||
// tls indicates whether the Client is using TLS
|
||||
tls bool
|
||||
|
||||
// serverName denotes the name of the server to which the application will connect. Used for
|
||||
// identification and routing.
|
||||
serverName string
|
||||
}
|
||||
|
||||
// Dial returns a new [Client] connected to an SMTP server at addr.
|
||||
// The addr must include a port, as in "mail.example.com:smtp".
|
||||
func Dial(addr string) (*Client, error) {
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
host, _, _ := net.SplitHostPort(addr)
|
||||
return NewClient(conn, host)
|
||||
}
|
||||
|
||||
// NewClient returns a new [Client] using an existing connection and host as a
|
||||
// server name to be used when authenticating.
|
||||
func NewClient(conn net.Conn, host string) (*Client, error) {
|
||||
text := textproto.NewConn(conn)
|
||||
_, _, err := text.ReadResponse(220)
|
||||
if err != nil {
|
||||
if cerr := text.Close(); cerr != nil {
|
||||
// Since we are being Go <1.20 compatible, we can't combine errorrs and
|
||||
// duplicate %w vers are not suppored. Therefore let's ignore this linting
|
||||
// error for now
|
||||
// nolint:errorlint
|
||||
return nil, fmt.Errorf("%w, %s", err, cerr)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
c := &Client{Text: text, conn: conn, serverName: host, localName: "localhost"}
|
||||
_, c.tls = conn.(*tls.Conn)
|
||||
c.isConnected = true
|
||||
c.ErrorHandlerRegistry = NewErrorHandlerRegistry()
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Close closes the connection.
|
||||
func (c *Client) Close() error {
|
||||
c.mutex.Lock()
|
||||
err := c.Text.Close()
|
||||
c.isConnected = false
|
||||
c.mutex.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
// hello runs a hello exchange if needed.
|
||||
func (c *Client) hello() error {
|
||||
if !c.didHello {
|
||||
c.didHello = true
|
||||
err := c.ehlo()
|
||||
if err != nil {
|
||||
c.helloError = c.helo()
|
||||
}
|
||||
}
|
||||
return c.helloError
|
||||
}
|
||||
|
||||
// Hello sends a HELO or EHLO to the server as the given host name.
|
||||
// Calling this method is only necessary if the client needs control
|
||||
// over the host name used. The client will introduce itself as "localhost"
|
||||
// automatically otherwise. If Hello is called, it must be called before
|
||||
// any of the other methods.
|
||||
func (c *Client) Hello(localName string) error {
|
||||
if err := validateLine(localName); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.didHello {
|
||||
return errors.New("smtp: Hello called after other methods")
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
c.localName = localName
|
||||
c.mutex.Unlock()
|
||||
|
||||
return c.hello()
|
||||
}
|
||||
|
||||
// cmd is a convenience function that sends a command and returns the response
|
||||
func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
|
||||
c.mutex.Lock()
|
||||
|
||||
var logMsg []interface{}
|
||||
logMsg = args
|
||||
logFmt := format
|
||||
if c.authIsActive {
|
||||
logMsg = []interface{}{"<SMTP auth data redacted>"}
|
||||
logFmt = "%s"
|
||||
}
|
||||
c.debugLog(log.DirClientToServer, logFmt, logMsg...)
|
||||
|
||||
id, err := c.Text.Cmd(format, args...)
|
||||
if err != nil {
|
||||
c.mutex.Unlock()
|
||||
return 0, "", err
|
||||
}
|
||||
c.Text.StartResponse(id)
|
||||
defer c.Text.EndResponse(id)
|
||||
code, msg, err := c.Text.ReadResponse(expectCode)
|
||||
if err != nil {
|
||||
fmtValues := strings.Split(format, " ")
|
||||
currentCmd := strings.ToLower(fmtValues[0])
|
||||
handler := c.ErrorHandlerRegistry.GetHandler(c.serverName, currentCmd)
|
||||
handledErr := handler.HandleError(c.serverName, currentCmd, c.Text, err)
|
||||
if handledErr != nil {
|
||||
c.mutex.Unlock()
|
||||
return 0, "", handledErr
|
||||
}
|
||||
|
||||
// If the handler successfully recovered, we try reading the response again
|
||||
// This assumes the handler has consumed the problematic data beforehand.
|
||||
code, msg, err = c.Text.ReadResponse(expectCode)
|
||||
}
|
||||
|
||||
logMsg = []interface{}{code, msg}
|
||||
if c.authIsActive && code >= 300 && code <= 400 {
|
||||
logMsg = []interface{}{code, "<SMTP auth data redacted>"}
|
||||
}
|
||||
c.debugLog(log.DirServerToClient, "%d %s", logMsg...)
|
||||
|
||||
c.mutex.Unlock()
|
||||
return code, msg, err
|
||||
}
|
||||
|
||||
// helo sends the HELO greeting to the server. It should be used only when the
|
||||
// server does not support ehlo.
|
||||
func (c *Client) helo() error {
|
||||
c.mutex.Lock()
|
||||
c.ext = nil
|
||||
c.mutex.Unlock()
|
||||
|
||||
_, _, err := c.cmd(250, "HELO %s", c.localName)
|
||||
return err
|
||||
}
|
||||
|
||||
// StartTLS sends the STARTTLS command and encrypts all further communication.
|
||||
// Only servers that advertise the STARTTLS extension support this function.
|
||||
func (c *Client) StartTLS(config *tls.Config) error {
|
||||
if err := c.hello(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, err := c.cmd(220, "STARTTLS")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
c.conn = tls.Client(c.conn, config)
|
||||
c.Text = textproto.NewConn(c.conn)
|
||||
c.tls = true
|
||||
c.mutex.Unlock()
|
||||
|
||||
return c.ehlo()
|
||||
}
|
||||
|
||||
// TLSConnectionState returns the client's TLS connection state.
|
||||
// The return values are their zero values if [Client.StartTLS] did
|
||||
// not succeed.
|
||||
func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
|
||||
tc, ok := c.conn.(*tls.Conn)
|
||||
if !ok {
|
||||
return state, ok
|
||||
}
|
||||
state, ok = tc.ConnectionState(), true
|
||||
return state, ok
|
||||
}
|
||||
|
||||
// Verify checks the validity of an email address on the server.
|
||||
// If Verify returns nil, the address is valid. A non-nil return
|
||||
// does not necessarily indicate an invalid address. Many servers
|
||||
// will not verify addresses for security reasons.
|
||||
func (c *Client) Verify(addr string) error {
|
||||
if err := validateLine(addr); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.hello(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, err := c.cmd(250, "VRFY %s", addr)
|
||||
return err
|
||||
}
|
||||
|
||||
// Auth authenticates a client using the provided authentication mechanism.
|
||||
// A failed authentication closes the connection.
|
||||
// Only servers that advertise the AUTH extension support this function.
|
||||
func (c *Client) Auth(a Auth) error {
|
||||
if err := c.hello(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
if !c.logAuthData {
|
||||
c.authIsActive = true
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
defer func() {
|
||||
c.mutex.Lock()
|
||||
if !c.logAuthData {
|
||||
c.authIsActive = false
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
}()
|
||||
|
||||
encoding := base64.StdEncoding
|
||||
mech, resp, err := a.Start(&ServerInfo{c.serverName, c.tls, c.auth})
|
||||
if err != nil {
|
||||
if qerr := c.Quit(); qerr != nil {
|
||||
// Since we are being Go <1.20 compatible, we can't combine errorrs and
|
||||
// duplicate %w vers are not suppored. Therefore let's ignore this linting
|
||||
// error for now
|
||||
// nolint:errorlint
|
||||
return fmt.Errorf("%w, %s", err, qerr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
resp64 := make([]byte, encoding.EncodedLen(len(resp)))
|
||||
encoding.Encode(resp64, resp)
|
||||
code, msg64, err := c.cmd(0, "%s", strings.TrimSpace(fmt.Sprintf("AUTH %s %s", mech,
|
||||
resp64)))
|
||||
for err == nil {
|
||||
var msg []byte
|
||||
switch code {
|
||||
case 334:
|
||||
msg, err = encoding.DecodeString(msg64)
|
||||
case 235:
|
||||
// the last message isn't base64 because it isn't a challenge
|
||||
msg = []byte(msg64)
|
||||
default:
|
||||
err = &textproto.Error{Code: code, Msg: msg64}
|
||||
}
|
||||
if err == nil {
|
||||
resp, err = a.Next(msg, code == 334)
|
||||
}
|
||||
if err != nil {
|
||||
if mech != "XOAUTH2" {
|
||||
// abort the AUTH. Not required for XOAUTH2
|
||||
_, _, _ = c.cmd(501, "*")
|
||||
}
|
||||
_ = c.Quit()
|
||||
break
|
||||
}
|
||||
if resp == nil {
|
||||
break
|
||||
}
|
||||
resp64 = make([]byte, encoding.EncodedLen(len(resp)))
|
||||
encoding.Encode(resp64, resp)
|
||||
code, msg64, err = c.cmd(0, "%s", resp64)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Mail issues a MAIL command to the server using the provided email address.
|
||||
// If the server supports the 8BITMIME extension, Mail adds the BODY=8BITMIME
|
||||
// parameter. If the server supports the SMTPUTF8 extension, Mail adds the
|
||||
// SMTPUTF8 parameter.
|
||||
// This initiates a mail transaction and is followed by one or more [Client.Rcpt] calls.
|
||||
func (c *Client) Mail(from string) error {
|
||||
if err := validateLine(from); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.hello(); err != nil {
|
||||
return err
|
||||
}
|
||||
cmdStr := "MAIL FROM:%s"
|
||||
|
||||
c.mutex.RLock()
|
||||
if c.ext != nil {
|
||||
if _, ok := c.ext["8BITMIME"]; ok {
|
||||
cmdStr += " BODY=8BITMIME"
|
||||
}
|
||||
if _, ok := c.ext["SMTPUTF8"]; ok {
|
||||
cmdStr += " SMTPUTF8"
|
||||
}
|
||||
_, ok := c.ext["DSN"]
|
||||
if ok && c.dsnmrtype != "" {
|
||||
cmdStr += fmt.Sprintf(" RET=%s", c.dsnmrtype)
|
||||
}
|
||||
}
|
||||
c.mutex.RUnlock()
|
||||
|
||||
_, _, err := c.cmd(250, cmdStr, from)
|
||||
return err
|
||||
}
|
||||
|
||||
// Rcpt issues a RCPT command to the server using the provided email address.
|
||||
// A call to Rcpt must be preceded by a call to [Client.Mail] and may be followed by
|
||||
// a [Client.Data] call or another Rcpt call.
|
||||
func (c *Client) Rcpt(to string) error {
|
||||
if err := validateLine(to); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mutex.RLock()
|
||||
_, ok := c.ext["DSN"]
|
||||
c.mutex.RUnlock()
|
||||
|
||||
if ok && c.dsnrntype != "" {
|
||||
_, _, err := c.cmd(25, "RCPT TO:%s NOTIFY=%s", to, c.dsnrntype)
|
||||
return err
|
||||
}
|
||||
_, _, err := c.cmd(25, "RCPT TO:%s", to)
|
||||
return err
|
||||
}
|
||||
|
||||
type DataCloser struct {
|
||||
c *Client
|
||||
done bool
|
||||
io.WriteCloser
|
||||
response string
|
||||
}
|
||||
|
||||
// Close releases the lock, closes the WriteCloser, waits for a response, and then returns any error encountered.
|
||||
func (d *DataCloser) Close() error {
|
||||
d.c.mutex.Lock()
|
||||
_ = d.WriteCloser.Close()
|
||||
_, resp, err := d.c.Text.ReadResponse(250)
|
||||
d.response = resp
|
||||
d.done = true
|
||||
d.c.mutex.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
// Write writes data to the underlying WriteCloser while ensuring thread-safety by locking and unlocking a mutex.
|
||||
func (d *DataCloser) Write(p []byte) (n int, err error) {
|
||||
d.c.mutex.Lock()
|
||||
n, err = d.WriteCloser.Write(p)
|
||||
d.c.mutex.Unlock()
|
||||
return n, err
|
||||
}
|
||||
|
||||
// ServerResponse returns the response that was returned by the server after the DataCloser has
|
||||
// been closed. If the DataCloser has not been closed yet, it will return an empty string.
|
||||
func (d *DataCloser) ServerResponse() string {
|
||||
if !d.done {
|
||||
return ""
|
||||
}
|
||||
return d.response
|
||||
}
|
||||
|
||||
// Data issues a DATA command to the server and returns a writer that
|
||||
// can be used to write the mail headers and body. The caller should
|
||||
// close the writer before calling any more methods on c. A call to
|
||||
// Data must be preceded by one or more calls to [Client.Rcpt].
|
||||
func (c *Client) Data() (io.WriteCloser, error) {
|
||||
_, _, err := c.cmd(354, "DATA")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
datacloser := &DataCloser{}
|
||||
|
||||
c.mutex.Lock()
|
||||
datacloser.c = c
|
||||
datacloser.WriteCloser = c.Text.DotWriter()
|
||||
c.mutex.Unlock()
|
||||
|
||||
return datacloser, nil
|
||||
}
|
||||
|
||||
var testHookStartTLS func(*tls.Config) // nil, except for tests
|
||||
|
||||
// SendMail connects to the server at addr, switches to TLS if
|
||||
// possible, authenticates with the optional mechanism a if possible,
|
||||
// and then sends an email from address from, to addresses to, with
|
||||
// message msg.
|
||||
// The addr must include a port, as in "mail.example.com:smtp".
|
||||
//
|
||||
// The addresses in the to parameter are the SMTP RCPT addresses.
|
||||
//
|
||||
// The msg parameter should be an RFC 822-style email with headers
|
||||
// first, a blank line, and then the message body. The lines of msg
|
||||
// should be CRLF terminated. The msg headers should usually include
|
||||
// fields such as "From", "To", "Subject", and "Cc". Sending "Bcc"
|
||||
// messages is accomplished by including an email address in the to
|
||||
// parameter but not including it in the msg headers.
|
||||
//
|
||||
// The SendMail function and the net/smtp package are low-level
|
||||
// mechanisms and provide no support for DKIM signing, MIME
|
||||
// attachments (see the mime/multipart package), or other mail
|
||||
// functionality. Higher-level packages exist outside of the standard
|
||||
// library.
|
||||
func SendMail(addr string, a Auth, from string, to []string, msg []byte) error {
|
||||
if err := validateLine(from); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, recp := range to {
|
||||
if err := validateLine(recp); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
c, err := Dial(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Close()
|
||||
}()
|
||||
if err = c.hello(); err != nil {
|
||||
return err
|
||||
}
|
||||
if ok, _ := c.Extension("STARTTLS"); ok {
|
||||
config := &tls.Config{ServerName: c.serverName}
|
||||
if testHookStartTLS != nil {
|
||||
testHookStartTLS(config)
|
||||
}
|
||||
if err = c.StartTLS(config); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if a != nil && c.ext != nil {
|
||||
if _, ok := c.ext["AUTH"]; !ok {
|
||||
return errors.New("smtp: server doesn't support AUTH")
|
||||
}
|
||||
if err = c.Auth(a); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = c.Mail(from); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, addr := range to {
|
||||
if err = c.Rcpt(addr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Quit()
|
||||
}
|
||||
|
||||
// Extension reports whether an extension is support by the server.
|
||||
// The extension name is case-insensitive. If the extension is supported,
|
||||
// Extension also returns a string that contains any parameters the
|
||||
// server specifies for the extension.
|
||||
func (c *Client) Extension(ext string) (bool, string) {
|
||||
if err := c.hello(); err != nil {
|
||||
return false, ""
|
||||
}
|
||||
if c.ext == nil {
|
||||
return false, ""
|
||||
}
|
||||
ext = strings.ToUpper(ext)
|
||||
|
||||
c.mutex.RLock()
|
||||
param, ok := c.ext[ext]
|
||||
c.mutex.RUnlock()
|
||||
return ok, param
|
||||
}
|
||||
|
||||
// Reset sends the RSET command to the server, aborting the current mail
|
||||
// transaction.
|
||||
func (c *Client) Reset() error {
|
||||
if err := c.hello(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, err := c.cmd(250, "RSET")
|
||||
return err
|
||||
}
|
||||
|
||||
// Noop sends the NOOP command to the server. It does nothing but check
|
||||
// that the connection to the server is okay.
|
||||
func (c *Client) Noop() error {
|
||||
if err := c.hello(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, err := c.cmd(250, "NOOP")
|
||||
return err
|
||||
}
|
||||
|
||||
// Quit sends the QUIT command and closes the connection to the server.
|
||||
func (c *Client) Quit() error {
|
||||
// See https://github.com/golang/go/issues/70011
|
||||
_ = c.hello() // ignore error; we're quitting anyhow
|
||||
|
||||
_, _, err := c.cmd(221, "QUIT")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.mutex.Lock()
|
||||
err = c.Text.Close()
|
||||
c.isConnected = false
|
||||
c.mutex.Unlock()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// SetDebugLog enables the debug logging for incoming and outgoing SMTP messages
|
||||
func (c *Client) SetDebugLog(v bool) {
|
||||
c.debug = v
|
||||
if v {
|
||||
if c.logger == nil {
|
||||
c.logger = log.New(os.Stderr, log.LevelDebug)
|
||||
}
|
||||
return
|
||||
}
|
||||
c.logger = nil
|
||||
}
|
||||
|
||||
// SetLogger overrides the default log.Stdlog for the debug logging with a logger that
|
||||
// satisfies the log.Logger interface
|
||||
func (c *Client) SetLogger(l log.Logger) {
|
||||
if l == nil {
|
||||
return
|
||||
}
|
||||
c.mutex.Lock()
|
||||
c.logger = l
|
||||
c.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetLogAuthData enables logging of authentication data in the Client.
|
||||
func (c *Client) SetLogAuthData() {
|
||||
c.mutex.Lock()
|
||||
c.logAuthData = true
|
||||
c.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetDSNMailReturnOption sets the DSN mail return option for the Mail method
|
||||
func (c *Client) SetDSNMailReturnOption(d string) {
|
||||
c.mutex.Lock()
|
||||
c.dsnmrtype = d
|
||||
c.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetDSNRcptNotifyOption sets the DSN recipient notify option for the Mail method
|
||||
func (c *Client) SetDSNRcptNotifyOption(d string) {
|
||||
c.mutex.Lock()
|
||||
c.dsnrntype = d
|
||||
c.mutex.Unlock()
|
||||
}
|
||||
|
||||
// HasConnection checks if the client has an active connection.
|
||||
// Returns true if the `conn` field is not nil, indicating an active connection.
|
||||
func (c *Client) HasConnection() bool {
|
||||
c.mutex.RLock()
|
||||
isConn := c.isConnected
|
||||
c.mutex.RUnlock()
|
||||
return isConn
|
||||
}
|
||||
|
||||
// UpdateDeadline sets a new deadline on the SMTP connection with the specified timeout duration.
|
||||
func (c *Client) UpdateDeadline(timeout time.Duration) error {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
if c.conn == nil {
|
||||
return errors.New("smtp: client has no connection")
|
||||
}
|
||||
if err := c.conn.SetDeadline(time.Now().Add(timeout)); err != nil {
|
||||
return fmt.Errorf("smtp: failed to update deadline: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTLSConnectionState retrieves the TLS connection state of the client's current connection.
|
||||
// Returns an error if the connection is not using TLS or if the connection is not established.
|
||||
func (c *Client) GetTLSConnectionState() (*tls.ConnectionState, error) {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
|
||||
if !c.isConnected {
|
||||
return nil, ErrNoConnection
|
||||
}
|
||||
if !c.tls {
|
||||
return nil, ErrNonTLSConnection
|
||||
}
|
||||
if conn, ok := c.conn.(*tls.Conn); ok {
|
||||
cstate := conn.ConnectionState()
|
||||
return &cstate, nil
|
||||
}
|
||||
return nil, errors.New("unable to retrieve TLS connection state")
|
||||
}
|
||||
|
||||
// debugLog checks if the debug flag is set and if so logs the provided message to
|
||||
// the log.Logger interface
|
||||
func (c *Client) debugLog(d log.Direction, f string, a ...interface{}) {
|
||||
if c.debug {
|
||||
c.logger.Debugf(log.Log{Direction: d, Format: f, Messages: a})
|
||||
}
|
||||
}
|
||||
|
||||
// validateLine checks to see if a line has CR or LF as per RFC 5321.
|
||||
func validateLine(line string) error {
|
||||
if strings.ContainsAny(line, "\n\r") {
|
||||
return errors.New("smtp: A line must not contain CR or LF")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
|
||||
// SPDX-FileCopyrightText: Copyright (c) The go-mail Authors
|
||||
//
|
||||
// Original net/smtp code from the Go stdlib by the Go Authors.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// LICENSE file that can be found in this directory.
|
||||
//
|
||||
// go-mail specific modifications by the go-mail Authors.
|
||||
// Licensed under the MIT License.
|
||||
// See [PROJECT ROOT]/LICENSES directory for more information.
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-3-Clause AND MIT
|
||||
|
||||
package smtp
|
||||
|
||||
import "strings"
|
||||
|
||||
// ehlo sends the EHLO (extended hello) greeting to the server. It
|
||||
// should be the preferred greeting for servers that support it.
|
||||
func (c *Client) ehlo() error {
|
||||
_, msg, err := c.cmd(250, "EHLO %s", c.localName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
ext := make(map[string]string)
|
||||
extList := strings.Split(msg, "\n")
|
||||
if len(extList) > 1 {
|
||||
extList = extList[1:]
|
||||
for _, line := range extList {
|
||||
k, v, _ := strings.Cut(line, " ")
|
||||
ext[k] = v
|
||||
}
|
||||
}
|
||||
if mechs, ok := ext["AUTH"]; ok {
|
||||
c.auth = strings.Split(mechs, " ")
|
||||
}
|
||||
c.ext = ext
|
||||
return err
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
// SPDX-FileCopyrightText: The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mail
|
||||
|
||||
// TLSPolicy is a type wrapper for an int type and describes the different TLS policies we allow.
|
||||
type TLSPolicy int
|
||||
|
||||
const (
|
||||
// TLSMandatory requires that the connection to the server is
|
||||
// encrypting using STARTTLS. If the server does not support STARTTLS
|
||||
// the connection will be terminated with an error.
|
||||
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.
|
||||
TLSOpportunistic
|
||||
|
||||
// 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.
|
||||
func (p TLSPolicy) String() string {
|
||||
switch p {
|
||||
case TLSMandatory:
|
||||
return "TLSMandatory"
|
||||
case TLSOpportunistic:
|
||||
return "TLSOpportunistic"
|
||||
case NoTLS:
|
||||
return "NoTLS"
|
||||
default:
|
||||
return "UnknownPolicy"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user