Compare commits

..

57 Commits

Author SHA1 Message Date
Paolo Barbolini
b0db759e5f Prepare 0.10.0-rc.6 (#761) 2022-04-29 15:59:36 +02:00
Paolo Barbolini
5daf5d397a Fix parsing Mailboxes with a comma in the name (#760) 2022-04-26 12:18:12 +02:00
Paolo Barbolini
3f1647fa48 Bump dependencies (#759) 2022-04-25 09:17:58 +00:00
Paolo Barbolini
fd106d9b0c Bump rsa crate to the final 0.6.0 release (#758) 2022-04-14 09:39:30 +00:00
Vincent Breitmoser
c1d37d54b4 Use +0000 timezone format in Date header (#756)
Since the Date we emit is UTC, it's correct to use "+0000". The
previously used -0000 timezone indicator means "no timezone info".
2022-04-10 08:34:50 +02:00
David Krasnitsky
efa0d58778 Improve compiler error messages (#754) 2022-04-07 05:03:28 +00:00
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
59 changed files with 2779 additions and 619 deletions

View File

@@ -7,11 +7,13 @@ on:
- master - master
env: env:
RUSTFLAGS: "--cfg lettre_ignore_tls_mismatch"
RUSTDOCFLAGS: "--cfg lettre_ignore_tls_mismatch"
RUST_BACKTRACE: full RUST_BACKTRACE: full
jobs: jobs:
rustfmt: rustfmt:
name: rustfmt / stable name: rustfmt / nightly-2022-02-11
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -20,7 +22,7 @@ jobs:
- name: Install rust - name: Install rust
run: | run: |
rustup update --no-self-update stable rustup default nightly-2022-02-11
rustup component add rustfmt rustup component add rustfmt
- name: cargo fmt - name: cargo fmt
@@ -79,8 +81,8 @@ jobs:
rust: stable rust: stable
- name: beta - name: beta
rust: beta rust: beta
- name: 1.46.0 - name: 1.56.0
rust: 1.46.0 rust: 1.56.0
steps: steps:
- name: Checkout - name: Checkout
@@ -108,6 +110,20 @@ jobs:
- name: Run SMTP server - name: Run SMTP server
run: smtp-sink 2525 1000& 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 - name: Test with no default features
run: cargo test --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: Several breaking changes were made between 0.9 and 0.10, but changes should be straightforward:
* MSRV is now 1.46 * MSRV is now 1.56.0
* The `lettre_email` crate has been merged into `lettre`. To migrate, replace `lettre_email` with `lettre::message` * 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). 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, * `SendableEmail` has been renamed to `Email` and `EmailBuilder::build()` produces it directly. To migrate,
@@ -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) * 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 * The `new` method of `ClientId` is deprecated
* Rename `serde-impls` feature to `serde` * 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 #### Bug Fixes

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "lettre" name = "lettre"
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge) # remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
version = "0.10.0-rc.2" version = "0.10.0-rc.6"
description = "Email client" description = "Email client"
readme = "README.md" readme = "README.md"
homepage = "https://lettre.rs" homepage = "https://lettre.rs"
@@ -10,7 +10,8 @@ license = "MIT"
authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo565.org>"] authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo565.org>"]
categories = ["email", "network-programming"] categories = ["email", "network-programming"]
keywords = ["email", "smtp", "mailer", "message", "sendmail"] keywords = ["email", "smtp", "mailer", "message", "sendmail"]
edition = "2018" edition = "2021"
rust-version = "1.56"
[badges] [badges]
is-it-maintained-issue-resolution = { repository = "lettre/lettre" } is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
@@ -19,6 +20,7 @@ maintenance = { status = "actively-developed" }
[dependencies] [dependencies]
idna = "0.2" idna = "0.2"
once_cell = "1"
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
# builder # builder
@@ -27,24 +29,23 @@ mime = { version = "0.3.4", optional = true }
fastrand = { version = "1.4", optional = true } fastrand = { version = "1.4", optional = true }
quoted_printable = { version = "0.4", optional = true } quoted_printable = { version = "0.4", optional = true }
base64 = { version = "0.13", optional = true } base64 = { version = "0.13", optional = true }
once_cell = "1"
regex = { version = "1", default-features = false, features = ["std", "unicode-case"] } regex = { version = "1", default-features = false, features = ["std", "unicode-case"] }
email-encoding = { version = "0.1", optional = true }
# file transport # file transport
uuid = { version = "0.8", features = ["v4"], optional = true } uuid = { version = "1", features = ["v4"], optional = true }
serde = { version = "1", optional = true, features = ["derive"] } serde = { version = "1", optional = true, features = ["derive"] }
serde_json = { version = "1", optional = true } serde_json = { version = "1", optional = true }
# smtp # smtp
nom = { version = "6", default-features = false, features = ["alloc", "std"], optional = true } nom = { version = "7", optional = true }
r2d2 = { version = "0.8", optional = true } # feature
hostname = { version = "0.3", optional = true } # feature hostname = { version = "0.3", optional = true } # feature
## tls ## tls
native-tls = { version = "0.2", optional = true } # feature native-tls = { version = "0.2", optional = true } # feature
rustls = { version = "0.19", features = ["dangerous_configuration"], optional = true } rustls = { version = "0.20", features = ["dangerous_configuration"], optional = true }
webpki = { version = "0.21", optional = true } rustls-pemfile = { version = "1", optional = true }
webpki-roots = { version = "0.21", optional = true } webpki-roots = { version = "0.22", optional = true }
# async # async
futures-io = { version = "0.3.7", optional = true } futures-io = { version = "0.3.7", optional = true }
@@ -54,30 +55,36 @@ async-trait = { version = "0.1", optional = true }
## async-std ## async-std
async-std = { version = "1.8", optional = true, features = ["unstable"] } async-std = { version = "1.8", optional = true, features = ["unstable"] }
#async-native-tls = { version = "0.3.3", optional = true } #async-native-tls = { version = "0.3.3", optional = true }
async-rustls = { version = "0.2", optional = true } futures-rustls = { version = "0.22", optional = true }
## tokio ## tokio
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_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", optional = true }
ed25519-dalek = { version = "1.0.1", optional = true }
[dev-dependencies] [dev-dependencies]
criterion = "0.3" criterion = "0.3"
tracing-subscriber = "0.2.10" tracing-subscriber = "0.3"
glob = "0.3" glob = "0.3"
walkdir = "2" walkdir = "2"
tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] } tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] }
async-std = { version = "1.8", features = ["attributes"] } async-std = { version = "1.8", features = ["attributes"] }
serde_json = "1" serde_json = "1"
maud = "0.22.1" maud = "0.23"
[[bench]] [[bench]]
harness = false harness = false
name = "transport_smtp" name = "transport_smtp"
[features] [features]
default = ["smtp-transport", "native-tls", "hostname", "r2d2", "builder"] default = ["smtp-transport", "pool", "native-tls", "hostname", "builder"]
builder = ["httpdate", "mime", "base64", "fastrand", "quoted_printable"] builder = ["httpdate", "mime", "base64", "fastrand", "quoted_printable", "email-encoding"]
mime03 = ["mime"]
# transports # transports
file-transport = ["uuid"] file-transport = ["uuid"]
@@ -85,19 +92,23 @@ file-transport-envelope = ["serde", "serde_json", "file-transport"]
sendmail-transport = [] sendmail-transport = []
smtp-transport = ["base64", "nom"] smtp-transport = ["base64", "nom"]
rustls-tls = ["webpki", "webpki-roots", "rustls"] pool = ["futures-util"]
rustls-tls = ["webpki-roots", "rustls", "rustls-pemfile"]
# async # async
async-std1 = ["async-std", "async-trait", "futures-io", "futures-util"] async-std1 = ["async-std", "async-trait", "futures-io", "futures-util"]
#async-std1-native-tls = ["async-std1", "native-tls", "async-native-tls"] #async-std1-native-tls = ["async-std1", "native-tls", "async-native-tls"]
async-std1-rustls-tls = ["async-std1", "rustls-tls", "async-rustls"] async-std1-rustls-tls = ["async-std1", "rustls-tls", "futures-rustls"]
tokio1 = ["tokio1_crate", "async-trait", "futures-io", "futures-util"] tokio1 = ["tokio1_crate", "async-trait", "futures-io", "futures-util"]
tokio1-native-tls = ["tokio1", "native-tls", "tokio1_native_tls_crate"] tokio1-native-tls = ["tokio1", "native-tls", "tokio1_native_tls_crate"]
tokio1-rustls-tls = ["tokio1", "rustls-tls", "tokio1_rustls"] tokio1-rustls-tls = ["tokio1", "rustls-tls", "tokio1_rustls"]
dkim = ["sha2", "rsa", "ed25519-dalek"]
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true
rustdoc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs", "--cfg", "lettre_ignore_tls_mismatch"]
[[example]] [[example]]
name = "basic_html" name = "basic_html"

View File

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

View File

@@ -28,8 +28,8 @@
</div> </div>
<div align="center"> <div align="center">
<a href="https://deps.rs/crate/lettre/0.10.0-rc.2"> <a href="https://deps.rs/crate/lettre/0.10.0-rc.6">
<img src="https://deps.rs/crate/lettre/0.10.0-rc.2/status.svg" <img src="https://deps.rs/crate/lettre/0.10.0-rc.6/status.svg"
alt="dependency status" /> alt="dependency status" />
</a> </a>
</div> </div>
@@ -65,13 +65,13 @@ Lettre does not provide (for now):
## Example ## Example
This library requires Rust 1.46 or newer. This library requires Rust 1.56.0 or newer.
To use this library, add the following to your `Cargo.toml`: To use this library, add the following to your `Cargo.toml`:
```toml ```toml
[dependencies] [dependencies]
lettre = "0.10.0-rc.2" lettre = "0.10.0-rc.6"
``` ```
```rust,no_run ```rust,no_run
@@ -103,10 +103,22 @@ match mailer.send(&email) {
## Testing ## 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`. 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 ## Code of conduct
Anyone who interacts with Lettre in any space, including but not limited to Anyone who interacts with Lettre in any space, including but not limited to

View File

@@ -1,12 +1,11 @@
// This line is only to make it compile from lettre's examples folder, // This line is only to make it compile from lettre's examples folder,
// since it uses Rust 2018 crate renaming to import tokio. // since it uses Rust 2018 crate renaming to import tokio.
// Won't be needed in user's code. // Won't be needed in user's code.
use tokio1_crate as tokio;
use lettre::{ use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message, transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message,
Tokio1Executor, Tokio1Executor,
}; };
use tokio1_crate as tokio;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {

View File

@@ -1,12 +1,11 @@
// This line is only to make it compile from lettre's examples folder, // This line is only to make it compile from lettre's examples folder,
// since it uses Rust 2018 crate renaming to import tokio. // since it uses Rust 2018 crate renaming to import tokio.
// Won't be needed in user's code. // Won't be needed in user's code.
use tokio1_crate as tokio;
use lettre::{ use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message, transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message,
Tokio1Executor, Tokio1Executor,
}; };
use tokio1_crate as tokio;
#[tokio::main] #[tokio::main]
async fn 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; use super::Address;
#[cfg(feature = "builder")] #[cfg(feature = "builder")]
use crate::message::header::{self, Headers}; use crate::message::header::{self, Headers};

View File

