Compare commits

..

13 Commits

Author SHA1 Message Date
Alexis Mousset
d930c42d50 Prepare 0.9.5 release 2020-11-11 17:39:04 +01:00
Alexis Mousset
1c4a630833 Prepare 0.9.4 release 2020-11-11 17:32:30 +01:00
Alexis Mousset
1738c8e52d fix(transport-sendmail): Stop argument parsing before destination addresses 2020-11-11 17:28:02 +01:00
Alexis Mousset
2b9e476b17 Merge pull request #421 from Zoruk/v0.9.x-fix-attachment
Fix attachment line length
2020-05-06 11:07:22 +02:00
Loïc Haas
44852e42f2 Fix attachment line length 2020-05-06 10:50:36 +02:00
Alexis Mousset
54032b5ce5 fix(builder): lettre_email 0.9.4 2020-04-21 22:05:54 +02:00
Alexis Mousset
6a40f4a5fe fix(builder): Go back to email 0.0.20 2020-04-21 22:04:55 +02:00
Alexis Mousset
a468cb3ab8 chore(all): 0.9.3 release 2020-04-19 10:56:46 +02:00
Alexis Mousset
9b591ff932 chore(all): Fix warnings 2020-04-19 10:24:23 +02:00
Alexis Mousset
eff4e1693f chore(builder): Update email to 0.0.21 2020-04-19 09:56:31 +02:00
Alexis Mousset
75ab05229a feat(all): v0.9.2 release 2019-06-11 19:58:32 +02:00
Alexis Mousset
393ef8dcd1 Simplify header formatting and fix nightly build (fixes #340) 2019-06-11 19:48:01 +02:00
Alexis Mousset
ceb57edfdd Remove failure crate usage (fixes #331) 2019-05-06 09:36:10 +02:00
142 changed files with 4263 additions and 18709 deletions

14
.appveyor.yml Normal file
View File

@@ -0,0 +1,14 @@
environment:
matrix:
- TARGET: x86_64-pc-windows-msvc
install:
- curl -sSf -o rustup-init.exe https://win.rustup.rs/
- rustup-init.exe -y --default-host %TARGET%
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
- rustc -vV
- cargo -vV
build: false
test_script:
- cargo build --verbose --manifest-path lettre/Cargo.toml
- cargo test --verbose --manifest-path lettre_email/Cargo.toml

20
.build-scripts/codecov.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/bash
set -xe
wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz
tar xzf master.tar.gz
cd kcov-master
mkdir build
cd build
cmake ..
make
make install DESTDIR=../../kcov-build
cd ../..
rm -rf kcov-master
for file in target/debug/lettre*[^\.d]; do
mkdir -p "target/cov/$(basename $file)"
./kcov-build/usr/local/bin/kcov --exclude-pattern=/.cargo,/usr/lib --verify "target/cov/$(basename $file)" "$file"
done
bash <(curl -s https://codecov.io/bash)
echo "Uploaded code coverage"

10
.build-scripts/site-upload.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
set -xe
cd website
make clean && make
echo "lettre.at" > _book/html/CNAME
sudo pip install ghp-import
ghp-import -n _book/html
git push -f https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages

View File

@@ -1,8 +0,0 @@
root = true
[*]
insert_final_newline = true
[*.rs]
indent_size = 4
indent_style = space

1
.github/FUNDING.yml vendored
View File

@@ -1 +0,0 @@
liberapay: amousset

View File

@@ -1,12 +0,0 @@
name: Security audit
on:
schedule:
- cron: '0 0 * * *'
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/audit-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,151 +0,0 @@
name: CI
on:
pull_request:
push:
branches:
- master
env:
RUSTFLAGS: "--cfg lettre_ignore_tls_mismatch"
RUSTDOCFLAGS: "--cfg lettre_ignore_tls_mismatch"
RUST_BACKTRACE: full
jobs:
rustfmt:
name: rustfmt / nightly-2024-09-01
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install rust
run: |
rustup default nightly-2024-09-01
rustup component add rustfmt
- name: cargo fmt
run: cargo fmt --all -- --check
clippy:
name: clippy / stable
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install rust
run: |
rustup update --no-self-update stable
rustup component add clippy
- name: Run clippy
run: cargo clippy --all-features --all-targets -- -D warnings
check:
name: check / stable
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install rust
run: rustup update --no-self-update stable
- name: Setup cache
uses: Swatinem/rust-cache@v2
- name: Install cargo hack
run: cargo install cargo-hack --debug
- name: Check with cargo hack
run: cargo hack check --feature-powerset --depth 3
test:
name: test / ${{ matrix.name }}
runs-on: ubuntu-latest
strategy:
matrix:
include:
- name: stable
rust: stable
- name: beta
rust: beta
- name: '1.70'
rust: '1.70'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install rust
run: |
rustup default ${{ matrix.rust }}
rustup update --no-self-update ${{ matrix.rust }}
- name: Setup cache
uses: Swatinem/rust-cache@v2
- name: Install postfix
run: |
DEBIAN_FRONTEND=noninteractive sudo apt-get update
DEBIAN_FRONTEND=noninteractive sudo apt-get -y install postfix
- name: Run SMTP server
run: smtp-sink 2525 1000&
- name: Install coredns
run: |
wget -q https://github.com/coredns/coredns/releases/download/v1.8.6/coredns_1.8.6_linux_amd64.tgz
tar xzf coredns_1.8.6_linux_amd64.tgz
- name: Start coredns
run: |
sudo ./coredns -conf testdata/coredns.conf &
sudo systemctl stop systemd-resolved
echo "nameserver 127.0.0.54" | sudo tee /etc/resolv.conf
- name: Install dkimverify
run: sudo apt -y install python3-dkim
- name: Test with no default features
run: cargo test --no-default-features
- name: Test with default features
run: cargo test
- name: Test with all features (-native-tls)
run: cargo test --no-default-features --features async-std1,async-std1-rustls-tls,boring-tls,builder,dkim,file-transport,file-transport-envelope,hostname,mime03,pool,rustls-native-certs,rustls-tls,sendmail-transport,smtp-transport,tokio1,tokio1-boring-tls,tokio1-rustls-tls,tracing
- name: Test with all features (-boring-tls)
run: cargo test --no-default-features --features async-std1,async-std1-rustls-tls,builder,dkim,file-transport,file-transport-envelope,hostname,mime03,native-tls,pool,rustls-native-certs,rustls-tls,sendmail-transport,smtp-transport,tokio1,tokio1-native-tls,tokio1-rustls-tls,tracing
# coverage:
# name: Coverage
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - uses: actions-rs/toolchain@v1
# with:
# toolchain: nightly
# override: true
# - run: sudo DEBIAN_FRONTEND=noninteractive apt-get -y install postfix
# - run: smtp-sink 2525 1000&
# - uses: actions-rs/cargo@v1
# with:
# command: test
# args: --no-fail-fast
# env:
# CARGO_INCREMENTAL: "0"
# RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Zno-landing-pads"
# - id: coverage
# uses: actions-rs/grcov@v0.1
# - name: Coveralls upload
# uses: coverallsapp/github-action@master
# with:
# github-token: ${{ secrets.GITHUB_TOKEN }}
# path-to-lcov: ${{ steps.coverage.outputs.report }}

2
.gitignore vendored
View File

@@ -1,6 +1,6 @@
.vscode/
.project/
.idea/
lettre.sublime-*
lettre.iml
target/
/Cargo.lock

29
.travis.yml Normal file
View File

@@ -0,0 +1,29 @@
language: rust
rust:
- stable
- beta
- nightly
- 1.32.0
matrix:
allow_failures:
- rust: nightly
sudo: required
addons:
apt:
packages:
- postfix
- libcurl4-openssl-dev
- libelf-dev
- libdw-dev
- cmake
- gcc
- binutils-dev
- libiberty-dev
before_script:
- smtp-sink 2525 1000&
- sudo chgrp -R postdrop /var/spool/postfix/maildrop
script:
- cargo test --verbose --all --all-features
after_success:
- ./.build-scripts/codecov.sh
- '[ "$TRAVIS_RUST_VERSION" = "stable" ] && [ "$TRAVIS_BRANCH" = "v0.9.x" ] && [ $TRAVIS_PULL_REQUEST = false ] && ./.build-scripts/site-upload.sh'

View File

@@ -1,345 +1,29 @@
<a name="v0.11.9"></a>
### v0.11.9 (2024-09-13)
#### Bug fixes
* Fix feature gate for `accept_invalid_hostnames` for rustls ([#988])
* Fix parsing `Mailbox` with trailing spaces ([#986])
#### Misc
* Bump `rustls-native-certs` to v0.8 ([#992])
* Make getting started example in readme complete ([#990])
[#988]: https://github.com/lettre/lettre/pull/988
[#986]: https://github.com/lettre/lettre/pull/986
[#990]: https://github.com/lettre/lettre/pull/990
[#992]: https://github.com/lettre/lettre/pull/992
<a name="v0.11.8"></a>
### v0.11.8 (2024-09-03)
#### Features
* Add mTLS support ([#974])
* Implement `accept_invalid_hostnames` for rustls ([#977])
* Provide certificate chain for peer certificates when using `rustls` or `boring-tls` ([#976])
#### Changes
* Make `HeaderName` comparisons via `PartialEq` case insensitive ([#980])
#### Misc
* Fix clippy warnings ([#979])
* Replace manual impl of `#[non_exhaustive]` for `InvalidHeaderName` ([#981])
[#974]: https://github.com/lettre/lettre/pull/974
[#976]: https://github.com/lettre/lettre/pull/976
[#977]: https://github.com/lettre/lettre/pull/977
[#980]: https://github.com/lettre/lettre/pull/980
[#981]: https://github.com/lettre/lettre/pull/981
<a name="v0.11.7"></a>
### v0.11.7 (2024-04-23)
#### Misc
* Bump `hostname` to v0.4 ([#956])
* Fix `tracing` message consistency ([#960])
* Bump minimum required `rustls` to v0.23.5 ([#958])
* Dropped use of `ref` syntax in the entire project ([#959])
[#956]: https://github.com/lettre/lettre/pull/956
[#958]: https://github.com/lettre/lettre/pull/958
[#959]: https://github.com/lettre/lettre/pull/959
[#960]: https://github.com/lettre/lettre/pull/960
<a name="v0.11.6"></a>
### v0.11.6 (2024-03-28)
#### Bug fixes
* Upgraded `email-encoding` to v0.3 - fixing multiple encoding bugs in the process ([#952])
#### Misc
* Updated copyright year in license ([#954])
[#952]: https://github.com/lettre/lettre/pull/952
[#954]: https://github.com/lettre/lettre/pull/954
<a name="v0.11.5"></a>
### v0.11.5 (2024-03-25)
#### Features
* Support SMTP SASL draft login challenge ([#911])
* Add conversion from SMTP response code to integer ([#941])
#### Misc
* Upgrade `rustls` to v0.23 ([#950])
* Bump `base64` to v0.22 ([#945])
* Fix typos in documentation ([#943], [#944])
* Add `Cargo.lock` ([#942])
[#911]: https://github.com/lettre/lettre/pull/911
[#941]: https://github.com/lettre/lettre/pull/941
[#942]: https://github.com/lettre/lettre/pull/942
[#943]: https://github.com/lettre/lettre/pull/943
[#944]: https://github.com/lettre/lettre/pull/944
[#945]: https://github.com/lettre/lettre/pull/945
[#950]: https://github.com/lettre/lettre/pull/950
<a name="v0.11.4"></a>
### v0.11.4 (2024-01-28)
#### Bug fixes
* Percent decode credentials in SMTP connect URL ([#932], [#934])
* Fix mimebody DKIM body-hash computation ([#923])
[#923]: https://github.com/lettre/lettre/pull/923
[#932]: https://github.com/lettre/lettre/pull/932
[#934]: https://github.com/lettre/lettre/pull/934
<a name="v0.11.3"></a>
### v0.11.3 (2024-01-02)
#### Features
* Derive `Clone` for `FileTransport` and `AsyncFileTransport` ([#924])
* Derive `Debug` for `SmtpTransport` ([#925])
#### Misc
* Upgrade `rustls` to v0.22 ([#921])
* Drop once_cell dependency in favor of OnceLock from std ([#928])
[#921]: https://github.com/lettre/lettre/pull/921
[#924]: https://github.com/lettre/lettre/pull/924
[#925]: https://github.com/lettre/lettre/pull/925
[#928]: https://github.com/lettre/lettre/pull/928
<a name="v0.11.2"></a>
### v0.11.2 (2023-11-23)
#### Upgrade notes
* MSRV is now 1.70 ([#916])
#### Misc
* Bump `idna` to v0.5 ([#918])
* Bump `boring` and `tokio-boring` to v4 ([#915])
[#915]: https://github.com/lettre/lettre/pull/915
[#916]: https://github.com/lettre/lettre/pull/916
[#918]: https://github.com/lettre/lettre/pull/918
<a name="v0.11.1"></a>
### v0.11.1 (2023-10-24)
#### Bug fixes
* Fix `webpki-roots` certificate store setup ([#909])
[#909]: https://github.com/lettre/lettre/pull/909
<a name="v0.11.0"></a>
### v0.11.0 (2023-10-15)
While this release technically contains breaking changes, we expect most projects
to be able to upgrade by only bumping the version in `Cargo.toml`.
#### Upgrade notes
* MSRV is now 1.65 ([#869] and [#881])
* `AddressError` is now marked as `#[non_exhaustive]` ([#839])
#### Features
* Improve mailbox parsing ([#839])
* Add construction of SMTP transport from URL ([#901])
* Add `From<Address>` implementation for `Mailbox` ([#879])
#### Misc
* Bump `socket2` to v0.5 ([#868])
* Bump `idna` to v0.4, `fastrand` to v2, `quoted_printable` to v0.5, `rsa` to v0.9 ([#882])
* Bump `webpki-roots` to v0.25 ([#884] and [#890])
* Bump `ed25519-dalek` to v2 fixing RUSTSEC-2022-0093 ([#896])
* Bump `boring`ssl crates to v3 ([#897])
[#839]: https://github.com/lettre/lettre/pull/839
[#868]: https://github.com/lettre/lettre/pull/868
[#869]: https://github.com/lettre/lettre/pull/869
[#879]: https://github.com/lettre/lettre/pull/879
[#881]: https://github.com/lettre/lettre/pull/881
[#882]: https://github.com/lettre/lettre/pull/882
[#884]: https://github.com/lettre/lettre/pull/884
[#890]: https://github.com/lettre/lettre/pull/890
[#896]: https://github.com/lettre/lettre/pull/896
[#897]: https://github.com/lettre/lettre/pull/897
[#901]: https://github.com/lettre/lettre/pull/901
<a name="v0.10.4"></a>
### v0.10.4 (2023-04-02)
#### Misc
* Bumped rustls to 0.21 and all related dependencies ([#867])
[#867]: https://github.com/lettre/lettre/pull/867
<a name="v0.10.3"></a>
### v0.10.3 (2023-02-20)
#### Announcements
It was found that what had been used until now as a basic lettre 0.10
`MessageBuilder::body` example failed to mention that for maximum
compatibility with various email clients a `Content-Type` header
should always be present in the message.
##### Before
```rust
Message::builder()
// [...] some headers skipped for brevity
.body(String::from("A plaintext or html body"))?
```
##### Patch
```diff
Message::builder()
// [...] some headers skipped for brevity
+ .header(ContentType::TEXT_PLAIN) // or `TEXT_HTML` if the body is html
.body(String::from("A plaintext or html body"))?
```
#### Features
* Add support for rustls-native-certs when using rustls ([#843])
[#843]: https://github.com/lettre/lettre/pull/843
<a name="v0.10.2"></a>
### v0.10.2 (2023-01-29)
#### Upgrade notes
* MSRV is now 1.60 ([#828])
#### Features
* Allow providing a custom `tokio` stream for `AsyncSmtpTransport` ([#805])
* Return whole SMTP error message ([#821])
#### Bug fixes
* Mailbox displays wrongly when containing a comma and a non-ascii char in its name ([#827])
* Require `quoted_printable` ^0.4.6 in order to fix encoding of tabs and spaces at the end of line ([#837])
#### Misc
* Increase tracing ([#848])
* Bump `idna` to 0.3 ([#816])
* Update `base64` to 0.21 ([#840] and [#851])
* Update `rsa` to 0.8 ([#829] and [#852])
[#805]: https://github.com/lettre/lettre/pull/805
[#816]: https://github.com/lettre/lettre/pull/816
[#821]: https://github.com/lettre/lettre/pull/821
[#827]: https://github.com/lettre/lettre/pull/827
[#828]: https://github.com/lettre/lettre/pull/828
[#829]: https://github.com/lettre/lettre/pull/829
[#837]: https://github.com/lettre/lettre/pull/837
[#840]: https://github.com/lettre/lettre/pull/840
[#848]: https://github.com/lettre/lettre/pull/848
[#851]: https://github.com/lettre/lettre/pull/851
[#852]: https://github.com/lettre/lettre/pull/852
<a name="v0.10.1"></a>
### v0.10.1 (2022-07-20)
#### Features
* Add `boring-tls` support for `SmtpTransport` and `AsyncSmtpTransport`. The latter is only supported with the tokio runtime. ([#797]) ([#798])
* Make the minimum TLS version configurable. ([#799]) ([#800])
<a name="v0.9.5"></a>
### v0.9.5 (2020-11-11)
#### Bug Fixes
* Ensure connections are closed on abort. ([#801])
* Fix SMTP dot stuffing. ([#803])
* **transport**
* **SECURITY**: Prevent argument injection in sendmail transport
[#797]: https://github.com/lettre/lettre/pull/797
[#798]: https://github.com/lettre/lettre/pull/798
[#799]: https://github.com/lettre/lettre/pull/799
[#800]: https://github.com/lettre/lettre/pull/800
[#801]: https://github.com/lettre/lettre/pull/801
[#803]: https://github.com/lettre/lettre/pull/803
<a name="v0.10.0"></a>
### v0.10.0 (2022-06-29)
#### Upgrade notes
Several breaking changes were made between 0.9 and 0.10, but changes should be straightforward:
* MSRV is now 1.56.0
* The `lettre_email` crate has been merged into `lettre`. To migrate, replace `lettre_email` with `lettre::message`
and make sure to enable the `builder` feature (it's enabled by default).
* `SendableEmail` has been renamed to `Email` and `EmailBuilder::build()` produces it directly. To migrate,
rename `SendableEmail` to `Email`.
* The `serde-impls` feature has been renamed to `serde`. To migrate, rename the feature.
#### Features
* Add `tokio` 1 support
* Add `rustls` support
* Add `async-std` support. NOTE: native-tls isn't supported when using async-std for the smtp transport.
* Allow enabling multiple SMTP authentication mechanisms
* Allow providing a custom message id
* Allow sending raw emails
#### Breaking Changes
* Merge `lettre_email` into `lettre`
* Merge `Email` and `SendableEmail` into `lettre::message::Email`
* SmtpTransport is now an high level SMTP client. It provides connection pooling and shortcuts for building clients using commonly desired values
* Refactor `TlsParameters` implementation to not expose the internal TLS library
* `FileTransport` writes emails into `.eml` instead of `.json`
* When the hostname feature is disabled or hostname cannot be fetched, `127.0.0.1` is used instead of `localhost` as EHLO parameter (for better RFC compliance and mail server compatibility)
* The `sendmail` and `file` transports aren't enabled by default anymore.
* The `new` method of `ClientId` is deprecated
* Rename `serde-impls` feature to `serde`
* The `SendmailTransport` now uses the `sendmail` command in current `PATH` by default instead of
`/usr/bin/sendmail`.
<a name="v0.9.4"></a>
### v0.9.4 (2020-04-21)
#### Bug Fixes
* Fix argument injection in `SendmailTransport` (see [RUSTSEC-2020-0069](https://github.com/RustSec/advisory-db/blob/master/crates/lettre/RUSTSEC-2020-0069.md))
* Correctly encode header values containing non-ASCII characters
* Timeout bug causing infinite hang
* Fix doc tests in website
* Fix docs for `domain` field
* **email**
* Go back to `rust-email` 0.0.20 as upgrade broke message formatting ([6a40f4a](https://github.com/lettre/lettre/commit/6a40f4a)
#### Misc
<a name="v0.9.3"></a>
### v0.9.3 (2020-04-19)
* Improve documentation, examples and tests
* Replace `line-wrap`, `email`, `bufstream` with our own implementations
* Remove `bytes`
* Remove `time`
* Remove `fast_chemail`
* Update `base64` to 0.13
* Update `hostname` to 0.3
* Update to `nom` 6
* Replace `log` with `tracing`
* Move CI to GitHub Actions
* Use criterion for benchmarks
#### Bug Fixes
* **all:**
* Fix compilation warnings ([9b591ff](https://github.com/lettre/lettre/commit/9b591ff932e35947f793aaaeec0e3f06e8818449))
* **email**
* Update `rust-email` to 0.0.21 ([eff4e169](https://github.com/lettre/lettre/commit/eff4e1693f5e65430b851707fdfd18046bc796e3))
<a name="v0.9.2"></a>
### v0.9.2 (2019-06-11)

View File

@@ -34,13 +34,13 @@ This Code of Conduct applies both within project spaces and in public spaces whe
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@lettre.rs. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@lettre.at. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://www.contributor-covenant.org/version/1/4][version]
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: https://www.contributor-covenant.org
[version]: https://www.contributor-covenant.org/version/1/4/
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@@ -1,6 +1,6 @@
## Contributing to Lettre
The following guidelines are inspired by the [hyper project](https://github.com/hyperium/hyper/blob/master/CONTRIBUTING.md).
The following guidelines are inspired from the [hyper project](https://github.com/hyperium/hyper/blob/master/CONTRIBUTING.md).
### Code formatting
@@ -33,11 +33,3 @@ Any line of the commit message cannot be longer 72 characters.
all
The body explains the change, and the footer contains relevant changelog notes and references to fixed issues.
### Release process
Releases are made using `cargo-release`:
```bash
cargo release --dry-run 0.10.0 --prev-tag-name v0.9.2 -v
```

2710
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,174 +1,5 @@
[package]
name = "lettre"
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
version = "0.11.9"
description = "Email client"
readme = "README.md"
homepage = "https://lettre.rs"
repository = "https://github.com/lettre/lettre"
license = "MIT"
authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo565.org>"]
categories = ["email", "network-programming"]
keywords = ["email", "smtp", "mailer", "message", "sendmail"]
edition = "2021"
rust-version = "1.70"
[badges]
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
is-it-maintained-open-issues = { repository = "lettre/lettre" }
maintenance = { status = "actively-developed" }
[dependencies]
chumsky = "0.9"
idna = "1"
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
# builder
httpdate = { version = "1", optional = true }
mime = { version = "0.3.4", optional = true }
fastrand = { version = "2.0", optional = true }
quoted_printable = { version = "0.5", optional = true }
base64 = { version = "0.22", optional = true }
email-encoding = { version = "0.3", optional = true }
# file transport
uuid = { version = "1", features = ["v4"], optional = true }
serde = { version = "1", optional = true, features = ["derive"] }
serde_json = { version = "1", optional = true }
# smtp-transport
nom = { version = "7", optional = true }
hostname = { version = "0.4", optional = true } # feature
socket2 = { version = "0.5.1", optional = true }
url = { version = "2.4", optional = true }
percent-encoding = { version = "2.3", optional = true }
## tls
native-tls = { version = "0.2.5", optional = true } # feature
rustls = { version = "0.23.5", default-features = false, features = ["ring", "logging", "std", "tls12"], optional = true }
rustls-pemfile = { version = "2", optional = true }
rustls-native-certs = { version = "0.8", optional = true }
rustls-pki-types = { version = "1.7", optional = true }
webpki-roots = { version = "0.26", optional = true }
boring = { version = "4", optional = true }
# async
futures-io = { version = "0.3.7", optional = true }
futures-util = { version = "0.3.7", default-features = false, features = ["io"], optional = true }
async-trait = { version = "0.1", optional = true }
## async-std
async-std = { version = "1.8", optional = true }
futures-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12", "ring"], optional = true }
## tokio
tokio1_crate = { package = "tokio", version = "1", optional = true }
tokio1_native_tls_crate = { package = "tokio-native-tls", version = "0.3", optional = true }
tokio1_rustls = { package = "tokio-rustls", version = "0.26", default-features = false, features = ["logging", "tls12", "ring"], optional = true }
tokio1_boring = { package = "tokio-boring", version = "4", optional = true }
## dkim
sha2 = { version = "0.10", optional = true, features = ["oid"] }
rsa = { version = "0.9", optional = true }
ed25519-dalek = { version = "2", optional = true }
# email formats
email_address = { version = "0.2.1", default-features = false }
[dev-dependencies]
pretty_assertions = "1"
criterion = "0.5"
tracing = { version = "0.1.16", default-features = false, features = ["std"] }
tracing-subscriber = "0.3"
glob = "0.3"
walkdir = "2"
tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] }
async-std = { version = "1.8", features = ["attributes"] }
serde_json = "1"
maud = "0.26"
[[bench]]
harness = false
name = "transport_smtp"
[[bench]]
harness = false
name = "mailbox_parsing"
[features]
default = ["smtp-transport", "pool", "native-tls", "hostname", "builder"]
builder = ["dep:httpdate", "dep:mime", "dep:fastrand", "dep:quoted_printable", "dep:email-encoding"]
mime03 = ["dep:mime"]
# transports
file-transport = ["dep:uuid", "tokio1_crate?/fs", "tokio1_crate?/io-util"]
file-transport-envelope = ["serde", "dep:serde_json", "file-transport"]
sendmail-transport = ["tokio1_crate?/process", "tokio1_crate?/io-util", "async-std?/unstable"]
smtp-transport = ["dep:base64", "dep:nom", "dep:socket2", "dep:url", "dep:percent-encoding", "tokio1_crate?/rt", "tokio1_crate?/time", "tokio1_crate?/net"]
pool = ["dep:futures-util"]
rustls-tls = ["dep:webpki-roots", "dep:rustls", "dep:rustls-pemfile", "dep:rustls-pki-types"]
boring-tls = ["dep:boring"]
# async
async-std1 = ["dep:async-std", "dep:async-trait", "dep:futures-io", "dep:futures-util"]
async-std1-rustls-tls = ["async-std1", "rustls-tls", "dep:futures-rustls"]
tokio1 = ["dep:tokio1_crate", "dep:async-trait", "dep:futures-io", "dep:futures-util"]
tokio1-native-tls = ["tokio1", "native-tls", "dep:tokio1_native_tls_crate"]
tokio1-rustls-tls = ["tokio1", "rustls-tls", "dep:tokio1_rustls"]
tokio1-boring-tls = ["tokio1", "boring-tls", "dep:tokio1_boring"]
dkim = ["dep:base64", "dep:sha2", "dep:rsa", "dep:ed25519-dalek"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(lettre_ignore_tls_mismatch)'] }
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs", "--cfg", "lettre_ignore_tls_mismatch"]
[[example]]
name = "autoconfigure"
required-features = ["smtp-transport", "native-tls"]
[[example]]
name = "basic_html"
required-features = ["file-transport", "builder"]
[[example]]
name = "maud_html"
required-features = ["file-transport", "builder"]
[[example]]
name = "smtp"
required-features = ["smtp-transport", "builder"]
[[example]]
name = "smtp_tls"
required-features = ["smtp-transport", "native-tls", "builder"]
[[example]]
name = "smtp_starttls"
required-features = ["smtp-transport", "native-tls", "builder"]
[[example]]
name = "smtp_selfsigned"
required-features = ["smtp-transport", "native-tls", "builder"]
[[example]]
name = "tokio1_smtp_tls"
required-features = ["smtp-transport", "tokio1", "tokio1-native-tls", "builder"]
[[example]]
name = "tokio1_smtp_starttls"
required-features = ["smtp-transport", "tokio1", "tokio1-native-tls", "builder"]
[[example]]
name = "asyncstd1_smtp_tls"
required-features = ["smtp-transport", "async-std1", "async-std1-rustls-tls", "builder"]
[[example]]
name = "asyncstd1_smtp_starttls"
required-features = ["smtp-transport", "async-std1", "async-std1-rustls-tls", "builder"]
[workspace]
members = [
"lettre",
"lettre_email",
]

View File

@@ -1,6 +1,4 @@
Copyright (c) 2014-2024 Alexis Mousset <contact@amousset.me>
Copyright (c) 2019-2024 Paolo Barbolini <paolo@paolo565.org>
Copyright (c) 2018 K. <kayo@illumium.org>
Copyright (c) 2014-2018 Alexis Mousset
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated

140
README.md
View File

@@ -1,38 +1,25 @@
<h1 align="center">lettre</h1>
<div align="center">
<strong>
A mailer library for Rust
</strong>
</div>
# lettre
<br />
**Lettre is a mailer library for Rust.**
<div align="center">
<a href="https://docs.rs/lettre">
<img src="https://docs.rs/lettre/badge.svg"
alt="docs" />
</a>
<a href="https://crates.io/crates/lettre">
<img src="https://img.shields.io/crates/d/lettre.svg"
alt="downloads" />
</a>
<br />
<a href="https://gitter.im/lettre/lettre">
<img src="https://badges.gitter.im/lettre/lettre.svg"
alt="chat on gitter" />
</a>
<a href="https://lettre.rs">
<img src="https://img.shields.io/badge/visit-website-blueviolet"
alt="website" />
</a>
</div>
[![Build Status](https://travis-ci.org/lettre/lettre.svg?branch=master)](https://travis-ci.org/lettre/lettre)
[![Build status](https://ci.appveyor.com/api/projects/status/mpwglemugjtkps2d/branch/master?svg=true)](https://ci.appveyor.com/project/amousset/lettre/branch/master)
[![codecov](https://codecov.io/gh/lettre/lettre/branch/master/graph/badge.svg)](https://codecov.io/gh/lettre/lettre)
<div align="center">
<a href="https://deps.rs/crate/lettre/0.11.9">
<img src="https://deps.rs/crate/lettre/0.11.9/status.svg"
alt="dependency status" />
</a>
</div>
[![Crate](https://img.shields.io/crates/v/lettre.svg)](https://crates.io/crates/lettre)
[![Docs](https://docs.rs/lettre/badge.svg)](https://docs.rs/lettre/)
[![Required Rust version](https://img.shields.io/badge/rustc-1.20-green.svg)]()
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
[![Gitter](https://badges.gitter.im/lettre/lettre.svg)](https://gitter.im/lettre/lettre?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/lettre/lettre.svg)](http://isitmaintained.com/project/lettre/lettre "Average time to resolve an issue")
[![Percentage of issues still open](http://isitmaintained.com/badge/open/lettre/lettre.svg)](http://isitmaintained.com/project/lettre/lettre "Percentage of issues still open")
Useful links:
* [User documentation](http://lettre.at/)
* [API documentation](https://docs.rs/lettre/)
* [Changelog](https://github.com/lettre/lettre/blob/master/CHANGELOG.md)
---
@@ -44,84 +31,56 @@ Lettre provides the following features:
* Unicode support (for email content and addresses)
* Secure delivery with SMTP using encryption and authentication
* Easy email builders
* Async support
Lettre does not provide (for now):
* Email parsing
## Supported Rust Versions
Lettre supports all Rust versions released in the last 6 months. At the time of writing
the minimum supported Rust version is 1.70, but this could change at any time either from
one of our dependencies bumping their MSRV or by a new patch release of lettre.
## Example
This library requires Rust 1.70 or newer.
This library requires Rust 1.32 or newer.
To use this library, add the following to your `Cargo.toml`:
```toml
[dependencies]
lettre = "0.11"
lettre = "0.9"
lettre_email = "0.9"
```
```rust,no_run
use lettre::message::header::ContentType;
use lettre::transport::smtp::authentication::Credentials;
use lettre::{Message, SmtpTransport, Transport};
extern crate lettre;
extern crate lettre_email;
use lettre::{EmailTransport, SmtpTransport};
use lettre_email::EmailBuilder;
use std::path::Path;
fn main() {
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!"))
let email = EmailBuilder::new()
// Addresses can be specified by the tuple (email, alias)
.to(("user@example.org", "Firstname Lastname"))
// ... or by an address only
.from("user@example.com")
.subject("Hi, Hello world")
.text("Hello world.")
.build()
.unwrap();
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail
let mailer = SmtpTransport::relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Open a local connection on port 25
let mut mailer = SmtpTransport::builder_unencrypted_localhost().unwrap()
.build();
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {e:?}"),
let result = mailer.send(&email);
if result.is_ok() {
println!("Email sent");
} else {
println!("Could not send email: {:?}", result);
}
assert!(result.is_ok());
}
```
## Not sure of which connect options to use?
Clone the lettre git repository and run the following command (replacing `SMTP_HOST` with your SMTP server's hostname)
```shell
cargo run --example autoconfigure SMTP_HOST
```
## Testing
The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command. If you have python installed
such a server can be launched with `python -m smtpd -n -c DebuggingServer 127.0.0.1:2525`
Alternatively only unit tests can be run by doing `cargo test --lib`.
## Troubleshooting
These are general steps to be followed when troubleshooting SMTP related issues.
- Ensure basic connectivity, ensure requisite ports are open and daemons are listening.
- Confirm that your service provider allows traffic on the ports being used for mail transfer.
- Check SMTP relay authentication and configuration.
- Validate your DNS records. (DMARC, SPF, DKIM, MX)
- Verify your SSL/TLS certificates are setup properly.
- Investigate if filtering, formatting, or filesize limits are causing messages to be lost, delayed, or blocked by relays or remote hosts.
The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command.
## Code of conduct
@@ -132,7 +91,4 @@ this GitHub repository, must follow our [code of conduct](https://github.com/let
This program is distributed under the terms of the MIT license.
The builder comes from [emailmessage-rs](https://github.com/katyo/emailmessage-rs) by
Kayo, under MIT license.
See [LICENSE](./LICENSE) for details.

View File

@@ -1,9 +0,0 @@
## Report a security issue
The lettre project team welcomes security reports and is committed to providing prompt attention to security issues.
Security issues should be reported privately via [security@lettre.rs](mailto:security@lettre.rs). Security issues
should not be reported via the public GitHub Issue tracker.
## Security advisories
Security issues will be announced via the [RustSec advisory database](https://github.com/RustSec/advisory-db).

View File

@@ -1,27 +0,0 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use lettre::message::{Mailbox, Mailboxes};
fn bench_parse_single(mailbox: &str) {
assert!(mailbox.parse::<Mailbox>().is_ok());
}
fn bench_parse_multiple(mailboxes: &str) {
assert!(mailboxes.parse::<Mailboxes>().is_ok());
}
fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("parse single mailbox", |b| {
b.iter(|| bench_parse_single(black_box("\"Benchmark test\" <test@mail.local>")))
});
c.bench_function("parse multiple mailboxes", |b| {
b.iter(|| {
bench_parse_multiple(black_box(
"\"Benchmark test\" <test@mail.local>, Test <test@mail.local>, <test@mail.local>, test@mail.local",
))
})
});
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

View File

@@ -1,44 +0,0 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use lettre::{Message, SmtpTransport, Transport};
fn bench_simple_send(c: &mut Criterion) {
let sender = SmtpTransport::builder_dangerous("127.0.0.1")
.port(2525)
.build();
c.bench_function("send email", move |b| {
b.iter(|| {
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.body(String::from("Be happy!"))
.unwrap();
let result = black_box(sender.send(&email));
assert!(result.is_ok());
})
});
}
fn bench_reuse_send(c: &mut Criterion) {
let sender = SmtpTransport::builder_dangerous("127.0.0.1")
.port(2525)
.build();
c.bench_function("send email with connection reuse", move |b| {
b.iter(|| {
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.body(String::from("Be happy!"))
.unwrap();
let result = black_box(sender.send(&email));
assert!(result.is_ok());
})
});
}
criterion_group!(benches, bench_simple_send, bench_reuse_send);
criterion_main!(benches);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,24 +0,0 @@
# Lettre Examples
This folder contains examples showing how to use lettre in your own projects.
## Message builder examples
- [basic_html.rs] - Create an HTML email.
- [maud_html.rs] - Create an HTML email using a [maud](https://github.com/lambda-fairy/maud) template.
## SMTP Examples
- [smtp.rs] - Send an email using a local SMTP daemon on port 25 as a relay.
- [smtp_tls.rs] - Send an email over SMTP encrypted with TLS and authenticating with username and password.
- [smtp_starttls.rs] - Send an email over SMTP with STARTTLS and authenticating with username and password.
- [smtp_selfsigned.rs] - Send an email over SMTP encrypted with TLS using a self-signed certificate and authenticating with username and password.
- The [smtp_tls.rs] and [smtp_starttls.rs] examples also feature `async`hronous implementations powered by [Tokio](https://tokio.rs/).
These files are prefixed with `tokio1_` or `asyncstd1_`.
[basic_html.rs]: ./basic_html.rs
[maud_html.rs]: ./maud_html.rs
[smtp.rs]: ./smtp.rs
[smtp_tls.rs]: ./smtp_tls.rs
[smtp_starttls.rs]: ./smtp_starttls.rs
[smtp_selfsigned.rs]: ./smtp_selfsigned.rs

View File

@@ -1,33 +0,0 @@
use lettre::{
message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
AsyncStd1Executor, AsyncTransport, Message,
};
#[async_std::main]
async fn main() {
tracing_subscriber::fmt::init();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy with async!"))
.unwrap();
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail using STARTTLS
let mailer: AsyncSmtpTransport<AsyncStd1Executor> =
AsyncSmtpTransport::<AsyncStd1Executor>::starttls_relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {e:?}"),
}
}

View File

@@ -1,33 +0,0 @@
use lettre::{
message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
AsyncStd1Executor, AsyncTransport, Message,
};
#[async_std::main]
async fn main() {
tracing_subscriber::fmt::init();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy with async!"))
.unwrap();
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail
let mailer: AsyncSmtpTransport<AsyncStd1Executor> =
AsyncSmtpTransport::<AsyncStd1Executor>::relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {e:?}"),
}
}

View File

@@ -1,93 +0,0 @@
use std::{env, process, time::Duration};
use lettre::SmtpTransport;
fn main() {
tracing_subscriber::fmt::init();
let smtp_host = match env::args().nth(1) {
Some(smtp_host) => smtp_host,
None => {
println!("Please provide the SMTP host as the first argument to this command");
process::exit(1);
}
};
// TLS wrapped connection
{
tracing::info!(
"Trying to establish a TLS wrapped connection to {}",
smtp_host
);
let transport = SmtpTransport::relay(&smtp_host)
.expect("build SmtpTransport::relay")
.timeout(Some(Duration::from_secs(10)))
.build();
match transport.test_connection() {
Ok(true) => {
tracing::info!("Successfully connected to {} via a TLS wrapped connection (SmtpTransport::relay). This is the fastest option available for connecting to an SMTP server", smtp_host);
}
Ok(false) => {
tracing::error!("Couldn't connect to {} via a TLS wrapped connection. No more information is available", smtp_host);
}
Err(err) => {
tracing::error!(err = %err, "Couldn't connect to {} via a TLS wrapped connection", smtp_host);
}
}
}
println!();
// Plaintext connection which MUST then successfully upgrade to TLS via STARTTLS
{
tracing::info!("Trying to establish a plaintext connection to {} and then upgrading it via the SMTP STARTTLS extension", smtp_host);
let transport = SmtpTransport::starttls_relay(&smtp_host)
.expect("build SmtpTransport::starttls_relay")
.timeout(Some(Duration::from_secs(10)))
.build();
match transport.test_connection() {
Ok(true) => {
tracing::info!("Successfully connected to {} via a plaintext connection which then got upgraded to TLS via the SMTP STARTTLS extension (SmtpTransport::starttls_relay). This is the second best option after the previous TLS wrapped option", smtp_host);
}
Ok(false) => {
tracing::error!(
"Couldn't connect to {} via STARTTLS. No more information is available",
smtp_host
);
}
Err(err) => {
tracing::error!(err = %err, "Couldn't connect to {} via STARTTLS", smtp_host);
}
}
}
println!();
// Plaintext connection (very insecure)
{
tracing::info!(
"Trying to establish a plaintext connection to {}",
smtp_host
);
let transport = SmtpTransport::builder_dangerous(&smtp_host)
.timeout(Some(Duration::from_secs(10)))
.build();
match transport.test_connection() {
Ok(true) => {
tracing::info!("Successfully connected to {} via a plaintext connection. This option is very insecure and shouldn't be used on the public internet (SmtpTransport::builder_dangerous)", smtp_host);
}
Ok(false) => {
tracing::error!(
"Couldn't connect to {} via a plaintext connection. No more information is available",
smtp_host
);
}
Err(err) => {
tracing::error!(err = %err, "Couldn't connect to {} via a plaintext connection", smtp_host);
}
}
}
}

View File

@@ -1,49 +0,0 @@
use lettre::{
message::{header, MultiPart, SinglePart},
FileTransport, Message, Transport,
};
fn main() {
// The html we want to send.
let html = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello from Lettre!</title>
</head>
<body>
<div style="display: flex; flex-direction: column; align-items: center;">
<h2 style="font-family: Arial, Helvetica, sans-serif;">Hello from Lettre!</h2>
<h4 style="font-family: Arial, Helvetica, sans-serif;">A mailer library for Rust</h4>
</div>
</body>
</html>"#;
// Build the message.
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Hello from Lettre!")
.multipart(
MultiPart::alternative() // This is composed of two parts.
.singlepart(
SinglePart::builder()
.header(header::ContentType::TEXT_PLAIN)
.body(String::from("Hello from Lettre! A mailer library for Rust")), // Every message should have a plain text fallback.
)
.singlepart(
SinglePart::builder()
.header(header::ContentType::TEXT_HTML)
.body(String::from(html)),
),
)
.expect("failed to build email");
// Create our mailer. Please see the other examples for creating SMTP mailers.
// The path given here must exist on the filesystem.
let mailer = FileTransport::new("./");
// Store the message when you're ready.
mailer.send(&email).expect("failed to deliver message");
}

View File

@@ -1,58 +0,0 @@
use lettre::{
message::{header, MultiPart, SinglePart},
FileTransport, Message, Transport,
};
use maud::html;
fn main() {
// The recipient's name. We might obtain this from a form or their email address.
let recipient = "Hei";
// Create the html we want to send.
let html = html! {
head {
title { "Hello from Lettre!" }
style type="text/css" {
"h2, h4 { font-family: Arial, Helvetica, sans-serif; }"
}
}
div style="display: flex; flex-direction: column; align-items: center;" {
h2 { "Hello from Lettre!" }
// Substitute in the name of our recipient.
p { "Dear " (recipient) "," }
p { "This email was sent with Lettre, a mailer library for Rust!"}
p {
"This example uses "
a href="https://crates.io/crates/maud" { "maud" }
". It is about 20% cooler than the basic HTML example."
}
}
};
// Build the message.
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Hello from Lettre!")
.multipart(
MultiPart::alternative() // This is composed of two parts.
.singlepart(
SinglePart::builder()
.header(header::ContentType::TEXT_PLAIN)
.body(String::from("Hello from Lettre! A mailer library for Rust")), // Every message should have a plain text fallback.
)
.singlepart(
SinglePart::builder()
.header(header::ContentType::TEXT_HTML)
.body(html.into_string()),
),
)
.expect("failed to build email");
// Create our mailer. Please see the other examples for creating SMTP mailers.
// The path given here must exist on the filesystem.
let mailer = FileTransport::new("./");
// Store the message when you're ready.
mailer.send(&email).expect("failed to deliver message");
}

View File

@@ -1,23 +0,0 @@
use lettre::{message::header::ContentType, Message, SmtpTransport, Transport};
fn main() {
tracing_subscriber::fmt::init();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!"))
.unwrap();
// Open a local connection on port 25
let mailer = SmtpTransport::unencrypted_localhost();
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {e:?}"),
}
}

View File

@@ -1,46 +0,0 @@
use std::fs;
use lettre::{
message::header::ContentType,
transport::smtp::{
authentication::Credentials,
client::{Certificate, Tls, TlsParameters},
},
Message, SmtpTransport, Transport,
};
fn main() {
tracing_subscriber::fmt::init();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!"))
.unwrap();
// Use a custom certificate stored on disk to securely verify the server's certificate
let pem_cert = fs::read("certificate.pem").unwrap();
let cert = Certificate::from_pem(&pem_cert).unwrap();
let tls = TlsParameters::builder("smtp.server.com".to_owned())
.add_root_certificate(cert)
.build()
.unwrap();
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to the smtp server
let mailer = SmtpTransport::builder_dangerous("smtp.server.com")
.port(465)
.tls(Tls::Wrapper(tls))
.credentials(creds)
.build();
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {e:?}"),
}
}

View File

@@ -1,31 +0,0 @@
use lettre::{
message::header::ContentType, transport::smtp::authentication::Credentials, Message,
SmtpTransport, Transport,
};
fn main() {
tracing_subscriber::fmt::init();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!"))
.unwrap();
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail using STARTTLS
let mailer = SmtpTransport::starttls_relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {e:?}"),
}
}

View File

@@ -1,31 +0,0 @@
use lettre::{
message::header::ContentType, transport::smtp::authentication::Credentials, Message,
SmtpTransport, Transport,
};
fn main() {
tracing_subscriber::fmt::init();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!"))
.unwrap();
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail
let mailer = SmtpTransport::relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {e:?}"),
}
}

View File

@@ -1,37 +0,0 @@
// This line is only to make it compile from lettre's examples folder,
// since it uses Rust 2018 crate renaming to import tokio.
// Won't be needed in user's code.
use lettre::{
message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
AsyncTransport, Message, Tokio1Executor,
};
use tokio1_crate as tokio;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy with async!"))
.unwrap();
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail using STARTTLS
let mailer: AsyncSmtpTransport<Tokio1Executor> =
AsyncSmtpTransport::<Tokio1Executor>::starttls_relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {e:?}"),
}
}

View File

@@ -1,37 +0,0 @@
// This line is only to make it compile from lettre's examples folder,
// since it uses Rust 2018 crate renaming to import tokio.
// Won't be needed in user's code.
use lettre::{
message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
AsyncTransport, Message, Tokio1Executor,
};
use tokio1_crate as tokio;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy with async!"))
.unwrap();
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail
let mailer: AsyncSmtpTransport<Tokio1Executor> =
AsyncSmtpTransport::<Tokio1Executor>::relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {e:?}"),
}
}

1
lettre/CHANGELOG.md Symbolic link
View File

@@ -0,0 +1 @@
../CHANGELOG.md

53
lettre/Cargo.toml Normal file
View File

@@ -0,0 +1,53 @@
[package]
name = "lettre"
version = "0.9.5" # remember to update html_root_url
description = "Email client"
readme = "README.md"
homepage = "http://lettre.at"
repository = "https://github.com/lettre/lettre"
license = "MIT"
authors = ["Alexis Mousset <contact@amousset.me>"]
categories = ["email"]
keywords = ["email", "smtp", "mailer"]
[badges]
travis-ci = { repository = "lettre/lettre" }
appveyor = { repository = "lettre/lettre" }
maintenance = { status = "actively-developed" }
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
is-it-maintained-open-issues = { repository = "lettre/lettre" }
[dependencies]
log = "^0.4"
nom = { version = "^4.0", optional = true }
bufstream = { version = "^0.1", optional = true }
native-tls = { version = "^0.2", optional = true }
base64 = { version = "^0.10", optional = true }
hostname = { version = "^0.1", optional = true }
serde = { version = "^1.0", optional = true }
serde_json = { version = "^1.0", optional = true }
serde_derive = { version = "^1.0", optional = true }
fast_chemail = "^0.9"
r2d2 = { version = "^0.8", optional = true}
[dev-dependencies]
env_logger = "^0.6"
glob = "0.3"
[features]
default = ["file-transport", "smtp-transport", "sendmail-transport"]
unstable = []
serde-impls = ["serde", "serde_derive"]
file-transport = ["serde-impls", "serde_json"]
smtp-transport = ["bufstream", "native-tls", "base64", "nom", "hostname"]
sendmail-transport = []
connection-pool = [ "r2d2" ]
[[example]]
name = "smtp"
required-features = ["smtp-transport"]
[[example]]
name = "smtp_gmail"
required-features = ["smtp-transport"]

1
lettre/LICENSE Symbolic link
View File

@@ -0,0 +1 @@
../LICENSE

1
lettre/README.md Symbolic link
View File

@@ -0,0 +1 @@
../README.md

View File

@@ -0,0 +1,50 @@
#![feature(test)]
extern crate lettre;
extern crate test;
use lettre::smtp::ConnectionReuseParameters;
use lettre::{ClientSecurity, Envelope, SmtpTransport};
use lettre::{EmailAddress, SendableEmail, Transport};
#[bench]
fn bench_simple_send(b: &mut test::Bencher) {
let mut sender = SmtpTransport::builder("127.0.0.1:2525", ClientSecurity::None)
.unwrap()
.build();
b.iter(|| {
let email = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
)
.unwrap(),
"id".to_string(),
"Hello ß☺ example".to_string().into_bytes(),
);
let result = sender.send(email);
assert!(result.is_ok());
});
}
#[bench]
fn bench_reuse_send(b: &mut test::Bencher) {
let mut sender = SmtpTransport::builder("127.0.0.1:2525", ClientSecurity::None)
.unwrap()
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
.build();
b.iter(|| {
let email = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
)
.unwrap(),
"id".to_string(),
"Hello ß☺ example".to_string().into_bytes(),
);
let result = sender.send(email);
assert!(result.is_ok());
});
sender.close()
}

31
lettre/examples/smtp.rs Normal file
View File

@@ -0,0 +1,31 @@
extern crate env_logger;
extern crate lettre;
use lettre::{EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
fn main() {
env_logger::init();
let email = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
)
.unwrap(),
"id".to_string(),
"Hello ß☺ example".to_string().into_bytes(),
);
// Open a local connection on port 25
let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport();
// Send the email
let result = mailer.send(email);
if result.is_ok() {
println!("Email sent");
} else {
println!("Could not send email: {:?}", result);
}
assert!(result.is_ok());
}

View File

@@ -0,0 +1,38 @@
extern crate lettre;
use lettre::smtp::authentication::Credentials;
use lettre::{EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
fn main() {
let email = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("from@gmail.com".to_string()).unwrap()),
vec![EmailAddress::new("to@example.com".to_string()).unwrap()],
)
.unwrap(),
"id".to_string(),
"Hello example".to_string().into_bytes(),
);
let creds = Credentials::new(
"example_username".to_string(),
"example_password".to_string(),
);
// Open a remote connection to gmail
let mut mailer = SmtpClient::new_simple("smtp.gmail.com")
.unwrap()
.credentials(creds)
.transport();
// Send the email
let result = mailer.send(email);
if result.is_ok() {
println!("Email sent");
} else {
println!("Could not send email: {:?}", result);
}
assert!(result.is_ok());
}

35
lettre/src/error.rs Normal file
View File

@@ -0,0 +1,35 @@
use self::Error::*;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
};
/// Error type for email content
#[derive(Debug, Clone, Copy)]
pub enum Error {
/// Missing from in envelope
MissingFrom,
/// Missing to in envelope
MissingTo,
/// Invalid email
InvalidEmailAddress,
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
fmt.write_str(&match *self {
MissingFrom => "missing source address, invalid envelope".to_owned(),
MissingTo => "missing destination address, invalid envelope".to_owned(),
InvalidEmailAddress => "invalid email address".to_owned(),
})
}
}
impl StdError for Error {
fn cause(&self) -> Option<&dyn StdError> {
None
}
}
/// Email result type
pub type EmailResult<T> = Result<T, Error>;

61
lettre/src/file/error.rs Normal file
View File

@@ -0,0 +1,61 @@
//! Error and result type for file transport
use self::Error::*;
use serde_json;
use std::io;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
};
/// An enum of all error kinds.
#[derive(Debug)]
pub enum Error {
/// Internal client error
Client(&'static str),
/// IO error
Io(io::Error),
/// JSON serialization error
JsonSerialization(serde_json::Error),
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
match *self {
Client(err) => fmt.write_str(err),
Io(ref err) => err.fmt(fmt),
JsonSerialization(ref err) => err.fmt(fmt),
}
}
}
impl StdError for Error {
fn cause(&self) -> Option<&dyn StdError> {
match *self {
Io(ref err) => Some(&*err),
JsonSerialization(ref err) => Some(&*err),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::Io(err)
}
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Error {
Error::JsonSerialization(err)
}
}
impl From<&'static str> for Error {
fn from(string: &'static str) -> Error {
Error::Client(string)
}
}
/// SMTP result type
pub type FileResult = Result<(), Error>;

60
lettre/src/file/mod.rs Normal file
View File

@@ -0,0 +1,60 @@
//! The file transport writes the emails to the given directory. The name of the file will be
//! `message_id.txt`.
//! It can be useful for testing purposes, or if you want to keep track of sent messages.
//!
use file::error::FileResult;
use serde_json;
use std::fs::File;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use Envelope;
use SendableEmail;
use Transport;
pub mod error;
/// Writes the content and the envelope information to a file
#[derive(Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct FileTransport {
path: PathBuf,
}
impl FileTransport {
/// Creates a new transport to the given directory
pub fn new<P: AsRef<Path>>(path: P) -> FileTransport {
FileTransport {
path: PathBuf::from(path.as_ref()),
}
}
}
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
struct SerializableEmail {
envelope: Envelope,
message_id: String,
message: Vec<u8>,
}
impl<'a> Transport<'a> for FileTransport {
type Result = FileResult;
fn send(&mut self, email: SendableEmail) -> FileResult {
let message_id = email.message_id().to_string();
let envelope = email.envelope().clone();
let mut file = self.path.clone();
file.push(format!("{}.json", message_id));
let serialized = serde_json::to_string(&SerializableEmail {
envelope,
message_id,
message: email.message_to_string()?.as_bytes().to_vec(),
})?;
File::create(file.as_path())?.write_all(serialized.as_bytes())?;
Ok(())
}
}

213
lettre/src/lib.rs Normal file
View File

@@ -0,0 +1,213 @@
//! Lettre is a mailer written in Rust. It provides a simple email builder and several transports.
//!
//! This mailer contains the available transports for your emails.
//!
#![doc(html_root_url = "https://docs.rs/lettre/0.9.5")]
#![deny(
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unstable_features,
unused_import_braces
)]
#[cfg(feature = "smtp-transport")]
extern crate base64;
#[cfg(feature = "smtp-transport")]
extern crate bufstream;
#[cfg(feature = "smtp-transport")]
extern crate hostname;
#[macro_use]
extern crate log;
#[cfg(feature = "smtp-transport")]
extern crate native_tls;
#[cfg(feature = "smtp-transport")]
#[macro_use]
extern crate nom;
#[cfg(feature = "serde-impls")]
extern crate serde;
#[cfg(feature = "serde-impls")]
#[macro_use]
extern crate serde_derive;
extern crate fast_chemail;
#[cfg(feature = "connection-pool")]
extern crate r2d2;
#[cfg(feature = "file-transport")]
extern crate serde_json;
pub mod error;
#[cfg(feature = "file-transport")]
pub mod file;
#[cfg(feature = "sendmail-transport")]
pub mod sendmail;
#[cfg(feature = "smtp-transport")]
pub mod smtp;
pub mod stub;
use error::EmailResult;
use error::Error;
use fast_chemail::is_valid_email;
#[cfg(feature = "file-transport")]
pub use file::FileTransport;
#[cfg(feature = "sendmail-transport")]
pub use sendmail::SendmailTransport;
#[cfg(feature = "smtp-transport")]
pub use smtp::client::net::ClientTlsParameters;
#[cfg(all(feature = "smtp-transport", feature = "connection-pool"))]
pub use smtp::r2d2::SmtpConnectionManager;
#[cfg(feature = "smtp-transport")]
pub use smtp::{ClientSecurity, SmtpClient, SmtpTransport};
use std::ffi::OsStr;
use std::fmt::{self, Display, Formatter};
use std::io;
use std::io::Cursor;
use std::io::Read;
use std::str::FromStr;
/// Email address
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct EmailAddress(String);
impl EmailAddress {
pub fn new(address: String) -> EmailResult<EmailAddress> {
if !is_valid_email(&address) && !address.ends_with("localhost") {
return Err(Error::InvalidEmailAddress);
}
Ok(EmailAddress(address))
}
}
impl FromStr for EmailAddress {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
EmailAddress::new(s.to_string())
}
}
impl Display for EmailAddress {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for EmailAddress {
fn as_ref(&self) -> &str {
&self.0
}
}
impl AsRef<OsStr> for EmailAddress {
fn as_ref(&self) -> &OsStr {
&self.0.as_ref()
}
}
/// Simple email envelope representation
///
/// We only accept mailboxes, and do not support source routes (as per RFC).
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct Envelope {
/// The envelope recipients' addresses
///
/// This can not be empty.
forward_path: Vec<EmailAddress>,
/// The envelope sender address
reverse_path: Option<EmailAddress>,
}
impl Envelope {
/// Creates a new envelope, which may fail if `to` is empty.
pub fn new(from: Option<EmailAddress>, to: Vec<EmailAddress>) -> EmailResult<Envelope> {
if to.is_empty() {
return Err(Error::MissingTo);
}
Ok(Envelope {
forward_path: to,
reverse_path: from,
})
}
/// Destination addresses of the envelope
pub fn to(&self) -> &[EmailAddress] {
self.forward_path.as_slice()
}
/// Source address of the envelope
pub fn from(&self) -> Option<&EmailAddress> {
self.reverse_path.as_ref()
}
}
pub enum Message {
Reader(Box<dyn Read + Send>),
Bytes(Cursor<Vec<u8>>),
}
impl Read for Message {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match *self {
Message::Reader(ref mut rdr) => rdr.read(buf),
Message::Bytes(ref mut rdr) => rdr.read(buf),
}
}
}
/// Sendable email structure
pub struct SendableEmail {
envelope: Envelope,
message_id: String,
message: Message,
}
impl SendableEmail {
pub fn new(envelope: Envelope, message_id: String, message: Vec<u8>) -> SendableEmail {
SendableEmail {
envelope,
message_id,
message: Message::Bytes(Cursor::new(message)),
}
}
pub fn new_with_reader(
envelope: Envelope,
message_id: String,
message: Box<dyn Read + Send>,
) -> SendableEmail {
SendableEmail {
envelope,
message_id,
message: Message::Reader(message),
}
}
pub fn envelope(&self) -> &Envelope {
&self.envelope
}
pub fn message_id(&self) -> &str {
&self.message_id
}
pub fn message(self) -> Message {
self.message
}
pub fn message_to_string(mut self) -> Result<String, io::Error> {
let mut message_content = String::new();
self.message.read_to_string(&mut message_content)?;
Ok(message_content)
}
}
/// Transport method for emails
pub trait Transport<'a> {
/// Result type for the transport
type Result;
/// Sends the email
fn send(&mut self, email: SendableEmail) -> Self::Result;
}

View File

@@ -0,0 +1,50 @@
//! Error and result type for sendmail transport
use self::Error::*;
use std::io;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
};
/// An enum of all error kinds.
#[derive(Debug)]
pub enum Error {
/// Internal client error
Client(&'static str),
/// IO error
Io(io::Error),
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
match *self {
Client(ref err) => err.fmt(fmt),
Io(ref err) => err.fmt(fmt),
}
}
}
impl StdError for Error {
fn cause(&self) -> Option<&dyn StdError> {
match *self {
Io(ref err) => Some(&*err),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::Io(err)
}
}
impl From<&'static str> for Error {
fn from(string: &'static str) -> Error {
Error::Client(string)
}
}
/// sendmail result type
pub type SendmailResult = Result<(), Error>;

View File

@@ -0,0 +1,79 @@
//! The sendmail transport sends the email using the local sendmail command.
//!
use sendmail::error::SendmailResult;
use std::io::prelude::*;
use std::io::Read;
use std::process::{Command, Stdio};
use SendableEmail;
use Transport;
pub mod error;
/// Sends an email using the `sendmail` command
#[derive(Debug, Default)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct SendmailTransport {
command: String,
}
impl SendmailTransport {
/// Creates a new transport with the default `/usr/sbin/sendmail` command
pub fn new() -> SendmailTransport {
SendmailTransport {
command: "/usr/sbin/sendmail".to_string(),
}
}
/// Creates a new transport to the given sendmail command
pub fn new_with_command<S: Into<String>>(command: S) -> SendmailTransport {
SendmailTransport {
command: command.into(),
}
}
}
impl<'a> Transport<'a> for SendmailTransport {
type Result = SendmailResult;
fn send(&mut self, email: SendableEmail) -> SendmailResult {
let message_id = email.message_id().to_string();
// Spawn the sendmail command
let mut process = Command::new(&self.command)
.arg("-i")
.arg("-f")
.arg(
email
.envelope()
.from()
.map(|x| x.as_ref())
.unwrap_or("\"\""),
)
.arg("--")
.args(email.envelope.to())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
let mut message_content = String::new();
let _ = email.message().read_to_string(&mut message_content);
process
.stdin
.as_mut()
.unwrap()
.write_all(message_content.as_bytes())?;
info!("Wrote {} message to stdin", message_id);
let output = process.wait_with_output()?;
if output.status.success() {
Ok(())
} else {
// TODO display stderr
Err(error::Error::Client("The message could not be sent"))
}
}
}

View File

@@ -1,17 +1,37 @@
//! Provides limited SASL authentication mechanisms
use std::fmt::{self, Debug, Display, Formatter};
use smtp::error::Error;
use std::fmt::{self, Display, Formatter};
use crate::transport::smtp::error::{self, Error};
/// Accepted authentication mechanisms
///
/// Accepted authentication mechanisms on an encrypted connection
/// Trying LOGIN last as it is deprecated.
pub const DEFAULT_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
pub const DEFAULT_ENCRYPTED_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
/// Accepted authentication mechanisms on an unencrypted connection
pub const DEFAULT_UNENCRYPTED_MECHANISMS: &[Mechanism] = &[];
/// Convertable to user credentials
pub trait IntoCredentials {
/// Converts to a `Credentials` struct
fn into_credentials(self) -> Credentials;
}
impl IntoCredentials for Credentials {
fn into_credentials(self) -> Credentials {
self
}
}
impl<S: Into<String>, T: Into<String>> IntoCredentials for (S, T) {
fn into_credentials(self) -> Credentials {
let (username, password) = self;
Credentials::new(username.into(), password.into())
}
}
/// Contains user credentials
#[derive(PartialEq, Eq, Clone, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(PartialEq, Eq, Clone, Hash, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct Credentials {
authentication_identity: String,
secret: String,
@@ -27,51 +47,38 @@ impl Credentials {
}
}
impl<S, T> From<(S, T)> for Credentials
where
S: Into<String>,
T: Into<String>,
{
fn from((username, password): (S, T)) -> Self {
Credentials::new(username.into(), password.into())
}
}
impl Debug for Credentials {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("Credentials").finish()
}
}
/// Represents authentication mechanisms
#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub enum Mechanism {
/// PLAIN authentication mechanism, defined in
/// [RFC 4616](https://tools.ietf.org/html/rfc4616)
/// PLAIN authentication mechanism
/// RFC 4616: https://tools.ietf.org/html/rfc4616
Plain,
/// LOGIN authentication mechanism
/// Obsolete but needed for some providers (like Office 365)
///
/// Defined in [draft-murchison-sasl-login-00](https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt).
/// Obsolete but needed for some providers (like office365)
/// https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt
Login,
/// Non-standard XOAUTH2 mechanism, defined in
/// [xoauth2-protocol](https://developers.google.com/gmail/imap/xoauth2-protocol)
/// Non-standard XOAUTH2 mechanism
/// https://developers.google.com/gmail/imap/xoauth2-protocol
Xoauth2,
}
impl Display for Mechanism {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(match *self {
Mechanism::Plain => "PLAIN",
Mechanism::Login => "LOGIN",
Mechanism::Xoauth2 => "XOAUTH2",
})
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(
f,
"{}",
match *self {
Mechanism::Plain => "PLAIN",
Mechanism::Login => "LOGIN",
Mechanism::Xoauth2 => "XOAUTH2",
}
)
}
}
impl Mechanism {
/// Does the mechanism support initial response?
/// Does the mechanism supports initial response
pub fn supports_initial_response(self) -> bool {
match self {
Mechanism::Plain | Mechanism::Xoauth2 => true,
@@ -88,30 +95,28 @@ impl Mechanism {
) -> Result<String, Error> {
match self {
Mechanism::Plain => match challenge {
Some(_) => Err(error::client("This mechanism does not expect a challenge")),
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
None => Ok(format!(
"\u{0}{}\u{0}{}",
credentials.authentication_identity, credentials.secret
)),
},
Mechanism::Login => {
let decoded_challenge = challenge
.ok_or_else(|| error::client("This mechanism does expect a challenge"))?;
let decoded_challenge =
challenge.ok_or(Error::Client("This mechanism does expect a challenge"))?;
if ["User Name", "Username:", "Username", "User Name\0"]
.contains(&decoded_challenge)
{
return Ok(credentials.authentication_identity.clone());
if vec!["User Name", "Username:", "Username"].contains(&decoded_challenge) {
return Ok(credentials.authentication_identity.to_string());
}
if ["Password", "Password:", "Password\0"].contains(&decoded_challenge) {
return Ok(credentials.secret.clone());
if vec!["Password", "Password:"].contains(&decoded_challenge) {
return Ok(credentials.secret.to_string());
}
Err(error::client("Unrecognized challenge"))
Err(Error::Client("Unrecognized challenge"))
}
Mechanism::Xoauth2 => match challenge {
Some(_) => Err(error::client("This mechanism does not expect a challenge")),
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
None => Ok(format!(
"user={}\x01auth=Bearer {}\x01\x01",
credentials.authentication_identity, credentials.secret
@@ -129,7 +134,7 @@ mod test {
fn test_plain() {
let mechanism = Mechanism::Plain;
let credentials = Credentials::new("username".to_owned(), "password".to_owned());
let credentials = Credentials::new("username".to_string(), "password".to_string());
assert_eq!(
mechanism.response(&credentials, None).unwrap(),
@@ -142,7 +147,7 @@ mod test {
fn test_login() {
let mechanism = Mechanism::Login;
let credentials = Credentials::new("alice".to_owned(), "wonderland".to_owned());
let credentials = Credentials::new("alice".to_string(), "wonderland".to_string());
assert_eq!(
mechanism.response(&credentials, Some("Username")).unwrap(),
@@ -160,8 +165,8 @@ mod test {
let mechanism = Mechanism::Xoauth2;
let credentials = Credentials::new(
"username".to_owned(),
"vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_owned(),
"username".to_string(),
"vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_string(),
);
assert_eq!(
@@ -170,12 +175,4 @@ mod test {
);
assert!(mechanism.response(&credentials, Some("test")).is_err());
}
#[test]
fn test_from_user_pass_for_credentials() {
assert_eq!(
Credentials::new("alice".to_owned(), "wonderland".to_owned()),
Credentials::from(("alice", "wonderland"))
);
}
}

View File

@@ -0,0 +1,120 @@
#![allow(missing_docs)]
// Comes from https://github.com/inre/rust-mq/blob/master/netopt
use std::io::{self, Cursor, Read, Write};
use std::sync::{Arc, Mutex};
pub type MockCursor = Cursor<Vec<u8>>;
#[derive(Clone, Debug)]
pub struct MockStream {
reader: Arc<Mutex<MockCursor>>,
writer: Arc<Mutex<MockCursor>>,
}
impl Default for MockStream {
fn default() -> Self {
Self::new()
}
}
impl MockStream {
pub fn new() -> MockStream {
MockStream {
reader: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
writer: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
}
}
pub fn with_vec(vec: Vec<u8>) -> MockStream {
MockStream {
reader: Arc::new(Mutex::new(MockCursor::new(vec))),
writer: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
}
}
pub fn take_vec(&mut self) -> Vec<u8> {
let mut cursor = self.writer.lock().unwrap();
let vec = cursor.get_ref().to_vec();
cursor.set_position(0);
cursor.get_mut().clear();
vec
}
pub fn next_vec(&mut self, vec: &[u8]) {
let mut cursor = self.reader.lock().unwrap();
cursor.set_position(0);
cursor.get_mut().clear();
cursor.get_mut().extend_from_slice(vec);
}
pub fn swap(&mut self) {
let mut cur_write = self.writer.lock().unwrap();
let mut cur_read = self.reader.lock().unwrap();
let vec_write = cur_write.get_ref().to_vec();
let vec_read = cur_read.get_ref().to_vec();
cur_write.set_position(0);
cur_read.set_position(0);
cur_write.get_mut().clear();
cur_read.get_mut().clear();
// swap cursors
cur_read.get_mut().extend_from_slice(vec_write.as_slice());
cur_write.get_mut().extend_from_slice(vec_read.as_slice());
}
}
impl Write for MockStream {
fn write(&mut self, msg: &[u8]) -> io::Result<usize> {
self.writer.lock().unwrap().write(msg)
}
fn flush(&mut self) -> io::Result<()> {
self.writer.lock().unwrap().flush()
}
}
impl Read for MockStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.reader.lock().unwrap().read(buf)
}
}
#[cfg(test)]
mod test {
use super::MockStream;
use std::io::{Read, Write};
#[test]
fn write_take_test() {
let mut mock = MockStream::new();
// write to mock stream
mock.write(&[1, 2, 3]).unwrap();
assert_eq!(mock.take_vec(), vec![1, 2, 3]);
}
#[test]
fn read_with_vec_test() {
let mut mock = MockStream::with_vec(vec![4, 5]);
let mut vec = Vec::new();
mock.read_to_end(&mut vec).unwrap();
assert_eq!(vec, vec![4, 5]);
}
#[test]
fn clone_test() {
let mut mock = MockStream::new();
let mut cloned = mock.clone();
mock.write(&[6, 7]).unwrap();
assert_eq!(cloned.take_vec(), vec![6, 7]);
}
#[test]
fn swap_test() {
let mut mock = MockStream::new();
let mut vec = Vec::new();
mock.write(&[8, 9, 10]).unwrap();
mock.swap();
mock.read_to_end(&mut vec).unwrap();
assert_eq!(vec, vec![8, 9, 10]);
}
}

View File

@@ -0,0 +1,312 @@
//! SMTP client
use bufstream::BufStream;
use nom::ErrorKind as NomErrorKind;
use smtp::authentication::{Credentials, Mechanism};
use smtp::client::net::{ClientTlsParameters, Connector, NetworkStream, Timeout};
use smtp::commands::*;
use smtp::error::{Error, SmtpResult};
use smtp::response::Response;
use std::fmt::{Debug, Display};
use std::io::{self, BufRead, BufReader, Read, Write};
use std::net::ToSocketAddrs;
use std::string::String;
use std::time::Duration;
pub mod mock;
pub mod net;
/// The codec used for transparency
#[derive(Default, Clone, Copy, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct ClientCodec {
escape_count: u8,
}
impl ClientCodec {
/// Creates a new client codec
pub fn new() -> Self {
ClientCodec::default()
}
}
impl ClientCodec {
/// Adds transparency
/// TODO: replace CR and LF by CRLF
fn encode(&mut self, frame: &[u8], buf: &mut Vec<u8>) -> Result<(), Error> {
match frame.len() {
0 => {
match self.escape_count {
0 => buf.write_all(b"\r\n.\r\n")?,
1 => buf.write_all(b"\n.\r\n")?,
2 => buf.write_all(b".\r\n")?,
_ => unreachable!(),
}
self.escape_count = 0;
Ok(())
}
_ => {
let mut start = 0;
for (idx, byte) in frame.iter().enumerate() {
match self.escape_count {
0 => self.escape_count = if *byte == b'\r' { 1 } else { 0 },
1 => self.escape_count = if *byte == b'\n' { 2 } else { 0 },
2 => self.escape_count = if *byte == b'.' { 3 } else { 0 },
_ => unreachable!(),
}
if self.escape_count == 3 {
self.escape_count = 0;
buf.write_all(&frame[start..idx])?;
buf.write_all(b".")?;
start = idx;
}
}
buf.write_all(&frame[start..])?;
Ok(())
}
}
}
}
/// Returns the string replacing all the CRLF with "\<CRLF\>"
/// Used for debug displays
fn escape_crlf(string: &str) -> String {
string.replace("\r\n", "<CRLF>")
}
/// Structure that implements the SMTP client
#[derive(Debug, Default)]
pub struct InnerClient<S: Write + Read = NetworkStream> {
/// TCP stream between client and server
/// Value is None before connection
stream: Option<BufStream<S>>,
}
macro_rules! return_err (
($err: expr, $client: ident) => ({
return Err(From::from($err))
})
);
#[cfg_attr(feature = "cargo-clippy", allow(clippy::new_without_default_derive))]
impl<S: Write + Read> InnerClient<S> {
/// Creates a new SMTP client
///
/// It does not connects to the server, but only creates the `Client`
pub fn new() -> InnerClient<S> {
InnerClient { stream: None }
}
}
impl<S: Connector + Write + Read + Timeout + Debug> InnerClient<S> {
/// Closes the SMTP transaction if possible
pub fn close(&mut self) {
let _ = self.command(QuitCommand);
self.stream = None;
}
/// Sets the underlying stream
pub fn set_stream(&mut self, stream: S) {
self.stream = Some(BufStream::new(stream));
}
/// Upgrades the underlying connection to SSL/TLS
pub fn upgrade_tls_stream(&mut self, tls_parameters: &ClientTlsParameters) -> io::Result<()> {
match self.stream {
Some(ref mut stream) => stream.get_mut().upgrade_tls(tls_parameters),
None => Ok(()),
}
}
/// Tells if the underlying stream is currently encrypted
pub fn is_encrypted(&self) -> bool {
match self.stream {
Some(ref stream) => stream.get_ref().is_encrypted(),
None => false,
}
}
/// Set timeout
pub fn set_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
match self.stream {
Some(ref mut stream) => {
stream.get_mut().set_read_timeout(duration)?;
stream.get_mut().set_write_timeout(duration)?;
Ok(())
}
None => Ok(()),
}
}
/// Connects to the configured server
pub fn connect<A: ToSocketAddrs>(
&mut self,
addr: &A,
tls_parameters: Option<&ClientTlsParameters>,
) -> SmtpResult {
// Connect should not be called when the client is already connected
if self.stream.is_some() {
return_err!("The connection is already established", self);
}
let mut addresses = addr.to_socket_addrs()?;
let server_addr = match addresses.next() {
Some(addr) => addr,
None => return_err!("Could not resolve hostname", self),
};
debug!("connecting to {}", server_addr);
// Try to connect
self.set_stream(Connector::connect(&server_addr, tls_parameters)?);
self.read_response()
}
/// Checks if the server is connected using the NOOP SMTP command
#[cfg_attr(feature = "cargo-clippy", allow(clippy::wrong_self_convention))]
pub fn is_connected(&mut self) -> bool {
self.stream.is_some() && self.command(NoopCommand).is_ok()
}
/// Sends an AUTH command with the given mechanism, and handles challenge if needed
pub fn auth(&mut self, mechanism: Mechanism, credentials: &Credentials) -> SmtpResult {
// TODO
let mut challenges = 10;
let mut response = self.command(AuthCommand::new(mechanism, credentials.clone(), None)?)?;
while challenges > 0 && response.has_code(334) {
challenges -= 1;
response = self.command(AuthCommand::new_from_response(
mechanism,
credentials.clone(),
&response,
)?)?;
}
if challenges == 0 {
Err(Error::ResponseParsing("Unexpected number of challenges"))
} else {
Ok(response)
}
}
/// Sends the message content
pub fn message(&mut self, message: Box<dyn Read>) -> SmtpResult {
let mut out_buf: Vec<u8> = vec![];
let mut codec = ClientCodec::new();
let mut message_reader = BufReader::new(message);
loop {
out_buf.clear();
let consumed = match message_reader.fill_buf() {
Ok(bytes) => {
codec.encode(bytes, &mut out_buf)?;
bytes.len()
}
Err(ref err) => panic!("Failed with: {}", err),
};
message_reader.consume(consumed);
if consumed == 0 {
break;
}
self.write(out_buf.as_slice())?;
}
self.write(b"\r\n.\r\n")?;
self.read_response()
}
/// Sends an SMTP command
pub fn command<C: Display>(&mut self, command: C) -> SmtpResult {
self.write(command.to_string().as_bytes())?;
self.read_response()
}
/// Writes a string to the server
fn write(&mut self, string: &[u8]) -> Result<(), Error> {
if self.stream.is_none() {
return Err(From::from("Connection closed"));
}
self.stream.as_mut().unwrap().write_all(string)?;
self.stream.as_mut().unwrap().flush()?;
debug!(
"Wrote: {}",
escape_crlf(String::from_utf8_lossy(string).as_ref())
);
Ok(())
}
/// Gets the SMTP response
fn read_response(&mut self) -> SmtpResult {
let mut raw_response = String::new();
let mut response = raw_response.parse::<Response>();
while response.is_err() {
if response.as_ref().err().unwrap() != &NomErrorKind::Complete {
break;
}
// TODO read more than one line
let read_count = self.stream.as_mut().unwrap().read_line(&mut raw_response)?;
// EOF is reached
if read_count == 0 {
break;
}
response = raw_response.parse::<Response>();
}
debug!("Read: {}", escape_crlf(raw_response.as_ref()));
let final_response = response?;
if final_response.is_positive() {
Ok(final_response)
} else {
Err(From::from(final_response))
}
}
}
#[cfg(test)]
mod test {
use super::{escape_crlf, ClientCodec};
#[test]
fn test_codec() {
let mut codec = ClientCodec::new();
let mut buf: Vec<u8> = vec![];
assert!(codec.encode(b"test\r\n", &mut buf).is_ok());
assert!(codec.encode(b".\r\n", &mut buf).is_ok());
assert!(codec.encode(b"\r\ntest", &mut buf).is_ok());
assert!(codec.encode(b"te\r\n.\r\nst", &mut buf).is_ok());
assert!(codec.encode(b"test", &mut buf).is_ok());
assert!(codec.encode(b"test.", &mut buf).is_ok());
assert!(codec.encode(b"test\n", &mut buf).is_ok());
assert!(codec.encode(b".test\n", &mut buf).is_ok());
assert!(codec.encode(b"test", &mut buf).is_ok());
assert_eq!(
String::from_utf8(buf).unwrap(),
"test\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntest"
);
}
#[test]
fn test_escape_crlf() {
assert_eq!(escape_crlf("\r\n"), "<CRLF>");
assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CRLF>");
assert_eq!(
escape_crlf("EHLO my_name\r\nSIZE 42\r\n"),
"EHLO my_name<CRLF>SIZE 42<CRLF>"
);
}
}

View File

@@ -0,0 +1,172 @@
//! A trait to represent a stream
use native_tls::{Protocol, TlsConnector, TlsStream};
use smtp::client::mock::MockStream;
use std::io::{self, ErrorKind, Read, Write};
use std::net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream};
use std::time::Duration;
/// Parameters to use for secure clients
#[derive(Clone)]
#[allow(missing_debug_implementations)]
pub struct ClientTlsParameters {
/// A connector from `native-tls`
pub connector: TlsConnector,
/// The domain to send during the TLS handshake
pub domain: String,
}
impl ClientTlsParameters {
/// Creates a `ClientTlsParameters`
pub fn new(domain: String, connector: TlsConnector) -> ClientTlsParameters {
ClientTlsParameters { connector, domain }
}
}
/// Accepted protocols by default.
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults.
pub const DEFAULT_TLS_PROTOCOLS: &[Protocol] = &[Protocol::Tlsv12];
#[derive(Debug)]
/// Represents the different types of underlying network streams
pub enum NetworkStream {
/// Plain TCP stream
Tcp(TcpStream),
/// Encrypted TCP stream
Tls(TlsStream<TcpStream>),
/// Mock stream
Mock(MockStream),
}
impl NetworkStream {
/// Returns peer's address
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
match *self {
NetworkStream::Tcp(ref s) => s.peer_addr(),
NetworkStream::Tls(ref s) => s.get_ref().peer_addr(),
NetworkStream::Mock(_) => Ok(SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::new(127, 0, 0, 1),
80,
))),
}
}
/// Shutdowns the connection
pub fn shutdown(&self, how: Shutdown) -> io::Result<()> {
match *self {
NetworkStream::Tcp(ref s) => s.shutdown(how),
NetworkStream::Tls(ref s) => s.get_ref().shutdown(how),
NetworkStream::Mock(_) => Ok(()),
}
}
}
impl Read for NetworkStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match *self {
NetworkStream::Tcp(ref mut s) => s.read(buf),
NetworkStream::Tls(ref mut s) => s.read(buf),
NetworkStream::Mock(ref mut s) => s.read(buf),
}
}
}
impl Write for NetworkStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match *self {
NetworkStream::Tcp(ref mut s) => s.write(buf),
NetworkStream::Tls(ref mut s) => s.write(buf),
NetworkStream::Mock(ref mut s) => s.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match *self {
NetworkStream::Tcp(ref mut s) => s.flush(),
NetworkStream::Tls(ref mut s) => s.flush(),
NetworkStream::Mock(ref mut s) => s.flush(),
}
}
}
/// A trait for the concept of opening a stream
pub trait Connector: Sized {
/// Opens a connection to the given IP socket
fn connect(addr: &SocketAddr, tls_parameters: Option<&ClientTlsParameters>)
-> io::Result<Self>;
/// Upgrades to TLS connection
fn upgrade_tls(&mut self, tls_parameters: &ClientTlsParameters) -> io::Result<()>;
/// Is the NetworkStream encrypted
fn is_encrypted(&self) -> bool;
}
impl Connector for NetworkStream {
fn connect(
addr: &SocketAddr,
tls_parameters: Option<&ClientTlsParameters>,
) -> io::Result<NetworkStream> {
let tcp_stream = TcpStream::connect(addr)?;
match tls_parameters {
Some(context) => context
.connector
.connect(context.domain.as_ref(), tcp_stream)
.map(NetworkStream::Tls)
.map_err(|e| io::Error::new(ErrorKind::Other, e)),
None => Ok(NetworkStream::Tcp(tcp_stream)),
}
}
#[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))]
fn upgrade_tls(&mut self, tls_parameters: &ClientTlsParameters) -> io::Result<()> {
*self = match *self {
NetworkStream::Tcp(ref mut stream) => match tls_parameters
.connector
.connect(tls_parameters.domain.as_ref(), stream.try_clone().unwrap())
{
Ok(tls_stream) => NetworkStream::Tls(tls_stream),
Err(err) => return Err(io::Error::new(ErrorKind::Other, err)),
},
NetworkStream::Tls(_) => return Ok(()),
NetworkStream::Mock(_) => return Ok(()),
};
Ok(())
}
#[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))]
fn is_encrypted(&self) -> bool {
match *self {
NetworkStream::Tcp(_) => false,
NetworkStream::Tls(_) => true,
NetworkStream::Mock(_) => false,
}
}
}
/// A trait for read and write timeout support
pub trait Timeout: Sized {
/// Set read timeout for IO calls
fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()>;
/// Set write timeout for IO calls
fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()>;
}
impl Timeout for NetworkStream {
fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
match *self {
NetworkStream::Tcp(ref mut stream) => stream.set_read_timeout(duration),
NetworkStream::Tls(ref mut stream) => stream.get_ref().set_read_timeout(duration),
NetworkStream::Mock(_) => Ok(()),
}
}
/// Set write timeout for IO calls
fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
match *self {
NetworkStream::Tcp(ref mut stream) => stream.set_write_timeout(duration),
NetworkStream::Tls(ref mut stream) => stream.get_ref().set_write_timeout(duration),
NetworkStream::Mock(_) => Ok(()),
}
}
}

378
lettre/src/smtp/commands.rs Normal file
View File

@@ -0,0 +1,378 @@
#![cfg_attr(feature = "cargo-clippy", allow(clippy::write_with_newline))]
//! SMTP commands
use base64;
use smtp::authentication::{Credentials, Mechanism};
use smtp::error::Error;
use smtp::extension::ClientId;
use smtp::extension::{MailParameter, RcptParameter};
use smtp::response::Response;
use std::fmt::{self, Display, Formatter};
use EmailAddress;
/// EHLO command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct EhloCommand {
client_id: ClientId,
}
impl Display for EhloCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "EHLO {}\r\n", self.client_id)
}
}
impl EhloCommand {
/// Creates a EHLO command
pub fn new(client_id: ClientId) -> EhloCommand {
EhloCommand { client_id }
}
}
/// STARTTLS command
#[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct StarttlsCommand;
impl Display for StarttlsCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("STARTTLS\r\n")
}
}
/// MAIL command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct MailCommand {
sender: Option<EmailAddress>,
parameters: Vec<MailParameter>,
}
impl Display for MailCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(
f,
"MAIL FROM:<{}>",
self.sender.as_ref().map(|x| x.as_ref()).unwrap_or("")
)?;
for parameter in &self.parameters {
write!(f, " {}", parameter)?;
}
f.write_str("\r\n")
}
}
impl MailCommand {
/// Creates a MAIL command
pub fn new(sender: Option<EmailAddress>, parameters: Vec<MailParameter>) -> MailCommand {
MailCommand { sender, parameters }
}
}
/// RCPT command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct RcptCommand {
recipient: EmailAddress,
parameters: Vec<RcptParameter>,
}
impl Display for RcptCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "RCPT TO:<{}>", self.recipient)?;
for parameter in &self.parameters {
write!(f, " {}", parameter)?;
}
f.write_str("\r\n")
}
}
impl RcptCommand {
/// Creates an RCPT command
pub fn new(recipient: EmailAddress, parameters: Vec<RcptParameter>) -> RcptCommand {
RcptCommand {
recipient,
parameters,
}
}
}
/// DATA command
#[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct DataCommand;
impl Display for DataCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("DATA\r\n")
}
}
/// QUIT command
#[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct QuitCommand;
impl Display for QuitCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("QUIT\r\n")
}
}
/// NOOP command
#[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct NoopCommand;
impl Display for NoopCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("NOOP\r\n")
}
}
/// HELP command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct HelpCommand {
argument: Option<String>,
}
impl Display for HelpCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("HELP")?;
if self.argument.is_some() {
write!(f, " {}", self.argument.as_ref().unwrap())?;
}
f.write_str("\r\n")
}
}
impl HelpCommand {
/// Creates an HELP command
pub fn new(argument: Option<String>) -> HelpCommand {
HelpCommand { argument }
}
}
/// VRFY command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct VrfyCommand {
argument: String,
}
impl Display for VrfyCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
#[cfg_attr(feature = "cargo-clippy", allow(clippy::write_with_newline))]
write!(f, "VRFY {}\r\n", self.argument)
}
}
impl VrfyCommand {
/// Creates a VRFY command
pub fn new(argument: String) -> VrfyCommand {
VrfyCommand { argument }
}
}
/// EXPN command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct ExpnCommand {
argument: String,
}
impl Display for ExpnCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "EXPN {}\r\n", self.argument)
}
}
impl ExpnCommand {
/// Creates an EXPN command
pub fn new(argument: String) -> ExpnCommand {
ExpnCommand { argument }
}
}
/// RSET command
#[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct RsetCommand;
impl Display for RsetCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("RSET\r\n")
}
}
/// AUTH command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct AuthCommand {
mechanism: Mechanism,
credentials: Credentials,
challenge: Option<String>,
response: Option<String>,
}
impl Display for AuthCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let encoded_response = self
.response
.as_ref()
.map(|r| base64::encode_config(r.as_bytes(), base64::STANDARD));
if self.mechanism.supports_initial_response() {
write!(f, "AUTH {} {}", self.mechanism, encoded_response.unwrap())?;
} else {
match encoded_response {
Some(response) => f.write_str(&response)?,
None => write!(f, "AUTH {}", self.mechanism)?,
}
}
f.write_str("\r\n")
}
}
impl AuthCommand {
/// Creates an AUTH command (from a challenge if provided)
pub fn new(
mechanism: Mechanism,
credentials: Credentials,
challenge: Option<String>,
) -> Result<AuthCommand, Error> {
let response = if mechanism.supports_initial_response() || challenge.is_some() {
Some(mechanism.response(&credentials, challenge.as_deref())?)
} else {
None
};
Ok(AuthCommand {
mechanism,
credentials,
challenge,
response,
})
}
/// Creates an AUTH command from a response that needs to be a
/// valid challenge (with 334 response code)
pub fn new_from_response(
mechanism: Mechanism,
credentials: Credentials,
response: &Response,
) -> Result<AuthCommand, Error> {
if !response.has_code(334) {
return Err(Error::ResponseParsing("Expecting a challenge"));
}
let encoded_challenge = response
.first_word()
.ok_or(Error::ResponseParsing("Could not read auth challenge"))?;
debug!("auth encoded challenge: {}", encoded_challenge);
let decoded_challenge = String::from_utf8(base64::decode(&encoded_challenge)?)?;
debug!("auth decoded challenge: {}", decoded_challenge);
let response = Some(mechanism.response(&credentials, Some(decoded_challenge.as_ref()))?);
Ok(AuthCommand {
mechanism,
credentials,
challenge: Some(decoded_challenge),
response,
})
}
}
#[cfg(test)]
mod test {
use super::*;
use smtp::extension::MailBodyParameter;
#[test]
fn test_display() {
let id = ClientId::Domain("localhost".to_string());
let email = EmailAddress::new("test@example.com".to_string()).unwrap();
let mail_parameter = MailParameter::Other {
keyword: "TEST".to_string(),
value: Some("value".to_string()),
};
let rcpt_parameter = RcptParameter::Other {
keyword: "TEST".to_string(),
value: Some("value".to_string()),
};
assert_eq!(format!("{}", EhloCommand::new(id)), "EHLO localhost\r\n");
assert_eq!(
format!("{}", MailCommand::new(Some(email.clone()), vec![])),
"MAIL FROM:<test@example.com>\r\n"
);
assert_eq!(
format!("{}", MailCommand::new(None, vec![])),
"MAIL FROM:<>\r\n"
);
assert_eq!(
format!(
"{}",
MailCommand::new(Some(email.clone()), vec![MailParameter::Size(42)])
),
"MAIL FROM:<test@example.com> SIZE=42\r\n"
);
assert_eq!(
format!(
"{}",
MailCommand::new(
Some(email.clone()),
vec![
MailParameter::Size(42),
MailParameter::Body(MailBodyParameter::EightBitMime),
mail_parameter,
],
)
),
"MAIL FROM:<test@example.com> SIZE=42 BODY=8BITMIME TEST=value\r\n"
);
assert_eq!(
format!("{}", RcptCommand::new(email.clone(), vec![])),
"RCPT TO:<test@example.com>\r\n"
);
assert_eq!(
format!("{}", RcptCommand::new(email.clone(), vec![rcpt_parameter])),
"RCPT TO:<test@example.com> TEST=value\r\n"
);
assert_eq!(format!("{}", QuitCommand), "QUIT\r\n");
assert_eq!(format!("{}", DataCommand), "DATA\r\n");
assert_eq!(format!("{}", NoopCommand), "NOOP\r\n");
assert_eq!(format!("{}", HelpCommand::new(None)), "HELP\r\n");
assert_eq!(
format!("{}", HelpCommand::new(Some("test".to_string()))),
"HELP test\r\n"
);
assert_eq!(
format!("{}", VrfyCommand::new("test".to_string())),
"VRFY test\r\n"
);
assert_eq!(
format!("{}", ExpnCommand::new("test".to_string())),
"EXPN test\r\n"
);
assert_eq!(format!("{}", RsetCommand), "RSET\r\n");
let credentials = Credentials::new("user".to_string(), "password".to_string());
assert_eq!(
format!(
"{}",
AuthCommand::new(Mechanism::Plain, credentials.clone(), None).unwrap()
),
"AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n"
);
assert_eq!(
format!(
"{}",
AuthCommand::new(Mechanism::Login, credentials.clone(), None).unwrap()
),
"AUTH LOGIN\r\n"
);
}
}

127
lettre/src/smtp/error.rs Normal file
View File

@@ -0,0 +1,127 @@
//! Error and result type for SMTP clients
use self::Error::*;
use base64::DecodeError;
use native_tls;
use nom;
use smtp::response::{Response, Severity};
use std::error::Error as StdError;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::io;
use std::string::FromUtf8Error;
/// An enum of all error kinds.
#[derive(Debug)]
pub enum Error {
/// Transient SMTP error, 4xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
Transient(Response),
/// Permanent SMTP error, 5xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
Permanent(Response),
/// Error parsing a response
ResponseParsing(&'static str),
/// Error parsing a base64 string in response
ChallengeParsing(DecodeError),
/// Error parsing UTF8in response
Utf8Parsing(FromUtf8Error),
/// Internal client error
Client(&'static str),
/// DNS resolution error
Resolution,
/// IO error
Io(io::Error),
/// TLS error
Tls(native_tls::Error),
/// Parsing error
Parsing(nom::ErrorKind),
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
match *self {
// Try to display the first line of the server's response that usually
// contains a short humanly readable error message
Transient(ref err) => fmt.write_str(match err.first_line() {
Some(line) => line,
None => "transient error during SMTP transaction",
}),
Permanent(ref err) => fmt.write_str(match err.first_line() {
Some(line) => line,
None => "permanent error during SMTP transaction",
}),
ResponseParsing(err) => fmt.write_str(err),
ChallengeParsing(ref err) => err.fmt(fmt),
Utf8Parsing(ref err) => err.fmt(fmt),
Resolution => fmt.write_str("could not resolve hostname"),
Client(err) => fmt.write_str(err),
Io(ref err) => err.fmt(fmt),
Tls(ref err) => err.fmt(fmt),
Parsing(ref err) => fmt.write_str(err.description()),
}
}
}
impl StdError for Error {
fn cause(&self) -> Option<&dyn StdError> {
match *self {
ChallengeParsing(ref err) => Some(&*err),
Utf8Parsing(ref err) => Some(&*err),
Io(ref err) => Some(&*err),
Tls(ref err) => Some(&*err),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Io(err)
}
}
impl From<native_tls::Error> for Error {
fn from(err: native_tls::Error) -> Error {
Tls(err)
}
}
impl From<nom::ErrorKind> for Error {
fn from(err: nom::ErrorKind) -> Error {
Parsing(err)
}
}
impl From<DecodeError> for Error {
fn from(err: DecodeError) -> Error {
ChallengeParsing(err)
}
}
impl From<FromUtf8Error> for Error {
fn from(err: FromUtf8Error) -> Error {
Utf8Parsing(err)
}
}
impl From<Response> for Error {
fn from(response: Response) -> Error {
match response.code.severity {
Severity::TransientNegativeCompletion => Transient(response),
Severity::PermanentNegativeCompletion => Permanent(response),
_ => Client("Unknown error code"),
}
}
}
impl From<&'static str> for Error {
fn from(string: &'static str) -> Error {
Client(string)
}
}
/// SMTP result type
pub type SmtpResult = Result<Response, Error>;

View File

@@ -1,22 +1,21 @@
//! ESMTP features
use std::{
collections::HashSet,
fmt::{self, Display, Formatter},
net::{Ipv4Addr, Ipv6Addr},
};
use hostname::get_hostname;
use smtp::authentication::Mechanism;
use smtp::error::Error;
use smtp::response::Response;
use smtp::util::XText;
use std::collections::HashSet;
use std::fmt::{self, Display, Formatter};
use std::net::{Ipv4Addr, Ipv6Addr};
use std::result::Result;
use crate::transport::smtp::{
authentication::Mechanism,
error::{self, Error},
response::Response,
util::XText,
};
/// Default client id
pub const DEFAULT_DOMAIN_CLIENT_ID: &str = "localhost";
/// Client identifier, the parameter to `EHLO`
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub enum ClientId {
/// A fully-qualified domain name
Domain(String),
@@ -26,103 +25,86 @@ pub enum ClientId {
Ipv6(Ipv6Addr),
}
const LOCALHOST_CLIENT: ClientId = ClientId::Ipv4(Ipv4Addr::new(127, 0, 0, 1));
impl Default for ClientId {
fn default() -> Self {
// https://tools.ietf.org/html/rfc5321#section-4.1.4
//
// The SMTP client MUST, if possible, ensure that the domain parameter
// to the EHLO command is a primary host name as specified for this
// command in Section 2.3.5. If this is not possible (e.g., when the
// client's address is dynamically assigned and the client does not have
// an obvious name), an address literal SHOULD be substituted for the
// domain name.
#[cfg(feature = "hostname")]
{
hostname::get()
.ok()
.and_then(|s| s.into_string().map(Self::Domain).ok())
.unwrap_or(LOCALHOST_CLIENT)
}
#[cfg(not(feature = "hostname"))]
LOCALHOST_CLIENT
}
}
impl Display for ClientId {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Domain(value) => f.write_str(value),
Self::Ipv4(value) => write!(f, "[{value}]"),
Self::Ipv6(value) => write!(f, "[IPv6:{value}]"),
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match *self {
ClientId::Domain(ref value) => f.write_str(value),
ClientId::Ipv4(ref value) => write!(f, "{}", value),
ClientId::Ipv6(ref value) => write!(f, "{}", value),
}
}
}
impl ClientId {
#[doc(hidden)]
#[deprecated(since = "0.10.0", note = "Please use ClientId::Domain(domain) instead")]
/// Creates a new `ClientId` from a fully qualified domain name
pub fn new(domain: String) -> Self {
Self::Domain(domain)
pub fn new(domain: String) -> ClientId {
ClientId::Domain(domain)
}
/// Defines a `ClientId` with the current hostname, of `localhost` if hostname could not be
/// found
pub fn hostname() -> ClientId {
ClientId::Domain(get_hostname().unwrap_or_else(|| DEFAULT_DOMAIN_CLIENT_ID.to_string()))
}
}
/// Supported ESMTP keywords
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub enum Extension {
/// 8BITMIME keyword
///
/// Defined in [RFC 6152](https://tools.ietf.org/html/rfc6152)
/// RFC 6152: https://tools.ietf.org/html/rfc6152
EightBitMime,
/// SMTPUTF8 keyword
///
/// Defined in [RFC 6531](https://tools.ietf.org/html/rfc6531)
/// RFC 6531: https://tools.ietf.org/html/rfc6531
SmtpUtfEight,
/// STARTTLS keyword
///
/// Defined in [RFC 2487](https://tools.ietf.org/html/rfc2487)
/// RFC 2487: https://tools.ietf.org/html/rfc2487
StartTls,
/// AUTH mechanism
Authentication(Mechanism),
}
impl Display for Extension {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Extension::EightBitMime => f.write_str("8BITMIME"),
Extension::SmtpUtfEight => f.write_str("SMTPUTF8"),
Extension::StartTls => f.write_str("STARTTLS"),
Extension::Authentication(mechanism) => write!(f, "AUTH {mechanism}"),
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match *self {
Extension::EightBitMime => write!(f, "8BITMIME"),
Extension::SmtpUtfEight => write!(f, "SMTPUTF8"),
Extension::StartTls => write!(f, "STARTTLS"),
Extension::Authentication(ref mechanism) => write!(f, "AUTH {}", mechanism),
}
}
}
/// Contains information about an SMTP server
#[derive(Clone, Debug, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct ServerInfo {
/// Server name
///
/// The name given in the server banner
name: String,
pub name: String,
/// ESMTP features supported by the server
///
/// It contains the features supported by the server and known by the `Extension` module.
features: HashSet<Extension>,
pub features: HashSet<Extension>,
}
impl Display for ServerInfo {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let features = if self.features.is_empty() {
"no supported features".to_owned()
} else {
format!("{:?}", self.features)
};
write!(f, "{} with {}", self.name, features)
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(
f,
"{} with {}",
self.name,
if self.features.is_empty() {
"no supported features".to_string()
} else {
format!("{:?}", self.features)
}
)
}
}
@@ -131,18 +113,18 @@ impl ServerInfo {
pub fn from_response(response: &Response) -> Result<ServerInfo, Error> {
let name = match response.first_word() {
Some(name) => name,
None => return Err(error::response("Could not read server name")),
None => return Err(Error::ResponseParsing("Could not read server name")),
};
let mut features: HashSet<Extension> = HashSet::new();
for line in response.message() {
for line in response.message.as_slice() {
if line.is_empty() {
continue;
}
let mut split = line.split_whitespace();
match split.next().unwrap() {
let split: Vec<&str> = line.split_whitespace().collect();
match split[0] {
"8BITMIME" => {
features.insert(Extension::EightBitMime);
}
@@ -153,7 +135,7 @@ impl ServerInfo {
features.insert(Extension::StartTls);
}
"AUTH" => {
for mechanism in split {
for &mechanism in &split[1..] {
match mechanism {
"PLAIN" => {
features.insert(Extension::Authentication(Mechanism::Plain));
@@ -173,7 +155,7 @@ impl ServerInfo {
}
Ok(ServerInfo {
name: name.to_owned(),
name: name.to_string(),
features,
})
}
@@ -188,26 +170,11 @@ impl ServerInfo {
self.features
.contains(&Extension::Authentication(mechanism))
}
/// Gets a compatible mechanism from a list
pub fn get_auth_mechanism(&self, mechanisms: &[Mechanism]) -> Option<Mechanism> {
for mechanism in mechanisms {
if self.supports_auth_mechanism(*mechanism) {
return Some(*mechanism);
}
}
None
}
/// The name given in the server banner
pub fn name(&self) -> &str {
self.name.as_ref()
}
}
/// A `MAIL FROM` extension parameter
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub enum MailParameter {
/// `BODY` parameter
Body(MailBodyParameter),
@@ -225,17 +192,17 @@ pub enum MailParameter {
}
impl Display for MailParameter {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
MailParameter::Body(value) => write!(f, "BODY={value}"),
MailParameter::Size(size) => write!(f, "SIZE={size}"),
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match *self {
MailParameter::Body(ref value) => write!(f, "BODY={}", value),
MailParameter::Size(size) => write!(f, "SIZE={}", size),
MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"),
MailParameter::Other {
keyword,
value: Some(value),
ref keyword,
value: Some(ref value),
} => write!(f, "{}={}", keyword, XText(value)),
MailParameter::Other {
keyword,
ref keyword,
value: None,
} => f.write_str(keyword),
}
@@ -244,7 +211,7 @@ impl Display for MailParameter {
/// Values for the `BODY` parameter to `MAIL FROM`
#[derive(PartialEq, Eq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub enum MailBodyParameter {
/// `7BIT`
SevenBit,
@@ -253,7 +220,7 @@ pub enum MailBodyParameter {
}
impl Display for MailBodyParameter {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match *self {
MailBodyParameter::SevenBit => f.write_str("7BIT"),
MailBodyParameter::EightBitMime => f.write_str("8BITMIME"),
@@ -263,7 +230,7 @@ impl Display for MailBodyParameter {
/// A `RCPT TO` extension parameter
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub enum RcptParameter {
/// Custom parameter
Other {
@@ -275,14 +242,14 @@ pub enum RcptParameter {
}
impl Display for RcptParameter {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match &self {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match *self {
RcptParameter::Other {
keyword,
value: Some(value),
} => write!(f, "{keyword}={}", XText(value)),
ref keyword,
value: Some(ref value),
} => write!(f, "{}={}", keyword, XText(value)),
RcptParameter::Other {
keyword,
ref keyword,
value: None,
} => f.write_str(keyword),
}
@@ -291,27 +258,29 @@ impl Display for RcptParameter {
#[cfg(test)]
mod test {
use super::*;
use crate::transport::smtp::response::{Category, Code, Detail, Severity};
use super::{ClientId, Extension, ServerInfo};
use smtp::authentication::Mechanism;
use smtp::response::{Category, Code, Detail, Response, Severity};
use std::collections::HashSet;
#[test]
fn test_clientid_fmt() {
assert_eq!(
format!("{}", ClientId::Domain("test".to_owned())),
"test".to_owned()
format!("{}", ClientId::new("test".to_string())),
"test".to_string()
);
assert_eq!(format!("{LOCALHOST_CLIENT}"), "[127.0.0.1]".to_owned());
}
#[test]
fn test_extension_fmt() {
assert_eq!(
format!("{}", Extension::EightBitMime),
"8BITMIME".to_owned()
"8BITMIME".to_string()
);
assert_eq!(
format!("{}", Extension::Authentication(Mechanism::Plain)),
"AUTH PLAIN".to_owned()
"AUTH PLAIN".to_string()
);
}
@@ -324,11 +293,11 @@ mod test {
format!(
"{}",
ServerInfo {
name: "name".to_owned(),
features: eightbitmime,
name: "name".to_string(),
features: eightbitmime.clone(),
}
),
"name with {EightBitMime}".to_owned()
"name with {EightBitMime}".to_string()
);
let empty = HashSet::new();
@@ -337,11 +306,11 @@ mod test {
format!(
"{}",
ServerInfo {
name: "name".to_owned(),
name: "name".to_string(),
features: empty,
}
),
"name with no supported features".to_owned()
"name with no supported features".to_string()
);
let mut plain = HashSet::new();
@@ -351,11 +320,11 @@ mod test {
format!(
"{}",
ServerInfo {
name: "name".to_owned(),
features: plain,
name: "name".to_string(),
features: plain.clone(),
}
),
"name with {Authentication(Plain)}".to_owned()
"name with {Authentication(Plain)}".to_string()
);
}
@@ -367,14 +336,18 @@ mod test {
Category::Unspecified4,
Detail::One,
),
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned()],
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
);
let mut features = HashSet::new();
assert!(features.insert(Extension::EightBitMime));
let server_info = ServerInfo {
name: "me".to_owned(),
name: "me".to_string(),
features,
};
@@ -390,10 +363,10 @@ mod test {
Detail::One,
),
vec![
"me".to_owned(),
"AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_owned(),
"8BITMIME".to_owned(),
"SIZE 42".to_owned(),
"me".to_string(),
"AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
);
@@ -403,7 +376,7 @@ mod test {
assert!(features2.insert(Extension::Authentication(Mechanism::Xoauth2),));
let server_info2 = ServerInfo {
name: "me".to_owned(),
name: "me".to_string(),
features: features2,
};

479
lettre/src/smtp/mod.rs Normal file
View File

@@ -0,0 +1,479 @@
//! The SMTP transport sends emails using the SMTP protocol.
//!
//! This SMTP client follows [RFC
//! 5321](https://tools.ietf.org/html/rfc5321), and is designed to efficiently send emails from an
//! application to a relay email server, as it relies as much as possible on the relay server
//! for sanity and RFC compliance checks.
//!
//! It implements the following extensions:
//!
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and XOAUTH2 mechanisms
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
//!
use native_tls::TlsConnector;
use smtp::authentication::{
Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS, DEFAULT_UNENCRYPTED_MECHANISMS,
};
use smtp::client::net::ClientTlsParameters;
use smtp::client::net::DEFAULT_TLS_PROTOCOLS;
use smtp::client::InnerClient;
use smtp::commands::*;
use smtp::error::{Error, SmtpResult};
use smtp::extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo};
use std::net::{SocketAddr, ToSocketAddrs};
use std::time::Duration;
use {SendableEmail, Transport};
pub mod authentication;
pub mod client;
pub mod commands;
pub mod error;
pub mod extension;
#[cfg(feature = "connection-pool")]
pub mod r2d2;
pub mod response;
pub mod util;
// Registered port numbers:
// https://www.iana.
// org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
/// Default smtp port
pub const SMTP_PORT: u16 = 25;
/// Default submission port
pub const SUBMISSION_PORT: u16 = 587;
/// Default submission over TLS port
pub const SUBMISSIONS_PORT: u16 = 465;
/// How to apply TLS to a client connection
#[derive(Clone)]
#[allow(missing_debug_implementations)]
pub enum ClientSecurity {
/// Insecure connection only (for testing purposes)
None,
/// Start with insecure connection and use `STARTTLS` when available
Opportunistic(ClientTlsParameters),
/// Start with insecure connection and require `STARTTLS`
Required(ClientTlsParameters),
/// Use TLS wrapped connection
Wrapper(ClientTlsParameters),
}
/// Configures connection reuse behavior
#[derive(Clone, Debug, Copy)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub enum ConnectionReuseParameters {
/// Unlimited connection reuse
ReuseUnlimited,
/// Maximum number of connection reuse
ReuseLimited(u16),
/// Disable connection reuse, close connection after each transaction
NoReuse,
}
/// Contains client configuration
#[allow(missing_debug_implementations)]
#[derive(Clone)]
pub struct SmtpClient {
/// Enable connection reuse
connection_reuse: ConnectionReuseParameters,
/// Name sent during EHLO
hello_name: ClientId,
/// Credentials
credentials: Option<Credentials>,
/// Socket we are connecting to
server_addr: SocketAddr,
/// TLS security configuration
security: ClientSecurity,
/// Enable UTF8 mailboxes in envelope or headers
smtp_utf8: bool,
/// Optional enforced authentication mechanism
authentication_mechanism: Option<Mechanism>,
/// Define network timeout
/// It can be changed later for specific needs (like a different timeout for each SMTP command)
timeout: Option<Duration>,
}
/// Builder for the SMTP `SmtpTransport`
impl SmtpClient {
/// Creates a new SMTP client
///
/// Defaults are:
///
/// * No connection reuse
/// * No authentication
/// * No SMTPUTF8 support
/// * A 60 seconds timeout for smtp commands
///
/// Consider using [`SmtpClient::new_simple`] instead, if possible.
pub fn new<A: ToSocketAddrs>(addr: A, security: ClientSecurity) -> Result<SmtpClient, Error> {
let mut addresses = addr.to_socket_addrs()?;
match addresses.next() {
Some(addr) => Ok(SmtpClient {
server_addr: addr,
security,
smtp_utf8: false,
credentials: None,
connection_reuse: ConnectionReuseParameters::NoReuse,
hello_name: ClientId::hostname(),
authentication_mechanism: None,
timeout: Some(Duration::new(60, 0)),
}),
None => Err(Error::Resolution),
}
}
/// Simple and secure transport, should be used when possible.
/// Creates an encrypted transport over submissions port, using the provided domain
/// to validate TLS certificates.
pub fn new_simple(domain: &str) -> Result<SmtpClient, Error> {
let mut tls_builder = TlsConnector::builder();
tls_builder.min_protocol_version(Some(DEFAULT_TLS_PROTOCOLS[0]));
let tls_parameters =
ClientTlsParameters::new(domain.to_string(), tls_builder.build().unwrap());
SmtpClient::new(
(domain, SUBMISSIONS_PORT),
ClientSecurity::Wrapper(tls_parameters),
)
}
/// Creates a new local SMTP client to port 25
pub fn new_unencrypted_localhost() -> Result<SmtpClient, Error> {
SmtpClient::new(("localhost", SMTP_PORT), ClientSecurity::None)
}
/// Enable SMTPUTF8 if the server supports it
pub fn smtp_utf8(mut self, enabled: bool) -> SmtpClient {
self.smtp_utf8 = enabled;
self
}
/// Set the name used during EHLO
pub fn hello_name(mut self, name: ClientId) -> SmtpClient {
self.hello_name = name;
self
}
/// Enable connection reuse
pub fn connection_reuse(mut self, parameters: ConnectionReuseParameters) -> SmtpClient {
self.connection_reuse = parameters;
self
}
/// Set the client credentials
pub fn credentials<S: Into<Credentials>>(mut self, credentials: S) -> SmtpClient {
self.credentials = Some(credentials.into());
self
}
/// Set the authentication mechanism to use
pub fn authentication_mechanism(mut self, mechanism: Mechanism) -> SmtpClient {
self.authentication_mechanism = Some(mechanism);
self
}
/// Set the timeout duration
pub fn timeout(mut self, timeout: Option<Duration>) -> SmtpClient {
self.timeout = timeout;
self
}
/// Build the SMTP client
///
/// It does not connect to the server, but only creates the `SmtpTransport`
pub fn transport(self) -> SmtpTransport {
SmtpTransport::new(self)
}
}
/// Represents the state of a client
#[derive(Debug)]
struct State {
/// Panic state
pub panic: bool,
/// Connection reuse counter
pub connection_reuse_count: u16,
}
/// Structure that implements the high level SMTP client
#[allow(missing_debug_implementations)]
pub struct SmtpTransport {
/// Information about the server
/// Value is None before HELO/EHLO
server_info: Option<ServerInfo>,
/// SmtpTransport variable states
state: State,
/// Information about the client
client_info: SmtpClient,
/// Low level client
client: InnerClient,
}
macro_rules! try_smtp (
($err: expr, $client: ident) => ({
match $err {
Ok(val) => val,
Err(err) => {
if !$client.state.panic {
$client.state.panic = true;
$client.close();
}
return Err(From::from(err))
},
}
})
);
impl<'a> SmtpTransport {
/// Creates a new SMTP client
///
/// It does not connect to the server, but only creates the `SmtpTransport`
pub fn new(builder: SmtpClient) -> SmtpTransport {
let client = InnerClient::new();
SmtpTransport {
client,
server_info: None,
client_info: builder,
state: State {
panic: false,
connection_reuse_count: 0,
},
}
}
fn connect(&mut self) -> Result<(), Error> {
// Check if the connection is still available
if (self.state.connection_reuse_count > 0) && (!self.client.is_connected()) {
self.close();
}
if self.state.connection_reuse_count > 0 {
info!(
"connection already established to {}",
self.client_info.server_addr
);
return Ok(());
}
self.client.connect(
&self.client_info.server_addr,
match self.client_info.security {
ClientSecurity::Wrapper(ref tls_parameters) => Some(tls_parameters),
_ => None,
},
)?;
self.client.set_timeout(self.client_info.timeout)?;
// Log the connection
info!("connection established to {}", self.client_info.server_addr);
self.ehlo()?;
match (
&self.client_info.security.clone(),
self.server_info
.as_ref()
.unwrap()
.supports_feature(Extension::StartTls),
) {
(&ClientSecurity::Required(_), false) => {
return Err(From::from("Could not encrypt connection, aborting"));
}
(&ClientSecurity::Opportunistic(_), false) => (),
(&ClientSecurity::None, _) => (),
(&ClientSecurity::Wrapper(_), _) => (),
(&ClientSecurity::Opportunistic(ref tls_parameters), true)
| (&ClientSecurity::Required(ref tls_parameters), true) => {
try_smtp!(self.client.command(StarttlsCommand), self);
try_smtp!(self.client.upgrade_tls_stream(tls_parameters), self);
debug!("connection encrypted");
// Send EHLO again
self.ehlo()?;
}
}
if self.client_info.credentials.is_some() {
let mut found = false;
// Compute accepted mechanism
let accepted_mechanisms = match self.client_info.authentication_mechanism {
Some(mechanism) => vec![mechanism],
None => {
if self.client.is_encrypted() {
DEFAULT_ENCRYPTED_MECHANISMS.to_vec()
} else {
DEFAULT_UNENCRYPTED_MECHANISMS.to_vec()
}
}
};
for mechanism in accepted_mechanisms {
if self
.server_info
.as_ref()
.unwrap()
.supports_auth_mechanism(mechanism)
{
found = true;
try_smtp!(
self.client
.auth(mechanism, self.client_info.credentials.as_ref().unwrap(),),
self
);
break;
}
}
if !found {
info!("No supported authentication mechanisms available");
}
}
Ok(())
}
/// Gets the EHLO response and updates server information
fn ehlo(&mut self) -> SmtpResult {
// Extended Hello
let ehlo_response = try_smtp!(
self.client.command(EhloCommand::new(ClientId::new(
self.client_info.hello_name.to_string()
),)),
self
);
self.server_info = Some(try_smtp!(ServerInfo::from_response(&ehlo_response), self));
// Print server information
debug!("server {}", self.server_info.as_ref().unwrap());
Ok(ehlo_response)
}
/// Reset the client state
pub fn close(&mut self) {
// Close the SMTP transaction if needed
self.client.close();
// Reset the client state
self.server_info = None;
self.state.panic = false;
self.state.connection_reuse_count = 0;
}
}
impl<'a> Transport<'a> for SmtpTransport {
type Result = SmtpResult;
/// Sends an email
#[cfg_attr(
feature = "cargo-clippy",
allow(clippy::match_same_arms, clippy::cyclomatic_complexity)
)]
fn send(&mut self, email: SendableEmail) -> SmtpResult {
let message_id = email.message_id().to_string();
if !self.client.is_connected() {
self.connect()?;
}
// Mail
let mut mail_options = vec![];
if self
.server_info
.as_ref()
.unwrap()
.supports_feature(Extension::EightBitMime)
{
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
}
if self
.server_info
.as_ref()
.unwrap()
.supports_feature(Extension::SmtpUtfEight)
&& self.client_info.smtp_utf8
{
mail_options.push(MailParameter::SmtpUtfEight);
}
try_smtp!(
self.client.command(MailCommand::new(
email.envelope().from().cloned(),
mail_options,
)),
self
);
// Log the mail command
info!(
"{}: from=<{}>",
message_id,
match email.envelope().from() {
Some(address) => address.to_string(),
None => "".to_string(),
}
);
// Recipient
for to_address in email.envelope().to() {
try_smtp!(
self.client
.command(RcptCommand::new(to_address.clone(), vec![])),
self
);
// Log the rcpt command
info!("{}: to=<{}>", message_id, to_address);
}
// Data
try_smtp!(self.client.command(DataCommand), self);
// Message content
let result = self.client.message(Box::new(email.message()));
if result.is_ok() {
// Increment the connection reuse counter
self.state.connection_reuse_count += 1;
// Log the message
info!(
"{}: conn_use={}, status=sent ({})",
message_id,
self.state.connection_reuse_count,
result
.as_ref()
.ok()
.unwrap()
.message
.iter()
.next()
.unwrap_or(&"no response".to_string())
);
}
// Test if we can reuse the existing connection
match self.client_info.connection_reuse {
ConnectionReuseParameters::ReuseLimited(limit)
if self.state.connection_reuse_count >= limit =>
{
self.close()
}
ConnectionReuseParameters::NoReuse => self.close(),
_ => (),
}
result
}
}

38
lettre/src/smtp/r2d2.rs Normal file
View File

@@ -0,0 +1,38 @@
use r2d2::ManageConnection;
use smtp::error::Error;
use smtp::{ConnectionReuseParameters, SmtpClient, SmtpTransport};
pub struct SmtpConnectionManager {
transport_builder: SmtpClient,
}
impl SmtpConnectionManager {
pub fn new(transport_builder: SmtpClient) -> Result<SmtpConnectionManager, Error> {
Ok(SmtpConnectionManager {
transport_builder: transport_builder
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited),
})
}
}
impl ManageConnection for SmtpConnectionManager {
type Connection = SmtpTransport;
type Error = Error;
fn connect(&self) -> Result<Self::Connection, Error> {
let mut transport = SmtpTransport::new(self.transport_builder.clone());
transport.connect()?;
Ok(transport)
}
fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Error> {
if conn.client.is_connected() {
return Ok(());
}
Err(Error::Client("is not connected anymore"))
}
fn has_broken(&self, conn: &mut Self::Connection) -> bool {
conn.state.panic
}
}

View File

@@ -1,26 +1,14 @@
//! SMTP response, containing a mandatory return code and an optional text
//! message
use std::{
fmt::{Display, Formatter, Result},
result,
str::FromStr,
};
use nom::{crlf, ErrorKind as NomErrorKind};
use std::fmt::{Display, Formatter, Result};
use std::result;
use std::str::{from_utf8, FromStr};
use nom::{
branch::alt,
bytes::streaming::{tag, take_until},
combinator::{complete, map},
multi::many0,
sequence::{preceded, tuple},
IResult,
};
use crate::transport::smtp::{error, Error};
/// The first digit indicates severity
/// First digit indicates severity
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub enum Severity {
/// 2yx
PositiveCompletion = 2,
@@ -33,14 +21,14 @@ pub enum Severity {
}
impl Display for Severity {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}", *self as u8)
}
}
/// Second digit
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub enum Category {
/// x0z
Syntax = 0,
@@ -57,14 +45,14 @@ pub enum Category {
}
impl Display for Category {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}", *self as u8)
}
}
/// The detail digit of a response code (third digit)
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub enum Detail {
#[allow(missing_docs)]
Zero = 0,
@@ -89,14 +77,14 @@ pub enum Detail {
}
impl Display for Detail {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}", *self as u8)
}
}
/// Represents a 3 digit SMTP response code
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct Code {
/// First digit of the response code
pub severity: Severity,
@@ -107,7 +95,7 @@ pub struct Code {
}
impl Display for Code {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}{}{}", self.severity, self.category, self.detail)
}
}
@@ -121,42 +109,29 @@ impl Code {
detail,
}
}
/// Tells if the response is positive
pub fn is_positive(self) -> bool {
matches!(
self.severity,
Severity::PositiveCompletion | Severity::PositiveIntermediate
)
}
}
impl From<Code> for u16 {
fn from(code: Code) -> Self {
code.detail as u16 + 10 * code.category as u16 + 100 * code.severity as u16
}
}
/// Contains an SMTP reply, with separated code and message
///
/// The text message is optional, only the code is mandatory
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct Response {
/// Response code
code: Code,
pub code: Code,
/// Server response string (optional)
/// Handle multiline responses
message: Vec<String>,
pub message: Vec<String>,
}
impl FromStr for Response {
type Err = Error;
type Err = NomErrorKind;
fn from_str(s: &str) -> result::Result<Response, Error> {
parse_response(s)
.map(|(_, r)| r)
.map_err(|e| error::response(e.to_owned()))
fn from_str(s: &str) -> result::Result<Response, NomErrorKind> {
match parse_response(s.as_bytes()) {
Ok((_, res)) => Ok(res),
Err(e) => Err(e.into_error_kind()),
}
}
}
@@ -168,7 +143,10 @@ impl Response {
/// Tells if the response is positive
pub fn is_positive(&self) -> bool {
self.code.is_positive()
match self.code.severity {
Severity::PositiveCompletion | Severity::PositiveIntermediate => true,
_ => false,
}
}
/// Tests code equality
@@ -179,7 +157,7 @@ impl Response {
/// Returns only the first word of the message if possible
pub fn first_word(&self) -> Option<&str> {
self.message
.first()
.get(0)
.and_then(|line| line.split_whitespace().next())
}
@@ -187,103 +165,105 @@ impl Response {
pub fn first_line(&self) -> Option<&str> {
self.message.first().map(String::as_str)
}
/// Response code
pub fn code(&self) -> Code {
self.code
}
/// Server response string (array of lines)
pub fn message(&self) -> impl Iterator<Item = &str> {
self.message.iter().map(String::as_str)
}
}
// Parsers (originally from tokio-smtp)
fn parse_code(i: &str) -> IResult<&str, Code> {
let (i, severity) = parse_severity(i)?;
let (i, category) = parse_category(i)?;
let (i, detail) = parse_detail(i)?;
Ok((
i,
Code {
named!(
parse_code<Code>,
map!(
tuple!(parse_severity, parse_category, parse_detail),
|(severity, category, detail)| Code {
severity,
category,
detail,
},
))
}
}
)
);
fn parse_severity(i: &str) -> IResult<&str, Severity> {
alt((
map(tag("2"), |_| Severity::PositiveCompletion),
map(tag("3"), |_| Severity::PositiveIntermediate),
map(tag("4"), |_| Severity::TransientNegativeCompletion),
map(tag("5"), |_| Severity::PermanentNegativeCompletion),
))(i)
}
named!(
parse_severity<Severity>,
alt!(
tag!("2") => { |_| Severity::PositiveCompletion } |
tag!("3") => { |_| Severity::PositiveIntermediate } |
tag!("4") => { |_| Severity::TransientNegativeCompletion } |
tag!("5") => { |_| Severity::PermanentNegativeCompletion }
)
);
fn parse_category(i: &str) -> IResult<&str, Category> {
alt((
map(tag("0"), |_| Category::Syntax),
map(tag("1"), |_| Category::Information),
map(tag("2"), |_| Category::Connections),
map(tag("3"), |_| Category::Unspecified3),
map(tag("4"), |_| Category::Unspecified4),
map(tag("5"), |_| Category::MailSystem),
))(i)
}
named!(
parse_category<Category>,
alt!(
tag!("0") => { |_| Category::Syntax } |
tag!("1") => { |_| Category::Information } |
tag!("2") => { |_| Category::Connections } |
tag!("3") => { |_| Category::Unspecified3 } |
tag!("4") => { |_| Category::Unspecified4 } |
tag!("5") => { |_| Category::MailSystem }
)
);
fn parse_detail(i: &str) -> IResult<&str, Detail> {
alt((
map(tag("0"), |_| Detail::Zero),
map(tag("1"), |_| Detail::One),
map(tag("2"), |_| Detail::Two),
map(tag("3"), |_| Detail::Three),
map(tag("4"), |_| Detail::Four),
map(tag("5"), |_| Detail::Five),
map(tag("6"), |_| Detail::Six),
map(tag("7"), |_| Detail::Seven),
map(tag("8"), |_| Detail::Eight),
map(tag("9"), |_| Detail::Nine),
))(i)
}
named!(
parse_detail<Detail>,
alt!(
tag!("0") => { |_| Detail::Zero } |
tag!("1") => { |_| Detail::One } |
tag!("2") => { |_| Detail::Two } |
tag!("3") => { |_| Detail::Three } |
tag!("4") => { |_| Detail::Four} |
tag!("5") => { |_| Detail::Five } |
tag!("6") => { |_| Detail::Six} |
tag!("7") => { |_| Detail::Seven } |
tag!("8") => { |_| Detail::Eight } |
tag!("9") => { |_| Detail::Nine }
)
);
pub(crate) fn parse_response(i: &str) -> IResult<&str, Response> {
let (i, lines) = many0(tuple((
parse_code,
preceded(tag("-"), take_until("\r\n")),
tag("\r\n"),
)))(i)?;
let (i, (last_code, last_line)) =
tuple((parse_code, preceded(tag(" "), take_until("\r\n"))))(i)?;
let (i, _) = complete(tag("\r\n"))(i)?;
named!(
parse_response<Response>,
map_res!(
tuple!(
// Parse any number of continuation lines.
many0!(tuple!(
parse_code,
preceded!(char!('-'), take_until_and_consume!(b"\r\n".as_ref()))
)),
// Parse the final line.
tuple!(
parse_code,
terminated!(
opt!(preceded!(char!(' '), take_until!(b"\r\n".as_ref()))),
crlf
)
)
),
|(lines, (last_code, last_line)): (Vec<_>, _)| {
// Check that all codes are equal.
if !lines.iter().all(|&(ref code, _)| *code == last_code) {
return Err(());
}
// Check that all codes are equal.
if !lines.iter().all(|&(code, _, _)| code == last_code) {
return Err(nom::Err::Failure(nom::error::Error::new(
"",
nom::error::ErrorKind::Not,
)));
}
// Extract text from lines, and append last line.
let mut lines = lines.into_iter().map(|(_, text)| text).collect::<Vec<_>>();
if let Some(text) = last_line {
lines.push(text);
}
// Extract text from lines, and append last line.
let mut lines: Vec<String> = lines.into_iter().map(|(_, text, _)| text.into()).collect();
lines.push(last_line.into());
Ok((
i,
Response {
code: last_code,
message: lines,
},
))
}
Ok(Response {
code: last_code,
message: lines
.into_iter()
.map(|line| from_utf8(line).map(|s| s.to_string()))
.collect::<result::Result<Vec<_>, _>>()
.map_err(|_| ())?,
})
}
)
);
#[cfg(test)]
mod test {
use super::*;
use super::{Category, Code, Detail, Response, Severity};
#[test]
fn test_severity_fmt() {
@@ -322,17 +302,6 @@ mod test {
assert_eq!(code.to_string(), "421");
}
#[test]
fn test_code_to_u16() {
let code = Code {
severity: Severity::TransientNegativeCompletion,
category: Category::Connections,
detail: Detail::One,
};
let c: u16 = code.into();
assert_eq!(c, 421);
}
#[test]
fn test_response_from_str() {
let raw_response = "250-me\r\n250-8BITMIME\r\n250-SIZE 42\r\n250 AUTH PLAIN CRAM-MD5\r\n";
@@ -345,10 +314,10 @@ mod test {
detail: Detail::Zero,
},
message: vec![
"me".to_owned(),
"8BITMIME".to_owned(),
"SIZE 42".to_owned(),
"AUTH PLAIN CRAM-MD5".to_owned(),
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
"AUTH PLAIN CRAM-MD5".to_string(),
],
}
);
@@ -368,7 +337,11 @@ mod test {
category: Category::MailSystem,
detail: Detail::Zero,
},
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.is_positive());
assert!(!Response::new(
@@ -377,7 +350,11 @@ mod test {
category: Category::MailSystem,
detail: Detail::Zero,
},
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.is_positive());
}
@@ -390,7 +367,11 @@ mod test {
category: Category::MailSystem,
detail: Detail::One,
},
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.has_code(451));
assert!(!Response::new(
@@ -399,7 +380,11 @@ mod test {
category: Category::MailSystem,
detail: Detail::One,
},
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.has_code(251));
}
@@ -413,7 +398,11 @@ mod test {
category: Category::MailSystem,
detail: Detail::One,
},
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.first_word(),
Some("me")
@@ -426,9 +415,9 @@ mod test {
detail: Detail::One,
},
vec![
"me mo".to_owned(),
"8BITMIME".to_owned(),
"SIZE 42".to_owned(),
"me mo".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.first_word(),
@@ -453,7 +442,7 @@ mod test {
category: Category::MailSystem,
detail: Detail::One,
},
vec![" ".to_owned()],
vec![" ".to_string()],
)
.first_word(),
None
@@ -465,7 +454,7 @@ mod test {
category: Category::MailSystem,
detail: Detail::One,
},
vec![" ".to_owned()],
vec![" ".to_string()],
)
.first_word(),
None
@@ -477,23 +466,13 @@ mod test {
category: Category::MailSystem,
detail: Detail::One,
},
vec!["".to_owned()],
vec!["".to_string()],
)
.first_word(),
None
);
}
#[test]
fn test_response_incomplete() {
let raw_response = "250-smtp.example.org\r\n";
let res = parse_response(raw_response);
match res {
Err(nom::Err::Incomplete(_)) => {}
_ => panic!("Expected incomplete response, got {res:?}"),
}
}
#[test]
fn test_response_first_line() {
assert_eq!(
@@ -503,7 +482,11 @@ mod test {
category: Category::MailSystem,
detail: Detail::One,
},
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.first_line(),
Some("me")
@@ -516,9 +499,9 @@ mod test {
detail: Detail::One,
},
vec![
"me mo".to_owned(),
"8BITMIME".to_owned(),
"SIZE 42".to_owned(),
"me mo".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.first_line(),
@@ -543,7 +526,7 @@ mod test {
category: Category::MailSystem,
detail: Detail::One,
},
vec![" ".to_owned()],
vec![" ".to_string()],
)
.first_line(),
Some(" ")
@@ -555,7 +538,7 @@ mod test {
category: Category::MailSystem,
detail: Detail::One,
},
vec![" ".to_owned()],
vec![" ".to_string()],
)
.first_line(),
Some(" ")
@@ -567,7 +550,7 @@ mod test {
category: Category::MailSystem,
detail: Detail::One,
},
vec!["".to_owned()],
vec!["".to_string()],
)
.first_line(),
Some("")

View File

@@ -4,10 +4,11 @@ use std::fmt::{Display, Formatter, Result as FmtResult};
/// Encode a string as xtext
#[derive(Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct XText<'a>(pub &'a str);
impl<'a> Display for XText<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
let mut rest = self.0;
while let Some(idx) = rest.find(|c| c < '!' || c == '+' || c == '=') {
let (start, end) = rest.split_at(idx);
@@ -33,15 +34,13 @@ mod tests {
#[test]
fn test() {
for (input, expect) in [
for (input, expect) in vec![
("bjorn", "bjorn"),
("bjørn", "bjørn"),
("Ø+= ❤️‰", "Ø+2B+3D+20❤"),
("+", "+2B"),
]
.iter()
{
assert_eq!(format!("{}", XText(input)), (*expect).to_owned());
] {
assert_eq!(format!("{}", XText(input)), expect);
}
}
}

44
lettre/src/stub/mod.rs Normal file
View File

@@ -0,0 +1,44 @@
//! The stub transport only logs message envelope and drops the content. It can be useful for
//! testing purposes.
//!
use SendableEmail;
use Transport;
/// This transport logs the message envelope and returns the given response
#[derive(Debug, Clone, Copy)]
pub struct StubTransport {
response: StubResult,
}
impl StubTransport {
/// Creates a new transport that always returns the given response
pub fn new(response: StubResult) -> StubTransport {
StubTransport { response }
}
/// Creates a new transport that always returns a success response
pub fn new_positive() -> StubTransport {
StubTransport { response: Ok(()) }
}
}
/// SMTP result type
pub type StubResult = Result<(), ()>;
impl<'a> Transport<'a> for StubTransport {
type Result = StubResult;
fn send(&mut self, email: SendableEmail) -> StubResult {
info!(
"{}: from=<{}> to=<{:?}>",
email.message_id(),
match email.envelope().from() {
Some(address) => address.to_string(),
None => "".to_string(),
},
email.envelope().to()
);
self.response
}
}

74
lettre/tests/r2d2_smtp.rs Normal file
View File

@@ -0,0 +1,74 @@
#[cfg(all(test, feature = "smtp-transport", feature = "connection-pool"))]
mod test {
extern crate lettre;
extern crate r2d2;
use self::lettre::{ClientSecurity, EmailAddress, Envelope, SendableEmail, SmtpClient};
use self::lettre::{SmtpConnectionManager, Transport};
use self::r2d2::Pool;
use std::sync::mpsc;
use std::thread;
fn email(message: &str) -> SendableEmail {
SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
)
.unwrap(),
"id".to_string(),
message.to_string().into_bytes(),
)
}
#[test]
fn send_one() {
let client = SmtpClient::new("localhost:2525", ClientSecurity::None).unwrap();
let manager = SmtpConnectionManager::new(client).unwrap();
let pool = Pool::builder().max_size(1).build(manager).unwrap();
let mut mailer = pool.get().unwrap();
let result = (*mailer).send(email("send one"));
assert!(result.is_ok());
}
#[test]
fn send_from_thread() {
let client = SmtpClient::new("127.0.0.1:2525", ClientSecurity::None).unwrap();
let manager = SmtpConnectionManager::new(client).unwrap();
let pool = Pool::builder().max_size(2).build(manager).unwrap();
let (s1, r1) = mpsc::channel();
let (s2, r2) = mpsc::channel();
let pool1 = pool.clone();
let t1 = thread::spawn(move || {
let mut conn = pool1.get().unwrap();
s1.send(()).unwrap();
r2.recv().unwrap();
(*conn)
.send(email("send from thread 1"))
.expect("Send failed from thread 1");
drop(conn);
});
let pool2 = pool.clone();
let t2 = thread::spawn(move || {
let mut conn = pool2.get().unwrap();
s2.send(()).unwrap();
r1.recv().unwrap();
(*conn)
.send(email("send from thread 2"))
.expect("Send failed from thread 2");
drop(conn);
});
t1.join().unwrap();
t2.join().unwrap();
let mut mailer = pool.get().unwrap();
(*mailer)
.send(email("send from main thread"))
.expect("Send failed from main thread");
}
}

65
lettre/tests/skeptic.rs Normal file
View File

@@ -0,0 +1,65 @@
extern crate glob;
use self::glob::glob;
use std::env;
use std::env::consts::EXE_EXTENSION;
use std::path::Path;
use std::process::Command;
/*
#[test]
fn test_readme() {
let readme = Path::new(file!())
.parent()
.unwrap()
.parent()
.unwrap()
.parent()
.unwrap()
.join("README.md");
skeptic_test(&readme);
}
*/
#[test]
fn book_test() {
let mut book_path = env::current_dir().unwrap();
book_path.push(
Path::new(file!())
.parent()
.unwrap()
.parent()
.unwrap()
.parent()
.unwrap()
.join("../website/content/sending-messages"),
); // For some reasons, calling .parent() once more gives us None...
for md in glob(&format!("{}/*.md", book_path.to_str().unwrap())).unwrap() {
skeptic_test(&md.unwrap());
}
}
fn skeptic_test(path: &Path) {
let rustdoc = Path::new("rustdoc").with_extension(EXE_EXTENSION);
let exe = env::current_exe().unwrap();
let depdir = exe.parent().unwrap();
let mut cmd = Command::new(rustdoc);
cmd.args(&["--verbose", "--test"])
.arg("-L")
.arg(&depdir)
.arg(path);
let result = cmd
.spawn()
.expect("Failed to spawn process")
.wait()
.expect("Failed to run process");
assert!(
result.success(),
format!("Failed to run rustdoc tests on {:?}", path)
);
}

View File

@@ -0,0 +1,44 @@
extern crate lettre;
#[cfg(test)]
#[cfg(feature = "file-transport")]
mod test {
use lettre::file::FileTransport;
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
use std::env::temp_dir;
use std::fs::remove_file;
use std::fs::File;
use std::io::Read;
#[test]
fn file_transport() {
let mut sender = FileTransport::new(temp_dir());
let email = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
)
.unwrap(),
"id".to_string(),
"Hello ß☺ example".to_string().into_bytes(),
);
let message_id = email.message_id().to_string();
let result = sender.send(email);
assert!(result.is_ok());
let file = format!("{}/{}.json", temp_dir().to_str().unwrap(), message_id);
let mut f = File::open(file.clone()).unwrap();
let mut buffer = String::new();
let _ = f.read_to_string(&mut buffer);
assert_eq!(
buffer,
"{\"envelope\":{\"forward_path\":[\"root@localhost\"],\"reverse_path\":\"user@localhost\"},\"message_id\":\"id\",\"message\":[72,101,108,108,111,32,195,159,226,152,186,32,101,120,97,109,112,108,101]}"
);
remove_file(file).unwrap();
}
}

View File

@@ -0,0 +1,27 @@
extern crate lettre;
#[cfg(test)]
#[cfg(feature = "sendmail-transport")]
mod test {
use lettre::sendmail::SendmailTransport;
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
#[test]
fn sendmail_transport_simple() {
let mut sender = SendmailTransport::new();
let email = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
)
.unwrap(),
"id".to_string(),
"Hello ß☺ example".to_string().into_bytes(),
);
let result = sender.send(email);
println!("{:?}", result);
assert!(result.is_ok());
}
}

View File

@@ -0,0 +1,27 @@
extern crate lettre;
#[cfg(test)]
#[cfg(feature = "smtp-transport")]
mod test {
use lettre::{ClientSecurity, EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
#[test]
fn smtp_transport_simple() {
let email = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
)
.unwrap(),
"id".to_string(),
"Hello ß☺ example".to_string().into_bytes(),
);
SmtpClient::new("127.0.0.1:2525", ClientSecurity::None)
.unwrap()
.transport()
.send(email)
.unwrap();
}
}

View File

@@ -0,0 +1,31 @@
extern crate lettre;
use lettre::stub::StubTransport;
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
#[test]
fn stub_transport() {
let mut sender_ok = StubTransport::new_positive();
let mut sender_ko = StubTransport::new(Err(()));
let email_ok = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
)
.unwrap(),
"id".to_string(),
"Hello ß☺ example".to_string().into_bytes(),
);
let email_ko = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
)
.unwrap(),
"id".to_string(),
"Hello ß☺ example".to_string().into_bytes(),
);
sender_ok.send(email_ok).unwrap();
sender_ko.send(email_ko).unwrap_err();
}

1
lettre_email/CHANGELOG.md Symbolic link
View File

@@ -0,0 +1 @@
../CHANGELOG.md

31
lettre_email/Cargo.toml Normal file
View File

@@ -0,0 +1,31 @@
[package]
name = "lettre_email"
version = "0.9.4" # remember to update html_root_url
description = "Email builder"
readme = "README.md"
homepage = "http://lettre.at"
repository = "https://github.com/lettre/lettre"
license = "MIT"
authors = ["Alexis Mousset <contact@amousset.me>"]
categories = ["email"]
keywords = ["email", "mailer"]
[badges]
travis-ci = { repository = "lettre/lettre_email" }
appveyor = { repository = "lettre/lettre_email" }
maintenance = { status = "actively-developed" }
is-it-maintained-issue-resolution = { repository = "lettre/lettre_email" }
is-it-maintained-open-issues = { repository = "lettre/lettre_email" }
[dev-dependencies]
lettre = { version = "^0.9", path = "../lettre", features = ["smtp-transport"] }
glob = "0.3"
[dependencies]
email = "^0.0.20"
mime = "^0.3"
time = "^0.1"
uuid = { version = "^0.7", features = ["v4"] }
lettre = { version = "^0.9", path = "../lettre", default-features = false }
base64 = "^0.10"

1
lettre_email/LICENSE Symbolic link
View File

@@ -0,0 +1 @@
../LICENSE

1
lettre_email/README.md Symbolic link
View File

@@ -0,0 +1 @@
../README.md

View File

@@ -0,0 +1,34 @@
extern crate lettre;
extern crate lettre_email;
extern crate mime;
use lettre::{SmtpClient, Transport};
use lettre_email::Email;
use std::path::Path;
fn main() {
let email = Email::builder()
// Addresses can be specified by the tuple (email, alias)
.to(("user@example.org", "Firstname Lastname"))
// ... or by an address only
.from("user@example.com")
.subject("Hi, Hello world")
.text("Hello world.")
.attachment_from_file(Path::new("Cargo.toml"), None, &mime::TEXT_PLAIN)
.unwrap()
.build()
.unwrap();
// Open a local connection on port 25
let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport();
// Send the email
let result = mailer.send(email.into());
if result.is_ok() {
println!("Email sent");
} else {
println!("Could not send email: {:?}", result);
}
assert!(result.is_ok());
}

53
lettre_email/src/error.rs Normal file
View File

@@ -0,0 +1,53 @@
//! Error and result type for emails
use lettre;
use std::io;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
};
use self::Error::*;
/// An enum of all error kinds.
#[derive(Debug)]
pub enum Error {
/// Envelope error
Envelope(lettre::error::Error),
/// Unparseable filename for attachment
CannotParseFilename,
/// IO error
Io(io::Error),
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
fmt.write_str(&match *self {
CannotParseFilename => "Could not parse attachment filename".to_owned(),
Io(ref err) => err.to_string(),
Envelope(ref err) => err.to_string(),
})
}
}
impl StdError for Error {
fn cause(&self) -> Option<&dyn StdError> {
match *self {
Envelope(ref err) => Some(err),
Io(ref err) => Some(err),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::Io(err)
}
}
impl From<lettre::error::Error> for Error {
fn from(err: lettre::error::Error) -> Error {
Error::Envelope(err)
}
}

588
lettre_email/src/lib.rs Normal file
View File

@@ -0,0 +1,588 @@
//! Lettre is a mailer written in Rust. lettre_email provides a simple email builder.
//!
#![doc(html_root_url = "https://docs.rs/lettre_email/0.9.4")]
#![deny(
missing_docs,
missing_debug_implementations,
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unstable_features,
unused_import_braces
)]
extern crate base64;
extern crate email as email_format;
extern crate lettre;
pub extern crate mime;
extern crate time;
extern crate uuid;
pub mod error;
pub use email_format::{Address, Header, Mailbox, MimeMessage, MimeMultipartType};
use error::Error;
use lettre::{error::Error as LettreError, EmailAddress, Envelope, SendableEmail};
use mime::Mime;
use std::fs;
use std::path::Path;
use std::str::FromStr;
use time::{now, Tm};
use uuid::Uuid;
/// Builds a `MimeMessage` structure
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct PartBuilder {
/// Message
message: MimeMessage,
}
impl Default for PartBuilder {
fn default() -> Self {
Self::new()
}
}
/// Represents a message id
pub type MessageId = String;
/// Builds an `Email` structure
#[derive(PartialEq, Eq, Clone, Debug, Default)]
pub struct EmailBuilder {
/// Message
message: PartBuilder,
/// The recipients' addresses for the mail header
to: Vec<Address>,
/// The sender addresses for the mail header
from: Vec<Address>,
/// The Cc addresses for the mail header
cc: Vec<Address>,
/// The Bcc addresses for the mail header
bcc: Vec<Address>,
/// The Reply-To addresses for the mail header
reply_to: Vec<Address>,
/// The In-Reply-To ids for the mail header
in_reply_to: Vec<MessageId>,
/// The References ids for the mail header
references: Vec<MessageId>,
/// The sender address for the mail header
sender: Option<Mailbox>,
/// The envelope
envelope: Option<Envelope>,
/// Date issued
date_issued: bool,
}
/// Simple email representation
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct Email {
/// Message
message: Vec<u8>,
/// Envelope
envelope: Envelope,
/// Message-ID
message_id: Uuid,
}
impl Into<SendableEmail> for Email {
fn into(self) -> SendableEmail {
SendableEmail::new(
self.envelope.clone(),
self.message_id.to_string(),
self.message,
)
}
}
impl Email {
/// TODO
pub fn builder() -> EmailBuilder {
EmailBuilder::new()
}
}
impl PartBuilder {
/// Creates a new empty part
pub fn new() -> PartBuilder {
PartBuilder {
message: MimeMessage::new_blank_message(),
}
}
/// Adds a generic header
pub fn header<A: Into<Header>>(mut self, header: A) -> PartBuilder {
self.message.headers.insert(header.into());
self
}
/// Sets the body
pub fn body<S: Into<String>>(mut self, body: S) -> PartBuilder {
self.message.body = body.into();
self
}
/// Defines a `MimeMultipartType` value
pub fn message_type(mut self, mime_type: MimeMultipartType) -> PartBuilder {
self.message.message_type = Some(mime_type);
self
}
/// Adds a `ContentType` header with the given MIME type
pub fn content_type(self, content_type: &Mime) -> PartBuilder {
self.header(("Content-Type", content_type.to_string()))
}
/// Adds a child part
pub fn child(mut self, child: MimeMessage) -> PartBuilder {
self.message.children.push(child);
self
}
/// Gets built `MimeMessage`
pub fn build(mut self) -> MimeMessage {
self.message.update_headers();
self.message
}
}
impl EmailBuilder {
/// Creates a new empty email
pub fn new() -> EmailBuilder {
EmailBuilder {
message: PartBuilder::new(),
to: vec![],
from: vec![],
cc: vec![],
bcc: vec![],
reply_to: vec![],
in_reply_to: vec![],
references: vec![],
sender: None,
envelope: None,
date_issued: false,
}
}
/// Sets the email body
pub fn body<S: Into<String>>(mut self, body: S) -> EmailBuilder {
self.message = self.message.body(body);
self
}
/// Add a generic header
pub fn header<A: Into<Header>>(mut self, header: A) -> EmailBuilder {
self.message = self.message.header(header);
self
}
/// Adds a `From` header and stores the sender address
pub fn from<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
let mailbox = address.into();
self.from.push(Address::Mailbox(mailbox));
self
}
/// Adds a `To` header and stores the recipient address
pub fn to<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
let mailbox = address.into();
self.to.push(Address::Mailbox(mailbox));
self
}
/// Adds a `Cc` header and stores the recipient address
pub fn cc<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
let mailbox = address.into();
self.cc.push(Address::Mailbox(mailbox));
self
}
/// Adds a `Bcc` header and stores the recipient address
pub fn bcc<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
let mailbox = address.into();
self.bcc.push(Address::Mailbox(mailbox));
self
}
/// Adds a `Reply-To` header
pub fn reply_to<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
let mailbox = address.into();
self.reply_to.push(Address::Mailbox(mailbox));
self
}
/// Adds a `In-Reply-To` header
pub fn in_reply_to(mut self, message_id: MessageId) -> EmailBuilder {
self.in_reply_to.push(message_id);
self
}
/// Adds a `References` header
pub fn references(mut self, message_id: MessageId) -> EmailBuilder {
self.references.push(message_id);
self
}
/// Adds a `Sender` header
pub fn sender<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
let mailbox = address.into();
self.sender = Some(mailbox);
self
}
/// Adds a `Subject` header
pub fn subject<S: Into<String>>(mut self, subject: S) -> EmailBuilder {
self.message = self.message.header(("Subject".to_string(), subject.into()));
self
}
/// Adds a `Date` header with the given date
pub fn date(mut self, date: &Tm) -> EmailBuilder {
self.message = self.message.header(("Date", Tm::rfc822z(date).to_string()));
self.date_issued = true;
self
}
/// Adds an attachment to the email from a file
///
/// If not specified, the filename will be extracted from the file path.
pub fn attachment_from_file(
self,
path: &Path,
filename: Option<&str>,
content_type: &Mime,
) -> Result<EmailBuilder, Error> {
self.attachment(
fs::read(path)?.as_slice(),
filename.unwrap_or(
path.file_name()
.and_then(|x| x.to_str())
.ok_or(Error::CannotParseFilename)?,
),
content_type,
)
}
/// Adds an attachment to the email from a vector of bytes.
pub fn attachment(
self,
body: &[u8],
filename: &str,
content_type: &Mime,
) -> Result<EmailBuilder, Error> {
let encoded_body = base64::encode(&body)
.as_bytes()
.chunks(72)
// base64 encoding is guaranteed to return utf-8, so this won't panic
.map(|s| std::str::from_utf8(s).unwrap())
.collect::<Vec<_>>()
.join("\r\n");
let content = PartBuilder::new()
.body(encoded_body)
.header((
"Content-Disposition",
format!("attachment; filename=\"{}\"", filename),
))
.header(("Content-Type", content_type.to_string()))
.header(("Content-Transfer-Encoding", "base64"))
.build();
Ok(self.message_type(MimeMultipartType::Mixed).child(content))
}
/// Set the message type
pub fn message_type(mut self, message_type: MimeMultipartType) -> EmailBuilder {
self.message = self.message.message_type(message_type);
self
}
/// Adds a child
pub fn child(mut self, child: MimeMessage) -> EmailBuilder {
self.message = self.message.child(child);
self
}
/// Sets the email body to plain text content
pub fn text<S: Into<String>>(self, body: S) -> EmailBuilder {
let text = PartBuilder::new()
.body(body)
.header(("Content-Type", mime::TEXT_PLAIN_UTF_8.to_string()))
.build();
self.child(text)
}
/// Sets the email body to HTML content
pub fn html<S: Into<String>>(self, body: S) -> EmailBuilder {
let html = PartBuilder::new()
.body(body)
.header(("Content-Type", mime::TEXT_HTML_UTF_8.to_string()))
.build();
self.child(html)
}
/// Sets the email content
pub fn alternative<S: Into<String>, T: Into<String>>(
self,
body_html: S,
body_text: T,
) -> EmailBuilder {
let text = PartBuilder::new()
.body(body_text)
.header(("Content-Type", mime::TEXT_PLAIN_UTF_8.to_string()))
.build();
let html = PartBuilder::new()
.body(body_html)
.header(("Content-Type", mime::TEXT_HTML_UTF_8.to_string()))
.build();
let alternate = PartBuilder::new()
.message_type(MimeMultipartType::Alternative)
.child(text)
.child(html);
self.message_type(MimeMultipartType::Mixed)
.child(alternate.build())
}
/// Sets the envelope for manual destination control
/// If this function is not called, the envelope will be calculated
/// from the "to" and "cc" addresses you set.
pub fn envelope(mut self, envelope: Envelope) -> EmailBuilder {
self.envelope = Some(envelope);
self
}
/// Builds the Email
pub fn build(mut self) -> Result<Email, Error> {
// If there are multiple addresses in "From", the "Sender" is required.
if self.from.len() >= 2 && self.sender.is_none() {
// So, we must find something to put as Sender.
for possible_sender in &self.from {
// Only a mailbox can be used as sender, not Address::Group.
if let Address::Mailbox(ref mbx) = *possible_sender {
self.sender = Some(mbx.clone());
break;
}
}
// Address::Group is not yet supported, so the line below will never panic.
// If groups are supported one day, add another Error for this case
// and return it here, if sender_header is still None at this point.
assert!(self.sender.is_some());
}
// Add the sender header, if any.
if let Some(ref v) = self.sender {
self.message = self.message.header(("Sender", v.to_string()));
}
// Calculate the envelope
let envelope = match self.envelope {
Some(e) => e,
None => {
// we need to generate the envelope
let mut to = vec![];
// add all receivers in to_header and cc_header
for receiver in self.to.iter().chain(self.cc.iter()).chain(self.bcc.iter()) {
match *receiver {
Address::Mailbox(ref m) => to.push(EmailAddress::from_str(&m.address)?),
Address::Group(_, ref ms) => {
for m in ms.iter() {
to.push(EmailAddress::from_str(&m.address.clone())?);
}
}
}
}
let from = Some(EmailAddress::from_str(&match self.sender {
Some(x) => Ok(x.address), // if we have a sender_header, use it
None => {
// use a from header
debug_assert!(self.from.len() <= 1); // else we'd have sender_header
match self.from.first() {
Some(a) => match *a {
// if we have a from header
Address::Mailbox(ref mailbox) => Ok(mailbox.address.clone()), // use it
Address::Group(_, ref mailbox_list) => match mailbox_list.first() {
// if it's an author group, use the first author
Some(mailbox) => Ok(mailbox.address.clone()),
// for an empty author group (the rarest of the rare cases)
None => Err(Error::Envelope(LettreError::MissingFrom)), // empty envelope sender
},
},
// if we don't have a from header
None => Err(Error::Envelope(LettreError::MissingFrom)), // empty envelope sender
}
}
}?)?);
Envelope::new(from, to)?
}
};
// Add the collected addresses as mailbox-list all at once.
// The unwraps are fine because the conversions for Vec<Address> never errs.
if !self.to.is_empty() {
self.message = self
.message
.header(Header::new_with_value("To".into(), self.to).unwrap());
}
if !self.from.is_empty() {
self.message = self
.message
.header(Header::new_with_value("From".into(), self.from).unwrap());
} else if let Some(from) = envelope.from() {
let from = vec![Address::new_mailbox(from.to_string())];
self.message = self
.message
.header(Header::new_with_value("From".into(), from).unwrap());
} else {
return Err(Error::Envelope(LettreError::MissingFrom));
}
if !self.cc.is_empty() {
self.message = self
.message
.header(Header::new_with_value("Cc".into(), self.cc).unwrap());
}
if !self.reply_to.is_empty() {
self.message = self
.message
.header(Header::new_with_value("Reply-To".into(), self.reply_to).unwrap());
}
if !self.in_reply_to.is_empty() {
self.message = self.message.header(
Header::new_with_value("In-Reply-To".into(), self.in_reply_to.join(" ")).unwrap(),
);
}
if !self.references.is_empty() {
self.message = self.message.header(
Header::new_with_value("References".into(), self.references.join(" ")).unwrap(),
);
}
if !self.date_issued {
self.message = self
.message
.header(("Date", Tm::rfc822z(&now()).to_string()));
}
self.message = self.message.header(("MIME-Version", "1.0"));
let message_id = Uuid::new_v4();
if let Ok(header) = Header::new_with_value(
"Message-ID".to_string(),
format!("<{}.lettre@localhost>", message_id),
) {
self.message = self.message.header(header)
}
Ok(Email {
message: self.message.build().as_string().into_bytes(),
envelope,
message_id,
})
}
}
#[cfg(test)]
mod test {
use super::{EmailBuilder, SendableEmail};
use lettre::EmailAddress;
use time::now;
#[test]
fn test_multiple_from() {
let email_builder = EmailBuilder::new();
let date_now = now();
let email: SendableEmail = email_builder
.to("anna@example.com")
.from("dieter@example.com")
.from("joachim@example.com")
.date(&date_now)
.subject("Invitation")
.body("We invite you!")
.build()
.unwrap()
.into();
let id = email.message_id().to_string();
assert_eq!(
email.message_to_string().unwrap(),
format!(
"Date: {}\r\nSubject: Invitation\r\nSender: \
<dieter@example.com>\r\nTo: <anna@example.com>\r\nFrom: \
<dieter@example.com>, <joachim@example.com>\r\nMIME-Version: \
1.0\r\nMessage-ID: <{}.lettre@localhost>\r\n\r\nWe invite you!\r\n",
date_now.rfc822z(),
id
)
);
}
#[test]
fn test_email_builder() {
let email_builder = EmailBuilder::new();
let date_now = now();
let email: SendableEmail = email_builder
.to("user@localhost")
.from("user@localhost")
.cc(("cc@localhost", "Alias"))
.bcc("bcc@localhost")
.reply_to("reply@localhost")
.in_reply_to("original".to_string())
.sender("sender@localhost")
.body("Hello World!")
.date(&date_now)
.subject("Hello")
.header(("X-test", "value"))
.build()
.unwrap()
.into();
let id = email.message_id().to_string();
assert_eq!(
email.message_to_string().unwrap(),
format!(
"Date: {}\r\nSubject: Hello\r\nX-test: value\r\nSender: \
<sender@localhost>\r\nTo: <user@localhost>\r\nFrom: \
<user@localhost>\r\nCc: \"Alias\" <cc@localhost>\r\n\
Reply-To: <reply@localhost>\r\nIn-Reply-To: original\r\n\
MIME-Version: 1.0\r\nMessage-ID: \
<{}.lettre@localhost>\r\n\r\nHello World!\r\n",
date_now.rfc822z(),
id
)
);
}
#[test]
fn test_email_sendable() {
let email_builder = EmailBuilder::new();
let date_now = now();
let email: SendableEmail = email_builder
.to("user@localhost")
.from("user@localhost")
.cc(("cc@localhost", "Alias"))
.bcc("bcc@localhost")
.reply_to("reply@localhost")
.sender("sender@localhost")
.body("Hello World!")
.date(&date_now)
.subject("Hello")
.header(("X-test", "value"))
.build()
.unwrap()
.into();
assert_eq!(
email.envelope().from().unwrap().to_string(),
"sender@localhost".to_string()
);
assert_eq!(
email.envelope().to(),
vec![
EmailAddress::new("user@localhost".to_string()).unwrap(),
EmailAddress::new("cc@localhost".to_string()).unwrap(),
EmailAddress::new("bcc@localhost".to_string()).unwrap(),
]
.as_slice()
);
}
}

View File

@@ -0,0 +1,34 @@
extern crate lettre;
extern crate lettre_email;
use lettre::{EmailAddress, Envelope};
use lettre_email::EmailBuilder;
#[test]
fn build_with_envelope_test() {
let e = Envelope::new(
Some(EmailAddress::new("from@example.org".to_string()).unwrap()),
vec![EmailAddress::new("to@example.org".to_string()).unwrap()],
)
.unwrap();
let _email = EmailBuilder::new()
.envelope(e)
.subject("subject")
.text("message")
.build()
.unwrap();
}
#[test]
fn build_with_envelope_without_from_test() {
let e = Envelope::new(
None,
vec![EmailAddress::new("to@example.org".to_string()).unwrap()],
)
.unwrap();
let _email = EmailBuilder::new()
.envelope(e)
.subject("subject")
.text("message")
.build()
.unwrap_err();
}

View File

@@ -0,0 +1,49 @@
extern crate glob;
use self::glob::glob;
use std::env;
use std::env::consts::EXE_EXTENSION;
use std::path::Path;
use std::process::Command;
#[test]
fn book_test() {
let mut book_path = env::current_dir().unwrap();
book_path.push(
Path::new(file!())
.parent()
.unwrap()
.parent()
.unwrap()
.parent()
.unwrap()
.join("../website/content/creating-messages"),
); // For some reasons, calling .parent() once more gives us None...
for md in glob(&format!("{}/*.md", book_path.to_str().unwrap())).unwrap() {
skeptic_test(&md.unwrap());
}
}
fn skeptic_test(path: &Path) {
let rustdoc = Path::new("rustdoc").with_extension(EXE_EXTENSION);
let exe = env::current_exe().unwrap();
let depdir = exe.parent().unwrap();
let mut cmd = Command::new(rustdoc);
cmd.args(&["--verbose", "--test"])
.arg("-L")
.arg(&depdir)
.arg(path);
let result = cmd
.spawn()
.expect("Failed to spawn process")
.wait()
.expect("Failed to run process");
assert!(
result.success(),
format!("Failed to run rustdoc tests on {:?}!", path)
);
}

View File

@@ -1,3 +0,0 @@
format_code_in_doc_comments = true
imports_granularity = "Crate"
group_imports = "StdExternalCrate"

View File

@@ -1,202 +0,0 @@
use super::Address;
#[cfg(feature = "builder")]
use crate::message::header::{self, Headers};
#[cfg(feature = "builder")]
use crate::message::{Mailbox, Mailboxes};
use crate::Error;
/// Simple email envelope representation
///
/// We only accept mailboxes, and do not support source routes (as per RFC).
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Envelope {
/// The envelope recipient's addresses
///
/// This can not be empty.
#[cfg_attr(
feature = "serde",
serde(deserialize_with = "serde_forward_path::deserialize")
)]
forward_path: Vec<Address>,
/// The envelope sender address
reverse_path: Option<Address>,
}
/// just like the default implementation to deserialize `Vec<Address>` but it
/// forbids **de**serializing empty lists
#[cfg(feature = "serde")]
mod serde_forward_path {
use super::Address;
/// dummy type required for serde
/// see example: https://serde.rs/deserialize-map.html
struct CustomVisitor;
impl<'de> serde::de::Visitor<'de> for CustomVisitor {
type Value = Vec<Address>;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("a non-empty list of recipient addresses")
}
fn visit_seq<S>(self, mut access: S) -> Result<Self::Value, S::Error>
where
S: serde::de::SeqAccess<'de>,
{
let mut seq: Vec<Address> = Vec::with_capacity(access.size_hint().unwrap_or(0));
while let Some(key) = access.next_element()? {
seq.push(key);
}
if seq.is_empty() {
Err(serde::de::Error::invalid_length(seq.len(), &self))
} else {
Ok(seq)
}
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Address>, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_seq(CustomVisitor {})
}
#[cfg(test)]
mod tests {
#[test]
fn deserializing_empty_recipient_list_returns_error() {
assert!(
serde_json::from_str::<crate::address::Envelope>(r#"{"forward_path": []}"#)
.is_err()
);
}
#[test]
fn deserializing_non_empty_recipient_list_is_ok() {
serde_json::from_str::<crate::address::Envelope>(
r#"{ "forward_path": [ {"user":"foo", "domain":"example.com"} ] }"#,
)
.unwrap();
}
}
}
impl Envelope {
/// Creates a new envelope, which may fail if `to` is empty.
///
/// # Examples
///
/// ```rust
/// # use lettre::address::{Address, Envelope};
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let sender = "sender@email.com".parse::<Address>()?;
/// let recipients = vec!["to@email.com".parse::<Address>()?];
///
/// let envelope = Envelope::new(Some(sender), recipients);
/// # Ok(())
/// # }
/// ```
///
/// # Errors
///
/// If `to` has no elements in it.
pub fn new(from: Option<Address>, to: Vec<Address>) -> Result<Envelope, Error> {
if to.is_empty() {
return Err(Error::MissingTo);
}
Ok(Envelope {
forward_path: to,
reverse_path: from,
})
}
/// Gets the destination addresses of the envelope.
///
/// # Examples
///
/// ```rust
/// # use lettre::address::{Address, Envelope};
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let sender = "from@email.com".parse::<Address>()?;
/// let recipients = vec!["to@email.com".parse::<Address>()?];
///
/// let envelope = Envelope::new(Some(sender), recipients.clone())?;
/// assert_eq!(envelope.to(), recipients.as_slice());
/// # Ok(())
/// # }
/// ```
pub fn to(&self) -> &[Address] {
self.forward_path.as_slice()
}
/// Gets the sender of the envelope.
///
/// # Examples
///
/// ```rust
/// # use lettre::address::{Address, Envelope};
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let sender = "from@email.com".parse::<Address>()?;
/// let recipients = vec!["to@email.com".parse::<Address>()?];
///
/// let envelope = Envelope::new(Some(sender), recipients.clone())?;
/// assert!(envelope.from().is_some());
///
/// let senderless = Envelope::new(None, recipients.clone())?;
/// assert!(senderless.from().is_none());
/// # Ok(())
/// # }
/// ```
pub fn from(&self) -> Option<&Address> {
self.reverse_path.as_ref()
}
#[cfg(feature = "smtp-transport")]
/// Check if any of the addresses in the envelope contains non-ascii chars
pub(crate) fn has_non_ascii_addresses(&self) -> bool {
self.reverse_path
.iter()
.chain(self.forward_path.iter())
.any(|a| !a.is_ascii())
}
}
#[cfg(feature = "builder")]
impl TryFrom<&Headers> for Envelope {
type Error = Error;
fn try_from(headers: &Headers) -> Result<Self, Self::Error> {
let from = match headers.get::<header::Sender>() {
// If there is a Sender, use it
Some(sender) => Some(Mailbox::from(sender).email),
// ... else try From
None => match headers.get::<header::From>() {
Some(header::From(a)) => {
let mut from: Vec<Mailbox> = a.into();
if from.len() > 1 {
return Err(Error::TooManyFrom);
}
let from = from.pop().expect("From header has 1 Mailbox");
Some(from.email)
}
None => None,
},
};
fn add_addresses_from_mailboxes(
addresses: &mut Vec<Address>,
mailboxes: Option<Mailboxes>,
) {
if let Some(mailboxes) = mailboxes {
addresses.extend(mailboxes.into_iter().map(|mb| mb.email));
}
}
let mut to = vec![];
add_addresses_from_mailboxes(&mut to, headers.get::<header::To>().map(|h| h.0));
add_addresses_from_mailboxes(&mut to, headers.get::<header::Cc>().map(|h| h.0));
add_addresses_from_mailboxes(&mut to, headers.get::<header::Bcc>().map(|h| h.0));
Self::new(from, to)
}
}

View File

@@ -1,12 +0,0 @@
//! Email addresses
#[cfg(feature = "serde")]
mod serde;
mod envelope;
mod types;
pub use self::{
envelope::Envelope,
types::{Address, AddressError},
};

View File

@@ -1,112 +0,0 @@
use std::fmt::{Formatter, Result as FmtResult};
use serde::{
de::{Deserializer, Error as DeError, MapAccess, Visitor},
ser::Serializer,
Deserialize, Serialize,
};
use super::Address;
impl Serialize for Address {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_ref())
}
}
impl<'de> Deserialize<'de> for Address {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
enum Field {
User,
Domain,
}
const FIELDS: &[&str] = &["user", "domain"];
impl<'de> Deserialize<'de> for Field {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct FieldVisitor;
impl<'de> Visitor<'de> for FieldVisitor {
type Value = Field;
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
formatter.write_str("'user' or 'domain'")
}
fn visit_str<E>(self, value: &str) -> Result<Field, E>
where
E: DeError,
{
match value {
"user" => Ok(Field::User),
"domain" => Ok(Field::Domain),
_ => Err(DeError::unknown_field(value, FIELDS)),
}
}
}
deserializer.deserialize_identifier(FieldVisitor)
}
}
struct AddressVisitor;
impl<'de> Visitor<'de> for AddressVisitor {
type Value = Address;
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
formatter.write_str("email address string or object")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: DeError,
{
s.parse().map_err(DeError::custom)
}
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
where
V: MapAccess<'de>,
{
let mut user = None;
let mut domain = None;
while let Some(key) = map.next_key()? {
match key {
Field::User => {
if user.is_some() {
return Err(DeError::duplicate_field("user"));
}
let val = map.next_value()?;
Address::check_user(val).map_err(DeError::custom)?;
user = Some(val);
}
Field::Domain => {
if domain.is_some() {
return Err(DeError::duplicate_field("domain"));
}
let val = map.next_value()?;
Address::check_domain(val).map_err(DeError::custom)?;
domain = Some(val);
}
}
}
let user: &str = user.ok_or_else(|| DeError::missing_field("user"))?;
let domain: &str = domain.ok_or_else(|| DeError::missing_field("domain"))?;
Ok(Address::new(user, domain).unwrap())
}
}
deserializer.deserialize_any(AddressVisitor)
}
}

View File

@@ -1,306 +0,0 @@
//! Representation of an email address
use std::{
error::Error,
ffi::OsStr,
fmt::{Display, Formatter, Result as FmtResult},
net::IpAddr,
str::FromStr,
};
use email_address::EmailAddress;
use idna::domain_to_ascii;
/// Represents an email address with a user and a domain name.
///
/// This type contains email in canonical form (_user@domain.tld_).
///
/// **NOTE**: Enable feature "serde" to be able to serialize/deserialize it using [serde](https://serde.rs/).
///
/// # Examples
///
/// You can create an `Address` from a user and a domain:
///
/// ```
/// use lettre::Address;
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("user", "email.com")?;
/// assert_eq!(address.user(), "user");
/// assert_eq!(address.domain(), "email.com");
/// # Ok(())
/// # }
/// ```
///
/// You can also create an `Address` from a string literal by parsing it:
///
/// ```
/// use lettre::Address;
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = "user@email.com".parse::<Address>()?;
/// assert_eq!(address.user(), "user");
/// assert_eq!(address.domain(), "email.com");
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct Address {
/// Complete address
serialized: String,
/// Index into `serialized` before the '@'
at_start: usize,
}
impl Address {
/// Creates a new email address from a user and domain.
///
/// # Examples
///
/// ```
/// use lettre::Address;
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("user", "email.com")?;
/// let expected = "user@email.com".parse::<Address>()?;
/// assert_eq!(expected, address);
/// # Ok(())
/// # }
/// ```
pub fn new<U: AsRef<str>, D: AsRef<str>>(user: U, domain: D) -> Result<Self, AddressError> {
(user, domain).try_into()
}
/// Gets the user portion of the `Address`.
///
/// # Examples
///
/// ```
/// use lettre::Address;
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("user", "email.com")?;
/// assert_eq!(address.user(), "user");
/// # Ok(())
/// # }
/// ```
pub fn user(&self) -> &str {
&self.serialized[..self.at_start]
}
/// Gets the domain portion of the `Address`.
///
/// # Examples
///
/// ```
/// use lettre::Address;
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("user", "email.com")?;
/// assert_eq!(address.domain(), "email.com");
/// # Ok(())
/// # }
/// ```
pub fn domain(&self) -> &str {
&self.serialized[self.at_start + 1..]
}
pub(super) fn check_user(user: &str) -> Result<(), AddressError> {
if EmailAddress::is_valid_local_part(user) {
Ok(())
} else {
Err(AddressError::InvalidUser)
}
}
pub(super) fn check_domain(domain: &str) -> Result<(), AddressError> {
Address::check_domain_ascii(domain).or_else(|_| {
domain_to_ascii(domain)
.map_err(|_| AddressError::InvalidDomain)
.and_then(|domain| Address::check_domain_ascii(&domain))
})
}
fn check_domain_ascii(domain: &str) -> Result<(), AddressError> {
// Domain
if EmailAddress::is_valid_domain(domain) {
return Ok(());
}
// IP
let ip = domain
.strip_prefix('[')
.and_then(|ip| ip.strip_suffix(']'))
.unwrap_or(domain);
if ip.parse::<IpAddr>().is_ok() {
return Ok(());
}
Err(AddressError::InvalidDomain)
}
#[cfg(feature = "smtp-transport")]
/// Check if the address contains non-ascii chars
pub(super) fn is_ascii(&self) -> bool {
self.serialized.is_ascii()
}
}
impl Display for Address {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
f.write_str(&self.serialized)
}
}
impl FromStr for Address {
type Err = AddressError;
fn from_str(val: &str) -> Result<Self, AddressError> {
let at_start = check_address(val)?;
Ok(Address {
serialized: val.into(),
at_start,
})
}
}
impl<U, D> TryFrom<(U, D)> for Address
where
U: AsRef<str>,
D: AsRef<str>,
{
type Error = AddressError;
fn try_from((user, domain): (U, D)) -> Result<Self, Self::Error> {
let user = user.as_ref();
Address::check_user(user)?;
let domain = domain.as_ref();
Address::check_domain(domain)?;
let serialized = format!("{user}@{domain}");
Ok(Address {
serialized,
at_start: user.len(),
})
}
}
impl TryFrom<String> for Address {
type Error = AddressError;
fn try_from(serialized: String) -> Result<Self, AddressError> {
let at_start = check_address(&serialized)?;
Ok(Address {
serialized,
at_start,
})
}
}
impl AsRef<str> for Address {
fn as_ref(&self) -> &str {
&self.serialized
}
}
impl AsRef<OsStr> for Address {
fn as_ref(&self) -> &OsStr {
self.serialized.as_ref()
}
}
fn check_address(val: &str) -> Result<usize, AddressError> {
let mut parts = val.rsplitn(2, '@');
let domain = parts.next().ok_or(AddressError::MissingParts)?;
let user = parts.next().ok_or(AddressError::MissingParts)?;
Address::check_user(user)?;
Address::check_domain(domain)?;
Ok(user.len())
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[non_exhaustive]
/// Errors in email addresses parsing
pub enum AddressError {
/// Missing domain or user
MissingParts,
/// Unbalanced angle bracket
Unbalanced,
/// Invalid email user
InvalidUser,
/// Invalid email domain
InvalidDomain,
/// Invalid input found
InvalidInput,
}
impl Error for AddressError {}
impl Display for AddressError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
AddressError::MissingParts => f.write_str("Missing domain or user"),
AddressError::Unbalanced => f.write_str("Unbalanced angle bracket"),
AddressError::InvalidUser => f.write_str("Invalid email user"),
AddressError::InvalidDomain => f.write_str("Invalid email domain"),
AddressError::InvalidInput => f.write_str("Invalid input"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_address() {
let addr_str = "something@example.com";
let addr = Address::from_str(addr_str).unwrap();
let addr2 = Address::new("something", "example.com").unwrap();
assert_eq!(addr, addr2);
assert_eq!(addr.user(), "something");
assert_eq!(addr.domain(), "example.com");
assert_eq!(addr2.user(), "something");
assert_eq!(addr2.domain(), "example.com");
}
#[test]
fn ascii_address_ipv4() {
let addr_str = "something@1.1.1.1";
let addr = Address::from_str(addr_str).unwrap();
let addr2 = Address::new("something", "1.1.1.1").unwrap();
assert_eq!(addr, addr2);
assert_eq!(addr.user(), "something");
assert_eq!(addr.domain(), "1.1.1.1");
assert_eq!(addr2.user(), "something");
assert_eq!(addr2.domain(), "1.1.1.1");
}
#[test]
fn ascii_address_ipv6() {
let addr_str = "something@[2606:4700:4700::1111]";
let addr = Address::from_str(addr_str).unwrap();
let addr2 = Address::new("something", "[2606:4700:4700::1111]").unwrap();
assert_eq!(addr, addr2);
assert_eq!(addr.user(), "something");
assert_eq!(addr.domain(), "[2606:4700:4700::1111]");
assert_eq!(addr2.user(), "something");
assert_eq!(addr2.domain(), "[2606:4700:4700::1111]");
}
#[test]
fn check_parts() {
assert!(Address::check_user("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").is_err());
assert!(
Address::check_domain("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com").is_err()
);
}
}

View File

@@ -1,12 +0,0 @@
use ::base64::{
engine::{general_purpose::STANDARD, Engine},
DecodeError,
};
pub(crate) fn encode<T: AsRef<[u8]>>(input: T) -> String {
STANDARD.encode(input)
}
pub(crate) fn decode<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, DecodeError> {
STANDARD.decode(input)
}

View File

@@ -1,54 +0,0 @@
//! Error type for email messages
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
};
// FIXME message-specific errors
/// Error type for email content
#[derive(Debug)]
pub enum Error {
/// Missing from in envelope
MissingFrom,
/// Missing to in envelope
MissingTo,
/// Can only be one from in envelope
TooManyFrom,
/// Invalid email: missing at
EmailMissingAt,
/// Invalid email: missing local part
EmailMissingLocalPart,
/// Invalid email: missing domain
EmailMissingDomain,
/// Cannot parse filename for attachment
CannotParseFilename,
/// IO error
Io(std::io::Error),
/// Non-ASCII chars
NonAsciiChars,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
match self {
Error::MissingFrom => f.write_str("missing source address, invalid envelope"),
Error::MissingTo => f.write_str("missing destination address, invalid envelope"),
Error::TooManyFrom => f.write_str("there can only be one source address"),
Error::EmailMissingAt => f.write_str("missing @ in email address"),
Error::EmailMissingLocalPart => f.write_str("missing local part in email address"),
Error::EmailMissingDomain => f.write_str("missing domain in email address"),
Error::CannotParseFilename => f.write_str("could not parse attachment filename"),
Error::NonAsciiChars => f.write_str("contains non-ASCII chars"),
Error::Io(e) => e.fmt(f),
}
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Error {
Error::Io(err)
}
}
impl StdError for Error {}

View File

@@ -1,295 +0,0 @@
use std::fmt::Debug;
#[cfg(feature = "smtp-transport")]
use std::future::Future;
#[cfg(feature = "file-transport")]
use std::io::Result as IoResult;
#[cfg(feature = "file-transport")]
use std::path::Path;
#[cfg(feature = "smtp-transport")]
use std::time::Duration;
use async_trait::async_trait;
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
use futures_util::future::BoxFuture;
#[cfg(all(
feature = "smtp-transport",
any(feature = "tokio1", feature = "async-std1")
))]
use crate::transport::smtp::client::AsyncSmtpConnection;
#[cfg(all(
feature = "smtp-transport",
any(feature = "tokio1", feature = "async-std1")
))]
use crate::transport::smtp::client::Tls;
#[cfg(all(
feature = "smtp-transport",
any(feature = "tokio1", feature = "async-std1")
))]
use crate::transport::smtp::extension::ClientId;
#[cfg(all(
feature = "smtp-transport",
any(feature = "tokio1", feature = "async-std1")
))]
use crate::transport::smtp::Error;
/// Async executor abstraction trait
///
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
/// in order to be able to work with different async runtimes.
///
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
#[async_trait]
pub trait Executor: Debug + Send + Sync + 'static + private::Sealed {
#[cfg(feature = "smtp-transport")]
type Handle: SpawnHandle;
#[cfg(feature = "smtp-transport")]
type Sleep: Future<Output = ()> + Send + 'static;
#[doc(hidden)]
#[cfg(feature = "smtp-transport")]
fn spawn<F>(fut: F) -> Self::Handle
where
F: Future<Output = ()> + Send + 'static,
F::Output: Send + 'static;
#[doc(hidden)]
#[cfg(feature = "smtp-transport")]
fn sleep(duration: Duration) -> Self::Sleep;
#[doc(hidden)]
#[cfg(feature = "smtp-transport")]
async fn connect(
hostname: &str,
port: u16,
timeout: Option<Duration>,
hello_name: &ClientId,
tls: &Tls,
) -> Result<AsyncSmtpConnection, Error>;
#[doc(hidden)]
#[cfg(feature = "file-transport-envelope")]
async fn fs_read(path: &Path) -> IoResult<Vec<u8>>;
#[doc(hidden)]
#[cfg(feature = "file-transport")]
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()>;
}
#[doc(hidden)]
#[cfg(feature = "smtp-transport")]
#[async_trait]
pub trait SpawnHandle: Debug + Send + Sync + 'static + private::Sealed {
async fn shutdown(self);
}
/// Async [`Executor`] using `tokio` `1.x`
///
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
/// in order to be able to work with different async runtimes.
///
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
#[allow(missing_copy_implementations)]
#[non_exhaustive]
#[cfg(feature = "tokio1")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
#[derive(Debug)]
pub struct Tokio1Executor;
#[async_trait]
#[cfg(feature = "tokio1")]
impl Executor for Tokio1Executor {
#[cfg(feature = "smtp-transport")]
type Handle = tokio1_crate::task::JoinHandle<()>;
#[cfg(feature = "smtp-transport")]
type Sleep = tokio1_crate::time::Sleep;
#[cfg(feature = "smtp-transport")]
fn spawn<F>(fut: F) -> Self::Handle
where
F: Future<Output = ()> + Send + 'static,
F::Output: Send + 'static,
{
tokio1_crate::spawn(fut)
}
#[cfg(feature = "smtp-transport")]
fn sleep(duration: Duration) -> Self::Sleep {
tokio1_crate::time::sleep(duration)
}
#[cfg(feature = "smtp-transport")]
async fn connect(
hostname: &str,
port: u16,
timeout: Option<Duration>,
hello_name: &ClientId,
tls: &Tls,
) -> Result<AsyncSmtpConnection, Error> {
#[allow(clippy::match_single_binding)]
let tls_parameters = match tls {
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
Tls::Wrapper(tls_parameters) => Some(tls_parameters.clone()),
_ => None,
};
#[allow(unused_mut)]
let mut conn = AsyncSmtpConnection::connect_tokio1(
(hostname, port),
timeout,
hello_name,
tls_parameters,
None,
)
.await?;
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
match tls {
Tls::Opportunistic(tls_parameters) => {
if conn.can_starttls() {
conn.starttls(tls_parameters.clone(), hello_name).await?;
}
}
Tls::Required(tls_parameters) => {
conn.starttls(tls_parameters.clone(), hello_name).await?;
}
_ => (),
}
Ok(conn)
}
#[cfg(feature = "file-transport-envelope")]
async fn fs_read(path: &Path) -> IoResult<Vec<u8>> {
tokio1_crate::fs::read(path).await
}
#[cfg(feature = "file-transport")]
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> {
tokio1_crate::fs::write(path, contents).await
}
}
#[cfg(all(feature = "smtp-transport", feature = "tokio1"))]
#[async_trait]
impl SpawnHandle for tokio1_crate::task::JoinHandle<()> {
async fn shutdown(self) {
self.abort();
}
}
/// Async [`Executor`] using `async-std` `1.x`
///
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
/// in order to be able to work with different async runtimes.
///
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
#[allow(missing_copy_implementations)]
#[non_exhaustive]
#[cfg(feature = "async-std1")]
#[cfg_attr(docsrs, doc(cfg(feature = "async-std1")))]
#[derive(Debug)]
pub struct AsyncStd1Executor;
#[async_trait]
#[cfg(feature = "async-std1")]
impl Executor for AsyncStd1Executor {
#[cfg(feature = "smtp-transport")]
type Handle = async_std::task::JoinHandle<()>;
#[cfg(feature = "smtp-transport")]
type Sleep = BoxFuture<'static, ()>;
#[cfg(feature = "smtp-transport")]
fn spawn<F>(fut: F) -> Self::Handle
where
F: Future<Output = ()> + Send + 'static,
F::Output: Send + 'static,
{
async_std::task::spawn(fut)
}
#[cfg(feature = "smtp-transport")]
fn sleep(duration: Duration) -> Self::Sleep {
let fut = async_std::task::sleep(duration);
Box::pin(fut)
}
#[cfg(feature = "smtp-transport")]
async fn connect(
hostname: &str,
port: u16,
timeout: Option<Duration>,
hello_name: &ClientId,
tls: &Tls,
) -> Result<AsyncSmtpConnection, Error> {
#[allow(clippy::match_single_binding)]
let tls_parameters = match tls {
#[cfg(feature = "async-std1-rustls-tls")]
Tls::Wrapper(tls_parameters) => Some(tls_parameters.clone()),
_ => None,
};
#[allow(unused_mut)]
let mut conn = AsyncSmtpConnection::connect_asyncstd1(
(hostname, port),
timeout,
hello_name,
tls_parameters,
)
.await?;
#[cfg(feature = "async-std1-rustls-tls")]
match tls {
Tls::Opportunistic(tls_parameters) => {
if conn.can_starttls() {
conn.starttls(tls_parameters.clone(), hello_name).await?;
}
}
Tls::Required(tls_parameters) => {
conn.starttls(tls_parameters.clone(), hello_name).await?;
}
_ => (),
}
Ok(conn)
}
#[cfg(feature = "file-transport-envelope")]
async fn fs_read(path: &Path) -> IoResult<Vec<u8>> {
async_std::fs::read(path).await
}
#[cfg(feature = "file-transport")]
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> {
async_std::fs::write(path, contents).await
}
}
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
#[async_trait]
impl SpawnHandle for async_std::task::JoinHandle<()> {
async fn shutdown(self) {
self.cancel().await;
}
}
mod private {
pub trait Sealed {}
#[cfg(feature = "tokio1")]
impl Sealed for super::Tokio1Executor {}
#[cfg(feature = "async-std1")]
impl Sealed for super::AsyncStd1Executor {}
#[cfg(all(feature = "smtp-transport", feature = "tokio1"))]
impl Sealed for tokio1_crate::task::JoinHandle<()> {}
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
impl Sealed for async_std::task::JoinHandle<()> {}
}

View File

@@ -1,307 +0,0 @@
//! Lettre is an email library that allows creating and sending messages. It provides:
//!
//! * An easy to use email builder
//! * Pluggable email transports
//! * Unicode support
//! * Secure defaults
//! * Async support
//!
//! Lettre requires Rust 1.70 or newer.
//!
//! ## Features
//!
//! This section lists each lettre feature and briefly explains it.
//! More info about each module can be found in the corresponding module page.
//!
//! Features with `📫` near them are enabled by default.
//!
//! ### Typed message builder
//!
//! _Strongly typed [`message`] builder_
//!
//! * **builder** 📫: Enable the [`Message`] builder
//! * **hostname** 📫: Try to use the actual system hostname in the `Message-ID` header
//!
//! ### SMTP transport
//!
//! _Send emails using [`SMTP`]_
//!
//! * **smtp-transport** 📫: Enable the SMTP transport
//! * **pool** 📫: Connection pool for SMTP transport
//! * **hostname** 📫: Try to use the actual system hostname for the SMTP `CLIENTID`
//!
//! #### SMTP over TLS via the native-tls crate
//!
//! _Secure SMTP connections using TLS from the `native-tls` crate_
//!
//! Uses schannel on Windows, Security-Framework on macOS, and OpenSSL on Linux.
//!
//! * **native-tls** 📫: TLS support for the synchronous version of the API
//! * **tokio1-native-tls**: TLS support for the `tokio1` async version of the API
//!
//! NOTE: native-tls isn't supported with `async-std`
//!
//! #### SMTP over TLS via the boring crate (Boring TLS)
//!
//! _Secure SMTP connections using TLS from the `boring-tls` crate_
//!
//! * **boring-tls**: TLS support for the synchronous version of the API
//! * **tokio1-boring-tls**: TLS support for the `tokio1` async version of the API
//!
//! NOTE: boring-tls isn't supported with `async-std`
//!
//! #### SMTP over TLS via the rustls crate
//!
//! _Secure SMTP connections using TLS from the `rustls-tls` crate_
//!
//! Rustls uses [ring] as the cryptography implementation. As a result, [not all Rust's targets are supported][ring-support].
//!
//! * **rustls-tls**: TLS support for the synchronous version of the API
//! * **tokio1-rustls-tls**: TLS support for the `tokio1` async version of the API
//! * **async-std1-rustls-tls**: TLS support for the `async-std1` async version of the API
//!
//! ### Sendmail transport
//!
//! _Send emails using the [`sendmail`] command_
//!
//! * **sendmail-transport**: Enable the `sendmail` transport
//!
//! ### File transport
//!
//! _Save emails as an `.eml` [`file`]_
//!
//! * **file-transport**: Enable the file transport (saves emails into an `.eml` file)
//! * **file-transport-envelope**: Allow writing the envelope into a JSON file (additionally saves envelopes into a `.json` file)
//!
//! ### Async execution runtimes
//!
//! _Use [tokio] or [async-std] as an async execution runtime for sending emails_
//!
//! The correct runtime version must be chosen in order for lettre to work correctly.
//! For example, when sending emails from a Tokio 1.x context, the Tokio 1.x executor
//! ([`Tokio1Executor`]) must be used. Using a different version (for example Tokio 0.2.x),
//! or async-std, would result in a runtime panic.
//!
//! * **tokio1**: Allow to asynchronously send emails using [Tokio 1.x]
//! * **async-std1**: Allow to asynchronously send emails using [async-std 1.x]
//!
//! NOTE: native-tls isn't supported with `async-std`
//!
//! ### Misc features
//!
//! _Additional features_
//!
//! * **serde**: Serialization/Deserialization of entities
//! * **tracing**: Logging using the `tracing` crate
//! * **mime03**: Allow creating a [`ContentType`] from an existing [mime 0.3] `Mime` struct
//! * **dkim**: Add support for signing email with DKIM
//!
//! [`SMTP`]: crate::transport::smtp
//! [`sendmail`]: crate::transport::sendmail
//! [`file`]: crate::transport::file
//! [`ContentType`]: crate::message::header::ContentType
//! [tokio]: https://docs.rs/tokio/1
//! [async-std]: https://docs.rs/async-std/1
//! [ring]: https://github.com/briansmith/ring#ring
//! [ring-support]: https://github.com/briansmith/ring#online-automated-testing
//! [Tokio 1.x]: https://docs.rs/tokio/1
//! [async-std 1.x]: https://docs.rs/async-std/1
//! [mime 0.3]: https://docs.rs/mime/0.3
//! [DKIM]: https://datatracker.ietf.org/doc/html/rfc6376
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.9")]
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
#![forbid(unsafe_code)]
#![deny(
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unstable_features,
unused_import_braces,
rust_2018_idioms,
clippy::string_add,
clippy::string_add_assign,
clippy::clone_on_ref_ptr,
clippy::verbose_file_reads,
clippy::unnecessary_self_imports,
clippy::string_to_string,
clippy::mem_forget,
clippy::cast_lossless,
clippy::inefficient_to_string,
clippy::inline_always,
clippy::linkedlist,
clippy::macro_use_imports,
clippy::manual_assert,
clippy::unnecessary_join,
clippy::wildcard_imports,
clippy::str_to_string,
clippy::empty_structs_with_brackets,
clippy::zero_sized_map_values
)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#[cfg(not(lettre_ignore_tls_mismatch))]
mod compiletime_checks {
#[cfg(all(feature = "native-tls", feature = "boring-tls"))]
compile_error!("feature \"native-tls\" and feature \"boring-tls\" cannot be enabled at the same time, otherwise
the executable will fail to link.");
#[cfg(all(
feature = "tokio1",
feature = "native-tls",
not(feature = "tokio1-native-tls")
))]
compile_error!("Lettre is being built with the `tokio1` and the `native-tls` features, but the `tokio1-native-tls` feature hasn't been turned on.
If you were trying to opt into `rustls-tls` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
#[cfg(all(
feature = "tokio1",
feature = "rustls-tls",
not(feature = "tokio1-rustls-tls")
))]
compile_error!("Lettre is being built with the `tokio1` and the `rustls-tls` features, but the `tokio1-rustls-tls` feature hasn't been turned on.
If you'd like to use `native-tls` make sure that the `rustls-tls` feature hasn't been enabled by mistake.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
#[cfg(all(
feature = "tokio1",
feature = "boring-tls",
not(feature = "tokio1-boring-tls")
))]
compile_error!("Lettre is being built with the `tokio1` and the `boring-tls` features, but the `tokio1-boring-tls` feature hasn't been turned on.
If you'd like to use `boring-tls` make sure that the `rustls-tls` feature hasn't been enabled by mistake.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
#[cfg(all(feature = "async-std1", feature = "native-tls",))]
compile_error!("Lettre is being built with the `async-std1` and the `native-tls` features, but the async-std integration doesn't support native-tls yet.
If you'd like to work on the issue please take a look at https://github.com/lettre/lettre/issues/576.
If you were trying to opt into `rustls-tls` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
#[cfg(all(
feature = "async-std1",
feature = "rustls-tls",
not(feature = "async-std1-rustls-tls")
))]
compile_error!("Lettre is being built with the `async-std1` and the `rustls-tls` features, but the `async-std1-rustls-tls` feature hasn't been turned on.
If you'd like to use `native-tls` make sure that the `rustls-tls` hasn't been enabled by mistake.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
}
pub mod address;
#[cfg(any(feature = "smtp-transport", feature = "dkim"))]
mod base64;
pub mod error;
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
mod executor;
#[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
pub mod message;
pub mod transport;
use std::error::Error as StdError;
#[cfg(feature = "async-std1")]
pub use self::executor::AsyncStd1Executor;
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub use self::executor::Executor;
#[cfg(feature = "tokio1")]
pub use self::executor::Tokio1Executor;
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
#[doc(inline)]
pub use self::transport::AsyncTransport;
pub use crate::address::Address;
#[cfg(feature = "builder")]
#[doc(inline)]
pub use crate::message::Message;
#[cfg(all(
feature = "file-transport",
any(feature = "tokio1", feature = "async-std1")
))]
#[doc(inline)]
pub use crate::transport::file::AsyncFileTransport;
#[cfg(feature = "file-transport")]
#[doc(inline)]
pub use crate::transport::file::FileTransport;
#[cfg(all(
feature = "sendmail-transport",
any(feature = "tokio1", feature = "async-std1")
))]
#[doc(inline)]
pub use crate::transport::sendmail::AsyncSendmailTransport;
#[cfg(feature = "sendmail-transport")]
#[doc(inline)]
pub use crate::transport::sendmail::SendmailTransport;
#[cfg(all(
feature = "smtp-transport",
any(feature = "tokio1", feature = "async-std1")
))]
pub use crate::transport::smtp::AsyncSmtpTransport;
#[cfg(feature = "smtp-transport")]
pub use crate::transport::smtp::SmtpTransport;
#[doc(inline)]
pub use crate::transport::Transport;
use crate::{address::Envelope, error::Error};
pub(crate) type BoxError = Box<dyn StdError + Send + Sync>;
#[cfg(test)]
#[cfg(feature = "builder")]
mod test {
use super::*;
use crate::message::{header, header::Headers, Mailbox, Mailboxes};
#[test]
fn envelope_from_headers() {
let from = Mailboxes::new().with("kayo@example.com".parse().unwrap());
let to = Mailboxes::new().with("amousset@example.com".parse().unwrap());
let mut headers = Headers::new();
headers.set(header::From(from));
headers.set(header::To(to));
assert_eq!(
Envelope::try_from(&headers).unwrap(),
Envelope::new(
Some(Address::new("kayo", "example.com").unwrap()),
vec![Address::new("amousset", "example.com").unwrap()]
)
.unwrap()
);
}
#[test]
fn envelope_from_headers_sender() {
let from = Mailboxes::new().with("kayo@example.com".parse().unwrap());
let sender = Mailbox::new(None, "kayo2@example.com".parse().unwrap());
let to = Mailboxes::new().with("amousset@example.com".parse().unwrap());
let mut headers = Headers::new();
headers.set(header::From::from(from));
headers.set(header::Sender::from(sender));
headers.set(header::To::from(to));
assert_eq!(
Envelope::try_from(&headers).unwrap(),
Envelope::new(
Some(Address::new("kayo2", "example.com").unwrap()),
vec![Address::new("amousset", "example.com").unwrap()]
)
.unwrap()
);
}
#[test]
fn envelope_from_headers_no_to() {
let from = Mailboxes::new().with("kayo@example.com".parse().unwrap());
let sender = Mailbox::new(None, "kayo2@example.com".parse().unwrap());
let mut headers = Headers::new();
headers.set(header::From::from(from));
headers.set(header::Sender::from(sender));
assert!(Envelope::try_from(&headers).is_err(),);
}
}

View File

@@ -1,145 +0,0 @@
use crate::message::{
header::{self, ContentType},
IntoBody, SinglePart,
};
/// `SinglePart` builder for attachments
///
/// Allows building attachment parts easily.
#[derive(Clone)]
pub struct Attachment {
disposition: Disposition,
}
#[derive(Clone)]
enum Disposition {
/// File name
Attached(String),
/// Content id
Inline(String),
}
impl Attachment {
/// Create a new attachment
///
/// This attachment will be displayed as a normal attachment,
/// with the chosen `filename` appearing as the file name.
///
/// ```rust
/// # use std::error::Error;
/// use std::fs;
///
/// use lettre::message::{header::ContentType, Attachment};
///
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let filename = String::from("invoice.pdf");
/// # if false {
/// let filebody = fs::read("invoice.pdf")?;
/// # }
/// # let filebody = fs::read("docs/lettre.png")?;
/// let content_type = ContentType::parse("application/pdf").unwrap();
/// let attachment = Attachment::new(filename).body(filebody, content_type);
///
/// // The document `attachment` will show up as a normal attachment.
/// # Ok(())
/// # }
/// ```
pub fn new(filename: String) -> Self {
Attachment {
disposition: Disposition::Attached(filename),
}
}
/// Create a new inline attachment
///
/// This attachment should be displayed inline into the message
/// body:
///
/// ```html
/// <img src="cid:123">
/// ```
///
///
/// ```rust
/// # use std::error::Error;
/// use std::fs;
///
/// use lettre::message::{header::ContentType, Attachment};
///
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let content_id = String::from("123");
/// # if false {
/// let filebody = fs::read("image.jpg")?;
/// # }
/// # let filebody = fs::read("docs/lettre.png")?;
/// let content_type = ContentType::parse("image/jpeg").unwrap();
/// let attachment = Attachment::new_inline(content_id).body(filebody, content_type);
///
/// // The image `attachment` will display inline into the email.
/// # Ok(())
/// # }
/// ```
pub fn new_inline(content_id: String) -> Self {
Attachment {
disposition: Disposition::Inline(content_id),
}
}
/// Build the attachment into a [`SinglePart`] which can then be used to build the rest of the email
///
/// Look at the [Complex MIME body example](crate::message#complex-mime-body)
/// to see how [`SinglePart`] can be put into the email.
pub fn body<T: IntoBody>(self, content: T, content_type: ContentType) -> SinglePart {
let mut builder = SinglePart::builder();
builder = match self.disposition {
Disposition::Attached(filename) => {
builder.header(header::ContentDisposition::attachment(&filename))
}
Disposition::Inline(content_id) => builder
.header(header::ContentId::from(format!("<{content_id}>")))
.header(header::ContentDisposition::inline()),
};
builder = builder.header(content_type);
builder.body(content)
}
}
#[cfg(test)]
mod tests {
use crate::message::header::ContentType;
#[test]
fn attachment() {
let part = super::Attachment::new(String::from("test.txt")).body(
String::from("Hello world!"),
ContentType::parse("text/plain").unwrap(),
);
assert_eq!(
&String::from_utf8_lossy(&part.formatted()),
concat!(
"Content-Disposition: attachment; filename=\"test.txt\"\r\n",
"Content-Type: text/plain\r\n",
"Content-Transfer-Encoding: 7bit\r\n\r\n",
"Hello world!\r\n",
)
);
}
#[test]
fn attachment_inline() {
let part = super::Attachment::new_inline(String::from("id")).body(
String::from("Hello world!"),
ContentType::parse("text/plain").unwrap(),
);
assert_eq!(
&String::from_utf8_lossy(&part.formatted()),
concat!(
"Content-ID: <id>\r\n",
"Content-Disposition: inline\r\n",
"Content-Type: text/plain\r\n",
"Content-Transfer-Encoding: 7bit\r\n\r\n",
"Hello world!\r\n"
)
);
}
}

View File

@@ -1,575 +0,0 @@
use std::{mem, ops::Deref};
use crate::message::header::ContentTransferEncoding;
/// A [`Message`][super::Message] or [`SinglePart`][super::SinglePart] body that has already been encoded.
#[derive(Debug, Clone)]
pub struct Body {
buf: Vec<u8>,
encoding: ContentTransferEncoding,
}
/// Either a `Vec<u8>` or a `String`.
///
/// If the content is valid utf-8 a `String` should be passed, as it
/// makes for a more efficient `Content-Transfer-Encoding` to be chosen.
#[derive(Debug, Clone)]
pub enum MaybeString {
/// Binary data
Binary(Vec<u8>),
/// UTF-8 string
String(String),
}
impl Body {
/// Encode the supplied `buf`, making it ready to be sent as a body.
///
/// Takes a `Vec<u8>` or a `String`.
///
/// Automatically chooses the most efficient encoding between
/// `7bit`, `quoted-printable` and `base64`.
///
/// If `String` is passed, line endings are converted to `CRLF`.
///
/// If `buf` is valid utf-8 a `String` should be supplied, as `String`s
/// can be encoded as `7bit` or `quoted-printable`, while `Vec<u8>` always
/// get encoded as `base64`.
pub fn new<B: Into<MaybeString>>(buf: B) -> Self {
let mut buf: MaybeString = buf.into();
let encoding = buf.encoding(false);
buf.encode_crlf();
Self::new_impl(buf.into(), encoding)
}
/// Encode the supplied `buf`, using the provided `encoding`.
///
/// [`Body::new`] is generally the better option.
///
/// If `String` is passed, line endings are converted to `CRLF`.
///
/// Returns an [`Err`] giving back the supplied `buf`, in case the chosen
/// encoding would have resulted into `buf` being encoded
/// into an invalid body.
pub fn new_with_encoding<B: Into<MaybeString>>(
buf: B,
encoding: ContentTransferEncoding,
) -> Result<Self, Vec<u8>> {
let mut buf: MaybeString = buf.into();
let best_encoding = buf.encoding(true);
let ok = match (encoding, best_encoding) {
(ContentTransferEncoding::SevenBit, ContentTransferEncoding::SevenBit) => true,
(
ContentTransferEncoding::EightBit,
ContentTransferEncoding::SevenBit | ContentTransferEncoding::EightBit,
) => true,
(ContentTransferEncoding::SevenBit | ContentTransferEncoding::EightBit, _) => false,
(
ContentTransferEncoding::QuotedPrintable
| ContentTransferEncoding::Base64
| ContentTransferEncoding::Binary,
_,
) => true,
};
if !ok {
return Err(buf.into());
}
buf.encode_crlf();
Ok(Self::new_impl(buf.into(), encoding))
}
/// Builds a new `Body` using a pre-encoded buffer.
///
/// **Generally not you want.**
///
/// `buf` shouldn't contain non-ascii characters, lines longer than 1000 characters or nul bytes.
#[inline]
pub fn dangerous_pre_encoded(buf: Vec<u8>, encoding: ContentTransferEncoding) -> Self {
Self { buf, encoding }
}
/// Encodes the supplied `buf` using the provided `encoding`
fn new_impl(buf: Vec<u8>, encoding: ContentTransferEncoding) -> Self {
match encoding {
ContentTransferEncoding::SevenBit
| ContentTransferEncoding::EightBit
| ContentTransferEncoding::Binary => Self { buf, encoding },
ContentTransferEncoding::QuotedPrintable => {
let encoded = quoted_printable::encode(buf);
Self::dangerous_pre_encoded(encoded, ContentTransferEncoding::QuotedPrintable)
}
ContentTransferEncoding::Base64 => {
let len = email_encoding::body::base64::encoded_len(buf.len());
let mut out = String::with_capacity(len);
email_encoding::body::base64::encode(&buf, &mut out)
.expect("encode body as base64");
Self::dangerous_pre_encoded(out.into_bytes(), ContentTransferEncoding::Base64)
}
}
}
/// Returns the length of this `Body` in bytes.
#[inline]
pub fn len(&self) -> usize {
self.buf.len()
}
/// Returns `true` if this `Body` has a length of zero, `false` otherwise.
#[inline]
pub fn is_empty(&self) -> bool {
self.buf.is_empty()
}
/// Returns the `Content-Transfer-Encoding` of this `Body`.
#[inline]
pub fn encoding(&self) -> ContentTransferEncoding {
self.encoding
}
/// Consumes `Body` and returns the inner `Vec<u8>`
#[inline]
pub fn into_vec(self) -> Vec<u8> {
self.buf
}
}
impl MaybeString {
/// Suggests the best `Content-Transfer-Encoding` to be used for this `MaybeString`
///
/// The `binary` encoding is never returned
fn encoding(&self, supports_utf8: bool) -> ContentTransferEncoding {
use email_encoding::body::Encoding;
let output = match self {
Self::String(s) => Encoding::choose(s.as_str(), supports_utf8),
Self::Binary(b) => Encoding::choose(b.as_slice(), supports_utf8),
};
match output {
Encoding::SevenBit => ContentTransferEncoding::SevenBit,
Encoding::EightBit => ContentTransferEncoding::EightBit,
Encoding::QuotedPrintable => ContentTransferEncoding::QuotedPrintable,
Encoding::Base64 => ContentTransferEncoding::Base64,
}
}
/// Encode line endings to CRLF if the variant is `String`
fn encode_crlf(&mut self) {
match self {
Self::String(string) => in_place_crlf_line_endings(string),
Self::Binary(_) => {}
}
}
}
/// A trait for something that takes an encoded [`Body`].
///
/// Used by [`MessageBuilder::body`][super::MessageBuilder::body] and
/// [`SinglePartBuilder::body`][super::SinglePartBuilder::body],
/// which can either take something that can be encoded into [`Body`]
/// or a pre-encoded [`Body`].
///
/// If `encoding` is `None` the best encoding between `7bit`, `quoted-printable`
/// and `base64` is chosen based on the input body. **Best option.**
///
/// If `encoding` is `Some` the supplied encoding is used.
/// **NOTE:** if using the specified `encoding` would result into a malformed
/// body, this will panic!
pub trait IntoBody {
/// Encode as valid body
fn into_body(self, encoding: Option<ContentTransferEncoding>) -> Body;
}
impl<T> IntoBody for T
where
T: Into<MaybeString>,
{
fn into_body(self, encoding: Option<ContentTransferEncoding>) -> Body {
match encoding {
Some(encoding) => Body::new_with_encoding(self, encoding).expect("invalid encoding"),
None => Body::new(self),
}
}
}
impl IntoBody for Body {
fn into_body(self, encoding: Option<ContentTransferEncoding>) -> Body {
let _ = encoding;
self
}
}
impl AsRef<[u8]> for Body {
#[inline]
fn as_ref(&self) -> &[u8] {
self.buf.as_ref()
}
}
impl From<Vec<u8>> for MaybeString {
#[inline]
fn from(b: Vec<u8>) -> Self {
Self::Binary(b)
}
}
impl From<String> for MaybeString {
#[inline]
fn from(s: String) -> Self {
Self::String(s)
}
}
impl From<MaybeString> for Vec<u8> {
#[inline]
fn from(s: MaybeString) -> Self {
match s {
MaybeString::Binary(b) => b,
MaybeString::String(s) => s.into(),
}
}
}
impl Deref for MaybeString {
type Target = [u8];
#[inline]
fn deref(&self) -> &Self::Target {
match self {
Self::Binary(b) => b.as_ref(),
Self::String(s) => s.as_ref(),
}
}
}
/// In place conversion to CRLF line endings
fn in_place_crlf_line_endings(string: &mut String) {
let indices = find_all_lf_char_indices(string);
for i in indices {
// this relies on `indices` being in reverse order
string.insert(i, '\r');
}
}
/// Find indices to all places where `\r` should be inserted
/// in order to make `s` have CRLF line endings
///
/// The list is reversed, which is more efficient.
fn find_all_lf_char_indices(s: &str) -> Vec<usize> {
let mut indices = Vec::new();
let mut found_lf = false;
for (i, c) in s.char_indices().rev() {
if mem::take(&mut found_lf) && c != '\r' {
// the previous character was `\n`, but this isn't a `\r`
indices.push(i + c.len_utf8());
}
found_lf = c == '\n';
}
if found_lf {
// the first character is `\n`
indices.push(0);
}
indices
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::{in_place_crlf_line_endings, Body, ContentTransferEncoding};
#[test]
fn seven_bit_detect() {
let encoded = Body::new(String::from("Hello, world!"));
assert_eq!(encoded.encoding(), ContentTransferEncoding::SevenBit);
assert_eq!(encoded.as_ref(), b"Hello, world!");
}
#[test]
fn seven_bit_encode() {
let encoded = Body::new_with_encoding(
String::from("Hello, world!"),
ContentTransferEncoding::SevenBit,
)
.unwrap();
assert_eq!(encoded.encoding(), ContentTransferEncoding::SevenBit);
assert_eq!(encoded.as_ref(), b"Hello, world!");
}
#[test]
fn seven_bit_too_long_detect() {
let encoded = Body::new("Hello, world!".repeat(100));
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
assert_eq!(
encoded.as_ref(),
concat!(
"Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wor=\r\n",
"ld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, =\r\n",
"world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hell=\r\n",
"o, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!H=\r\n",
"ello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, worl=\r\n",
"d!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, w=\r\n",
"orld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello=\r\n",
", world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!He=\r\n",
"llo, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world=\r\n",
"!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wo=\r\n",
"rld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello,=\r\n",
" world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hel=\r\n",
"lo, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!=\r\n",
"Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wor=\r\n",
"ld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, =\r\n",
"world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hell=\r\n",
"o, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!H=\r\n",
"ello, world!Hello, world!"
)
.as_bytes()
);
}
#[test]
fn seven_bit_too_long_fail() {
let result = Body::new_with_encoding(
"Hello, world!".repeat(100),
ContentTransferEncoding::SevenBit,
);
assert!(result.is_err());
}
#[test]
fn seven_bit_too_long_encode_quotedprintable() {
let encoded = Body::new_with_encoding(
"Hello, world!".repeat(100),
ContentTransferEncoding::QuotedPrintable,
)
.unwrap();
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
assert_eq!(
encoded.as_ref(),
concat!(
"Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wor=\r\n",
"ld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, =\r\n",
"world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hell=\r\n",
"o, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!H=\r\n",
"ello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, worl=\r\n",
"d!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, w=\r\n",
"orld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello=\r\n",
", world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!He=\r\n",
"llo, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world=\r\n",
"!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wo=\r\n",
"rld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello,=\r\n",
" world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hel=\r\n",
"lo, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!=\r\n",
"Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wor=\r\n",
"ld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, =\r\n",
"world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hell=\r\n",
"o, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!H=\r\n",
"ello, world!Hello, world!"
)
.as_bytes()
);
}
#[test]
fn seven_bit_invalid() {
let result = Body::new_with_encoding(
String::from("Привет, мир!"),
ContentTransferEncoding::SevenBit,
);
assert!(result.is_err());
}
#[test]
fn eight_bit_encode() {
let encoded = Body::new_with_encoding(
String::from("Привет, мир!"),
ContentTransferEncoding::EightBit,
)
.unwrap();
assert_eq!(encoded.encoding(), ContentTransferEncoding::EightBit);
assert_eq!(encoded.as_ref(), "Привет, мир!".as_bytes());
}
#[test]
fn eight_bit_too_long_fail() {
let result = Body::new_with_encoding(
"Привет, мир!".repeat(200),
ContentTransferEncoding::EightBit,
);
assert!(result.is_err());
}
#[test]
fn quoted_printable_detect() {
let encoded = Body::new(String::from("Questo messaggio è corto"));
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
assert_eq!(encoded.as_ref(), b"Questo messaggio =C3=A8 corto");
}
#[test]
fn quoted_printable_encode_ascii() {
let encoded = Body::new_with_encoding(
String::from("Hello, world!"),
ContentTransferEncoding::QuotedPrintable,
)
.unwrap();
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
assert_eq!(encoded.as_ref(), b"Hello, world!");
}
#[test]
fn quoted_printable_encode_utf8() {
let encoded = Body::new_with_encoding(
String::from("Привет, мир!"),
ContentTransferEncoding::QuotedPrintable,
)
.unwrap();
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
assert_eq!(
encoded.as_ref(),
b"=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!".as_ref()
);
}
#[test]
fn quoted_printable_encode_line_wrap() {
let encoded = Body::new(String::from(
"Se lo standard 📬 fosse stato più semplice avremmo finito molto prima.",
));
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
println!("{}", std::str::from_utf8(encoded.as_ref()).unwrap());
assert_eq!(
encoded.as_ref(),
concat!(
"Se lo standard =F0=9F=93=AC fosse stato pi=C3=B9 semplice avremmo finito mo=\r\n",
"lto prima."
)
.as_bytes()
);
}
#[test]
fn base64_detect() {
let input = Body::new(vec![0; 80]);
let encoding = input.encoding();
assert_eq!(encoding, ContentTransferEncoding::Base64);
}
#[test]
fn base64_encode_bytes() {
let encoded =
Body::new_with_encoding(vec![0; 80], ContentTransferEncoding::Base64).unwrap();
assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
assert_eq!(
encoded.as_ref(),
concat!(
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n",
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
)
.as_bytes()
);
}
#[test]
fn base64_encode_bytes_wrapping() {
let encoded = Body::new_with_encoding(
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].repeat(20),
ContentTransferEncoding::Base64,
)
.unwrap();
assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
assert_eq!(
encoded.as_ref(),
concat!(
"AAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUG\r\n",
"BwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQID\r\n",
"BAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkA\r\n",
"AQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAk="
)
.as_bytes()
);
}
#[test]
fn base64_encode_ascii() {
let encoded = Body::new_with_encoding(
String::from("Hello World!"),
ContentTransferEncoding::Base64,
)
.unwrap();
assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
assert_eq!(encoded.as_ref(), b"SGVsbG8gV29ybGQh");
}
#[test]
fn base64_encode_ascii_wrapping() {
let encoded =
Body::new_with_encoding("Hello World!".repeat(20), ContentTransferEncoding::Base64)
.unwrap();
assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
assert_eq!(
encoded.as_ref(),
concat!(
"SGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29y\r\n",
"bGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8g\r\n",
"V29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVs\r\n",
"bG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQh\r\n",
"SGVsbG8gV29ybGQh"
)
.as_bytes()
);
}
#[test]
fn crlf() {
let mut string = String::from("Send me a ✉️\nwith\nlettre!\n😀");
in_place_crlf_line_endings(&mut string);
assert_eq!(string, "Send me a ✉️\r\nwith\r\nlettre!\r\n😀");
}
#[test]
fn harsh_crlf() {
let mut string = String::from("\n\nSend me a ✉️\r\n\nwith\n\nlettre!\n\r\n😀");
in_place_crlf_line_endings(&mut string);
assert_eq!(
string,
"\r\n\r\nSend me a ✉️\r\n\r\nwith\r\n\r\nlettre!\r\n\r\n😀"
);
}
#[test]
fn crlf_noop() {
let mut string = String::from("\r\nSend me a ✉️\r\nwith\r\nlettre!\r\n😀");
in_place_crlf_line_endings(&mut string);
assert_eq!(string, "\r\nSend me a ✉️\r\nwith\r\nlettre!\r\n😀");
}
}

View File

@@ -1,616 +0,0 @@
use std::{
borrow::Cow,
error::Error as StdError,
fmt::{self, Display},
time::SystemTime,
};
use ed25519_dalek::Signer;
use rsa::{pkcs1::DecodeRsaPrivateKey, pkcs1v15::Pkcs1v15Sign, RsaPrivateKey};
use sha2::{Digest, Sha256};
use crate::message::{
header::{HeaderName, HeaderValue},
Headers, Message,
};
/// Describe Dkim Canonicalization to apply to either body or headers
#[derive(Copy, Clone, Debug)]
pub enum DkimCanonicalizationType {
Simple,
Relaxed,
}
impl Display for DkimCanonicalizationType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
DkimCanonicalizationType::Simple => "simple",
DkimCanonicalizationType::Relaxed => "relaxed",
})
}
}
/// Describe Canonicalization to be applied before signing
#[derive(Copy, Clone, Debug)]
pub struct DkimCanonicalization {
pub header: DkimCanonicalizationType,
pub body: DkimCanonicalizationType,
}
impl Default for DkimCanonicalization {
fn default() -> Self {
DkimCanonicalization {
header: DkimCanonicalizationType::Simple,
body: DkimCanonicalizationType::Relaxed,
}
}
}
/// Format canonicalization to be shown in Dkim header
impl Display for DkimCanonicalization {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.header, self.body)
}
}
/// Describe the algorithm used for signing the message
#[derive(Copy, Clone, Debug)]
pub enum DkimSigningAlgorithm {
Rsa,
Ed25519,
}
impl Display for DkimSigningAlgorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
DkimSigningAlgorithm::Rsa => "rsa",
DkimSigningAlgorithm::Ed25519 => "ed25519",
})
}
}
/// Describe DkimSigning key error
#[derive(Debug)]
pub struct DkimSigningKeyError(InnerDkimSigningKeyError);
#[derive(Debug)]
enum InnerDkimSigningKeyError {
Base64(base64::DecodeError),
Rsa(rsa::pkcs1::Error),
Ed25519(ed25519_dalek::ed25519::Error),
}
impl Display for DkimSigningKeyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match &self.0 {
InnerDkimSigningKeyError::Base64(_err) => "base64 decode error",
InnerDkimSigningKeyError::Rsa(_err) => "rsa decode error",
InnerDkimSigningKeyError::Ed25519(_err) => "ed25519 decode error",
})
}
}
impl StdError for DkimSigningKeyError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(match &self.0 {
InnerDkimSigningKeyError::Base64(err) => err,
InnerDkimSigningKeyError::Rsa(err) => err,
InnerDkimSigningKeyError::Ed25519(err) => err,
})
}
}
/// Describe a signing key to be carried by DkimConfig struct
#[derive(Debug)]
pub struct DkimSigningKey(InnerDkimSigningKey);
#[derive(Debug)]
enum InnerDkimSigningKey {
Rsa(RsaPrivateKey),
Ed25519(ed25519_dalek::SigningKey),
}
impl DkimSigningKey {
pub fn new(
private_key: &str,
algorithm: DkimSigningAlgorithm,
) -> Result<DkimSigningKey, DkimSigningKeyError> {
Ok(Self(match algorithm {
DkimSigningAlgorithm::Rsa => InnerDkimSigningKey::Rsa(
RsaPrivateKey::from_pkcs1_pem(private_key)
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Rsa(err)))?,
),
DkimSigningAlgorithm::Ed25519 => {
InnerDkimSigningKey::Ed25519(ed25519_dalek::SigningKey::from_bytes(
&crate::base64::decode(private_key)
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Base64(err)))?
.try_into()
.map_err(|_| {
DkimSigningKeyError(InnerDkimSigningKeyError::Ed25519(
ed25519_dalek::ed25519::Error::new(),
))
})?,
))
}
}))
}
fn get_signing_algorithm(&self) -> DkimSigningAlgorithm {
match self.0 {
InnerDkimSigningKey::Rsa(_) => DkimSigningAlgorithm::Rsa,
InnerDkimSigningKey::Ed25519(_) => DkimSigningAlgorithm::Ed25519,
}
}
}
/// A struct to describe Dkim configuration applied when signing a message
#[derive(Debug)]
pub struct DkimConfig {
/// The name of the key published in DNS
selector: String,
/// The domain for which we sign the message
domain: String,
/// The private key in PKCS1 string format
private_key: DkimSigningKey,
/// A list of header names to be included in the signature. Signing of more than one
/// header with the same name is not supported
headers: Vec<HeaderName>,
/// The signing algorithm to be used when signing
canonicalization: DkimCanonicalization,
}
impl DkimConfig {
/// Create a default signature configuration with a set of headers and "simple/relaxed"
/// canonicalization
pub fn default_config(
selector: String,
domain: String,
private_key: DkimSigningKey,
) -> DkimConfig {
DkimConfig {
selector,
domain,
private_key,
headers: vec![
HeaderName::new_from_ascii_str("From"),
HeaderName::new_from_ascii_str("Subject"),
HeaderName::new_from_ascii_str("To"),
HeaderName::new_from_ascii_str("Date"),
],
canonicalization: DkimCanonicalization {
header: DkimCanonicalizationType::Simple,
body: DkimCanonicalizationType::Relaxed,
},
}
}
/// Create a DkimConfig
pub fn new(
selector: String,
domain: String,
private_key: DkimSigningKey,
headers: Vec<HeaderName>,
canonicalization: DkimCanonicalization,
) -> DkimConfig {
DkimConfig {
selector,
domain,
private_key,
headers,
canonicalization,
}
}
}
/// Create a Headers struct with a Dkim-Signature Header created from given parameters
fn dkim_header_format(
config: &DkimConfig,
timestamp: u64,
headers_list: &str,
body_hash: &str,
signature: &str,
) -> Headers {
let mut headers = Headers::new();
let header_name =
dkim_canonicalize_header_tag("DKIM-Signature", config.canonicalization.header);
let header_name = HeaderName::new_from_ascii(header_name.into()).unwrap();
headers.insert_raw(HeaderValue::new(header_name, format!("v=1; a={signing_algorithm}-sha256; d={domain}; s={selector}; c={canon}; q=dns/txt; t={timestamp}; h={headers_list}; bh={body_hash}; b={signature}",domain=config.domain, selector=config.selector,canon=config.canonicalization,timestamp=timestamp,headers_list=headers_list,body_hash=body_hash,signature=signature,signing_algorithm=config.private_key.get_signing_algorithm())));
headers
}
/// Canonicalize the body of an email
fn dkim_canonicalize_body(
mut body: &[u8],
canonicalization: DkimCanonicalizationType,
) -> Cow<'_, [u8]> {
match canonicalization {
DkimCanonicalizationType::Simple => {
// Remove empty lines at end
while body.ends_with(b"\r\n\r\n") {
body = &body[..body.len() - 2];
}
Cow::Borrowed(body)
}
DkimCanonicalizationType::Relaxed => {
let mut out = Vec::with_capacity(body.len());
loop {
match body {
[b' ' | b'\t', b'\r', b'\n', ..] => {}
[b' ' | b'\t', b' ' | b'\t', ..] => {}
[b' ' | b'\t', ..] => out.push(b' '),
[c, ..] => out.push(*c),
[] => break,
}
body = &body[1..];
}
// Remove empty lines at end
while out.ends_with(b"\r\n\r\n") {
out.truncate(out.len() - 2);
}
Cow::Owned(out)
}
}
}
fn dkim_canonicalize_headers_relaxed(headers: &str) -> String {
let mut r = String::with_capacity(headers.len());
fn skip_whitespace(h: &str) -> &str {
match h.as_bytes().first() {
Some(b' ' | b'\t') => skip_whitespace(&h[1..]),
_ => h,
}
}
fn name(h: &str, out: &mut String) {
if let Some(name_end) = h.bytes().position(|c| c == b':') {
let (name, rest) = h.split_at(name_end + 1);
*out += name;
// Space after header colon is stripped.
value(skip_whitespace(rest), out);
} else {
// This should never happen.
*out += h;
}
}
fn value(h: &str, out: &mut String) {
match h.as_bytes() {
// Continuation lines.
[b'\r', b'\n', b' ' | b'\t', ..] => {
out.push(' ');
value(skip_whitespace(&h[2..]), out);
}
// End of header.
[b'\r', b'\n', ..] => {
*out += "\r\n";
name(&h[2..], out)
}
// Sequential whitespace.
[b' ' | b'\t', b' ' | b'\t' | b'\r', ..] => value(&h[1..], out),
// All whitespace becomes spaces.
[b'\t', ..] => {
out.push(' ');
value(&h[1..], out)
}
[_, ..] => {
let mut chars = h.chars();
out.push(chars.next().unwrap());
value(chars.as_str(), out)
}
[] => {}
}
}
name(headers, &mut r);
r
}
/// Canonicalize header tag
fn dkim_canonicalize_header_tag(
name: &str,
canonicalization: DkimCanonicalizationType,
) -> Cow<'_, str> {
match canonicalization {
DkimCanonicalizationType::Simple => Cow::Borrowed(name),
DkimCanonicalizationType::Relaxed => Cow::Owned(name.to_lowercase()),
}
}
/// Canonicalize signed headers passed as headers_list among mail_headers using canonicalization
fn dkim_canonicalize_headers<'a>(
headers_list: impl IntoIterator<Item = &'a str>,
mail_headers: &Headers,
canonicalization: DkimCanonicalizationType,
) -> String {
let mut covered_headers = Headers::new();
for name in headers_list {
if let Some(h) = mail_headers.find_header(name) {
let name = dkim_canonicalize_header_tag(name, canonicalization);
covered_headers.insert_raw(HeaderValue::dangerous_new_pre_encoded(
HeaderName::new_from_ascii(name.into()).unwrap(),
h.get_raw().into(),
h.get_encoded().into(),
));
}
}
let serialized = covered_headers.to_string();
match canonicalization {
DkimCanonicalizationType::Simple => serialized,
DkimCanonicalizationType::Relaxed => dkim_canonicalize_headers_relaxed(&serialized),
}
}
/// Sign with Dkim a message by adding Dkim-Signature header created with configuration expressed by
/// dkim_config
pub fn dkim_sign(message: &mut Message, dkim_config: &DkimConfig) {
dkim_sign_fixed_time(message, dkim_config, SystemTime::now())
}
fn dkim_sign_fixed_time(message: &mut Message, dkim_config: &DkimConfig, timestamp: SystemTime) {
let timestamp = timestamp
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
let headers = message.headers();
let body_hash = Sha256::digest(dkim_canonicalize_body(
&message.body_raw(),
dkim_config.canonicalization.body,
));
let bh = crate::base64::encode(body_hash);
let mut signed_headers_list =
dkim_config
.headers
.iter()
.fold(String::new(), |mut list, header| {
if !list.is_empty() {
list.push(':');
}
list.push_str(header);
list
});
if let DkimCanonicalizationType::Relaxed = dkim_config.canonicalization.header {
signed_headers_list.make_ascii_lowercase();
}
let dkim_header = dkim_header_format(dkim_config, timestamp, &signed_headers_list, &bh, "");
let signed_headers = dkim_canonicalize_headers(
dkim_config.headers.iter().map(|h| h.as_ref()),
headers,
dkim_config.canonicalization.header,
);
let canonicalized_dkim_header = dkim_canonicalize_headers(
["DKIM-Signature"],
&dkim_header,
dkim_config.canonicalization.header,
);
let mut hashed_headers = Sha256::new();
hashed_headers.update(signed_headers.as_bytes());
hashed_headers.update(canonicalized_dkim_header.trim_end().as_bytes());
let hashed_headers = hashed_headers.finalize();
let signature = match &dkim_config.private_key.0 {
InnerDkimSigningKey::Rsa(private_key) => crate::base64::encode(
private_key
.sign(Pkcs1v15Sign::new::<Sha256>(), &hashed_headers)
.unwrap(),
),
InnerDkimSigningKey::Ed25519(private_key) => {
crate::base64::encode(private_key.sign(&hashed_headers).to_bytes())
}
};
let dkim_header = dkim_header_format(
dkim_config,
timestamp,
&signed_headers_list,
&bh,
&signature,
);
message.headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("DKIM-Signature"),
dkim_header.get_raw("DKIM-Signature").unwrap().to_owned(),
));
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::{
super::{
header::{HeaderName, HeaderValue},
Header, Message,
},
dkim_canonicalize_body, dkim_canonicalize_headers, dkim_sign_fixed_time,
DkimCanonicalization, DkimCanonicalizationType, DkimConfig, DkimSigningAlgorithm,
DkimSigningKey,
};
use crate::StdError;
const KEY_RSA: &str = "-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAwOsW7UFcWn1ch3UM8Mll5qZH5hVHKJQ8Z0tUlebUECq0vjw6
VcsIucZ/B70VpCN63whyi7oApdCIS1o0zad7f0UaW/BfxXADqdcFL36uMaG0RHer
uSASjQGnsl9Kozt/dXiDZX5ngjr/arLJhNZSNR4/9VSwqbE2OPXaSaQ9BsqneD0P
8dCVSfkkDZCcfC2864z7hvC01lFzWQKF36ZAoGBERHScHtFMAzUOgGuqqPiP5khw
DQB3Ffccf+BsWLU2OOteshUwTGjpoangbPCYj6kckwNm440lQwuqTinpC92yyIE5
Ol8psNMW49DLowAeZb6JrjLhD+wY9bghTaOkcwIDAQABAoIBAHTZ8LkkrdvhsvoZ
XA088AwVC9fBa6iYoT2v0zw45JomQ/Q2Zt8wa8ibAradQU56byJI65jWwS2ucd+y
c+ldWOBt6tllb50XjCCDrRBnmvtVBuux0MIBOztNlVXlgj/8+ecdZ/lB51Bqi+sF
ACsF5iVmfTcMZTVjsYQu5llUseI6Lwgqpx6ktaXD2PVsVo9Gf01ssZ4GCy69wB/3
20CsOz4LEpSYkq1oE98lMMGCfD7py3L9kWHYNNisam78GM+1ynRxRGwEDUbz6pxs
fGPIAwHLaZsOmibPkBB0PJTW742w86qQ8KAqC6ZbRYOF19rSMj3oTfRnPMHn9Uu5
N8eQcoECgYEA97SMUrz2hqII5i8igKylO9kV8pjcIWKI0rdt8MKj4FXTNYjjO9I+
41ONOjhUOpFci/G3YRKi8UiwbKxIRTvIxNMh2xj6Ws3iO9gQHK1j8xTWxJdjEBEz
EuZI59Mi5H7fxSL1W+n8nS8JVsaH93rvQErngqTUAsihAzjxHWdFwm0CgYEAx2Dh
claESJP2cOKgYp+SUNwc26qMaqnl1f37Yn+AflrQOfgQqJe5TRbicEC+nFlm6XUt
3st1Nj29H0uOMmMZDmDCO+cOs5Qv5A9pG6jSC6wM+2KNHQDtrxlakBFygePEPVVy
GXaY9DRa9Q4/4ataxDR2/VvIAWfEEtMTJIBDtl8CgYAIXEuwLziS6r0qJ8UeWrVp
A7a97XLgnZbIpfBMBAXL+JmcYPZqenos6hEGOgh9wZJCFvJ9kEd3pWBvCpGV5KKu
IgIuhvVMQ06zfmNs1F1fQwDMud9aF3qF1Mf5KyMuWynqWXe2lns0QvYpu6GzNK8G
mICf5DhTr7nfhfh9aZLtMQKBgCxKsmqzG5n//MxhHB4sstVxwJtwDNeZPKzISnM8
PfBT/lQSbqj1Y73japRjXbTgC4Ore3A2JKjTGFN+dm1tJGDUT/H8x4BPWEBCyCfT
3i2noA6sewrJbQPsDvlYVubSEYNKmxlbBmmhw98StlBMv9I8kX6BSDI/uggwid0e
/WvjAoGBAKpZ0UOKQyrl9reBiUfrpRCvIMakBMd79kNiH+5y0Soq/wCAnAuABayj
XEIBhFv+HxeLEnT7YV+Zzqp5L9kKw/EU4ik3JX/XsEihdSxEuGX00ZYOw05FEfpW
cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT
-----END RSA PRIVATE KEY-----";
#[derive(Clone)]
struct TestHeader(String);
impl Header for TestHeader {
fn name() -> HeaderName {
HeaderName::new_from_ascii_str("Test")
}
fn parse(s: &str) -> Result<Self, Box<dyn StdError + Send + Sync>> {
Ok(Self(s.into()))
}
fn display(&self) -> HeaderValue {
HeaderValue::new(Self::name(), self.0.clone())
}
}
fn test_message() -> Message {
Message::builder()
.from("Test O'Leary <test+ezrz@example.net>".parse().unwrap())
.to("Test2 <test2@example.org>".parse().unwrap())
.date(std::time::UNIX_EPOCH)
.header(TestHeader("test test very very long with spaces and extra spaces \twill be folded to several lines ".to_owned()))
.subject("Test with utf-8 ë")
.body("test\r\n\r\ntest \ttest\r\n\r\n\r\n".to_owned()).unwrap()
}
#[test]
fn test_headers_simple_canonicalize() {
let message = test_message();
dbg!(message.headers.to_string());
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Simple), "From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\nTest: test test very very long with spaces and extra spaces \twill be\r\n folded to several lines \r\n")
}
#[test]
fn test_headers_relaxed_canonicalize() {
let message = test_message();
dbg!(message.headers.to_string());
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Relaxed),"from:=?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\ntest:test test very very long with spaces and extra spaces will be folded to several lines\r\n")
}
#[test]
fn test_body_simple_canonicalize() {
let body = b" C \r\nD \t E\r\n\r\n\r\n";
assert_eq!(
dkim_canonicalize_body(body, DkimCanonicalizationType::Simple).into_owned(),
b" C \r\nD \t E\r\n"
);
}
#[test]
fn test_body_relaxed_canonicalize() {
let body = b" C \r\nD \t E\r\n\tF\r\n\t\r\n\r\n\r\n";
assert_eq!(
dkim_canonicalize_body(body, DkimCanonicalizationType::Relaxed).into_owned(),
b" C\r\nD E\r\n F\r\n"
);
}
#[test]
fn test_signature_rsa_simple() {
let mut message = test_message();
let signing_key = DkimSigningKey::new(KEY_RSA, DkimSigningAlgorithm::Rsa).unwrap();
dkim_sign_fixed_time(
&mut message,
&DkimConfig::new(
"dkimtest".to_owned(),
"example.org".to_owned(),
signing_key,
vec![
HeaderName::new_from_ascii_str("Date"),
HeaderName::new_from_ascii_str("From"),
HeaderName::new_from_ascii_str("Subject"),
HeaderName::new_from_ascii_str("To"),
],
DkimCanonicalization {
header: DkimCanonicalizationType::Simple,
body: DkimCanonicalizationType::Simple,
},
),
std::time::UNIX_EPOCH,
);
let signed = message.formatted();
let signed = std::str::from_utf8(&signed).unwrap();
assert_eq!(
signed,
std::concat!(
"From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\n",
"To: Test2 <test2@example.org>\r\n",
"Date: Thu, 01 Jan 1970 00:00:00 +0000\r\n",
"Test: test test very very long with spaces and extra spaces \twill be\r\n",
" folded to several lines \r\n",
"Subject: Test with utf-8 =?utf-8?b?w6s=?=\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"DKIM-Signature: v=1; a=rsa-sha256; d=example.org; s=dkimtest;\r\n",
" c=simple/simple; q=dns/txt; t=0; h=Date:From:Subject:To;\r\n",
" bh=f3Zksdcjqa/xRBwdyFzIXWCcgP7XTgxjCgYsXOMKQl4=;\r\n",
" b=NhoIMMAALoSgu5lKAR0+MUQunOWnU7wpF9ORUFtpxq9sGZDo9AX43AMhFemyM5W204jpFwMU6pm7AMR1nOYBdSYye4yUALtvT2nqbJBwSh7JeYu+z22t1RFKp7qQR1il8aSrkbZuNMFHYuSEwW76QtKwcNqP4bQOzS9CzgQp0ABu8qwYPBr/EypykPTfqjtyN+ywrfdqjjGOzTpRGolH0hc3CrAETNjjHbNBgKgucXmXTN7hMRdzqWjeFPxizXwouwNAavFClPG0l33gXVArFWn+CkgA84G/s4zuJiF7QPZR87Pu4pw/vIlSXxH4a42W3tT19v9iBTH7X7ldYegtmQ==\r\n",
"\r\n",
"test\r\n",
"\r\n",
"test \ttest\r\n",
"\r\n",
"\r\n",
)
);
}
#[test]
fn test_signature_rsa_relaxed() {
let mut message = test_message();
let signing_key = DkimSigningKey::new(KEY_RSA, DkimSigningAlgorithm::Rsa).unwrap();
dkim_sign_fixed_time(
&mut message,
&DkimConfig::new(
"dkimtest".to_owned(),
"example.org".to_owned(),
signing_key,
vec![
HeaderName::new_from_ascii_str("Date"),
HeaderName::new_from_ascii_str("From"),
HeaderName::new_from_ascii_str("Subject"),
HeaderName::new_from_ascii_str("To"),
],
DkimCanonicalization {
header: DkimCanonicalizationType::Relaxed,
body: DkimCanonicalizationType::Relaxed,
},
),
std::time::UNIX_EPOCH,
);
let signed = message.formatted();
let signed = std::str::from_utf8(&signed).unwrap();
println!("{signed}");
assert_eq!(
signed,
std::concat!(
"From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\n",
"To: Test2 <test2@example.org>\r\n",
"Date: Thu, 01 Jan 1970 00:00:00 +0000\r\n",
"Test: test test very very long with spaces and extra spaces \twill be\r\n",
" folded to several lines \r\n","Subject: Test with utf-8 =?utf-8?b?w6s=?=\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"DKIM-Signature: v=1; a=rsa-sha256; d=example.org; s=dkimtest;\r\n",
" c=relaxed/relaxed; q=dns/txt; t=0; h=date:from:subject:to;\r\n",
" bh=qN8je6qJgWFGSnN2MycC/XKPbN6BOrMJyAX2h4m19Ss=;\r\n",
" b=YaVfmH8dbGEywoLJ4uhbvYqDyQG1UGKFH3PE7zXGgk+YFxUgkwWjoA3aQupDNQtfTjfUsNe0dnrjyZP+ylnESpZBpbCIf5/n3FEh6j3RQthqNbQblcfH/U8mazTuRbVjYBbTZQDaQCMPTz+8D+ZQfXo2oq6dGzTuGvmuYft0CVsq/BIp/EkhZHqiphDeVJSHD4iKW8+L2XwEWThoY92xOYc1G0TtBwz2UJgtiHX2YulH/kRBHeK3dKn9RTNVL3VZ+9ZrnFwIhET9TPGtU2I+q0EMSWF9H9bTrASMgW/U+E0VM2btqJlrTU6rQ7wlQeHdwecLnzXcyhCUInF1+veMNw==\r\n",
"\r\n",
"test\r\n",
"\r\n",
"test \ttest\r\n",
"\r\n",
"\r\n",
)
);
}
}

View File

@@ -1,116 +0,0 @@
use std::{
fmt::{Display, Formatter as FmtFormatter, Result as FmtResult},
str::FromStr,
};
use super::{Header, HeaderName, HeaderValue};
use crate::BoxError;
/// `Content-Transfer-Encoding` of the body
///
/// The `Message` builder takes care of choosing the most
/// efficient encoding based on the chosen body, so in most
/// use-caches this header shouldn't be set manually.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Default)]
pub enum ContentTransferEncoding {
/// ASCII
SevenBit,
/// Quoted-Printable encoding
QuotedPrintable,
/// base64 encoding
#[default]
Base64,
/// Requires `8BITMIME`
EightBit,
/// Binary data
Binary,
}
impl Header for ContentTransferEncoding {
fn name() -> HeaderName {
HeaderName::new_from_ascii_str("Content-Transfer-Encoding")
}
fn parse(s: &str) -> Result<Self, BoxError> {
Ok(s.parse()?)
}
fn display(&self) -> HeaderValue {
let val = self.to_string();
HeaderValue::dangerous_new_pre_encoded(Self::name(), val.clone(), val)
}
}
impl Display for ContentTransferEncoding {
fn fmt(&self, f: &mut FmtFormatter<'_>) -> FmtResult {
f.write_str(match *self {
Self::SevenBit => "7bit",
Self::QuotedPrintable => "quoted-printable",
Self::Base64 => "base64",
Self::EightBit => "8bit",
Self::Binary => "binary",
})
}
}
impl FromStr for ContentTransferEncoding {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"7bit" => Ok(Self::SevenBit),
"quoted-printable" => Ok(Self::QuotedPrintable),
"base64" => Ok(Self::Base64),
"8bit" => Ok(Self::EightBit),
"binary" => Ok(Self::Binary),
_ => Err(s.into()),
}
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::ContentTransferEncoding;
use crate::message::header::{HeaderName, HeaderValue, Headers};
#[test]
fn format_content_transfer_encoding() {
let mut headers = Headers::new();
headers.set(ContentTransferEncoding::SevenBit);
assert_eq!(headers.to_string(), "Content-Transfer-Encoding: 7bit\r\n");
headers.set(ContentTransferEncoding::Base64);
assert_eq!(headers.to_string(), "Content-Transfer-Encoding: base64\r\n");
}
#[test]
fn parse_content_transfer_encoding() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
"7bit".to_owned(),
));
assert_eq!(
headers.get::<ContentTransferEncoding>(),
Some(ContentTransferEncoding::SevenBit)
);
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
"base64".to_owned(),
));
assert_eq!(
headers.get::<ContentTransferEncoding>(),
Some(ContentTransferEncoding::Base64)
);
}
}

View File

@@ -1,127 +0,0 @@
use std::fmt::Write;
use email_encoding::headers::writer::EmailWriter;
use super::{Header, HeaderName, HeaderValue};
use crate::BoxError;
/// `Content-Disposition` of an attachment
///
/// Defined in [RFC2183](https://tools.ietf.org/html/rfc2183)
#[derive(Debug, Clone, PartialEq)]
pub struct ContentDisposition(HeaderValue);
impl ContentDisposition {
/// An attachment which should be displayed inline into the message
pub fn inline() -> Self {
Self(HeaderValue::dangerous_new_pre_encoded(
Self::name(),
"inline".to_owned(),
"inline".to_owned(),
))
}
/// An attachment which should be displayed inline into the message, but that also
/// species the filename in case it is downloaded
pub fn inline_with_name(file_name: &str) -> Self {
Self::with_name("inline", file_name)
}
/// An attachment which is separate from the body of the message, and can be downloaded separately
pub fn attachment(file_name: &str) -> Self {
Self::with_name("attachment", file_name)
}
fn with_name(kind: &str, file_name: &str) -> Self {
let raw_value = format!("{kind}; filename=\"{file_name}\"");
let mut encoded_value = String::new();
let line_len = "Content-Disposition: ".len();
{
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false);
w.write_str(kind).expect("writing `kind` returned an error");
w.write_char(';').expect("writing `;` returned an error");
w.space();
email_encoding::headers::rfc2231::encode("filename", file_name, &mut w)
.expect("some Write implementation returned an error");
}
Self(HeaderValue::dangerous_new_pre_encoded(
Self::name(),
raw_value,
encoded_value,
))
}
}
impl Header for ContentDisposition {
fn name() -> HeaderName {
HeaderName::new_from_ascii_str("Content-Disposition")
}
fn parse(s: &str) -> Result<Self, BoxError> {
match (s.split_once(';'), s) {
(_, "inline") => Ok(Self::inline()),
(Some((kind @ ("inline" | "attachment"), file_name)), _) => file_name
.split_once(" filename=\"")
.and_then(|(_, file_name)| file_name.strip_suffix('"'))
.map(|file_name| Self::with_name(kind, file_name))
.ok_or_else(|| "Unsupported ContentDisposition value".into()),
_ => Err("Unsupported ContentDisposition value".into()),
}
}
fn display(&self) -> HeaderValue {
self.0.clone()
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::ContentDisposition;
use crate::message::header::{HeaderName, HeaderValue, Headers};
#[test]
fn format_content_disposition() {
let mut headers = Headers::new();
headers.set(ContentDisposition::inline());
assert_eq!(format!("{headers}"), "Content-Disposition: inline\r\n");
headers.set(ContentDisposition::attachment("something.txt"));
assert_eq!(
format!("{headers}"),
"Content-Disposition: attachment; filename=\"something.txt\"\r\n"
);
}
#[test]
fn parse_content_disposition() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Disposition"),
"inline".to_owned(),
));
assert_eq!(
headers.get::<ContentDisposition>(),
Some(ContentDisposition::inline())
);
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Disposition"),
"attachment; filename=\"something.txt\"".to_owned(),
));
assert_eq!(
headers.get::<ContentDisposition>(),
Some(ContentDisposition::attachment("something.txt"))
);
}
}

View File

@@ -1,193 +0,0 @@
use std::{
error::Error as StdError,
fmt::{self, Display},
str::FromStr,
};
use mime::Mime;
use super::{Header, HeaderName, HeaderValue};
use crate::BoxError;
/// `Content-Type` of the body
///
/// This struct can represent any valid [MIME type], which can be parsed via
/// [`ContentType::parse`]. Constants are provided for the most-used mime-types.
///
/// Defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-5)
///
/// [MIME type]: https://www.iana.org/assignments/media-types/media-types.xhtml
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContentType(Mime);
impl ContentType {
/// A `ContentType` of type `text/plain; charset=utf-8`
///
/// Indicates that the body is in utf-8 encoded plain text.
pub const TEXT_PLAIN: ContentType = Self::from_mime(mime::TEXT_PLAIN_UTF_8);
/// A `ContentType` of type `text/html; charset=utf-8`
///
/// Indicates that the body is in utf-8 encoded html.
pub const TEXT_HTML: ContentType = Self::from_mime(mime::TEXT_HTML_UTF_8);
/// Parse `s` into `ContentType`
pub fn parse(s: &str) -> Result<ContentType, ContentTypeErr> {
Ok(Self::from_mime(s.parse().map_err(ContentTypeErr)?))
}
pub(crate) const fn from_mime(mime: Mime) -> Self {
Self(mime)
}
pub(crate) fn as_ref(&self) -> &Mime {
&self.0
}
}
impl Header for ContentType {
fn name() -> HeaderName {
HeaderName::new_from_ascii_str("Content-Type")
}
fn parse(s: &str) -> Result<Self, BoxError> {
Ok(Self(s.parse()?))
}
fn display(&self) -> HeaderValue {
HeaderValue::new(Self::name(), self.0.to_string())
}
}
impl FromStr for ContentType {
type Err = ContentTypeErr;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
#[cfg(feature = "mime03")]
#[cfg_attr(docsrs, doc(cfg(feature = "mime03")))]
impl From<Mime> for ContentType {
fn from(mime: Mime) -> Self {
Self::from_mime(mime)
}
}
/// An error occurred while trying to [`ContentType::parse`].
#[derive(Debug)]
pub struct ContentTypeErr(mime::FromStrError);
impl StdError for ContentTypeErr {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(&self.0)
}
}
impl Display for ContentTypeErr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
// -- Serialization and Deserialization --
#[cfg(feature = "serde")]
mod serde {
use std::fmt;
use serde::{
de::{self, Deserialize, Deserializer, Visitor},
ser::{Serialize, Serializer},
};
use super::ContentType;
impl Serialize for ContentType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_newtype_struct("ContentType", &format!("{}", &self.0))
}
}
impl<'de> Deserialize<'de> for ContentType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct ContentTypeVisitor;
impl<'de> Visitor<'de> for ContentTypeVisitor {
type Value = ContentType;
// The error message which states what the Visitor expects to
// receive
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a ContentType string like `text/plain`")
}
fn visit_str<E>(self, mime: &str) -> Result<ContentType, E>
where
E: de::Error,
{
match ContentType::parse(mime) {
Ok(content_type) => Ok(content_type),
Err(_) => Err(E::custom(format!(
"Couldn't parse the following MIME-Type: {mime}"
))),
}
}
}
deserializer.deserialize_str(ContentTypeVisitor)
}
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::ContentType;
use crate::message::header::{HeaderName, HeaderValue, Headers};
#[test]
fn format_content_type() {
let mut headers = Headers::new();
headers.set(ContentType::TEXT_PLAIN);
assert_eq!(
headers.to_string(),
"Content-Type: text/plain; charset=utf-8\r\n"
);
headers.set(ContentType::TEXT_HTML);
assert_eq!(
headers.to_string(),
"Content-Type: text/html; charset=utf-8\r\n"
);
}
#[test]
fn parse_content_type() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Type"),
"text/plain; charset=utf-8".to_owned(),
));
assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_PLAIN));
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Type"),
"text/html; charset=utf-8".to_owned(),
));
assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_HTML));
}
}

View File

@@ -1,135 +0,0 @@
use std::time::SystemTime;
use httpdate::HttpDate;
use super::{Header, HeaderName, HeaderValue};
use crate::BoxError;
/// Message `Date` header
///
/// Defined in [RFC2822](https://tools.ietf.org/html/rfc2822#section-3.3)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Date(HttpDate);
impl Date {
/// Build a `Date` from [`SystemTime`]
pub fn new(st: SystemTime) -> Self {
Self(st.into())
}
/// Get the current date
///
/// Shortcut for `Date::new(SystemTime::now())`
pub fn now() -> Self {
Self::new(SystemTime::now())
}
}
impl Header for Date {
fn name() -> HeaderName {
HeaderName::new_from_ascii_str("Date")
}
fn parse(s: &str) -> Result<Self, BoxError> {
let mut s = String::from(s);
if s.ends_with("+0000") {
// The httpdate crate expects the `Date` to end in ` GMT`, but email
// uses `+0000` to indicate UTC, so we crudely fix this issue here.
s.truncate(s.len() - "+0000".len());
s.push_str("GMT");
}
Ok(Self(s.parse::<HttpDate>()?))
}
fn display(&self) -> HeaderValue {
let mut val = self.0.to_string();
if val.ends_with(" GMT") {
// The httpdate crate always appends ` GMT` to the end of the string,
// but this is considered an obsolete date format for email
// https://tools.ietf.org/html/rfc2822#appendix-A.6.2,
// so we replace `GMT` with `+0000`
val.truncate(val.len() - "GMT".len());
val.push_str("+0000");
}
HeaderValue::dangerous_new_pre_encoded(Self::name(), val.clone(), val)
}
}
impl From<SystemTime> for Date {
fn from(st: SystemTime) -> Self {
Self::new(st)
}
}
impl From<Date> for SystemTime {
fn from(this: Date) -> SystemTime {
this.0.into()
}
}
#[cfg(test)]
mod test {
use std::time::{Duration, SystemTime};
use pretty_assertions::assert_eq;
use super::Date;
use crate::message::header::{HeaderName, HeaderValue, Headers};
#[test]
fn format_date() {
let mut headers = Headers::new();
// Tue, 15 Nov 1994 08:12:31 GMT
headers.set(Date::from(
SystemTime::UNIX_EPOCH + Duration::from_secs(784887151),
));
assert_eq!(
headers.to_string(),
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n".to_owned()
);
// Tue, 15 Nov 1994 08:12:32 GMT
headers.set(Date::from(
SystemTime::UNIX_EPOCH + Duration::from_secs(784887152),
));
assert_eq!(
headers.to_string(),
"Date: Tue, 15 Nov 1994 08:12:32 +0000\r\n"
);
}
#[test]
fn parse_date() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Date"),
"Tue, 15 Nov 1994 08:12:31 +0000".to_owned(),
));
assert_eq!(
headers.get::<Date>(),
Some(Date::from(
SystemTime::UNIX_EPOCH + Duration::from_secs(784887151),
))
);
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Date"),
"Tue, 15 Nov 1994 08:12:32 +0000".to_owned(),
));
assert_eq!(
headers.get::<Date>(),
Some(Date::from(
SystemTime::UNIX_EPOCH + Duration::from_secs(784887152),
))
);
}
}

View File

@@ -1,359 +0,0 @@
use email_encoding::headers::writer::EmailWriter;
use super::{Header, HeaderName, HeaderValue};
use crate::{
message::mailbox::{Mailbox, Mailboxes},
BoxError,
};
/// Header which can contains multiple mailboxes
pub trait MailboxesHeader {
fn join_mailboxes(&mut self, other: Self);
}
macro_rules! mailbox_header {
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
$(#[$doc])*
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct $type_name(Mailbox);
impl Header for $type_name {
fn name() -> HeaderName {
HeaderName::new_from_ascii_str($header_name)
}
fn parse(s: &str) -> Result<Self, BoxError> {
let mailbox: Mailbox = s.parse()?;
Ok(Self(mailbox))
}
fn display(&self) -> HeaderValue {
let mut encoded_value = String::new();
let line_len = $header_name.len() + ": ".len();
{
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false);
self.0.encode(&mut w).expect("writing `Mailbox` returned an error");
}
HeaderValue::dangerous_new_pre_encoded(Self::name(), self.0.to_string(), encoded_value)
}
}
impl std::convert::From<Mailbox> for $type_name {
#[inline]
fn from(mailbox: Mailbox) -> Self {
Self(mailbox)
}
}
impl std::convert::From<$type_name> for Mailbox {
#[inline]
fn from(this: $type_name) -> Mailbox {
this.0
}
}
};
}
macro_rules! mailboxes_header {
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
$(#[$doc])*
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct $type_name(pub(crate) Mailboxes);
impl MailboxesHeader for $type_name {
fn join_mailboxes(&mut self, other: Self) {
self.0.extend(other.0);
}
}
impl Header for $type_name {
fn name() -> HeaderName {
HeaderName::new_from_ascii_str($header_name)
}
fn parse(s: &str) -> Result<Self, BoxError> {
let mailbox: Mailboxes = s.parse()?;
Ok(Self(mailbox))
}
fn display(&self) -> HeaderValue {
let mut encoded_value = String::new();
let line_len = $header_name.len() + ": ".len();
{
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false);
self.0.encode(&mut w).expect("writing `Mailboxes` returned an error");
}
HeaderValue::dangerous_new_pre_encoded(Self::name(), self.0.to_string(), encoded_value)
}
}
impl std::convert::From<Mailboxes> for $type_name {
#[inline]
fn from(mailboxes: Mailboxes) -> Self {
Self(mailboxes)
}
}
impl std::convert::From<$type_name> for Mailboxes {
#[inline]
fn from(this: $type_name) -> Mailboxes {
this.0
}
}
};
}
mailbox_header! {
/**
`Sender` header
This header contains [`Mailbox`][self::Mailbox] associated with sender.
```no_test
header::Sender("Mr. Sender <sender@example.com>".parse().unwrap())
```
*/
(Sender, "Sender")
}
mailboxes_header! {
/**
`From` header
This header contains [`Mailboxes`][self::Mailboxes].
*/
(From, "From")
}
mailboxes_header! {
/**
`Reply-To` header
This header contains [`Mailboxes`][self::Mailboxes].
*/
(ReplyTo, "Reply-To")
}
mailboxes_header! {
/**
`To` header
This header contains [`Mailboxes`][self::Mailboxes].
*/
(To, "To")
}
mailboxes_header! {
/**
`Cc` header
This header contains [`Mailboxes`][self::Mailboxes].
*/
(Cc, "Cc")
}
mailboxes_header! {
/**
`Bcc` header
This header contains [`Mailboxes`][self::Mailboxes].
*/
(Bcc, "Bcc")
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::{From, Mailbox, Mailboxes};
use crate::message::header::{HeaderName, HeaderValue, Headers};
#[test]
fn format_single_without_name() {
let from = Mailboxes::new().with("kayo@example.com".parse().unwrap());
let mut headers = Headers::new();
headers.set(From(from));
assert_eq!(headers.to_string(), "From: kayo@example.com\r\n");
}
#[test]
fn format_single_with_name() {
let from = Mailboxes::new().with("Kayo <kayo@example.com>".parse().unwrap());
let mut headers = Headers::new();
headers.set(From(from));
assert_eq!(headers.to_string(), "From: Kayo <kayo@example.com>\r\n");
}
#[test]
fn format_multi_without_name() {
let from = Mailboxes::new()
.with("kayo@example.com".parse().unwrap())
.with("pony@domain.tld".parse().unwrap());
let mut headers = Headers::new();
headers.set(From(from));
assert_eq!(
headers.to_string(),
"From: kayo@example.com, pony@domain.tld\r\n"
);
}
#[test]
fn format_multi_with_name() {
let from = vec![
"Kayo <kayo@example.com>".parse().unwrap(),
"Pony P. <pony@domain.tld>".parse().unwrap(),
];
let mut headers = Headers::new();
headers.set(From(from.into()));
assert_eq!(
headers.to_string(),
"From: Kayo <kayo@example.com>, \"Pony P.\" <pony@domain.tld>\r\n"
);
}
#[test]
fn format_single_with_utf8_name() {
let from = vec!["Кайо <kayo@example.com>".parse().unwrap()];
let mut headers = Headers::new();
headers.set(From(from.into()));
assert_eq!(
headers.to_string(),
"From: =?utf-8?b?0JrQsNC50L4=?= <kayo@example.com>\r\n"
);
}
#[test]
fn parse_single_without_name() {
let from = vec!["kayo@example.com".parse().unwrap()].into();
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"kayo@example.com".to_owned(),
));
assert_eq!(headers.get::<From>(), Some(From(from)));
}
#[test]
fn parse_single_with_name() {
let from = vec!["K. <kayo@example.com>".parse().unwrap()].into();
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"K. <kayo@example.com>".to_owned(),
));
assert_eq!(headers.get::<From>(), Some(From(from)));
}
#[test]
fn parse_multi_without_name() {
let from: Vec<Mailbox> = vec![
"kayo@example.com".parse().unwrap(),
"pony@domain.tld".parse().unwrap(),
];
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"kayo@example.com, pony@domain.tld".to_owned(),
));
assert_eq!(headers.get::<From>(), Some(From(from.into())));
}
#[test]
fn parse_multi_with_name() {
let from: Vec<Mailbox> = vec![
"K. <kayo@example.com>".parse().unwrap(),
"Pony P. <pony@domain.tld>".parse().unwrap(),
];
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"K. <kayo@example.com>, Pony P. <pony@domain.tld>".to_owned(),
));
assert_eq!(headers.get::<From>(), Some(From(from.into())));
}
#[test]
fn parse_multi_with_name_containing_comma() {
let from: Vec<Mailbox> = vec![
"\"Test, test\" <1@example.com>".parse().unwrap(),
"\"Test2, test2\" <2@example.com>".parse().unwrap(),
];
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"\"Test, test\" <1@example.com>, \"Test2, test2\" <2@example.com>".to_owned(),
));
assert_eq!(headers.get::<From>(), Some(From(from.into())));
}
#[test]
fn parse_multi_with_name_containing_double_quotes() {
let from: Vec<Mailbox> = vec![
"\"Test, test\" <1@example.com>".parse().unwrap(),
"\"Test2, \"test2\"\" <2@example.com>".parse().unwrap(),
];
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"\"Test, test\" <1@example.com>, \"Test2, \"test2\"\" <2@example.com>".to_owned(),
));
assert_eq!(headers.get::<From>(), Some(From(from.into())));
}
#[test]
fn parse_multi_with_name_containing_comma_last_broken() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"\"Test, test\" <1@example.com>, \"Test2, test2\"".to_owned(),
));
assert_eq!(headers.get::<From>(), None);
}
#[test]
fn mailbox_format_address_with_angle_bracket() {
assert_eq!(
format!(
"{}",
Mailbox::new(Some("<3".into()), "i@love.example".parse().unwrap())
),
r#""<3" <i@love.example>"#
);
}
}

View File

@@ -1,754 +0,0 @@
//! Headers widely used in email messages
use std::{
borrow::Cow,
error::Error,
fmt::{self, Display, Formatter, Write},
ops::Deref,
};
use email_encoding::headers::writer::EmailWriter;
pub use self::{
content::*,
content_disposition::ContentDisposition,
content_type::{ContentType, ContentTypeErr},
date::Date,
mailbox::*,
special::*,
textual::*,
};
use crate::BoxError;
mod content;
mod content_disposition;
mod content_type;
mod date;
mod mailbox;
mod special;
mod textual;
/// Represents an email header
///
/// Email header as defined in [RFC5322](https://datatracker.ietf.org/doc/html/rfc5322) and extensions.
pub trait Header: Clone {
fn name() -> HeaderName;
fn parse(s: &str) -> Result<Self, BoxError>;
fn display(&self) -> HeaderValue;
}
/// A set of email headers
#[derive(Debug, Clone, Default)]
pub struct Headers {
headers: Vec<HeaderValue>,
}
impl Headers {
/// Create an empty `Headers`
///
/// This function does not allocate.
#[inline]
pub const fn new() -> Self {
Self {
headers: Vec::new(),
}
}
/// Create an empty `Headers` with a pre-allocated capacity
///
/// Pre-allocates a capacity of at least `capacity`.
#[inline]
pub fn with_capacity(capacity: usize) -> Self {
Self {
headers: Vec::with_capacity(capacity),
}
}
/// Returns a copy of a `Header` present in `Headers`
///
/// Returns `None` if `Header` isn't present in `Headers`.
pub fn get<H: Header>(&self) -> Option<H> {
self.get_raw(&H::name())
.and_then(|raw_value| H::parse(raw_value).ok())
}
/// Sets `Header` into `Headers`, overriding `Header` if it
/// was already present in `Headers`
pub fn set<H: Header>(&mut self, header: H) {
self.insert_raw(header.display());
}
/// Remove `Header` from `Headers`, returning it
///
/// Returns `None` if `Header` isn't in `Headers`.
pub fn remove<H: Header>(&mut self) -> Option<H> {
self.remove_raw(&H::name())
.and_then(|value| H::parse(&value.raw_value).ok())
}
/// Clears `Headers`, removing all headers from it
///
/// Any pre-allocated capacity is left untouched.
#[inline]
pub fn clear(&mut self) {
self.headers.clear();
}
/// Returns a reference to the raw value of header `name`
///
/// Returns `None` if `name` isn't present in `Headers`.
pub fn get_raw(&self, name: &str) -> Option<&str> {
self.find_header(name).map(|value| value.raw_value.as_str())
}
/// Inserts a raw header into `Headers`, overriding `value` if it
/// was already present in `Headers`.
pub fn insert_raw(&mut self, value: HeaderValue) {
match self.find_header_mut(&value.name) {
Some(current_value) => {
*current_value = value;
}
None => {
self.headers.push(value);
}
}
}
/// Remove a raw header from `Headers`, returning it
///
/// Returns `None` if `name` isn't present in `Headers`.
pub fn remove_raw(&mut self, name: &str) -> Option<HeaderValue> {
self.find_header_index(name).map(|i| self.headers.remove(i))
}
pub(crate) fn find_header(&self, name: &str) -> Option<&HeaderValue> {
self.headers.iter().find(|value| name == value.name)
}
fn find_header_mut(&mut self, name: &str) -> Option<&mut HeaderValue> {
self.headers.iter_mut().find(|value| name == value.name)
}
fn find_header_index(&self, name: &str) -> Option<usize> {
self.headers
.iter()
.enumerate()
.find(|(_i, value)| name == value.name)
.map(|(i, _)| i)
}
}
impl Display for Headers {
/// Formats `Headers`, ready to put them into an email
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for value in &self.headers {
f.write_str(&value.name)?;
f.write_str(": ")?;
f.write_str(&value.encoded_value)?;
f.write_str("\r\n")?;
}
Ok(())
}
}
/// A possible error when converting a `HeaderName` from another type.
// comes from `http` crate
#[allow(missing_copy_implementations)]
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct InvalidHeaderName;
impl fmt::Display for InvalidHeaderName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("invalid header name")
}
}
impl Error for InvalidHeaderName {}
/// A valid header name
#[derive(Debug, Clone)]
pub struct HeaderName(Cow<'static, str>);
impl HeaderName {
/// Creates a new header name
pub fn new_from_ascii(ascii: String) -> Result<Self, InvalidHeaderName> {
if !ascii.is_empty() && ascii.len() <= 76 && ascii.is_ascii() && !ascii.contains([':', ' '])
{
Ok(Self(Cow::Owned(ascii)))
} else {
Err(InvalidHeaderName)
}
}
/// Creates a new header name, panics on invalid name
pub const fn new_from_ascii_str(ascii: &'static str) -> Self {
macro_rules! static_assert {
($condition:expr) => {
let _ = [()][(!($condition)) as usize];
};
}
static_assert!(!ascii.is_empty());
static_assert!(ascii.len() <= 76);
let bytes = ascii.as_bytes();
let mut i = 0;
while i < bytes.len() {
static_assert!(bytes[i].is_ascii());
static_assert!(bytes[i] != b' ');
static_assert!(bytes[i] != b':');
i += 1;
}
Self(Cow::Borrowed(ascii))
}
}
impl Display for HeaderName {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self)
}
}
impl Deref for HeaderName {
type Target = str;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<[u8]> for HeaderName {
#[inline]
fn as_ref(&self) -> &[u8] {
let s: &str = self.as_ref();
s.as_bytes()
}
}
impl AsRef<str> for HeaderName {
#[inline]
fn as_ref(&self) -> &str {
&self.0
}
}
impl PartialEq<HeaderName> for HeaderName {
fn eq(&self, other: &HeaderName) -> bool {
self.eq_ignore_ascii_case(other)
}
}
impl PartialEq<&str> for HeaderName {
fn eq(&self, other: &&str) -> bool {
self.eq_ignore_ascii_case(other)
}
}
impl PartialEq<HeaderName> for &str {
fn eq(&self, other: &HeaderName) -> bool {
self.eq_ignore_ascii_case(other)
}
}
/// A safe for use header value
#[derive(Debug, Clone, PartialEq)]
pub struct HeaderValue {
name: HeaderName,
raw_value: String,
encoded_value: String,
}
impl HeaderValue {
/// Construct a new `HeaderValue` and encode it
///
/// Takes the header `name` and the `raw_value` and encodes
/// it via `RFC2047` and line folds it.
///
/// [`RFC2047`]: https://datatracker.ietf.org/doc/html/rfc2047
pub fn new(name: HeaderName, raw_value: String) -> Self {
let mut encoded_value = String::with_capacity(raw_value.len());
HeaderValueEncoder::encode(&name, &raw_value, &mut encoded_value).unwrap();
Self {
name,
raw_value,
encoded_value,
}
}
/// Construct a new `HeaderValue` using a pre-encoded header value
///
/// This method is _extremely_ dangerous as it opens up
/// the encoder to header injection attacks, but is sometimes
/// acceptable for use if `encoded_value` contains only ascii
/// printable characters and is already line folded.
///
/// When in doubt, use [`HeaderValue::new`].
pub fn dangerous_new_pre_encoded(
name: HeaderName,
raw_value: String,
encoded_value: String,
) -> Self {
Self {
name,
raw_value,
encoded_value,
}
}
#[cfg(feature = "dkim")]
pub(crate) fn get_raw(&self) -> &str {
&self.raw_value
}
#[cfg(feature = "dkim")]
pub(crate) fn get_encoded(&self) -> &str {
&self.encoded_value
}
}
/// [RFC 1522](https://tools.ietf.org/html/rfc1522) header value encoder
struct HeaderValueEncoder<'a> {
writer: EmailWriter<'a>,
encode_buf: String,
}
impl<'a> HeaderValueEncoder<'a> {
fn encode(name: &str, value: &'a str, f: &'a mut impl fmt::Write) -> fmt::Result {
let encoder = Self::new(name, f);
encoder.format(value.split_inclusive(' '))
}
fn new(name: &str, writer: &'a mut dyn Write) -> Self {
let line_len = name.len() + ": ".len();
let writer = EmailWriter::new(writer, line_len, 0, false);
Self {
writer,
encode_buf: String::new(),
}
}
fn format(mut self, words_iter: impl Iterator<Item = &'a str>) -> fmt::Result {
for next_word in words_iter {
let allowed = allowed_str(next_word);
if allowed {
// This word only contains allowed characters
// the next word is allowed, but we may have accumulated some words to encode
self.flush_encode_buf()?;
self.writer.folding().write_str(next_word)?;
} else {
// This word contains unallowed characters
self.encode_buf.push_str(next_word);
}
}
self.flush_encode_buf()?;
Ok(())
}
fn flush_encode_buf(&mut self) -> fmt::Result {
if self.encode_buf.is_empty() {
// nothing to encode
return Ok(());
}
let prefix = self.encode_buf.trim_end_matches(' ');
email_encoding::headers::rfc2047::encode(prefix, &mut self.writer)?;
// TODO: add a better API for doing this in email-encoding
let spaces = self.encode_buf.len() - prefix.len();
for _ in 0..spaces {
self.writer.space();
}
self.encode_buf.clear();
Ok(())
}
}
fn allowed_str(s: &str) -> bool {
s.bytes().all(allowed_char)
}
const fn allowed_char(c: u8) -> bool {
c >= 1 && c <= 9 || c == 11 || c == 12 || c >= 14 && c <= 127
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::{HeaderName, HeaderValue, Headers, To};
use crate::message::Mailboxes;
#[test]
fn valid_headername() {
assert!(HeaderName::new_from_ascii(String::from("From")).is_ok());
}
#[test]
fn non_ascii_headername() {
assert!(HeaderName::new_from_ascii(String::from("🌎")).is_err());
}
#[test]
fn spaces_in_headername() {
assert!(HeaderName::new_from_ascii(String::from("From ")).is_err());
}
#[test]
fn colons_in_headername() {
assert!(HeaderName::new_from_ascii(String::from("From:")).is_err());
}
#[test]
fn empty_headername() {
assert!(HeaderName::new_from_ascii(String::from("")).is_err());
}
#[test]
fn const_valid_headername() {
let _ = HeaderName::new_from_ascii_str("From");
}
#[test]
#[should_panic]
fn const_non_ascii_headername() {
let _ = HeaderName::new_from_ascii_str("🌎");
}
#[test]
#[should_panic]
fn const_spaces_in_headername() {
let _ = HeaderName::new_from_ascii_str("From ");
}
#[test]
#[should_panic]
fn const_colons_in_headername() {
let _ = HeaderName::new_from_ascii_str("From:");
}
#[test]
#[should_panic]
fn const_empty_headername() {
let _ = HeaderName::new_from_ascii_str("");
}
#[test]
fn headername_headername_eq() {
assert_eq!(
HeaderName::new_from_ascii_str("From"),
HeaderName::new_from_ascii_str("From")
);
}
#[test]
fn headername_str_eq() {
assert_eq!(HeaderName::new_from_ascii_str("From"), "From");
}
#[test]
fn str_headername_eq() {
assert_eq!("From", HeaderName::new_from_ascii_str("From"));
}
#[test]
fn headername_headername_eq_case_insensitive() {
assert_eq!(
HeaderName::new_from_ascii_str("From"),
HeaderName::new_from_ascii_str("from")
);
}
#[test]
fn headername_str_eq_case_insensitive() {
assert_eq!(HeaderName::new_from_ascii_str("From"), "from");
}
#[test]
fn str_headername_eq_case_insensitive() {
assert_eq!("from", HeaderName::new_from_ascii_str("From"));
}
#[test]
fn headername_headername_ne() {
assert_ne!(
HeaderName::new_from_ascii_str("From"),
HeaderName::new_from_ascii_str("To")
);
}
#[test]
fn headername_str_ne() {
assert_ne!(HeaderName::new_from_ascii_str("From"), "To");
}
#[test]
fn str_headername_ne() {
assert_ne!("From", HeaderName::new_from_ascii_str("To"));
}
// names taken randomly from https://it.wikipedia.org/wiki/Pinco_Pallino
#[test]
fn format_ascii() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"),
"John Doe <example@example.com>, Jean Dupont <jean@example.com>".to_owned(),
));
assert_eq!(
headers.to_string(),
"To: John Doe <example@example.com>, Jean Dupont <jean@example.com>\r\n"
);
}
#[test]
fn format_ascii_with_folding() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"),
"Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand <jemand@example.com>, Jean Dupont <jean@example.com>".to_owned(),
));
assert_eq!(
headers.to_string(),
concat!(
"To: Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith\r\n",
" <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand\r\n",
" <jemand@example.com>, Jean Dupont <jean@example.com>\r\n"
)
);
}
#[test]
fn format_ascii_with_folding_long_line() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"),
"Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_owned()
));
assert_eq!(
headers.to_string(),
concat!(
"Subject: Hello! This is lettre, and this\r\n",
" IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I\r\n",
" guess that's it!\r\n"
)
);
}
#[test]
fn format_ascii_with_folding_very_long_line() {
let mut headers = Headers::new();
headers.insert_raw(
HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"),
"Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know".to_owned()
));
assert_eq!(
headers.to_string(),
concat!(
"Subject: Hello!\r\n",
" IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut!\r\n",
" I don't know\r\n",
)
);
}
#[test]
fn format_ascii_with_folding_giant_word() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"),
"1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz".to_owned()
));
assert_eq!(
headers.to_string(),
"Subject: 1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz\r\n",
);
}
#[test]
fn format_special() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"),
"Seán <sean@example.com>".to_owned(),
));
assert_eq!(
headers.to_string(),
"To: =?utf-8?b?U2XDoW4=?= <sean@example.com>\r\n"
);
}
#[test]
fn format_special_emoji() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"),
"🌎 <world@example.com>".to_owned(),
));
assert_eq!(
headers.to_string(),
"To: =?utf-8?b?8J+Mjg==?= <world@example.com>\r\n"
);
}
#[test]
fn format_special_with_folding() {
let mut headers = Headers::new();
let to = To::from(Mailboxes::from_iter([
"🌍 <world@example.com>".parse().unwrap(),
"🦆 Everywhere <ducks@example.com>".parse().unwrap(),
"Иванов Иван Иванович <ivanov@example.com>".parse().unwrap(),
"Jānis Bērziņš <janis@example.com>".parse().unwrap(),
"Seán Ó Rudaí <sean@example.com>".parse().unwrap(),
]));
headers.set(to);
assert_eq!(
headers.to_string(),
concat!(
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhiBFdmVyeXdo?=\r\n",
" =?utf-8?b?ZXJl?= <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LI=?=\r\n",
" =?utf-8?b?0LDQvSDQmNCy0LDQvdC+0LLQuNGH?= <ivanov@example.com>,\r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, =?utf-8?b?U2U=?=\r\n",
" =?utf-8?b?w6FuIMOTIFJ1ZGHDrQ==?= <sean@example.com>\r\n",
)
);
}
#[test]
fn format_special_with_folding_raw() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"),
"🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_owned(),
));
assert_eq!(
headers.to_string(),
concat!(
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?=\r\n",
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9?=\r\n",
" =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, =?utf-8?b?U2U=?=\r\n",
" =?utf-8?b?w6FuIMOTIFJ1ZGHDrQ==?= <sean@example.com>\r\n",
)
);
}
#[test]
fn format_slice_on_char_boundary_bug() {
let mut headers = Headers::new();
headers.insert_raw(
HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"),
"🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳".to_owned(),)
);
assert_eq!(
headers.to_string(),
concat!(
"Subject: =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz?=\r\n",
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n",
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n",
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n",
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n",
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+lsw==?=\r\n"
)
);
}
#[test]
fn format_bad_stuff() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"),
"Hello! \r\n This is \" bad \0. 👋".to_owned(),
));
assert_eq!(
headers.to_string(),
"Subject: Hello! =?utf-8?b?DQo=?= This is \" bad =?utf-8?b?AC4g8J+Riw==?=\r\n"
);
}
#[test]
fn format_everything() {
let mut headers = Headers::new();
headers.insert_raw(
HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"),
"Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_owned()
)
);
headers.insert_raw(
HeaderValue::new(
HeaderName::new_from_ascii_str("To"),
"🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_owned(),
)
);
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"Someone <somewhere@example.com>".to_owned(),
));
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
"quoted-printable".to_owned(),
));
assert_eq!(
headers.to_string(),
concat!(
"Subject: Hello! This is lettre, and this\r\n",
" IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I\r\n",
" guess that's it!\r\n",
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?=\r\n",
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9?=\r\n",
" =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, =?utf-8?b?U2U=?=\r\n",
" =?utf-8?b?w6FuIMOTIFJ1ZGHDrQ==?= <sean@example.com>\r\n",
"From: Someone <somewhere@example.com>\r\n",
"Content-Transfer-Encoding: quoted-printable\r\n",
)
);
}
#[test]
fn issue_653() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"),
"+仮名 :a;go; ;;;;;s;;;;;;;;;;;;;;;;fffeinmjggggggggg".to_owned(),
));
assert_eq!(
headers.to_string(),
concat!(
"Subject: =?utf-8?b?77yL5Luu5ZCN?= :a;go; =?utf-8?b?Ozs7OztzOzs7Ozs7Ozs7?=\r\n",
" =?utf-8?b?Ozs7Ozs7O2ZmZmVpbm1qZ2dnZ2dnZ2dn772G44Gj?=\r\n",
)
);
}
}

View File

@@ -1,106 +0,0 @@
use crate::{
message::header::{Header, HeaderName, HeaderValue},
BoxError,
};
/// Message format version, defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-4)
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct MimeVersion {
major: u8,
minor: u8,
}
/// MIME version 1.0
///
/// Should be used in all MIME messages.
pub const MIME_VERSION_1_0: MimeVersion = MimeVersion::new(1, 0);
impl MimeVersion {
/// Build a new `MimeVersion` header
pub const fn new(major: u8, minor: u8) -> Self {
MimeVersion { major, minor }
}
/// Get the `major` value of this `MimeVersion` header.
#[inline]
pub const fn major(self) -> u8 {
self.major
}
/// Get the `minor` value of this `MimeVersion` header.
#[inline]
pub const fn minor(self) -> u8 {
self.minor
}
}
impl Header for MimeVersion {
fn name() -> HeaderName {
HeaderName::new_from_ascii_str("MIME-Version")
}
fn parse(s: &str) -> Result<Self, BoxError> {
let mut s = s.split('.');
let major = s
.next()
.expect("The first call to next for a Split<char> always succeeds");
let minor = s
.next()
.ok_or_else(|| String::from("MIME-Version header doesn't contain '.'"))?;
let major = major.parse()?;
let minor = minor.parse()?;
Ok(MimeVersion::new(major, minor))
}
fn display(&self) -> HeaderValue {
let val = format!("{}.{}", self.major, self.minor);
HeaderValue::dangerous_new_pre_encoded(Self::name(), val.clone(), val)
}
}
impl Default for MimeVersion {
fn default() -> Self {
MIME_VERSION_1_0
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::{MimeVersion, MIME_VERSION_1_0};
use crate::message::header::{HeaderName, HeaderValue, Headers};
#[test]
fn format_mime_version() {
let mut headers = Headers::new();
headers.set(MIME_VERSION_1_0);
assert_eq!(headers.to_string(), "MIME-Version: 1.0\r\n");
headers.set(MimeVersion::new(0, 1));
assert_eq!(headers.to_string(), "MIME-Version: 0.1\r\n");
}
#[test]
fn parse_mime_version() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("MIME-Version"),
"1.0".to_owned(),
));
assert_eq!(headers.get::<MimeVersion>(), Some(MIME_VERSION_1_0));
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("MIME-Version"),
"0.1".to_owned(),
));
assert_eq!(headers.get::<MimeVersion>(), Some(MimeVersion::new(0, 1)));
}
}

View File

@@ -1,136 +0,0 @@
use super::{Header, HeaderName, HeaderValue};
use crate::BoxError;
macro_rules! text_header {
($(#[$attr:meta])* Header($type_name: ident, $header_name: expr )) => {
$(#[$attr])*
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct $type_name(String);
impl Header for $type_name {
fn name() -> HeaderName {
HeaderName::new_from_ascii_str($header_name)
}
fn parse(s: &str) -> Result<Self, BoxError> {
Ok(Self(s.into()))
}
fn display(&self) -> HeaderValue {
HeaderValue::new(Self::name(), self.0.clone())
}
}
impl From<String> for $type_name {
#[inline]
fn from(text: String) -> Self {
Self(text)
}
}
impl AsRef<str> for $type_name {
#[inline]
fn as_ref(&self) -> &str {
&self.0
}
}
};
}
text_header!(
/// `Subject` of the message, defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.5)
Header(Subject, "Subject")
);
text_header!(
/// `Comments` of the message, defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.5)
Header(Comments, "Comments")
);
text_header!(
/// `Keywords` header. Should contain a comma-separated list of one or more
/// words or quoted-strings, defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.5)
Header(Keywords, "Keywords")
);
text_header!(
/// `In-Reply-To` header. Contains one or more
/// unique message identifiers,
/// defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.4)
Header(InReplyTo, "In-Reply-To")
);
text_header!(
/// `References` header. Contains one or more
/// unique message identifiers,
/// defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.4)
Header(References, "References")
);
text_header!(
/// `Message-Id` header. Contains a unique message identifier,
/// defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.4)
Header(MessageId, "Message-ID")
);
text_header!(
/// `User-Agent` header. Contains information about the client,
/// defined in [draft-melnikov-email-user-agent-00](https://tools.ietf.org/html/draft-melnikov-email-user-agent-00#section-3)
Header(UserAgent, "User-Agent")
);
text_header! {
/// `Content-Id` header,
/// defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-7)
Header(ContentId, "Content-ID")
}
text_header! {
/// `Content-Location` header,
/// defined in [RFC2110](https://tools.ietf.org/html/rfc2110#section-4.3)
Header(ContentLocation, "Content-Location")
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::Subject;
use crate::message::header::{HeaderName, HeaderValue, Headers};
#[test]
fn format_ascii() {
let mut headers = Headers::new();
headers.set(Subject("Sample subject".into()));
assert_eq!(headers.to_string(), "Subject: Sample subject\r\n");
}
#[test]
fn format_utf8() {
let mut headers = Headers::new();
headers.set(Subject("Тема сообщения".into()));
assert_eq!(
headers.to_string(),
"Subject: =?utf-8?b?0KLQtdC80LAg0YHQvtC+0LHRidC10L3QuNGP?=\r\n"
);
}
#[test]
fn format_utf8_word() {
let mut headers = Headers::new();
headers.set(Subject("Administratör".into()));
assert_eq!(
headers.to_string(),
"Subject: =?utf-8?b?QWRtaW5pc3RyYXTDtnI=?=\r\n"
);
}
#[test]
fn parse_ascii() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"),
"Sample subject".to_owned(),
));
assert_eq!(
headers.get::<Subject>(),
Some(Subject("Sample subject".into()))
);
}
}

View File

@@ -1,6 +0,0 @@
mod parsers;
#[cfg(feature = "serde")]
mod serde;
mod types;
pub use self::types::*;

View File

@@ -1,5 +0,0 @@
mod rfc2234;
mod rfc2822;
mod rfc5336;
pub(crate) use rfc2822::{mailbox, mailbox_list};

View File

@@ -1,32 +0,0 @@
//! Partial parsers implementation of [RFC2234]: Augmented BNF for
//! Syntax Specifications: ABNF.
//!
//! [RFC2234]: https://datatracker.ietf.org/doc/html/rfc2234
use chumsky::{error::Cheap, prelude::*};
// 6.1 Core Rules
// https://datatracker.ietf.org/doc/html/rfc2234#section-6.1
// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
pub(super) fn alpha() -> impl Parser<char, char, Error = Cheap<char>> {
filter(|c: &char| c.is_ascii_alphabetic())
}
// DIGIT = %x30-39
// ; 0-9
pub(super) fn digit() -> impl Parser<char, char, Error = Cheap<char>> {
filter(|c: &char| c.is_ascii_digit())
}
// DQUOTE = %x22
// ; " (Double Quote)
pub(super) fn dquote() -> impl Parser<char, char, Error = Cheap<char>> {
just('"')
}
// WSP = SP / HTAB
// ; white space
pub(super) fn wsp() -> impl Parser<char, char, Error = Cheap<char>> {
choice((just(' '), just('\t')))
}

View File

@@ -1,250 +0,0 @@
//! Partial parsers implementation of [RFC2822]: Internet Message
//! Format.
//!
//! [RFC2822]: https://datatracker.ietf.org/doc/html/rfc2822
use chumsky::{error::Cheap, prelude::*};
use super::{rfc2234, rfc5336};
// 3.2.1. Primitive Tokens
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.1
// NO-WS-CTL = %d1-8 / ; US-ASCII control characters
// %d11 / ; that do not include the
// %d12 / ; carriage return, line feed,
// %d14-31 / ; and white space characters
// %d127
fn no_ws_ctl() -> impl Parser<char, char, Error = Cheap<char>> {
filter(|c| matches!(u32::from(*c), 1..=8 | 11 | 12 | 14..=31 | 127))
}
// text = %d1-9 / ; Characters excluding CR and LF
// %d11 /
// %d12 /
// %d14-127 /
// obs-text
fn text() -> impl Parser<char, char, Error = Cheap<char>> {
filter(|c| matches!(u32::from(*c), 1..=9 | 11 | 12 | 14..=127))
}
// 3.2.2. Quoted characters
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.2
// quoted-pair = ("\" text) / obs-qp
fn quoted_pair() -> impl Parser<char, char, Error = Cheap<char>> {
just('\\').ignore_then(text())
}
// 3.2.3. Folding white space and comments
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.3
// FWS = ([*WSP CRLF] 1*WSP) / ; Folding white space
// obs-FWS
pub fn fws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
rfc2234::wsp()
.or_not()
.then_ignore(rfc2234::wsp().ignored().repeated())
}
// CFWS = *([FWS] comment) (([FWS] comment) / FWS)
pub fn cfws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
// TODO: comment are not currently supported, so for now a cfws is
// the same as a fws.
fws()
}
// 3.2.4. Atom
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.4
// atext = ALPHA / DIGIT / ; Any character except controls,
// "!" / "#" / ; SP, and specials.
// "$" / "%" / ; Used for atoms
// "&" / "'" /
// "*" / "+" /
// "-" / "/" /
// "=" / "?" /
// "^" / "_" /
// "`" / "{" /
// "|" / "}" /
// "~"
pub(super) fn atext() -> impl Parser<char, char, Error = Cheap<char>> {
choice((
rfc2234::alpha(),
rfc2234::digit(),
filter(|c| {
matches!(
*c,
'!' | '#'
| '$'
| '%'
| '&'
| '\''
| '*'
| '+'
| '-'
| '/'
| '='
| '?'
| '^'
| '_'
| '`'
| '{'
| '|'
| '}'
| '~'
)
}),
// also allow non ASCII UTF8 chars
rfc5336::utf8_non_ascii(),
))
}
// atom = [CFWS] 1*atext [CFWS]
pub(super) fn atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
cfws().chain(atext().repeated().at_least(1))
}
// dot-atom = [CFWS] dot-atom-text [CFWS]
pub fn dot_atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
cfws().chain(dot_atom_text())
}
// dot-atom-text = 1*atext *("." 1*atext)
pub fn dot_atom_text() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
atext().repeated().at_least(1).chain(
just('.')
.chain(atext().repeated().at_least(1))
.repeated()
.at_least(1)
.flatten(),
)
}
// 3.2.5. Quoted strings
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.5
// qtext = NO-WS-CTL / ; Non white space controls
//
// %d33 / ; The rest of the US-ASCII
// %d35-91 / ; characters not including "\"
// %d93-126 ; or the quote character
fn qtext() -> impl Parser<char, char, Error = Cheap<char>> {
choice((
filter(|c| matches!(u32::from(*c), 33 | 35..=91 | 93..=126)),
no_ws_ctl(),
))
}
// qcontent = qtext / quoted-pair
pub(super) fn qcontent() -> impl Parser<char, char, Error = Cheap<char>> {
choice((qtext(), quoted_pair(), rfc5336::utf8_non_ascii()))
}
// quoted-string = [CFWS]
// DQUOTE *([FWS] qcontent) [FWS] DQUOTE
// [CFWS]
fn quoted_string() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
rfc2234::dquote()
.ignore_then(fws().chain(qcontent()).repeated().flatten())
.then_ignore(text::whitespace())
.then_ignore(rfc2234::dquote())
}
// 3.2.6. Miscellaneous tokens
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.6
// word = atom / quoted-string
fn word() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
choice((quoted_string(), atom()))
}
// phrase = 1*word / obs-phrase
fn phrase() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
choice((obs_phrase(), word().repeated().at_least(1).flatten()))
}
// 3.4. Address Specification
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.4
// mailbox = name-addr / addr-spec
pub(crate) fn mailbox() -> impl Parser<char, (Option<String>, (String, String)), Error = Cheap<char>>
{
choice((name_addr(), addr_spec().map(|addr| (None, addr))))
.padded()
.then_ignore(end())
}
// name-addr = [display-name] angle-addr
fn name_addr() -> impl Parser<char, (Option<String>, (String, String)), Error = Cheap<char>> {
display_name().collect().or_not().then(angle_addr())
}
// angle-addr = [CFWS] "<" addr-spec ">" [CFWS] / obs-angle-addr
fn angle_addr() -> impl Parser<char, (String, String), Error = Cheap<char>> {
addr_spec()
.delimited_by(just('<').ignored(), just('>').ignored())
.padded()
}
// display-name = phrase
fn display_name() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
phrase()
}
// mailbox-list = (mailbox *("," mailbox)) / obs-mbox-list
pub(crate) fn mailbox_list(
) -> impl Parser<char, Vec<(Option<String>, (String, String))>, Error = Cheap<char>> {
choice((name_addr(), addr_spec().map(|addr| (None, addr))))
.separated_by(just(',').padded())
.then_ignore(end())
}
// 3.4.1. Addr-spec specification
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.4.1
// addr-spec = local-part "@" domain
pub fn addr_spec() -> impl Parser<char, (String, String), Error = Cheap<char>> {
local_part()
.collect()
.then_ignore(just('@'))
.then(domain().collect())
}
// local-part = dot-atom / quoted-string / obs-local-part
pub fn local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
choice((dot_atom(), quoted_string(), obs_local_part()))
}
// domain = dot-atom / domain-literal / obs-domain
pub fn domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
// NOTE: omitting domain-literal since it may never be used
choice((dot_atom(), obs_domain()))
}
// 4.1. Miscellaneous obsolete tokens
// https://datatracker.ietf.org/doc/html/rfc2822#section-4.1
// obs-phrase = word *(word / "." / CFWS)
fn obs_phrase() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
// NOTE: the CFWS is already captured by the word, no need to add
// it there.
word().chain(
choice((word(), just('.').repeated().exactly(1)))
.repeated()
.flatten(),
)
}
// 4.4. Obsolete Addressing
// https://datatracker.ietf.org/doc/html/rfc2822#section-4.4
// obs-local-part = word *("." word)
pub fn obs_local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
word().chain(just('.').chain(word()).repeated().flatten())
}
// obs-domain = atom *("." atom)
pub fn obs_domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
atom().chain(just('.').chain(atom()).repeated().flatten())
}

View File

@@ -1,17 +0,0 @@
//! Partial parsers implementation of [RFC5336]: SMTP Extension for
//! Internationalized Email Addresses.
//!
//! [RFC5336]: https://datatracker.ietf.org/doc/html/rfc5336
use chumsky::{error::Cheap, prelude::*};
// 3.3. Extended Mailbox Address Syntax
// https://datatracker.ietf.org/doc/html/rfc5336#section-3.3
// UTF8-non-ascii = UTF8-2 / UTF8-3 / UTF8-4
// UTF8-2 = <See Section 4 of RFC 3629>
// UTF8-3 = <See Section 4 of RFC 3629>
// UTF8-4 = <See Section 4 of RFC 3629>
pub(super) fn utf8_non_ascii() -> impl Parser<char, char, Error = Cheap<char>> {
filter(|c: &char| c.len_utf8() > 1)
}

View File

@@ -1,219 +0,0 @@
use std::fmt::{Formatter, Result as FmtResult};
use serde::{
de::{Deserializer, Error as DeError, MapAccess, SeqAccess, Visitor},
ser::Serializer,
Deserialize, Serialize,
};
use crate::message::{Mailbox, Mailboxes};
impl Serialize for Mailbox {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(self)
}
}
impl<'de> Deserialize<'de> for Mailbox {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
enum Field {
Name,
Email,
}
const FIELDS: &[&str] = &["name", "email"];
impl<'de> Deserialize<'de> for Field {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct FieldVisitor;
impl<'de> Visitor<'de> for FieldVisitor {
type Value = Field;
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
formatter.write_str("'name' or 'email'")
}
fn visit_str<E>(self, value: &str) -> Result<Field, E>
where
E: DeError,
{
match value {
"name" => Ok(Field::Name),
"email" => Ok(Field::Email),
_ => Err(DeError::unknown_field(value, FIELDS)),
}
}
}
deserializer.deserialize_identifier(FieldVisitor)
}
}
struct MailboxVisitor;
impl<'de> Visitor<'de> for MailboxVisitor {
type Value = Mailbox;
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
formatter.write_str("mailbox string or object")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: DeError,
{
s.parse().map_err(DeError::custom)
}
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
where
V: MapAccess<'de>,
{
let mut name = None;
let mut addr = None;
while let Some(key) = map.next_key()? {
match key {
Field::Name => {
if name.is_some() {
return Err(DeError::duplicate_field("name"));
}
name = Some(map.next_value()?);
}
Field::Email => {
if addr.is_some() {
return Err(DeError::duplicate_field("email"));
}
addr = Some(map.next_value()?);
}
}
}
let addr = addr.ok_or_else(|| DeError::missing_field("email"))?;
Ok(Mailbox::new(name, addr))
}
}
deserializer.deserialize_any(MailboxVisitor)
}
}
impl Serialize for Mailboxes {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(self)
}
}
impl<'de> Deserialize<'de> for Mailboxes {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct MailboxesVisitor;
impl<'de> Visitor<'de> for MailboxesVisitor {
type Value = Mailboxes;
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
formatter.write_str("mailboxes string or sequence")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: DeError,
{
s.parse().map_err(DeError::custom)
}
fn visit_seq<V>(self, mut seq: V) -> Result<Self::Value, V::Error>
where
V: SeqAccess<'de>,
{
let mut mboxes = Mailboxes::new();
while let Some(mbox) = seq.next_element()? {
mboxes.push(mbox);
}
Ok(mboxes)
}
}
deserializer.deserialize_any(MailboxesVisitor)
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use serde_json::from_str;
use super::*;
use crate::address::Address;
#[test]
fn parse_address_string() {
let m: Address = from_str(r#""kayo@example.com""#).unwrap();
assert_eq!(m, "kayo@example.com".parse().unwrap());
}
#[test]
fn parse_address_object() {
let m: Address = from_str(r#"{ "user": "kayo", "domain": "example.com" }"#).unwrap();
assert_eq!(m, "kayo@example.com".parse().unwrap());
}
#[test]
fn parse_mailbox_string() {
let m: Mailbox = from_str(r#""Kai <kayo@example.com>""#).unwrap();
assert_eq!(m, "Kai <kayo@example.com>".parse().unwrap());
}
#[test]
fn parse_mailbox_object_address_string() {
let m: Mailbox = from_str(r#"{ "name": "Kai", "email": "kayo@example.com" }"#).unwrap();
assert_eq!(m, "Kai <kayo@example.com>".parse().unwrap());
}
#[test]
fn parse_mailbox_object_address_object() {
let m: Mailbox =
from_str(r#"{ "name": "Kai", "email": { "user": "kayo", "domain": "example.com" } }"#)
.unwrap();
assert_eq!(m, "Kai <kayo@example.com>".parse().unwrap());
}
#[test]
fn parse_mailboxes_string() {
let m: Mailboxes =
from_str(r#""yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>""#).unwrap();
assert_eq!(
m,
"yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>"
.parse()
.unwrap()
);
}
#[test]
fn parse_mailboxes_array() {
let m: Mailboxes =
from_str(r#"["yin@dtb.com", { "name": "Hei", "email": "hei@dtb.com" }, { "name": "Kai", "email": { "user": "kayo", "domain": "example.com" } }]"#)
.unwrap();
assert_eq!(
m,
"yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>"
.parse()
.unwrap()
);
}
}

View File

@@ -1,615 +0,0 @@
use std::{
fmt::{Display, Formatter, Result as FmtResult, Write},
mem,
slice::Iter,
str::FromStr,
};
use chumsky::prelude::*;
use email_encoding::headers::writer::EmailWriter;
use super::parsers;
use crate::address::{Address, AddressError};
/// Represents an email address with an optional name for the sender/recipient.
///
/// This type contains email address and the sender/recipient name (_Some Name \<user@domain.tld\>_ or _withoutname@domain.tld_).
///
/// **NOTE**: Enable feature "serde" to be able to serialize/deserialize it using [serde](https://serde.rs/).
///
/// # Examples
///
/// You can create a `Mailbox` from a string and an [`Address`]:
///
/// ```
/// # use lettre::{Address, message::Mailbox};
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("example", "email.com")?;
/// let mailbox = Mailbox::new(None, address);
/// # Ok(())
/// # }
/// ```
///
/// You can also create one from a string literal:
///
/// ```
/// # use lettre::message::Mailbox;
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let mailbox: Mailbox = "John Smith <example@email.com>".parse()?;
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct Mailbox {
/// The name associated with the address.
pub name: Option<String>,
/// The email address itself.
pub email: Address,
}
impl Mailbox {
/// Creates a new `Mailbox` using an email address and the name of the recipient if there is one.
///
/// # Examples
///
/// ```
/// use lettre::{message::Mailbox, Address};
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("example", "email.com")?;
/// let mailbox = Mailbox::new(None, address);
/// # Ok(())
/// # }
/// ```
pub fn new(name: Option<String>, email: Address) -> Self {
Mailbox { name, email }
}
pub(crate) fn encode(&self, w: &mut EmailWriter<'_>) -> FmtResult {
if let Some(name) = &self.name {
email_encoding::headers::quoted_string::encode(name, w)?;
w.space();
w.write_char('<')?;
}
w.write_str(self.email.as_ref())?;
if self.name.is_some() {
w.write_char('>')?;
}
Ok(())
}
}
impl Display for Mailbox {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
if let Some(name) = &self.name {
let name = name.trim();
if !name.is_empty() {
write_word(f, name)?;
f.write_str(" <")?;
self.email.fmt(f)?;
return f.write_char('>');
}
}
self.email.fmt(f)
}
}
impl<S: Into<String>, T: Into<String>> TryFrom<(S, T)> for Mailbox {
type Error = AddressError;
fn try_from(header: (S, T)) -> Result<Self, Self::Error> {
let (name, address) = header;
Ok(Mailbox::new(Some(name.into()), address.into().parse()?))
}
}
impl FromStr for Mailbox {
type Err = AddressError;
fn from_str(src: &str) -> Result<Mailbox, Self::Err> {
let (name, (user, domain)) = parsers::mailbox().parse(src).map_err(|_errs| {
// TODO: improve error management
AddressError::InvalidInput
})?;
let mailbox = Mailbox::new(name, Address::new(user, domain)?);
Ok(mailbox)
}
}
impl From<Address> for Mailbox {
fn from(value: Address) -> Self {
Self::new(None, value)
}
}
/// Represents a sequence of [`Mailbox`] instances.
///
/// This type contains a sequence of mailboxes (_Some Name \<user@domain.tld\>, Another Name \<other@domain.tld\>, withoutname@domain.tld, ..._).
///
/// **NOTE**: Enable feature "serde" to be able to serialize/deserialize it using [serde](https://serde.rs/).
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct Mailboxes(Vec<Mailbox>);
impl Mailboxes {
/// Creates a new list of [`Mailbox`] instances.
///
/// # Examples
///
/// ```
/// use lettre::message::Mailboxes;
/// let mailboxes = Mailboxes::new();
/// ```
pub fn new() -> Self {
Mailboxes(Vec::new())
}
/// Adds a new [`Mailbox`] to the list, in a builder style pattern.
///
/// # Examples
///
/// ```
/// use lettre::{
/// message::{Mailbox, Mailboxes},
/// Address,
/// };
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("example", "email.com")?;
/// let mut mailboxes = Mailboxes::new().with(Mailbox::new(None, address));
/// # Ok(())
/// # }
/// ```
pub fn with(mut self, mbox: Mailbox) -> Self {
self.0.push(mbox);
self
}
/// Adds a new [`Mailbox`] to the list, in a Vec::push style pattern.
///
/// # Examples
///
/// ```
/// use lettre::{
/// message::{Mailbox, Mailboxes},
/// Address,
/// };
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("example", "email.com")?;
/// let mut mailboxes = Mailboxes::new();
/// mailboxes.push(Mailbox::new(None, address));
/// # Ok(())
/// # }
/// ```
pub fn push(&mut self, mbox: Mailbox) {
self.0.push(mbox);
}
/// Extracts the first [`Mailbox`] if it exists.
///
/// # Examples
///
/// ```
/// use lettre::{
/// message::{Mailbox, Mailboxes},
/// Address,
/// };
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let empty = Mailboxes::new();
/// assert!(empty.into_single().is_none());
///
/// let mut mailboxes = Mailboxes::new();
/// let address = Address::new("example", "email.com")?;
///
/// mailboxes.push(Mailbox::new(None, address));
/// assert!(mailboxes.into_single().is_some());
/// # Ok(())
/// # }
/// ```
pub fn into_single(self) -> Option<Mailbox> {
self.into()
}
/// Creates an iterator over the [`Mailbox`] instances that are currently stored.
///
/// # Examples
///
/// ```
/// use lettre::{
/// message::{Mailbox, Mailboxes},
/// Address,
/// };
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let mut mailboxes = Mailboxes::new();
///
/// let address = Address::new("example", "email.com")?;
/// mailboxes.push(Mailbox::new(None, address));
///
/// let address = Address::new("example", "email.com")?;
/// mailboxes.push(Mailbox::new(None, address));
///
/// let mut iter = mailboxes.iter();
///
/// assert!(iter.next().is_some());
/// assert!(iter.next().is_some());
///
/// assert!(iter.next().is_none());
/// # Ok(())
/// # }
/// ```
pub fn iter(&self) -> Iter<'_, Mailbox> {
self.0.iter()
}
pub(crate) fn encode(&self, w: &mut EmailWriter<'_>) -> FmtResult {
let mut first = true;
for mailbox in self.iter() {
if !mem::take(&mut first) {
w.write_char(',')?;
w.space();
}
mailbox.encode(w)?;
}
Ok(())
}
}
impl Default for Mailboxes {
fn default() -> Self {
Self::new()
}
}
impl From<Mailbox> for Mailboxes {
fn from(mailbox: Mailbox) -> Self {
Mailboxes(vec![mailbox])
}
}
impl From<Mailboxes> for Option<Mailbox> {
fn from(mailboxes: Mailboxes) -> Option<Mailbox> {
mailboxes.into_iter().next()
}
}
impl From<Vec<Mailbox>> for Mailboxes {
fn from(vec: Vec<Mailbox>) -> Self {
Mailboxes(vec)
}
}
impl From<Mailboxes> for Vec<Mailbox> {
fn from(mailboxes: Mailboxes) -> Vec<Mailbox> {
mailboxes.0
}
}
impl FromIterator<Mailbox> for Mailboxes {
fn from_iter<T: IntoIterator<Item = Mailbox>>(iter: T) -> Self {
Self(Vec::from_iter(iter))
}
}
impl Extend<Mailbox> for Mailboxes {
fn extend<T: IntoIterator<Item = Mailbox>>(&mut self, iter: T) {
self.0.extend(iter);
}
}
impl IntoIterator for Mailboxes {
type Item = Mailbox;
type IntoIter = ::std::vec::IntoIter<Mailbox>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl Display for Mailboxes {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let mut iter = self.iter();
if let Some(mbox) = iter.next() {
mbox.fmt(f)?;
for mbox in iter {
f.write_str(", ")?;
mbox.fmt(f)?;
}
}
Ok(())
}
}
impl FromStr for Mailboxes {
type Err = AddressError;
fn from_str(src: &str) -> Result<Self, Self::Err> {
let mut mailboxes = Vec::new();
let parsed_mailboxes = parsers::mailbox_list().parse(src).map_err(|_errs| {
// TODO: improve error management
AddressError::InvalidInput
})?;
for (name, (user, domain)) in parsed_mailboxes {
mailboxes.push(Mailbox::new(name, Address::new(user, domain)?))
}
Ok(Mailboxes(mailboxes))
}
}
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.6
fn write_word(f: &mut Formatter<'_>, s: &str) -> FmtResult {
if s.as_bytes().iter().copied().all(is_valid_atom_char) {
f.write_str(s)
} else {
// Quoted string: https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.5
f.write_char('"')?;
for c in s.chars() {
write_quoted_string_char(f, c)?;
}
f.write_char('"')?;
Ok(())
}
}
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.4
fn is_valid_atom_char(c: u8) -> bool {
matches!(c,
// Not really allowed but can be inserted between atoms.
b'\t' |
b' ' |
b'!' |
b'#' |
b'$' |
b'%' |
b'&' |
b'\'' |
b'*' |
b'+' |
b'-' |
b'/' |
b'0'..=b'8' |
b'=' |
b'?' |
b'A'..=b'Z' |
b'^' |
b'_' |
b'`' |
b'a'..=b'z' |
b'{' |
b'|' |
b'}' |
b'~' |
// Not technically allowed but will be escaped into allowed characters.
128..=255)
}
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.5
fn write_quoted_string_char(f: &mut Formatter<'_>, c: char) -> FmtResult {
match c {
// Can not be encoded.
'\n' | '\r' => Err(std::fmt::Error),
// Note, not qcontent but can be put before or after any qcontent.
'\t' | ' ' => f.write_char(c),
c if match c as u32 {
// NO-WS-CTL: https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.1
1..=8 | 11 | 12 | 14..=31 | 127 |
// The rest of the US-ASCII except \ and "
33 |
35..=91 |
93..=126 |
// Non-ascii characters will be escaped separately later.
128.. => true,
_ => false,
} =>
{
f.write_char(c)
}
_ => {
// quoted-pair https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.2
f.write_char('\\')?;
f.write_char(c)
}
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::Mailbox;
#[test]
fn mailbox_format_address_only() {
assert_eq!(
format!(
"{}",
Mailbox::new(None, "kayo@example.com".parse().unwrap())
),
"kayo@example.com"
);
}
#[test]
fn mailbox_format_address_with_name() {
assert_eq!(
format!(
"{}",
Mailbox::new(Some("K.".into()), "kayo@example.com".parse().unwrap())
),
"\"K.\" <kayo@example.com>"
);
}
#[test]
fn mailbox_format_address_with_comma() {
assert_eq!(
format!(
"{}",
Mailbox::new(
Some("Last, First".into()),
"kayo@example.com".parse().unwrap()
)
),
r#""Last, First" <kayo@example.com>"#
);
}
#[test]
fn mailbox_format_address_with_comma_and_non_ascii() {
assert_eq!(
format!(
"{}",
Mailbox::new(
Some("Laşt, First".into()),
"kayo@example.com".parse().unwrap()
)
),
r#""Laşt, First" <kayo@example.com>"#
);
}
#[test]
fn mailbox_format_address_with_comma_and_quoted_non_ascii() {
assert_eq!(
format!(
"{}",
Mailbox::new(
Some(r#"Laşt, "First""#.into()),
"kayo@example.com".parse().unwrap()
)
),
r#""Laşt, \"First\"" <kayo@example.com>"#
);
}
#[test]
fn mailbox_format_address_with_color() {
assert_eq!(
format!(
"{}",
Mailbox::new(
Some("Chris's Wiki :: blog".into()),
"kayo@example.com".parse().unwrap()
)
),
r#""Chris's Wiki :: blog" <kayo@example.com>"#
);
}
#[test]
fn format_address_with_empty_name() {
assert_eq!(
format!(
"{}",
Mailbox::new(Some("".into()), "kayo@example.com".parse().unwrap())
),
"kayo@example.com"
);
}
#[test]
fn format_address_with_name_trim() {
assert_eq!(
format!(
"{}",
Mailbox::new(Some(" K. ".into()), "kayo@example.com".parse().unwrap())
),
"\"K.\" <kayo@example.com>"
);
}
#[test]
fn parse_address_only() {
assert_eq!(
"kayo@example.com".parse(),
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
);
}
#[test]
fn parse_address_only_trim() {
assert_eq!(
" kayo@example.com ".parse(),
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
);
}
#[test]
fn parse_address_with_name() {
assert_eq!(
"K. <kayo@example.com>".parse(),
Ok(Mailbox::new(
Some("K.".into()),
"kayo@example.com".parse().unwrap()
))
);
}
#[test]
fn parse_address_with_name_trim() {
assert_eq!(
" K. <kayo@example.com> ".parse(),
Ok(Mailbox::new(
Some("K.".into()),
"kayo@example.com".parse().unwrap()
))
);
}
#[test]
fn parse_address_with_empty_name() {
assert_eq!(
"<kayo@example.com>".parse(),
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
);
}
#[test]
fn parse_address_with_empty_name_trim() {
assert_eq!(
" <kayo@example.com> ".parse(),
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
);
}
#[test]
fn parse_address_from_tuple() {
assert_eq!(
("K.".to_owned(), "kayo@example.com".to_owned()).try_into(),
Ok(Mailbox::new(
Some("K.".into()),
"kayo@example.com".parse().unwrap()
))
);
}
}

View File

@@ -1,728 +0,0 @@
use std::{io::Write, iter::repeat_with};
use mime::Mime;
use crate::message::{
header::{self, ContentTransferEncoding, ContentType, Header, Headers},
EmailFormat, IntoBody,
};
/// MIME part variants
#[derive(Debug, Clone)]
pub(super) enum Part {
/// Single part with content
Single(SinglePart),
/// Multiple parts of content
Multi(MultiPart),
}
impl Part {
#[cfg(feature = "dkim")]
pub(super) fn format_body(&self, out: &mut Vec<u8>) {
match self {
Part::Single(part) => part.format_body(out),
Part::Multi(part) => part.format_body(out),
}
}
}
impl EmailFormat for Part {
fn format(&self, out: &mut Vec<u8>) {
match self {
Part::Single(part) => part.format(out),
Part::Multi(part) => part.format(out),
}
}
}
/// Creates builder for single part
#[derive(Debug, Clone)]
pub struct SinglePartBuilder {
headers: Headers,
}
impl SinglePartBuilder {
/// Creates a default singlepart builder
pub fn new() -> Self {
Self {
headers: Headers::new(),
}
}
/// Set the header to singlepart
pub fn header<H: Header>(mut self, header: H) -> Self {
self.headers.set(header);
self
}
/// Set the Content-Type header of the singlepart
pub fn content_type(mut self, content_type: ContentType) -> Self {
self.headers.set(content_type);
self
}
/// Build singlepart using body
pub fn body<T: IntoBody>(mut self, body: T) -> SinglePart {
let maybe_encoding = self.headers.get::<ContentTransferEncoding>();
let body = body.into_body(maybe_encoding);
self.headers.set(body.encoding());
SinglePart {
headers: self.headers,
body: body.into_vec(),
}
}
}
impl Default for SinglePartBuilder {
fn default() -> Self {
Self::new()
}
}
/// Single part
///
/// # Example
///
/// ```
/// use lettre::message::{header, SinglePart};
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let part = SinglePart::builder()
/// .header(header::ContentType::TEXT_PLAIN)
/// .body(String::from("Текст письма в уникоде"));
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone)]
pub struct SinglePart {
headers: Headers,
body: Vec<u8>,
}
impl SinglePart {
/// Creates a builder for singlepart
#[inline]
pub fn builder() -> SinglePartBuilder {
SinglePartBuilder::new()
}
/// Directly create a `SinglePart` from a plain UTF-8 content
pub fn plain<T: IntoBody>(body: T) -> Self {
Self::builder()
.header(header::ContentType::TEXT_PLAIN)
.body(body)
}
/// Directly create a `SinglePart` from a UTF-8 HTML content
pub fn html<T: IntoBody>(body: T) -> Self {
Self::builder()
.header(header::ContentType::TEXT_HTML)
.body(body)
}
/// Get the headers from singlepart
#[inline]
pub fn headers(&self) -> &Headers {
&self.headers
}
/// Get the encoded body
#[inline]
pub fn raw_body(&self) -> &[u8] {
&self.body
}
/// Get message content formatted for sending
pub fn formatted(&self) -> Vec<u8> {
let mut out = Vec::new();
self.format(&mut out);
out
}
/// Format only the signlepart body
fn format_body(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&self.body);
out.extend_from_slice(b"\r\n");
}
}
impl EmailFormat for SinglePart {
fn format(&self, out: &mut Vec<u8>) {
write!(out, "{}", self.headers)
.expect("A Write implementation panicked while formatting headers");
out.extend_from_slice(b"\r\n");
self.format_body(out);
}
}
/// The kind of multipart
#[derive(Debug, Clone)]
pub enum MultiPartKind {
/// Mixed kind to combine unrelated content parts
///
/// For example, this kind can be used to mix an email message and attachments.
Mixed,
/// Alternative kind to join several variants of same email contents.
///
/// That kind is recommended to use for joining plain (text) and rich (HTML) messages into a single email message.
Alternative,
/// Related kind to mix content and related resources.
///
/// For example, you can include images in HTML content using that.
Related,
/// Encrypted kind for encrypted messages
Encrypted { protocol: String },
/// Signed kind for signed messages
Signed { protocol: String, micalg: String },
}
/// Create a random MIME boundary.
/// (Not cryptographically random)
fn make_boundary() -> String {
repeat_with(fastrand::alphanumeric).take(40).collect()
}
impl MultiPartKind {
pub(crate) fn to_mime<S: Into<String>>(&self, boundary: Option<S>) -> Mime {
let boundary = boundary.map_or_else(make_boundary, Into::into);
format!(
"multipart/{}; boundary=\"{}\"{}",
match self {
Self::Mixed => "mixed",
Self::Alternative => "alternative",
Self::Related => "related",
Self::Encrypted { .. } => "encrypted",
Self::Signed { .. } => "signed",
},
boundary,
match self {
Self::Encrypted { protocol } => format!("; protocol=\"{protocol}\""),
Self::Signed { protocol, micalg } =>
format!("; protocol=\"{protocol}\"; micalg=\"{micalg}\""),
_ => String::new(),
}
)
.parse()
.unwrap()
}
fn from_mime(m: &Mime) -> Option<Self> {
match m.subtype().as_ref() {
"mixed" => Some(Self::Mixed),
"alternative" => Some(Self::Alternative),
"related" => Some(Self::Related),
"signed" => m.get_param("protocol").and_then(|p| {
m.get_param("micalg").map(|micalg| Self::Signed {
protocol: p.as_str().to_owned(),
micalg: micalg.as_str().to_owned(),
})
}),
"encrypted" => m.get_param("protocol").map(|p| Self::Encrypted {
protocol: p.as_str().to_owned(),
}),
_ => None,
}
}
}
/// Multipart builder
#[derive(Debug, Clone)]
pub struct MultiPartBuilder {
headers: Headers,
}
impl MultiPartBuilder {
/// Creates default multipart builder
pub fn new() -> Self {
Self {
headers: Headers::new(),
}
}
/// Set a header
pub fn header<H: Header>(mut self, header: H) -> Self {
self.headers.set(header);
self
}
/// Set `Content-Type` header using [`MultiPartKind`]
pub fn kind(self, kind: MultiPartKind) -> Self {
self.header(ContentType::from_mime(kind.to_mime::<String>(None)))
}
/// Set custom boundary
pub fn boundary<S: Into<String>>(self, boundary: S) -> Self {
let kind = {
let content_type = self.headers.get::<ContentType>().unwrap();
MultiPartKind::from_mime(content_type.as_ref()).unwrap()
};
let mime = kind.to_mime(Some(boundary));
self.header(ContentType::from_mime(mime))
}
/// Creates multipart without parts
pub fn build(self) -> MultiPart {
MultiPart {
headers: self.headers,
parts: Vec::new(),
}
}
/// Creates multipart using singlepart
pub fn singlepart(self, part: SinglePart) -> MultiPart {
self.build().singlepart(part)
}
/// Creates multipart using multipart
pub fn multipart(self, part: MultiPart) -> MultiPart {
self.build().multipart(part)
}
}
impl Default for MultiPartBuilder {
fn default() -> Self {
Self::new()
}
}
/// Multipart variant with parts
#[derive(Debug, Clone)]
pub struct MultiPart {
headers: Headers,
parts: Vec<Part>,
}
impl MultiPart {
/// Creates multipart builder
pub fn builder() -> MultiPartBuilder {
MultiPartBuilder::new()
}
/// Creates mixed multipart builder
///
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Mixed)`
pub fn mixed() -> MultiPartBuilder {
MultiPart::builder().kind(MultiPartKind::Mixed)
}
/// Creates alternative multipart builder
///
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Alternative)`
pub fn alternative() -> MultiPartBuilder {
MultiPart::builder().kind(MultiPartKind::Alternative)
}
/// Creates related multipart builder
///
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Related)`
pub fn related() -> MultiPartBuilder {
MultiPart::builder().kind(MultiPartKind::Related)
}
/// Creates encrypted multipart builder
///
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Encrypted{ protocol })`
pub fn encrypted(protocol: String) -> MultiPartBuilder {
MultiPart::builder().kind(MultiPartKind::Encrypted { protocol })
}
/// Creates signed multipart builder
///
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Signed{ protocol, micalg })`
pub fn signed(protocol: String, micalg: String) -> MultiPartBuilder {
MultiPart::builder().kind(MultiPartKind::Signed { protocol, micalg })
}
/// Alias for HTML and plain text versions of an email
pub fn alternative_plain_html<T: IntoBody, V: IntoBody>(plain: T, html: V) -> Self {
Self::alternative()
.singlepart(SinglePart::plain(plain))
.singlepart(SinglePart::html(html))
}
/// Add single part to multipart
pub fn singlepart(mut self, part: SinglePart) -> Self {
self.parts.push(Part::Single(part));
self
}
/// Add multi part to multipart
pub fn multipart(mut self, part: MultiPart) -> Self {
self.parts.push(Part::Multi(part));
self
}
/// Get the boundary of multipart contents
pub fn boundary(&self) -> String {
let content_type = self.headers.get::<ContentType>().unwrap();
content_type
.as_ref()
.get_param("boundary")
.unwrap()
.as_str()
.into()
}
/// Get the headers from the multipart
pub fn headers(&self) -> &Headers {
&self.headers
}
/// Get a mutable reference to the headers
pub fn headers_mut(&mut self) -> &mut Headers {
&mut self.headers
}
/// Get message content formatted for SMTP
pub fn formatted(&self) -> Vec<u8> {
let mut out = Vec::new();
self.format(&mut out);
out
}
/// Format only the multipart body
fn format_body(&self, out: &mut Vec<u8>) {
let boundary = self.boundary();
for part in &self.parts {
out.extend_from_slice(b"--");
out.extend_from_slice(boundary.as_bytes());
out.extend_from_slice(b"\r\n");
part.format(out);
}
out.extend_from_slice(b"--");
out.extend_from_slice(boundary.as_bytes());
out.extend_from_slice(b"--\r\n");
}
}
impl EmailFormat for MultiPart {
fn format(&self, out: &mut Vec<u8>) {
write!(out, "{}", self.headers)
.expect("A Write implementation panicked while formatting headers");
out.extend_from_slice(b"\r\n");
self.format_body(out);
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn single_part_binary() {
let part = SinglePart::builder()
.header(header::ContentType::TEXT_PLAIN)
.header(header::ContentTransferEncoding::Binary)
.body(String::from("Текст письма в уникоде"));
assert_eq!(
String::from_utf8(part.formatted()).unwrap(),
concat!(
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"Текст письма в уникоде\r\n"
)
);
}
#[test]
fn single_part_quoted_printable() {
let part = SinglePart::builder()
.header(header::ContentType::TEXT_PLAIN)
.header(header::ContentTransferEncoding::QuotedPrintable)
.body(String::from("Текст письма в уникоде"));
assert_eq!(
String::from_utf8(part.formatted()).unwrap(),
concat!(
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: quoted-printable\r\n",
"\r\n",
"=D0=A2=D0=B5=D0=BA=D1=81=D1=82 =D0=BF=D0=B8=D1=81=D1=8C=D0=BC=D0=B0 =D0=B2 =\r\n",
"=D1=83=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5\r\n"
)
);
}
#[test]
fn single_part_base64() {
let part = SinglePart::builder()
.header(header::ContentType::TEXT_PLAIN)
.header(header::ContentTransferEncoding::Base64)
.body(String::from("Текст письма в уникоде"));
assert_eq!(
String::from_utf8(part.formatted()).unwrap(),
concat!(
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: base64\r\n",
"\r\n",
"0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LU=\r\n"
)
);
}
#[test]
fn multi_part_mixed() {
let part = MultiPart::mixed()
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.singlepart(
SinglePart::builder()
.header(header::ContentType::TEXT_PLAIN)
.header(header::ContentTransferEncoding::Binary)
.body(String::from("Текст письма в уникоде")),
)
.singlepart(
SinglePart::builder()
.header(header::ContentType::TEXT_PLAIN)
.header(header::ContentDisposition::attachment("example.c"))
.header(header::ContentTransferEncoding::Binary)
.body(String::from("int main() { return 0; }")),
);
assert_eq!(
String::from_utf8(part.formatted()).unwrap(),
concat!(
"Content-Type: multipart/mixed;\r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
"\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"Текст письма в уникоде\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Disposition: attachment; filename=\"example.c\"\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"int main() { return 0; }\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"
)
);
}
#[test]
fn multi_part_encrypted() {
let part = MultiPart::encrypted("application/pgp-encrypted".to_owned())
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.singlepart(
SinglePart::builder()
.header(header::ContentType::parse("application/pgp-encrypted").unwrap())
.body(String::from("Version: 1")),
)
.singlepart(
SinglePart::builder()
.header(
ContentType::parse("application/octet-stream; name=\"encrypted.asc\"")
.unwrap(),
)
.header(header::ContentDisposition::inline_with_name(
"encrypted.asc",
))
.body(String::from(concat!(
"-----BEGIN PGP MESSAGE-----\r\n",
"wV4D0dz5vDXklO8SAQdA5lGX1UU/eVQqDxNYdHa7tukoingHzqUB6wQssbMfHl8w\r\n",
"...\r\n",
"-----END PGP MESSAGE-----\r\n"
))),
);
assert_eq!(
String::from_utf8(part.formatted()).unwrap(),
concat!(
"Content-Type: multipart/encrypted;\r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\";\r\n",
" protocol=\"application/pgp-encrypted\"\r\n",
"\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: application/pgp-encrypted\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Version: 1\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: application/octet-stream; name=\"encrypted.asc\"\r\n",
"Content-Disposition: inline; filename=\"encrypted.asc\"\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"-----BEGIN PGP MESSAGE-----\r\n",
"wV4D0dz5vDXklO8SAQdA5lGX1UU/eVQqDxNYdHa7tukoingHzqUB6wQssbMfHl8w\r\n",
"...\r\n",
"-----END PGP MESSAGE-----\r\n",
"\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"
)
);
}
#[test]
fn multi_part_signed() {
let part = MultiPart::signed(
"application/pgp-signature".to_owned(),
"pgp-sha256".to_owned(),
)
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.singlepart(
SinglePart::builder()
.header(header::ContentType::TEXT_PLAIN)
.body(String::from("Test email for signature")),
)
.singlepart(
SinglePart::builder()
.header(
ContentType::parse("application/pgp-signature; name=\"signature.asc\"")
.unwrap(),
)
.header(header::ContentDisposition::attachment("signature.asc"))
.body(String::from(concat!(
"-----BEGIN PGP SIGNATURE-----\r\n",
"\r\n",
"iHUEARYIAB0WIQTNsp3S/GbdE0KoiQ+IGQOscREZuQUCXyOzDAAKCRCIGQOscREZ\r\n",
"udgDAQCv3FJ3QWW5bRaGZAa0Ug6vASFdkvDMKoRwcoFnHPthjQEAiQ8skkIyE2GE\r\n",
"PoLpAXiKpT+NU8S8+8dfvwutnb4dSwM=\r\n",
"=3FYZ\r\n",
"-----END PGP SIGNATURE-----\r\n",
))),
);
assert_eq!(
String::from_utf8(part.formatted()).unwrap(),
concat!(
"Content-Type: multipart/signed;\r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\";\r\n",
" protocol=\"application/pgp-signature\";",
" micalg=\"pgp-sha256\"\r\n",
"\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Test email for signature\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: application/pgp-signature; name=\"signature.asc\"\r\n",
"Content-Disposition: attachment; filename=\"signature.asc\"\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"-----BEGIN PGP SIGNATURE-----\r\n",
"\r\n",
"iHUEARYIAB0WIQTNsp3S/GbdE0KoiQ+IGQOscREZuQUCXyOzDAAKCRCIGQOscREZ\r\n",
"udgDAQCv3FJ3QWW5bRaGZAa0Ug6vASFdkvDMKoRwcoFnHPthjQEAiQ8skkIyE2GE\r\n",
"PoLpAXiKpT+NU8S8+8dfvwutnb4dSwM=\r\n",
"=3FYZ\r\n",
"-----END PGP SIGNATURE-----\r\n",
"\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"
)
);
}
#[test]
fn multi_part_alternative() {
let part = MultiPart::alternative()
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.singlepart(SinglePart::builder()
.header(header::ContentType::TEXT_PLAIN)
.header(header::ContentTransferEncoding::Binary)
.body(String::from("Текст письма в уникоде")))
.singlepart(SinglePart::builder()
.header(header::ContentType::TEXT_HTML)
.header(header::ContentTransferEncoding::Binary)
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")));
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
concat!("Content-Type: multipart/alternative;\r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
"\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"Текст письма в уникоде\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: text/html; charset=utf-8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"));
}
#[test]
fn multi_part_mixed_related() {
let part = MultiPart::mixed()
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.multipart(MultiPart::related()
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.singlepart(SinglePart::builder()
.header(header::ContentType::TEXT_HTML)
.header(header::ContentTransferEncoding::Binary)
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")))
.singlepart(SinglePart::builder()
.header(header::ContentType::parse("image/png").unwrap())
.header(header::ContentLocation::from(String::from("/image.png")))
.header(header::ContentTransferEncoding::Base64)
.body(String::from("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"))))
.singlepart(SinglePart::builder()
.header(header::ContentType::TEXT_PLAIN)
.header(header::ContentDisposition::attachment("example.c"))
.header(header::ContentTransferEncoding::Binary)
.body(String::from("int main() { return 0; }")));
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
concat!("Content-Type: multipart/mixed;\r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
"\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: multipart/related;\r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
"\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: text/html; charset=utf-8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: image/png\r\n",
"Content-Location: /image.png\r\n",
"Content-Transfer-Encoding: base64\r\n",
"\r\n",
"MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3\r\n",
"ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0\r\n",
"NTY3ODkwMTIzNDU2Nzg5MA==\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Disposition: attachment; filename=\"example.c\"\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"int main() { return 0; }\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"));
}
#[test]
fn test_make_boundary() {
let mut boundaries = std::collections::HashSet::with_capacity(10);
for _ in 0..1000 {
boundaries.insert(make_boundary());
}
// Ensure there are no duplicates
assert_eq!(1000, boundaries.len());
// Ensure correct length
for boundary in boundaries {
assert_eq!(40, boundary.len());
}
}
}

Some files were not shown because too many files have changed in this diff Show More