Compare commits

...

80 Commits

Author SHA1 Message Date
Paolo Barbolini
9567b23f4d Prepare 0.10.0-rc.5 (#750) 2022-04-02 10:21:38 +02:00
Paolo Barbolini
f77376fa19 Update to released email-encoding crate (#749) 2022-04-02 08:10:25 +00:00
Paolo Barbolini
6e35b9b30d Bump RustCrypto crates (#748) 2022-04-02 07:55:36 +00:00
Sven-Hendrik Haase
c24213c850 Add message logging to StubTransport (#744)
This makes it more useful as a testing tool as it now allows you to retrieve
all messages sent via this transport.
2022-03-25 08:22:47 +01:00
fluentpwn
8b40e438fd Year update (#725) 2022-03-24 06:06:24 +00:00
Paolo Barbolini
e1462b2d1b Bump MSRV to 1.56 - Edition 2021 (#745) 2022-03-24 05:52:28 +00:00
Paolo Barbolini
96b42515cd Don't run headers that don't need encoding though the encoder (#739) 2022-02-17 19:18:54 +00:00
Paolo Barbolini
1ea4987023 Encode mailbox headers through email-encoding (#737) 2022-02-17 20:00:43 +01:00
Paolo Barbolini
9273d24e54 Use nightly rustfmt features to improve code style (#734)
* format_code_in_doc_comments

* imports_granularity

* group_imports

* Add ci job
2022-02-12 20:03:37 +01:00
Paolo Barbolini
7a0dd5bd92 clippy: deny string_add (#735) 2022-02-12 17:12:41 +00:00
Paolo Barbolini
9a8aa46dba Start future proofing the DKIM API (#733) 2022-02-12 16:41:34 +00:00
Paolo Barbolini
0377ea29b7 Dkim improvements (#732)
Some tweaks to the DKIM implementation to make it a tiny bit more readable in some places and allocate less in general.
2022-02-12 16:19:06 +00:00
Paolo Barbolini
89e5b9083e Bump dev dependencies (#730) 2022-02-12 10:25:42 +00:00
Paolo Barbolini
8c370e28c9 Follow RFC 2231 in order to properly encode the Content-Disposition header (#685)
Uses RFC 2231 to encode the Content-Disposition header
2022-02-12 10:17:44 +00:00
Paolo Barbolini
3eed80ef30 Bump MSRV to 1.53 (#731) 2022-02-12 10:02:44 +00:00
Paolo Barbolini
dbb135c533 Introduce HeaderValue (#729) 2022-02-12 10:24:52 +01:00
Gaëtan Duchaussois
4c5f02b4f6 feat(email): add dkim signing capacity to message (#670) 2022-02-12 09:21:35 +01:00
Paolo Barbolini
f02542841c Bump rustls-pemfile to 0.3 (#728) 2022-02-05 18:34:13 +01:00
Jacob Halsey
29c34adc25 feature(transport-smtp): make peer certificate available in SmtpConnection (#716) 2022-01-19 11:28:34 +01:00
Kevin Cox
5e3ebbb189 Properly quote mailbox name (#700)
This quoting is performed according to https://datatracker.ietf.org/doc/html/rfc2822.

Note that the obsolete phrase specification allows periods in the mailbox name. This does not implement the obsolete specification, instead periods force the mailbox to use a quoted string.

Fixes #698
2021-11-17 18:28:34 +01:00
Dirkjan Ochtman
60399a93cc Forward first line of response for negative responses (#701) 2021-11-15 17:41:54 +01:00
facklambda
a48bc8a1b2 docs(smtp-transport) Add Troubleshooting steps (#692) 2021-11-12 21:53:12 +01:00
Paolo Barbolini
94cc0149d1 Prepare 0.10.0-rc.4 (#691) 2021-10-29 09:22:53 +02:00
Filip Gospodinov
a89383cdb6 Re-enable pool tests (#684)
The pool tests have been implicitly disabled by testing
for a feature that has been removed.
2021-10-20 19:31:56 +02:00
Filip Gospodinov
592593f4b8 Expose test_connected via transport (#677)
It is useful for application developers to validate SMTP
settings by testing the connection.

Co-authored-by: Alexis Mousset <contact@amousset.me>
2021-10-20 16:42:49 +02:00
Paolo Barbolini
97d3c760c0 Update rustls to 0.20 (#648) 2021-10-20 14:37:16 +02:00
Alexis Mousset
8f28b0c341 fix(transport): Use the sendmail command in PATH by default (#682)
This will allow the transport to work with default settings on more systems,
while preserving the ability to use a specific binary.
2021-10-16 09:49:45 +02:00
Paolo Barbolini
dc9c5df210 Sync pool impl (#644)
Co-authored-by: Alexis Mousset <contact@amousset.me>
2021-10-16 09:39:06 +02:00
Gaëtan Duchaussois
c9b3fa0baa docs(all): Add instruction to launch fake smtp server (#681) 2021-10-11 16:41:03 +02:00
Paolo Barbolini
addf8754dd smtp: don't send QUIT to connections that failed the STARTTLS handshake (#679) 2021-10-06 17:25:50 +00:00
Jacob Mischka
af157c5f26 Add From<Mime> under mime03 flag (#676)
Closes #615
2021-10-05 11:22:25 +02:00
TornaxO7
3e8988ae55 ContentTransferEncoding derives changes (#652) 2021-09-29 06:42:54 +02:00
Paolo Barbolini
941a00bcaa Bump MSRV to 1.52.1 (#671) 2021-09-12 18:40:26 +00:00
Paolo Barbolini
14079bff8c Give a compiletime error when using an incorrect combination of TLS features (#666) 2021-09-07 20:43:07 +02:00
Paolo Barbolini
696c06e8d7 Bump nom to v7 (#663) 2021-08-22 14:54:09 +02:00
Paolo Barbolini
d4f7618898 Better document how Attachments can be used (#658) 2021-08-20 08:17:36 +00:00
Christopher Vittal
e0a0a2e624 feat(address): Add TryFrom<String> for Address (#660)
Refactor the validation part of from_str into its own function as the
behavior for try_from is identical.
2021-08-20 09:09:49 +02:00
Paolo Barbolini
9ab6bb56d3 Fix broken <summary> style (#659) 2021-08-20 04:48:30 +02:00
Paolo Barbolini
e1d3778329 Revert "Allow a Message to be decomposed into a MessageBuilder (#633)" (#649)
This reverts commit aadcc0f83c.

Co-authored-by: Alexis Mousset <contact@amousset.me>
2021-08-02 12:20:02 +00:00
Paolo Barbolini
623d69c553 Fix #653 (#654) 2021-08-02 09:42:56 +00:00
Paolo Barbolini
55c2618201 Fix latest clippy warnings (#655) 2021-08-02 09:26:34 +00:00
Paolo Barbolini
9f550bce86 Bump MSRV to 1.49 (#656) 2021-08-02 09:26:25 +00:00
TornaxO7
e875d9ff64 ContentType Documentation (#642)
Co-authored-by: Paolo Barbolini <paolo@paolo565.org>
2021-07-06 11:46:42 +02:00
Alex Feldman-Crough
aadcc0f83c Allow a Message to be decomposed into a MessageBuilder (#633) 2021-06-30 18:21:55 +00:00
Paolo Barbolini
b534a18017 Async pool implementation (#637) 2021-06-29 15:23:47 +00:00
TornaxO7
0684bccd47 Implement Serialize and Deserialize for ContentType (#643) 2021-06-29 10:58:39 +02:00
Paolo Barbolini
4471759221 Implement connection timeouts for AsyncSmtpTransport (#635) 2021-06-17 22:39:48 +00:00
Paolo Barbolini
ed454819ee Refactor pool module (#636)
* Move pool to it's own module

* pool: deprecate configuring the connection timeout
2021-06-13 14:36:47 +02:00
Alexis Mousset
47cad567b0 Prepare 0.10.0-rc.3 (#629) 2021-05-22 19:52:38 +02:00
Alexis Mousset
b0e2fc9bca fix(transport-smtp): Fix transparency codec (#627)
It fails to add transparency when a period is preceded by two
successive CRLF.

Co-authored-by: Paolo Barbolini <paolo@paolo565.org>
2021-05-22 19:41:29 +02:00
Alexis Mousset
1d8249165c Makes more things private and add missing docs (#621) 2021-05-19 18:51:03 +02:00
Alexis Mousset
98fc0cb2f3 Prepare 0.10.0-rc.2 (#624) 2021-05-18 18:12:10 +02:00
Alexis Mousset
0439bab874 fix(builder): Don't include Bcc headers in formatted messages (#623)
fixes #622
2021-05-18 18:03:20 +02:00
Alexis Mousset
504fc51b26 Prepare 0.10.0-rc.1 (#620) 2021-05-14 17:48:42 +02:00
Paolo Barbolini
d54343cf00 Remove Part from the public API (#619) 2021-05-14 17:27:03 +02:00
Alexis Mousset
904789ac3d feat(builder): Add helper methods for attachments and text (#618) 2021-05-14 16:59:08 +02:00
Paolo Barbolini
94cae6df0d Drop tokio 0.2 support (#617) 2021-05-12 19:09:30 +02:00
Alexis Mousset
f17dccc46d builder: Fix Message-ID header (#614) 2021-05-04 18:45:29 +02:00
Paolo Barbolini
7e7f05eb45 Prepare 0.10.0-beta.4 (#613) 2021-05-04 18:31:55 +02:00
Paolo Barbolini
99df9e8d7c Headers insert_raw -> append_raw, set_raw -> insert_raw (#612) 2021-05-04 18:19:21 +02:00
Paolo Barbolini
1b5109b6ac Add docs to Headers (#610) 2021-05-02 11:10:04 +02:00
Alexis Mousset
a4be3c4cd8 Add InvalidHeaderName error (#608)
* Add InvalidHeaderName error
2021-05-01 22:00:33 +02:00
Paolo Barbolini
4586f2ad8a Remove useless clones (#609) 2021-05-01 18:22:53 +02:00
Paolo Barbolini
31de9e508b Replace hyperx Header and Headers with our own implementation (#607)
* Replace hyperx Header and Headers with our own implementation

* Remove utf8_b

* Add RFC 1522 encoder

* Fix most tests

* Throw away old tests

* Header encoding tests

* Fix slicing in the middle of a char

* Content-Disposition after rebase

* Fix the rest of the tests

* Fix useless clone clippy warnings

* Remove Headers::get_raw_mut

* HeaderName::new_from_ascii fallible API

* Tidy up HeaderName::new_from_ascii_str

* HeaderName::new_from_ascii(_str) tests
2021-05-01 13:27:00 +02:00
Paolo Barbolini
69334fe5eb Replace the hyperx ContentDisposition header with our own implementation (#601) 2021-04-24 18:21:29 +02:00
Paolo Barbolini
2ad2444183 Replace the hyperx ContentLocation header with our own implementation (#603) 2021-04-24 18:00:36 +02:00
Paolo Barbolini
8afa442e93 Add missing doc(cfg(..)) attributes (#604) 2021-04-20 19:09:28 +02:00
Paolo Barbolini
486e0f9d50 Replace hyperx ContentType header with our own implementation (#598)
* Replace hyperx ContentType header with our own implementation

* Let's not forget ContentTypeErr

* Adress code review comment
2021-04-08 08:40:07 +00:00
Paolo Barbolini
acc4ff4898 Replace hyperx Date header with our own implementation (#597) 2021-04-08 07:55:20 +02:00
Paolo Barbolini
1728d57c34 Stop using the uuid crate for generating the Message-Id (#602) 2021-04-07 18:38:56 +00:00
Alex Wennerberg
53bfb65423 Replace rand with fastrand (#600)
We don't need cryptographically secure random numbers, this simplifies
the dependency tree and speeds up builds.
2021-04-06 21:32:43 +02:00
Paolo Barbolini
61b08814c9 Avoid useless allocations while formatting headers (#599) 2021-04-06 17:02:37 +00:00
Paolo Barbolini
0e74042b4e Convert String Body line-endings to CRLF (#588) 2021-04-01 12:29:51 +02:00
Paolo Barbolini
29affe9398 Seal header contents (#591) 2021-04-01 12:18:38 +02:00
Jupp56
b10f6ff8de Fix: example does not compile (#592)
A missing simple string conversion prevented the example code from compiling.
2021-03-31 12:14:49 +02:00
Paolo Barbolini
2002a9d75a tls: use rustls if both native-tls and rustls-tls are enabled (#586)
Co-authored-by: Alexis Mousset <contact@amousset.me>
2021-03-30 09:06:26 +00:00
Paolo Barbolini
1193e1134d Bump MSRV to 1.46.0 (#587) 2021-03-30 08:40:05 +00:00
Alexis Mousset
7c6ade7afe Add a get started doc for transports (#577)
* Add a get started doc for transports

This will help users not familiar with email infrastructure.
2021-03-19 08:34:13 +00:00
Paolo Barbolini
3bc729ca64 Remove MockStream and all internal uses of it (#580)
Co-authored-by: Alexis Mousset <contact@amousset.me>
2021-03-19 08:20:05 +00:00
Paolo Barbolini
f041c00df7 Fix a few clippy warnings (#579) 2021-03-19 08:03:41 +00:00
67 changed files with 4535 additions and 1920 deletions

View File

@@ -7,11 +7,13 @@ on:
- master
env:
RUSTFLAGS: "--cfg lettre_ignore_tls_mismatch"
RUSTDOCFLAGS: "--cfg lettre_ignore_tls_mismatch"
RUST_BACKTRACE: full
jobs:
rustfmt:
name: rustfmt / stable
name: rustfmt / nightly-2022-02-11
runs-on: ubuntu-latest
steps:
@@ -20,7 +22,7 @@ jobs:
- name: Install rust
run: |
rustup update --no-self-update stable
rustup default nightly-2022-02-11
rustup component add rustfmt
- name: cargo fmt
@@ -79,8 +81,8 @@ jobs:
rust: stable
- name: beta
rust: beta
- name: 1.45.2
rust: 1.45.2
- name: 1.56.0
rust: 1.56.0
steps:
- name: Checkout
@@ -108,6 +110,20 @@ jobs:
- 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

View File

@@ -5,7 +5,7 @@
Several breaking changes were made between 0.9 and 0.10, but changes should be straightforward:
* MSRV is now 1.45.2
* 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,
@@ -14,7 +14,7 @@ Several breaking changes were made between 0.9 and 0.10, but changes should be s
#### Features
* Add `tokio` 0.2 and 1.0 support
* 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
@@ -31,7 +31,8 @@ Several breaking changes were made between 0.9 and 0.10, but changes should be s
* 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 `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`.
#### Bug Fixes

View File

@@ -1,7 +1,7 @@
[package]
name = "lettre"
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
version = "0.10.0-beta.3"
version = "0.10.0-rc.5"
description = "Email client"
readme = "README.md"
homepage = "https://lettre.rs"
@@ -10,7 +10,8 @@ license = "MIT"
authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo565.org>"]
categories = ["email", "network-programming"]
keywords = ["email", "smtp", "mailer", "message", "sendmail"]
edition = "2018"
edition = "2021"
rust-version = "1.56"
[badges]
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
@@ -19,32 +20,32 @@ maintenance = { status = "actively-developed" }
[dependencies]
idna = "0.2"
once_cell = "1"
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
# builder
hyperx = { version = "1", optional = true, features = ["headers"] }
httpdate = { version = "1", optional = true }
mime = { version = "0.3.4", optional = true }
uuid = { version = "0.8", features = ["v4"] }
rand = { version = "0.8", optional = true }
fastrand = { version = "1.4", optional = true }
quoted_printable = { version = "0.4", optional = true }
base64 = { version = "0.13", optional = true }
once_cell = "1"
regex = { version = "1", default-features = false, features = ["std", "unicode-case"] }
email-encoding = { version = "0.1", optional = true }
# file transport
uuid = { version = "0.8", features = ["v4"], optional = true }
serde = { version = "1", optional = true, features = ["derive"] }
serde_json = { version = "1", optional = true }
# smtp
nom = { version = "6", default-features = false, features = ["alloc", "std"], optional = true }
r2d2 = { version = "0.8", optional = true } # feature
nom = { version = "7", optional = true }
hostname = { version = "0.3", optional = true } # feature
## tls
native-tls = { version = "0.2", optional = true } # feature
rustls = { version = "0.19", features = ["dangerous_configuration"], optional = true }
webpki = { version = "0.21", optional = true }
webpki-roots = { version = "0.21", optional = true }
rustls = { version = "0.20", features = ["dangerous_configuration"], optional = true }
rustls-pemfile = { version = "0.3", optional = true }
webpki-roots = { version = "0.22", optional = true }
# async
futures-io = { version = "0.3.7", optional = true }
@@ -54,22 +55,23 @@ async-trait = { version = "0.1", optional = true }
## async-std
async-std = { version = "1.8", optional = true, features = ["unstable"] }
#async-native-tls = { version = "0.3.3", optional = true }
async-rustls = { version = "0.2", optional = true }
futures-rustls = { version = "0.22", optional = true }
## tokio
tokio02_crate = { package = "tokio", version = "0.2.7", features = ["fs", "process", "tcp", "dns", "io-util"], optional = true }
tokio02_native_tls_crate = { package = "tokio-native-tls", version = "0.1", optional = true }
tokio02_rustls = { package = "tokio-rustls", version = "0.15", optional = true }
tokio1_crate = { package = "tokio", version = "1", features = ["fs", "process", "net", "io-util"], optional = true }
tokio1_crate = { package = "tokio", version = "1", features = ["fs", "rt", "process", "time", "net", "io-util"], optional = true }
tokio1_native_tls_crate = { package = "tokio-native-tls", version = "0.3", optional = true }
tokio1_rustls = { package = "tokio-rustls", version = "0.22", optional = true }
tokio1_rustls = { package = "tokio-rustls", version = "0.23", optional = true }
## dkim
sha2 = { version = "0.10", optional = true }
rsa = { version = "0.6.0-pre", optional = true }
ed25519-dalek = { version = "1.0.1", optional = true }
[dev-dependencies]
criterion = "0.3"
tracing-subscriber = "0.2.10"
tracing-subscriber = "0.3"
glob = "0.3"
walkdir = "2"
tokio02_crate = { package = "tokio", version = "0.2.7", features = ["macros", "rt-threaded"] }
tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] }
async-std = { version = "1.8", features = ["attributes"] }
serde_json = "1"
@@ -80,31 +82,33 @@ harness = false
name = "transport_smtp"
[features]
default = ["smtp-transport", "native-tls", "hostname", "r2d2", "builder"]
builder = ["mime", "base64", "hyperx", "rand", "quoted_printable"]
default = ["smtp-transport", "pool", "native-tls", "hostname", "builder"]
builder = ["httpdate", "mime", "base64", "fastrand", "quoted_printable", "email-encoding"]
mime03 = ["mime"]
# transports
file-transport = []
file-transport = ["uuid"]
file-transport-envelope = ["serde", "serde_json", "file-transport"]
sendmail-transport = []
smtp-transport = ["base64", "nom"]
rustls-tls = ["webpki", "webpki-roots", "rustls"]
pool = ["futures-util"]
rustls-tls = ["webpki-roots", "rustls", "rustls-pemfile"]
# async
async-std1 = ["async-std", "async-trait", "futures-io", "futures-util"]
#async-std1-native-tls = ["async-std1", "native-tls", "async-native-tls"]
async-std1-rustls-tls = ["async-std1", "rustls-tls", "async-rustls"]
tokio02 = ["tokio02_crate", "async-trait", "futures-io", "futures-util"]
tokio02-native-tls = ["tokio02", "native-tls", "tokio02_native_tls_crate"]
tokio02-rustls-tls = ["tokio02", "rustls-tls", "tokio02_rustls"]
async-std1-rustls-tls = ["async-std1", "rustls-tls", "futures-rustls"]
tokio1 = ["tokio1_crate", "async-trait", "futures-io", "futures-util"]
tokio1-native-tls = ["tokio1", "native-tls", "tokio1_native_tls_crate"]
tokio1-rustls-tls = ["tokio1", "rustls-tls", "tokio1_rustls"]
dkim = ["sha2", "rsa", "ed25519-dalek"]
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
rustdoc-args = ["--cfg", "docsrs", "--cfg", "lettre_ignore_tls_mismatch"]
[[example]]
name = "basic_html"
@@ -130,14 +134,6 @@ required-features = ["smtp-transport", "native-tls", "builder"]
name = "smtp_selfsigned"
required-features = ["smtp-transport", "native-tls", "builder"]
[[example]]
name = "tokio02_smtp_tls"
required-features = ["smtp-transport", "tokio02", "tokio02-native-tls", "builder"]
[[example]]
name = "tokio02_smtp_starttls"
required-features = ["smtp-transport", "tokio02", "tokio02-native-tls", "builder"]
[[example]]
name = "tokio1_smtp_tls"
required-features = ["smtp-transport", "tokio1", "tokio1-native-tls", "builder"]

View File

@@ -1,5 +1,5 @@
Copyright (c) 2014-2020 Alexis Mousset <contact@amousset.me>
Copyright (c) 2019-2020 Paolo Barbolini <paolo@paolo565.org>
Copyright (c) 2014-2022 Alexis Mousset <contact@amousset.me>
Copyright (c) 2019-2022 Paolo Barbolini <paolo@paolo565.org>
Copyright (c) 2018 K. <kayo@illumium.org>
Permission is hereby granted, free of charge, to any

View File

@@ -28,8 +28,8 @@
</div>
<div align="center">
<a href="https://deps.rs/crate/lettre/0.10.0-beta.3">
<img src="https://deps.rs/crate/lettre/0.10.0-beta.3/status.svg"
<a href="https://deps.rs/crate/lettre/0.10.0-rc.5">
<img src="https://deps.rs/crate/lettre/0.10.0-rc.5/status.svg"
alt="dependency status" />
</a>
</div>
@@ -37,10 +37,15 @@
---
**NOTE**: this readme refers to the 0.10 version of lettre, which is
still being worked on. The master branch and the alpha releases will see
API breaking changes and some features may be missing or incomplete until
the stable 0.10.0 release is out.
Use the [`v0.9.x`](https://github.com/lettre/lettre/tree/v0.9.x) branch for stable releases.
in release candidate state. Use the [`v0.9.x`](https://github.com/lettre/lettre/tree/v0.9.x)
branch for the previous stable release.
0.10 is already widely used and is already thought to be more reliable than 0.9, so it should generally be used
for new projects.
We'd love to hear your feedback about 0.10 design and APIs before final release!
Start a [discussion](https://github.com/lettre/lettre/discussions) in the repository, whether for
feedback or if you need help or advice using or upgrading lettre 0.10.
---
@@ -60,13 +65,13 @@ Lettre does not provide (for now):
## Example
This library requires Rust 1.45 or newer.
This library requires Rust 1.56.0 or newer.
To use this library, add the following to your `Cargo.toml`:
```toml
[dependencies]
lettre = "0.10.0-beta.3"
lettre = "0.10.0-rc.5"
```
```rust,no_run
@@ -78,7 +83,7 @@ let email = Message::builder()
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.body("Be happy!")
.body(String::from("Be happy!"))
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
@@ -98,10 +103,22 @@ match mailer.send(&email) {
## Testing
The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command.
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 localhost: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.
## Code of conduct
Anyone who interacts with Lettre in any space, including but not limited to

View File

@@ -14,7 +14,7 @@ This folder contains examples showing how to use lettre in your own projects.
- [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 `tokio02_`, `tokio1_` or `asyncstd1_`.
These files are prefixed with `tokio1_` or `asyncstd1_`.
[basic_html.rs]: ./basic_html.rs
[maud_html.rs]: ./maud_html.rs

View File

@@ -29,16 +29,12 @@ fn main() {
MultiPart::alternative() // This is composed of two parts.
.singlepart(
SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.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; charset=utf8".parse().unwrap(),
))
.header(header::ContentType::TEXT_HTML)
.body(String::from(html)),
),
)

View File

@@ -38,16 +38,12 @@ fn main() {
MultiPart::alternative() // This is composed of two parts.
.singlepart(
SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.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; charset=utf8".parse().unwrap(),
))
.header(header::ContentType::TEXT_HTML)
.body(html.into_string()),
),
)

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 tokio02_crate as tokio;
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message,
Tokio02Executor,
};
#[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")
.body(String::from("Be happy with async!"))
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
// Open a remote connection to gmail using STARTTLS
let mailer: AsyncSmtpTransport<Tokio02Executor> =
AsyncSmtpTransport::<Tokio02Executor>::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 tokio02_crate as tokio;
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message,
Tokio02Executor,
};
#[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")
.body(String::from("Be happy with async!"))
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
// Open a remote connection to gmail
let mailer: AsyncSmtpTransport<Tokio02Executor> =
AsyncSmtpTransport::<Tokio02Executor>::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,12 +1,11 @@
// 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 tokio1_crate as tokio;
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message,
Tokio1Executor,
};
use tokio1_crate as tokio;
#[tokio::main]
async fn main() {

View File

@@ -1,12 +1,11 @@
// 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 tokio1_crate as tokio;
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message,
Tokio1Executor,
};
use tokio1_crate as tokio;
#[tokio::main]
async fn main() {

3
rustfmt.toml Normal file
View File

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

View File

@@ -1,6 +1,3 @@
#[cfg(feature = "builder")]
use std::convert::TryFrom;
use super::Address;
#[cfg(feature = "builder")]
use crate::message::header::{self, Headers};
@@ -112,15 +109,16 @@ impl TryFrom<&Headers> for Envelope {
fn try_from(headers: &Headers) -> Result<Self, Self::Error> {
let from = match headers.get::<header::Sender>() {
// If there is a Sender, use it
Some(header::Sender(a)) => Some(a.email.clone()),
Some(sender) => Some(Mailbox::from(sender).email),
// ... else try From
None => match headers.get::<header::From>() {
Some(header::From(a)) => {
let from: Vec<Mailbox> = a.clone().into();
let mut from: Vec<Mailbox> = a.into();
if from.len() > 1 {
return Err(Error::TooManyFrom);
}
Some(from[0].email.clone())
let from = from.pop().expect("From header has 1 Mailbox");
Some(from.email)
}
None => None,
},
@@ -128,18 +126,16 @@ impl TryFrom<&Headers> for Envelope {
fn add_addresses_from_mailboxes(
addresses: &mut Vec<Address>,
mailboxes: Option<&Mailboxes>,
mailboxes: Option<Mailboxes>,
) {
if let Some(mailboxes) = mailboxes {
for mailbox in mailboxes.iter() {
addresses.push(mailbox.email.clone());
}
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));
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,10 +1,6 @@
//! Representation of an email address
use idna::domain_to_ascii;
use once_cell::sync::Lazy;
use regex::Regex;
use std::{
convert::{TryFrom, TryInto},
error::Error,
ffi::OsStr,
fmt::{Display, Formatter, Result as FmtResult},
@@ -12,6 +8,10 @@ use std::{
str::FromStr,
};
use idna::domain_to_ascii;
use once_cell::sync::Lazy;
use regex::Regex;
/// Represents an email address with a user and a domain name.
///
/// This type contains email in canonical form (_user@domain.tld_).
@@ -174,15 +174,10 @@ impl FromStr for Address {
type Err = AddressError;
fn from_str(val: &str) -> Result<Self, 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)?;
let at_start = check_address(val)?;
Ok(Address {
serialized: val.into(),
at_start: user.len(),
at_start,
})
}
}
@@ -209,6 +204,18 @@ where
}
}
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
@@ -221,14 +228,27 @@ impl AsRef<OsStr> for Address {
}
}
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, Clone, Copy)]
/// 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,
InvalidUtf8b,
}
impl Error for AddressError {}
@@ -240,7 +260,6 @@ impl Display for AddressError {
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::InvalidUtf8b => f.write_str("Invalid UTF8b data"),
}
}
}

View File

@@ -1,29 +1,35 @@
use async_trait::async_trait;
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 = "tokio02", feature = "tokio1", feature = "async-std1")
any(feature = "tokio1", feature = "async-std1")
))]
use crate::transport::smtp::client::AsyncSmtpConnection;
#[cfg(all(
feature = "smtp-transport",
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
any(feature = "tokio1", feature = "async-std1")
))]
use crate::transport::smtp::client::Tls;
#[cfg(all(
feature = "smtp-transport",
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
any(feature = "tokio1", feature = "async-std1")
))]
use crate::transport::smtp::extension::ClientId;
#[cfg(all(
feature = "smtp-transport",
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
any(feature = "tokio1", feature = "async-std1")
))]
use crate::transport::smtp::Error;
@@ -35,17 +41,31 @@ use crate::transport::smtp::Error;
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))
)]
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
#[async_trait]
pub trait Executor: Debug + Send + Sync + private::Sealed {
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>;
@@ -59,70 +79,11 @@ pub trait Executor: Debug + Send + Sync + private::Sealed {
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()>;
}
/// Async [`Executor`] using `tokio` `0.2.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 = "tokio02")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio02")))]
#[derive(Debug)]
pub struct Tokio02Executor;
#[doc(hidden)]
#[cfg(feature = "smtp-transport")]
#[async_trait]
#[cfg(feature = "tokio02")]
impl Executor for Tokio02Executor {
#[doc(hidden)]
#[cfg(feature = "smtp-transport")]
async fn connect(
hostname: &str,
port: u16,
hello_name: &ClientId,
tls: &Tls,
) -> Result<AsyncSmtpConnection, Error> {
#[allow(clippy::match_single_binding)]
let tls_parameters = match tls {
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()),
_ => None,
};
#[allow(unused_mut)]
let mut conn =
AsyncSmtpConnection::connect_tokio02(hostname, port, hello_name, tls_parameters)
.await?;
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
match tls {
Tls::Opportunistic(ref tls_parameters) => {
if conn.can_starttls() {
conn.starttls(tls_parameters.clone(), hello_name).await?;
}
}
Tls::Required(ref tls_parameters) => {
conn.starttls(tls_parameters.clone(), hello_name).await?;
}
_ => (),
}
Ok(conn)
}
#[doc(hidden)]
#[cfg(feature = "file-transport-envelope")]
async fn fs_read(path: &Path) -> IoResult<Vec<u8>> {
tokio02_crate::fs::read(path).await
}
#[doc(hidden)]
#[cfg(feature = "file-transport")]
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> {
tokio02_crate::fs::write(path, contents).await
}
pub trait SpawnHandle: Debug + Send + Sync + 'static + private::Sealed {
async fn shutdown(self);
}
/// Async [`Executor`] using `tokio` `1.x`
@@ -143,11 +104,33 @@ 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;
#[doc(hidden)]
#[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)
}
#[doc(hidden)]
#[cfg(feature = "smtp-transport")]
fn sleep(duration: Duration) -> Self::Sleep {
tokio1_crate::time::sleep(duration)
}
#[doc(hidden)]
#[cfg(feature = "smtp-transport")]
async fn connect(
hostname: &str,
port: u16,
timeout: Option<Duration>,
hello_name: &ClientId,
tls: &Tls,
) -> Result<AsyncSmtpConnection, Error> {
@@ -158,8 +141,13 @@ impl Executor for Tokio1Executor {
_ => None,
};
#[allow(unused_mut)]
let mut conn =
AsyncSmtpConnection::connect_tokio1(hostname, port, hello_name, tls_parameters).await?;
let mut conn = AsyncSmtpConnection::connect_tokio1(
(hostname, port),
timeout,
hello_name,
tls_parameters,
)
.await?;
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
match tls {
@@ -190,6 +178,14 @@ impl Executor for Tokio1Executor {
}
}
#[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`]
@@ -208,11 +204,34 @@ 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, ()>;
#[doc(hidden)]
#[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)
}
#[doc(hidden)]
#[cfg(feature = "smtp-transport")]
fn sleep(duration: Duration) -> Self::Sleep {
let fut = async move { async_std::task::sleep(duration).await };
Box::pin(fut)
}
#[doc(hidden)]
#[cfg(feature = "smtp-transport")]
async fn connect(
hostname: &str,
port: u16,
timeout: Option<Duration>,
hello_name: &ClientId,
tls: &Tls,
) -> Result<AsyncSmtpConnection, Error> {
@@ -223,9 +242,13 @@ impl Executor for AsyncStd1Executor {
_ => None,
};
#[allow(unused_mut)]
let mut conn =
AsyncSmtpConnection::connect_asyncstd1(hostname, port, hello_name, tls_parameters)
.await?;
let mut conn = AsyncSmtpConnection::connect_asyncstd1(
(hostname, port),
timeout,
hello_name,
tls_parameters,
)
.await?;
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
match tls {
@@ -256,17 +279,28 @@ impl Executor for AsyncStd1Executor {
}
}
#[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 {
use super::*;
pub trait Sealed {}
#[cfg(feature = "tokio02")]
impl Sealed for Tokio02Executor {}
#[cfg(feature = "tokio1")]
impl Sealed for Tokio1Executor {}
#[cfg(feature = "async-std1")]
impl Sealed for 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

@@ -6,7 +6,7 @@
//! * Secure defaults
//! * Async support
//!
//! Lettre requires Rust 1.45 or newer.
//! Lettre requires Rust 1.56.0 or newer.
//!
//! ## Features
//!
@@ -27,7 +27,7 @@
//! _Send emails using [`SMTP`]_
//!
//! * **smtp-transport** 📫: Enable the SMTP transport
//! * **r2d2** 📫: Connection pool for 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
@@ -37,7 +37,6 @@
//! Uses schannel on Windows, Security-Framework on macOS, and OpenSSL on Linux.
//!
//! * **native-tls** 📫: TLS support for the synchronous version of the API
//! * **tokio02-native-tls**: TLS support for the `tokio02` async 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`
@@ -49,7 +48,6 @@
//! 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
//! * **tokio02-rustls-tls**: TLS support for the `tokio02` async 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
//!
@@ -71,11 +69,10 @@
//! _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.3.0 context, the Tokio 1.x executor
//! 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.
//!
//! * **tokio02**: Allow to asynchronously send emails using [Tokio 0.2.x]
//! * **tokio1**: Allow to asynchronously send emails using [Tokio 1.x]
//! * **async-std1**: Allow to asynchronously send emails using [async-std 1.x]
//!
@@ -87,19 +84,23 @@
//!
//! * **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 0.2.x]: https://docs.rs/tokio/0.2
//! [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.10.0-beta.3")]
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.0-rc.5")]
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
#![forbid(unsafe_code)]
@@ -109,32 +110,80 @@
trivial_numeric_casts,
unstable_features,
unused_import_braces,
rust_2018_idioms
rust_2018_idioms,
clippy::string_add,
clippy::string_add_assign
)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#[cfg(not(lettre_ignore_tls_mismatch))]
mod compiletime_checks {
#[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'd like to use rustls make sure that the `native-tls` hasn't been enabled by mistake (you may need to import lettre without default features)
If you're building a library which depends on lettre import it without default features and enable just the features you need.");
#[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` hasn't been enabled by mistake.
If you're building a library which depends on lettre import it without default features and enable just the features you need.");
/*
#[cfg(all(
feature = "async-std1",
feature = "native-tls",
not(feature = "async-std1-native-tls")
))]
compile_error!("Lettre is being built with the `async-std1` and the `native-tls` features, but the `async-std1-native-tls` feature hasn't been turned on.
If you'd like to use rustls make sure that the `native-tls` hasn't been enabled by mistake (you may need to import lettre without default features)
If you're building a library which depends on lettre import it without default features and enable just the features you need.");
*/
#[cfg(all(
feature = "async-std1",
feature = "native-tls",
not(feature = "async-std1-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'd like to use rustls make sure that the `native-tls` hasn't been enabled by mistake (you may need to import lettre without default features)
If you're building a library which depends on lettre import lettre without default features and enable just the features you need.");
#[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 (you may need to import lettre without default features)
If you're building a library which depends on lettre import it without default features and enable just the features you need.");
}
pub mod address;
pub mod error;
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
#[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;
#[cfg(feature = "builder")]
#[macro_use]
extern crate hyperx;
use std::error::Error as StdError;
#[cfg(feature = "async-std1")]
pub use self::executor::AsyncStd1Executor;
#[cfg(all(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))]
#[cfg(all(any(feature = "tokio1", feature = "async-std1")))]
pub use self::executor::Executor;
#[cfg(feature = "tokio02")]
pub use self::executor::Tokio02Executor;
#[cfg(feature = "tokio1")]
pub use self::executor::Tokio1Executor;
#[cfg(all(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))]
#[cfg(all(any(feature = "tokio1", feature = "async-std1")))]
#[doc(inline)]
pub use self::transport::AsyncTransport;
pub use crate::address::Address;
@@ -143,7 +192,7 @@ pub use crate::address::Address;
pub use crate::message::Message;
#[cfg(all(
feature = "file-transport",
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
any(feature = "tokio1", feature = "async-std1")
))]
#[doc(inline)]
pub use crate::transport::file::AsyncFileTransport;
@@ -152,7 +201,7 @@ pub use crate::transport::file::AsyncFileTransport;
pub use crate::transport::file::FileTransport;
#[cfg(all(
feature = "sendmail-transport",
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
any(feature = "tokio1", feature = "async-std1")
))]
#[doc(inline)]
pub use crate::transport::sendmail::AsyncSendmailTransport;
@@ -161,26 +210,22 @@ pub use crate::transport::sendmail::AsyncSendmailTransport;
pub use crate::transport::sendmail::SendmailTransport;
#[cfg(all(
feature = "smtp-transport",
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
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};
#[cfg(feature = "smtp-transport")]
pub use crate::transport::smtp::SmtpTransport;
use std::error::Error as StdError;
pub(crate) type BoxError = Box<dyn StdError + Send + Sync>;
#[cfg(test)]
#[cfg(feature = "builder")]
mod test {
use super::*;
use crate::message::{header, Mailbox, Mailboxes};
use hyperx::header::Headers;
use std::convert::TryFrom;
use crate::message::{header, header::Headers, Mailbox, Mailboxes};
#[test]
fn envelope_from_headers() {
@@ -208,9 +253,9 @@ mod test {
let to = Mailboxes::new().with("amousset@example.com".parse().unwrap());
let mut headers = Headers::new();
headers.set(header::From(from));
headers.set(header::Sender(sender));
headers.set(header::To(to));
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(),
@@ -228,8 +273,8 @@ mod test {
let sender = Mailbox::new(None, "kayo2@example.com".parse().unwrap());
let mut headers = Headers::new();
headers.set(header::From(from));
headers.set(header::Sender(sender));
headers.set(header::From::from(from));
headers.set(header::Sender::from(sender));
assert!(Envelope::try_from(&headers).is_err(),);
}

145
src/message/attachment.rs Normal file
View File

@@ -0,0 +1,145 @@
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,5 +1,6 @@
use std::{
io::{self, Write},
mem,
ops::Deref,
};
@@ -18,7 +19,9 @@ pub struct Body {
/// 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),
}
@@ -30,13 +33,16 @@ impl Body {
/// 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 buf: MaybeString = buf.into();
let mut buf: MaybeString = buf.into();
let encoding = buf.encoding();
buf.encode_crlf();
Self::new_impl(buf.into(), encoding)
}
@@ -44,6 +50,8 @@ impl Body {
///
/// [`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.
@@ -51,12 +59,13 @@ impl Body {
buf: B,
encoding: ContentTransferEncoding,
) -> Result<Self, Vec<u8>> {
let buf: MaybeString = buf.into();
let mut buf: MaybeString = buf.into();
if !buf.is_encoding_ok(encoding) {
return Err(buf.into());
}
buf.encode_crlf();
Ok(Self::new_impl(buf.into(), encoding))
}
@@ -162,12 +171,20 @@ impl MaybeString {
}
}
/// 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(_) => {}
}
}
/// Returns `true` if using `encoding` to encode this `MaybeString`
/// would result into an invalid encoded body.
fn is_encoding_ok(&self, encoding: ContentTransferEncoding) -> bool {
match encoding {
ContentTransferEncoding::SevenBit => is_7bit_encoded(&self),
ContentTransferEncoding::EightBit => is_8bit_encoded(&self),
ContentTransferEncoding::SevenBit => is_7bit_encoded(self),
ContentTransferEncoding::EightBit => is_8bit_encoded(self),
ContentTransferEncoding::Binary
| ContentTransferEncoding::QuotedPrintable
| ContentTransferEncoding::Base64 => true,
@@ -189,6 +206,7 @@ impl MaybeString {
/// **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;
}
@@ -322,9 +340,44 @@ where
}
}
/// 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 super::{Body, ContentTransferEncoding};
use super::{in_place_crlf_line_endings, Body, ContentTransferEncoding};
#[test]
fn seven_bit_detect() {
@@ -578,4 +631,31 @@ mod test {
.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😀");
}
}

528
src/message/dkim.rs Normal file
View File

@@ -0,0 +1,528 @@
use std::{
borrow::Cow,
error::Error as StdError,
fmt::{self, Display, Write},
iter::IntoIterator,
time::SystemTime,
};
use ed25519_dalek::Signer;
use once_cell::sync::Lazy;
use regex::{bytes::Regex as BRegex, Regex};
use rsa::{pkcs1::DecodeRsaPrivateKey, Hash, PaddingScheme, 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::Keypair),
}
impl DkimSigningKey {
pub fn new(
private_key: String,
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::Keypair::from_bytes(&base64::decode(private_key).map_err(
|err| DkimSigningKeyError(InnerDkimSigningKeyError::Base64(err)),
)?)
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Ed25519(err)))?,
)
}
}))
}
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
/// selector: the name of the key publied in DNS
/// domain: the domain for which we sign the message
/// private_key: private key in PKCS1 string format
/// headers: a list of headers name to be included in the signature. Signing of more than one
/// header with same name is not supported
/// canonicalization: the canonicalization to be applied on the message
/// pub signing_algorithm: the signing algorithm to be used when signing
#[derive(Debug)]
pub struct DkimConfig {
selector: String,
domain: String,
private_key: DkimSigningKey,
headers: Vec<HeaderName>,
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(
body: &[u8],
canonicalization: DkimCanonicalizationType,
) -> Cow<'_, [u8]> {
static RE: Lazy<BRegex> = Lazy::new(|| BRegex::new("(\r\n)+$").unwrap());
static RE_DOUBLE_SPACE: Lazy<BRegex> = Lazy::new(|| BRegex::new("[\\t ]+").unwrap());
static RE_SPACE_EOL: Lazy<BRegex> = Lazy::new(|| BRegex::new("[\t ]\r\n").unwrap());
match canonicalization {
DkimCanonicalizationType::Simple => RE.replace(body, &b"\r\n"[..]),
DkimCanonicalizationType::Relaxed => {
let body = RE_DOUBLE_SPACE.replace_all(body, &b" "[..]);
let body = match RE_SPACE_EOL.replace_all(&body, &b"\r\n"[..]) {
Cow::Borrowed(_body) => body,
Cow::Owned(body) => Cow::Owned(body),
};
match RE.replace(&body, &b"\r\n"[..]) {
Cow::Borrowed(_body) => body,
Cow::Owned(body) => Cow::Owned(body),
}
}
}
}
/// Canonicalize the value of an header
fn dkim_canonicalize_header_value(
value: &str,
canonicalization: DkimCanonicalizationType,
) -> Cow<'_, str> {
match canonicalization {
DkimCanonicalizationType::Simple => Cow::Borrowed(value),
DkimCanonicalizationType::Relaxed => {
static RE_EOL: Lazy<Regex> = Lazy::new(|| Regex::new("\r\n").unwrap());
static RE_SPACES: Lazy<Regex> = Lazy::new(|| Regex::new("[\\t ]+").unwrap());
let value = RE_EOL.replace_all(value, "");
Cow::Owned(format!(
"{}\r\n",
RE_SPACES.replace_all(&value, " ").trim_end()
))
}
}
}
/// 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 {
match canonicalization {
DkimCanonicalizationType::Simple => {
let mut signed_headers = Headers::new();
for h in headers_list {
let h = dkim_canonicalize_header_tag(h, canonicalization);
if let Some(value) = mail_headers.get_raw(&h) {
signed_headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii(h.into()).unwrap(),
dkim_canonicalize_header_value(value, canonicalization).to_string(),
))
}
}
signed_headers.to_string()
}
DkimCanonicalizationType::Relaxed => {
let mut signed_headers = String::new();
for h in headers_list {
let h = dkim_canonicalize_header_tag(h, canonicalization);
if let Some(value) = mail_headers.get_raw(&h) {
write!(
signed_headers,
"{}:{}",
h,
dkim_canonicalize_header_value(value, canonicalization)
)
.expect("write implementation returned an error")
}
}
signed_headers
}
}
}
/// Sign with Dkim a message by adding Dkim-Signture header created with configuration expressed by
/// dkim_config
pub(super) fn dkim_sign(message: &mut Message, dkim_config: &DkimConfig) {
let timestamp = SystemTime::now()
.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 = 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) => base64::encode(
private_key
.sign(
PaddingScheme::new_pkcs1v15_sign(Some(Hash::SHA2_256)),
&hashed_headers,
)
.unwrap(),
),
InnerDkimSigningKey::Ed25519(private_key) => {
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 std::{
io::Write,
process::{Command, Stdio},
};
use super::{
super::{
header::{HeaderName, HeaderValue},
Header, Message,
},
dkim_canonicalize_body, dkim_canonicalize_header_value, dkim_canonicalize_headers,
DkimCanonicalizationType, DkimConfig, DkimSigningAlgorithm, DkimSigningKey,
};
use crate::StdError;
#[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())
}
}
#[test]
fn test_body_simple_canonicalize() {
let body = b"test\r\n\r\ntest \ttest\r\n\r\n\r\n";
let expected: &[u8] = b"test\r\n\r\ntest \ttest\r\n";
assert_eq!(
dkim_canonicalize_body(body, DkimCanonicalizationType::Simple),
expected
)
}
#[test]
fn test_body_relaxed_canonicalize() {
let body = b"test\r\n\r\ntest \ttest\r\n\r\n\r\n";
let expected: &[u8] = b"test\r\n\r\ntest test\r\n";
assert_eq!(
dkim_canonicalize_body(body, DkimCanonicalizationType::Relaxed),
expected
)
}
#[test]
fn test_header_simple_canonicalize() {
let value = "test\r\n\r\ntest \ttest\r\n";
let expected = "test\r\n\r\ntest \ttest\r\n";
assert_eq!(
dkim_canonicalize_header_value(value, DkimCanonicalizationType::Simple),
expected
)
}
#[test]
fn test_header_relaxed_canonicalize() {
let value = "test\r\n\r\ntest \ttest\r\n";
let expected = "testtest test\r\n";
assert_eq!(
dkim_canonicalize_header_value(value, DkimCanonicalizationType::Relaxed),
expected
)
}
fn test_message() -> Message {
Message::builder()
.from("Test <test+ezrz@example.net>".parse().unwrap())
.to("Test2 <test2@example.org>".parse().unwrap())
.header(TestHeader("test test very very long with spaces and extra spaces \twill be folded to several lines ".to_string()))
.subject("Test with utf-8 ë")
.body("test\r\n\r\ntest \ttest\r\n\r\n\r\n".to_string()).unwrap()
}
#[test]
fn test_headers_simple_canonicalize() {
let message = test_message();
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Simple),"From: Test <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();
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Relaxed),"from:Test <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_signature_rsa() {
let mut message = test_message();
let key = "-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAz+FHbM8BwkBBz/Ux5OYLQ5Bp1HVuCHTP6Rr3HXTnome/2cGl
/ze0tsmmFbCjjsS89MXbMGs9xJhjv18LmL1N0UTllblOizzVjorQyN4RwBOfG34j
7SS56pwzrA738Ry8FAbL5InPWEgVzbOhXuTCs8yuzcqTnm4sH/csnIl7cMWeQkVn
1FR9LKMtUG0fjhDPkdX0jx3qTX1L3Z7a7gX6geY191yNd9i9DvE2/+wMigMYz1LA
ts4alk2g86MQhtbjc8AOR7EC15hSw37/lmamlunYLa3wC+PzHNMA8sAfnmkgNvip
ssjh8LnelD9qn+VtsjQB5ppkeQx3TcUPvz5z+QIDAQABAoIBAQCzRa5ZEbSMlumq
s+PRaOox3CrIRHUd6c8bUlvmFVllX1++JRhInvvD3ubSMcD7cIMb/D1o5jMgheMP
uKHBmQ+w91+e3W30+gOZp/EiKRDZupIuHXxSGKgUwZx2N3pvfr5b7viLIKWllpTn
DpCNy251rIDbjGX97Tk0X+8jGBVSTCxtruGJR5a+hz4t9Z7bz7JjZWcRNJC+VA+Q
ATjnV7AHO1WR+0tAdPJaHsRLI7drKFSqTYq0As+MksZ40p7T6blZW8NUXA09fJRn
3mP2TZdWjjfBXZje026v4T7TZl+TELKw5WirL/UJ8Zw8dGGV6EZvbfMacZuUB1YQ
0vZnGe4BAoGBAO63xWP3OV8oLAMF90umuusPaQNSc6DnpjnP+sTAcXEYJA0Sa4YD
y8dpTAdFJ4YvUQhLxtbZFK5Ih3x7ZhuerLSJiZiDPC2IJJb7j/812zQQriOi4mQ8
bimxM4Nzql8FKGaXMppE5grFLsy8tw7neIM9KE4uwe9ajwJrRrOTUY8ZAoGBAN7t
+xFeuhg4F9expyaPpCvKT2YNAdMcDzpm7GtLX292u+DQgBfg50Ur9XmbS+RPlx1W
r2Sw3bTjRjJU9QnSZLL2w3hiii/wdaePI4SCaydHdLi4ZGz/pNUsUY+ck2pLptS0
F7rL+s9MV9lUyhvX+pIh+O3idMWAdaymzs7ZlgfhAoGAVoFn2Wrscmw3Tr0puVNp
JudFsbt+RU/Mr+SLRiNKuKX74nTLXBwiC1hAAd5wjTK2VaBIJPEzilikKFr7TIT6
ps20e/0KoKFWSRROQTh9/+cPg8Bx88rmTNt3BGq00Ywn8M1XvAm9pyd/Zxf36kG9
LSnLYlGVW6xgaIsBau+2vXkCgYAeChVdxtTutIhJ8U9ju9FUcUN3reMEDnDi3sGW
x6ZJf8dbSN0p2o1vXbgLNejpD+x98JNbzxVg7Ysk9xu5whb9opC+ZRDX2uAPvxL7
JRPJTDCnP3mQ0nXkn78xydh3Z1BIsyfLbPcT/eaMi4dcbyL9lARWEcDIaEHzDNsr
NlioIQKBgQCXIZp5IBfG5WSXzFk8xvP4BUwHKEI5bttClBmm32K+vaSz8qO6ak6G
4frg+WVopFg3HBHdK9aotzPEd0eHMXJv3C06Ynt2lvF+Rgi/kwGbkuq/mFVnmYYR
Fz0TZ6sKrTAF3fdkN3bcQv6JG1CfnWENDGtekemwcCEA9v46/RsOfg==
-----END RSA PRIVATE KEY-----";
let signing_key = DkimSigningKey::new(key.to_string(), DkimSigningAlgorithm::Rsa).unwrap();
message.sign(&DkimConfig::default_config(
"dkimtest".to_string(),
"example.org".to_string(),
signing_key,
));
println!("{}", std::str::from_utf8(&message.formatted()).unwrap());
let mut verify_command = Command::new("dkimverify")
.stdin(Stdio::piped())
.spawn()
.expect("Fail to verify message signature");
let mut stdin = verify_command.stdin.take().expect("Failed to open stdin");
std::thread::spawn(move || {
stdin
.write_all(&message.formatted())
.expect("Failed to write to stdin");
});
assert!(verify_command
.wait()
.expect("Command did not run")
.success());
}
}

View File

@@ -1,35 +1,43 @@
use hyperx::{
header::{Formatter as HeaderFormatter, Header, RawLike},
Error as HeaderError, Result as HyperResult,
};
use std::{
fmt::{Display, Formatter as FmtFormatter, Result as FmtResult},
str::{from_utf8, FromStr},
str::FromStr,
};
header! {
/// `Content-Id` header, defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-7)
(ContentId, "Content-ID") => [String]
}
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)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ContentTransferEncoding {
/// ASCII
SevenBit,
/// Quoted-Printable encoding
QuotedPrintable,
/// base64 encoding
Base64,
// 8BITMIME
/// Requires `8BITMIME`
EightBit,
/// Binary data
Binary,
}
impl Default for ContentTransferEncoding {
fn default() -> Self {
ContentTransferEncoding::Base64
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)
}
}
@@ -59,35 +67,16 @@ impl FromStr for ContentTransferEncoding {
}
}
impl Header for ContentTransferEncoding {
fn header_name() -> &'static str {
"Content-Transfer-Encoding"
}
// FIXME HeaderError->HeaderError, same for result
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<Self>
where
T: RawLike<'a>,
Self: Sized,
{
raw.one()
.ok_or(HeaderError::Header)
.and_then(|r| from_utf8(r).map_err(|_| HeaderError::Header))
.and_then(|s| {
s.parse::<ContentTransferEncoding>()
.map_err(|_| HeaderError::Header)
})
}
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&format!("{}", self))
impl Default for ContentTransferEncoding {
fn default() -> Self {
ContentTransferEncoding::Base64
}
}
#[cfg(test)]
mod test {
use super::ContentTransferEncoding;
use hyperx::header::Headers;
use crate::message::header::{HeaderName, HeaderValue, Headers};
#[test]
fn format_content_transfer_encoding() {
@@ -95,35 +84,35 @@ mod test {
headers.set(ContentTransferEncoding::SevenBit);
assert_eq!(
format!("{}", headers),
"Content-Transfer-Encoding: 7bit\r\n"
);
assert_eq!(headers.to_string(), "Content-Transfer-Encoding: 7bit\r\n");
headers.set(ContentTransferEncoding::Base64);
assert_eq!(
format!("{}", headers),
"Content-Transfer-Encoding: base64\r\n"
);
assert_eq!(headers.to_string(), "Content-Transfer-Encoding: base64\r\n");
}
#[test]
fn parse_content_transfer_encoding() {
let mut headers = Headers::new();
headers.set_raw("Content-Transfer-Encoding", "7bit");
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
"7bit".to_string(),
));
assert_eq!(
headers.get::<ContentTransferEncoding>(),
Some(&ContentTransferEncoding::SevenBit)
Some(ContentTransferEncoding::SevenBit)
);
headers.set_raw("Content-Transfer-Encoding", "base64");
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
"base64".to_string(),
));
assert_eq!(
headers.get::<ContentTransferEncoding>(),
Some(&ContentTransferEncoding::Base64)
Some(ContentTransferEncoding::Base64)
);
}
}

View File

@@ -0,0 +1,123 @@
use std::fmt::Write;
use email_encoding::headers::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_string(),
"inline".to_string(),
))
}
/// An attachment which should be displayed inline into the message, but that also
/// species the filename in case it were to be 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!("{}; filename=\"{}\"", kind, file_name);
let mut encoded_value = String::new();
let line_len = "Content-Disposition: ".len();
let mut w = EmailWriter::new(&mut encoded_value, line_len, 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 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_string(),
));
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_string(),
));
assert_eq!(
headers.get::<ContentDisposition>(),
Some(ContentDisposition::attachment("something.txt"))
);
}
}

View File

@@ -0,0 +1,192 @@
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 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_string(),
));
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_string(),
));
assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_HTML));
}
}

133
src/message/header/date.rs Normal file
View File

@@ -0,0 +1,133 @@
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)]
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`, 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 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_string()
);
// 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_string(),
));
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_string(),
));
assert_eq!(
headers.get::<Date>(),
Some(Date::from(
SystemTime::UNIX_EPOCH + Duration::from_secs(784887152),
))
);
}
}

View File

@@ -1,12 +1,10 @@
use crate::message::{
mailbox::{Mailbox, Mailboxes},
utf8_b,
use email_encoding::headers::EmailWriter;
use super::{Header, HeaderName, HeaderValue};
use crate::{
message::mailbox::{Mailbox, Mailboxes},
BoxError,
};
use hyperx::{
header::{Formatter as HeaderFormatter, Header, RawLike},
Error as HeaderError, Result as HyperResult,
};
use std::{fmt::Result as FmtResult, slice::Iter, str::from_utf8};
/// Header which can contains multiple mailboxes
pub trait MailboxesHeader {
@@ -17,26 +15,39 @@ macro_rules! mailbox_header {
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
$(#[$doc])*
#[derive(Debug, Clone, PartialEq)]
pub struct $type_name(pub Mailbox);
pub struct $type_name(Mailbox);
impl Header for $type_name {
fn header_name() -> &'static str {
$header_name
fn name() -> HeaderName {
HeaderName::new_from_ascii_str($header_name)
}
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<Self> where
T: RawLike<'a>,
Self: Sized {
raw.one()
.ok_or(HeaderError::Header)
.and_then(parse_mailboxes)
.and_then(|mbs| {
mbs.into_single().ok_or(HeaderError::Header)
}).map($type_name)
fn parse(s: &str) -> Result<Self, BoxError> {
let mailbox: Mailbox = s.parse()?;
Ok(Self(mailbox))
}
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&self.0.recode_name(utf8_b::encode))
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, 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
}
}
};
@@ -46,7 +57,7 @@ macro_rules! mailboxes_header {
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
$(#[$doc])*
#[derive(Debug, Clone, PartialEq)]
pub struct $type_name(pub Mailboxes);
pub struct $type_name(pub(crate) Mailboxes);
impl MailboxesHeader for $type_name {
fn join_mailboxes(&mut self, other: Self) {
@@ -55,23 +66,36 @@ macro_rules! mailboxes_header {
}
impl Header for $type_name {
fn header_name() -> &'static str {
$header_name
fn name() -> HeaderName {
HeaderName::new_from_ascii_str($header_name)
}
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<$type_name>
where
T: RawLike<'a>,
Self: Sized,
{
raw.one()
.ok_or(HeaderError::Header)
.and_then(parse_mailboxes)
.map($type_name)
fn parse(s: &str) -> Result<Self, BoxError> {
let mailbox: Mailboxes = s.parse()?;
Ok(Self(mailbox))
}
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
format_mailboxes(self.0.iter(), f)
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, 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
}
}
};
@@ -146,26 +170,10 @@ mailboxes_header! {
(Bcc, "Bcc")
}
fn parse_mailboxes(raw: &[u8]) -> HyperResult<Mailboxes> {
if let Ok(src) = from_utf8(raw) {
if let Ok(mbs) = src.parse() {
return Ok(mbs);
}
}
Err(HeaderError::Header)
}
fn format_mailboxes<'a>(mbs: Iter<'a, Mailbox>, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&Mailboxes::from(
mbs.map(|mb| mb.recode_name(utf8_b::encode))
.collect::<Vec<_>>(),
))
}
#[cfg(test)]
mod test {
use super::{From, Mailbox, Mailboxes};
use hyperx::header::Headers;
use crate::message::header::{HeaderName, HeaderValue, Headers};
#[test]
fn format_single_without_name() {
@@ -174,17 +182,17 @@ mod test {
let mut headers = Headers::new();
headers.set(From(from));
assert_eq!(format!("{}", headers), "From: kayo@example.com\r\n");
assert_eq!(headers.to_string(), "From: kayo@example.com\r\n");
}
#[test]
fn format_single_with_name() {
let from = Mailboxes::new().with("K. <kayo@example.com>".parse().unwrap());
let from = Mailboxes::new().with("Kayo <kayo@example.com>".parse().unwrap());
let mut headers = Headers::new();
headers.set(From(from));
assert_eq!(format!("{}", headers), "From: K. <kayo@example.com>\r\n");
assert_eq!(headers.to_string(), "From: Kayo <kayo@example.com>\r\n");
}
#[test]
@@ -197,7 +205,7 @@ mod test {
headers.set(From(from));
assert_eq!(
format!("{}", headers),
headers.to_string(),
"From: kayo@example.com, pony@domain.tld\r\n"
);
}
@@ -205,7 +213,7 @@ mod test {
#[test]
fn format_multi_with_name() {
let from = vec![
"K. <kayo@example.com>".parse().unwrap(),
"Kayo <kayo@example.com>".parse().unwrap(),
"Pony P. <pony@domain.tld>".parse().unwrap(),
];
@@ -213,8 +221,8 @@ mod test {
headers.set(From(from.into()));
assert_eq!(
format!("{}", headers),
"From: K. <kayo@example.com>, Pony P. <pony@domain.tld>\r\n"
headers.to_string(),
"From: Kayo <kayo@example.com>, \"Pony P.\" <pony@domain.tld>\r\n"
);
}
@@ -226,7 +234,7 @@ mod test {
headers.set(From(from.into()));
assert_eq!(
format!("{}", headers),
headers.to_string(),
"From: =?utf-8?b?0JrQsNC50L4=?= <kayo@example.com>\r\n"
);
}
@@ -236,9 +244,12 @@ mod test {
let from = vec!["kayo@example.com".parse().unwrap()].into();
let mut headers = Headers::new();
headers.set_raw("From", "kayo@example.com");
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"kayo@example.com".to_string(),
));
assert_eq!(headers.get::<From>(), Some(&From(from)));
assert_eq!(headers.get::<From>(), Some(From(from)));
}
#[test]
@@ -246,9 +257,12 @@ mod test {
let from = vec!["K. <kayo@example.com>".parse().unwrap()].into();
let mut headers = Headers::new();
headers.set_raw("From", "K. <kayo@example.com>");
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"K. <kayo@example.com>".to_string(),
));
assert_eq!(headers.get::<From>(), Some(&From(from)));
assert_eq!(headers.get::<From>(), Some(From(from)));
}
#[test]
@@ -259,9 +273,12 @@ mod test {
];
let mut headers = Headers::new();
headers.set_raw("From", "kayo@example.com, pony@domain.tld");
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"kayo@example.com, pony@domain.tld".to_string(),
));
assert_eq!(headers.get::<From>(), Some(&From(from.into())));
assert_eq!(headers.get::<From>(), Some(From(from.into())));
}
#[test]
@@ -272,18 +289,11 @@ mod test {
];
let mut headers = Headers::new();
headers.set_raw("From", "K. <kayo@example.com>, Pony P. <pony@domain.tld>");
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"K. <kayo@example.com>, Pony P. <pony@domain.tld>".to_string(),
));
assert_eq!(headers.get::<From>(), Some(&From(from.into())));
}
#[test]
fn parse_single_with_utf8_name() {
let from: Vec<Mailbox> = vec!["Кайо <kayo@example.com>".parse().unwrap()];
let mut headers = Headers::new();
headers.set_raw("From", "=?utf-8?b?0JrQsNC50L4=?= <kayo@example.com>");
assert_eq!(headers.get::<From>(), Some(&From(from.into())));
assert_eq!(headers.get::<From>(), Some(From(from.into())));
}
}

View File

@@ -1,13 +1,842 @@
//! Headers widely used in email messages
use std::{
borrow::Cow,
error::Error,
fmt::{self, Display, Formatter},
ops::Deref,
};
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;
pub use self::{content::*, mailbox::*, special::*, 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;
pub use hyperx::header::{
Charset, ContentDisposition, ContentLocation, ContentType, Date, DispositionParam,
DispositionType, Header, Headers, HttpDate as EmailDate,
};
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 an `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))
}
fn find_header(&self, name: &str) -> Option<&HeaderValue> {
self.headers
.iter()
.find(|value| name.eq_ignore_ascii_case(&value.name))
}
fn find_header_mut(&mut self, name: &str) -> Option<&mut HeaderValue> {
self.headers
.iter_mut()
.find(|value| name.eq_ignore_ascii_case(&value.name))
}
fn find_header_index(&self, name: &str) -> Option<usize> {
self.headers
.iter()
.enumerate()
.find(|(_i, value)| name.eq_ignore_ascii_case(&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(Clone)]
pub struct InvalidHeaderName {
_priv: (),
}
impl fmt::Debug for InvalidHeaderName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("InvalidHeaderName")
// skip _priv noise
.finish()
}
}
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(|c| c == ':' || c == ' ')
{
Ok(Self(Cow::Owned(ascii)))
} else {
Err(InvalidHeaderName { _priv: () })
}
}
/// 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 {
let s1: &str = self.as_ref();
let s2: &str = other.as_ref();
s1 == s2
}
}
impl PartialEq<&str> for HeaderName {
fn eq(&self, other: &&str) -> bool {
let s: &str = self.as_ref();
s == *other
}
}
impl PartialEq<HeaderName> for &str {
fn eq(&self, other: &HeaderName) -> bool {
let s: &str = other.as_ref();
*self == s
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct HeaderValue {
name: HeaderName,
raw_value: String,
encoded_value: String,
}
impl HeaderValue {
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,
}
}
pub fn dangerous_new_pre_encoded(
name: HeaderName,
raw_value: String,
encoded_value: String,
) -> Self {
Self {
name,
raw_value,
encoded_value,
}
}
}
const ENCODING_START_PREFIX: &str = "=?utf-8?b?";
const ENCODING_END_SUFFIX: &str = "?=";
const MAX_LINE_LEN: usize = 76;
/// [RFC 1522](https://tools.ietf.org/html/rfc1522) header value encoder
struct HeaderValueEncoder {
line_len: usize,
encode_buf: String,
}
impl HeaderValueEncoder {
fn encode(name: &str, value: &str, f: &mut impl fmt::Write) -> fmt::Result {
let (words_iter, encoder) = Self::new(name, value);
encoder.format(words_iter, f)
}
fn new<'a>(name: &str, value: &'a str) -> (WordsPlusFillIterator<'a>, Self) {
(
WordsPlusFillIterator { s: value },
Self {
line_len: name.len() + ": ".len(),
encode_buf: String::new(),
},
)
}
fn format(
mut self,
words_iter: WordsPlusFillIterator<'_>,
f: &mut impl fmt::Write,
) -> fmt::Result {
/// Estimate if an encoded string of `len` would fix in an empty line
fn would_fit_new_line(len: usize) -> bool {
len < (MAX_LINE_LEN - " ".len())
}
/// Estimate how long a string of `len` would be after base64 encoding plus
/// adding the encoding prefix and suffix to it
fn base64_len(len: usize) -> usize {
ENCODING_START_PREFIX.len() + (len * 4 / 3 + 4) + ENCODING_END_SUFFIX.len()
}
/// Estimate how many more bytes we can fit in the current line
fn available_len_to_max_encode_len(len: usize) -> usize {
len.saturating_sub(
ENCODING_START_PREFIX.len() + (len * 3 / 4 + 4) + ENCODING_END_SUFFIX.len(),
)
}
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(f, true)?;
if next_word.len() > self.remaining_line_len() {
// not enough space left on this line to encode word
if self.something_written_to_this_line() && would_fit_new_line(next_word.len())
{
// word doesn't fit this line, but something had already been written to it,
// and word would fit the next line, so go to a new line
// so go to new line
self.new_line(f)?;
} else {
// word neither fits this line and the next one, cut it
// in the middle and make it fit
let mut next_word = next_word;
while !next_word.is_empty() {
if self.remaining_line_len() == 0 {
self.new_line(f)?;
}
let len = self.remaining_line_len().min(next_word.len());
let first_part = &next_word[..len];
next_word = &next_word[len..];
f.write_str(first_part)?;
self.line_len += first_part.len();
}
continue;
}
}
// word fits, write it!
f.write_str(next_word)?;
self.line_len += next_word.len();
} else {
// This word contains unallowed characters
if self.remaining_line_len() >= base64_len(self.encode_buf.len() + next_word.len())
{
// next_word fits
self.encode_buf.push_str(next_word);
continue;
}
// next_word doesn't fit this line
if would_fit_new_line(base64_len(next_word.len())) {
// ...but it would fit the next one
self.flush_encode_buf(f, false)?;
self.new_line(f)?;
self.encode_buf.push_str(next_word);
continue;
}
// ...and also wouldn't fit the next one.
// chop it up into pieces
let mut next_word = next_word;
while !next_word.is_empty() {
let mut len = available_len_to_max_encode_len(self.remaining_line_len())
.min(next_word.len());
if len == 0 {
self.flush_encode_buf(f, false)?;
self.new_line(f)?;
}
// avoid slicing on a char boundary
while !next_word.is_char_boundary(len) {
len += 1;
}
let first_part = &next_word[..len];
next_word = &next_word[len..];
self.encode_buf.push_str(first_part);
}
}
}
self.flush_encode_buf(f, false)?;
Ok(())
}
/// Returns the number of bytes left for the current line
fn remaining_line_len(&self) -> usize {
MAX_LINE_LEN - self.line_len
}
/// Returns true if something has been written to the current line
fn something_written_to_this_line(&self) -> bool {
self.line_len > 1
}
fn flush_encode_buf(
&mut self,
f: &mut impl fmt::Write,
switching_to_allowed: bool,
) -> fmt::Result {
if self.encode_buf.is_empty() {
// nothing to encode
return Ok(());
}
let mut write_after = None;
if switching_to_allowed {
// If the next word only contains allowed characters, and the string to encode
// ends with a space, take the space out of the part to encode
let last_char = self.encode_buf.pop().expect("self.encode_buf isn't empty");
if is_space_like(last_char) {
write_after = Some(last_char);
} else {
self.encode_buf.push(last_char);
}
}
f.write_str(ENCODING_START_PREFIX)?;
let encoded = base64::display::Base64Display::with_config(
self.encode_buf.as_bytes(),
base64::STANDARD,
);
write!(f, "{}", encoded)?;
f.write_str(ENCODING_END_SUFFIX)?;
self.line_len += ENCODING_START_PREFIX.len();
self.line_len += self.encode_buf.len() * 4 / 3 + 4;
self.line_len += ENCODING_END_SUFFIX.len();
if let Some(write_after) = write_after {
f.write_char(write_after)?;
self.line_len += 1;
}
self.encode_buf.clear();
Ok(())
}
fn new_line(&mut self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str("\r\n ")?;
self.line_len = 1;
Ok(())
}
}
/// Iterator yielding a string split space by space, but including all space
/// characters between it and the next word
struct WordsPlusFillIterator<'a> {
s: &'a str,
}
impl<'a> Iterator for WordsPlusFillIterator<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
if self.s.is_empty() {
return None;
}
let next_word = self
.s
.char_indices()
.skip(1)
.skip_while(|&(_i, c)| !is_space_like(c))
.find(|&(_i, c)| !is_space_like(c))
.map(|(i, _)| i);
let word = &self.s[..next_word.unwrap_or(self.s.len())];
self.s = &self.s[word.len()..];
Some(word)
}
}
const fn is_space_like(c: char) -> bool {
c == ',' || c == ' '
}
fn allowed_str(s: &str) -> bool {
s.chars().all(allowed_char)
}
const fn allowed_char(c: char) -> bool {
c >= 1 as char && c <= 9 as char
|| c == 11 as char
|| c == 12 as char
|| c >= 14 as char && c <= 127 as char
}
#[cfg(test)]
mod tests {
use super::{HeaderName, HeaderValue, Headers};
#[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("");
}
// 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_string(),
));
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_string(),
));
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_string()
));
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_string()
));
assert_eq!(
headers.to_string(),
concat!(
"Subject: Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoY\r\n",
" ouThinkItsGoingToHappenIGuessWereAboutToFindOut! 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_string()
));
assert_eq!(
headers.to_string(),
concat!(
"Subject: 1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijkl\r\n",
" mnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdef\r\n",
" ghijklmnopqrstuvwxyz\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_string(),
));
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_string(),
));
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();
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_string(),
) );
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?0JjQstCw0L3QvtCyIA==?=\r\n",
" =?utf-8?b?0JjQstCw0L0g0JjQstCw0L3QvtCy0LjRhw==?= <ivanov@example.com>, \r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, \r\n",
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <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_string(),)
);
assert_eq!(
headers.to_string(),
"Subject: =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz?=\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_string(),
));
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_string()
)
);
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_string(),
)
);
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"Someone <somewhere@example.com>".to_string(),
));
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
"quoted-printable".to_string(),
));
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?0JjQstCw0L3QvtCyIA==?=\r\n",
" =?utf-8?b?0JjQstCw0L0g0JjQstCw0L3QvtCy0LjRhw==?= <ivanov@example.com>, \r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, \r\n",
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <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_string(),
));
assert_eq!(
headers.to_string(),
concat!(
"Subject: =?utf-8?b?77yL5Luu5ZCN?= :a;go; \r\n",
" =?utf-8?b?Ozs7OztzOzs7Ozs7Ozs7Ozs7Ozs7O2ZmZmVpbm1qZ2dnZ2dnZ2dn772G44Gj?=\r\n"
)
);
}
}

