Compare commits
57 Commits
v0.10.0-rc
...
v0.10.0-rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0db759e5f | ||
|
|
5daf5d397a | ||
|
|
3f1647fa48 | ||
|
|
fd106d9b0c | ||
|
|
c1d37d54b4 | ||
|
|
efa0d58778 | ||
|
|
9567b23f4d | ||
|
|
f77376fa19 | ||
|
|
6e35b9b30d | ||
|
|
c24213c850 | ||
|
|
8b40e438fd | ||
|
|
e1462b2d1b | ||
|
|
96b42515cd | ||
|
|
1ea4987023 | ||
|
|
9273d24e54 | ||
|
|
7a0dd5bd92 | ||
|
|
9a8aa46dba | ||
|
|
0377ea29b7 | ||
|
|
89e5b9083e | ||
|
|
8c370e28c9 | ||
|
|
3eed80ef30 | ||
|
|
dbb135c533 | ||
|
|
4c5f02b4f6 | ||
|
|
f02542841c | ||
|
|
29c34adc25 | ||
|
|
5e3ebbb189 | ||
|
|
60399a93cc | ||
|
|
a48bc8a1b2 | ||
|
|
94cc0149d1 | ||
|
|
a89383cdb6 | ||
|
|
592593f4b8 | ||
|
|
97d3c760c0 | ||
|
|
8f28b0c341 | ||
|
|
dc9c5df210 | ||
|
|
c9b3fa0baa | ||
|
|
addf8754dd | ||
|
|
af157c5f26 | ||
|
|
3e8988ae55 | ||
|
|
941a00bcaa | ||
|
|
14079bff8c | ||
|
|
696c06e8d7 | ||
|
|
d4f7618898 | ||
|
|
e0a0a2e624 | ||
|
|
9ab6bb56d3 | ||
|
|
e1d3778329 | ||
|
|
623d69c553 | ||
|
|
55c2618201 | ||
|
|
9f550bce86 | ||
|
|
e875d9ff64 | ||
|
|
aadcc0f83c | ||
|
|
b534a18017 | ||
|
|
0684bccd47 | ||
|
|
4471759221 | ||
|
|
ed454819ee | ||
|
|
47cad567b0 | ||
|
|
b0e2fc9bca | ||
|
|
1d8249165c |
24
.github/workflows/test.yml
vendored
24
.github/workflows/test.yml
vendored
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
49
Cargo.toml
49
Cargo.toml
@@ -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"
|
||||||
|
|||||||
4
LICENSE
4
LICENSE
@@ -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
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
3
rustfmt.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
format_code_in_doc_comments = true
|
||||||
|
imports_granularity = "Crate"
|
||||||
|
group_imports = "StdExternalCrate"
|
||||||
@@ -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};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
122
src/executor.rs
122
src/executor.rs
@@ -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<()> {}
|
||||||
}
|
}
|
||||||
|
|||||||
75
src/lib.rs
75
src/lib.rs
@@ -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};
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
528
src/message/dkim.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>(),
|
||||||
|
|||||||
@@ -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>(),
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>(),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;;;;;;;;;;;;;;;;fffeinmjgggggggggfっ".to_string(),
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
headers.to_string(),
|
||||||
|
concat!(
|
||||||
|
"Subject: =?utf-8?b?77yL5Luu5ZCN?= :a;go; \r\n",
|
||||||
|
" =?utf-8?b?Ozs7OztzOzs7Ozs7Ozs7Ozs7Ozs7O2ZmZmVpbm1qZ2dnZ2dnZ2dn772G44Gj?=\r\n"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>(),
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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>"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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)?;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
301
src/transport/smtp/pool/async_impl.rs
Normal file
301
src/transport/smtp/pool/async_impl.rs
Normal 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;
|
||||||
|
}
|
||||||
66
src/transport/smtp/pool/mod.rs
Normal file
66
src/transport/smtp/pool/mod.rs
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
259
src/transport/smtp/pool/sync_impl.rs
Normal file
259
src/transport/smtp/pool/sync_impl.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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
7
testdata/coredns.conf
vendored
Normal 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
2
testdata/db.example.org
vendored
Normal 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"
|
||||||
2
testdata/email_with_png.eml
vendored
2
testdata/email_with_png.eml
vendored
@@ -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>
|
||||||
|
|||||||
@@ -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!"
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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()),
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user