@@ -1,10 +1,6 @@
//! Representation of an email address //! Representation of an email address
use idna::domain_to_ascii;
use once_cell::sync::Lazy;
use regex::Regex;
use std::{ use std::{
convert::{TryFrom, TryInto},
error::Error, error::Error,
ffi::OsStr, ffi::OsStr,
fmt::{Display, Formatter, Result as FmtResult}, fmt::{Display, Formatter, Result as FmtResult},
@@ -12,6 +8,10 @@ use std::{
str::FromStr, 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. /// Represents an email address with a user and a domain name.
/// ///
/// This type contains email in canonical form (_user@domain.tld_). /// This type contains email in canonical form (_user@domain.tld_).
@@ -174,15 +174,10 @@ impl FromStr for Address {
type Err = AddressError; type Err = AddressError;
fn from_str(val: &str) -> Result<Self, AddressError> { fn from_str(val: &str) -> Result<Self, AddressError> {
let mut parts = val.rsplitn(2, '@'); let at_start = check_address(val)?;
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(Address { Ok(Address {
serialized: val.into(), 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 { impl AsRef<str> for Address {
fn as_ref(&self) -> &str { fn as_ref(&self) -> &str {
&self.serialized &self.serialized
@@ -221,6 +228,16 @@ 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)] #[derive(Debug, PartialEq, Clone, Copy)]
/// Errors in email addresses parsing /// Errors in email addresses parsing
pub enum AddressError { pub enum AddressError {

View File

@@ -1,10 +1,16 @@
use async_trait::async_trait;
use std::fmt::Debug; use std::fmt::Debug;
#[cfg(feature = "smtp-transport")]
use std::future::Future;
#[cfg(feature = "file-transport")] #[cfg(feature = "file-transport")]
use std::io::Result as IoResult; use std::io::Result as IoResult;
#[cfg(feature = "file-transport")] #[cfg(feature = "file-transport")]
use std::path::Path; 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( #[cfg(all(
feature = "smtp-transport", feature = "smtp-transport",
@@ -37,12 +43,29 @@ use crate::transport::smtp::Error;
/// [`AsyncFileTransport`]: crate::AsyncFileTransport /// [`AsyncFileTransport`]: crate::AsyncFileTransport
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))] #[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
#[async_trait] #[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)] #[doc(hidden)]
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
async fn connect( async fn connect(
hostname: &str, hostname: &str,
port: u16, port: u16,
timeout: Option<Duration>,
hello_name: &ClientId, hello_name: &ClientId,
tls: &Tls, tls: &Tls,
) -> Result<AsyncSmtpConnection, Error>; ) -> Result<AsyncSmtpConnection, Error>;
@@ -56,6 +79,13 @@ pub trait Executor: Debug + Send + Sync + private::Sealed {
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()>; async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()>;
} }
#[doc(hidden)]
#[cfg(feature = "smtp-transport")]
#[async_trait]
pub trait SpawnHandle: Debug + Send + Sync + 'static + private::Sealed {
async fn shutdown(self);
}
/// Async [`Executor`] using `tokio` `1.x` /// Async [`Executor`] using `tokio` `1.x`
/// ///
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`] /// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
@@ -74,11 +104,33 @@ pub struct Tokio1Executor;
#[async_trait] #[async_trait]
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
impl Executor for Tokio1Executor { 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)] #[doc(hidden)]
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
async fn connect( async fn connect(
hostname: &str, hostname: &str,
port: u16, port: u16,
timeout: Option<Duration>,
hello_name: &ClientId, hello_name: &ClientId,
tls: &Tls, tls: &Tls,
) -> Result<AsyncSmtpConnection, Error> { ) -> Result<AsyncSmtpConnection, Error> {
@@ -89,8 +141,13 @@ impl Executor for Tokio1Executor {
_ => None, _ => None,
}; };
#[allow(unused_mut)] #[allow(unused_mut)]
let mut conn = let mut conn = AsyncSmtpConnection::connect_tokio1(
AsyncSmtpConnection::connect_tokio1(hostname, port, hello_name, tls_parameters).await?; (hostname, port),
timeout,
hello_name,
tls_parameters,
)
.await?;
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))] #[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
match tls { match tls {
@@ -121,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` /// Async [`Executor`] using `async-std` `1.x`
/// ///
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`] /// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
@@ -139,11 +204,34 @@ pub struct AsyncStd1Executor;
#[async_trait] #[async_trait]
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
impl Executor for AsyncStd1Executor { 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)] #[doc(hidden)]
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
async fn connect( async fn connect(
hostname: &str, hostname: &str,
port: u16, port: u16,
timeout: Option<Duration>,
hello_name: &ClientId, hello_name: &ClientId,
tls: &Tls, tls: &Tls,
) -> Result<AsyncSmtpConnection, Error> { ) -> Result<AsyncSmtpConnection, Error> {
@@ -154,9 +242,13 @@ impl Executor for AsyncStd1Executor {
_ => None, _ => None,
}; };
#[allow(unused_mut)] #[allow(unused_mut)]
let mut conn = let mut conn = AsyncSmtpConnection::connect_asyncstd1(
AsyncSmtpConnection::connect_asyncstd1(hostname, port, hello_name, tls_parameters) (hostname, port),
.await?; timeout,
hello_name,
tls_parameters,
)
.await?;
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))] #[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
match tls { match tls {
@@ -187,6 +279,14 @@ 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 { mod private {
use super::*; use super::*;
@@ -197,4 +297,10 @@ mod private {
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
impl Sealed for AsyncStd1Executor {} 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 //! * Secure defaults
//! * Async support //! * Async support
//! //!
//! Lettre requires Rust 1.46 or newer. //! Lettre requires Rust 1.56.0 or newer.
//! //!
//! ## Features //! ## Features
//! //!
@@ -27,7 +27,7 @@
//! _Send emails using [`SMTP`]_ //! _Send emails using [`SMTP`]_
//! //!
//! * **smtp-transport** 📫: Enable the SMTP transport //! * **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` //! * **hostname** 📫: Try to use the actual system hostname for the SMTP `CLIENTID`
//! //!
//! #### SMTP over TLS via the native-tls crate //! #### SMTP over TLS via the native-tls crate
@@ -84,18 +84,23 @@
//! //!
//! * **serde**: Serialization/Deserialization of entities //! * **serde**: Serialization/Deserialization of entities
//! * **tracing**: Logging using the `tracing` crate //! * **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 //! [`SMTP`]: crate::transport::smtp
//! [`sendmail`]: crate::transport::sendmail //! [`sendmail`]: crate::transport::sendmail
//! [`file`]: crate::transport::file //! [`file`]: crate::transport::file
//! [`ContentType`]: crate::message::header::ContentType
//! [tokio]: https://docs.rs/tokio/1 //! [tokio]: https://docs.rs/tokio/1
//! [async-std]: https://docs.rs/async-std/1 //! [async-std]: https://docs.rs/async-std/1
//! [ring]: https://github.com/briansmith/ring#ring //! [ring]: https://github.com/briansmith/ring#ring
//! [ring-support]: https://github.com/briansmith/ring#online-automated-testing //! [ring-support]: https://github.com/briansmith/ring#online-automated-testing
//! [Tokio 1.x]: https://docs.rs/tokio/1 //! [Tokio 1.x]: https://docs.rs/tokio/1
//! [async-std 1.x]: https://docs.rs/async-std/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-rc.2")] #![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.0-rc.6")]
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")] #![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")] #![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
@@ -105,10 +110,62 @@
trivial_numeric_casts, trivial_numeric_casts,
unstable_features, unstable_features,
unused_import_braces, unused_import_braces,
rust_2018_idioms rust_2018_idioms,
clippy::string_add,
clippy::string_add_assign
)] )]
#![cfg_attr(docsrs, feature(doc_cfg))] #![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 were trying to opt into `rustls-tls` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
#[cfg(all(
feature = "tokio1",
feature = "rustls-tls",
not(feature = "tokio1-rustls-tls")
))]
compile_error!("Lettre is being built with the `tokio1` and the `rustls-tls` features, but the `tokio1-rustls-tls` feature hasn't been turned on.
If you'd like to use `native-tls` make sure that the `rustls-tls` feature hasn't been enabled by mistake.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
/*
#[cfg(all(
feature = "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 were trying to opt into `rustls-tls` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
#[cfg(all(
feature = "async-std1",
feature = "rustls-tls",
not(feature = "async-std1-rustls-tls")
))]
compile_error!("Lettre is being built with the `async-std1` and the `rustls-tls` features, but the `async-std1-rustls-tls` feature hasn't been turned on.
If you'd like to use `native-tls` make sure that the `rustls-tls` hasn't been enabled by mistake.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
}
pub mod address; pub mod address;
pub mod error; pub mod error;
#[cfg(any(feature = "tokio1", feature = "async-std1"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
@@ -118,6 +175,8 @@ mod executor;
pub mod message; pub mod message;
pub mod transport; pub mod transport;
use std::error::Error as StdError;
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
pub use self::executor::AsyncStd1Executor; pub use self::executor::AsyncStd1Executor;
#[cfg(all(any(feature = "tokio1", feature = "async-std1")))] #[cfg(all(any(feature = "tokio1", feature = "async-std1")))]
@@ -154,21 +213,17 @@ pub use crate::transport::sendmail::SendmailTransport;
any(feature = "tokio1", feature = "async-std1") any(feature = "tokio1", feature = "async-std1")
))] ))]
pub use crate::transport::smtp::AsyncSmtpTransport; pub use crate::transport::smtp::AsyncSmtpTransport;
#[cfg(feature = "smtp-transport")]
pub use crate::transport::smtp::SmtpTransport;
#[doc(inline)] #[doc(inline)]
pub use crate::transport::Transport; pub use crate::transport::Transport;
use crate::{address::Envelope, error::Error}; 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>; pub(crate) type BoxError = Box<dyn StdError + Send + Sync>;
#[cfg(test)] #[cfg(test)]
#[cfg(feature = "builder")] #[cfg(feature = "builder")]
mod test { mod test {
use std::convert::TryFrom;
use super::*; use super::*;
use crate::message::{header, header::Headers, Mailbox, Mailboxes}; use crate::message::{header, header::Headers, Mailbox, Mailboxes};

View File

@@ -20,21 +20,75 @@ enum Disposition {
} }
impl Attachment { impl Attachment {
/// Creates a new 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 { pub fn new(filename: String) -> Self {
Attachment { Attachment {
disposition: Disposition::Attached(filename), disposition: Disposition::Attached(filename),
} }
} }
/// Creates a new inline attachment /// 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 { pub fn new_inline(content_id: String) -> Self {
Attachment { Attachment {
disposition: Disposition::Inline(content_id), disposition: Disposition::Inline(content_id),
} }
} }
/// Build the attachment part /// 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 { pub fn body<T: IntoBody>(self, content: T, content_type: ContentType) -> SinglePart {
let mut builder = SinglePart::builder(); let mut builder = SinglePart::builder();
builder = match self.disposition { builder = match self.disposition {

View File

@@ -183,8 +183,8 @@ impl MaybeString {
/// would result into an invalid encoded body. /// would result into an invalid encoded body.
fn is_encoding_ok(&self, encoding: ContentTransferEncoding) -> bool { fn is_encoding_ok(&self, encoding: ContentTransferEncoding) -> bool {
match encoding { match encoding {
ContentTransferEncoding::SevenBit => is_7bit_encoded(&self), ContentTransferEncoding::SevenBit => is_7bit_encoded(self),
ContentTransferEncoding::EightBit => is_8bit_encoded(&self), ContentTransferEncoding::EightBit => is_8bit_encoded(self),
ContentTransferEncoding::Binary ContentTransferEncoding::Binary
| ContentTransferEncoding::QuotedPrintable | ContentTransferEncoding::QuotedPrintable
| ContentTransferEncoding::Base64 => true, | ContentTransferEncoding::Base64 => true,
@@ -342,7 +342,7 @@ where
/// In place conversion to CRLF line endings /// In place conversion to CRLF line endings
fn in_place_crlf_line_endings(string: &mut String) { fn in_place_crlf_line_endings(string: &mut String) {
let indices = find_all_lf_char_indices(&string); let indices = find_all_lf_char_indices(string);
for i in indices { for i in indices {
// this relies on `indices` being in reverse order // this relies on `indices` being in reverse order

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

@@ -3,7 +3,7 @@ use std::{
str::FromStr, str::FromStr,
}; };
use super::{Header, HeaderName}; use super::{Header, HeaderName, HeaderValue};
use crate::BoxError; use crate::BoxError;
/// `Content-Transfer-Encoding` of the body /// `Content-Transfer-Encoding` of the body
@@ -11,7 +11,8 @@ use crate::BoxError;
/// The `Message` builder takes care of choosing the most /// The `Message` builder takes care of choosing the most
/// efficient encoding based on the chosen body, so in most /// efficient encoding based on the chosen body, so in most
/// use-caches this header shouldn't be set manually. /// 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 { pub enum ContentTransferEncoding {
/// ASCII /// ASCII
SevenBit, SevenBit,
@@ -34,8 +35,9 @@ impl Header for ContentTransferEncoding {
Ok(s.parse()?) Ok(s.parse()?)
} }
fn display(&self) -> String { fn display(&self) -> HeaderValue {
self.to_string() let val = self.to_string();
HeaderValue::dangerous_new_pre_encoded(Self::name(), val.clone(), val)
} }
} }
@@ -74,7 +76,7 @@ impl Default for ContentTransferEncoding {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::ContentTransferEncoding; use super::ContentTransferEncoding;
use crate::message::header::{HeaderName, Headers}; use crate::message::header::{HeaderName, HeaderValue, Headers};
#[test] #[test]
fn format_content_transfer_encoding() { fn format_content_transfer_encoding() {
@@ -93,20 +95,20 @@ mod test {
fn parse_content_transfer_encoding() { fn parse_content_transfer_encoding() {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"), HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
"7bit".to_string(), "7bit".to_string(),
); ));
assert_eq!( assert_eq!(
headers.get::<ContentTransferEncoding>(), headers.get::<ContentTransferEncoding>(),
Some(ContentTransferEncoding::SevenBit) Some(ContentTransferEncoding::SevenBit)
); );
headers.insert_raw( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"), HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
"base64".to_string(), "base64".to_string(),
); ));
assert_eq!( assert_eq!(
headers.get::<ContentTransferEncoding>(), headers.get::<ContentTransferEncoding>(),

View File

@@ -1,29 +1,55 @@
use super::{Header, HeaderName}; use std::fmt::Write;
use email_encoding::headers::EmailWriter;
use super::{Header, HeaderName, HeaderValue};
use crate::BoxError; use crate::BoxError;
/// `Content-Disposition` of an attachment /// `Content-Disposition` of an attachment
/// ///
/// Defined in [RFC2183](https://tools.ietf.org/html/rfc2183) /// Defined in [RFC2183](https://tools.ietf.org/html/rfc2183)
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct ContentDisposition(String); pub struct ContentDisposition(HeaderValue);
impl ContentDisposition { impl ContentDisposition {
/// An attachment which should be displayed inline into the message /// An attachment which should be displayed inline into the message
pub fn inline() -> Self { pub fn inline() -> Self {
Self("inline".into()) 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 /// An attachment which should be displayed inline into the message, but that also
/// species the filename in case it were to be downloaded /// species the filename in case it were to be downloaded
pub fn inline_with_name(file_name: &str) -> Self { pub fn inline_with_name(file_name: &str) -> Self {
debug_assert!(!file_name.contains('"'), "file_name shouldn't contain '\"'"); Self::with_name("inline", file_name)
Self(format!("inline; filename=\"{}\"", file_name))
} }
/// An attachment which is separate from the body of the message, and can be downloaded separately /// An attachment which is separate from the body of the message, and can be downloaded separately
pub fn attachment(file_name: &str) -> Self { pub fn attachment(file_name: &str) -> Self {
debug_assert!(!file_name.contains('"'), "file_name shouldn't contain '\"'"); Self::with_name("attachment", file_name)
Self(format!("attachment; filename=\"{}\"", 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,
))
} }
} }
@@ -33,10 +59,18 @@ impl Header for ContentDisposition {
} }
fn parse(s: &str) -> Result<Self, BoxError> { fn parse(s: &str) -> Result<Self, BoxError> {
Ok(Self(s.into())) 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) -> String { fn display(&self) -> HeaderValue {
self.0.clone() self.0.clone()
} }
} }
@@ -44,7 +78,7 @@ impl Header for ContentDisposition {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::ContentDisposition; use super::ContentDisposition;
use crate::message::header::{HeaderName, Headers}; use crate::message::header::{HeaderName, HeaderValue, Headers};
#[test] #[test]
fn format_content_disposition() { fn format_content_disposition() {
@@ -66,20 +100,20 @@ mod test {
fn parse_content_disposition() { fn parse_content_disposition() {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Disposition"), HeaderName::new_from_ascii_str("Content-Disposition"),
"inline".to_string(), "inline".to_string(),
); ));
assert_eq!( assert_eq!(
headers.get::<ContentDisposition>(), headers.get::<ContentDisposition>(),
Some(ContentDisposition::inline()) Some(ContentDisposition::inline())
); );
headers.insert_raw( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Disposition"), HeaderName::new_from_ascii_str("Content-Disposition"),
"attachment; filename=\"something.txt\"".to_string(), "attachment; filename=\"something.txt\"".to_string(),
); ));
assert_eq!( assert_eq!(
headers.get::<ContentDisposition>(), headers.get::<ContentDisposition>(),

View File

@@ -6,13 +6,18 @@ use std::{
use mime::Mime; use mime::Mime;
use super::{Header, HeaderName}; use super::{Header, HeaderName, HeaderValue};
use crate::BoxError; use crate::BoxError;
/// `Content-Type` of the body /// `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) /// Defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-5)
#[derive(Debug, Clone, PartialEq)] ///
/// [mime type]: https://www.iana.org/assignments/media-types/media-types.xhtml
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContentType(Mime); pub struct ContentType(Mime);
impl ContentType { impl ContentType {
@@ -49,8 +54,8 @@ impl Header for ContentType {
Ok(Self(s.parse()?)) Ok(Self(s.parse()?))
} }
fn display(&self) -> String { fn display(&self) -> HeaderValue {
self.0.to_string() HeaderValue::new(Self::name(), self.0.to_string())
} }
} }
@@ -62,6 +67,14 @@ impl FromStr for ContentType {
} }
} }
#[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`]. /// An error occurred while trying to [`ContentType::parse`].
#[derive(Debug)] #[derive(Debug)]
pub struct ContentTypeErr(mime::FromStrError); pub struct ContentTypeErr(mime::FromStrError);
@@ -78,10 +91,66 @@ impl Display for ContentTypeErr {
} }
} }
// -- 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)] #[cfg(test)]
mod test { mod test {
use super::ContentType; use super::ContentType;
use crate::message::header::{HeaderName, Headers}; use crate::message::header::{HeaderName, HeaderValue, Headers};
#[test] #[test]
fn format_content_type() { fn format_content_type() {
@@ -106,17 +175,17 @@ mod test {
fn parse_content_type() { fn parse_content_type() {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Type"), HeaderName::new_from_ascii_str("Content-Type"),
"text/plain; charset=utf-8".to_string(), "text/plain; charset=utf-8".to_string(),
); ));
assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_PLAIN)); assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_PLAIN));
headers.insert_raw( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Type"), HeaderName::new_from_ascii_str("Content-Type"),
"text/html; charset=utf-8".to_string(), "text/html; charset=utf-8".to_string(),
); ));
assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_HTML)); assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_HTML));
} }

View File

@@ -2,7 +2,7 @@ use std::time::SystemTime;
use httpdate::HttpDate; use httpdate::HttpDate;
use super::{Header, HeaderName}; use super::{Header, HeaderName, HeaderValue};
use crate::BoxError; use crate::BoxError;
/// Message `Date` header /// Message `Date` header
@@ -32,29 +32,29 @@ impl Header for Date {
fn parse(s: &str) -> Result<Self, BoxError> { fn parse(s: &str) -> Result<Self, BoxError> {
let mut s = String::from(s); let mut s = String::from(s);
if s.ends_with(" -0000") { if s.ends_with("+0000") {
// The httpdate crate expects the `Date` to end in ` GMT`, but email // The httpdate crate expects the `Date` to end in ` GMT`, but email
// uses `-0000`, so we crudely fix this issue here. // uses `+0000` to indicate UTC, so we crudely fix this issue here.
s.truncate(s.len() - "-0000".len()); s.truncate(s.len() - "+0000".len());
s.push_str("GMT"); s.push_str("GMT");
} }
Ok(Self(s.parse::<HttpDate>()?)) Ok(Self(s.parse::<HttpDate>()?))
} }
fn display(&self) -> String { fn display(&self) -> HeaderValue {
let mut s = self.0.to_string(); let mut val = self.0.to_string();
if s.ends_with(" GMT") { if val.ends_with(" GMT") {
// The httpdate crate always appends ` GMT` to the end of the string, // The httpdate crate always appends ` GMT` to the end of the string,
// but this is considered an obsolete date format for email // but this is considered an obsolete date format for email
// https://tools.ietf.org/html/rfc2822#appendix-A.6.2, // https://tools.ietf.org/html/rfc2822#appendix-A.6.2,
// so we replace `GMT` with `-0000` // so we replace `GMT` with `+0000`
s.truncate(s.len() - "GMT".len()); val.truncate(val.len() - "GMT".len());
s.push_str("-0000"); val.push_str("+0000");
} }
s HeaderValue::dangerous_new_pre_encoded(Self::name(), val.clone(), val)
} }
} }
@@ -75,7 +75,7 @@ mod test {
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
use super::Date; use super::Date;
use crate::message::header::{HeaderName, Headers}; use crate::message::header::{HeaderName, HeaderValue, Headers};
#[test] #[test]
fn format_date() { fn format_date() {
@@ -88,7 +88,7 @@ mod test {
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n".to_string() "Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n".to_string()
); );
// Tue, 15 Nov 1994 08:12:32 GMT // Tue, 15 Nov 1994 08:12:32 GMT
@@ -98,7 +98,7 @@ mod test {
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
"Date: Tue, 15 Nov 1994 08:12:32 -0000\r\n" "Date: Tue, 15 Nov 1994 08:12:32 +0000\r\n"
); );
} }
@@ -106,10 +106,10 @@ mod test {
fn parse_date() { fn parse_date() {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Date"), HeaderName::new_from_ascii_str("Date"),
"Tue, 15 Nov 1994 08:12:31 -0000".to_string(), "Tue, 15 Nov 1994 08:12:31 +0000".to_string(),
); ));
assert_eq!( assert_eq!(
headers.get::<Date>(), headers.get::<Date>(),
@@ -118,10 +118,10 @@ mod test {
)) ))
); );
headers.insert_raw( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Date"), HeaderName::new_from_ascii_str("Date"),
"Tue, 15 Nov 1994 08:12:32 -0000".to_string(), "Tue, 15 Nov 1994 08:12:32 +0000".to_string(),
); ));
assert_eq!( assert_eq!(
headers.get::<Date>(), headers.get::<Date>(),

View File

@@ -1,4 +1,6 @@
use super::{Header, HeaderName}; use email_encoding::headers::EmailWriter;
use super::{Header, HeaderName, HeaderValue};
use crate::{ use crate::{
message::mailbox::{Mailbox, Mailboxes}, message::mailbox::{Mailbox, Mailboxes},
BoxError, BoxError,
@@ -25,8 +27,13 @@ macro_rules! mailbox_header {
Ok(Self(mailbox)) Ok(Self(mailbox))
} }
fn display(&self) -> String { fn display(&self) -> HeaderValue {
self.0.to_string() 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)
} }
} }
@@ -68,8 +75,13 @@ macro_rules! mailboxes_header {
Ok(Self(mailbox)) Ok(Self(mailbox))
} }
fn display(&self) -> String { fn display(&self) -> HeaderValue {
self.0.to_string() 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)
} }
} }
@@ -161,7 +173,7 @@ mailboxes_header! {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::{From, Mailbox, Mailboxes}; use super::{From, Mailbox, Mailboxes};
use crate::message::header::{HeaderName, Headers}; use crate::message::header::{HeaderName, HeaderValue, Headers};
#[test] #[test]
fn format_single_without_name() { fn format_single_without_name() {
@@ -175,12 +187,12 @@ mod test {
#[test] #[test]
fn format_single_with_name() { 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(); let mut headers = Headers::new();
headers.set(From(from)); headers.set(From(from));
assert_eq!(headers.to_string(), "From: K. <kayo@example.com>\r\n"); assert_eq!(headers.to_string(), "From: Kayo <kayo@example.com>\r\n");
} }
#[test] #[test]
@@ -201,7 +213,7 @@ mod test {
#[test] #[test]
fn format_multi_with_name() { fn format_multi_with_name() {
let from = vec![ let from = vec![
"K. <kayo@example.com>".parse().unwrap(), "Kayo <kayo@example.com>".parse().unwrap(),
"Pony P. <pony@domain.tld>".parse().unwrap(), "Pony P. <pony@domain.tld>".parse().unwrap(),
]; ];
@@ -210,7 +222,7 @@ mod test {
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
"From: K. <kayo@example.com>, Pony P. <pony@domain.tld>\r\n" "From: Kayo <kayo@example.com>, \"Pony P.\" <pony@domain.tld>\r\n"
); );
} }
@@ -232,10 +244,10 @@ mod test {
let from = vec!["kayo@example.com".parse().unwrap()].into(); let from = vec!["kayo@example.com".parse().unwrap()].into();
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"), HeaderName::new_from_ascii_str("From"),
"kayo@example.com".to_string(), "kayo@example.com".to_string(),
); ));
assert_eq!(headers.get::<From>(), Some(From(from))); assert_eq!(headers.get::<From>(), Some(From(from)));
} }
@@ -245,10 +257,10 @@ mod test {
let from = vec!["K. <kayo@example.com>".parse().unwrap()].into(); let from = vec!["K. <kayo@example.com>".parse().unwrap()].into();
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"), HeaderName::new_from_ascii_str("From"),
"K. <kayo@example.com>".to_string(), "K. <kayo@example.com>".to_string(),
); ));
assert_eq!(headers.get::<From>(), Some(From(from))); assert_eq!(headers.get::<From>(), Some(From(from)));
} }
@@ -261,10 +273,10 @@ mod test {
]; ];
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"), HeaderName::new_from_ascii_str("From"),
"kayo@example.com, pony@domain.tld".to_string(), "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())));
} }
@@ -277,11 +289,38 @@ mod test {
]; ];
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"), HeaderName::new_from_ascii_str("From"),
"K. <kayo@example.com>, Pony P. <pony@domain.tld>".to_string(), "K. <kayo@example.com>, Pony P. <pony@domain.tld>".to_string(),
); ));
assert_eq!(headers.get::<From>(), Some(From(from.into()))); assert_eq!(headers.get::<From>(), Some(From(from.into())));
} }
#[test]
fn parse_multi_with_name_containing_comma() {
let from: Vec<Mailbox> = vec![
"Test, test <1@example.com>".parse().unwrap(),
"Test2, test2 <2@example.com>".parse().unwrap(),
];
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"Test, test <1@example.com>, Test2, test2 <2@example.com>".to_string(),
));
assert_eq!(headers.get::<From>(), Some(From(from.into())));
}
#[test]
fn parse_multi_with_name_containing_comma_last_broken() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"Test, test <1@example.com>, Test2, test2".to_string(),
));
assert_eq!(headers.get::<From>(), None);
}
} }

View File

@@ -26,18 +26,21 @@ mod mailbox;
mod special; mod special;
mod textual; mod textual;
/// Represents an email header
///
/// Email header as defined in [RFC5322](https://datatracker.ietf.org/doc/html/rfc5322) and extensions.
pub trait Header: Clone { pub trait Header: Clone {
fn name() -> HeaderName; fn name() -> HeaderName;
fn parse(s: &str) -> Result<Self, BoxError>; fn parse(s: &str) -> Result<Self, BoxError>;
fn display(&self) -> String; fn display(&self) -> HeaderValue;
} }
/// A set of email headers /// A set of email headers
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct Headers { pub struct Headers {
headers: Vec<(HeaderName, String)>, headers: Vec<HeaderValue>,
} }
impl Headers { impl Headers {
@@ -65,13 +68,14 @@ impl Headers {
/// ///
/// Returns `None` if `Header` isn't present in `Headers`. /// Returns `None` if `Header` isn't present in `Headers`.
pub fn get<H: Header>(&self) -> Option<H> { pub fn get<H: Header>(&self) -> Option<H> {
self.get_raw(&H::name()).and_then(|raw| H::parse(raw).ok()) self.get_raw(&H::name())
.and_then(|raw_value| H::parse(raw_value).ok())
} }
/// Sets `Header` into `Headers`, overriding `Header` if it /// Sets `Header` into `Headers`, overriding `Header` if it
/// was already present in `Headers` /// was already present in `Headers`
pub fn set<H: Header>(&mut self, header: H) { pub fn set<H: Header>(&mut self, header: H) {
self.insert_raw(H::name(), header.display()); self.insert_raw(header.display());
} }
/// Remove `Header` from `Headers`, returning it /// Remove `Header` from `Headers`, returning it
@@ -79,7 +83,7 @@ impl Headers {
/// Returns `None` if `Header` isn't in `Headers`. /// Returns `None` if `Header` isn't in `Headers`.
pub fn remove<H: Header>(&mut self) -> Option<H> { pub fn remove<H: Header>(&mut self) -> Option<H> {
self.remove_raw(&H::name()) self.remove_raw(&H::name())
.and_then(|(_name, raw)| H::parse(&raw).ok()) .and_then(|value| H::parse(&value.raw_value).ok())
} }
/// Clears `Headers`, removing all headers from it /// Clears `Headers`, removing all headers from it
@@ -94,62 +98,46 @@ impl Headers {
/// ///
/// Returns `None` if `name` isn't present in `Headers`. /// Returns `None` if `name` isn't present in `Headers`.
pub fn get_raw(&self, name: &str) -> Option<&str> { pub fn get_raw(&self, name: &str) -> Option<&str> {
self.find_header(name).map(|(_name, value)| value) self.find_header(name).map(|value| value.raw_value.as_str())
} }
/// Inserts a raw header into `Headers`, overriding `value` if it /// Inserts a raw header into `Headers`, overriding `value` if it
/// was already present in `Headers`. /// was already present in `Headers`.
pub fn insert_raw(&mut self, name: HeaderName, value: String) { pub fn insert_raw(&mut self, value: HeaderValue) {
match self.find_header_mut(&name) { match self.find_header_mut(&value.name) {
Some((_, current_value)) => { Some(current_value) => {
*current_value = value; *current_value = value;
} }
None => { None => {
self.headers.push((name, value)); self.headers.push(value);
} }
} }
} }
/// Appends a raw header into `Headers`
///
/// If a header with a name of `name` is already present,
/// appends `, ` + `value` to it's current value.
pub fn append_raw(&mut self, name: HeaderName, value: String) {
match self.find_header_mut(&name) {
Some((_name, prev_value)) => {
prev_value.push_str(", ");
prev_value.push_str(&value);
}
None => self.headers.push((name, value)),
}
}
/// Remove a raw header from `Headers`, returning it /// Remove a raw header from `Headers`, returning it
/// ///
/// Returns `None` if `name` isn't present in `Headers`. /// Returns `None` if `name` isn't present in `Headers`.
pub fn remove_raw(&mut self, name: &str) -> Option<(HeaderName, String)> { pub fn remove_raw(&mut self, name: &str) -> Option<HeaderValue> {
self.find_header_index(name).map(|i| self.headers.remove(i)) self.find_header_index(name).map(|i| self.headers.remove(i))
} }
fn find_header(&self, name: &str) -> Option<(&HeaderName, &str)> { fn find_header(&self, name: &str) -> Option<&HeaderValue> {
self.headers self.headers
.iter() .iter()
.find(|&(name_, _value)| name.eq_ignore_ascii_case(name_)) .find(|value| name.eq_ignore_ascii_case(&value.name))
.map(|t| (&t.0, t.1.as_str()))
} }
fn find_header_mut(&mut self, name: &str) -> Option<(&HeaderName, &mut String)> { fn find_header_mut(&mut self, name: &str) -> Option<&mut HeaderValue> {
self.headers self.headers
.iter_mut() .iter_mut()
.find(|(name_, _value)| name.eq_ignore_ascii_case(name_)) .find(|value| name.eq_ignore_ascii_case(&value.name))
.map(|t| (&t.0, &mut t.1))
} }
fn find_header_index(&self, name: &str) -> Option<usize> { fn find_header_index(&self, name: &str) -> Option<usize> {
self.headers self.headers
.iter() .iter()
.enumerate() .enumerate()
.find(|&(_i, (name_, _value))| name.eq_ignore_ascii_case(name_)) .find(|(_i, value)| name.eq_ignore_ascii_case(&value.name))
.map(|(i, _)| i) .map(|(i, _)| i)
} }
} }
@@ -157,10 +145,10 @@ impl Headers {
impl Display for Headers { impl Display for Headers {
/// Formats `Headers`, ready to put them into an email /// Formats `Headers`, ready to put them into an email
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (name, value) in &self.headers { for value in &self.headers {
Display::fmt(name, f)?; f.write_str(&value.name)?;
f.write_str(": ")?; f.write_str(": ")?;
HeaderValueEncoder::encode(&name, &value, f)?; f.write_str(&value.encoded_value)?;
f.write_str("\r\n")?; f.write_str("\r\n")?;
} }
@@ -237,7 +225,7 @@ impl HeaderName {
impl Display for HeaderName { impl Display for HeaderName {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(&self) f.write_str(self)
} }
} }
@@ -287,6 +275,38 @@ impl PartialEq<HeaderName> for &str {
} }
} }
#[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_START_PREFIX: &str = "=?utf-8?b?";
const ENCODING_END_SUFFIX: &str = "?="; const ENCODING_END_SUFFIX: &str = "?=";
const MAX_LINE_LEN: usize = 76; const MAX_LINE_LEN: usize = 76;
@@ -298,7 +318,7 @@ struct HeaderValueEncoder {
} }
impl HeaderValueEncoder { impl HeaderValueEncoder {
fn encode(name: &str, value: &str, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn encode(name: &str, value: &str, f: &mut impl fmt::Write) -> fmt::Result {
let (words_iter, encoder) = Self::new(name, value); let (words_iter, encoder) = Self::new(name, value);
encoder.format(words_iter, f) encoder.format(words_iter, f)
} }
@@ -316,7 +336,7 @@ impl HeaderValueEncoder {
fn format( fn format(
mut self, mut self,
words_iter: WordsPlusFillIterator<'_>, words_iter: WordsPlusFillIterator<'_>,
f: &mut fmt::Formatter<'_>, f: &mut impl fmt::Write,
) -> fmt::Result { ) -> fmt::Result {
/// Estimate if an encoded string of `len` would fix in an empty line /// Estimate if an encoded string of `len` would fix in an empty line
fn would_fit_new_line(len: usize) -> bool { fn would_fit_new_line(len: usize) -> bool {
@@ -408,13 +428,14 @@ impl HeaderValueEncoder {
let mut next_word = next_word; let mut next_word = next_word;
while !next_word.is_empty() { while !next_word.is_empty() {
if self.remaining_line_len() <= base64_len(1) { 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.flush_encode_buf(f, false)?;
self.new_line(f)?; self.new_line(f)?;
} }
let mut len = available_len_to_max_encode_len(self.remaining_line_len())
.min(next_word.len());
// avoid slicing on a char boundary // avoid slicing on a char boundary
while !next_word.is_char_boundary(len) { while !next_word.is_char_boundary(len) {
len += 1; len += 1;
@@ -444,11 +465,9 @@ impl HeaderValueEncoder {
fn flush_encode_buf( fn flush_encode_buf(
&mut self, &mut self,
f: &mut fmt::Formatter<'_>, f: &mut impl fmt::Write,
switching_to_allowed: bool, switching_to_allowed: bool,
) -> fmt::Result { ) -> fmt::Result {
use std::fmt::Write;
if self.encode_buf.is_empty() { if self.encode_buf.is_empty() {
// nothing to encode // nothing to encode
return Ok(()); return Ok(());
@@ -473,7 +492,7 @@ impl HeaderValueEncoder {
self.encode_buf.as_bytes(), self.encode_buf.as_bytes(),
base64::STANDARD, base64::STANDARD,
); );
Display::fmt(&encoded, f)?; write!(f, "{}", encoded)?;
f.write_str(ENCODING_END_SUFFIX)?; f.write_str(ENCODING_END_SUFFIX)?;
self.line_len += ENCODING_START_PREFIX.len(); self.line_len += ENCODING_START_PREFIX.len();
@@ -489,7 +508,7 @@ impl HeaderValueEncoder {
Ok(()) Ok(())
} }
fn new_line(&mut self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn new_line(&mut self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str("\r\n ")?; f.write_str("\r\n ")?;
self.line_len = 1; self.line_len = 1;
@@ -519,7 +538,7 @@ impl<'a> Iterator for WordsPlusFillIterator<'a> {
.find(|&(_i, c)| !is_space_like(c)) .find(|&(_i, c)| !is_space_like(c))
.map(|(i, _)| i); .map(|(i, _)| i);
let word = &self.s[..next_word.unwrap_or_else(|| self.s.len())]; let word = &self.s[..next_word.unwrap_or(self.s.len())];
self.s = &self.s[word.len()..]; self.s = &self.s[word.len()..];
Some(word) Some(word)
} }
@@ -542,7 +561,7 @@ const fn allowed_char(c: char) -> bool {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{HeaderName, Headers}; use super::{HeaderName, HeaderValue, Headers};
#[test] #[test]
fn valid_headername() { fn valid_headername() {
@@ -603,10 +622,10 @@ mod tests {
#[test] #[test]
fn format_ascii() { fn format_ascii() {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"), HeaderName::new_from_ascii_str("To"),
"John Doe <example@example.com>, Jean Dupont <jean@example.com>".to_string(), "John Doe <example@example.com>, Jean Dupont <jean@example.com>".to_string(),
); ));
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
@@ -617,10 +636,10 @@ mod tests {
#[test] #[test]
fn format_ascii_with_folding() { fn format_ascii_with_folding() {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"), 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(), "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!( assert_eq!(
headers.to_string(), headers.to_string(),
@@ -635,10 +654,10 @@ mod tests {
#[test] #[test]
fn format_ascii_with_folding_long_line() { fn format_ascii_with_folding_long_line() {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"), HeaderName::new_from_ascii_str("Subject"),
"Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_string() "Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_string()
); ));
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
@@ -654,9 +673,10 @@ mod tests {
fn format_ascii_with_folding_very_long_line() { fn format_ascii_with_folding_very_long_line() {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw( headers.insert_raw(
HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"), HeaderName::new_from_ascii_str("Subject"),
"Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know".to_string() "Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know".to_string()
); ));
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
@@ -670,10 +690,10 @@ mod tests {
#[test] #[test]
fn format_ascii_with_folding_giant_word() { fn format_ascii_with_folding_giant_word() {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"), HeaderName::new_from_ascii_str("Subject"),
"1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz".to_string() "1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz".to_string()
); ));
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
@@ -688,10 +708,10 @@ mod tests {
#[test] #[test]
fn format_special() { fn format_special() {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"), HeaderName::new_from_ascii_str("To"),
"Seán <sean@example.com>".to_string(), "Seán <sean@example.com>".to_string(),
); ));
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
@@ -702,10 +722,10 @@ mod tests {
#[test] #[test]
fn format_special_emoji() { fn format_special_emoji() {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"), HeaderName::new_from_ascii_str("To"),
"🌎 <world@example.com>".to_string(), "🌎 <world@example.com>".to_string(),
); ));
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
@@ -716,10 +736,10 @@ mod tests {
#[test] #[test]
fn format_special_with_folding() { fn format_special_with_folding() {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"), 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(), "🌍 <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!( assert_eq!(
headers.to_string(), headers.to_string(),
@@ -737,8 +757,9 @@ mod tests {
fn format_slice_on_char_boundary_bug() { fn format_slice_on_char_boundary_bug() {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw( headers.insert_raw(
HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"), HeaderName::new_from_ascii_str("Subject"),
"🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳".to_string(), "🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳".to_string(),)
); );
assert_eq!( assert_eq!(
@@ -750,10 +771,10 @@ mod tests {
#[test] #[test]
fn format_bad_stuff() { fn format_bad_stuff() {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"), HeaderName::new_from_ascii_str("Subject"),
"Hello! \r\n This is \" bad \0. 👋".to_string(), "Hello! \r\n This is \" bad \0. 👋".to_string(),
); ));
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
@@ -765,21 +786,25 @@ mod tests {
fn format_everything() { fn format_everything() {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw( headers.insert_raw(
HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"), HeaderName::new_from_ascii_str("Subject"),
"Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_string() "Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_string()
)
); );
headers.insert_raw( headers.insert_raw(
HeaderValue::new(
HeaderName::new_from_ascii_str("To"), 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(), "🌍 <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( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"), HeaderName::new_from_ascii_str("From"),
"Someone <somewhere@example.com>".to_string(), "Someone <somewhere@example.com>".to_string(),
); ));
headers.insert_raw( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"), HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
"quoted-printable".to_string(), "quoted-printable".to_string(),
); ));
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
@@ -797,4 +822,21 @@ mod tests {
) )
); );
} }
#[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,5 +1,5 @@
use crate::{ use crate::{
message::header::{Header, HeaderName}, message::header::{Header, HeaderName, HeaderValue},
BoxError, BoxError,
}; };
@@ -10,6 +10,9 @@ pub struct MimeVersion {
minor: u8, minor: u8,
} }
/// MIME version 1.0
///
/// Should be used in all MIME messages.
pub const MIME_VERSION_1_0: MimeVersion = MimeVersion::new(1, 0); pub const MIME_VERSION_1_0: MimeVersion = MimeVersion::new(1, 0);
impl MimeVersion { impl MimeVersion {
@@ -47,8 +50,9 @@ impl Header for MimeVersion {
Ok(MimeVersion::new(major, minor)) Ok(MimeVersion::new(major, minor))
} }
fn display(&self) -> String { fn display(&self) -> HeaderValue {
format!("{}.{}", self.major, self.minor) let val = format!("{}.{}", self.major, self.minor);
HeaderValue::dangerous_new_pre_encoded(Self::name(), val.clone(), val)
} }
} }
@@ -61,7 +65,7 @@ impl Default for MimeVersion {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::{MimeVersion, MIME_VERSION_1_0}; use super::{MimeVersion, MIME_VERSION_1_0};
use crate::message::header::{HeaderName, Headers}; use crate::message::header::{HeaderName, HeaderValue, Headers};
#[test] #[test]
fn format_mime_version() { fn format_mime_version() {
@@ -80,17 +84,17 @@ mod test {
fn parse_mime_version() { fn parse_mime_version() {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("MIME-Version"), HeaderName::new_from_ascii_str("MIME-Version"),
"1.0".to_string(), "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.insert_raw( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("MIME-Version"), HeaderName::new_from_ascii_str("MIME-Version"),
"0.1".to_string(), "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,4 +1,4 @@
use super::{Header, HeaderName}; use super::{Header, HeaderName, HeaderValue};
use crate::BoxError; use crate::BoxError;
macro_rules! text_header { macro_rules! text_header {
@@ -16,8 +16,8 @@ macro_rules! text_header {
Ok(Self(s.into())) Ok(Self(s.into()))
} }
fn display(&self) -> String { fn display(&self) -> HeaderValue {
self.0.clone() HeaderValue::new(Self::name(), self.0.clone())
} }
} }
@@ -86,7 +86,7 @@ text_header! {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::Subject; use super::Subject;
use crate::message::header::{HeaderName, Headers}; use crate::message::header::{HeaderName, HeaderValue, Headers};
#[test] #[test]
fn format_ascii() { fn format_ascii() {
@@ -110,10 +110,10 @@ mod test {
#[test] #[test]
fn parse_ascii() { fn parse_ascii() {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"), HeaderName::new_from_ascii_str("Subject"),
"Sample subject".to_string(), "Sample subject".to_string(),
); ));
assert_eq!( assert_eq!(
headers.get::<Subject>(), headers.get::<Subject>(),

View File

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

View File

@@ -1,11 +1,14 @@
use crate::address::{Address, AddressError};
use std::{ use std::{
convert::TryFrom,
fmt::{Display, Formatter, Result as FmtResult, Write}, fmt::{Display, Formatter, Result as FmtResult, Write},
mem,
slice::Iter, slice::Iter,
str::FromStr, str::FromStr,
}; };
use email_encoding::headers::EmailWriter;
use crate::address::{Address, AddressError};
/// Represents an email address with an optional name for the sender/recipient. /// 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_). /// This type contains email address and the sender/recipient name (_Some Name \<user@domain.tld\>_ or _withoutname@domain.tld_).
@@ -63,6 +66,22 @@ impl Mailbox {
pub fn new(name: Option<String>, email: Address) -> Self { pub fn new(name: Option<String>, email: Address) -> Self {
Mailbox { name, email } Mailbox { name, email }
} }
pub(crate) fn encode(&self, w: &mut EmailWriter<'_>) -> FmtResult {
if let Some(name) = &self.name {
email_encoding::headers::quoted_string::encode(name, w)?;
w.space();
w.write_char('<')?;
}
w.write_str(self.email.as_ref())?;
if self.name.is_some() {
w.write_char('>')?;
}
Ok(())
}
} }
impl Display for Mailbox { impl Display for Mailbox {
@@ -70,7 +89,7 @@ impl Display for Mailbox {
if let Some(ref name) = self.name { if let Some(ref name) = self.name {
let name = name.trim(); let name = name.trim();
if !name.is_empty() { if !name.is_empty() {
f.write_str(&name)?; write_word(f, name)?;
f.write_str(" <")?; f.write_str(" <")?;
self.email.fmt(f)?; self.email.fmt(f)?;
return f.write_char('>'); return f.write_char('>');
@@ -250,6 +269,20 @@ impl Mailboxes {
pub fn iter(&self) -> Iter<'_, Mailbox> { pub fn iter(&self) -> Iter<'_, Mailbox> {
self.0.iter() 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 { impl Default for Mailboxes {
@@ -319,19 +352,127 @@ impl Display for Mailboxes {
impl FromStr for Mailboxes { impl FromStr for Mailboxes {
type Err = AddressError; type Err = AddressError;
fn from_str(src: &str) -> Result<Self, Self::Err> { fn from_str(mut src: &str) -> Result<Self, Self::Err> {
src.split(',') let mut mailboxes = Vec::new();
.map(|m| m.trim().parse())
.collect::<Result<Vec<_>, _>>() if !src.is_empty() {
.map(Mailboxes) // n-1 elements
let mut skip = 0;
while let Some(i) = src[skip..].find(',') {
let left = &src[..skip + i];
match left.trim().parse() {
Ok(mailbox) => {
mailboxes.push(mailbox);
src = &src[left.len() + ",".len()..];
skip = 0;
}
Err(AddressError::MissingParts) => {
skip = left.len() + ",".len();
}
Err(err) => {
return Err(err);
}
}
}
// last element
let mailbox = src.trim().parse()?;
mailboxes.push(mailbox);
}
Ok(Mailboxes(mailboxes))
} }
} }
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.6
fn write_word(f: &mut Formatter<'_>, s: &str) -> FmtResult {
if s.as_bytes().iter().copied().all(is_valid_atom_char) {
f.write_str(s)
} else {
// Quoted string: https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.5
f.write_char('"')?;
for &c in s.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)] #[cfg(test)]
mod test { mod test {
use super::Mailbox;
use std::convert::TryInto; use std::convert::TryInto;
use super::Mailbox;
#[test] #[test]
fn mailbox_format_address_only() { fn mailbox_format_address_only() {
assert_eq!( assert_eq!(
@@ -350,7 +491,35 @@ mod test {
"{}", "{}",
Mailbox::new(Some("K.".into()), "kayo@example.com".parse().unwrap()) 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>"#
); );
} }
@@ -372,7 +541,7 @@ mod test {
"{}", "{}",
Mailbox::new(Some(" K. ".into()), "kayo@example.com".parse().unwrap()) Mailbox::new(Some(" K. ".into()), "kayo@example.com".parse().unwrap())
), ),
"K. <kayo@example.com>" "\"K.\" <kayo@example.com>"
); );
} }

View File

@@ -1,11 +1,11 @@
use std::io::Write; use std::{io::Write, iter::repeat_with};
use mime::Mime;
use crate::message::{ use crate::message::{
header::{self, ContentTransferEncoding, ContentType, Header, Headers}, header::{self, ContentTransferEncoding, ContentType, Header, Headers},
EmailFormat, IntoBody, EmailFormat, IntoBody,
}; };
use mime::Mime;
use std::iter::repeat_with;
/// MIME part variants /// MIME part variants
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@@ -4,15 +4,8 @@
//! //!
//! This section demonstrates how to build messages. //! 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> //! <style>
//! summary, details:not([open]) { cursor: pointer; } //! summary, details:not([open]) { cursor: pointer; }
//! summary { display: list-item; }
//! summary::marker { content: '▶ '; }
//! details[open] summary::marker { content: '▼ '; }
//! </style> //! </style>
//! //!
//! //!
@@ -113,9 +106,10 @@
//! //!
//! ```rust //! ```rust
//! # use std::error::Error; //! # use std::error::Error;
//! use lettre::message::{header, Attachment, Body, Message, MultiPart, SinglePart};
//! use std::fs; //! use std::fs;
//! //!
//! use lettre::message::{header, Attachment, Body, Message, MultiPart, SinglePart};
//!
//! # fn main() -> Result<(), Box<dyn Error>> { //! # fn main() -> Result<(), Box<dyn Error>> {
//! let image = fs::read("docs/lettre.png")?; //! let image = fs::read("docs/lettre.png")?;
//! // this image_body can be cloned and reused between emails. //! // this image_body can be cloned and reused between emails.
@@ -202,15 +196,19 @@
//! ``` //! ```
//! </details> //! </details>
use std::{convert::TryFrom, io::Write, iter, time::SystemTime}; use std::{io::Write, iter, time::SystemTime};
pub use attachment::Attachment; pub use attachment::Attachment;
pub use body::{Body, IntoBody, MaybeString}; pub use body::{Body, IntoBody, MaybeString};
#[cfg(feature = "dkim")]
pub use dkim::*;
pub use mailbox::*; pub use mailbox::*;
pub use mimebody::*; pub use mimebody::*;
mod attachment; mod attachment;
mod body; mod body;
#[cfg(feature = "dkim")]
pub mod dkim;
pub mod header; pub mod header;
mod mailbox; mod mailbox;
mod mimebody; mod mimebody;
@@ -496,6 +494,77 @@ impl Message {
self.format(&mut out); self.format(&mut out);
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 { impl EmailFormat for Message {
@@ -507,7 +576,7 @@ impl EmailFormat for Message {
MessageBody::Mime(p) => p.format(out), MessageBody::Mime(p) => p.format(out),
MessageBody::Raw(r) => { MessageBody::Raw(r) => {
out.extend_from_slice(b"\r\n"); out.extend_from_slice(b"\r\n");
out.extend_from_slice(&r) out.extend_from_slice(r)
} }
} }
} }
@@ -581,9 +650,9 @@ mod test {
assert_eq!( assert_eq!(
String::from_utf8(email.formatted()).unwrap(), String::from_utf8(email.formatted()).unwrap(),
concat!( concat!(
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n", "Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\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", "Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n",
"Content-Transfer-Encoding: 7bit\r\n", "Content-Transfer-Encoding: 7bit\r\n",
"\r\n", "\r\n",

View File

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

View File

@@ -9,9 +9,10 @@
//! # //! #
//! # #[cfg(all(feature = "file-transport", feature = "builder"))] //! # #[cfg(all(feature = "file-transport", feature = "builder"))]
//! # fn main() -> Result<(), Box<dyn Error>> { //! # fn main() -> Result<(), Box<dyn Error>> {
//! use lettre::{FileTransport, Message, Transport};
//! use std::env::temp_dir; //! use std::env::temp_dir;
//! //!
//! use lettre::{FileTransport, Message, Transport};
//!
//! // Write to the local temp directory //! // Write to the local temp directory
//! let sender = FileTransport::new(temp_dir()); //! let sender = FileTransport::new(temp_dir());
//! let email = Message::builder() //! let email = Message::builder()
@@ -41,9 +42,10 @@
//! # //! #
//! # #[cfg(all(feature = "file-transport-envelope", feature = "builder"))] //! # #[cfg(all(feature = "file-transport-envelope", feature = "builder"))]
//! # fn main() -> Result<(), Box<dyn Error>> { //! # fn main() -> Result<(), Box<dyn Error>> {
//! use lettre::{FileTransport, Message, Transport};
//! use std::env::temp_dir; //! use std::env::temp_dir;
//! //!
//! use lettre::{FileTransport, Message, Transport};
//!
//! // Write to the local temp directory //! // Write to the local temp directory
//! let sender = FileTransport::with_envelope(temp_dir()); //! let sender = FileTransport::with_envelope(temp_dir());
//! let email = Message::builder() //! let email = Message::builder()
@@ -70,7 +72,8 @@
//! # #[cfg(all(feature = "tokio1", feature = "file-transport", feature = "builder"))] //! # #[cfg(all(feature = "tokio1", feature = "file-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> { //! # async fn run() -> Result<(), Box<dyn Error>> {
//! use std::env::temp_dir; //! use std::env::temp_dir;
//! use lettre::{AsyncTransport, Tokio1Executor, Message, AsyncFileTransport}; //!
//! use lettre::{AsyncFileTransport, AsyncTransport, Message, Tokio1Executor};
//! //!
//! // Write to the local temp directory //! // Write to the local temp directory
//! let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir()); //! let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir());
@@ -95,7 +98,8 @@
//! # #[cfg(all(feature = "async-std1", feature = "file-transport", feature = "builder"))] //! # #[cfg(all(feature = "async-std1", feature = "file-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> { //! # async fn run() -> Result<(), Box<dyn Error>> {
//! use std::env::temp_dir; //! use std::env::temp_dir;
//! use lettre::{AsyncTransport, AsyncStd1Executor, Message, AsyncFileTransport}; //!
//! use lettre::{AsyncFileTransport, AsyncStd1Executor, AsyncTransport, Message};
//! //!
//! // Write to the local temp directory //! // Write to the local temp directory
//! let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir()); //! let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir());
@@ -132,20 +136,22 @@
//! {"forward_path":["hei@domain.tld"],"reverse_path":"nobody@domain.tld"} //! {"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 = "tokio1"))]
use crate::{AsyncTransport, Executor};
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
use async_trait::async_trait;
#[cfg(any(feature = "async-std1", feature = "tokio1"))] #[cfg(any(feature = "async-std1", feature = "tokio1"))]
use std::marker::PhantomData; use std::marker::PhantomData;
use std::{ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
str, str,
}; };
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
use async_trait::async_trait;
use uuid::Uuid; 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; mod error;
type Id = String; type Id = String;

View File

@@ -56,8 +56,7 @@
//! # //! #
//! # #[cfg(all(feature = "builder", feature = "smtp-transport"))] //! # #[cfg(all(feature = "builder", feature = "smtp-transport"))]
//! # fn main() -> Result<(), Box<dyn Error>> { //! # fn main() -> Result<(), Box<dyn Error>> {
//! use lettre::transport::smtp::authentication::Credentials; //! use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
//! use lettre::{Message, SmtpTransport, Transport};
//! //!
//! let email = Message::builder() //! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?) //! .from("NoBody <nobody@domain.tld>".parse()?)
@@ -152,7 +151,7 @@ pub trait AsyncTransport {
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> { async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
let raw = message.formatted(); let raw = message.formatted();
let envelope = message.envelope(); 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>; 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 //! Error and result type for sendmail transport
use crate::BoxError;
use std::{error::Error as StdError, fmt}; use std::{error::Error as StdError, fmt};
use crate::BoxError;
/// The Errors that may occur when sending an email over sendmail /// The Errors that may occur when sending an email over sendmail
pub struct Error { pub struct Error {
inner: Box<Inner>, inner: Box<Inner>,

View File

@@ -33,7 +33,9 @@
//! # //! #
//! # #[cfg(all(feature = "tokio1", feature = "sendmail-transport", feature = "builder"))] //! # #[cfg(all(feature = "tokio1", feature = "sendmail-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> { //! # 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() //! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?) //! .from("NoBody <nobody@domain.tld>".parse()?)
@@ -72,16 +74,6 @@
//! # } //! # }
//! ``` //! ```
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};
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
use async_trait::async_trait;
#[cfg(any(feature = "async-std1", feature = "tokio1"))] #[cfg(any(feature = "async-std1", feature = "tokio1"))]
use std::marker::PhantomData; use std::marker::PhantomData;
use std::{ use std::{
@@ -90,9 +82,21 @@ use std::{
process::{Command, Stdio}, 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; mod error;
const DEFAULT_SENDMAIL: &str = "/usr/sbin/sendmail"; const DEFAULT_SENDMAIL: &str = "sendmail";
/// Sends emails using the `sendmail` command /// Sends emails using the `sendmail` command
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -113,7 +117,10 @@ pub struct AsyncSendmailTransport<E: Executor> {
} }
impl SendmailTransport { 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 { pub fn new() -> SendmailTransport {
SendmailTransport { SendmailTransport {
command: DEFAULT_SENDMAIL.into(), command: DEFAULT_SENDMAIL.into(),
@@ -147,7 +154,10 @@ impl<E> AsyncSendmailTransport<E>
where where
E: Executor, 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 { pub fn new() -> Self {
Self { Self {
inner: SendmailTransport::new(), inner: SendmailTransport::new(),
@@ -260,7 +270,7 @@ impl AsyncTransport for AsyncSendmailTransport<AsyncStd1Executor> {
.stdin .stdin
.as_mut() .as_mut()
.unwrap() .unwrap()
.write_all(&email) .write_all(email)
.await .await
.map_err(error::client)?; .map_err(error::client)?;
let output = process.output().await.map_err(error::client)?; let output = process.output().await.map_err(error::client)?;
@@ -292,7 +302,7 @@ impl AsyncTransport for AsyncSendmailTransport<Tokio1Executor> {
.stdin .stdin
.as_mut() .as_mut()
.unwrap() .unwrap()
.write_all(&email) .write_all(email)
.await .await
.map_err(error::client)?; .map_err(error::client)?;
let output = process.wait_with_output().await.map_err(error::client)?; let output = process.wait_with_output().await.map_err(error::client)?;

View File

@@ -1,10 +1,16 @@
use std::{ use std::{
fmt::{self, Debug}, fmt::{self, Debug},
marker::PhantomData, marker::PhantomData,
sync::Arc,
time::Duration,
}; };
use async_trait::async_trait; use async_trait::async_trait;
#[cfg(feature = "pool")]
use super::pool::async_impl::Pool;
#[cfg(feature = "pool")]
use super::PoolConfig;
use super::{ use super::{
client::AsyncSmtpConnection, ClientId, Credentials, Error, Mechanism, Response, SmtpInfo, client::AsyncSmtpConnection, ClientId, Credentials, Error, Mechanism, Response, SmtpInfo,
}; };
@@ -18,8 +24,10 @@ use crate::{Envelope, Executor};
/// Asynchronously sends emails using the SMTP protocol /// Asynchronously sends emails using the SMTP protocol
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))] #[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
pub struct AsyncSmtpTransport<E> { pub struct AsyncSmtpTransport<E: Executor> {
// TODO: pool #[cfg(feature = "pool")]
inner: Arc<Pool<E>>,
#[cfg(not(feature = "pool"))]
inner: AsyncSmtpClient<E>, inner: AsyncSmtpClient<E>,
} }
@@ -35,6 +43,7 @@ impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> {
let result = conn.send(envelope, email).await?; let result = conn.send(envelope, email).await?;
#[cfg(not(feature = "pool"))]
conn.quit().await?; conn.quit().await?;
Ok(result) Ok(result)
@@ -141,21 +150,41 @@ where
/// ///
/// * No authentication /// * No authentication
/// * No TLS /// * No TLS
/// * A 60 seconds timeout for smtp commands
/// * Port 25 /// * Port 25
/// ///
/// Consider using [`AsyncSmtpTransport::relay`](#method.relay) or /// Consider using [`AsyncSmtpTransport::relay`](#method.relay) or
/// [`AsyncSmtpTransport::starttls_relay`](#method.starttls_relay) instead, /// [`AsyncSmtpTransport::starttls_relay`](#method.starttls_relay) instead,
/// if possible. /// if possible.
pub fn builder_dangerous<T: Into<String>>(server: T) -> AsyncSmtpTransportBuilder { pub fn builder_dangerous<T: Into<String>>(server: T) -> AsyncSmtpTransportBuilder {
let new = SmtpInfo { let info = SmtpInfo {
server: server.into(), server: server.into(),
..Default::default() ..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 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut builder = f.debug_struct("AsyncSmtpTransport"); let mut builder = f.debug_struct("AsyncSmtpTransport");
builder.field("inner", &self.inner); builder.field("inner", &self.inner);
@@ -180,6 +209,8 @@ where
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))] #[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
pub struct AsyncSmtpTransportBuilder { pub struct AsyncSmtpTransportBuilder {
info: SmtpInfo, info: SmtpInfo,
#[cfg(feature = "pool")]
pool_config: PoolConfig,
} }
/// Builder for the SMTP `AsyncSmtpTransport` /// Builder for the SMTP `AsyncSmtpTransport`
@@ -208,6 +239,12 @@ impl AsyncSmtpTransportBuilder {
self 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 /// Set the TLS settings to use
#[cfg(any( #[cfg(any(
feature = "tokio1-native-tls", feature = "tokio1-native-tls",
@@ -228,6 +265,16 @@ impl AsyncSmtpTransportBuilder {
self 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 /// Build the transport
pub fn build<E>(self) -> AsyncSmtpTransport<E> pub fn build<E>(self) -> AsyncSmtpTransport<E>
where where
@@ -237,6 +284,10 @@ impl AsyncSmtpTransportBuilder {
info: self.info, info: self.info,
marker_: PhantomData, marker_: PhantomData,
}; };
#[cfg(feature = "pool")]
let client = Pool::new(self.pool_config, client);
AsyncSmtpTransport { inner: client } AsyncSmtpTransport { inner: client }
} }
} }
@@ -258,13 +309,14 @@ where
let mut conn = E::connect( let mut conn = E::connect(
&self.info.server, &self.info.server,
self.info.port, self.info.port,
self.info.timeout,
&self.info.hello_name, &self.info.hello_name,
&self.info.tls, &self.info.tls,
) )
.await?; .await?;
if let Some(credentials) = &self.info.credentials { if let Some(credentials) = &self.info.credentials {
conn.auth(&self.info.authentication, &credentials).await?; conn.auth(&self.info.authentication, credentials).await?;
} }
Ok(conn) Ok(conn)
} }
@@ -278,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> impl<E> AsyncSmtpClient<E>
where where
E: Executor, E: Executor,

View File

@@ -1,9 +1,11 @@
//! Provides limited SASL authentication mechanisms //! Provides limited SASL authentication mechanisms
use crate::transport::smtp::error::{self, Error};
use std::fmt::{self, Debug, Display, Formatter}; use std::fmt::{self, Debug, Display, Formatter};
use crate::transport::smtp::error::{self, Error};
/// Accepted authentication mechanisms /// Accepted authentication mechanisms
///
/// Trying LOGIN last as it is deprecated. /// Trying LOGIN last as it is deprecated.
pub const DEFAULT_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login]; 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 super::{AsyncNetworkStream, ClientCodec, TlsParameters};
use crate::{ use crate::{
transport::smtp::{ transport::smtp::{
@@ -10,11 +16,6 @@ use crate::{
}, },
Envelope, Envelope,
}; };
use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use std::fmt::Display;
#[cfg(feature = "tracing")]
use super::escape_crlf;
macro_rules! try_smtp ( macro_rules! try_smtp (
($err: expr, $client: ident) => ({ ($err: expr, $client: ident) => ({
@@ -48,13 +49,13 @@ impl AsyncSmtpConnection {
/// ///
/// Sends EHLO and parses server information /// Sends EHLO and parses server information
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
pub async fn connect_tokio1( pub async fn connect_tokio1<T: tokio1_crate::net::ToSocketAddrs>(
hostname: &str, server: T,
port: u16, timeout: Option<Duration>,
hello_name: &ClientId, hello_name: &ClientId,
tls_parameters: Option<TlsParameters>, tls_parameters: Option<TlsParameters>,
) -> Result<AsyncSmtpConnection, Error> { ) -> 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 Self::connect_impl(stream, hello_name).await
} }
@@ -62,13 +63,13 @@ impl AsyncSmtpConnection {
/// ///
/// Sends EHLO and parses server information /// Sends EHLO and parses server information
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
pub async fn connect_asyncstd1( pub async fn connect_asyncstd1<T: async_std::net::ToSocketAddrs>(
hostname: &str, server: T,
port: u16, timeout: Option<Duration>,
hello_name: &ClientId, hello_name: &ClientId,
tls_parameters: Option<TlsParameters>, tls_parameters: Option<TlsParameters>,
) -> Result<AsyncSmtpConnection, Error> { ) -> 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 Self::connect_impl(stream, hello_name).await
} }
@@ -161,10 +162,7 @@ impl AsyncSmtpConnection {
) -> Result<(), Error> { ) -> Result<(), Error> {
if self.server_info.supports_feature(Extension::StartTls) { if self.server_info.supports_feature(Extension::StartTls) {
try_smtp!(self.command(Starttls).await, self); try_smtp!(self.command(Starttls).await, self);
try_smtp!( self.stream.get_mut().upgrade_tls(tls_parameters).await?;
self.stream.get_mut().upgrade_tls(tls_parameters).await,
self
);
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("connection encrypted"); tracing::debug!("connection encrypted");
// Send EHLO again // Send EHLO again
@@ -298,7 +296,10 @@ impl AsyncSmtpConnection {
return if response.is_positive() { return if response.is_positive() {
Ok(response) Ok(response)
} else { } else {
Err(error::code(response.code)) Err(error::code(
response.code(),
response.first_line().map(|s| s.to_owned()),
))
} }
} }
Err(nom::Err::Failure(e)) => { Err(nom::Err::Failure(e)) => {
@@ -313,4 +314,10 @@ impl AsyncSmtpConnection {
Err(error::response("incomplete response")) 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,29 +1,27 @@
use std::{ use std::{
mem, io, mem,
net::SocketAddr, net::SocketAddr,
pin::Pin, pin::Pin,
task::{Context, Poll}, 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::{ use futures_io::{
AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Error as IoError, ErrorKind, AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Error as IoError, ErrorKind,
Result as IoResult, Result as IoResult,
}; };
#[cfg(feature = "async-std1-rustls-tls")]
use futures_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
use tokio1_crate::io::{AsyncRead as _, AsyncWrite as _, ReadBuf as Tokio1ReadBuf}; use tokio1_crate::io::{AsyncRead as _, AsyncWrite as _, ReadBuf as Tokio1ReadBuf};
#[cfg(feature = "async-std1")]
use async_std::net::TcpStream as AsyncStd1TcpStream;
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
use tokio1_crate::net::TcpStream as Tokio1TcpStream; use tokio1_crate::net::{TcpStream as Tokio1TcpStream, ToSocketAddrs as Tokio1ToSocketAddrs};
#[cfg(feature = "async-std1-native-tls")]
use async_native_tls::TlsStream as AsyncStd1TlsStream;
#[cfg(feature = "tokio1-native-tls")] #[cfg(feature = "tokio1-native-tls")]
use tokio1_native_tls_crate::TlsStream as Tokio1TlsStream; use tokio1_native_tls_crate::TlsStream as Tokio1TlsStream;
#[cfg(feature = "async-std1-rustls-tls")]
use async_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
#[cfg(feature = "tokio1-rustls-tls")] #[cfg(feature = "tokio1-rustls-tls")]
use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream; use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
@@ -107,14 +105,47 @@ impl AsyncNetworkStream {
} }
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
pub async fn connect_tokio1( pub async fn connect_tokio1<T: Tokio1ToSocketAddrs>(
hostname: &str, server: T,
port: u16, timeout: Option<Duration>,
tls_parameters: Option<TlsParameters>, tls_parameters: Option<TlsParameters>,
) -> Result<AsyncNetworkStream, Error> { ) -> Result<AsyncNetworkStream, Error> {
let tcp_stream = Tokio1TcpStream::connect((hostname, port)) async fn try_connect_timeout<T: Tokio1ToSocketAddrs>(
.await server: T,
.map_err(error::connection)?; 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)); let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters { if let Some(tls_parameters) = tls_parameters {
@@ -124,14 +155,45 @@ impl AsyncNetworkStream {
} }
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
pub async fn connect_asyncstd1( pub async fn connect_asyncstd1<T: AsyncStd1ToSocketAddrs>(
hostname: &str, server: T,
port: u16, timeout: Option<Duration>,
tls_parameters: Option<TlsParameters>, tls_parameters: Option<TlsParameters>,
) -> Result<AsyncNetworkStream, Error> { ) -> Result<AsyncNetworkStream, Error> {
let tcp_stream = AsyncStd1TcpStream::connect((hostname, port)) async fn try_connect_timeout<T: AsyncStd1ToSocketAddrs>(
.await server: T,
.map_err(error::connection)?; 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)); let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters { if let Some(tls_parameters) = tls_parameters {
@@ -196,9 +258,9 @@ impl AsyncNetworkStream {
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))] #[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
async fn upgrade_tokio1_tls( async fn upgrade_tokio1_tls(
tcp_stream: Tokio1TcpStream, tcp_stream: Tokio1TcpStream,
mut tls_parameters: TlsParameters, tls_parameters: TlsParameters,
) -> Result<InnerAsyncNetworkStream, Error> { ) -> Result<InnerAsyncNetworkStream, Error> {
let domain = mem::take(&mut tls_parameters.domain); let domain = tls_parameters.domain().to_string();
match tls_parameters.connector { match tls_parameters.connector {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
@@ -225,10 +287,11 @@ impl AsyncNetworkStream {
#[cfg(feature = "tokio1-rustls-tls")] #[cfg(feature = "tokio1-rustls-tls")]
return { return {
use tokio1_rustls::{webpki::DNSNameRef, TlsConnector}; use rustls::ServerName;
use tokio1_rustls::TlsConnector;
let domain = let domain = ServerName::try_from(domain.as_str())
DNSNameRef::try_from_ascii_str(&domain).map_err(error::connection)?; .map_err(|_| error::connection("domain isn't a valid DNS name"))?;
let connector = TlsConnector::from(config); let connector = TlsConnector::from(config);
let stream = connector let stream = connector
@@ -277,10 +340,11 @@ impl AsyncNetworkStream {
#[cfg(feature = "async-std1-rustls-tls")] #[cfg(feature = "async-std1-rustls-tls")]
return { return {
use async_rustls::{webpki::DNSNameRef, TlsConnector}; use futures_rustls::TlsConnector;
use rustls::ServerName;
let domain = let domain = ServerName::try_from(domain.as_str())
DNSNameRef::try_from_ascii_str(&domain).map_err(error::connection)?; .map_err(|_| error::connection("domain isn't a valid DNS name"))?;
let connector = TlsConnector::from(config); let connector = TlsConnector::from(config);
let stream = connector let stream = connector
@@ -310,6 +374,50 @@ impl AsyncNetworkStream {
InnerAsyncNetworkStream::None => false, 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 { impl FuturesAsyncRead for AsyncNetworkStream {

View File

@@ -5,6 +5,8 @@ use std::{
time::Duration, time::Duration,
}; };
#[cfg(feature = "tracing")]
use super::escape_crlf;
use super::{ClientCodec, NetworkStream, TlsParameters}; use super::{ClientCodec, NetworkStream, TlsParameters};
use crate::{ use crate::{
address::Envelope, address::Envelope,
@@ -18,9 +20,6 @@ use crate::{
}, },
}; };
#[cfg(feature = "tracing")]
use super::escape_crlf;
macro_rules! try_smtp ( macro_rules! try_smtp (
($err: expr, $client: ident) => ({ ($err: expr, $client: ident) => ({
match $err { match $err {
@@ -145,7 +144,7 @@ impl SmtpConnection {
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
{ {
try_smtp!(self.command(Starttls), self); 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")] #[cfg(feature = "tracing")]
tracing::debug!("connection encrypted"); tracing::debug!("connection encrypted");
// Send EHLO again // Send EHLO again
@@ -276,7 +275,10 @@ impl SmtpConnection {
return if response.is_positive() { return if response.is_positive() {
Ok(response) Ok(response)
} else { } else {
Err(error::code(response.code)) Err(error::code(
response.code(),
response.first_line().map(|s| s.to_owned()),
))
}; };
} }
Err(nom::Err::Failure(e)) => { Err(nom::Err::Failure(e)) => {
@@ -291,4 +293,10 @@ impl SmtpConnection {
Err(error::response("incomplete response")) 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

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

@@ -7,9 +7,8 @@ use std::{
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
use native_tls::TlsStream; use native_tls::TlsStream;
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
use rustls::{ClientSession, StreamOwned}; use rustls::{ClientConnection, ServerName, StreamOwned};
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
use super::InnerTlsParameters; use super::InnerTlsParameters;
@@ -33,7 +32,7 @@ enum InnerNetworkStream {
NativeTls(TlsStream<TcpStream>), NativeTls(TlsStream<TcpStream>),
/// Encrypted TCP stream /// Encrypted TCP stream
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
RustlsTls(StreamOwned<ClientSession, TcpStream>), RustlsTls(StreamOwned<ClientConnection, TcpStream>),
/// Can't be built /// Can't be built
None, None,
} }
@@ -90,12 +89,20 @@ impl NetworkStream {
timeout: Duration, timeout: Duration,
) -> Result<TcpStream, Error> { ) -> Result<TcpStream, Error> {
let addrs = server.to_socket_addrs().map_err(error::connection)?; let addrs = server.to_socket_addrs().map_err(error::connection)?;
let mut last_err = None;
for addr in addrs { for addr in addrs {
if let Ok(result) = TcpStream::connect_timeout(&addr, timeout) { match TcpStream::connect_timeout(&addr, timeout) {
return Ok(result); 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 { let tcp_stream = match timeout {
@@ -149,12 +156,11 @@ impl NetworkStream {
} }
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InnerTlsParameters::RustlsTls(connector) => { InnerTlsParameters::RustlsTls(connector) => {
use webpki::DNSNameRef; let domain = ServerName::try_from(tls_parameters.domain())
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
let domain = DNSNameRef::try_from_ascii_str(tls_parameters.domain()) let connection =
.map_err(error::connection)?; ClientConnection::new(connector.clone(), domain).map_err(error::connection)?;
let stream = StreamOwned::new(ClientSession::new(&connector, domain), tcp_stream); let stream = StreamOwned::new(connection, tcp_stream);
InnerNetworkStream::RustlsTls(stream) InnerNetworkStream::RustlsTls(stream)
} }
}) })
@@ -174,6 +180,30 @@ impl NetworkStream {
} }
} }
#[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"),
}
}
pub fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> { pub fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
match self.inner { match self.inner {
InnerNetworkStream::Tcp(ref mut stream) => stream.set_read_timeout(duration), InnerNetworkStream::Tcp(ref mut stream) => stream.set_read_timeout(duration),

View File

@@ -1,14 +1,17 @@
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] use std::fmt::{self, Debug};
use crate::transport::smtp::{error, Error}; #[cfg(feature = "rustls-tls")]
use std::{sync::Arc, time::SystemTime};
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
use native_tls::{Protocol, TlsConnector}; use native_tls::{Protocol, TlsConnector};
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
use rustls::{ClientConfig, RootCertStore, ServerCertVerified, ServerCertVerifier, TLSError}; use rustls::{
use std::fmt::{self, Debug}; client::{ServerCertVerified, ServerCertVerifier, WebPkiVerifier},
#[cfg(feature = "rustls-tls")] ClientConfig, Error as TlsError, OwnedTrustAnchor, RootCertStore, ServerName,
use std::sync::Arc; };
#[cfg(feature = "rustls-tls")]
use webpki::DNSNameRef; #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
use crate::transport::smtp::{error, Error};
/// Accepted protocols by default. /// Accepted protocols by default.
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults. /// This removes TLS 1.0 and 1.1 compared to tls-native defaults.
@@ -163,21 +166,35 @@ impl TlsParametersBuilder {
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))] #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
pub fn build_rustls(self) -> Result<TlsParameters, Error> { 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(); let tls = if self.accept_invalid_certs {
tls.with_custom_certificate_verifier(Arc::new(InvalidCertsVerifier {}))
for cert in self.root_certs { } else {
for rustls_cert in cert.rustls { let mut root_cert_store = RootCertStore::empty();
tls.root_store.add(&rustls_cert).map_err(error::tls)?; for cert in self.root_certs {
for rustls_cert in cert.rustls {
root_cert_store.add(&rustls_cert).map_err(error::tls)?;
}
} }
} root_cert_store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(
if self.accept_invalid_certs { |ta| {
tls.dangerous() OwnedTrustAnchor::from_subject_spki_name_constraints(
.set_certificate_verifier(Arc::new(InvalidCertsVerifier {})); 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 { Ok(TlsParameters {
connector: InnerTlsParameters::RustlsTls(Arc::new(tls)), connector: InnerTlsParameters::RustlsTls(Arc::new(tls)),
domain: self.domain, domain: self.domain,
@@ -257,11 +274,14 @@ impl Certificate {
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
let rustls_cert = { let rustls_cert = {
use rustls::internal::pemfile;
use std::io::Cursor; use std::io::Cursor;
let mut pem = Cursor::new(pem); 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 { Ok(Self {
@@ -286,11 +306,13 @@ struct InvalidCertsVerifier;
impl ServerCertVerifier for InvalidCertsVerifier { impl ServerCertVerifier for InvalidCertsVerifier {
fn verify_server_cert( fn verify_server_cert(
&self, &self,
_roots: &RootCertStore, _end_entity: &rustls::Certificate,
_presented_certs: &[rustls::Certificate], _intermediates: &[rustls::Certificate],
_dns_name: DNSNameRef<'_>, _server_name: &ServerName,
_scts: &mut dyn Iterator<Item = &[u8]>,
_ocsp_response: &[u8], _ocsp_response: &[u8],
) -> Result<ServerCertVerified, TLSError> { _now: SystemTime,
) -> Result<ServerCertVerified, TlsError> {
Ok(ServerCertVerified::assertion()) Ok(ServerCertVerified::assertion())
} }
} }

View File

@@ -1,5 +1,7 @@
//! SMTP commands //! SMTP commands
use std::fmt::{self, Display, Formatter};
use crate::{ use crate::{
address::Address, address::Address,
transport::smtp::{ transport::smtp::{
@@ -9,7 +11,6 @@ use crate::{
response::Response, response::Response,
}, },
}; };
use std::fmt::{self, Display, Formatter};
/// EHLO command /// EHLO command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Clone, Debug)]
@@ -288,9 +289,10 @@ impl Auth {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::str::FromStr;
use super::*; use super::*;
use crate::transport::smtp::extension::MailBodyParameter; use crate::transport::smtp::extension::MailBodyParameter;
use std::str::FromStr;
#[test] #[test]
fn test_display() { fn test_display() {

View File

@@ -1,10 +1,11 @@
//! Error and result type for SMTP clients //! Error and result type for SMTP clients
use std::{error::Error as StdError, fmt};
use crate::{ use crate::{
transport::smtp::response::{Code, Severity}, transport::smtp::response::{Code, Severity},
BoxError, BoxError,
}; };
use std::{error::Error as StdError, fmt};
// Inspired by https://github.com/seanmonstar/reqwest/blob/a8566383168c0ef06c21f38cbc9213af6ff6db31/src/error.rs // Inspired by https://github.com/seanmonstar/reqwest/blob/a8566383168c0ef06c21f38cbc9213af6ff6db31/src/error.rs
@@ -154,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 { match c.severity {
Severity::TransientNegativeCompletion => Error::new::<Error>(Kind::Transient(c), None), Severity::TransientNegativeCompletion => Error::new(Kind::Transient(c), s),
Severity::PermanentNegativeCompletion => Error::new::<Error>(Kind::Permanent(c), None), Severity::PermanentNegativeCompletion => Error::new(Kind::Permanent(c), s),
_ => client("Unknown error code"), _ => client("Unknown error code"),
} }
} }

View File

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

View File

@@ -33,7 +33,7 @@
//! ```rust,no_run //! ```rust,no_run
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))] //! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
//! # fn test() -> Result<(), Box<dyn std::error::Error>> { //! # fn test() -> Result<(), Box<dyn std::error::Error>> {
//! use lettre::{Message, Transport, SmtpTransport}; //! use lettre::{Message, SmtpTransport, Transport};
//! //!
//! let email = Message::builder() //! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?) //! .from("NoBody <nobody@domain.tld>".parse()?)
@@ -43,8 +43,7 @@
//! .body(String::from("Be happy!"))?; //! .body(String::from("Be happy!"))?;
//! //!
//! // Create TLS transport on port 465 //! // Create TLS transport on port 465
//! let sender = SmtpTransport::relay("smtp.example.com")? //! let sender = SmtpTransport::relay("smtp.example.com")?.build();
//! .build();
//! // Send the email via remote relay //! // Send the email via remote relay
//! let result = sender.send(&email); //! let result = sender.send(&email);
//! assert!(result.is_ok()); //! assert!(result.is_ok());
@@ -59,7 +58,13 @@
//! ```rust,no_run //! ```rust,no_run
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))] //! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
//! # fn test() -> Result<(), Box<dyn std::error::Error>> { //! # 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() //! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?) //! .from("NoBody <nobody@domain.tld>".parse()?)
@@ -71,11 +76,14 @@
//! // Create TLS transport on port 587 with STARTTLS //! // Create TLS transport on port 587 with STARTTLS
//! let sender = SmtpTransport::starttls_relay("smtp.example.com")? //! let sender = SmtpTransport::starttls_relay("smtp.example.com")?
//! // Add credentials for authentication //! // 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 //! // Configure expected authentication mechanism
//! .authentication(vec![Mechanism::Plain]) //! .authentication(vec![Mechanism::Plain])
//! // Connection pool settings //! // Connection pool settings
//! .pool_config( PoolConfig::new().max_size(20)) //! .pool_config(PoolConfig::new().max_size(20))
//! .build(); //! .build();
//! //!
//! // Send the email via remote relay //! // Send the email via remote relay
@@ -90,7 +98,10 @@
//! ```rust,no_run //! ```rust,no_run
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))] //! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
//! # fn test() -> Result<(), Box<dyn std::error::Error>> { //! # 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() //! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?) //! .from("NoBody <nobody@domain.tld>".parse()?)
@@ -101,7 +112,8 @@
//! //!
//! // Custom TLS configuration //! // Custom TLS configuration
//! let tls = TlsParameters::builder("smtp.example.com".to_string()) //! 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 //! // Create TLS transport on port 465
//! let sender = SmtpTransport::relay("smtp.example.com")? //! let sender = SmtpTransport::relay("smtp.example.com")?
@@ -116,12 +128,14 @@
//! # } //! # }
//! ``` //! ```
use std::time::Duration;
use client::Tls;
#[cfg(any(feature = "tokio1", feature = "async-std1"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub use self::async_transport::{AsyncSmtpTransport, AsyncSmtpTransportBuilder}; pub use self::async_transport::{AsyncSmtpTransport, AsyncSmtpTransportBuilder};
#[cfg(feature = "r2d2")] #[cfg(feature = "pool")]
pub use self::pool::PoolConfig; pub use self::pool::PoolConfig;
#[cfg(feature = "r2d2")]
pub(crate) use self::transport::SmtpClient;
pub use self::{ pub use self::{
error::Error, error::Error,
transport::{SmtpTransport, SmtpTransportBuilder}, transport::{SmtpTransport, SmtpTransportBuilder},
@@ -134,8 +148,6 @@ use crate::transport::smtp::{
extension::ClientId, extension::ClientId,
response::Response, response::Response,
}; };
use client::Tls;
use std::time::Duration;
#[cfg(any(feature = "tokio1", feature = "async-std1"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
mod async_transport; mod async_transport;
@@ -144,11 +156,11 @@ pub mod client;
pub mod commands; pub mod commands;
mod error; mod error;
pub mod extension; pub mod extension;
#[cfg(feature = "r2d2")] #[cfg(feature = "pool")]
mod pool; mod pool;
pub mod response; pub mod response;
mod transport; mod transport;
pub mod util; pub(super) mod util;
// Registered port numbers: // Registered port numbers:
// https://www.iana. // https://www.iana.
@@ -164,7 +176,7 @@ pub const SUBMISSION_PORT: u16 = 587;
pub const SUBMISSIONS_PORT: u16 = 465; pub const SUBMISSIONS_PORT: u16 = 465;
/// Default timeout /// Default timeout
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60); const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct SmtpInfo { 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 //! SMTP response, containing a mandatory return code and an optional text
//! message //! message
use crate::transport::smtp::{error, Error}; use std::{
fmt::{Display, Formatter, Result},
result,
str::FromStr,
string::ToString,
};
use nom::{ use nom::{
branch::alt, branch::alt,
bytes::streaming::{tag, take_until}, bytes::streaming::{tag, take_until},
@@ -10,12 +16,8 @@ use nom::{
sequence::{preceded, tuple}, sequence::{preceded, tuple},
IResult, IResult,
}; };
use std::{
fmt::{Display, Formatter, Result}, use crate::transport::smtp::{error, Error};
result,
str::FromStr,
string::ToString,
};
/// First digit indicates severity /// First digit indicates severity
#[derive(PartialEq, Eq, Copy, Clone, Debug)] #[derive(PartialEq, Eq, Copy, Clone, Debug)]
@@ -137,10 +139,10 @@ impl Code {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Response { pub struct Response {
/// Response code /// Response code
pub code: Code, code: Code,
/// Server response string (optional) /// Server response string (optional)
/// Handle multiline responses /// Handle multiline responses
pub message: Vec<String>, message: Vec<String>,
} }
impl FromStr for Response { impl FromStr for Response {
@@ -180,6 +182,16 @@ impl Response {
pub fn first_line(&self) -> Option<&str> { pub fn first_line(&self) -> Option<&str> {
self.message.first().map(String::as_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) // Parsers (originally from tokio-smtp)

View File

@@ -1,22 +1,23 @@
#[cfg(feature = "pool")]
use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
#[cfg(feature = "r2d2")] #[cfg(feature = "pool")]
use r2d2::Pool; use super::pool::sync_impl::Pool;
#[cfg(feature = "pool")]
#[cfg(feature = "r2d2")]
use super::PoolConfig; 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}; 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}; use crate::{address::Envelope, Transport};
/// Sends emails using the SMTP protocol /// Sends emails using the SMTP protocol
#[cfg_attr(docsrs, doc(cfg(feature = "smtp-transport")))] #[cfg_attr(docsrs, doc(cfg(feature = "smtp-transport")))]
#[derive(Clone)] #[derive(Clone)]
pub struct SmtpTransport { pub struct SmtpTransport {
#[cfg(feature = "r2d2")] #[cfg(feature = "pool")]
inner: Pool<SmtpClient>, inner: Arc<Pool>,
#[cfg(not(feature = "r2d2"))] #[cfg(not(feature = "pool"))]
inner: SmtpClient, inner: SmtpClient,
} }
@@ -26,14 +27,11 @@ impl Transport for SmtpTransport {
/// Sends an email /// Sends an email
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> {
#[cfg(feature = "r2d2")]
let mut conn = self.inner.get().map_err(error::client)?;
#[cfg(not(feature = "r2d2"))]
let mut conn = self.inner.connection()?; let mut conn = self.inner.connection()?;
let result = conn.send(envelope, email)?; let result = conn.send(envelope, email)?;
#[cfg(not(feature = "r2d2"))] #[cfg(not(feature = "pool"))]
conn.quit()?; conn.quit()?;
Ok(result) Ok(result)
@@ -105,10 +103,25 @@ impl SmtpTransport {
SmtpTransportBuilder { SmtpTransportBuilder {
info: new, info: new,
#[cfg(feature = "r2d2")] #[cfg(feature = "pool")]
pool_config: PoolConfig::default(), 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. /// Contains client configuration.
@@ -116,7 +129,7 @@ impl SmtpTransport {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SmtpTransportBuilder { pub struct SmtpTransportBuilder {
info: SmtpInfo, info: SmtpInfo,
#[cfg(feature = "r2d2")] #[cfg(feature = "pool")]
pool_config: PoolConfig, pool_config: PoolConfig,
} }
@@ -163,8 +176,8 @@ impl SmtpTransportBuilder {
/// Use a custom configuration for the connection pool /// Use a custom configuration for the connection pool
/// ///
/// Defaults can be found at [`PoolConfig`] /// Defaults can be found at [`PoolConfig`]
#[cfg(feature = "r2d2")] #[cfg(feature = "pool")]
#[cfg_attr(docsrs, doc(cfg(feature = "r2d2")))] #[cfg_attr(docsrs, doc(cfg(feature = "pool")))]
pub fn pool_config(mut self, pool_config: PoolConfig) -> Self { pub fn pool_config(mut self, pool_config: PoolConfig) -> Self {
self.pool_config = pool_config; self.pool_config = pool_config;
self self
@@ -172,16 +185,15 @@ impl SmtpTransportBuilder {
/// Build the transport /// 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`] /// Defaults can be found at [`PoolConfig`]
pub fn build(self) -> SmtpTransport { pub fn build(self) -> SmtpTransport {
let client = SmtpClient { info: self.info }; let client = SmtpClient { info: self.info };
SmtpTransport {
#[cfg(feature = "r2d2")] #[cfg(feature = "pool")]
inner: self.pool_config.build(client), let client = Pool::new(self.pool_config, client);
#[cfg(not(feature = "r2d2"))]
inner: client, SmtpTransport { inner: client }
}
} }
} }
@@ -225,7 +237,7 @@ impl SmtpClient {
} }
if let Some(credentials) = &self.info.credentials { if let Some(credentials) = &self.info.credentials {
conn.auth(&self.info.authentication, &credentials)?; conn.auth(&self.info.authentication, credentials)?;
} }
Ok(conn) Ok(conn)
} }

View File

@@ -4,7 +4,6 @@ use std::fmt::{Display, Formatter, Result as FmtResult};
/// Encode a string as xtext /// Encode a string as xtext
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct XText<'a>(pub &'a str); pub struct XText<'a>(pub &'a str);
impl<'a> Display for XText<'a> { 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 //! The stub transport logs message envelopes as well as contents. It can be useful for testing
//! testing purposes. //! purposes.
//! //!
//! #### Stub Transport //! # Stub Transport
//! //!
//! The stub transport returns provided result and drops the content. It can be useful for //! The stub transport logs message envelopes as well as contents. It can be useful for testing
//! testing purposes. //! purposes.
//!
//! # Examples
//! //!
//! ```rust //! ```rust
//! # #[cfg(feature = "builder")] //! # #[cfg(feature = "builder")]
@@ -12,7 +14,7 @@
//! use lettre::{transport::stub::StubTransport, Message, Transport}; //! use lettre::{transport::stub::StubTransport, Message, Transport};
//! //!
//! # use std::error::Error; //! # use std::error::Error;
//! # fn main() -> Result<(), Box<dyn Error>> { //! # fn try_main() -> Result<(), Box<dyn Error>> {
//! let email = Message::builder() //! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?) //! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?) //! .reply_to("Yuin <yuin@domain.tld>".parse()?)
@@ -23,17 +25,33 @@
//! let mut sender = StubTransport::new_ok(); //! let mut sender = StubTransport::new_ok();
//! let result = sender.send(&email); //! let result = sender.send(&email);
//! assert!(result.is_ok()); //! assert!(result.is_ok());
//! assert_eq!(
//! sender.messages(),
//! vec![(
//! email.envelope().clone(),
//! String::from_utf8(email.formatted()).unwrap()
//! )],
//! );
//! # Ok(()) //! # Ok(())
//! # } //! # }
//! # try_main().unwrap();
//! # } //! # }
//! ``` //! ```
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"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
use crate::AsyncTransport; use crate::AsyncTransport;
use crate::{address::Envelope, Transport}; use crate::{address::Envelope, Transport};
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
use async_trait::async_trait;
use std::{error::Error as StdError, fmt};
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub struct Error; pub struct Error;
@@ -46,47 +64,113 @@ impl fmt::Display for Error {
impl StdError for Error {} impl StdError for Error {}
/// This transport logs the message envelope and returns the given response /// This transport logs messages and always returns the given response
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone)]
pub struct StubTransport { pub struct StubTransport {
response: Result<(), Error>, 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 { impl StubTransport {
/// Creates a new transport that always returns the given Result /// Creates a new transport that always returns the given Result
pub fn new(response: Result<(), Error>) -> StubTransport { pub fn new(response: Result<(), Error>) -> Self {
StubTransport { response } Self {
response,
message_log: Arc::new(StdMutex::new(vec![])),
}
} }
/// Creates a new transport that always returns a success response /// Creates a new transport that always returns a success response
pub fn new_ok() -> StubTransport { pub fn new_ok() -> Self {
StubTransport { response: Ok(()) } Self {
response: Ok(()),
message_log: Arc::new(StdMutex::new(vec![])),
}
} }
/// Creates a new transport that always returns an error /// Creates a new transport that always returns an error
pub fn new_error() -> StubTransport { pub fn new_error() -> Self {
StubTransport { Self {
response: Err(Error), 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 { impl Transport for StubTransport {
type Ok = (); type Ok = ();
type Error = Error; 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 self.response
} }
} }
#[cfg(any(feature = "tokio1", feature = "async-std1"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
#[async_trait] #[async_trait]
impl AsyncTransport for StubTransport { impl AsyncTransport for AsyncStubTransport {
type Ok = (); type Ok = ();
type Error = Error; 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 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,4 +1,4 @@
Date: Tue, 15 Nov 1994 08:12:31 -0000 Date: Tue, 15 Nov 1994 08:12:31 +0000
From: NoBody <nobody@domain.tld> From: NoBody <nobody@domain.tld>
Reply-To: Yuin <yuin@domain.tld> Reply-To: Yuin <yuin@domain.tld>
To: Hei <hei@domain.tld> To: Hei <hei@domain.tld>

View File

@@ -9,13 +9,15 @@ fn default_date() -> std::time::SystemTime {
#[cfg(test)] #[cfg(test)]
#[cfg(all(feature = "file-transport", feature = "builder"))] #[cfg(all(feature = "file-transport", feature = "builder"))]
mod sync { mod sync {
use crate::default_date;
use lettre::{FileTransport, Message, Transport};
use std::{ use std::{
env::temp_dir, env::temp_dir,
fs::{read_to_string, remove_file}, fs::{read_to_string, remove_file},
}; };
use lettre::{FileTransport, Message, Transport};
use crate::default_date;
#[test] #[test]
fn file_transport() { fn file_transport() {
let sender = FileTransport::new(temp_dir()); let sender = FileTransport::new(temp_dir());
@@ -41,7 +43,7 @@ mod sync {
"Reply-To: Yuin <yuin@domain.tld>\r\n", "Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\r\n", "To: Hei <hei@domain.tld>\r\n",
"Subject: Happy new year\r\n", "Subject: Happy new year\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n", "Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
"Content-Transfer-Encoding: 7bit\r\n", "Content-Transfer-Encoding: 7bit\r\n",
"\r\n", "\r\n",
"Be happy!" "Be happy!"
@@ -79,7 +81,7 @@ mod sync {
"Reply-To: Yuin <yuin@domain.tld>\r\n", "Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\r\n", "To: Hei <hei@domain.tld>\r\n",
"Subject: Happy new year\r\n", "Subject: Happy new year\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n", "Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
"Content-Transfer-Encoding: 7bit\r\n", "Content-Transfer-Encoding: 7bit\r\n",
"\r\n", "\r\n",
"Be happy!" "Be happy!"
@@ -104,15 +106,16 @@ mod sync {
#[cfg(test)] #[cfg(test)]
#[cfg(all(feature = "file-transport", feature = "builder", feature = "tokio1"))] #[cfg(all(feature = "file-transport", feature = "builder", feature = "tokio1"))]
mod tokio_1 { mod tokio_1 {
use crate::default_date;
use lettre::{AsyncFileTransport, AsyncTransport, Message, Tokio1Executor};
use std::{ use std::{
env::temp_dir, env::temp_dir,
fs::{read_to_string, remove_file}, fs::{read_to_string, remove_file},
}; };
use lettre::{AsyncFileTransport, AsyncTransport, Message, Tokio1Executor};
use tokio1_crate as tokio; use tokio1_crate as tokio;
use crate::default_date;
#[tokio::test] #[tokio::test]
async fn file_transport_tokio1() { async fn file_transport_tokio1() {
let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir()); let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir());
@@ -138,7 +141,7 @@ mod tokio_1 {
"Reply-To: Yuin <yuin@domain.tld>\r\n", "Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\r\n", "To: Hei <hei@domain.tld>\r\n",
"Subject: Happy new year\r\n", "Subject: Happy new year\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n", "Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
"Content-Transfer-Encoding: 7bit\r\n", "Content-Transfer-Encoding: 7bit\r\n",
"\r\n", "\r\n",
"Be happy!" "Be happy!"
@@ -155,13 +158,15 @@ mod tokio_1 {
feature = "async-std1" feature = "async-std1"
))] ))]
mod asyncstd_1 { mod asyncstd_1 {
use crate::default_date;
use lettre::{AsyncFileTransport, AsyncStd1Executor, AsyncTransport, Message};
use std::{ use std::{
env::temp_dir, env::temp_dir,
fs::{read_to_string, remove_file}, fs::{read_to_string, remove_file},
}; };
use lettre::{AsyncFileTransport, AsyncStd1Executor, AsyncTransport, Message};
use crate::default_date;
#[async_std::test] #[async_std::test]
async fn file_transport_asyncstd1() { async fn file_transport_asyncstd1() {
let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir()); let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir());
@@ -187,7 +192,7 @@ mod asyncstd_1 {
"Reply-To: Yuin <yuin@domain.tld>\r\n", "Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\r\n", "To: Hei <hei@domain.tld>\r\n",
"Subject: Happy new year\r\n", "Subject: Happy new year\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n", "Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
"Content-Transfer-Encoding: 7bit\r\n", "Content-Transfer-Encoding: 7bit\r\n",
"\r\n", "\r\n",
"Be happy!" "Be happy!"

View File

@@ -24,7 +24,6 @@ mod sync {
#[cfg(all(feature = "smtp-transport", feature = "builder", feature = "tokio1"))] #[cfg(all(feature = "smtp-transport", feature = "builder", feature = "tokio1"))]
mod tokio_1 { mod tokio_1 {
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor}; use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
use tokio1_crate as tokio; use tokio1_crate as tokio;
#[tokio::test] #[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 { mod sync {
use lettre::{address::Envelope, SmtpTransport, Transport};
use std::{sync::mpsc, thread}; use std::{sync::mpsc, thread};
use lettre::{address::Envelope, SmtpTransport, Transport};
fn envelope() -> Envelope { fn envelope() -> Envelope {
Envelope::new( Envelope::new(
Some("user@localhost".parse().unwrap()), Some("user@localhost".parse().unwrap()),

View File

@@ -17,20 +17,25 @@ mod sync {
sender_ok.send(&email).unwrap(); sender_ok.send(&email).unwrap();
sender_ko.send(&email).unwrap_err(); sender_ko.send(&email).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(test)]
#[cfg(all(feature = "builder", feature = "tokio1"))] #[cfg(all(feature = "builder", feature = "tokio1"))]
mod tokio_1 { mod tokio_1 {
use lettre::{transport::stub::StubTransport, AsyncTransport, Message}; use lettre::{transport::stub::AsyncStubTransport, AsyncTransport, Message};
use tokio1_crate as tokio; use tokio1_crate as tokio;
#[tokio::test] #[tokio::test]
async fn stub_transport_tokio1() { async fn stub_transport_tokio1() {
let sender_ok = StubTransport::new_ok(); let sender_ok = AsyncStubTransport::new_ok();
let sender_ko = StubTransport::new_error(); let sender_ko = AsyncStubTransport::new_error();
let email = Message::builder() let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap()) .from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
@@ -40,19 +45,25 @@ mod tokio_1 {
.unwrap(); .unwrap();
sender_ok.send(email.clone()).await.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(test)]
#[cfg(all(feature = "builder", feature = "async-std1"))] #[cfg(all(feature = "builder", feature = "async-std1"))]
mod asyncstd_1 { mod asyncstd_1 {
use lettre::{transport::stub::StubTransport, AsyncTransport, Message}; use lettre::{transport::stub::AsyncStubTransport, AsyncTransport, Message};
#[async_std::test] #[async_std::test]
async fn stub_transport_asyncstd1() { async fn stub_transport_asyncstd1() {
let sender_ok = StubTransport::new_ok(); let sender_ok = AsyncStubTransport::new_ok();
let sender_ko = StubTransport::new_error(); let sender_ko = AsyncStubTransport::new_error();
let email = Message::builder() let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap()) .from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
@@ -62,6 +73,12 @@ mod asyncstd_1 {
.unwrap(); .unwrap();
sender_ok.send(email.clone()).await.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);
} }
} }