View File

@@ -1,22 +1,59 @@
use hyperx::{
header::{Formatter as HeaderFormatter, Header, RawLike},
Error as HeaderError, Result as HyperResult,
use crate::{
message::header::{Header, HeaderName, HeaderValue},
BoxError,
};
use std::{fmt::Result as FmtResult, str::from_utf8};
#[derive(Debug, Clone, Copy, PartialEq)]
/// Message format version, defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-4)
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct MimeVersion {
pub major: u8,
pub minor: u8,
major: u8,
minor: u8,
}
pub const MIME_VERSION_1_0: MimeVersion = MimeVersion { major: 1, minor: 0 };
/// MIME version 1.0
///
/// Should be used in all MIME messages.
pub const MIME_VERSION_1_0: MimeVersion = MimeVersion::new(1, 0);
impl MimeVersion {
pub fn new(major: u8, minor: u8) -> Self {
pub const fn new(major: u8, minor: u8) -> Self {
MimeVersion { major, minor }
}
#[inline]
pub const fn major(self) -> u8 {
self.major
}
#[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 {
@@ -25,36 +62,10 @@ impl Default for MimeVersion {
}
}
impl Header for MimeVersion {
fn header_name() -> &'static str {
"MIME-Version"
}
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<Self>
where
T: RawLike<'a>,
Self: Sized,
{
raw.one().ok_or(HeaderError::Header).and_then(|r| {
let mut s = from_utf8(r).map_err(|_| HeaderError::Header)?.split('.');
let major = s.next().ok_or(HeaderError::Header)?;
let minor = s.next().ok_or(HeaderError::Header)?;
let major = major.parse().map_err(|_| HeaderError::Header)?;
let minor = minor.parse().map_err(|_| HeaderError::Header)?;
Ok(MimeVersion::new(major, minor))
})
}
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&format!("{}.{}", self.major, self.minor))
}
}
#[cfg(test)]
mod test {
use super::{MimeVersion, MIME_VERSION_1_0};
use hyperx::header::Headers;
use crate::message::header::{HeaderName, HeaderValue, Headers};
#[test]
fn format_mime_version() {
@@ -62,23 +73,29 @@ mod test {
headers.set(MIME_VERSION_1_0);
assert_eq!(format!("{}", headers), "MIME-Version: 1.0\r\n");
assert_eq!(headers.to_string(), "MIME-Version: 1.0\r\n");
headers.set(MimeVersion::new(0, 1));
assert_eq!(format!("{}", headers), "MIME-Version: 0.1\r\n");
assert_eq!(headers.to_string(), "MIME-Version: 0.1\r\n");
}
#[test]
fn parse_mime_version() {
let mut headers = Headers::new();
headers.set_raw("MIME-Version", "1.0");
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("MIME-Version"),
"1.0".to_string(),
));
assert_eq!(headers.get::<MimeVersion>(), Some(&MIME_VERSION_1_0));
assert_eq!(headers.get::<MimeVersion>(), Some(MIME_VERSION_1_0));
headers.set_raw("MIME-Version", "0.1");
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("MIME-Version"),
"0.1".to_string(),
));
assert_eq!(headers.get::<MimeVersion>(), Some(&MimeVersion::new(0, 1)));
assert_eq!(headers.get::<MimeVersion>(), Some(MimeVersion::new(0, 1)));
}
}

View File

@@ -1,34 +1,37 @@
use crate::message::utf8_b;
use hyperx::{
header::{Formatter as HeaderFormatter, Header, RawLike},
Error as HeaderError, Result as HyperResult,
};
use std::{fmt::Result as FmtResult, str::from_utf8};
use super::{Header, HeaderName, HeaderValue};
use crate::BoxError;
macro_rules! text_header {
($(#[$attr:meta])* Header($type_name: ident, $header_name: expr )) => {
#[derive(Debug, Clone, PartialEq)]
$(#[$attr])*
pub struct $type_name(pub String);
#[derive(Debug, Clone, PartialEq)]
pub struct $type_name(String);
impl Header for $type_name {
fn header_name() -> &'static str {
$header_name
fn name() -> HeaderName {
HeaderName::new_from_ascii_str($header_name)
}
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<$type_name>
where
T: RawLike<'a>,
Self: Sized,
{
raw.one()
.ok_or(HeaderError::Header)
.and_then(parse_text)
.map($type_name)
fn parse(s: &str) -> Result<Self, BoxError> {
Ok(Self(s.into()))
}
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
fmt_text(&self.0, f)
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
}
}
};
@@ -62,38 +65,35 @@ text_header!(
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")
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")
);
fn parse_text(raw: &[u8]) -> HyperResult<String> {
if let Ok(src) = from_utf8(raw) {
if let Some(txt) = utf8_b::decode(src) {
return Ok(txt);
}
}
Err(HeaderError::Header)
text_header! {
/// `Content-Id` header,
/// defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-7)
Header(ContentId, "Content-ID")
}
fn fmt_text(s: &str, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&utf8_b::encode(s))
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 super::Subject;
use hyperx::header::Headers;
use crate::message::header::{HeaderName, HeaderValue, Headers};
#[test]
fn format_ascii() {
let mut headers = Headers::new();
headers.set(Subject("Sample subject".into()));
assert_eq!(format!("{}", headers), "Subject: Sample subject\r\n");
assert_eq!(headers.to_string(), "Subject: Sample subject\r\n");
}
#[test]
@@ -102,7 +102,7 @@ mod test {
headers.set(Subject("Тема сообщения".into()));
assert_eq!(
format!("{}", headers),
headers.to_string(),
"Subject: =?utf-8?b?0KLQtdC80LAg0YHQvtC+0LHRidC10L3QuNGP?=\r\n"
);
}
@@ -110,25 +110,14 @@ mod test {
#[test]
fn parse_ascii() {
let mut headers = Headers::new();
headers.set_raw("Subject", "Sample subject");
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"),
"Sample subject".to_string(),
));
assert_eq!(
headers.get::<Subject>(),
Some(&Subject("Sample subject".into()))
);
}
#[test]
fn parse_utf8() {
let mut headers = Headers::new();
headers.set_raw(
"Subject",
"=?utf-8?b?0KLQtdC80LAg0YHQvtC+0LHRidC10L3QuNGP?=",
);
assert_eq!(
headers.get::<Subject>(),
Some(&Subject("Тема сообщения".into()))
Some(Subject("Sample subject".into()))
);
}
}

View File

@@ -1,10 +1,12 @@
use crate::message::{Mailbox, Mailboxes};
use std::fmt::{Formatter, Result as FmtResult};
use serde::{
de::{Deserializer, Error as DeError, MapAccess, SeqAccess, Visitor},
ser::Serializer,
Deserialize, Serialize,
};
use std::fmt::{Formatter, Result as FmtResult};
use crate::message::{Mailbox, Mailboxes};
impl Serialize for Mailbox {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
@@ -152,9 +154,10 @@ impl<'de> Deserialize<'de> for Mailboxes {
#[cfg(test)]
mod test {
use serde_json::from_str;
use super::*;
use crate::address::Address;
use serde_json::from_str;
#[test]
fn parse_address_string() {

View File

@@ -1,14 +1,14 @@
use crate::{
address::{Address, AddressError},
message::utf8_b,
};
use std::{
convert::TryFrom,
fmt::{Display, Formatter, Result as FmtResult, Write},
mem,
slice::Iter,
str::FromStr,
};
use email_encoding::headers::EmailWriter;
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_).
@@ -67,12 +67,20 @@ impl Mailbox {
Mailbox { name, email }
}
/// Encode addressee name using function
pub(crate) fn recode_name<F>(&self, f: F) -> Self
where
F: FnOnce(&str) -> String,
{
Mailbox::new(self.name.clone().map(|s| f(&s)), self.email.clone())
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(())
}
}
@@ -81,7 +89,7 @@ impl Display for Mailbox {
if let Some(ref name) = self.name {
let name = name.trim();
if !name.is_empty() {
f.write_str(&name)?;
write_word(f, name)?;
f.write_str(" <")?;
self.email.fmt(f)?;
return f.write_char('>');
@@ -261,6 +269,20 @@ impl Mailboxes {
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 {
@@ -332,29 +354,99 @@ impl FromStr for Mailboxes {
fn from_str(src: &str) -> Result<Self, Self::Err> {
src.split(',')
.map(|m| {
m.trim().parse().and_then(|Mailbox { name, email }| {
if let Some(name) = name {
if let Some(name) = utf8_b::decode(&name) {
Ok(Mailbox::new(Some(name), email))
} else {
Err(AddressError::InvalidUtf8b)
}
} else {
Ok(Mailbox::new(None, email))
}
})
})
.map(|m| m.trim().parse())
.collect::<Result<Vec<_>, _>>()
.map(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.as_bytes() {
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 techically 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: u8) -> FmtResult {
match c {
// NO-WS-CTL: https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.1
1..=8 | 11 | 12 | 14..=31 | 127 |
// Note, not qcontent but can be put before or after any qcontent.
b'\t' |
b' ' |
// The rest of the US-ASCII except \ and "
33 |
35..=91 |
93..=126 |
// Non-ascii characters will be escaped separately later.
128..=255
=> f.write_char(c.into()),
// Can not be encoded.
b'\n' | b'\r' => Err(std::fmt::Error),
c => {
// quoted-pair https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.2
f.write_char('\\')?;
f.write_char(c.into())
}
}
}
#[cfg(test)]
mod test {
use super::Mailbox;
use std::convert::TryInto;
use super::Mailbox;
#[test]
fn mailbox_format_address_only() {
assert_eq!(
@@ -373,7 +465,35 @@ mod test {
"{}",
Mailbox::new(Some("K.".into()), "kayo@example.com".parse().unwrap())
),
"K. <kayo@example.com>"
"\"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_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>"#
);
}
@@ -395,7 +515,7 @@ mod test {
"{}",
Mailbox::new(Some(" K. ".into()), "kayo@example.com".parse().unwrap())
),
"K. <kayo@example.com>"
"\"K.\" <kayo@example.com>"
);
}

View File

@@ -1,13 +1,15 @@
use std::{io::Write, iter::repeat_with};
use mime::Mime;
use crate::message::{
header::{ContentTransferEncoding, ContentType, Header, Headers},
header::{self, ContentTransferEncoding, ContentType, Header, Headers},
EmailFormat, IntoBody,
};
use mime::Mime;
use rand::Rng;
/// MIME part variants
#[derive(Debug, Clone)]
pub enum Part {
pub(super) enum Part {
/// Single part with content
Single(SinglePart),
@@ -24,18 +26,6 @@ impl EmailFormat for Part {
}
}
impl Part {
/// Get message content formatted for SMTP
pub fn formatted(&self) -> Vec<u8> {
let mut out = Vec::new();
self.format(&mut out);
out
}
}
/// Parts of multipart body
pub type Parts = Vec<Part>;
/// Creates builder for single part
#[derive(Debug, Clone)]
pub struct SinglePartBuilder {
@@ -64,7 +54,7 @@ impl SinglePartBuilder {
/// Build singlepart using body
pub fn body<T: IntoBody>(mut self, body: T) -> SinglePart {
let maybe_encoding = self.headers.get::<ContentTransferEncoding>().copied();
let maybe_encoding = self.headers.get::<ContentTransferEncoding>();
let body = body.into_body(maybe_encoding);
self.headers.set(body.encoding());
@@ -92,7 +82,7 @@ impl Default for SinglePartBuilder {
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let part = SinglePart::builder()
/// .header(header::ContentType("text/plain; charset=utf8".parse()?))
/// .header(header::ContentType::TEXT_PLAIN)
/// .body(String::from("Текст письма в уникоде"));
/// # Ok(())
/// # }
@@ -110,6 +100,20 @@ impl SinglePart {
SinglePartBuilder::new()
}
/// Directly create a `SinglePart` from an 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 an 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 {
@@ -132,7 +136,8 @@ impl SinglePart {
impl EmailFormat for SinglePart {
fn format(&self, out: &mut Vec<u8>) {
out.extend_from_slice(self.headers.to_string().as_bytes());
write!(out, "{}", self.headers)
.expect("A Write implementation panicked while formatting headers");
out.extend_from_slice(b"\r\n");
out.extend_from_slice(&self.body);
out.extend_from_slice(b"\r\n");
@@ -165,17 +170,14 @@ pub enum MultiPartKind {
}
/// Create a random MIME boundary.
/// (Not cryptographically random)
fn make_boundary() -> String {
rand::thread_rng()
.sample_iter(rand::distributions::Alphanumeric)
.take(40)
.map(char::from)
.collect()
repeat_with(fastrand::alphanumeric).take(40).collect()
}
impl MultiPartKind {
fn to_mime<S: Into<String>>(&self, boundary: Option<S>) -> Mime {
let boundary = boundary.map_or_else(make_boundary, |s| s.into());
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=\"{}\"{}",
@@ -217,12 +219,6 @@ impl MultiPartKind {
}
}
impl From<MultiPartKind> for Mime {
fn from(m: MultiPartKind) -> Self {
m.to_mime::<String>(None)
}
}
/// Multipart builder
#[derive(Debug, Clone)]
pub struct MultiPartBuilder {
@@ -245,17 +241,17 @@ impl MultiPartBuilder {
/// Set `Content-Type` header using [`MultiPartKind`]
pub fn kind(self, kind: MultiPartKind) -> Self {
self.header(ContentType(kind.into()))
self.header(ContentType::from_mime(kind.to_mime::<String>(None)))
}
/// Set custom boundary
pub fn boundary<S: AsRef<str>>(self, boundary: S) -> Self {
pub fn boundary<S: Into<String>>(self, boundary: S) -> Self {
let kind = {
let mime = &self.headers.get::<ContentType>().unwrap().0;
MultiPartKind::from_mime(mime).unwrap()
let content_type = self.headers.get::<ContentType>().unwrap();
MultiPartKind::from_mime(content_type.as_ref()).unwrap()
};
let mime = kind.to_mime(Some(boundary.as_ref()));
self.header(ContentType(mime))
let mime = kind.to_mime(Some(boundary));
self.header(ContentType::from_mime(mime))
}
/// Creates multipart without parts
@@ -266,11 +262,6 @@ impl MultiPartBuilder {
}
}
/// Creates multipart using part
pub fn part(self, part: Part) -> MultiPart {
self.build().part(part)
}
/// Creates multipart using singlepart
pub fn singlepart(self, part: SinglePart) -> MultiPart {
self.build().singlepart(part)
@@ -292,7 +283,7 @@ impl Default for MultiPartBuilder {
#[derive(Debug, Clone)]
pub struct MultiPart {
headers: Headers,
parts: Parts,
parts: Vec<Part>,
}
impl MultiPart {
@@ -336,10 +327,11 @@ impl MultiPart {
MultiPart::builder().kind(MultiPartKind::Signed { protocol, micalg })
}
/// Add part to multipart
pub fn part(mut self, part: Part) -> Self {
self.parts.push(part);
self
/// 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
@@ -356,8 +348,13 @@ impl MultiPart {
/// Get the boundary of multipart contents
pub fn boundary(&self) -> String {
let content_type = &self.headers.get::<ContentType>().unwrap().0;
content_type.get_param("boundary").unwrap().as_str().into()
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
@@ -370,16 +367,6 @@ impl MultiPart {
&mut self.headers
}
/// Get the parts from the multipart
pub fn parts(&self) -> &Parts {
&self.parts
}
/// Get a mutable reference to the parts
pub fn parts_mut(&mut self) -> &mut Parts {
&mut self.parts
}
/// Get message content formatted for SMTP
pub fn formatted(&self) -> Vec<u8> {
let mut out = Vec::new();
@@ -390,7 +377,8 @@ impl MultiPart {
impl EmailFormat for MultiPart {
fn format(&self, out: &mut Vec<u8>) {
out.extend_from_slice(self.headers.to_string().as_bytes());
write!(out, "{}", self.headers)
.expect("A Write implementation panicked while formatting headers");
out.extend_from_slice(b"\r\n");
let boundary = self.boundary();
@@ -416,16 +404,14 @@ mod test {
#[test]
fn single_part_binary() {
let part = SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.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=utf8\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"Текст письма в уникоде\r\n"
@@ -436,16 +422,14 @@ mod test {
#[test]
fn single_part_quoted_printable() {
let part = SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.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=utf8\r\n",
"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",
@@ -457,16 +441,14 @@ mod test {
#[test]
fn single_part_base64() {
let part = SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.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=utf8\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: base64\r\n",
"\r\n",
"0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LU=\r\n"
@@ -477,75 +459,60 @@ mod test {
#[test]
fn multi_part_mixed() {
let part = MultiPart::mixed()
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.part(Part::Single(
SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentTransferEncoding::Binary)
.body(String::from("Текст письма в уникоде")),
))
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.singlepart(
SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentDisposition {
disposition: header::DispositionType::Attachment,
parameters: vec![header::DispositionParam::Filename(
header::Charset::Ext("utf-8".into()),
None,
"example.c".into(),
)],
})
.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;",
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"Текст письма в уникоде\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Disposition: attachment; filename=\"example.c\"\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"int main() { return 0; }\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
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("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.part(Part::Single(
SinglePart::builder()
.header(header::ContentType(
"application/pgp-encrypted".parse().unwrap(),
))
.body(String::from("Version: 1")),
))
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.singlepart(
SinglePart::builder()
.header(ContentType(
"application/octet-stream; name=\"encrypted.asc\""
.parse()
.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",
))
.header(header::ContentDisposition {
disposition: header::DispositionType::Inline,
parameters: vec![header::DispositionParam::Filename(
header::Charset::Ext("utf-8".into()),
None,
"encrypted.asc".into(),
)],
})
.body(String::from(concat!(
"-----BEGIN PGP MESSAGE-----\r\n",
"wV4D0dz5vDXklO8SAQdA5lGX1UU/eVQqDxNYdHa7tukoingHzqUB6wQssbMfHl8w\r\n",
@@ -554,27 +521,31 @@ mod test {
))),
);
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
concat!("Content-Type: multipart/encrypted;",
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\";",
" protocol=\"application/pgp-encrypted\"\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: application/pgp-encrypted\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Version: 1\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\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",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\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() {
@@ -582,27 +553,19 @@ mod test {
"application/pgp-signature".to_owned(),
"pgp-sha256".to_owned(),
)
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.part(Part::Single(
SinglePart::builder()
.header(header::ContentType("text/plain".parse().unwrap()))
.body(String::from("Test email for signature")),
))
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.singlepart(
SinglePart::builder()
.header(ContentType(
"application/pgp-signature; name=\"signature.asc\""
.parse()
.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 {
disposition: header::DispositionType::Attachment,
parameters: vec![header::DispositionParam::Filename(
header::Charset::Ext("utf-8".into()),
None,
"signature.asc".into(),
)],
})
)
.header(header::ContentDisposition::attachment("signature.asc"))
.body(String::from(concat!(
"-----BEGIN PGP SIGNATURE-----\r\n",
"\r\n",
@@ -614,101 +577,102 @@ mod test {
))),
);
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
concat!("Content-Type: multipart/signed;",
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\";",
" protocol=\"application/pgp-signature\";",
" micalg=\"pgp-sha256\"\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/plain\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Test email for signature\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\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",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\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("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.part(Part::Single(SinglePart::builder()
.header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
.header(header::ContentTransferEncoding::Binary)
.body(String::from("Текст письма в уникоде"))))
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.singlepart(SinglePart::builder()
.header(header::ContentType("text/html; charset=utf8".parse().unwrap()))
.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;",
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
concat!("Content-Type: multipart/alternative; \r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/plain; charset=utf8\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"Текст письма в уникоде\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/html; charset=utf8\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",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"));
}
#[test]
fn multi_part_mixed_related() {
let part = MultiPart::mixed()
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.multipart(MultiPart::related()
.boundary("E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh")
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.singlepart(SinglePart::builder()
.header(header::ContentType("text/html; charset=utf8".parse().unwrap()))
.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("image/png".parse().unwrap()))
.header(header::ContentLocation("/image.png".into()))
.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; charset=utf8".parse().unwrap()))
.header(header::ContentDisposition {
disposition: header::DispositionType::Attachment,
parameters: vec![header::DispositionParam::Filename(header::Charset::Ext("utf-8".into()), None, "example.c".into())]
})
.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;",
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
concat!("Content-Type: multipart/mixed; \r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: multipart/related;",
" boundary=\"E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\"\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: multipart/related; \r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
"\r\n",
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\r\n",
"Content-Type: text/html; charset=utf8\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",
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: image/png\r\n",
"Content-Location: /image.png\r\n",
"Content-Transfer-Encoding: base64\r\n",
@@ -716,14 +680,14 @@ mod test {
"MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3\r\n",
"ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0\r\n",
"NTY3ODkwMTIzNDU2Nzg5MA==\r\n",
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh--\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/plain; charset=utf8\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",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"));
}
#[test]

View File

@@ -4,15 +4,8 @@
//!
//! This section demonstrates how to build messages.
//!
//! <!--
//! style for <details><summary>Blablabla</summary> Lots of stuff</details>
//! borrowed from https://docs.rs/time/0.2.23/src/time/lib.rs.html#49-54
//! -->
//! <style>
//! summary, details:not([open]) { cursor: pointer; }
//! summary { display: list-item; }
//! summary::marker { content: '▶ '; }
//! details[open] summary::marker { content: '▼ '; }
//! </style>
//!
//!
@@ -63,7 +56,7 @@
//!
//! ```rust
//! # use std::error::Error;
//! use lettre::message::{header, Message, MultiPart, Part, SinglePart};
//! use lettre::message::{header, Message, MultiPart, SinglePart};
//!
//! # fn main() -> Result<(), Box<dyn Error>> {
//! let m = Message::builder()
@@ -71,21 +64,10 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .multipart(
//! MultiPart::alternative()
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType("text/plain; charset=utf8".parse()?))
//! .body(String::from("Hello, world! :)")),
//! )
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType("text/html; charset=utf8".parse()?))
//! .body(String::from(
//! "<p><b>Hello</b>, <i>world</i>! <img src=\"cid:123\"></p>",
//! )),
//! ),
//! )?;
//! .multipart(MultiPart::alternative_plain_html(
//! String::from("Hello, world! :)"),
//! String::from("<p><b>Hello</b>, <i>world</i>! <img src=\"cid:123\"></p>"),
//! ))?;
//! # Ok(())
//! # }
//! ```
@@ -124,9 +106,10 @@
//!
//! ```rust
//! # use std::error::Error;
//! use lettre::message::{header, Body, Message, MultiPart, Part, SinglePart};
//! use std::fs;
//!
//! use lettre::message::{header, Attachment, Body, Message, MultiPart, SinglePart};
//!
//! # fn main() -> Result<(), Box<dyn Error>> {
//! let image = fs::read("docs/lettre.png")?;
//! // this image_body can be cloned and reused between emails.
@@ -144,47 +127,22 @@
//! MultiPart::mixed()
//! .multipart(
//! MultiPart::alternative()
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType("text/plain; charset=utf8".parse()?))
//! .body(String::from("Hello, world! :)")),
//! )
//! .singlepart(SinglePart::plain(String::from("Hello, world! :)")))
//! .multipart(
//! MultiPart::related()
//! .singlepart(SinglePart::html(String::from(
//! "<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
//! )))
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType(
//! "text/html; charset=utf8".parse()?,
//! ))
//! .body(String::from(
//! "<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
//! )),
//! )
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType("image/png".parse()?))
//! .header(header::ContentDisposition {
//! disposition: header::DispositionType::Inline,
//! parameters: vec![],
//! })
//! .header(header::ContentId("<123>".into()))
//! .body(image_body),
//! Attachment::new_inline(String::from("123"))
//! .body(image_body, "image/png".parse().unwrap()),
//! ),
//! ),
//! )
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType("text/plain; charset=utf8".parse()?))
//! .header(header::ContentDisposition {
//! disposition: header::DispositionType::Attachment,
//! parameters: vec![header::DispositionParam::Filename(
//! header::Charset::Ext("utf-8".into()),
//! None,
//! "example.rs".as_bytes().into(),
//! )],
//! })
//! .body(String::from("fn main() { println!(\"Hello, World!\") }")),
//! ),
//! .singlepart(Attachment::new(String::from("example.rs")).body(
//! String::from("fn main() { println!(\"Hello, World!\") }"),
//! "text/plain".parse().unwrap(),
//! )),
//! )?;
//! # Ok(())
//! # }
@@ -238,25 +196,26 @@
//! ```
//! </details>
use std::{io::Write, iter, time::SystemTime};
pub use attachment::Attachment;
pub use body::{Body, IntoBody, MaybeString};
#[cfg(feature = "dkim")]
pub use dkim::*;
pub use mailbox::*;
pub use mimebody::*;
pub use mime;
mod attachment;
mod body;
#[cfg(feature = "dkim")]
pub mod dkim;
pub mod header;
mod mailbox;
mod mimebody;
mod utf8_b;
use std::{convert::TryFrom, time::SystemTime};
use uuid::Uuid;
use crate::{
address::Envelope,
message::header::{ContentTransferEncoding, EmailDate, Header, Headers, MailboxesHeader},
message::header::{ContentTransferEncoding, Header, Headers, MailboxesHeader},
Error as EmailError,
};
@@ -291,37 +250,40 @@ impl MessageBuilder {
}
/// Add mailbox to header
pub fn mailbox<H: Header + MailboxesHeader>(mut self, header: H) -> Self {
if self.headers.has::<H>() {
self.headers.get_mut::<H>().unwrap().join_mailboxes(header);
self
} else {
self.header(header)
pub fn mailbox<H: Header + MailboxesHeader>(self, header: H) -> Self {
match self.headers.get::<H>() {
Some(mut header_) => {
header_.join_mailboxes(header);
self.header(header_)
}
None => self.header(header),
}
}
/// Add `Date` header to message
///
/// Shortcut for `self.header(header::Date(date))`.
pub fn date(self, date: EmailDate) -> Self {
self.header(header::Date(date))
/// Shortcut for `self.header(header::Date::new(st))`.
pub fn date(self, st: SystemTime) -> Self {
self.header(header::Date::new(st))
}
/// Set `Date` header using current date/time
///
/// Shortcut for `self.date(SystemTime::now())`.
/// Shortcut for `self.date(SystemTime::now())`, it is automatically inserted
/// if no date has been provided.
pub fn date_now(self) -> Self {
self.date(SystemTime::now().into())
self.date(SystemTime::now())
}
/// Set `Subject` header to message
///
/// Shortcut for `self.header(header::Subject(subject.into()))`.
pub fn subject<S: Into<String>>(self, subject: S) -> Self {
self.header(header::Subject(subject.into()))
let s: String = subject.into();
self.header(header::Subject::from(s))
}
/// Set `Mime-Version` header to 1.0
/// Set `MIME-Version` header to 1.0
///
/// Shortcut for `self.header(header::MIME_VERSION_1_0)`.
///
@@ -336,7 +298,7 @@ impl MessageBuilder {
///
/// Shortcut for `self.header(header::Sender(mbox))`.
pub fn sender(self, mbox: Mailbox) -> Self {
self.header(header::Sender(mbox))
self.header(header::Sender::from(mbox))
}
/// Set or add mailbox to `From` header
@@ -345,7 +307,7 @@ impl MessageBuilder {
///
/// Shortcut for `self.mailbox(header::From(mbox))`.
pub fn from(self, mbox: Mailbox) -> Self {
self.mailbox(header::From(mbox.into()))
self.mailbox(header::From::from(Mailboxes::from(mbox)))
}
/// Set or add mailbox to `ReplyTo` header
@@ -381,16 +343,16 @@ impl MessageBuilder {
/// Set or add message id to [`In-Reply-To`
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
pub fn in_reply_to(self, id: String) -> Self {
self.header(header::InReplyTo(id))
self.header(header::InReplyTo::from(id))
}
/// Set or add message id to [`References`
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
pub fn references(self, id: String) -> Self {
self.header(header::References(id))
self.header(header::References::from(id))
}
/// Set [Message-Id
/// Set [Message-ID
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
///
/// Should generally be inserted by the mail relay.
@@ -399,7 +361,7 @@ impl MessageBuilder {
/// `<UUID@HOSTNAME>`.
pub fn message_id(self, id: Option<String>) -> Self {
match id {
Some(i) => self.header(header::MessageId(i)),
Some(i) => self.header(header::MessageId::from(i)),
None => {
#[cfg(feature = "hostname")]
let hostname = hostname::get()
@@ -409,9 +371,9 @@ impl MessageBuilder {
#[cfg(not(feature = "hostname"))]
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_string();
self.header(header::MessageId(
self.header(header::MessageId::from(
// https://tools.ietf.org/html/rfc5322#section-3.6.4
format!("<{}@{}>", Uuid::new_v4(), hostname),
format!("<{}@{}>", make_message_id(), hostname),
))
}
}
@@ -420,7 +382,7 @@ impl MessageBuilder {
/// Set [User-Agent
/// header](https://tools.ietf.org/html/draft-melnikov-email-user-agent-004)
pub fn user_agent(self, id: String) -> Self {
self.header(header::UserAgent(id))
self.header(header::UserAgent::from(id))
}
/// Force specific envelope (by default it is derived from headers)
@@ -437,7 +399,7 @@ impl MessageBuilder {
// https://tools.ietf.org/html/rfc5322#section-3.6
// Insert Date if missing
let res = if self.headers.get::<header::Date>().is_none() {
let mut res = if self.headers.get::<header::Date>().is_none() {
self.date_now()
} else {
self
@@ -446,7 +408,7 @@ impl MessageBuilder {
// Fail is missing correct originator (Sender or From)
match res.headers.get::<header::From>() {
Some(header::From(f)) => {
let from: Vec<Mailbox> = f.clone().into();
let from: Vec<Mailbox> = f.into();
if from.len() > 1 && res.headers.get::<header::Sender>().is_none() {
return Err(EmailError::TooManyFrom);
}
@@ -460,6 +422,10 @@ impl MessageBuilder {
Some(e) => e,
None => Envelope::try_from(&res.headers)?,
};
// Remove `Bcc` headers now the envelope is set
res.headers.remove::<header::Bcc>();
Ok(Message {
headers: res.headers,
body,
@@ -473,7 +439,7 @@ impl MessageBuilder {
/// `Content-Transfer-Encoding`, based on the most efficient and valid encoding
/// for `body`.
pub fn body<T: IntoBody>(mut self, body: T) -> Result<Message, EmailError> {
let maybe_encoding = self.headers.get::<ContentTransferEncoding>().copied();
let maybe_encoding = self.headers.get::<ContentTransferEncoding>();
let body = body.into_body(maybe_encoding);
self.headers.set(body.encoding());
@@ -528,17 +494,89 @@ impl Message {
self.format(&mut out);
out
}
#[cfg(feature = "dkim")]
/// Format body for signing
pub(crate) fn body_raw(&self) -> Vec<u8> {
let mut out = Vec::new();
match &self.body {
MessageBody::Mime(p) => p.format(&mut out),
MessageBody::Raw(r) => out.extend_from_slice(r),
};
out.extend_from_slice(b"\r\n");
out
}
/// Sign the message using Dkim
///
/// Example:
/// ```rust
/// use lettre::{
/// message::dkim::{DkimConfig, DkimSigningAlgorithm, DkimSigningKey},
/// Message,
/// };
///
/// let mut message = Message::builder()
/// .from("Alice <alice@example.org>".parse().unwrap())
/// .reply_to("Bob <bob@example.org>".parse().unwrap())
/// .to("Carla <carla@example.net>".parse().unwrap())
/// .subject("Hello")
/// .body("Hi there, it's a test email, with utf-8 chars ë!\n\n\n".to_string())
/// .unwrap();
/// let key = "-----BEGIN RSA PRIVATE KEY-----
/// MIIEowIBAAKCAQEAt2gawjoybf0mAz0mSX0cq1ah5F9cPazZdCwLnFBhRufxaZB8
/// NLTdc9xfPIOK8l/xGrN7Nd63J4cTATqZukumczkA46O8YKHwa53pNT6NYwCNtDUL
/// eBu+7xUW18GmDzkIFkxGO2R5kkTeWPlKvKpEiicIMfl0OmyW/fI3AbtM7e/gmqQ4
/// kEYIO0mTjPT+jTgWE4JIi5KUTHudUBtfMKcSFyM2HkUOExl1c9+A4epjRFQwEXMA
/// hM5GrqZoOdUm4fIpvGpLIGIxFgHPpZYbyq6yJZzH3+5aKyCHrsHawPuPiCD45zsU
/// re31zCE6b6k1sDiiBR4CaRHnbL7hxFp0aNLOVQIDAQABAoIBAGMK3gBrKxaIcUGo
/// gQeIf7XrJ6vK72YC9L8uleqI4a9Hy++E7f4MedZ6eBeWta8jrnEL4Yp6xg+beuDc
/// A24+Mhng+6Dyp+TLLqj+8pQlPnbrMprRVms7GIXFrrs+wO1RkBNyhy7FmH0roaMM
/// pJZzoGW2pE9QdbqjL3rdlWTi/60xRX9eZ42nNxYnbc+RK03SBd46c3UBha6Y9iQX
/// 562yWilDnB5WCX2tBoSN39bEhJvuZDzMwOuGw68Q96Hdz82Iz1xVBnRhH+uNStjR
/// VnAssSHVxPSpwWrm3sHlhjBHWPnNIaOKIKl1lbL+qWfVQCj/6a5DquC+vYAeYR6L
/// 3mA0z0ECgYEA5YkNYcILSXyE0hZ8eA/t58h8eWvYI5iqt3nT4fznCoYJJ74Vukeg
/// 6BTlq/CsanwT1lDtvDKrOaJbA7DPTES/bqT0HoeIdOvAw9w/AZI5DAqYp61i6RMK
/// xfAQL/Ik5MDFN8gEMLLXRVMe/aR27f6JFZpShJOK/KCzHqikKfYVJ+UCgYEAzI2F
/// ZlTyittWSyUSl5UKyfSnFOx2+6vNy+lu5DeMJu8Wh9rqBk388Bxq98CfkCseWESN
/// pTCGdYltz9DvVNBdBLwSMdLuYJAI6U+Zd70MWyuNdHFPyWVHUNqMUBvbUtj2w74q
/// Hzu0GI0OrRjdX6C63S17PggmT/N2R9X7P4STxbECgYA+AZAD4I98Ao8+0aQ+Ks9x
/// 1c8KXf+9XfiAKAD9A3zGcv72JXtpHwBwsXR5xkJNYcdaFfKi7G0k3J8JmDHnwIqW
/// MSlhNeu+6hDg2BaNLhsLDbG/Wi9mFybJ4df9m8Qrp4efUgEPxsAwkgvFKTCXijMu
/// CspP1iutoxvAJH50d22voQKBgDIsSFtIXNGYaTs3Va8enK3at5zXP3wNsQXiNRP/
/// V/44yNL77EktmewfXFF2yuym1uOZtRCerWxpEClYO0wXa6l8pA3aiiPfUIBByQfo
/// s/4s2Z6FKKfikrKPWLlRi+NvWl+65kQQ9eTLvJzSq4IIP61+uWsGvrb/pbSLFPyI
/// fWKRAoGBALFCStBXvdMptjq4APUzAdJ0vytZzXkOZHxgmc+R0fQn22OiW0huW6iX
/// JcaBbL6ZSBIMA3AdaIjtvNRiomueHqh0GspTgOeCE2585TSFnw6vEOJ8RlR4A0Mw
/// I45fbR4l+3D/30WMfZlM6bzZbwPXEnr2s1mirmuQpjumY9wLhK25
/// -----END RSA PRIVATE KEY-----";
/// let signing_key = DkimSigningKey::new(key.to_string(), DkimSigningAlgorithm::Rsa).unwrap();
/// message.sign(&DkimConfig::default_config(
/// "dkimtest".to_string(),
/// "example.org".to_string(),
/// signing_key,
/// ));
/// println!(
/// "message: {}",
/// std::str::from_utf8(&message.formatted()).unwrap()
/// );
/// ```
#[cfg(feature = "dkim")]
pub fn sign(&mut self, dkim_config: &DkimConfig) {
dkim_sign(self, dkim_config);
}
}
impl EmailFormat for Message {
fn format(&self, out: &mut Vec<u8>) {
out.extend_from_slice(self.headers.to_string().as_bytes());
write!(out, "{}", self.headers)
.expect("A Write implementation panicked while formatting headers");
match &self.body {
MessageBody::Mime(p) => p.format(out),
MessageBody::Raw(r) => {
out.extend_from_slice(b"\r\n");
out.extend_from_slice(&r)
out.extend_from_slice(r)
}
}
}
@@ -550,9 +588,17 @@ impl Default for MessageBuilder {
}
}
/// Create a random message id.
/// (Not cryptographically random)
fn make_message_id() -> String {
iter::repeat_with(fastrand::alphanumeric).take(36).collect()
}
#[cfg(test)]
mod test {
use crate::message::{header, mailbox::Mailbox, Message, MultiPart, SinglePart};
use std::time::{Duration, SystemTime};
use super::{header, mailbox::Mailbox, make_message_id, Message, MultiPart, SinglePart};
#[test]
fn email_missing_originator() {
@@ -581,10 +627,12 @@ mod test {
#[test]
fn email_message() {
let date = "Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap();
// Tue, 15 Nov 1994 08:12:31 GMT
let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151);
let email = Message::builder()
.date(date)
.bcc("hidden@example.com".parse().unwrap())
.header(header::From(
vec![Mailbox::new(
Some("Каи".into()),
@@ -595,16 +643,16 @@ mod test {
.header(header::To(
vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(),
))
.header(header::Subject("яңа ел белән!".into()))
.header(header::Subject::from(String::from("яңа ел белән!")))
.body(String::from("Happy new year!"))
.unwrap();
assert_eq!(
String::from_utf8(email.formatted()).unwrap(),
concat!(
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n",
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
"To: Pony O.P. <pony@domain.tld>\r\n",
"To: \"Pony O.P.\" <pony@domain.tld>\r\n",
"Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
@@ -615,7 +663,8 @@ mod test {
#[test]
fn email_with_png() {
let date = "Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap();
// Tue, 15 Nov 1994 08:12:31 GMT
let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151);
let img = std::fs::read("./docs/lettre.png").unwrap();
let m = Message::builder()
.date(date)
@@ -627,21 +676,16 @@ mod test {
MultiPart::related()
.singlepart(
SinglePart::builder()
.header(header::ContentType(
"text/html; charset=utf8".parse().unwrap(),
))
.header(header::ContentType::TEXT_HTML)
.body(String::from(
"<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
)),
)
.singlepart(
SinglePart::builder()
.header(header::ContentType("image/png".parse().unwrap()))
.header(header::ContentDisposition {
disposition: header::DispositionType::Inline,
parameters: vec![],
})
.header(header::ContentId("<123>".into()))
.header(header::ContentType::parse("image/png").unwrap())
.header(header::ContentDisposition::inline())
.header(header::ContentId::from(String::from("<123>")))
.body(img),
),
)
@@ -652,11 +696,27 @@ mod test {
let expected = String::from_utf8(file_expected).unwrap();
for (i, line) in output.lines().zip(expected.lines()).enumerate() {
if i == 6 || i == 8 || i == 13 || i == 232 {
if i == 7 || i == 9 || i == 14 || i == 233 {
continue;
}
assert_eq!(line.0, line.1)
}
}
#[test]
fn test_make_message_id() {
let mut ids = std::collections::HashSet::with_capacity(10);
for _ in 0..1000 {
ids.insert(make_message_id());
}
// Ensure there are no duplicates
assert_eq!(1000, ids.len());
// Ensure correct length
for id in ids {
assert_eq!(36, id.len());
}
}
}

View File

@@ -1,60 +0,0 @@
// https://tools.ietf.org/html/rfc1522
fn allowed_char(c: char) -> bool {
c >= 1 as char && c <= 9 as char
|| c == 11 as char
|| c == 12 as char
|| c >= 14 as char && c <= 127 as char
}
pub fn encode(s: &str) -> String {
if s.chars().all(allowed_char) {
s.into()
} else {
format!("=?utf-8?b?{}?=", base64::encode(s))
}
}
pub fn decode(s: &str) -> Option<String> {
s.strip_prefix("=?utf-8?b?")
.and_then(|stripped| stripped.strip_suffix("?="))
.map_or_else(
|| Some(s.into()),
|stripped| {
let decoded = base64::decode(stripped).ok()?;
let decoded = String::from_utf8(decoded).ok()?;
Some(decoded)
},
)
}
#[cfg(test)]
mod test {
use super::{decode, encode};
#[test]
fn encode_ascii() {
assert_eq!(&encode("Kayo. ?"), "Kayo. ?");
}
#[test]
fn decode_ascii() {
assert_eq!(decode("Kayo. ?"), Some("Kayo. ?".into()));
}
#[test]
fn encode_utf8() {
assert_eq!(
&encode("Привет, мир!"),
"=?utf-8?b?0J/RgNC40LLQtdGCLCDQvNC40YAh?="
);
}
#[test]
fn decode_utf8() {
assert_eq!(
decode("=?utf-8?b?0J/RgNC40LLQtdGCLCDQvNC40YAh?="),
Some("Привет, мир!".into())
);
}
}

View File

@@ -1,8 +1,9 @@
//! Error and result type for file transport
use crate::BoxError;
use std::{error::Error as StdError, fmt};
use crate::BoxError;
/// The Errors that may occur when sending an email over SMTP
pub struct Error {
inner: Box<Inner>,

View File

@@ -9,9 +9,10 @@
//! #
//! # #[cfg(all(feature = "file-transport", feature = "builder"))]
//! # fn main() -> Result<(), Box<dyn Error>> {
//! use lettre::{FileTransport, Message, Transport};
//! use std::env::temp_dir;
//!
//! use lettre::{FileTransport, Message, Transport};
//!
//! // Write to the local temp directory
//! let sender = FileTransport::new(temp_dir());
//! let email = Message::builder()
@@ -41,9 +42,10 @@
//! #
//! # #[cfg(all(feature = "file-transport-envelope", feature = "builder"))]
//! # fn main() -> Result<(), Box<dyn Error>> {
//! use lettre::{FileTransport, Message, Transport};
//! use std::env::temp_dir;
//!
//! use lettre::{FileTransport, Message, Transport};
//!
//! // Write to the local temp directory
//! let sender = FileTransport::with_envelope(temp_dir());
//! let email = Message::builder()
@@ -70,7 +72,8 @@
//! # #[cfg(all(feature = "tokio1", feature = "file-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> {
//! use std::env::temp_dir;
//! use lettre::{AsyncTransport, Tokio1Executor, Message, AsyncFileTransport};
//!
//! use lettre::{AsyncFileTransport, AsyncTransport, Message, Tokio1Executor};
//!
//! // Write to the local temp directory
//! let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir());
@@ -95,7 +98,8 @@
//! # #[cfg(all(feature = "async-std1", feature = "file-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> {
//! use std::env::temp_dir;
//! use lettre::{AsyncTransport, AsyncStd1Executor, Message, AsyncFileTransport};
//!
//! use lettre::{AsyncFileTransport, AsyncStd1Executor, AsyncTransport, Message};
//!
//! // Write to the local temp directory
//! let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir());
@@ -132,20 +136,22 @@
//! {"forward_path":["hei@domain.tld"],"reverse_path":"nobody@domain.tld"}
//! ```
pub use self::error::Error;
use crate::{address::Envelope, Transport};
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
use crate::{AsyncTransport, Executor};
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
use async_trait::async_trait;
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
use std::marker::PhantomData;
use std::{
path::{Path, PathBuf},
str,
};
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
use async_trait::async_trait;
use uuid::Uuid;
pub use self::error::Error;
use crate::{address::Envelope, Transport};
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
use crate::{AsyncTransport, Executor};
mod error;
type Id = String;
@@ -163,11 +169,8 @@ pub struct FileTransport {
/// Asynchronously writes the content and the envelope information to a file
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))
)]
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
pub struct AsyncFileTransport<E: Executor> {
inner: FileTransport,
marker_: PhantomData<E>,
@@ -220,7 +223,7 @@ impl FileTransport {
}
}
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
impl<E> AsyncFileTransport<E>
where
E: Executor,
@@ -290,7 +293,7 @@ impl Transport for FileTransport {
}
}
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
#[async_trait]
impl<E> AsyncTransport for AsyncFileTransport<E>
where

View File

@@ -4,6 +4,27 @@
//! for sending emails. It automatically manages the underlying resources and doesn't require any
//! specific knowledge of email protocols in order to be used.
//!
//! ### Getting started
//!
//! Sending emails from your programs requires using an email relay, as client libraries are not
//! designed to handle email delivery by themselves. Depending on your infrastructure, your relay
//! could be:
//!
//! * a service from your Cloud or hosting provider
//! * an email server ([MTA] for Mail Transfer Agent, like Postfix or Exchange), running either
//! locally on your servers or accessible over the network
//! * a dedicated external service, like Mailchimp, Mailgun, etc.
//!
//! In most cases, the best option is to:
//!
//! * Use the [`SMTP`] transport, with the [`relay`] builder (or one of its async counterparts)
//! with your server's hostname. They provide modern and secure defaults.
//! * Use the [`credentials`] method of the builder to pass your credentials.
//!
//! These should be enough to safely cover most use cases.
//!
//! ### Available transports
//!
//! The following transports are available:
//!
//! | Module | Protocol | Sync API | Async API | Description |
@@ -35,8 +56,7 @@
//! #
//! # #[cfg(all(feature = "builder", feature = "smtp-transport"))]
//! # fn main() -> Result<(), Box<dyn Error>> {
//! use lettre::transport::smtp::authentication::Credentials;
//! use lettre::{Message, SmtpTransport, Transport};
//! use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
@@ -63,6 +83,11 @@
//! # fn main() {}
//! ```
//!
//! [MTA]: https://en.wikipedia.org/wiki/Message_transfer_agent
//! [`SMTP`]: crate::transport::smtp
//! [`relay`]: crate::SmtpTransport::relay
//! [`starttls_relay`]: crate::SmtpTransport::starttls_relay
//! [`credentials`]: crate::transport::smtp::SmtpTransportBuilder::credentials
//! [`Message`]: crate::Message
//! [`file`]: self::file
//! [`SmtpTransport`]: crate::SmtpTransport
@@ -73,7 +98,7 @@
//! [`AsyncFileTransport`]: crate::AsyncFileTransport
//! [`StubTransport`]: crate::transport::stub::StubTransport
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
use async_trait::async_trait;
use crate::Envelope;
@@ -110,11 +135,8 @@ pub trait Transport {
}
/// Async Transport method for emails
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))
)]
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
#[async_trait]
pub trait AsyncTransport {
/// Response produced by the Transport
@@ -129,7 +151,7 @@ pub trait AsyncTransport {
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
let raw = message.formatted();
let envelope = message.envelope();
self.send_raw(&envelope, &raw).await
self.send_raw(envelope, &raw).await
}
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;

View File

@@ -1,8 +1,9 @@
//! Error and result type for sendmail transport
use crate::BoxError;
use std::{error::Error as StdError, fmt};
use crate::BoxError;
/// The Errors that may occur when sending an email over sendmail
pub struct Error {
inner: Box<Inner>,

View File

@@ -26,29 +26,6 @@
//! # fn main() {}
//! ```
//!
//! ## Async tokio 0.2 example
//!
//! ```rust,no_run
//! # use std::error::Error;
//! #
//! # #[cfg(all(feature = "tokio02", feature = "sendmail-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> {
//! use lettre::{Message, AsyncTransport, Tokio02Executor, AsyncSendmailTransport, SendmailTransport};
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body(String::from("Be happy!"))?;
//!
//! let sender = AsyncSendmailTransport::<Tokio02Executor>::new();
//! let result = sender.send(email).await;
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//! ```
//!
//! ## Async tokio 1.x example
//!
//! ```rust,no_run
@@ -56,7 +33,9 @@
//! #
//! # #[cfg(all(feature = "tokio1", feature = "sendmail-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> {
//! use lettre::{Message, AsyncTransport, Tokio1Executor, AsyncSendmailTransport, SendmailTransport};
//! use lettre::{
//! AsyncSendmailTransport, AsyncTransport, Message, SendmailTransport, Tokio1Executor,
//! };
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
@@ -95,19 +74,7 @@
//! # }
//! ```
pub use self::error::Error;
#[cfg(feature = "async-std1")]
use crate::AsyncStd1Executor;
#[cfg(feature = "tokio02")]
use crate::Tokio02Executor;
#[cfg(feature = "tokio1")]
use crate::Tokio1Executor;
use crate::{address::Envelope, Transport};
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
use crate::{AsyncTransport, Executor};
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
use async_trait::async_trait;
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
use std::marker::PhantomData;
use std::{
ffi::OsString,
@@ -115,9 +82,21 @@ use std::{
process::{Command, Stdio},
};
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
use async_trait::async_trait;
pub use self::error::Error;
#[cfg(feature = "async-std1")]
use crate::AsyncStd1Executor;
#[cfg(feature = "tokio1")]
use crate::Tokio1Executor;
use crate::{address::Envelope, Transport};
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
use crate::{AsyncTransport, Executor};
mod error;
const DEFAULT_SENDMAIL: &str = "/usr/sbin/sendmail";
const DEFAULT_SENDMAIL: &str = "sendmail";
/// Sends emails using the `sendmail` command
#[derive(Debug, Clone)]
@@ -130,18 +109,18 @@ pub struct SendmailTransport {
/// Asynchronously sends emails using the `sendmail` command
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))
)]
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
pub struct AsyncSendmailTransport<E: Executor> {
inner: SendmailTransport,
marker_: PhantomData<E>,
}
impl SendmailTransport {
/// Creates a new transport with the default `/usr/sbin/sendmail` command
/// Creates a new transport with the `sendmail` command
///
/// Note: This uses the `sendmail` command in the current `PATH`. To use another command,
/// use [SendmailTransport::new_with_command].
pub fn new() -> SendmailTransport {
SendmailTransport {
command: DEFAULT_SENDMAIL.into(),
@@ -170,12 +149,15 @@ impl SendmailTransport {
}
}
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
impl<E> AsyncSendmailTransport<E>
where
E: Executor,
{
/// Creates a new transport with the default `/usr/sbin/sendmail` command
/// Creates a new transport with the `sendmail` command
///
/// Note: This uses the `sendmail` command in the current `PATH`. To use another command,
/// use [AsyncSendmailTransport::new_with_command].
pub fn new() -> Self {
Self {
inner: SendmailTransport::new(),
@@ -191,24 +173,6 @@ where
}
}
#[cfg(feature = "tokio02")]
fn tokio02_command(&self, envelope: &Envelope) -> tokio02_crate::process::Command {
use tokio02_crate::process::Command;
let mut c = Command::new(&self.inner.command);
c.kill_on_drop(true);
c.arg("-i");
if let Some(from) = envelope.from() {
c.arg("-f").arg(from);
}
c.arg("--")
.args(envelope.to())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
c
}
#[cfg(feature = "tokio1")]
fn tokio1_command(&self, envelope: &Envelope) -> tokio1_crate::process::Command {
use tokio1_crate::process::Command;
@@ -253,7 +217,7 @@ impl Default for SendmailTransport {
}
}
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
impl<E> Default for AsyncSendmailTransport<E>
where
E: Executor,
@@ -306,7 +270,7 @@ impl AsyncTransport for AsyncSendmailTransport<AsyncStd1Executor> {
.stdin
.as_mut()
.unwrap()
.write_all(&email)
.write_all(email)
.await
.map_err(error::client)?;
let output = process.output().await.map_err(error::client)?;
@@ -320,38 +284,6 @@ impl AsyncTransport for AsyncSendmailTransport<AsyncStd1Executor> {
}
}
#[cfg(feature = "tokio02")]
#[async_trait]
impl AsyncTransport for AsyncSendmailTransport<Tokio02Executor> {
type Ok = ();
type Error = Error;
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
use tokio02_crate::io::AsyncWriteExt;
let mut command = self.tokio02_command(envelope);
// Spawn the sendmail command
let mut process = command.spawn().map_err(error::client)?;
process
.stdin
.as_mut()
.unwrap()
.write_all(&email)
.await
.map_err(error::client)?;
let output = process.wait_with_output().await.map_err(error::client)?;
if output.status.success() {
Ok(())
} else {
let stderr = String::from_utf8(output.stderr).map_err(error::response)?;
Err(error::client(stderr))
}
}
}
#[cfg(feature = "tokio1")]
#[async_trait]
impl AsyncTransport for AsyncSendmailTransport<Tokio1Executor> {
@@ -370,7 +302,7 @@ impl AsyncTransport for AsyncSendmailTransport<Tokio1Executor> {
.stdin
.as_mut()
.unwrap()
.write_all(&email)
.write_all(email)
.await
.map_err(error::client)?;
let output = process.wait_with_output().await.map_err(error::client)?;

View File

@@ -1,49 +1,36 @@
use std::fmt::{self, Debug};
use std::marker::PhantomData;
use std::{
fmt::{self, Debug},
marker::PhantomData,
sync::Arc,
time::Duration,
};
use async_trait::async_trait;
#[cfg(feature = "pool")]
use super::pool::async_impl::Pool;
#[cfg(feature = "pool")]
use super::PoolConfig;
use super::{
client::AsyncSmtpConnection, ClientId, Credentials, Error, Mechanism, Response, SmtpInfo,
};
#[cfg(feature = "async-std1")]
use crate::AsyncStd1Executor;
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
use crate::AsyncTransport;
#[cfg(feature = "tokio02")]
use crate::Tokio02Executor;
#[cfg(feature = "tokio1")]
use crate::Tokio1Executor;
use crate::{Envelope, Executor};
/// Asynchronously sends emails using the SMTP protocol
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))
)]
pub struct AsyncSmtpTransport<E> {
// TODO: pool
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
pub struct AsyncSmtpTransport<E: Executor> {
#[cfg(feature = "pool")]
inner: Arc<Pool<E>>,
#[cfg(not(feature = "pool"))]
inner: AsyncSmtpClient<E>,
}
#[cfg(feature = "tokio02")]
#[async_trait]
impl AsyncTransport for AsyncSmtpTransport<Tokio02Executor> {
type Ok = Response;
type Error = Error;
/// Sends an email
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
let mut conn = self.inner.connection().await?;
let result = conn.send(envelope, email).await?;
conn.quit().await?;
Ok(result)
}
}
#[cfg(feature = "tokio1")]
#[async_trait]
impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> {
@@ -56,6 +43,7 @@ impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> {
let result = conn.send(envelope, email).await?;
#[cfg(not(feature = "pool"))]
conn.quit().await?;
Ok(result)
@@ -91,8 +79,6 @@ where
/// Creates an encrypted transport over submissions port, using the provided domain
/// to validate TLS certificates.
#[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
@@ -101,8 +87,6 @@ where
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-rustls-tls"
@@ -130,8 +114,6 @@ where
/// An error is returned if the connection can't be upgraded. No credentials
/// or emails will be sent to the server, protecting from downgrade attacks.
#[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
@@ -140,8 +122,6 @@ where
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-rustls-tls"
@@ -170,21 +150,41 @@ where
///
/// * No authentication
/// * No TLS
/// * A 60 seconds timeout for smtp commands
/// * Port 25
///
/// Consider using [`AsyncSmtpTransport::relay`](#method.relay) or
/// [`AsyncSmtpTransport::starttls_relay`](#method.starttls_relay) instead,
/// if possible.
pub fn builder_dangerous<T: Into<String>>(server: T) -> AsyncSmtpTransportBuilder {
let new = SmtpInfo {
let info = SmtpInfo {
server: server.into(),
..Default::default()
};
AsyncSmtpTransportBuilder { info: new }
AsyncSmtpTransportBuilder {
info,
#[cfg(feature = "pool")]
pool_config: PoolConfig::default(),
}
}
/// Tests the SMTP connection
///
/// `test_connection()` tests the connection by using the SMTP NOOP command.
/// The connection is closed afterwards if a connection pool is not used.
pub async fn test_connection(&self) -> Result<bool, Error> {
let mut conn = self.inner.connection().await?;
let is_connected = conn.test_connected().await;
#[cfg(not(feature = "pool"))]
conn.quit().await?;
Ok(is_connected)
}
}
impl<E> Debug for AsyncSmtpTransport<E> {
impl<E: Executor> Debug for AsyncSmtpTransport<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut builder = f.debug_struct("AsyncSmtpTransport");
builder.field("inner", &self.inner);
@@ -206,12 +206,11 @@ where
/// Contains client configuration.
/// Instances of this struct can be created using functions of [`AsyncSmtpTransport`].
#[derive(Debug, Clone)]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))
)]
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
pub struct AsyncSmtpTransportBuilder {
info: SmtpInfo,
#[cfg(feature = "pool")]
pool_config: PoolConfig,
}
/// Builder for the SMTP `AsyncSmtpTransport`
@@ -240,10 +239,14 @@ impl AsyncSmtpTransportBuilder {
self
}
/// Set the timeout duration
pub fn timeout(mut self, timeout: Option<Duration>) -> Self {
self.info.timeout = timeout;
self
}
/// Set the TLS settings to use
#[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
@@ -252,8 +255,6 @@ impl AsyncSmtpTransportBuilder {
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-rustls-tls"
@@ -264,6 +265,16 @@ impl AsyncSmtpTransportBuilder {
self
}
/// Use a custom configuration for the connection pool
///
/// Defaults can be found at [`PoolConfig`]
#[cfg(feature = "pool")]
#[cfg_attr(docsrs, doc(cfg(feature = "pool")))]
pub fn pool_config(mut self, pool_config: PoolConfig) -> Self {
self.pool_config = pool_config;
self
}
/// Build the transport
pub fn build<E>(self) -> AsyncSmtpTransport<E>
where
@@ -273,6 +284,10 @@ impl AsyncSmtpTransportBuilder {
info: self.info,
marker_: PhantomData,
};
#[cfg(feature = "pool")]
let client = Pool::new(self.pool_config, client);
AsyncSmtpTransport { inner: client }
}
}
@@ -294,13 +309,14 @@ where
let mut conn = E::connect(
&self.info.server,
self.info.port,
self.info.timeout,
&self.info.hello_name,
&self.info.tls,
)
.await?;
if let Some(credentials) = &self.info.credentials {
conn.auth(&self.info.authentication, &credentials).await?;
conn.auth(&self.info.authentication, credentials).await?;
}
Ok(conn)
}
@@ -314,6 +330,8 @@ impl<E> Debug for AsyncSmtpClient<E> {
}
}
// `clone` is unused when the `pool` feature is on
#[allow(dead_code)]
impl<E> AsyncSmtpClient<E>
where
E: Executor,

View File

@@ -1,9 +1,11 @@
//! Provides limited SASL authentication mechanisms
use crate::transport::smtp::error::{self, Error};
use std::fmt::{self, Debug, Display, Formatter};
use crate::transport::smtp::error::{self, Error};
/// Accepted authentication mechanisms
///
/// Trying LOGIN last as it is deprecated.
pub const DEFAULT_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];

View File

@@ -1,3 +1,9 @@
use std::{fmt::Display, time::Duration};
use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
#[cfg(feature = "tracing")]
use super::escape_crlf;
use super::{AsyncNetworkStream, ClientCodec, TlsParameters};
use crate::{
transport::smtp::{
@@ -10,11 +16,6 @@ use crate::{
},
Envelope,
};
use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use std::fmt::Display;
#[cfg(feature = "tracing")]
use super::escape_crlf;
macro_rules! try_smtp (
($err: expr, $client: ident) => ({
@@ -44,31 +45,17 @@ impl AsyncSmtpConnection {
&self.server_info
}
/// Connects to the configured server
///
/// Sends EHLO and parses server information
#[cfg(feature = "tokio02")]
pub async fn connect_tokio02(
hostname: &str,
port: u16,
hello_name: &ClientId,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncSmtpConnection, Error> {
let stream = AsyncNetworkStream::connect_tokio02(hostname, port, tls_parameters).await?;
Self::connect_impl(stream, hello_name).await
}
/// Connects to the configured server
///
/// Sends EHLO and parses server information
#[cfg(feature = "tokio1")]
pub async fn connect_tokio1(
hostname: &str,
port: u16,
pub async fn connect_tokio1<T: tokio1_crate::net::ToSocketAddrs>(
server: T,
timeout: Option<Duration>,
hello_name: &ClientId,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncSmtpConnection, Error> {
let stream = AsyncNetworkStream::connect_tokio1(hostname, port, tls_parameters).await?;
let stream = AsyncNetworkStream::connect_tokio1(server, timeout, tls_parameters).await?;
Self::connect_impl(stream, hello_name).await
}
@@ -76,13 +63,13 @@ impl AsyncSmtpConnection {
///
/// Sends EHLO and parses server information
#[cfg(feature = "async-std1")]
pub async fn connect_asyncstd1(
hostname: &str,
port: u16,
pub async fn connect_asyncstd1<T: async_std::net::ToSocketAddrs>(
server: T,
timeout: Option<Duration>,
hello_name: &ClientId,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncSmtpConnection, Error> {
let stream = AsyncNetworkStream::connect_asyncstd1(hostname, port, tls_parameters).await?;
let stream = AsyncNetworkStream::connect_asyncstd1(server, timeout, tls_parameters).await?;
Self::connect_impl(stream, hello_name).await
}
@@ -175,10 +162,7 @@ impl AsyncSmtpConnection {
) -> Result<(), Error> {
if self.server_info.supports_feature(Extension::StartTls) {
try_smtp!(self.command(Starttls).await, self);
try_smtp!(
self.stream.get_mut().upgrade_tls(tls_parameters).await,
self
);
self.stream.get_mut().upgrade_tls(tls_parameters).await?;
#[cfg(feature = "tracing")]
tracing::debug!("connection encrypted");
// Send EHLO again
@@ -312,7 +296,10 @@ impl AsyncSmtpConnection {
return if response.is_positive() {
Ok(response)
} else {
Err(error::code(response.code))
Err(error::code(
response.code(),
response.first_line().map(|s| s.to_owned()),
))
}
}
Err(nom::Err::Failure(e)) => {
@@ -327,4 +314,10 @@ impl AsyncSmtpConnection {
Err(error::response("incomplete response"))
}
/// The X509 certificate of the server (DER encoded)
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
self.stream.get_ref().peer_certificate()
}
}

View File

@@ -1,48 +1,31 @@
#[cfg(any(
feature = "tokio02-rustls-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-rustls-tls"
))]
use std::sync::Arc;
use std::{
io, mem,
net::SocketAddr,
pin::Pin,
task::{Context, Poll},
time::Duration,
};
#[cfg(feature = "async-std1-native-tls")]
use async_native_tls::TlsStream as AsyncStd1TlsStream;
#[cfg(feature = "async-std1")]
use async_std::net::{TcpStream as AsyncStd1TcpStream, ToSocketAddrs as AsyncStd1ToSocketAddrs};
use futures_io::{
AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Error as IoError, ErrorKind,
Result as IoResult,
};
#[cfg(feature = "tokio02")]
use tokio02_crate::io::{AsyncRead as _, AsyncWrite as _};
#[cfg(feature = "async-std1-rustls-tls")]
use futures_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
#[cfg(feature = "tokio1")]
use tokio1_crate::io::{AsyncRead as _, AsyncWrite as _, ReadBuf as Tokio1ReadBuf};
#[cfg(feature = "async-std1")]
use async_std::net::TcpStream as AsyncStd1TcpStream;
#[cfg(feature = "tokio02")]
use tokio02_crate::net::TcpStream as Tokio02TcpStream;
#[cfg(feature = "tokio1")]
use tokio1_crate::net::TcpStream as Tokio1TcpStream;
#[cfg(feature = "async-std1-native-tls")]
use async_native_tls::TlsStream as AsyncStd1TlsStream;
#[cfg(feature = "tokio02-native-tls")]
use tokio02_native_tls_crate::TlsStream as Tokio02TlsStream;
use tokio1_crate::net::{TcpStream as Tokio1TcpStream, ToSocketAddrs as Tokio1ToSocketAddrs};
#[cfg(feature = "tokio1-native-tls")]
use tokio1_native_tls_crate::TlsStream as Tokio1TlsStream;
#[cfg(feature = "async-std1-rustls-tls")]
use async_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
#[cfg(feature = "tokio02-rustls-tls")]
use tokio02_rustls::client::TlsStream as Tokio02RustlsTlsStream;
#[cfg(feature = "tokio1-rustls-tls")]
use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
#[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
@@ -63,15 +46,6 @@ pub struct AsyncNetworkStream {
#[allow(clippy::large_enum_variant)]
#[allow(dead_code)]
enum InnerAsyncNetworkStream {
/// Plain Tokio 0.2 TCP stream
#[cfg(feature = "tokio02")]
Tokio02Tcp(Tokio02TcpStream),
/// Encrypted Tokio 0.2 TCP stream
#[cfg(feature = "tokio02-native-tls")]
Tokio02NativeTls(Tokio02TlsStream<Tokio02TcpStream>),
/// Encrypted Tokio 0.2 TCP stream
#[cfg(feature = "tokio02-rustls-tls")]
Tokio02RustlsTls(Tokio02RustlsTlsStream<Tokio02TcpStream>),
/// Plain Tokio 1.x TCP stream
#[cfg(feature = "tokio1")]
Tokio1Tcp(Tokio1TcpStream),
@@ -106,14 +80,6 @@ impl AsyncNetworkStream {
/// Returns peer's address
pub fn peer_addr(&self) -> IoResult<SocketAddr> {
match self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(ref s) => s.peer_addr(),
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(ref s) => {
s.get_ref().get_ref().get_ref().peer_addr()
}
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref s) => s.get_ref().0.peer_addr(),
#[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(ref s) => s.peer_addr(),
#[cfg(feature = "tokio1-native-tls")]
@@ -138,32 +104,48 @@ impl AsyncNetworkStream {
}
}
#[cfg(feature = "tokio02")]
pub async fn connect_tokio02(
hostname: &str,
port: u16,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncNetworkStream, Error> {
let tcp_stream = Tokio02TcpStream::connect((hostname, port))
.await
.map_err(error::connection)?;
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio02Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters {
stream.upgrade_tls(tls_parameters).await?;
}
Ok(stream)
}
#[cfg(feature = "tokio1")]
pub async fn connect_tokio1(
hostname: &str,
port: u16,
pub async fn connect_tokio1<T: Tokio1ToSocketAddrs>(
server: T,
timeout: Option<Duration>,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncNetworkStream, Error> {
let tcp_stream = Tokio1TcpStream::connect((hostname, port))
.await
.map_err(error::connection)?;
async fn try_connect_timeout<T: Tokio1ToSocketAddrs>(
server: T,
timeout: Duration,
) -> Result<Tokio1TcpStream, Error> {
let addrs = tokio1_crate::net::lookup_host(server)
.await
.map_err(error::connection)?;
let mut last_err = None;
for addr in addrs {
let connect_future = Tokio1TcpStream::connect(&addr);
match tokio1_crate::time::timeout(timeout, connect_future).await {
Ok(Ok(stream)) => return Ok(stream),
Ok(Err(err)) => last_err = Some(err),
Err(_) => {
last_err = Some(io::Error::new(
io::ErrorKind::TimedOut,
"connection timed out",
))
}
}
}
Err(match last_err {
Some(last_err) => error::connection(last_err),
None => error::connection("could not resolve to any address"),
})
}
let tcp_stream = match timeout {
Some(t) => try_connect_timeout(server, t).await?,
None => Tokio1TcpStream::connect(server)
.await
.map_err(error::connection)?,
};
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters {
@@ -173,14 +155,45 @@ impl AsyncNetworkStream {
}
#[cfg(feature = "async-std1")]
pub async fn connect_asyncstd1(
hostname: &str,
port: u16,
pub async fn connect_asyncstd1<T: AsyncStd1ToSocketAddrs>(
server: T,
timeout: Option<Duration>,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncNetworkStream, Error> {
let tcp_stream = AsyncStd1TcpStream::connect((hostname, port))
.await
.map_err(error::connection)?;
async fn try_connect_timeout<T: AsyncStd1ToSocketAddrs>(
server: T,
timeout: Duration,
) -> Result<AsyncStd1TcpStream, Error> {
let addrs = server.to_socket_addrs().await.map_err(error::connection)?;
let mut last_err = None;
for addr in addrs {
let connect_future = AsyncStd1TcpStream::connect(&addr);
match async_std::future::timeout(timeout, connect_future).await {
Ok(Ok(stream)) => return Ok(stream),
Ok(Err(err)) => last_err = Some(err),
Err(_) => {
last_err = Some(io::Error::new(
io::ErrorKind::TimedOut,
"connection timed out",
))
}
}
}
Err(match last_err {
Some(last_err) => error::connection(last_err),
None => error::connection("could not resolve to any address"),
})
}
let tcp_stream = match timeout {
Some(t) => try_connect_timeout(server, t).await?,
None => AsyncStd1TcpStream::connect(server)
.await
.map_err(error::connection)?,
};
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters {
@@ -191,29 +204,6 @@ impl AsyncNetworkStream {
pub async fn upgrade_tls(&mut self, tls_parameters: TlsParameters) -> Result<(), Error> {
match &self.inner {
#[cfg(all(
feature = "tokio02",
not(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))
))]
InnerAsyncNetworkStream::Tokio02Tcp(_) => {
let _ = tls_parameters;
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio02-native-tls or the tokio02-rustls-tls feature");
}
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
InnerAsyncNetworkStream::Tokio02Tcp(_) => {
// get owned TcpStream
let tcp_stream = std::mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
let tcp_stream = match tcp_stream {
InnerAsyncNetworkStream::Tokio02Tcp(tcp_stream) => tcp_stream,
_ => unreachable!(),
};
self.inner = Self::upgrade_tokio02_tls(tcp_stream, tls_parameters)
.await
.map_err(error::connection)?;
Ok(())
}
#[cfg(all(
feature = "tokio1",
not(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))
@@ -226,7 +216,7 @@ impl AsyncNetworkStream {
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
// get owned TcpStream
let tcp_stream = std::mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
let tcp_stream = match tcp_stream {
InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) => tcp_stream,
_ => unreachable!(),
@@ -249,7 +239,7 @@ impl AsyncNetworkStream {
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
// get owned TcpStream
let tcp_stream = std::mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
let tcp_stream = match tcp_stream {
InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream) => tcp_stream,
_ => unreachable!(),
@@ -264,62 +254,13 @@ impl AsyncNetworkStream {
}
}
#[allow(unused_variables)]
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
async fn upgrade_tokio02_tls(
tcp_stream: Tokio02TcpStream,
mut tls_parameters: TlsParameters,
) -> Result<InnerAsyncNetworkStream, Error> {
let domain = std::mem::take(&mut tls_parameters.domain);
match tls_parameters.connector {
#[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls(connector) => {
#[cfg(not(feature = "tokio02-native-tls"))]
panic!("built without the tokio02-native-tls feature");
#[cfg(feature = "tokio02-native-tls")]
return {
use tokio02_native_tls_crate::TlsConnector;
let connector = TlsConnector::from(connector);
let stream = connector
.connect(&domain, tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::Tokio02NativeTls(stream))
};
}
#[cfg(feature = "rustls-tls")]
InnerTlsParameters::RustlsTls(config) => {
#[cfg(not(feature = "tokio02-rustls-tls"))]
panic!("built without the tokio02-rustls-tls feature");
#[cfg(feature = "tokio02-rustls-tls")]
return {
use tokio02_rustls::{webpki::DNSNameRef, TlsConnector};
let domain =
DNSNameRef::try_from_ascii_str(&domain).map_err(error::connection)?;
let connector = TlsConnector::from(Arc::new(config));
let stream = connector
.connect(domain, tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::Tokio02RustlsTls(stream))
};
}
}
}
#[allow(unused_variables)]
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
async fn upgrade_tokio1_tls(
tcp_stream: Tokio1TcpStream,
mut tls_parameters: TlsParameters,
tls_parameters: TlsParameters,
) -> Result<InnerAsyncNetworkStream, Error> {
let domain = std::mem::take(&mut tls_parameters.domain);
let domain = tls_parameters.domain().to_string();
match tls_parameters.connector {
#[cfg(feature = "native-tls")]
@@ -346,12 +287,13 @@ impl AsyncNetworkStream {
#[cfg(feature = "tokio1-rustls-tls")]
return {
use tokio1_rustls::{webpki::DNSNameRef, TlsConnector};
use rustls::ServerName;
use tokio1_rustls::TlsConnector;
let domain =
DNSNameRef::try_from_ascii_str(&domain).map_err(error::connection)?;
let domain = ServerName::try_from(domain.as_str())
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
let connector = TlsConnector::from(Arc::new(config));
let connector = TlsConnector::from(config);
let stream = connector
.connect(domain, tcp_stream)
.await
@@ -368,7 +310,7 @@ impl AsyncNetworkStream {
tcp_stream: AsyncStd1TcpStream,
mut tls_parameters: TlsParameters,
) -> Result<InnerAsyncNetworkStream, Error> {
let domain = std::mem::take(&mut tls_parameters.domain);
let domain = mem::take(&mut tls_parameters.domain);
match tls_parameters.connector {
#[cfg(feature = "native-tls")]
@@ -398,12 +340,13 @@ impl AsyncNetworkStream {
#[cfg(feature = "async-std1-rustls-tls")]
return {
use async_rustls::{webpki::DNSNameRef, TlsConnector};
use futures_rustls::TlsConnector;
use rustls::ServerName;
let domain =
DNSNameRef::try_from_ascii_str(&domain).map_err(error::connection)?;
let domain = ServerName::try_from(domain.as_str())
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
let connector = TlsConnector::from(Arc::new(config));
let connector = TlsConnector::from(config);
let stream = connector
.connect(domain, tcp_stream)
.await
@@ -416,12 +359,6 @@ impl AsyncNetworkStream {
pub fn is_encrypted(&self) -> bool {
match self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(_) => false,
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(_) => true,
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(_) => true,
#[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(_) => false,
#[cfg(feature = "tokio1-native-tls")]
@@ -437,6 +374,50 @@ impl AsyncNetworkStream {
InnerAsyncNetworkStream::None => false,
}
}
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
match &self.inner {
#[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
Err(error::client("Connection is not encrypted"))
}
#[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(stream) => Ok(stream
.get_ref()
.peer_certificate()
.map_err(error::tls)?
.unwrap()
.to_der()
.map_err(error::tls)?),
#[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(stream) => Ok(stream
.get_ref()
.1
.peer_certificates()
.unwrap()
.first()
.unwrap()
.clone()
.0),
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
Err(error::client("Connection is not encrypted"))
}
#[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(t) => panic!("Unsupported"),
#[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream) => Ok(stream
.get_ref()
.1
.peer_certificates()
.unwrap()
.first()
.unwrap()
.clone()
.0),
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
}
}
}
impl FuturesAsyncRead for AsyncNetworkStream {
@@ -446,12 +427,6 @@ impl FuturesAsyncRead for AsyncNetworkStream {
buf: &mut [u8],
) -> Poll<IoResult<usize>> {
match self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => {
let mut b = Tokio1ReadBuf::new(buf);
@@ -504,12 +479,6 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
buf: &[u8],
) -> Poll<IoResult<usize>> {
match self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio1-native-tls")]
@@ -535,12 +504,6 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
match self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio1-native-tls")]
@@ -562,12 +525,6 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
match self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio1-native-tls")]

View File

@@ -5,6 +5,8 @@ use std::{
time::Duration,
};
#[cfg(feature = "tracing")]
use super::escape_crlf;
use super::{ClientCodec, NetworkStream, TlsParameters};
use crate::{
address::Envelope,
@@ -18,9 +20,6 @@ use crate::{
},
};
#[cfg(feature = "tracing")]
use super::escape_crlf;
macro_rules! try_smtp (
($err: expr, $client: ident) => ({
match $err {
@@ -145,7 +144,7 @@ impl SmtpConnection {
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
{
try_smtp!(self.command(Starttls), self);
try_smtp!(self.stream.get_mut().upgrade_tls(tls_parameters), self);
self.stream.get_mut().upgrade_tls(tls_parameters)?;
#[cfg(feature = "tracing")]
tracing::debug!("connection encrypted");
// Send EHLO again
@@ -276,7 +275,10 @@ impl SmtpConnection {
return if response.is_positive() {
Ok(response)
} else {
Err(error::code(response.code))
Err(error::code(
response.code(),
response.first_line().map(|s| s.to_owned()),
))
};
}
Err(nom::Err::Failure(e)) => {
@@ -291,4 +293,10 @@ impl SmtpConnection {
Err(error::response("incomplete response"))
}
/// The X509 certificate of the server (DER encoded)
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
self.stream.get_ref().peer_certificate()
}
}

View File

@@ -1,122 +0,0 @@
#![allow(missing_docs)]
// Comes from https://github.com/inre/rust-mq/blob/master/netopt
use std::{
io::{self, Cursor, Read, Write},
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_all(&[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_all(&[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_all(&[8, 9, 10]).unwrap();
mock.swap();
mock.read_to_end(&mut vec).unwrap();
assert_eq!(vec, vec![8, 9, 10]);
}
}

View File

@@ -7,16 +7,14 @@
//!
//! # #[cfg(feature = "smtp-transport")]
//! # fn main() -> Result<(), Box<dyn Error>> {
//! use lettre::transport::smtp::{SMTP_PORT, extension::ClientId, commands::*, client::SmtpConnection};
//! use lettre::transport::smtp::{
//! client::SmtpConnection, commands::*, extension::ClientId, SMTP_PORT,
//! };
//!
//! let hello = ClientId::Domain("my_hostname".to_string());
//! let mut client = SmtpConnection::connect(&("localhost", SMTP_PORT), None, &hello, None)?;
//! client.command(
//! Mail::new(Some("user@example.com".parse()?), vec![])
//! )?;
//! client.command(
//! Rcpt::new("user@example.org".parse()?, vec![])
//! )?;
//! client.command(Mail::new(Some("user@example.com".parse()?), vec![]))?;
//! client.command(Rcpt::new("user@example.org".parse()?, vec![]))?;
//! client.command(Data)?;
//! client.message("Test email".as_bytes())?;
//! client.command(Quit)?;
@@ -27,25 +25,23 @@
#[cfg(feature = "serde")]
use std::fmt::Debug;
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
pub(crate) use self::async_connection::AsyncSmtpConnection;
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
pub(crate) use self::async_net::AsyncNetworkStream;
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub use self::async_connection::AsyncSmtpConnection;
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub use self::async_net::AsyncNetworkStream;
use self::net::NetworkStream;
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
pub(super) use self::tls::InnerTlsParameters;
pub use self::{
connection::SmtpConnection,
mock::MockStream,
tls::{Certificate, Tls, TlsParameters, TlsParametersBuilder},
};
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
mod async_connection;
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
mod async_net;
mod connection;
mod mock;
mod net;
mod tls;
@@ -80,7 +76,15 @@ impl ClientCodec {
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 },
2 => {
self.escape_count = if *byte == b'.' {
3
} else if *byte == b'\r' {
1
} else {
0
}
}
_ => unreachable!(),
}
if self.escape_count == 3 {
@@ -113,6 +117,7 @@ mod test {
let mut buf: Vec<u8> = vec![];
codec.encode(b"test\r\n", &mut buf);
codec.encode(b"test\r\n\r\n", &mut buf);
codec.encode(b".\r\n", &mut buf);
codec.encode(b"\r\ntest", &mut buf);
codec.encode(b"te\r\n.\r\nst", &mut buf);
@@ -123,7 +128,7 @@ mod test {
codec.encode(b"test", &mut buf);
assert_eq!(
String::from_utf8(buf).unwrap(),
"test\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntest"
"test\r\ntest\r\n\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntest"
);
}

View File

@@ -1,20 +1,18 @@
#[cfg(feature = "rustls-tls")]
use std::sync::Arc;
use std::{
io::{self, Read, Write},
mem,
net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs},
time::Duration,
};
#[cfg(feature = "native-tls")]
use native_tls::TlsStream;
#[cfg(feature = "rustls-tls")]
use rustls::{ClientSession, StreamOwned};
use rustls::{ClientConnection, ServerName, StreamOwned};
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
use super::InnerTlsParameters;
use super::{MockStream, TlsParameters};
use super::TlsParameters;
use crate::transport::smtp::{error, Error};
/// A network stream
@@ -34,18 +32,18 @@ enum InnerNetworkStream {
NativeTls(TlsStream<TcpStream>),
/// Encrypted TCP stream
#[cfg(feature = "rustls-tls")]
RustlsTls(StreamOwned<ClientSession, TcpStream>),
/// Mock stream
Mock(MockStream),
RustlsTls(StreamOwned<ClientConnection, TcpStream>),
/// Can't be built
None,
}
impl NetworkStream {
fn new(inner: InnerNetworkStream) -> Self {
NetworkStream { inner }
}
if let InnerNetworkStream::None = inner {
debug_assert!(false, "InnerNetworkStream::None must never be built");
}
pub fn new_mock(mock: MockStream) -> Self {
Self::new(InnerNetworkStream::Mock(mock))
NetworkStream { inner }
}
/// Returns peer's address
@@ -56,10 +54,13 @@ impl NetworkStream {
InnerNetworkStream::NativeTls(ref s) => s.get_ref().peer_addr(),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().peer_addr(),
InnerNetworkStream::Mock(_) => Ok(SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::new(127, 0, 0, 1),
80,
))),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::new(127, 0, 0, 1),
80,
)))
}
}
}
@@ -71,7 +72,10 @@ impl NetworkStream {
InnerNetworkStream::NativeTls(ref s) => s.get_ref().shutdown(how),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().shutdown(how),
InnerNetworkStream::Mock(_) => Ok(()),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(())
}
}
}
@@ -85,12 +89,20 @@ impl NetworkStream {
timeout: Duration,
) -> Result<TcpStream, Error> {
let addrs = server.to_socket_addrs().map_err(error::connection)?;
let mut last_err = None;
for addr in addrs {
if let Ok(result) = TcpStream::connect_timeout(&addr, timeout) {
return Ok(result);
match TcpStream::connect_timeout(&addr, timeout) {
Ok(stream) => return Ok(stream),
Err(err) => last_err = Some(err),
}
}
Err(error::connection("Could not connect"))
Err(match last_err {
Some(last_err) => error::connection(last_err),
None => error::connection("could not resolve to any address"),
})
}
let tcp_stream = match timeout {
@@ -116,8 +128,7 @@ impl NetworkStream {
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
InnerNetworkStream::Tcp(_) => {
// get owned TcpStream
let tcp_stream =
std::mem::replace(&mut self.inner, InnerNetworkStream::Mock(MockStream::new()));
let tcp_stream = mem::replace(&mut self.inner, InnerNetworkStream::None);
let tcp_stream = match tcp_stream {
InnerNetworkStream::Tcp(tcp_stream) => tcp_stream,
_ => unreachable!(),
@@ -145,15 +156,11 @@ impl NetworkStream {
}
#[cfg(feature = "rustls-tls")]
InnerTlsParameters::RustlsTls(connector) => {
use webpki::DNSNameRef;
let domain = DNSNameRef::try_from_ascii_str(tls_parameters.domain())
.map_err(error::connection)?;
let stream = StreamOwned::new(
ClientSession::new(&Arc::new(connector.clone()), domain),
tcp_stream,
);
let domain = ServerName::try_from(tls_parameters.domain())
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
let connection =
ClientConnection::new(connector.clone(), domain).map_err(error::connection)?;
let stream = StreamOwned::new(connection, tcp_stream);
InnerNetworkStream::RustlsTls(stream)
}
})
@@ -161,11 +168,39 @@ impl NetworkStream {
pub fn is_encrypted(&self) -> bool {
match self.inner {
InnerNetworkStream::Tcp(_) | InnerNetworkStream::Mock(_) => false,
InnerNetworkStream::Tcp(_) => false,
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(_) => true,
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(_) => true,
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
false
}
}
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
match &self.inner {
InnerNetworkStream::Tcp(_) => Err(error::client("Connection is not encrypted")),
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(stream) => Ok(stream
.peer_certificate()
.map_err(error::tls)?
.unwrap()
.to_der()
.map_err(error::tls)?),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(stream) => Ok(stream
.conn
.peer_certificates()
.unwrap()
.first()
.unwrap()
.clone()
.0),
InnerNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
}
}
@@ -180,7 +215,10 @@ impl NetworkStream {
InnerNetworkStream::RustlsTls(ref mut stream) => {
stream.get_ref().set_read_timeout(duration)
}
InnerNetworkStream::Mock(_) => Ok(()),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(())
}
}
}
@@ -198,7 +236,10 @@ impl NetworkStream {
stream.get_ref().set_write_timeout(duration)
}
InnerNetworkStream::Mock(_) => Ok(()),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(())
}
}
}
}
@@ -211,7 +252,10 @@ impl Read for NetworkStream {
InnerNetworkStream::NativeTls(ref mut s) => s.read(buf),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.read(buf),
InnerNetworkStream::Mock(ref mut s) => s.read(buf),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(0)
}
}
}
}
@@ -224,7 +268,10 @@ impl Write for NetworkStream {
InnerNetworkStream::NativeTls(ref mut s) => s.write(buf),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.write(buf),
InnerNetworkStream::Mock(ref mut s) => s.write(buf),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(0)
}
}
}
@@ -235,7 +282,10 @@ impl Write for NetworkStream {
InnerNetworkStream::NativeTls(ref mut s) => s.flush(),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.flush(),
InnerNetworkStream::Mock(ref mut s) => s.flush(),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(())
}
}
}
}

View File

@@ -1,14 +1,17 @@
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
use crate::transport::smtp::{error, Error};
use std::fmt::{self, Debug};
#[cfg(feature = "rustls-tls")]
use std::{sync::Arc, time::SystemTime};
#[cfg(feature = "native-tls")]
use native_tls::{Protocol, TlsConnector};
#[cfg(feature = "rustls-tls")]
use rustls::{ClientConfig, RootCertStore, ServerCertVerified, ServerCertVerifier, TLSError};
use std::fmt::{self, Debug};
#[cfg(feature = "rustls-tls")]
use std::sync::Arc;
#[cfg(feature = "rustls-tls")]
use webpki::DNSNameRef;
use rustls::{
client::{ServerCertVerified, ServerCertVerifier, WebPkiVerifier},
ClientConfig, Error as TlsError, OwnedTrustAnchor, RootCertStore, ServerName,
};
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
use crate::transport::smtp::{error, Error};
/// Accepted protocols by default.
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults.
@@ -24,12 +27,15 @@ pub enum Tls {
None,
/// Start with insecure connection and use `STARTTLS` when available
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
Opportunistic(TlsParameters),
/// Start with insecure connection and require `STARTTLS`
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
Required(TlsParameters),
/// Use TLS wrapped connection
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
Wrapper(TlsParameters),
}
@@ -128,18 +134,12 @@ impl TlsParametersBuilder {
/// depending on which one is available
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
// TODO: remove below line once native-tls is supported with async-std
#[allow(unreachable_code)]
pub fn build(self) -> Result<TlsParameters, Error> {
// TODO: remove once native-tls is supported with async-std
#[cfg(all(feature = "rustls-tls", feature = "async-std1"))]
#[cfg(feature = "rustls-tls")]
return self.build_rustls();
#[cfg(feature = "native-tls")]
#[cfg(not(feature = "rustls-tls"))]
return self.build_native();
#[cfg(not(feature = "native-tls"))]
return self.build_rustls();
}
/// Creates a new `TlsParameters` using native-tls with the provided configuration
@@ -166,23 +166,37 @@ impl TlsParametersBuilder {
#[cfg(feature = "rustls-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
pub fn build_rustls(self) -> Result<TlsParameters, Error> {
use webpki_roots::TLS_SERVER_ROOTS;
let tls = ClientConfig::builder();
let tls = tls.with_safe_defaults();
let mut tls = ClientConfig::new();
for cert in self.root_certs {
for rustls_cert in cert.rustls {
tls.root_store.add(&rustls_cert).map_err(error::tls)?;
let tls = if self.accept_invalid_certs {
tls.with_custom_certificate_verifier(Arc::new(InvalidCertsVerifier {}))
} else {
let mut root_cert_store = RootCertStore::empty();
for cert in self.root_certs {
for rustls_cert in cert.rustls {
root_cert_store.add(&rustls_cert).map_err(error::tls)?;
}
}
}
if self.accept_invalid_certs {
tls.dangerous()
.set_certificate_verifier(Arc::new(InvalidCertsVerifier {}));
}
root_cert_store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(
|ta| {
OwnedTrustAnchor::from_subject_spki_name_constraints(
ta.subject,
ta.spki,
ta.name_constraints,
)
},
));
tls.with_custom_certificate_verifier(Arc::new(WebPkiVerifier::new(
root_cert_store,
None,
)))
};
let tls = tls.with_no_client_auth();
tls.root_store.add_server_trust_anchors(&TLS_SERVER_ROOTS);
Ok(TlsParameters {
connector: InnerTlsParameters::RustlsTls(tls),
connector: InnerTlsParameters::RustlsTls(Arc::new(tls)),
domain: self.domain,
})
}
@@ -193,7 +207,7 @@ pub enum InnerTlsParameters {
#[cfg(feature = "native-tls")]
NativeTls(TlsConnector),
#[cfg(feature = "rustls-tls")]
RustlsTls(ClientConfig),
RustlsTls(Arc<ClientConfig>),
}
impl TlsParameters {
@@ -260,11 +274,14 @@ impl Certificate {
#[cfg(feature = "rustls-tls")]
let rustls_cert = {
use rustls::internal::pemfile;
use std::io::Cursor;
let mut pem = Cursor::new(pem);
pemfile::certs(&mut pem).map_err(|_| error::tls("invalid certificates"))?
rustls_pemfile::certs(&mut pem)
.map_err(|_| error::tls("invalid certificates"))?
.into_iter()
.map(rustls::Certificate)
.collect::<Vec<_>>()
};
Ok(Self {
@@ -289,11 +306,13 @@ struct InvalidCertsVerifier;
impl ServerCertVerifier for InvalidCertsVerifier {
fn verify_server_cert(
&self,
_roots: &RootCertStore,
_presented_certs: &[rustls::Certificate],
_dns_name: DNSNameRef<'_>,
_end_entity: &rustls::Certificate,
_intermediates: &[rustls::Certificate],
_server_name: &ServerName,
_scts: &mut dyn Iterator<Item = &[u8]>,
_ocsp_response: &[u8],
) -> Result<ServerCertVerified, TLSError> {
_now: SystemTime,
) -> Result<ServerCertVerified, TlsError> {
Ok(ServerCertVerified::assertion())
}
}

View File

@@ -1,5 +1,7 @@
//! SMTP commands
use std::fmt::{self, Display, Formatter};
use crate::{
address::Address,
transport::smtp::{
@@ -9,7 +11,6 @@ use crate::{
response::Response,
},
};
use std::fmt::{self, Display, Formatter};
/// EHLO command
#[derive(PartialEq, Clone, Debug)]
@@ -55,7 +56,7 @@ impl Display for Mail {
write!(
f,
"MAIL FROM:<{}>",
self.sender.as_ref().map(|s| s.as_ref()).unwrap_or("")
self.sender.as_ref().map_or("", |s| s.as_ref())
)?;
for parameter in &self.parameters {
write!(f, " {}", parameter)?;
@@ -288,9 +289,10 @@ impl Auth {
#[cfg(test)]
mod test {
use std::str::FromStr;
use super::*;
use crate::transport::smtp::extension::MailBodyParameter;
use std::str::FromStr;
#[test]
fn test_display() {

View File

@@ -1,10 +1,11 @@
//! Error and result type for SMTP clients
use std::{error::Error as StdError, fmt};
use crate::{
transport::smtp::response::{Code, Severity},
BoxError,
};
use std::{error::Error as StdError, fmt};
// Inspired by https://github.com/seanmonstar/reqwest/blob/a8566383168c0ef06c21f38cbc9213af6ff6db31/src/error.rs
@@ -76,8 +77,7 @@ impl Error {
/// Returns the status code, if the error was generated from a response.
pub fn status(&self) -> Option<Code> {
match self.inner.kind {
Kind::Transient(code) => Some(code),
Kind::Permanent(code) => Some(code),
Kind::Transient(code) | Kind::Permanent(code) => Some(code),
_ => None,
}
}
@@ -155,10 +155,10 @@ impl StdError for Error {
}
}
pub(crate) fn code(c: Code) -> Error {
pub(crate) fn code(c: Code, s: Option<String>) -> Error {
match c.severity {
Severity::TransientNegativeCompletion => Error::new::<Error>(Kind::Transient(c), None),
Severity::PermanentNegativeCompletion => Error::new::<Error>(Kind::Permanent(c), None),
Severity::TransientNegativeCompletion => Error::new(Kind::Transient(c), s),
Severity::PermanentNegativeCompletion => Error::new(Kind::Permanent(c), s),
_ => client("Unknown error code"),
}
}

View File

@@ -1,17 +1,18 @@
//! ESMTP features
use std::{
collections::HashSet,
fmt::{self, Display, Formatter},
net::{Ipv4Addr, Ipv6Addr},
result::Result,
};
use crate::transport::smtp::{
authentication::Mechanism,
error::{self, Error},
response::Response,
util::XText,
};
use std::{
collections::HashSet,
fmt::{self, Display, Formatter},
net::{Ipv4Addr, Ipv6Addr},
result::Result,
};
/// Client identifier, the parameter to `EHLO`
#[derive(PartialEq, Eq, Clone, Debug)]
@@ -72,6 +73,7 @@ impl ClientId {
/// Supported ESMTP keywords
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Extension {
/// 8BITMIME keyword
///
@@ -107,11 +109,11 @@ pub struct ServerInfo {
/// Server name
///
/// The name given in the server banner
pub name: String,
name: String,
/// ESMTP features supported by the server
///
/// It contains the features supported by the server and known by the `Extension` module.
pub features: HashSet<Extension>,
features: HashSet<Extension>,
}
impl Display for ServerInfo {
@@ -135,7 +137,7 @@ impl ServerInfo {
let mut features: HashSet<Extension> = HashSet::new();
for line in response.message.as_slice() {
for line in response.message() {
if line.is_empty() {
continue;
}
@@ -197,6 +199,11 @@ impl ServerInfo {
}
None
}
/// The name given in the server banner
pub fn name(&self) -> &str {
self.name.as_ref()
}
}
/// A `MAIL FROM` extension parameter
@@ -286,12 +293,13 @@ impl Display for RcptParameter {
#[cfg(test)]
mod test {
use std::collections::HashSet;
use super::*;
use crate::transport::smtp::{
authentication::Mechanism,
response::{Category, Code, Detail, Response, Severity},
};
use std::collections::HashSet;
#[test]
fn test_clientid_fmt() {

View File

@@ -33,7 +33,7 @@
//! ```rust,no_run
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
//! use lettre::{Message, Transport, SmtpTransport};
//! use lettre::{Message, SmtpTransport, Transport};
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
@@ -43,8 +43,7 @@
//! .body(String::from("Be happy!"))?;
//!
//! // Create TLS transport on port 465
//! let sender = SmtpTransport::relay("smtp.example.com")?
//! .build();
//! let sender = SmtpTransport::relay("smtp.example.com")?.build();
//! // Send the email via remote relay
//! let result = sender.send(&email);
//! assert!(result.is_ok());
@@ -59,7 +58,13 @@
//! ```rust,no_run
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
//! use lettre::{Message, Transport, SmtpTransport, transport::smtp::{PoolConfig, authentication::{Credentials, Mechanism}}};
//! use lettre::{
//! transport::smtp::{
//! authentication::{Credentials, Mechanism},
//! PoolConfig,
//! },
//! Message, SmtpTransport, Transport,
//! };
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
@@ -71,11 +76,14 @@
//! // Create TLS transport on port 587 with STARTTLS
//! let sender = SmtpTransport::starttls_relay("smtp.example.com")?
//! // Add credentials for authentication
//! .credentials(Credentials::new("username".to_string(), "password".to_string()))
//! .credentials(Credentials::new(
//! "username".to_string(),
//! "password".to_string(),
//! ))
//! // Configure expected authentication mechanism
//! .authentication(vec![Mechanism::Plain])
//! // Connection pool settings
//! .pool_config( PoolConfig::new().max_size(20))
//! .pool_config(PoolConfig::new().max_size(20))
//! .build();
//!
//! // Send the email via remote relay
@@ -90,7 +98,10 @@
//! ```rust,no_run
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
//! use lettre::{Message, Transport, SmtpTransport, transport::smtp::client::{TlsParameters, Tls}};
//! use lettre::{
//! transport::smtp::client::{Tls, TlsParameters},
//! Message, SmtpTransport, Transport,
//! };
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
@@ -101,7 +112,8 @@
//!
//! // Custom TLS configuration
//! let tls = TlsParameters::builder("smtp.example.com".to_string())
//! .dangerous_accept_invalid_certs(true).build()?;
//! .dangerous_accept_invalid_certs(true)
//! .build()?;
//!
//! // Create TLS transport on port 465
//! let sender = SmtpTransport::relay("smtp.example.com")?
@@ -116,12 +128,14 @@
//! # }
//! ```
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
use std::time::Duration;
use client::Tls;
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub use self::async_transport::{AsyncSmtpTransport, AsyncSmtpTransportBuilder};
#[cfg(feature = "r2d2")]
#[cfg(feature = "pool")]
pub use self::pool::PoolConfig;
#[cfg(feature = "r2d2")]
pub(crate) use self::transport::SmtpClient;
pub use self::{
error::Error,
transport::{SmtpTransport, SmtpTransportBuilder},
@@ -134,21 +148,19 @@ use crate::transport::smtp::{
extension::ClientId,
response::Response,
};
use client::Tls;
use std::time::Duration;
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
mod async_transport;
pub mod authentication;
pub mod client;
pub mod commands;
mod error;
pub mod extension;
#[cfg(feature = "r2d2")]
#[cfg(feature = "pool")]
mod pool;
pub mod response;
mod transport;
pub mod util;
pub(super) mod util;
// Registered port numbers:
// https://www.iana.
@@ -164,7 +176,7 @@ pub const SUBMISSION_PORT: u16 = 587;
pub const SUBMISSIONS_PORT: u16 = 465;
/// Default timeout
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
#[derive(Debug, Clone)]
struct SmtpInfo {

View File

@@ -1,111 +0,0 @@
use std::time::Duration;
use crate::transport::smtp::{client::SmtpConnection, error, error::Error, SmtpClient};
use r2d2::{CustomizeConnection, ManageConnection, Pool};
/// Configuration for a connection pool
#[derive(Debug, Clone)]
#[allow(missing_copy_implementations)]
#[cfg_attr(docsrs, doc(cfg(feature = "r2d2")))]
pub struct PoolConfig {
min_idle: u32,
max_size: u32,
connection_timeout: Duration,
idle_timeout: Duration,
}
impl PoolConfig {
/// Create a new pool configuration with default values
pub fn new() -> Self {
Self::default()
}
/// Minimum number of idle connections
///
/// Defaults to `0`
pub fn min_idle(mut self, min_idle: u32) -> Self {
self.min_idle = min_idle;
self
}
/// Maximum number of pooled connections
///
/// Defaults to `10`
pub fn max_size(mut self, max_size: u32) -> Self {
self.max_size = max_size;
self
}
/// Connection timeout
///
/// Defaults to `30 seconds`
pub fn connection_timeout(mut self, connection_timeout: Duration) -> Self {
self.connection_timeout = connection_timeout;
self
}
/// Connection idle timeout
///
/// Defaults to `60 seconds`
pub fn idle_timeout(mut self, idle_timeout: Duration) -> Self {
self.idle_timeout = idle_timeout;
self
}
pub(crate) fn build<C: ManageConnection<Connection = SmtpConnection, Error = Error>>(
&self,
client: C,
) -> Pool<C> {
Pool::builder()
.min_idle(Some(self.min_idle))
.max_size(self.max_size)
.connection_timeout(self.connection_timeout)
.idle_timeout(Some(self.idle_timeout))
.connection_customizer(Box::new(SmtpConnectionQuitter))
.build_unchecked(client)
}
}
impl Default for PoolConfig {
fn default() -> Self {
Self {
min_idle: 0,
max_size: 10,
connection_timeout: Duration::from_secs(30),
idle_timeout: Duration::from_secs(60),
}
}
}
impl ManageConnection for SmtpClient {
type Connection = SmtpConnection;
type Error = Error;
fn connect(&self) -> Result<Self::Connection, Error> {
self.connection()
}
fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Error> {
if conn.test_connected() {
return Ok(());
}
Err(error::network("is not connected anymore"))
}
fn has_broken(&self, conn: &mut Self::Connection) -> bool {
conn.has_broken()
}
}
#[derive(Copy, Clone, Debug)]
struct SmtpConnectionQuitter;
impl CustomizeConnection<SmtpConnection, Error> for SmtpConnectionQuitter {
fn on_release(&self, conn: SmtpConnection) {
let mut conn = conn;
if !conn.has_broken() {
let _quit = conn.quit();
}
}
}

View File

@@ -0,0 +1,301 @@
use std::{
fmt::{self, Debug},
mem,
ops::{Deref, DerefMut},
sync::Arc,
time::{Duration, Instant},
};
use futures_util::{
lock::Mutex,
stream::{self, StreamExt},
};
use once_cell::sync::OnceCell;
use super::{
super::{client::AsyncSmtpConnection, Error},
PoolConfig,
};
use crate::{executor::SpawnHandle, transport::smtp::async_transport::AsyncSmtpClient, Executor};
pub struct Pool<E: Executor> {
config: PoolConfig,
connections: Mutex<Vec<ParkedConnection>>,
client: AsyncSmtpClient<E>,
handle: OnceCell<E::Handle>,
}
struct ParkedConnection {
conn: AsyncSmtpConnection,
since: Instant,
}
pub struct PooledConnection<E: Executor> {
conn: Option<AsyncSmtpConnection>,
pool: Arc<Pool<E>>,
}
impl<E: Executor> Pool<E> {
pub fn new(config: PoolConfig, client: AsyncSmtpClient<E>) -> Arc<Self> {
let pool = Arc::new(Self {
config,
connections: Mutex::new(Vec::new()),
client,
handle: OnceCell::new(),
});
{
let pool_ = Arc::clone(&pool);
let min_idle = pool_.config.min_idle;
let idle_timeout = pool_.config.idle_timeout;
let pool = Arc::downgrade(&pool_);
let handle = E::spawn(async move {
loop {
#[cfg(feature = "tracing")]
tracing::trace!("running cleanup tasks");
match pool.upgrade() {
Some(pool) => {
#[allow(clippy::needless_collect)]
let (count, dropped) = {
let mut connections = pool.connections.lock().await;
let to_drop = connections
.iter()
.enumerate()
.rev()
.filter(|(_, conn)| conn.idle_duration() > idle_timeout)
.map(|(i, _)| i)
.collect::<Vec<_>>();
let dropped = to_drop
.into_iter()
.map(|i| connections.remove(i))
.collect::<Vec<_>>();
(connections.len(), dropped)
};
#[cfg(feature = "tracing")]
let mut created = 0;
for _ in count..=(min_idle as usize) {
let conn = match pool.client.connection().await {
Ok(conn) => conn,
Err(err) => {
#[cfg(feature = "tracing")]
tracing::warn!("couldn't create idle connection {}", err);
#[cfg(not(feature = "tracing"))]
let _ = err;
break;
}
};
let mut connections = pool.connections.lock().await;
connections.push(ParkedConnection::park(conn));
#[cfg(feature = "tracing")]
{
created += 1;
}
}
#[cfg(feature = "tracing")]
if created > 0 {
tracing::debug!("created {} idle connections", created);
}
if !dropped.is_empty() {
#[cfg(feature = "tracing")]
tracing::debug!("dropped {} idle connections", dropped.len());
abort_concurrent(dropped.into_iter().map(|conn| conn.unpark()))
.await;
}
}
None => {
#[cfg(feature = "tracing")]
tracing::warn!(
"breaking out of task - no more references to Pool are available"
);
break;
}
}
E::sleep(idle_timeout).await;
}
});
pool_
.handle
.set(handle)
.expect("handle hasn't been set yet");
}
pool
}
pub async fn connection(self: &Arc<Self>) -> Result<PooledConnection<E>, Error> {
loop {
let conn = {
let mut connections = self.connections.lock().await;
connections.pop()
};
match conn {
Some(conn) => {
let mut conn = conn.unpark();
// TODO: handle the client try another connection if this one isn't good
if !conn.test_connected().await {
#[cfg(feature = "tracing")]
tracing::debug!("dropping a broken connection");
conn.abort().await;
continue;
}
#[cfg(feature = "tracing")]
tracing::debug!("reusing a pooled connection");
return Ok(PooledConnection::wrap(conn, self.clone()));
}
None => {
#[cfg(feature = "tracing")]
tracing::debug!("creating a new connection");
let conn = self.client.connection().await?;
return Ok(PooledConnection::wrap(conn, self.clone()));
}
}
}
}
async fn recycle(&self, mut conn: AsyncSmtpConnection) {
if conn.has_broken() {
#[cfg(feature = "tracing")]
tracing::debug!("dropping a broken connection instead of recycling it");
conn.abort().await;
drop(conn);
} else {
#[cfg(feature = "tracing")]
tracing::debug!("recycling connection");
let mut connections = self.connections.lock().await;
if connections.len() >= self.config.max_size as usize {
drop(connections);
conn.abort().await;
} else {
let conn = ParkedConnection::park(conn);
connections.push(conn);
}
}
}
}
impl<E: Executor> Debug for Pool<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Pool")
.field("config", &self.config)
.field(
"connections",
&match self.connections.try_lock() {
Some(connections) => format!("{} connections", connections.len()),
None => "LOCKED".to_string(),
},
)
.field("client", &self.client)
.field(
"handle",
&match self.handle.get() {
Some(_) => "Some(JoinHandle)",
None => "None",
},
)
.finish()
}
}
impl<E: Executor> Drop for Pool<E> {
fn drop(&mut self) {
#[cfg(feature = "tracing")]
tracing::debug!("dropping Pool");
let connections = mem::take(self.connections.get_mut());
let handle = self.handle.take();
E::spawn(async move {
if let Some(handle) = handle {
handle.shutdown().await;
}
abort_concurrent(connections.into_iter().map(|conn| conn.unpark())).await;
});
}
}
impl ParkedConnection {
fn park(conn: AsyncSmtpConnection) -> Self {
Self {
conn,
since: Instant::now(),
}
}
fn idle_duration(&self) -> Duration {
self.since.elapsed()
}
fn unpark(self) -> AsyncSmtpConnection {
self.conn
}
}
impl<E: Executor> PooledConnection<E> {
fn wrap(conn: AsyncSmtpConnection, pool: Arc<Pool<E>>) -> Self {
Self {
conn: Some(conn),
pool,
}
}
}
impl<E: Executor> Deref for PooledConnection<E> {
type Target = AsyncSmtpConnection;
fn deref(&self) -> &Self::Target {
self.conn.as_ref().expect("conn hasn't been dropped yet")
}
}
impl<E: Executor> DerefMut for PooledConnection<E> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.conn.as_mut().expect("conn hasn't been dropped yet")
}
}
impl<E: Executor> Drop for PooledConnection<E> {
fn drop(&mut self) {
let conn = self
.conn
.take()
.expect("AsyncSmtpConnection hasn't been taken yet");
let pool = Arc::clone(&self.pool);
E::spawn(async move {
pool.recycle(conn).await;
});
}
}
async fn abort_concurrent<I>(iter: I)
where
I: Iterator<Item = AsyncSmtpConnection>,
{
stream::iter(iter)
.for_each_concurrent(8, |mut conn| async move {
conn.abort().await;
})
.await;
}

View File

@@ -0,0 +1,66 @@
use std::time::Duration;
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub mod async_impl;
pub mod sync_impl;
/// Configuration for a connection pool
#[derive(Debug, Clone)]
#[allow(missing_copy_implementations)]
#[cfg_attr(docsrs, doc(cfg(feature = "pool")))]
pub struct PoolConfig {
min_idle: u32,
max_size: u32,
idle_timeout: Duration,
}
impl PoolConfig {
/// Create a new pool configuration with default values
pub fn new() -> Self {
Self::default()
}
/// Minimum number of idle connections
///
/// Defaults to `0`
pub fn min_idle(mut self, min_idle: u32) -> Self {
self.min_idle = min_idle;
self
}
/// Maximum number of pooled connections
///
/// Defaults to `10`
pub fn max_size(mut self, max_size: u32) -> Self {
self.max_size = max_size;
self
}
/// Connection timeout
///
/// Defaults to `30 seconds`
#[doc(hidden)]
#[deprecated(note = "The Connection timeout is already configured on the SMTP transport")]
pub fn connection_timeout(self, connection_timeout: Duration) -> Self {
let _ = connection_timeout;
self
}
/// Connection idle timeout
///
/// Defaults to `60 seconds`
pub fn idle_timeout(mut self, idle_timeout: Duration) -> Self {
self.idle_timeout = idle_timeout;
self
}
}
impl Default for PoolConfig {
fn default() -> Self {
Self {
min_idle: 0,
max_size: 10,
idle_timeout: Duration::from_secs(60),
}
}
}

View File

@@ -0,0 +1,259 @@
use std::{
fmt::{self, Debug},
mem,
ops::{Deref, DerefMut},
sync::{Arc, Mutex, TryLockError},
thread,
time::{Duration, Instant},
};
use super::{
super::{client::SmtpConnection, Error},
PoolConfig,
};
use crate::transport::smtp::transport::SmtpClient;
pub struct Pool {
config: PoolConfig,
connections: Mutex<Vec<ParkedConnection>>,
client: SmtpClient,
}
struct ParkedConnection {
conn: SmtpConnection,
since: Instant,
}
pub struct PooledConnection {
conn: Option<SmtpConnection>,
pool: Arc<Pool>,
}
impl Pool {
pub fn new(config: PoolConfig, client: SmtpClient) -> Arc<Self> {
let pool = Arc::new(Self {
config,
connections: Mutex::new(Vec::new()),
client,
});
{
let pool_ = Arc::clone(&pool);
let min_idle = pool_.config.min_idle;
let idle_timeout = pool_.config.idle_timeout;
let pool = Arc::downgrade(&pool_);
thread::Builder::new()
.name("lettre-connection-pool".into())
.spawn(move || {
while let Some(pool) = pool.upgrade() {
#[cfg(feature = "tracing")]
tracing::trace!("running cleanup tasks");
#[allow(clippy::needless_collect)]
let (count, dropped) = {
let mut connections = pool.connections.lock().unwrap();
let to_drop = connections
.iter()
.enumerate()
.rev()
.filter(|(_, conn)| conn.idle_duration() > idle_timeout)
.map(|(i, _)| i)
.collect::<Vec<_>>();
let dropped = to_drop
.into_iter()
.map(|i| connections.remove(i))
.collect::<Vec<_>>();
(connections.len(), dropped)
};
#[cfg(feature = "tracing")]
let mut created = 0;
for _ in count..=(min_idle as usize) {
let conn = match pool.client.connection() {
Ok(conn) => conn,
Err(err) => {
#[cfg(feature = "tracing")]
tracing::warn!("couldn't create idle connection {}", err);
#[cfg(not(feature = "tracing"))]
let _ = err;
break;
}
};
let mut connections = pool.connections.lock().unwrap();
connections.push(ParkedConnection::park(conn));
#[cfg(feature = "tracing")]
{
created += 1;
}
}
#[cfg(feature = "tracing")]
if created > 0 {
tracing::debug!("created {} idle connections", created);
}
if !dropped.is_empty() {
#[cfg(feature = "tracing")]
tracing::debug!("dropped {} idle connections", dropped.len());
for conn in dropped {
let mut conn = conn.unpark();
conn.abort();
}
}
thread::sleep(idle_timeout);
}
})
.expect("couldn't spawn the Pool thread");
}
pool
}
pub fn connection(self: &Arc<Self>) -> Result<PooledConnection, Error> {
loop {
let conn = {
let mut connections = self.connections.lock().unwrap();
connections.pop()
};
match conn {
Some(conn) => {
let mut conn = conn.unpark();
// TODO: handle the client try another connection if this one isn't good
if !conn.test_connected() {
#[cfg(feature = "tracing")]
tracing::debug!("dropping a broken connection");
conn.abort();
continue;
}
#[cfg(feature = "tracing")]
tracing::debug!("reusing a pooled connection");
return Ok(PooledConnection::wrap(conn, self.clone()));
}
None => {
#[cfg(feature = "tracing")]
tracing::debug!("creating a new connection");
let conn = self.client.connection()?;
return Ok(PooledConnection::wrap(conn, self.clone()));
}
}
}
}
fn recycle(&self, mut conn: SmtpConnection) {
if conn.has_broken() {
#[cfg(feature = "tracing")]
tracing::debug!("dropping a broken connection instead of recycling it");
conn.abort();
drop(conn);
} else {
#[cfg(feature = "tracing")]
tracing::debug!("recycling connection");
let mut connections = self.connections.lock().unwrap();
if connections.len() >= self.config.max_size as usize {
drop(connections);
conn.abort();
} else {
let conn = ParkedConnection::park(conn);
connections.push(conn);
}
}
}
}
impl Debug for Pool {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Pool")
.field("config", &self.config)
.field(
"connections",
&match self.connections.try_lock() {
Ok(connections) => format!("{} connections", connections.len()),
Err(TryLockError::WouldBlock) => "LOCKED".to_string(),
Err(TryLockError::Poisoned(_)) => "POISONED".to_string(),
},
)
.field("client", &self.client)
.finish()
}
}
impl Drop for Pool {
fn drop(&mut self) {
#[cfg(feature = "tracing")]
tracing::debug!("dropping Pool");
let connections = mem::take(&mut *self.connections.get_mut().unwrap());
for conn in connections {
let mut conn = conn.unpark();
conn.abort();
}
}
}
impl ParkedConnection {
fn park(conn: SmtpConnection) -> Self {
Self {
conn,
since: Instant::now(),
}
}
fn idle_duration(&self) -> Duration {
self.since.elapsed()
}
fn unpark(self) -> SmtpConnection {
self.conn
}
}
impl PooledConnection {
fn wrap(conn: SmtpConnection, pool: Arc<Pool>) -> Self {
Self {
conn: Some(conn),
pool,
}
}
}
impl Deref for PooledConnection {
type Target = SmtpConnection;
fn deref(&self) -> &Self::Target {
self.conn.as_ref().expect("conn hasn't been dropped yet")
}
}
impl DerefMut for PooledConnection {
fn deref_mut(&mut self) -> &mut Self::Target {
self.conn.as_mut().expect("conn hasn't been dropped yet")
}
}
impl Drop for PooledConnection {
fn drop(&mut self) {
let conn = self
.conn
.take()
.expect("SmtpConnection hasn't been taken yet");
self.pool.recycle(conn);
}
}

View File

@@ -1,7 +1,13 @@
//! SMTP response, containing a mandatory return code and an optional text
//! message
use crate::transport::smtp::{error, Error};
use std::{
fmt::{Display, Formatter, Result},
result,
str::FromStr,
string::ToString,
};
use nom::{
branch::alt,
bytes::streaming::{tag, take_until},
@@ -10,12 +16,8 @@ use nom::{
sequence::{preceded, tuple},
IResult,
};
use std::{
fmt::{Display, Formatter, Result},
result,
str::FromStr,
string::ToString,
};
use crate::transport::smtp::{error, Error};
/// First digit indicates severity
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
@@ -122,7 +124,7 @@ impl Code {
}
/// Tells if the response is positive
pub fn is_positive(&self) -> bool {
pub fn is_positive(self) -> bool {
matches!(
self.severity,
Severity::PositiveCompletion | Severity::PositiveIntermediate
@@ -137,10 +139,10 @@ impl Code {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Response {
/// Response code
pub code: Code,
code: Code,
/// Server response string (optional)
/// Handle multiline responses
pub message: Vec<String>,
message: Vec<String>,
}
impl FromStr for Response {
@@ -180,6 +182,16 @@ 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)

View File

@@ -1,22 +1,23 @@
#[cfg(feature = "pool")]
use std::sync::Arc;
use std::time::Duration;
#[cfg(feature = "r2d2")]
use r2d2::Pool;
#[cfg(feature = "r2d2")]
#[cfg(feature = "pool")]
use super::pool::sync_impl::Pool;
#[cfg(feature = "pool")]
use super::PoolConfig;
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
use super::{error, Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, SmtpInfo};
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
use super::{Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
use crate::{address::Envelope, Transport};
/// Sends emails using the SMTP protocol
#[cfg_attr(docsrs, doc(cfg(feature = "smtp-transport")))]
#[derive(Clone)]
pub struct SmtpTransport {
#[cfg(feature = "r2d2")]
inner: Pool<SmtpClient>,
#[cfg(not(feature = "r2d2"))]
#[cfg(feature = "pool")]
inner: Arc<Pool>,
#[cfg(not(feature = "pool"))]
inner: SmtpClient,
}
@@ -26,14 +27,11 @@ impl Transport for SmtpTransport {
/// Sends an email
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
#[cfg(feature = "r2d2")]
let mut conn = self.inner.get().map_err(error::client)?;
#[cfg(not(feature = "r2d2"))]
let mut conn = self.inner.connection()?;
let result = conn.send(envelope, email)?;
#[cfg(not(feature = "r2d2"))]
#[cfg(not(feature = "pool"))]
conn.quit()?;
Ok(result)
@@ -48,6 +46,7 @@ impl SmtpTransport {
/// Creates an encrypted transport over submissions port, using the provided domain
/// to validate TLS certificates.
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
pub fn relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
let tls_parameters = TlsParameters::new(relay.into())?;
@@ -68,6 +67,7 @@ impl SmtpTransport {
/// An error is returned if the connection can't be upgraded. No credentials
/// or emails will be sent to the server, protecting from downgrade attacks.
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
pub fn starttls_relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
let tls_parameters = TlsParameters::new(relay.into())?;
@@ -103,10 +103,25 @@ impl SmtpTransport {
SmtpTransportBuilder {
info: new,
#[cfg(feature = "r2d2")]
#[cfg(feature = "pool")]
pool_config: PoolConfig::default(),
}
}
/// Tests the SMTP connection
///
/// `test_connection()` tests the connection by using the SMTP NOOP command.
/// The connection is closed afterwards if a connection pool is not used.
pub fn test_connection(&self) -> Result<bool, Error> {
let mut conn = self.inner.connection()?;
let is_connected = conn.test_connected();
#[cfg(not(feature = "pool"))]
conn.quit()?;
Ok(is_connected)
}
}
/// Contains client configuration.
@@ -114,7 +129,7 @@ impl SmtpTransport {
#[derive(Debug, Clone)]
pub struct SmtpTransportBuilder {
info: SmtpInfo,
#[cfg(feature = "r2d2")]
#[cfg(feature = "pool")]
pool_config: PoolConfig,
}
@@ -152,6 +167,7 @@ impl SmtpTransportBuilder {
/// Set the TLS settings to use
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
pub fn tls(mut self, tls: Tls) -> Self {
self.info.tls = tls;
self
@@ -160,8 +176,8 @@ impl SmtpTransportBuilder {
/// Use a custom configuration for the connection pool
///
/// Defaults can be found at [`PoolConfig`]
#[cfg(feature = "r2d2")]
#[cfg_attr(docsrs, doc(cfg(feature = "r2d2")))]
#[cfg(feature = "pool")]
#[cfg_attr(docsrs, doc(cfg(feature = "pool")))]
pub fn pool_config(mut self, pool_config: PoolConfig) -> Self {
self.pool_config = pool_config;
self
@@ -169,16 +185,15 @@ impl SmtpTransportBuilder {
/// Build the transport
///
/// If the `r2d2` feature is enabled an `Arc` wrapped pool is be created.
/// If the `pool` feature is enabled an `Arc` wrapped pool is be created.
/// Defaults can be found at [`PoolConfig`]
pub fn build(self) -> SmtpTransport {
let client = SmtpClient { info: self.info };
SmtpTransport {
#[cfg(feature = "r2d2")]
inner: self.pool_config.build(client),
#[cfg(not(feature = "r2d2"))]
inner: client,
}
#[cfg(feature = "pool")]
let client = Pool::new(self.pool_config, client);
SmtpTransport { inner: client }
}
}
@@ -222,7 +237,7 @@ impl SmtpClient {
}
if let Some(credentials) = &self.info.credentials {
conn.auth(&self.info.authentication, &credentials)?;
conn.auth(&self.info.authentication, credentials)?;
}
Ok(conn)
}

View File

@@ -4,7 +4,6 @@ use std::fmt::{Display, Formatter, Result as FmtResult};
/// Encode a string as xtext
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct XText<'a>(pub &'a str);
impl<'a> Display for XText<'a> {

View File

@@ -1,10 +1,12 @@
//! The stub transport only logs message envelope and drops the content. It can be useful for
//! testing purposes.
//! The stub transport logs message envelopes as well as contents. It can be useful for testing
//! purposes.
//!
//! #### Stub Transport
//! # Stub Transport
//!
//! The stub transport returns provided result and drops the content. It can be useful for
//! testing purposes.
//! The stub transport logs message envelopes as well as contents. It can be useful for testing
//! purposes.
//!
//! # Examples
//!
//! ```rust
//! # #[cfg(feature = "builder")]
@@ -12,7 +14,7 @@
//! use lettre::{transport::stub::StubTransport, Message, Transport};
//!
//! # use std::error::Error;
//! # fn main() -> Result<(), Box<dyn Error>> {
//! # fn try_main() -> Result<(), Box<dyn Error>> {
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
@@ -23,17 +25,33 @@
//! let mut sender = StubTransport::new_ok();
//! let result = sender.send(&email);
//! assert!(result.is_ok());
//! assert_eq!(
//! sender.messages(),
//! vec![(
//! email.envelope().clone(),
//! String::from_utf8(email.formatted()).unwrap()
//! )],
//! );
//! # Ok(())
//! # }
//! # try_main().unwrap();
//! # }
//! ```
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
use std::{
error::Error as StdError,
fmt,
sync::{Arc, Mutex as StdMutex},
};
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
use async_trait::async_trait;
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
use futures_util::lock::Mutex as FuturesMutex;
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
use crate::AsyncTransport;
use crate::{address::Envelope, Transport};
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
use async_trait::async_trait;
use std::{error::Error as StdError, fmt};
#[derive(Debug, Copy, Clone)]
pub struct Error;
@@ -46,47 +64,113 @@ impl fmt::Display for Error {
impl StdError for Error {}
/// This transport logs the message envelope and returns the given response
#[derive(Debug, Clone, Copy)]
/// This transport logs messages and always returns the given response
#[derive(Debug, Clone)]
pub struct StubTransport {
response: Result<(), Error>,
message_log: Arc<StdMutex<Vec<(Envelope, String)>>>,
}
/// This transport logs messages and always returns the given response
#[derive(Debug, Clone)]
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
pub struct AsyncStubTransport {
response: Result<(), Error>,
message_log: Arc<FuturesMutex<Vec<(Envelope, String)>>>,
}
impl StubTransport {
/// Creates a new transport that always returns the given Result
pub fn new(response: Result<(), Error>) -> StubTransport {
StubTransport { response }
pub fn new(response: Result<(), Error>) -> Self {
Self {
response,
message_log: Arc::new(StdMutex::new(vec![])),
}
}
/// Creates a new transport that always returns a success response
pub fn new_ok() -> StubTransport {
StubTransport { response: Ok(()) }
pub fn new_ok() -> Self {
Self {
response: Ok(()),
message_log: Arc::new(StdMutex::new(vec![])),
}
}
/// Creates a new transport that always returns an error
pub fn new_error() -> StubTransport {
StubTransport {
pub fn new_error() -> Self {
Self {
response: Err(Error),
message_log: Arc::new(StdMutex::new(vec![])),
}
}
/// Return all logged messages sent using [`Transport::send_raw`]
pub fn messages(&self) -> Vec<(Envelope, String)> {
self.message_log
.lock()
.expect("Couldn't acquire lock to write message log")
.clone()
}
}
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
impl AsyncStubTransport {
/// Creates a new transport that always returns the given Result
pub fn new(response: Result<(), Error>) -> Self {
Self {
response,
message_log: Arc::new(FuturesMutex::new(vec![])),
}
}
/// Creates a new transport that always returns a success response
pub fn new_ok() -> Self {
Self {
response: Ok(()),
message_log: Arc::new(FuturesMutex::new(vec![])),
}
}
/// Creates a new transport that always returns an error
pub fn new_error() -> Self {
Self {
response: Err(Error),
message_log: Arc::new(FuturesMutex::new(vec![])),
}
}
/// Return all logged messages sent using [`AsyncTransport::send_raw`]
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub async fn messages(&self) -> Vec<(Envelope, String)> {
self.message_log.lock().await.clone()
}
}
impl Transport for StubTransport {
type Ok = ();
type Error = Error;
fn send_raw(&self, _envelope: &Envelope, _email: &[u8]) -> Result<Self::Ok, Self::Error> {
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
self.message_log
.lock()
.expect("Couldn't acquire lock to write message log")
.push((envelope.clone(), String::from_utf8_lossy(email).into()));
self.response
}
}
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
#[async_trait]
impl AsyncTransport for StubTransport {
impl AsyncTransport for AsyncStubTransport {
type Ok = ();
type Error = Error;
async fn send_raw(&self, _envelope: &Envelope, _email: &[u8]) -> Result<Self::Ok, Self::Error> {
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
self.message_log
.lock()
.await
.push((envelope.clone(), String::from_utf8_lossy(email).into()));
self.response
}
}

7
testdata/coredns.conf vendored Normal file
View File

@@ -0,0 +1,7 @@
. {
bind 127.0.0.54
forward . 9.9.9.9 8.8.8.8 1.1.1.1 {
except example.org
}
file testdata/db.example.org example.org
}

2
testdata/db.example.org vendored Normal file
View File

@@ -0,0 +1,2 @@
@ 600 IN SOA ns.example.org hostmaster.example.org 1 10800 3600 604800 3600
dkimtest._domainkey 600 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+FHbM8BwkBBz/Ux5OYLQ5Bp1HVuCHTP6Rr3HXTnome/2cGl/ze0tsmmFbCjjsS89MXbMGs9xJhjv18LmL1N0UTllblOizzVjorQyN4RwBOfG34j7SS56pwzrA738Ry8FAbL5InPWEgVzbOhXuTCs8yuzcqTnm4sH/csnIl7cMWeQkVn1FR9LKMtUG0fjhDPkdX0jx3qTX1L3Z7a7gX6geY191yNd9i9DvE2/+wMigMYz1LAts4alk2g86MQhtbjc8AOR7EC15hSw37/lmamlunYLa3wC+PzHNMA8sAfnmkgNvipssjh8LnelD9qn+VtsjQB5ppkeQx3TcUPvz5z+QIDAQAB"

View File

@@ -1,13 +1,14 @@
Date: Tue, 15 Nov 1994 08:12:31 GMT
Date: Tue, 15 Nov 1994 08:12:31 -0000
From: NoBody <nobody@domain.tld>
Reply-To: Yuin <yuin@domain.tld>
To: Hei <hei@domain.tld>
Subject: Happy new year
MIME-Version: 1.0
Content-Type: multipart/related; boundary="0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1"
Content-Type: multipart/related;
boundary="GUEEoEeTXtLcK2sMhmH1RfC1co13g4rtnRUFjQFA"
--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
Content-Type: text/html; charset=utf8
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: 7bit
<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>

View File

@@ -1,12 +1,23 @@
#[cfg(all(feature = "file-transport", feature = "builder"))]
fn default_date() -> std::time::SystemTime {
use std::time::{Duration, SystemTime};
// Tue, 15 Nov 1994 08:12:31 GMT
SystemTime::UNIX_EPOCH + Duration::from_secs(784887151)
}
#[cfg(test)]
#[cfg(all(feature = "file-transport", feature = "builder"))]
mod sync {
use lettre::{FileTransport, Message, Transport};
use std::{
env::temp_dir,
fs::{read_to_string, remove_file},
};
use lettre::{FileTransport, Message, Transport};
use crate::default_date;
#[test]
fn file_transport() {
let sender = FileTransport::new(temp_dir());
@@ -15,7 +26,7 @@ mod sync {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.date(default_date())
.body(String::from("Be happy!"))
.unwrap();
@@ -32,7 +43,7 @@ mod sync {
"Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\r\n",
"Subject: Happy new year\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Be happy!"
@@ -50,7 +61,7 @@ mod sync {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.date(default_date())
.body(String::from("Be happy!"))
.unwrap();
@@ -70,7 +81,7 @@ mod sync {
"Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\r\n",
"Subject: Happy new year\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Be happy!"
@@ -92,64 +103,19 @@ mod sync {
}
}
#[cfg(test)]
#[cfg(all(feature = "file-transport", feature = "builder", feature = "tokio02"))]
mod tokio_02 {
use lettre::{AsyncFileTransport, AsyncTransport, Message, Tokio02Executor};
use std::{
env::temp_dir,
fs::{read_to_string, remove_file},
};
use tokio02_crate as tokio;
#[tokio::test]
async fn file_transport_tokio02() {
let sender = AsyncFileTransport::<Tokio02Executor>::new(temp_dir());
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")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body(String::from("Be happy!"))
.unwrap();
let result = sender.send(email).await;
let id = result.unwrap();
let eml_file = temp_dir().join(format!("{}.eml", id));
let eml = read_to_string(&eml_file).unwrap();
assert_eq!(
eml,
concat!(
"From: NoBody <nobody@domain.tld>\r\n",
"Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\r\n",
"Subject: Happy new year\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Be happy!"
)
);
remove_file(eml_file).unwrap();
}
}
#[cfg(test)]
#[cfg(all(feature = "file-transport", feature = "builder", feature = "tokio1"))]
mod tokio_1 {
use lettre::{AsyncFileTransport, AsyncTransport, Message, Tokio1Executor};
use std::{
env::temp_dir,
fs::{read_to_string, remove_file},
};
use lettre::{AsyncFileTransport, AsyncTransport, Message, Tokio1Executor};
use tokio1_crate as tokio;
#[cfg(feature = "tokio02")]
use crate::default_date;
#[tokio::test]
async fn file_transport_tokio1() {
let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir());
@@ -158,7 +124,7 @@ mod tokio_1 {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.date(default_date())
.body(String::from("Be happy!"))
.unwrap();
@@ -175,7 +141,7 @@ mod tokio_1 {
"Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\r\n",
"Subject: Happy new year\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Be happy!"
@@ -192,12 +158,15 @@ mod tokio_1 {
feature = "async-std1"
))]
mod asyncstd_1 {
use lettre::{AsyncFileTransport, AsyncStd1Executor, AsyncTransport, Message};
use std::{
env::temp_dir,
fs::{read_to_string, remove_file},
};
use lettre::{AsyncFileTransport, AsyncStd1Executor, AsyncTransport, Message};
use crate::default_date;
#[async_std::test]
async fn file_transport_asyncstd1() {
let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir());
@@ -206,7 +175,7 @@ mod asyncstd_1 {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.date(default_date())
.body(String::from("Be happy!"))
.unwrap();
@@ -223,7 +192,7 @@ mod asyncstd_1 {
"Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\r\n",
"Subject: Happy new year\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Be happy!"

View File

@@ -20,34 +20,6 @@ mod sync {
}
}
#[cfg(test)]
#[cfg(all(
feature = "sendmail-transport",
feature = "builder",
feature = "tokio02"
))]
mod tokio_02 {
use lettre::{AsyncSendmailTransport, AsyncTransport, Message, Tokio02Executor};
use tokio02_crate as tokio;
#[tokio::test]
async fn sendmail_transport_tokio02() {
let sender = AsyncSendmailTransport::<Tokio02Executor>::new();
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")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body(String::from("Be happy!"))
.unwrap();
let result = sender.send(email).await;
println!("{:?}", result);
assert!(result.is_ok());
}
}
#[cfg(test)]
#[cfg(all(
feature = "sendmail-transport",
@@ -66,7 +38,6 @@ mod tokio_1 {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body(String::from("Be happy!"))
.unwrap();
@@ -93,7 +64,6 @@ mod asyncstd_1 {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body(String::from("Be happy!"))
.unwrap();

View File

@@ -20,36 +20,10 @@ mod sync {
}
}
#[cfg(test)]
#[cfg(all(feature = "smtp-transport", feature = "builder", feature = "tokio02"))]
mod tokio_02 {
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio02Executor};
use tokio02_crate as tokio;
#[tokio::test]
async fn smtp_transport_simple_tokio02() {
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 sender: AsyncSmtpTransport<Tokio02Executor> =
AsyncSmtpTransport::<Tokio02Executor>::builder_dangerous("127.0.0.1")
.port(2525)
.build();
sender.send(email).await.unwrap();
}
}
#[cfg(test)]
#[cfg(all(feature = "smtp-transport", feature = "builder", feature = "tokio1"))]
mod tokio_1 {
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
use tokio1_crate as tokio;
#[tokio::test]

View File

@@ -1,8 +1,9 @@
#[cfg(all(test, feature = "smtp-transport", feature = "r2d2"))]
#[cfg(all(test, feature = "smtp-transport", feature = "pool"))]
mod sync {
use lettre::{address::Envelope, SmtpTransport, Transport};
use std::{sync::mpsc, thread};
use lettre::{address::Envelope, SmtpTransport, Transport};
fn envelope() -> Envelope {
Envelope::new(
Some("user@localhost".parse().unwrap()),

View File

@@ -17,78 +17,68 @@ mod sync {
sender_ok.send(&email).unwrap();
sender_ko.send(&email).unwrap_err();
}
}
#[cfg(test)]
#[cfg(all(feature = "builder", feature = "tokio02"))]
mod tokio_02 {
use lettre::{transport::stub::StubTransport, AsyncTransport, Message};
use tokio02_crate as tokio;
#[tokio::test]
async fn stub_transport_tokio02() {
let sender_ok = StubTransport::new_ok();
let sender_ko = StubTransport::new_error();
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")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body(String::from("Be happy!"))
.unwrap();
sender_ok.send(email.clone()).await.unwrap();
sender_ko.send(email).await.unwrap_err();
let expected_messages = vec![(
email.envelope().clone(),
String::from_utf8(email.formatted()).unwrap(),
)];
assert_eq!(sender_ok.messages(), expected_messages);
}
}
#[cfg(test)]
#[cfg(all(feature = "builder", feature = "tokio1"))]
mod tokio_1 {
use lettre::{transport::stub::StubTransport, AsyncTransport, Message};
use lettre::{transport::stub::AsyncStubTransport, AsyncTransport, Message};
use tokio1_crate as tokio;
#[tokio::test]
async fn stub_transport_tokio1() {
let sender_ok = StubTransport::new_ok();
let sender_ko = StubTransport::new_error();
let sender_ok = AsyncStubTransport::new_ok();
let sender_ko = AsyncStubTransport::new_error();
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")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body(String::from("Be happy!"))
.unwrap();
sender_ok.send(email.clone()).await.unwrap();
sender_ko.send(email).await.unwrap_err();
sender_ko.send(email.clone()).await.unwrap_err();
let expected_messages = vec![(
email.envelope().clone(),
String::from_utf8(email.formatted()).unwrap(),
)];
assert_eq!(sender_ok.messages().await, expected_messages);
}
}
#[cfg(test)]
#[cfg(all(feature = "builder", feature = "async-std1"))]
mod asyncstd_1 {
use lettre::{transport::stub::StubTransport, AsyncTransport, Message};
use lettre::{transport::stub::AsyncStubTransport, AsyncTransport, Message};
#[async_std::test]
async fn stub_transport_asyncstd1() {
let sender_ok = StubTransport::new_ok();
let sender_ko = StubTransport::new_error();
let sender_ok = AsyncStubTransport::new_ok();
let sender_ko = AsyncStubTransport::new_error();
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")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body(String::from("Be happy!"))
.unwrap();
sender_ok.send(email.clone()).await.unwrap();
sender_ko.send(email).await.unwrap_err();
sender_ko.send(email.clone()).await.unwrap_err();
let expected_messages = vec![(
email.envelope().clone(),
String::from_utf8(email.formatted()).unwrap(),
)];
assert_eq!(sender_ok.messages().await, expected_messages);
}
}