Compare commits

...

34 Commits

Author SHA1 Message Date
Paolo Barbolini
0c9fc6cb71 Prepare 0.10.1 (#804) 2022-07-20 10:44:55 +02:00
Paolo Barbolini
2228cbdf93 Fix SMTP dot stuffing (#803) 2022-07-19 23:20:44 +02:00
André Cruz
17c95b0fa8 Ensure connection is closed on abort (#801)
When aborting a connection, ensure the underlying stream is closed
before we return.
2022-07-19 21:13:51 +02:00
Paolo Barbolini
62725af00a Improve TlsVersion docs and remember to re-export it (#800) 2022-07-18 07:40:46 +00:00
André Cruz
758bf1a4a7 Configurable minimum TLS version (#799)
Added support for configuring the minimum accepted TLS version. The
supported versions differ between TLS toolkits.
2022-07-17 13:11:14 +02:00
Paolo Barbolini
054c79f914 Document the boring-tls features in lib.rs (#798) 2022-07-16 09:45:44 +00:00
André Cruz
985fa7edc4 Add support for boring TLS (#797)
In contexts where FIPS certification is mandatory, having the
ability to use the certified boring TLS library is sometimes necessary.
Added initial support for it, only one TLS toolkit can be enabled at
one time.
2022-07-16 11:28:14 +02:00
Paolo Barbolini
9004d4ccc5 Add documentation to undocumented items (#793) 2022-06-29 22:44:29 +02:00
Paolo Barbolini
10171f8c75 Prepare 0.10.0 release (#538) 2022-06-29 21:17:37 +02:00
Paolo Barbolini
99e805952d Make it possible to keep the Bcc header when building a message (#792) 2022-06-29 21:08:27 +02:00
Paolo Barbolini
2d21dde5a1 Add autoconfigure.rs example (#787) 2022-06-17 06:35:07 +00:00
Paolo Barbolini
6fec936c0c Remove useless vec! allocations (#786) 2022-06-16 18:17:19 +00:00
Paolo Barbolini
22dfa5aa96 MessageBuilder: improve order headers are defined in (#783) 2022-06-16 17:53:54 +00:00
Paolo Barbolini
44e4cfd622 clippy: make rules stricter (#784) 2022-06-16 19:42:13 +02:00
Paolo Barbolini
7ea3d38a00 Mailboxes: add FromIterator impl and optimize Extend impl (#782) 2022-06-11 18:18:55 +00:00
Paolo Barbolini
73b89f5a9f clippy: fix latest warnings (#781) 2022-06-11 16:31:12 +00:00
Paolo Barbolini
1ec1b705c9 Prepare 0.10.0-rc.7 (#777) 2022-06-04 11:47:50 +02:00
Paolo Barbolini
e4006518fe Stop using the regex crate for parsing addresses (#776) 2022-06-03 13:39:57 +00:00
Paolo Barbolini
b33dd562fc Fix and improve header wrapping (#774)
Instead of injecting spaces to ensure that lines stay under 76 characters only wrap at whitespace characters. This avoids changing the headers.

A best-effort to keep lines under 76 characters is still done, however it is only done at whitespace. Notably there is no hard wrap enforced. This means that it is possible for headers to break the 1000 character line-length limit in the specification. It is just hoped that the receiver will allow long lines in this case.

Closes #688

Co-authored-by: Kevin Cox <kevincox@kevincox.ca>
2022-06-03 15:24:53 +02:00
Paolo Barbolini
65958df14f Use pretty_assertions for all message tests (#775) 2022-06-02 12:20:06 +00:00
James Hillyerd
50628af5fd README.md: Use IPv4 notation for localhost (#771) 2022-05-30 17:20:51 +00:00
Paolo Barbolini
cf858cc682 Move most email body encoding to email-encoding (#769) 2022-05-30 15:12:42 +00:00
Paolo Barbolini
f9a4b5ba89 Work around async-global-executor bumping MSRV too early (#773) 2022-05-30 15:01:40 +00:00
Jacob Halsey
1391a834ce #715: Support setting the local IP address to connect from (#762)
This adds a `local_address: Option<IpAddr>` parameter to the synchronous, and tokio connect functions.

(As far as I can see there is no current way to support this in async-std, because the library doesn't provide any way to do an async connect for an existing socket)
2022-05-29 07:05:39 +00:00
André Cruz
e6b4529896 use email_address crate for checking formats (#763)
The email_address crate is more strict in validating user and domain
parts of email addresses. For example, it verifies the total size
of the local part, which the current method does not, and this has
caused upstream servers to fail to accept an email.
2022-05-26 19:21:14 +02:00
Kevin Cox
ca5cb3f8f7 Fix encoded header signing. (#765)
The header needs to be properly formatted so that Unicode characters are encoded the same way they will be in the final message. Previously the logical header value was being encoded.

A notable example is that a `'` in the `To:` header needs to be encoded. This was being encoded incorrectly.
2022-05-26 07:44:14 +02:00
Kevin Cox
1e2279457e Add editorconfig file. (#766)
Makes it easy for everyone to use the preferred settings.

https://editorconfig.org/
2022-05-18 13:07:38 +00:00
Kevin Cox
961364cc29 Remove unnecessary clone. (#767)
This is backwards-incompatible but hopefully is an acceptable change for a pre-release. The upgrade path is straight forward.
2022-05-18 10:51:02 +02:00
Paolo Barbolini
b0db759e5f Prepare 0.10.0-rc.6 (#761) 2022-04-29 15:59:36 +02:00
Paolo Barbolini
5daf5d397a Fix parsing Mailboxes with a comma in the name (#760) 2022-04-26 12:18:12 +02:00
Paolo Barbolini
3f1647fa48 Bump dependencies (#759) 2022-04-25 09:17:58 +00:00
Paolo Barbolini
fd106d9b0c Bump rsa crate to the final 0.6.0 release (#758) 2022-04-14 09:39:30 +00:00
Vincent Breitmoser
c1d37d54b4 Use +0000 timezone format in Date header (#756)
Since the Date we emit is UTC, it's correct to use "+0000". The
previously used -0000 timezone indicator means "no timezone info".
2022-04-10 08:34:50 +02:00
David Krasnitsky
efa0d58778 Improve compiler error messages (#754) 2022-04-07 05:03:28 +00:00
42 changed files with 1446 additions and 850 deletions

8
.editorconfig Normal file
View File

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

View File

@@ -124,14 +124,20 @@ jobs:
- name: Install dkimverify - name: Install dkimverify
run: sudo apt -y install python3-dkim run: sudo apt -y install python3-dkim
- name: Work around early dependencies MSRV bump
run: cargo update -p async-global-executor --precise 2.0.4
- name: Test with no default features - name: Test with no default features
run: cargo test --no-default-features run: cargo test --no-default-features
- name: Test with default features - name: Test with default features
run: cargo test run: cargo test
- name: Test with all features - name: Test with all features (-native-tls)
run: cargo test --all-features run: cargo test --no-default-features --features async-std,async-std1,async-std1-rustls-tls,async-trait,base64,boring,boring-tls,builder,dkim,ed25519-dalek,email-encoding,fastrand,file-transport,file-transport-envelope,futures-io,futures-rustls,futures-util,hostname,httpdate,mime,mime03,nom,once_cell,pool,quoted_printable,regex,rsa,rustls,rustls-pemfile,rustls-tls,sendmail-transport,serde,serde_json,sha2,smtp-transport,socket2,tokio1,tokio1-boring-tls,tokio1-rustls-tls,tokio1_boring,tokio1_crate,tokio1_rustls,tracing,uuid,webpki-roots
- name: Test with all features (-boring-tls)
run: cargo test --no-default-features --features async-std,async-std1,async-std1-rustls-tls,async-trait,base64,builder,dkim,ed25519-dalek,email-encoding,fastrand,file-transport,file-transport-envelope,futures-io,futures-rustls,futures-util,hostname,httpdate,mime,mime03,native-tls,nom,once_cell,pool,quoted_printable,regex,rsa,rustls,rustls-pemfile,rustls-tls,sendmail-transport,serde,serde_json,sha2,smtp-transport,socket2,tokio1,tokio1-native-tls,tokio1-rustls-tls,tokio1_crate,tokio1_native_tls_crate,tokio1_rustls,tracing,uuid,webpki-roots
# coverage: # coverage:
# name: Coverage # name: Coverage

View File

@@ -1,5 +1,25 @@
<a name="v0.10.1"></a>
### v0.10.1 (2022-07-20)
#### Features
* Add `boring-tls` support for `SmtpTransport` and `AsyncSmtpTransport`. The latter is only supported with the tokio runtime. ([#797]) ([#798])
* Make the minimum TLS version configurable. ([#799]) ([#800])
#### Bug Fixes
* Ensure connections are closed on abort. ([#801])
* Fix SMTP dot stuffing. ([#803])
[#797]: https://github.com/lettre/lettre/pull/797
[#798]: https://github.com/lettre/lettre/pull/798
[#799]: https://github.com/lettre/lettre/pull/799
[#800]: https://github.com/lettre/lettre/pull/800
[#801]: https://github.com/lettre/lettre/pull/801
[#803]: https://github.com/lettre/lettre/pull/803
<a name="v0.10.0"></a> <a name="v0.10.0"></a>
### v0.10.0 (unreleased) ### v0.10.0 (2022-06-29)
#### Upgrade notes #### Upgrade notes
@@ -29,6 +49,7 @@ Several breaking changes were made between 0.9 and 0.10, but changes should be s
* Refactor `TlsParameters` implementation to not expose the internal TLS library * Refactor `TlsParameters` implementation to not expose the internal TLS library
* `FileTransport` writes emails into `.eml` instead of `.json` * `FileTransport` writes emails into `.eml` instead of `.json`
* When the hostname feature is disabled or hostname cannot be fetched, `127.0.0.1` is used instead of `localhost` as EHLO parameter (for better RFC compliance and mail server compatibility) * When the hostname feature is disabled or hostname cannot be fetched, `127.0.0.1` is used instead of `localhost` as EHLO parameter (for better RFC compliance and mail server compatibility)
* The `sendmail` and `file` transports aren't enabled by default anymore.
* The `new` method of `ClientId` is deprecated * 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 * The `SendmailTransport` now uses the `sendmail` command in current `PATH` by default instead of

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "lettre" name = "lettre"
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge) # remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
version = "0.10.0-rc.5" version = "0.10.1"
description = "Email client" description = "Email client"
readme = "README.md" readme = "README.md"
homepage = "https://lettre.rs" homepage = "https://lettre.rs"
@@ -20,7 +20,7 @@ maintenance = { status = "actively-developed" }
[dependencies] [dependencies]
idna = "0.2" idna = "0.2"
once_cell = "1" once_cell = { version = "1", optional = true }
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
@@ -29,23 +29,24 @@ 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 }
regex = { version = "1", default-features = false, features = ["std", "unicode-case"] } email-encoding = { version = "0.1.1", optional = true }
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-transport
nom = { version = "7", optional = true } nom = { version = "7", optional = true }
hostname = { version = "0.3", optional = true } # feature hostname = { version = "0.3", optional = true } # feature
socket2 = { version = "0.4.4", optional = true }
## tls ## tls
native-tls = { version = "0.2", optional = true } # feature native-tls = { version = "0.2", optional = true } # feature
rustls = { version = "0.20", features = ["dangerous_configuration"], optional = true } rustls = { version = "0.20", features = ["dangerous_configuration"], optional = true }
rustls-pemfile = { version = "0.3", optional = true } rustls-pemfile = { version = "1", optional = true }
webpki-roots = { version = "0.22", optional = true } webpki-roots = { version = "0.22", optional = true }
boring = { version = "2.0.0", optional = true }
# async # async
futures-io = { version = "0.3.7", optional = true } futures-io = { version = "0.3.7", optional = true }
@@ -61,21 +62,28 @@ futures-rustls = { version = "0.22", optional = true }
tokio1_crate = { package = "tokio", version = "1", features = ["fs", "rt", "process", "time", "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.23", optional = true } tokio1_rustls = { package = "tokio-rustls", version = "0.23", optional = true }
tokio1_boring = { package = "tokio-boring", version = "2.1.4", optional = true }
## dkim ## dkim
sha2 = { version = "0.10", optional = true } sha2 = { version = "0.10", optional = true }
rsa = { version = "0.6.0-pre", optional = true } rsa = { version = "0.6.0", optional = true }
ed25519-dalek = { version = "1.0.1", optional = true } ed25519-dalek = { version = "1.0.1", optional = true }
regex = { version = "1", default-features = false, features = ["std"], optional = true }
# email formats
email_address = { version = "0.2.1", default-features = false }
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1"
criterion = "0.3" criterion = "0.3"
tracing = { version = "0.1.16", default-features = false, features = ["std"] }
tracing-subscriber = "0.3" 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
@@ -83,19 +91,21 @@ name = "transport_smtp"
[features] [features]
default = ["smtp-transport", "pool", "native-tls", "hostname", "builder"] default = ["smtp-transport", "pool", "native-tls", "hostname", "builder"]
builder = ["httpdate", "mime", "base64", "fastrand", "quoted_printable", "email-encoding"] builder = ["httpdate", "mime", "fastrand", "quoted_printable", "email-encoding"]
mime03 = ["mime"] mime03 = ["mime"]
# transports # transports
file-transport = ["uuid"] file-transport = ["uuid"]
file-transport-envelope = ["serde", "serde_json", "file-transport"] file-transport-envelope = ["serde", "serde_json", "file-transport"]
sendmail-transport = [] sendmail-transport = []
smtp-transport = ["base64", "nom"] smtp-transport = ["base64", "nom", "socket2", "once_cell"]
pool = ["futures-util"] pool = ["futures-util"]
rustls-tls = ["webpki-roots", "rustls", "rustls-pemfile"] rustls-tls = ["webpki-roots", "rustls", "rustls-pemfile"]
boring-tls = ["boring"]
# 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"]
@@ -103,13 +113,18 @@ 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"]
tokio1-boring-tls = ["tokio1", "boring-tls", "tokio1_boring"]
dkim = ["sha2", "rsa", "ed25519-dalek"] dkim = ["base64", "sha2", "rsa", "ed25519-dalek", "regex", "once_cell"]
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true
rustdoc-args = ["--cfg", "docsrs", "--cfg", "lettre_ignore_tls_mismatch"] rustdoc-args = ["--cfg", "docsrs", "--cfg", "lettre_ignore_tls_mismatch"]
[[example]]
name = "autoconfigure"
required-features = ["smtp-transport", "native-tls"]
[[example]] [[example]]
name = "basic_html" name = "basic_html"
required-features = ["file-transport", "builder"] required-features = ["file-transport", "builder"]

View File

@@ -28,27 +28,14 @@
</div> </div>
<div align="center"> <div align="center">
<a href="https://deps.rs/crate/lettre/0.10.0-rc.5"> <a href="https://deps.rs/crate/lettre/0.10.1">
<img src="https://deps.rs/crate/lettre/0.10.0-rc.5/status.svg" <img src="https://deps.rs/crate/lettre/0.10.1/status.svg"
alt="dependency status" /> alt="dependency status" />
</a> </a>
</div> </div>
--- ---
**NOTE**: this readme refers to the 0.10 version of lettre, which is
in release candidate state. Use the [`v0.9.x`](https://github.com/lettre/lettre/tree/v0.9.x)
branch for the previous stable release.
0.10 is already widely used and is already thought to be more reliable than 0.9, so it should generally be used
for new projects.
We'd love to hear your feedback about 0.10 design and APIs before final release!
Start a [discussion](https://github.com/lettre/lettre/discussions) in the repository, whether for
feedback or if you need help or advice using or upgrading lettre 0.10.
---
## Features ## Features
Lettre provides the following features: Lettre provides the following features:
@@ -63,15 +50,20 @@ Lettre does not provide (for now):
* Email parsing * Email parsing
## Supported Rust Versions
Lettre supports all Rust versions released in the last 6 months. At the time of writing
the minimum supported Rust version is 1.56, but this could change at any time either from
one of our dependencies bumping their MSRV or by a new patch release of lettre.
## Example ## Example
This library requires Rust 1.56.0 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.5" lettre = "0.10"
``` ```
```rust,no_run ```rust,no_run
@@ -101,10 +93,18 @@ match mailer.send(&email) {
} }
``` ```
## Not sure of which connect options to use?
Clone the lettre git repository and run the following command (replacing `SMTP_HOST` with your SMTP server's hostname)
```shell
cargo run --example autoconfigure SMTP_HOST
```
## Testing ## Testing
The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command. If you have python installed 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` such a server can be launched with `python -m smtpd -n -c DebuggingServer 127.0.0.1:2525`
Alternatively only unit tests can be run by doing `cargo test --lib`. Alternatively only unit tests can be run by doing `cargo test --lib`.

93
examples/autoconfigure.rs Normal file
View File

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

View File

@@ -8,9 +8,8 @@ use std::{
str::FromStr, str::FromStr,
}; };
use email_address::EmailAddress;
use idna::domain_to_ascii; 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.
/// ///
@@ -55,20 +54,6 @@ pub struct Address {
at_start: usize, at_start: usize,
} }
// Regex from the specs
// https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address
// It will mark esoteric email addresses like quoted string as invalid
static USER_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^(?i)[a-z0-9.!#$%&'*+/=?^_`{|}~-]+\z").unwrap());
static DOMAIN_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r"(?i)^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$",
)
.unwrap()
});
// literal form, ipv4 or ipv6 address (SMTP 4.1.3)
static LITERAL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)\[([A-f0-9:\.]+)\]\z").unwrap());
impl Address { impl Address {
/// Creates a new email address from a user and domain. /// Creates a new email address from a user and domain.
/// ///
@@ -126,7 +111,7 @@ impl Address {
} }
pub(super) fn check_user(user: &str) -> Result<(), AddressError> { pub(super) fn check_user(user: &str) -> Result<(), AddressError> {
if USER_RE.is_match(user) { if EmailAddress::is_valid_local_part(user) {
Ok(()) Ok(())
} else { } else {
Err(AddressError::InvalidUser) Err(AddressError::InvalidUser)
@@ -142,17 +127,20 @@ impl Address {
} }
fn check_domain_ascii(domain: &str) -> Result<(), AddressError> { fn check_domain_ascii(domain: &str) -> Result<(), AddressError> {
if DOMAIN_RE.is_match(domain) { // Domain
if EmailAddress::is_valid_domain(domain) {
return Ok(()); return Ok(());
} }
if let Some(caps) = LITERAL_RE.captures(domain) { // IP
if let Some(cap) = caps.get(1) { let ip = domain
if cap.as_str().parse::<IpAddr>().is_ok() { .strip_prefix('[')
.and_then(|ip| ip.strip_suffix(']'))
.unwrap_or(domain);
if ip.parse::<IpAddr>().is_ok() {
return Ok(()); return Ok(());
} }
}
}
Err(AddressError::InvalidDomain) Err(AddressError::InvalidDomain)
} }
@@ -238,7 +226,7 @@ fn check_address(val: &str) -> Result<usize, AddressError> {
Ok(user.len()) Ok(user.len())
} }
#[derive(Debug, PartialEq, Clone, Copy)] #[derive(Debug, PartialEq, Eq, Clone, Copy)]
/// Errors in email addresses parsing /// Errors in email addresses parsing
pub enum AddressError { pub enum AddressError {
/// Missing domain or user /// Missing domain or user
@@ -269,7 +257,7 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn parse_address() { fn ascii_address() {
let addr_str = "something@example.com"; let addr_str = "something@example.com";
let addr = Address::from_str(addr_str).unwrap(); let addr = Address::from_str(addr_str).unwrap();
let addr2 = Address::new("something", "example.com").unwrap(); let addr2 = Address::new("something", "example.com").unwrap();
@@ -279,4 +267,36 @@ mod tests {
assert_eq!(addr2.user(), "something"); assert_eq!(addr2.user(), "something");
assert_eq!(addr2.domain(), "example.com"); assert_eq!(addr2.domain(), "example.com");
} }
#[test]
fn ascii_address_ipv4() {
let addr_str = "something@1.1.1.1";
let addr = Address::from_str(addr_str).unwrap();
let addr2 = Address::new("something", "1.1.1.1").unwrap();
assert_eq!(addr, addr2);
assert_eq!(addr.user(), "something");
assert_eq!(addr.domain(), "1.1.1.1");
assert_eq!(addr2.user(), "something");
assert_eq!(addr2.domain(), "1.1.1.1");
}
#[test]
fn ascii_address_ipv6() {
let addr_str = "something@[2606:4700:4700::1111]";
let addr = Address::from_str(addr_str).unwrap();
let addr2 = Address::new("something", "[2606:4700:4700::1111]").unwrap();
assert_eq!(addr, addr2);
assert_eq!(addr.user(), "something");
assert_eq!(addr.domain(), "[2606:4700:4700::1111]");
assert_eq!(addr2.user(), "something");
assert_eq!(addr2.domain(), "[2606:4700:4700::1111]");
}
#[test]
fn check_parts() {
assert!(Address::check_user("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").is_err());
assert!(
Address::check_domain("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com").is_err()
);
}
} }

View File

@@ -109,7 +109,6 @@ impl Executor for Tokio1Executor {
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
type Sleep = tokio1_crate::time::Sleep; type Sleep = tokio1_crate::time::Sleep;
#[doc(hidden)]
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
fn spawn<F>(fut: F) -> Self::Handle fn spawn<F>(fut: F) -> Self::Handle
where where
@@ -119,13 +118,11 @@ impl Executor for Tokio1Executor {
tokio1_crate::spawn(fut) tokio1_crate::spawn(fut)
} }
#[doc(hidden)]
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
fn sleep(duration: Duration) -> Self::Sleep { fn sleep(duration: Duration) -> Self::Sleep {
tokio1_crate::time::sleep(duration) tokio1_crate::time::sleep(duration)
} }
#[doc(hidden)]
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
async fn connect( async fn connect(
hostname: &str, hostname: &str,
@@ -146,6 +143,7 @@ impl Executor for Tokio1Executor {
timeout, timeout,
hello_name, hello_name,
tls_parameters, tls_parameters,
None,
) )
.await?; .await?;
@@ -165,13 +163,11 @@ impl Executor for Tokio1Executor {
Ok(conn) Ok(conn)
} }
#[doc(hidden)]
#[cfg(feature = "file-transport-envelope")] #[cfg(feature = "file-transport-envelope")]
async fn fs_read(path: &Path) -> IoResult<Vec<u8>> { async fn fs_read(path: &Path) -> IoResult<Vec<u8>> {
tokio1_crate::fs::read(path).await tokio1_crate::fs::read(path).await
} }
#[doc(hidden)]
#[cfg(feature = "file-transport")] #[cfg(feature = "file-transport")]
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> { async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> {
tokio1_crate::fs::write(path, contents).await tokio1_crate::fs::write(path, contents).await
@@ -209,7 +205,6 @@ impl Executor for AsyncStd1Executor {
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
type Sleep = BoxFuture<'static, ()>; type Sleep = BoxFuture<'static, ()>;
#[doc(hidden)]
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
fn spawn<F>(fut: F) -> Self::Handle fn spawn<F>(fut: F) -> Self::Handle
where where
@@ -219,14 +214,12 @@ impl Executor for AsyncStd1Executor {
async_std::task::spawn(fut) async_std::task::spawn(fut)
} }
#[doc(hidden)]
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
fn sleep(duration: Duration) -> Self::Sleep { fn sleep(duration: Duration) -> Self::Sleep {
let fut = async move { async_std::task::sleep(duration).await }; let fut = async move { async_std::task::sleep(duration).await };
Box::pin(fut) Box::pin(fut)
} }
#[doc(hidden)]
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
async fn connect( async fn connect(
hostname: &str, hostname: &str,
@@ -266,13 +259,11 @@ impl Executor for AsyncStd1Executor {
Ok(conn) Ok(conn)
} }
#[doc(hidden)]
#[cfg(feature = "file-transport-envelope")] #[cfg(feature = "file-transport-envelope")]
async fn fs_read(path: &Path) -> IoResult<Vec<u8>> { async fn fs_read(path: &Path) -> IoResult<Vec<u8>> {
async_std::fs::read(path).await async_std::fs::read(path).await
} }
#[doc(hidden)]
#[cfg(feature = "file-transport")] #[cfg(feature = "file-transport")]
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> { async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> {
async_std::fs::write(path, contents).await async_std::fs::write(path, contents).await
@@ -288,15 +279,13 @@ impl SpawnHandle for async_std::task::JoinHandle<()> {
} }
mod private { mod private {
use super::*;
pub trait Sealed {} pub trait Sealed {}
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
impl Sealed for Tokio1Executor {} impl Sealed for super::Tokio1Executor {}
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
impl Sealed for AsyncStd1Executor {} impl Sealed for super::AsyncStd1Executor {}
#[cfg(all(feature = "smtp-transport", feature = "tokio1"))] #[cfg(all(feature = "smtp-transport", feature = "tokio1"))]
impl Sealed for tokio1_crate::task::JoinHandle<()> {} impl Sealed for tokio1_crate::task::JoinHandle<()> {}

View File

@@ -41,6 +41,15 @@
//! //!
//! NOTE: native-tls isn't supported with `async-std` //! NOTE: native-tls isn't supported with `async-std`
//! //!
//! #### SMTP over TLS via the boring crate (Boring TLS)
//!
//! _Secure SMTP connections using TLS from the `boring-tls` crate_
//!
//! * **boring-tls**: TLS support for the synchronous version of the API
//! * **tokio1-boring-tls**: TLS support for the `tokio1` async version of the API
//!
//! NOTE: boring-tls isn't supported with `async-std`
//!
//! #### SMTP over TLS via the rustls crate //! #### SMTP over TLS via the rustls crate
//! //!
//! _Secure SMTP connections using TLS from the `rustls-tls` crate_ //! _Secure SMTP connections using TLS from the `rustls-tls` crate_
@@ -100,7 +109,7 @@
//! [mime 0.3]: https://docs.rs/mime/0.3 //! [mime 0.3]: https://docs.rs/mime/0.3
//! [DKIM]: https://datatracker.ietf.org/doc/html/rfc6376 //! [DKIM]: https://datatracker.ietf.org/doc/html/rfc6376
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.0-rc.5")] #![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.1")]
#![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)]
@@ -112,20 +121,38 @@
unused_import_braces, unused_import_braces,
rust_2018_idioms, rust_2018_idioms,
clippy::string_add, clippy::string_add,
clippy::string_add_assign clippy::string_add_assign,
clippy::clone_on_ref_ptr,
clippy::verbose_file_reads,
clippy::unnecessary_self_imports,
clippy::string_to_string,
clippy::mem_forget,
clippy::cast_lossless,
clippy::inefficient_to_string,
clippy::inline_always,
clippy::linkedlist,
clippy::macro_use_imports,
clippy::manual_assert,
clippy::unnecessary_join,
clippy::wildcard_imports,
clippy::zero_sized_map_values
)] )]
#![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
#[cfg(not(lettre_ignore_tls_mismatch))] #[cfg(not(lettre_ignore_tls_mismatch))]
mod compiletime_checks { mod compiletime_checks {
#[cfg(all(feature = "native-tls", feature = "boring-tls"))]
compile_error!("feature \"native-tls\" and feature \"boring-tls\" cannot be enabled at the same time, otherwise
the executable will fail to link.");
#[cfg(all( #[cfg(all(
feature = "tokio1", feature = "tokio1",
feature = "native-tls", feature = "native-tls",
not(feature = "tokio1-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. compile_error!("Lettre is being built with the `tokio1` and the `native-tls` features, but the `tokio1-native-tls` feature hasn't been turned on.
If you'd like to use rustls make sure that the `native-tls` hasn't been enabled by mistake (you may need to import lettre without default features) If you 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.
If you're building a library which depends on lettre import it without default features and enable just the features you need."); Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
#[cfg(all( #[cfg(all(
feature = "tokio1", feature = "tokio1",
@@ -133,8 +160,17 @@ If you're building a library which depends on lettre import it without default f
not(feature = "tokio1-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. compile_error!("Lettre is being built with the `tokio1` and the `rustls-tls` features, but the `tokio1-rustls-tls` feature hasn't been turned on.
If you'd like to use native-tls make sure that the `rustls-tls` hasn't been enabled by mistake. If you'd like to use `native-tls` make sure that the `rustls-tls` feature hasn't been enabled by mistake.
If you're building a library which depends on lettre import it without default features and enable just the features you need."); Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
#[cfg(all(
feature = "tokio1",
feature = "boring-tls",
not(feature = "tokio1-boring-tls")
))]
compile_error!("Lettre is being built with the `tokio1` and the `boring-tls` features, but the `tokio1-boring-tls` feature hasn't been turned on.
If you'd like to use `boring-tls` make sure that the `rustls-tls` feature hasn't been enabled by mistake.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
/* /*
#[cfg(all( #[cfg(all(
@@ -153,8 +189,8 @@ If you're building a library which depends on lettre import it without default f
))] ))]
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. compile_error!("Lettre is being built with the `async-std1` and the `native-tls` features, but the async-std integration doesn't support native-tls yet.
If you'd like to work on the issue please take a look at https://github.com/lettre/lettre/issues/576. If you'd like to work on the issue please take a look at https://github.com/lettre/lettre/issues/576.
If you'd like to use rustls make sure that the `native-tls` hasn't been enabled by mistake (you may need to import lettre without default features) If you 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.
If you're building a library which depends on lettre import lettre without default features and enable just the features you need."); Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
#[cfg(all( #[cfg(all(
feature = "async-std1", feature = "async-std1",
@@ -162,8 +198,8 @@ If you're building a library which depends on lettre import lettre without defau
not(feature = "async-std1-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. compile_error!("Lettre is being built with the `async-std1` and the `rustls-tls` features, but the `async-std1-rustls-tls` feature hasn't been turned on.
If you'd like to use native-tls make sure that the `rustls-tls` hasn't been enabled by mistake (you may need to import lettre without default features) If you'd like to use `native-tls` make sure that the `rustls-tls` hasn't been enabled by mistake.
If you're building a library which depends on lettre import it without default features and enable just the features you need."); Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
} }
pub mod address; pub mod address;

View File

@@ -1,8 +1,4 @@
use std::{ use std::{mem, ops::Deref};
io::{self, Write},
mem,
ops::Deref,
};
use crate::message::header::ContentTransferEncoding; use crate::message::header::ContentTransferEncoding;
@@ -41,7 +37,7 @@ impl Body {
pub fn new<B: Into<MaybeString>>(buf: B) -> Self { pub fn new<B: Into<MaybeString>>(buf: B) -> Self {
let mut buf: MaybeString = buf.into(); let mut buf: MaybeString = buf.into();
let encoding = buf.encoding(); let encoding = buf.encoding(false);
buf.encode_crlf(); buf.encode_crlf();
Self::new_impl(buf.into(), encoding) Self::new_impl(buf.into(), encoding)
} }
@@ -61,7 +57,22 @@ impl Body {
) -> Result<Self, Vec<u8>> { ) -> Result<Self, Vec<u8>> {
let mut buf: MaybeString = buf.into(); let mut buf: MaybeString = buf.into();
if !buf.is_encoding_ok(encoding) { let best_encoding = buf.encoding(true);
let ok = match (encoding, best_encoding) {
(ContentTransferEncoding::SevenBit, ContentTransferEncoding::SevenBit) => true,
(
ContentTransferEncoding::EightBit,
ContentTransferEncoding::SevenBit | ContentTransferEncoding::EightBit,
) => true,
(ContentTransferEncoding::SevenBit | ContentTransferEncoding::EightBit, _) => false,
(
ContentTransferEncoding::QuotedPrintable
| ContentTransferEncoding::Base64
| ContentTransferEncoding::Binary,
_,
) => true,
};
if !ok {
return Err(buf.into()); return Err(buf.into());
} }
@@ -91,36 +102,13 @@ impl Body {
Self::dangerous_pre_encoded(encoded, ContentTransferEncoding::QuotedPrintable) Self::dangerous_pre_encoded(encoded, ContentTransferEncoding::QuotedPrintable)
} }
ContentTransferEncoding::Base64 => { ContentTransferEncoding::Base64 => {
let base64_len = buf.len() * 4 / 3 + 4; let len = email_encoding::body::base64::encoded_len(buf.len());
let base64_endings_len = base64_len + base64_len / LINE_MAX_LENGTH;
let mut out = Vec::with_capacity(base64_endings_len); let mut out = String::with_capacity(len);
{ email_encoding::body::base64::encode(&buf, &mut out)
let writer = LineWrappingWriter::new(&mut out, LINE_MAX_LENGTH); .expect("encode body as base64");
let mut writer = base64::write::EncoderWriter::new(writer, base64::STANDARD);
// TODO: use writer.write_all(self.as_ref()).expect("base64 encoding never fails"); Self::dangerous_pre_encoded(out.into_bytes(), ContentTransferEncoding::Base64)
// modified Write::write_all to work around base64 crate bug
// TODO: remove once https://github.com/marshallpierce/rust-base64/issues/148 is fixed
{
let mut buf: &[u8] = buf.as_ref();
while !buf.is_empty() {
match writer.write(buf) {
Ok(0) => {
// ignore 0 writes
}
Ok(n) => {
buf = &buf[n..];
}
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {}
Err(e) => panic!("base64 encoding never fails: {}", e),
}
}
}
}
Self::dangerous_pre_encoded(out, ContentTransferEncoding::Base64)
} }
} }
} }
@@ -153,21 +141,20 @@ impl Body {
impl MaybeString { impl MaybeString {
/// Suggests the best `Content-Transfer-Encoding` to be used for this `MaybeString` /// Suggests the best `Content-Transfer-Encoding` to be used for this `MaybeString`
/// ///
/// If the `MaybeString` was created from a `String` composed only of US-ASCII /// The `binary` encoding is never returned
/// characters, with no lines longer than 1000 characters, then 7bit fn encoding(&self, supports_utf8: bool) -> ContentTransferEncoding {
/// encoding will be used, else quoted-printable will be chosen. use email_encoding::body::Encoding;
///
/// If the `MaybeString` was instead created from a `Vec<u8>`, base64 encoding is always let output = match self {
/// chosen. Self::String(s) => Encoding::choose(s.as_str(), supports_utf8),
/// Self::Binary(b) => Encoding::choose(b.as_slice(), supports_utf8),
/// `8bit` and `binary` encodings are never returned, as they may not be };
/// supported by all SMTP servers.
pub fn encoding(&self) -> ContentTransferEncoding { match output {
match &self { Encoding::SevenBit => ContentTransferEncoding::SevenBit,
Self::String(s) if is_7bit_encoded(s.as_ref()) => ContentTransferEncoding::SevenBit, Encoding::EightBit => ContentTransferEncoding::EightBit,
// TODO: consider when base64 would be a better option because of output size Encoding::QuotedPrintable => ContentTransferEncoding::QuotedPrintable,
Self::String(_) => ContentTransferEncoding::QuotedPrintable, Encoding::Base64 => ContentTransferEncoding::Base64,
Self::Binary(_) => ContentTransferEncoding::Base64,
} }
} }
@@ -178,18 +165,6 @@ impl MaybeString {
Self::Binary(_) => {} Self::Binary(_) => {}
} }
} }
/// Returns `true` if using `encoding` to encode this `MaybeString`
/// would result into an invalid encoded body.
fn is_encoding_ok(&self, encoding: ContentTransferEncoding) -> bool {
match encoding {
ContentTransferEncoding::SevenBit => is_7bit_encoded(self),
ContentTransferEncoding::EightBit => is_8bit_encoded(self),
ContentTransferEncoding::Binary
| ContentTransferEncoding::QuotedPrintable
| ContentTransferEncoding::Base64 => true,
}
}
} }
/// A trait for something that takes an encoded [`Body`]. /// A trait for something that takes an encoded [`Body`].
@@ -273,73 +248,6 @@ impl Deref for MaybeString {
} }
} }
/// Checks whether it contains only US-ASCII characters,
/// and no lines are longer than 1000 characters including the `\n` character.
///
/// Most efficient content encoding available
fn is_7bit_encoded(buf: &[u8]) -> bool {
buf.is_ascii() && !contains_too_long_lines(buf)
}
/// Checks that no lines are longer than 1000 characters,
/// including the `\n` character.
/// NOTE: 8bit isn't supported by all SMTP servers.
fn is_8bit_encoded(buf: &[u8]) -> bool {
!contains_too_long_lines(buf)
}
/// Checks if there are lines that are longer than 1000 characters,
/// including the `\n` character.
fn contains_too_long_lines(buf: &[u8]) -> bool {
buf.len() > 1000 && buf.split(|&b| b == b'\n').any(|line| line.len() > 999)
}
const LINE_SEPARATOR: &[u8] = b"\r\n";
const LINE_MAX_LENGTH: usize = 78 - LINE_SEPARATOR.len();
/// A `Write`r that inserts a line separator `\r\n` every `max_line_length` bytes.
struct LineWrappingWriter<'a, W> {
writer: &'a mut W,
current_line_length: usize,
max_line_length: usize,
}
impl<'a, W> LineWrappingWriter<'a, W> {
pub fn new(writer: &'a mut W, max_line_length: usize) -> Self {
Self {
writer,
current_line_length: 0,
max_line_length,
}
}
}
impl<'a, W> Write for LineWrappingWriter<'a, W>
where
W: Write,
{
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let remaining_line_len = self.max_line_length - self.current_line_length;
let write_len = std::cmp::min(buf.len(), remaining_line_len);
self.writer.write_all(&buf[..write_len])?;
if remaining_line_len == write_len {
self.writer.write_all(LINE_SEPARATOR)?;
self.current_line_length = 0;
} else {
self.current_line_length += write_len;
}
Ok(write_len)
}
fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
}
}
/// 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);
@@ -377,6 +285,8 @@ fn find_all_lf_char_indices(s: &str) -> Vec<usize> {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq;
use super::{in_place_crlf_line_endings, Body, ContentTransferEncoding}; use super::{in_place_crlf_line_endings, Body, ContentTransferEncoding};
#[test] #[test]
@@ -509,13 +419,10 @@ mod test {
#[test] #[test]
fn quoted_printable_detect() { fn quoted_printable_detect() {
let encoded = Body::new(String::from("Привет, мир!")); let encoded = Body::new(String::from("Questo messaggio è corto"));
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable); assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
assert_eq!( assert_eq!(encoded.as_ref(), b"Questo messaggio =C3=A8 corto");
encoded.as_ref(),
b"=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!".as_ref()
);
} }
#[test] #[test]
@@ -547,14 +454,17 @@ mod test {
#[test] #[test]
fn quoted_printable_encode_line_wrap() { fn quoted_printable_encode_line_wrap() {
let encoded = Body::new(String::from("Текст письма в уникоде")); let encoded = Body::new(String::from(
"Se lo standard 📬 fosse stato più semplice avremmo finito molto prima.",
));
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable); assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
println!("{}", std::str::from_utf8(encoded.as_ref()).unwrap());
assert_eq!( assert_eq!(
encoded.as_ref(), encoded.as_ref(),
concat!( concat!(
"=D0=A2=D0=B5=D0=BA=D1=81=D1=82 =D0=BF=D0=B8=D1=81=D1=8C=D0=BC=D0=B0 =D0=B2 =\r\n", "Se lo standard =F0=9F=93=AC fosse stato pi=C3=B9 semplice avremmo finito mo=\r\n",
"=D1=83=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5" "lto prima."
) )
.as_bytes() .as_bytes()
); );
@@ -562,21 +472,25 @@ mod test {
#[test] #[test]
fn base64_detect() { fn base64_detect() {
let input = Body::new(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); let input = Body::new(vec![0; 80]);
let encoding = input.encoding(); let encoding = input.encoding();
assert_eq!(encoding, ContentTransferEncoding::Base64); assert_eq!(encoding, ContentTransferEncoding::Base64);
} }
#[test] #[test]
fn base64_encode_bytes() { fn base64_encode_bytes() {
let encoded = Body::new_with_encoding( let encoded =
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9], Body::new_with_encoding(vec![0; 80], ContentTransferEncoding::Base64).unwrap();
ContentTransferEncoding::Base64,
)
.unwrap();
assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64); assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
assert_eq!(encoded.as_ref(), b"AAECAwQFBgcICQ=="); assert_eq!(
encoded.as_ref(),
concat!(
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n",
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
)
.as_bytes()
);
} }
#[test] #[test]

View File

@@ -1,14 +1,14 @@
use std::{ use std::{
borrow::Cow, borrow::Cow,
error::Error as StdError, error::Error as StdError,
fmt::{self, Display, Write}, fmt::{self, Display},
iter::IntoIterator, iter::IntoIterator,
time::SystemTime, time::SystemTime,
}; };
use ed25519_dalek::Signer; use ed25519_dalek::Signer;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::{bytes::Regex as BRegex, Regex}; use regex::bytes::Regex;
use rsa::{pkcs1::DecodeRsaPrivateKey, Hash, PaddingScheme, RsaPrivateKey}; use rsa::{pkcs1::DecodeRsaPrivateKey, Hash, PaddingScheme, RsaPrivateKey};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
@@ -96,9 +96,9 @@ impl Display for DkimSigningKeyError {
impl StdError for DkimSigningKeyError { impl StdError for DkimSigningKeyError {
fn source(&self) -> Option<&(dyn StdError + 'static)> { fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(match &self.0 { Some(match &self.0 {
InnerDkimSigningKeyError::Base64(err) => &*err, InnerDkimSigningKeyError::Base64(err) => err,
InnerDkimSigningKeyError::Rsa(err) => &*err, InnerDkimSigningKeyError::Rsa(err) => err,
InnerDkimSigningKeyError::Ed25519(err) => &*err, InnerDkimSigningKeyError::Ed25519(err) => err,
}) })
} }
} }
@@ -115,12 +115,12 @@ enum InnerDkimSigningKey {
impl DkimSigningKey { impl DkimSigningKey {
pub fn new( pub fn new(
private_key: String, private_key: &str,
algorithm: DkimSigningAlgorithm, algorithm: DkimSigningAlgorithm,
) -> Result<DkimSigningKey, DkimSigningKeyError> { ) -> Result<DkimSigningKey, DkimSigningKeyError> {
Ok(Self(match algorithm { Ok(Self(match algorithm {
DkimSigningAlgorithm::Rsa => InnerDkimSigningKey::Rsa( DkimSigningAlgorithm::Rsa => InnerDkimSigningKey::Rsa(
RsaPrivateKey::from_pkcs1_pem(&private_key) RsaPrivateKey::from_pkcs1_pem(private_key)
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Rsa(err)))?, .map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Rsa(err)))?,
), ),
DkimSigningAlgorithm::Ed25519 => { DkimSigningAlgorithm::Ed25519 => {
@@ -222,9 +222,9 @@ fn dkim_canonicalize_body(
body: &[u8], body: &[u8],
canonicalization: DkimCanonicalizationType, canonicalization: DkimCanonicalizationType,
) -> Cow<'_, [u8]> { ) -> Cow<'_, [u8]> {
static RE: Lazy<BRegex> = Lazy::new(|| BRegex::new("(\r\n)+$").unwrap()); static RE: Lazy<Regex> = Lazy::new(|| Regex::new("(\r\n)+$").unwrap());
static RE_DOUBLE_SPACE: Lazy<BRegex> = Lazy::new(|| BRegex::new("[\\t ]+").unwrap()); static RE_DOUBLE_SPACE: Lazy<Regex> = Lazy::new(|| Regex::new("[\\t ]+").unwrap());
static RE_SPACE_EOL: Lazy<BRegex> = Lazy::new(|| BRegex::new("[\t ]\r\n").unwrap()); static RE_SPACE_EOL: Lazy<Regex> = Lazy::new(|| Regex::new("[\t ]\r\n").unwrap());
match canonicalization { match canonicalization {
DkimCanonicalizationType::Simple => RE.replace(body, &b"\r\n"[..]), DkimCanonicalizationType::Simple => RE.replace(body, &b"\r\n"[..]),
DkimCanonicalizationType::Relaxed => { DkimCanonicalizationType::Relaxed => {
@@ -241,23 +241,59 @@ fn dkim_canonicalize_body(
} }
} }
/// Canonicalize the value of an header fn dkim_canonicalize_headers_relaxed(headers: &str) -> String {
fn dkim_canonicalize_header_value( let mut r = String::with_capacity(headers.len());
value: &str,
canonicalization: DkimCanonicalizationType, fn skip_whitespace(h: &str) -> &str {
) -> Cow<'_, str> { match h.as_bytes().first() {
match canonicalization { Some(b' ' | b'\t') => skip_whitespace(&h[1..]),
DkimCanonicalizationType::Simple => Cow::Borrowed(value), _ => h,
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()
))
} }
} }
fn name(h: &str, out: &mut String) {
if let Some(name_end) = h.bytes().position(|c| c == b':') {
let (name, rest) = h.split_at(name_end + 1);
*out += name;
// Space after header colon is stripped.
value(skip_whitespace(rest), out);
} else {
// This should never happen.
*out += h;
}
}
fn value(h: &str, out: &mut String) {
match h.as_bytes() {
// Continuation lines.
[b'\r', b'\n', b' ' | b'\t', ..] => {
out.push(' ');
value(skip_whitespace(&h[2..]), out);
}
// End of header.
[b'\r', b'\n', ..] => {
*out += "\r\n";
name(&h[2..], out)
}
// Sequential whitespace.
[b' ' | b'\t', b' ' | b'\t' | b'\r', ..] => value(&h[1..], out),
// All whitespace becomes spaces.
[b'\t', ..] => {
out.push(' ');
value(&h[1..], out)
}
[_, ..] => {
let mut chars = h.chars();
out.push(chars.next().unwrap());
value(chars.as_str(), out)
}
[] => {}
}
}
name(headers, &mut r);
r
} }
/// Canonicalize header tag /// Canonicalize header tag
@@ -277,47 +313,35 @@ fn dkim_canonicalize_headers<'a>(
mail_headers: &Headers, mail_headers: &Headers,
canonicalization: DkimCanonicalizationType, canonicalization: DkimCanonicalizationType,
) -> String { ) -> String {
let mut covered_headers = Headers::new();
for name in headers_list {
if let Some(h) = mail_headers.find_header(name) {
let name = dkim_canonicalize_header_tag(name, canonicalization);
covered_headers.insert_raw(HeaderValue::dangerous_new_pre_encoded(
HeaderName::new_from_ascii(name.into()).unwrap(),
h.get_raw().into(),
h.get_encoded().into(),
));
}
}
let serialized = covered_headers.to_string();
match canonicalization { match canonicalization {
DkimCanonicalizationType::Simple => { DkimCanonicalizationType::Simple => serialized,
let mut signed_headers = Headers::new(); DkimCanonicalizationType::Relaxed => dkim_canonicalize_headers_relaxed(&serialized),
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 /// Sign with Dkim a message by adding Dkim-Signture header created with configuration expressed by
/// dkim_config /// dkim_config
pub(super) fn dkim_sign(message: &mut Message, dkim_config: &DkimConfig) {
let timestamp = SystemTime::now() pub fn dkim_sign(message: &mut Message, dkim_config: &DkimConfig) {
dkim_sign_fixed_time(message, dkim_config, SystemTime::now())
}
fn dkim_sign_fixed_time(message: &mut Message, dkim_config: &DkimConfig, timestamp: SystemTime) {
let timestamp = timestamp
.duration_since(SystemTime::UNIX_EPOCH) .duration_since(SystemTime::UNIX_EPOCH)
.unwrap() .unwrap()
.as_secs(); .as_secs();
@@ -385,21 +409,46 @@ pub(super) fn dkim_sign(message: &mut Message, dkim_config: &DkimConfig) {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::{ use pretty_assertions::assert_eq;
io::Write,
process::{Command, Stdio},
};
use super::{ use super::{
super::{ super::{
header::{HeaderName, HeaderValue}, header::{HeaderName, HeaderValue},
Header, Message, Header, Message,
}, },
dkim_canonicalize_body, dkim_canonicalize_header_value, dkim_canonicalize_headers, dkim_canonicalize_headers, dkim_sign_fixed_time, DkimCanonicalization,
DkimCanonicalizationType, DkimConfig, DkimSigningAlgorithm, DkimSigningKey, DkimCanonicalizationType, DkimConfig, DkimSigningAlgorithm, DkimSigningKey,
}; };
use crate::StdError; use crate::StdError;
const KEY_RSA: &str = "-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAwOsW7UFcWn1ch3UM8Mll5qZH5hVHKJQ8Z0tUlebUECq0vjw6
VcsIucZ/B70VpCN63whyi7oApdCIS1o0zad7f0UaW/BfxXADqdcFL36uMaG0RHer
uSASjQGnsl9Kozt/dXiDZX5ngjr/arLJhNZSNR4/9VSwqbE2OPXaSaQ9BsqneD0P
8dCVSfkkDZCcfC2864z7hvC01lFzWQKF36ZAoGBERHScHtFMAzUOgGuqqPiP5khw
DQB3Ffccf+BsWLU2OOteshUwTGjpoangbPCYj6kckwNm440lQwuqTinpC92yyIE5
Ol8psNMW49DLowAeZb6JrjLhD+wY9bghTaOkcwIDAQABAoIBAHTZ8LkkrdvhsvoZ
XA088AwVC9fBa6iYoT2v0zw45JomQ/Q2Zt8wa8ibAradQU56byJI65jWwS2ucd+y
c+ldWOBt6tllb50XjCCDrRBnmvtVBuux0MIBOztNlVXlgj/8+ecdZ/lB51Bqi+sF
ACsF5iVmfTcMZTVjsYQu5llUseI6Lwgqpx6ktaXD2PVsVo9Gf01ssZ4GCy69wB/3
20CsOz4LEpSYkq1oE98lMMGCfD7py3L9kWHYNNisam78GM+1ynRxRGwEDUbz6pxs
fGPIAwHLaZsOmibPkBB0PJTW742w86qQ8KAqC6ZbRYOF19rSMj3oTfRnPMHn9Uu5
N8eQcoECgYEA97SMUrz2hqII5i8igKylO9kV8pjcIWKI0rdt8MKj4FXTNYjjO9I+
41ONOjhUOpFci/G3YRKi8UiwbKxIRTvIxNMh2xj6Ws3iO9gQHK1j8xTWxJdjEBEz
EuZI59Mi5H7fxSL1W+n8nS8JVsaH93rvQErngqTUAsihAzjxHWdFwm0CgYEAx2Dh
claESJP2cOKgYp+SUNwc26qMaqnl1f37Yn+AflrQOfgQqJe5TRbicEC+nFlm6XUt
3st1Nj29H0uOMmMZDmDCO+cOs5Qv5A9pG6jSC6wM+2KNHQDtrxlakBFygePEPVVy
GXaY9DRa9Q4/4ataxDR2/VvIAWfEEtMTJIBDtl8CgYAIXEuwLziS6r0qJ8UeWrVp
A7a97XLgnZbIpfBMBAXL+JmcYPZqenos6hEGOgh9wZJCFvJ9kEd3pWBvCpGV5KKu
IgIuhvVMQ06zfmNs1F1fQwDMud9aF3qF1Mf5KyMuWynqWXe2lns0QvYpu6GzNK8G
mICf5DhTr7nfhfh9aZLtMQKBgCxKsmqzG5n//MxhHB4sstVxwJtwDNeZPKzISnM8
PfBT/lQSbqj1Y73japRjXbTgC4Ore3A2JKjTGFN+dm1tJGDUT/H8x4BPWEBCyCfT
3i2noA6sewrJbQPsDvlYVubSEYNKmxlbBmmhw98StlBMv9I8kX6BSDI/uggwid0e
/WvjAoGBAKpZ0UOKQyrl9reBiUfrpRCvIMakBMd79kNiH+5y0Soq/wCAnAuABayj
XEIBhFv+HxeLEnT7YV+Zzqp5L9kKw/EU4ik3JX/XsEihdSxEuGX00ZYOw05FEfpW
cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT
-----END RSA PRIVATE KEY-----";
#[derive(Clone)] #[derive(Clone)]
struct TestHeader(String); struct TestHeader(String);
@@ -417,47 +466,11 @@ mod test {
} }
} }
#[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 { fn test_message() -> Message {
Message::builder() Message::builder()
.from("Test <test+ezrz@example.net>".parse().unwrap()) .from("Test O'Leary <test+ezrz@example.net>".parse().unwrap())
.to("Test2 <test2@example.org>".parse().unwrap()) .to("Test2 <test2@example.org>".parse().unwrap())
.date(std::time::UNIX_EPOCH)
.header(TestHeader("test test very very long with spaces and extra spaces \twill be folded to several lines ".to_string())) .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 ë") .subject("Test with utf-8 ë")
.body("test\r\n\r\ntest \ttest\r\n\r\n\r\n".to_string()).unwrap() .body("test\r\n\r\ntest \ttest\r\n\r\n\r\n".to_string()).unwrap()
@@ -466,63 +479,112 @@ mod test {
#[test] #[test]
fn test_headers_simple_canonicalize() { fn test_headers_simple_canonicalize() {
let message = test_message(); 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") dbg!(message.headers.to_string());
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Simple), "From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\nTest: test test very very long with spaces and extra spaces \twill be\r\n folded to several lines \r\n")
} }
#[test] #[test]
fn test_headers_relaxed_canonicalize() { fn test_headers_relaxed_canonicalize() {
let message = test_message(); 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") dbg!(message.headers.to_string());
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Relaxed),"from:=?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\ntest:test test very very long with spaces and extra spaces will be folded to several lines\r\n")
} }
#[test] #[test]
fn test_signature_rsa() { fn test_signature_rsa_simple() {
let mut message = test_message(); let mut message = test_message();
let key = "-----BEGIN RSA PRIVATE KEY----- let signing_key = DkimSigningKey::new(KEY_RSA, DkimSigningAlgorithm::Rsa).unwrap();
MIIEpAIBAAKCAQEAz+FHbM8BwkBBz/Ux5OYLQ5Bp1HVuCHTP6Rr3HXTnome/2cGl dkim_sign_fixed_time(
/ze0tsmmFbCjjsS89MXbMGs9xJhjv18LmL1N0UTllblOizzVjorQyN4RwBOfG34j &mut message,
7SS56pwzrA738Ry8FAbL5InPWEgVzbOhXuTCs8yuzcqTnm4sH/csnIl7cMWeQkVn &DkimConfig::new(
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(), "dkimtest".to_string(),
"example.org".to_string(), "example.org".to_string(),
signing_key, signing_key,
)); vec![
println!("{}", std::str::from_utf8(&message.formatted()).unwrap()); HeaderName::new_from_ascii_str("Date"),
let mut verify_command = Command::new("dkimverify") HeaderName::new_from_ascii_str("From"),
.stdin(Stdio::piped()) HeaderName::new_from_ascii_str("Subject"),
.spawn() HeaderName::new_from_ascii_str("To"),
.expect("Fail to verify message signature"); ],
let mut stdin = verify_command.stdin.take().expect("Failed to open stdin"); DkimCanonicalization {
std::thread::spawn(move || { header: DkimCanonicalizationType::Simple,
stdin body: DkimCanonicalizationType::Simple,
.write_all(&message.formatted()) },
.expect("Failed to write to stdin"); ),
}); std::time::UNIX_EPOCH,
assert!(verify_command );
.wait() let signed = message.formatted();
.expect("Command did not run") let signed = std::str::from_utf8(&signed).unwrap();
.success()); assert_eq!(
signed,
std::concat!(
"From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\n",
"To: Test2 <test2@example.org>\r\n",
"Date: Thu, 01 Jan 1970 00:00:00 +0000\r\n",
"Test: test test very very long with spaces and extra spaces \twill be\r\n",
" folded to several lines \r\n",
"Subject: Test with utf-8 =?utf-8?b?w6s=?=\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"DKIM-Signature: v=1; a=rsa-sha256; d=example.org; s=dkimtest;\r\n",
" c=simple/simple; q=dns/txt; t=0; h=Date:From:Subject:To;\r\n",
" bh=f3Zksdcjqa/xRBwdyFzIXWCcgP7XTgxjCgYsXOMKQl4=;\r\n",
" b=NhoIMMAALoSgu5lKAR0+MUQunOWnU7wpF9ORUFtpxq9sGZDo9AX43AMhFemyM5W204jpFwMU6pm7AMR1nOYBdSYye4yUALtvT2nqbJBwSh7JeYu+z22t1RFKp7qQR1il8aSrkbZuNMFHYuSEwW76QtKwcNqP4bQOzS9CzgQp0ABu8qwYPBr/EypykPTfqjtyN+ywrfdqjjGOzTpRGolH0hc3CrAETNjjHbNBgKgucXmXTN7hMRdzqWjeFPxizXwouwNAavFClPG0l33gXVArFWn+CkgA84G/s4zuJiF7QPZR87Pu4pw/vIlSXxH4a42W3tT19v9iBTH7X7ldYegtmQ==\r\n",
"\r\n",
"test\r\n",
"\r\n",
"test \ttest\r\n",
"\r\n",
"\r\n",
)
);
}
#[test]
fn test_signature_rsa_relaxed() {
let mut message = test_message();
let signing_key = DkimSigningKey::new(KEY_RSA, DkimSigningAlgorithm::Rsa).unwrap();
dkim_sign_fixed_time(
&mut message,
&DkimConfig::new(
"dkimtest".to_string(),
"example.org".to_string(),
signing_key,
vec![
HeaderName::new_from_ascii_str("Date"),
HeaderName::new_from_ascii_str("From"),
HeaderName::new_from_ascii_str("Subject"),
HeaderName::new_from_ascii_str("To"),
],
DkimCanonicalization {
header: DkimCanonicalizationType::Relaxed,
body: DkimCanonicalizationType::Relaxed,
},
),
std::time::UNIX_EPOCH,
);
let signed = message.formatted();
let signed = std::str::from_utf8(&signed).unwrap();
println!("{}", signed);
assert_eq!(
signed,
std::concat!(
"From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\n",
"To: Test2 <test2@example.org>\r\n",
"Date: Thu, 01 Jan 1970 00:00:00 +0000\r\n",
"Test: test test very very long with spaces and extra spaces \twill be\r\n",
" folded to several lines \r\n","Subject: Test with utf-8 =?utf-8?b?w6s=?=\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"DKIM-Signature: v=1; a=rsa-sha256; d=example.org; s=dkimtest;\r\n",
" c=relaxed/relaxed; q=dns/txt; t=0; h=date:from:subject:to;\r\n",
" bh=qN8je6qJgWFGSnN2MycC/XKPbN6BOrMJyAX2h4m19Ss=;\r\n",
" b=YaVfmH8dbGEywoLJ4uhbvYqDyQG1UGKFH3PE7zXGgk+YFxUgkwWjoA3aQupDNQtfTjfUsNe0dnrjyZP+ylnESpZBpbCIf5/n3FEh6j3RQthqNbQblcfH/U8mazTuRbVjYBbTZQDaQCMPTz+8D+ZQfXo2oq6dGzTuGvmuYft0CVsq/BIp/EkhZHqiphDeVJSHD4iKW8+L2XwEWThoY92xOYc1G0TtBwz2UJgtiHX2YulH/kRBHeK3dKn9RTNVL3VZ+9ZrnFwIhET9TPGtU2I+q0EMSWF9H9bTrASMgW/U+E0VM2btqJlrTU6rQ7wlQeHdwecLnzXcyhCUInF1+veMNw==\r\n",
"\r\n",
"test\r\n",
"\r\n",
"test \ttest\r\n",
"\r\n",
"\r\n",
)
);
} }
} }

View File

@@ -75,6 +75,8 @@ impl Default for ContentTransferEncoding {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq;
use super::ContentTransferEncoding; use super::ContentTransferEncoding;
use crate::message::header::{HeaderName, HeaderValue, Headers}; use crate::message::header::{HeaderName, HeaderValue, Headers};

View File

@@ -77,6 +77,8 @@ impl Header for ContentDisposition {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq;
use super::ContentDisposition; use super::ContentDisposition;
use crate::message::header::{HeaderName, HeaderValue, Headers}; use crate::message::header::{HeaderName, HeaderValue, Headers};

View File

@@ -149,6 +149,8 @@ mod serde {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq;
use super::ContentType; use super::ContentType;
use crate::message::header::{HeaderName, HeaderValue, Headers}; use crate::message::header::{HeaderName, HeaderValue, Headers};

View File

@@ -8,7 +8,7 @@ use crate::BoxError;
/// Message `Date` header /// Message `Date` header
/// ///
/// Defined in [RFC2822](https://tools.ietf.org/html/rfc2822#section-3.3) /// Defined in [RFC2822](https://tools.ietf.org/html/rfc2822#section-3.3)
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Date(HttpDate); pub struct Date(HttpDate);
impl Date { impl Date {
@@ -32,11 +32,11 @@ 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");
} }
@@ -49,9 +49,9 @@ impl Header for Date {
// 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`
val.truncate(val.len() - "GMT".len()); val.truncate(val.len() - "GMT".len());
val.push_str("-0000"); val.push_str("+0000");
} }
HeaderValue::dangerous_new_pre_encoded(Self::name(), val.clone(), val) HeaderValue::dangerous_new_pre_encoded(Self::name(), val.clone(), val)
@@ -74,6 +74,8 @@ impl From<Date> for SystemTime {
mod test { mod test {
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
use pretty_assertions::assert_eq;
use super::Date; use super::Date;
use crate::message::header::{HeaderName, HeaderValue, Headers}; use crate::message::header::{HeaderName, HeaderValue, Headers};
@@ -88,7 +90,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 +100,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"
); );
} }
@@ -108,7 +110,7 @@ mod test {
headers.insert_raw(HeaderValue::new( 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!(
@@ -120,7 +122,7 @@ mod test {
headers.insert_raw(HeaderValue::new( 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!(

View File

@@ -14,7 +14,7 @@ pub trait MailboxesHeader {
macro_rules! mailbox_header { macro_rules! mailbox_header {
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => { ($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
$(#[$doc])* $(#[$doc])*
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct $type_name(Mailbox); pub struct $type_name(Mailbox);
impl Header for $type_name { impl Header for $type_name {
@@ -56,7 +56,7 @@ macro_rules! mailbox_header {
macro_rules! mailboxes_header { macro_rules! mailboxes_header {
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => { ($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
$(#[$doc])* $(#[$doc])*
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct $type_name(pub(crate) Mailboxes); pub struct $type_name(pub(crate) Mailboxes);
impl MailboxesHeader for $type_name { impl MailboxesHeader for $type_name {
@@ -172,6 +172,8 @@ mailboxes_header! {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq;
use super::{From, Mailbox, Mailboxes}; use super::{From, Mailbox, Mailboxes};
use crate::message::header::{HeaderName, HeaderValue, Headers}; use crate::message::header::{HeaderName, HeaderValue, Headers};
@@ -296,4 +298,31 @@ mod test {
assert_eq!(headers.get::<From>(), Some(From(from.into()))); assert_eq!(headers.get::<From>(), Some(From(from.into())));
} }
#[test]
fn parse_multi_with_name_containing_comma() {
let from: Vec<Mailbox> = vec![
"Test, test <1@example.com>".parse().unwrap(),
"Test2, test2 <2@example.com>".parse().unwrap(),
];
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"Test, test <1@example.com>, Test2, test2 <2@example.com>".to_string(),
));
assert_eq!(headers.get::<From>(), Some(From(from.into())));
}
#[test]
fn parse_multi_with_name_containing_comma_last_broken() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"Test, test <1@example.com>, Test2, test2".to_string(),
));
assert_eq!(headers.get::<From>(), None);
}
} }

View File

@@ -3,10 +3,12 @@
use std::{ use std::{
borrow::Cow, borrow::Cow,
error::Error, error::Error,
fmt::{self, Display, Formatter}, fmt::{self, Display, Formatter, Write},
ops::Deref, ops::Deref,
}; };
use email_encoding::headers::EmailWriter;
pub use self::{ pub use self::{
content::*, content::*,
content_disposition::ContentDisposition, content_disposition::ContentDisposition,
@@ -121,7 +123,7 @@ impl Headers {
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<&HeaderValue> { pub(crate) fn find_header(&self, name: &str) -> Option<&HeaderValue> {
self.headers self.headers
.iter() .iter()
.find(|value| name.eq_ignore_ascii_case(&value.name)) .find(|value| name.eq_ignore_ascii_case(&value.name))
@@ -275,6 +277,7 @@ impl PartialEq<HeaderName> for &str {
} }
} }
/// A safe for use header value
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct HeaderValue { pub struct HeaderValue {
name: HeaderName, name: HeaderName,
@@ -283,6 +286,12 @@ pub struct HeaderValue {
} }
impl HeaderValue { impl HeaderValue {
/// Construct a new `HeaderValue` and encode it
///
/// Takes the header `name` and the `raw_value` and encodes
/// it via `RFC2047` and line folds it.
///
/// [`RFC2047`]: https://datatracker.ietf.org/doc/html/rfc2047
pub fn new(name: HeaderName, raw_value: String) -> Self { pub fn new(name: HeaderName, raw_value: String) -> Self {
let mut encoded_value = String::with_capacity(raw_value.len()); let mut encoded_value = String::with_capacity(raw_value.len());
HeaderValueEncoder::encode(&name, &raw_value, &mut encoded_value).unwrap(); HeaderValueEncoder::encode(&name, &raw_value, &mut encoded_value).unwrap();
@@ -294,6 +303,14 @@ impl HeaderValue {
} }
} }
/// Construct a new `HeaderValue` using a pre-encoded header value
///
/// This method is _extremely_ dangerous as it opens up
/// the encoder to header injection attacks, but is sometimes
/// acceptable for use if `encoded_value` contains only ascii
/// printable characters and is already line folded.
///
/// When in doubt use [`HeaderValue::new`].
pub fn dangerous_new_pre_encoded( pub fn dangerous_new_pre_encoded(
name: HeaderName, name: HeaderName,
raw_value: String, raw_value: String,
@@ -305,57 +322,46 @@ impl HeaderValue {
encoded_value, encoded_value,
} }
} }
pub(crate) fn get_raw(&self) -> &str {
&self.raw_value
}
pub(crate) fn get_encoded(&self) -> &str {
&self.encoded_value
}
} }
const ENCODING_START_PREFIX: &str = "=?utf-8?b?";
const ENCODING_END_SUFFIX: &str = "?=";
const MAX_LINE_LEN: usize = 76;
/// [RFC 1522](https://tools.ietf.org/html/rfc1522) header value encoder /// [RFC 1522](https://tools.ietf.org/html/rfc1522) header value encoder
struct HeaderValueEncoder { struct HeaderValueEncoder<'a> {
line_len: usize, writer: EmailWriter<'a>,
encode_buf: String, encode_buf: String,
} }
impl HeaderValueEncoder { impl<'a> HeaderValueEncoder<'a> {
fn encode(name: &str, value: &str, f: &mut impl fmt::Write) -> fmt::Result { fn encode(name: &str, value: &'a str, f: &'a mut impl fmt::Write) -> fmt::Result {
let (words_iter, encoder) = Self::new(name, value); let (words_iter, encoder) = Self::new(name, value, f);
encoder.format(words_iter, f) encoder.format(words_iter)
} }
fn new<'a>(name: &str, value: &'a str) -> (WordsPlusFillIterator<'a>, Self) { fn new(
name: &str,
value: &'a str,
writer: &'a mut dyn Write,
) -> (WordsPlusFillIterator<'a>, Self) {
let line_len = name.len() + ": ".len();
let writer = EmailWriter::new(writer, line_len, false);
( (
WordsPlusFillIterator { s: value }, WordsPlusFillIterator { s: value },
Self { Self {
line_len: name.len() + ": ".len(), writer,
encode_buf: String::new(), encode_buf: String::new(),
}, },
) )
} }
fn format( fn format(mut self, words_iter: WordsPlusFillIterator<'_>) -> fmt::Result {
mut self,
words_iter: WordsPlusFillIterator<'_>,
f: &mut impl fmt::Write,
) -> fmt::Result {
/// Estimate if an encoded string of `len` would fix in an empty line
fn would_fit_new_line(len: usize) -> bool {
len < (MAX_LINE_LEN - " ".len())
}
/// Estimate how long a string of `len` would be after base64 encoding plus
/// adding the encoding prefix and suffix to it
fn base64_len(len: usize) -> usize {
ENCODING_START_PREFIX.len() + (len * 4 / 3 + 4) + ENCODING_END_SUFFIX.len()
}
/// Estimate how many more bytes we can fit in the current line
fn available_len_to_max_encode_len(len: usize) -> usize {
len.saturating_sub(
ENCODING_START_PREFIX.len() + (len * 3 / 4 + 4) + ENCODING_END_SUFFIX.len(),
)
}
for next_word in words_iter { for next_word in words_iter {
let allowed = allowed_str(next_word); let allowed = allowed_str(next_word);
@@ -363,161 +369,64 @@ impl HeaderValueEncoder {
// This word only contains allowed characters // This word only contains allowed characters
// the next word is allowed, but we may have accumulated some words to encode // the next word is allowed, but we may have accumulated some words to encode
self.flush_encode_buf(f, true)?; self.flush_encode_buf()?;
if next_word.len() > self.remaining_line_len() { self.writer.folding().write_str(next_word)?;
// not enough space left on this line to encode word
if self.something_written_to_this_line() && would_fit_new_line(next_word.len())
{
// word doesn't fit this line, but something had already been written to it,
// and word would fit the next line, so go to a new line
// so go to new line
self.new_line(f)?;
} else {
// word neither fits this line and the next one, cut it
// in the middle and make it fit
let mut next_word = next_word;
while !next_word.is_empty() {
if self.remaining_line_len() == 0 {
self.new_line(f)?;
}
let len = self.remaining_line_len().min(next_word.len());
let first_part = &next_word[..len];
next_word = &next_word[len..];
f.write_str(first_part)?;
self.line_len += first_part.len();
}
continue;
}
}
// word fits, write it!
f.write_str(next_word)?;
self.line_len += next_word.len();
} else { } else {
// This word contains unallowed characters // This word contains unallowed characters
if self.remaining_line_len() >= base64_len(self.encode_buf.len() + next_word.len())
{
// next_word fits
self.encode_buf.push_str(next_word); self.encode_buf.push_str(next_word);
continue;
}
// next_word doesn't fit this line
if would_fit_new_line(base64_len(next_word.len())) {
// ...but it would fit the next one
self.flush_encode_buf(f, false)?;
self.new_line(f)?;
self.encode_buf.push_str(next_word);
continue;
}
// ...and also wouldn't fit the next one.
// chop it up into pieces
let mut next_word = next_word;
while !next_word.is_empty() {
let mut len = available_len_to_max_encode_len(self.remaining_line_len())
.min(next_word.len());
if len == 0 {
self.flush_encode_buf(f, false)?;
self.new_line(f)?;
}
// avoid slicing on a char boundary
while !next_word.is_char_boundary(len) {
len += 1;
}
let first_part = &next_word[..len];
next_word = &next_word[len..];
self.encode_buf.push_str(first_part);
}
} }
} }
self.flush_encode_buf(f, false)?; self.flush_encode_buf()?;
Ok(()) Ok(())
} }
/// Returns the number of bytes left for the current line fn flush_encode_buf(&mut self) -> fmt::Result {
fn remaining_line_len(&self) -> usize {
MAX_LINE_LEN - self.line_len
}
/// Returns true if something has been written to the current line
fn something_written_to_this_line(&self) -> bool {
self.line_len > 1
}
fn flush_encode_buf(
&mut self,
f: &mut impl fmt::Write,
switching_to_allowed: bool,
) -> fmt::Result {
if self.encode_buf.is_empty() { if self.encode_buf.is_empty() {
// nothing to encode // nothing to encode
return Ok(()); return Ok(());
} }
let mut write_after = None; // It is important that we don't encode leading whitespace otherwise it breaks wrapping.
let first_not_allowed = self
.encode_buf
.bytes()
.enumerate()
.find(|(_i, c)| !allowed_char(*c))
.map(|(i, _)| i);
// May as well also write the tail in plain text.
let last_not_allowed = self
.encode_buf
.bytes()
.enumerate()
.rev()
.find(|(_i, c)| !allowed_char(*c))
.map(|(i, _)| i + 1);
if switching_to_allowed { let (prefix, to_encode, suffix) = match first_not_allowed {
// If the next word only contains allowed characters, and the string to encode Some(first_not_allowed) => {
// ends with a space, take the space out of the part to encode let last_not_allowed = last_not_allowed.unwrap();
let last_char = self.encode_buf.pop().expect("self.encode_buf isn't empty"); let (remaining, suffix) = self.encode_buf.split_at(last_not_allowed);
if is_space_like(last_char) { let (prefix, to_encode) = remaining.split_at(first_not_allowed);
write_after = Some(last_char);
} else { (prefix, to_encode, suffix)
self.encode_buf.push(last_char);
}
} }
None => ("", self.encode_buf.as_str(), ""),
};
f.write_str(ENCODING_START_PREFIX)?; self.writer.folding().write_str(prefix)?;
let encoded = base64::display::Base64Display::with_config( email_encoding::headers::rfc2047::encode(to_encode, &mut self.writer)?;
self.encode_buf.as_bytes(), self.writer.folding().write_str(suffix)?;
base64::STANDARD,
);
write!(f, "{}", encoded)?;
f.write_str(ENCODING_END_SUFFIX)?;
self.line_len += ENCODING_START_PREFIX.len();
self.line_len += self.encode_buf.len() * 4 / 3 + 4;
self.line_len += ENCODING_END_SUFFIX.len();
if let Some(write_after) = write_after {
f.write_char(write_after)?;
self.line_len += 1;
}
self.encode_buf.clear(); self.encode_buf.clear();
Ok(()) Ok(())
} }
fn new_line(&mut self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str("\r\n ")?;
self.line_len = 1;
Ok(())
}
} }
/// Iterator yielding a string split space by space, but including all space /// Iterator yielding a string split by space, but spaces are included before the next word.
/// characters between it and the next word
struct WordsPlusFillIterator<'a> { struct WordsPlusFillIterator<'a> {
s: &'a str, s: &'a str,
} }
@@ -532,35 +441,31 @@ impl<'a> Iterator for WordsPlusFillIterator<'a> {
let next_word = self let next_word = self
.s .s
.char_indices() .bytes()
.enumerate()
.skip(1) .skip(1)
.skip_while(|&(_i, c)| !is_space_like(c)) .find(|&(_i, c)| c == b' ')
.find(|&(_i, c)| !is_space_like(c)) .map(|(i, _)| i)
.map(|(i, _)| i); .unwrap_or(self.s.len());
let word = &self.s[..next_word.unwrap_or(self.s.len())]; let word = &self.s[..next_word];
self.s = &self.s[word.len()..]; self.s = &self.s[word.len()..];
Some(word) Some(word)
} }
} }
const fn is_space_like(c: char) -> bool {
c == ',' || c == ' '
}
fn allowed_str(s: &str) -> bool { fn allowed_str(s: &str) -> bool {
s.chars().all(allowed_char) s.bytes().all(allowed_char)
} }
const fn allowed_char(c: char) -> bool { const fn allowed_char(c: u8) -> bool {
c >= 1 as char && c <= 9 as char c >= 1 && c <= 9 || c == 11 || c == 12 || c >= 14 && c <= 127
|| c == 11 as char
|| c == 12 as char
|| c >= 14 as char && c <= 127 as char
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use pretty_assertions::assert_eq;
use super::{HeaderName, HeaderValue, Headers}; use super::{HeaderName, HeaderValue, Headers};
#[test] #[test]
@@ -644,8 +549,8 @@ mod tests {
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
concat!( concat!(
"To: Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith \r\n", "To: Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith\r\n",
" <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand \r\n", " <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand\r\n",
" <jemand@example.com>, Jean Dupont <jean@example.com>\r\n" " <jemand@example.com>, Jean Dupont <jean@example.com>\r\n"
) )
); );
@@ -662,8 +567,8 @@ mod tests {
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
concat!( concat!(
"Subject: Hello! This is lettre, and this \r\n ", "Subject: Hello! This is lettre, and this\r\n",
"IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I \r\n", " IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I\r\n",
" guess that's it!\r\n" " guess that's it!\r\n"
) )
); );
@@ -681,8 +586,9 @@ mod tests {
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
concat!( concat!(
"Subject: Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoY\r\n", "Subject: Hello!\r\n",
" ouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know\r\n", " IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut!\r\n",
" I don't know\r\n",
) )
); );
} }
@@ -697,11 +603,7 @@ mod tests {
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
concat!( "Subject: 1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz\r\n",
"Subject: 1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijkl\r\n",
" mnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdef\r\n",
" ghijklmnopqrstuvwxyz\r\n",
)
); );
} }
@@ -715,7 +617,7 @@ mod tests {
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
"To: =?utf-8?b?U2XDoW4=?= <sean@example.com>\r\n" "To: Se=?utf-8?b?w6E=?=n <sean@example.com>\r\n"
); );
} }
@@ -744,11 +646,11 @@ mod tests {
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
concat!( concat!(
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?= \r\n", "To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?= Everywhere\r\n",
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyIA==?=\r\n", " <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9INCY0LLQsNC9?=\r\n",
" =?utf-8?b?0JjQstCw0L0g0JjQstCw0L3QvtCy0LjRhw==?= <ivanov@example.com>, \r\n", " =?utf-8?b?0L7QstC40Yc=?= <ivanov@example.com>, J=?utf-8?b?xIFuaXMgQsST?=\r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, \r\n", " =?utf-8?b?cnppxYbFoQ==?= <janis@example.com>, Se=?utf-8?b?w6FuIMOTIFJ1?=\r\n",
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n" " =?utf-8?b?ZGHDrQ==?= <sean@example.com>\r\n",
) )
); );
} }
@@ -764,7 +666,14 @@ mod tests {
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
"Subject: =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz?=\r\n" concat!(
"Subject: =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz?=\r\n",
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n",
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n",
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n",
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n",
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+lsw==?=\r\n"
)
); );
} }
@@ -809,14 +718,14 @@ mod tests {
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
concat!( concat!(
"Subject: Hello! This is lettre, and this \r\n", "Subject: Hello! This is lettre, and this\r\n",
" IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I \r\n", " IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I\r\n",
" guess that's it!\r\n", " guess that's it!\r\n",
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?= \r\n", "To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?= Everywhere\r\n",
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyIA==?=\r\n", " <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9INCY0LLQsNC9?=\r\n",
" =?utf-8?b?0JjQstCw0L0g0JjQstCw0L3QvtCy0LjRhw==?= <ivanov@example.com>, \r\n", " =?utf-8?b?0L7QstC40Yc=?= <ivanov@example.com>, J=?utf-8?b?xIFuaXMgQsST?=\r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, \r\n", " =?utf-8?b?cnppxYbFoQ==?= <janis@example.com>, Se=?utf-8?b?w6FuIMOTIFJ1?=\r\n",
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n", " =?utf-8?b?ZGHDrQ==?= <sean@example.com>\r\n",
"From: Someone <somewhere@example.com>\r\n", "From: Someone <somewhere@example.com>\r\n",
"Content-Transfer-Encoding: quoted-printable\r\n", "Content-Transfer-Encoding: quoted-printable\r\n",
) )
@@ -834,8 +743,8 @@ mod tests {
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
concat!( concat!(
"Subject: =?utf-8?b?77yL5Luu5ZCN?= :a;go; \r\n", "Subject: =?utf-8?b?77yL5Luu5ZCN?= :a;go;\r\n",
" =?utf-8?b?Ozs7OztzOzs7Ozs7Ozs7Ozs7Ozs7O2ZmZmVpbm1qZ2dnZ2dnZ2dn772G44Gj?=\r\n" " ;;;;;s;;;;;;;;;;;;;;;;fffeinmjggggggggg=?utf-8?b?772G44Gj?=\r\n"
) )
); );
} }

View File

@@ -4,7 +4,7 @@ use crate::{
}; };
/// Message format version, defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-4) /// Message format version, defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-4)
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct MimeVersion { pub struct MimeVersion {
major: u8, major: u8,
minor: u8, minor: u8,
@@ -16,15 +16,18 @@ pub struct MimeVersion {
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 {
/// Build a new `MimeVersion` header
pub const fn new(major: u8, minor: u8) -> Self { pub const fn new(major: u8, minor: u8) -> Self {
MimeVersion { major, minor } MimeVersion { major, minor }
} }
/// Get the `major` value of this `MimeVersion` header.
#[inline] #[inline]
pub const fn major(self) -> u8 { pub const fn major(self) -> u8 {
self.major self.major
} }
/// Get the `minor` value of this `MimeVersion` header.
#[inline] #[inline]
pub const fn minor(self) -> u8 { pub const fn minor(self) -> u8 {
self.minor self.minor
@@ -64,6 +67,8 @@ impl Default for MimeVersion {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq;
use super::{MimeVersion, MIME_VERSION_1_0}; use super::{MimeVersion, MIME_VERSION_1_0};
use crate::message::header::{HeaderName, HeaderValue, Headers}; use crate::message::header::{HeaderName, HeaderValue, Headers};

View File

@@ -4,7 +4,7 @@ use crate::BoxError;
macro_rules! text_header { macro_rules! text_header {
($(#[$attr:meta])* Header($type_name: ident, $header_name: expr )) => { ($(#[$attr:meta])* Header($type_name: ident, $header_name: expr )) => {
$(#[$attr])* $(#[$attr])*
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct $type_name(String); pub struct $type_name(String);
impl Header for $type_name { impl Header for $type_name {
@@ -85,6 +85,8 @@ text_header! {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq;
use super::Subject; use super::Subject;
use crate::message::header::{HeaderName, HeaderValue, Headers}; use crate::message::header::{HeaderName, HeaderValue, Headers};

View File

@@ -154,6 +154,7 @@ impl<'de> Deserialize<'de> for Mailboxes {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq;
use serde_json::from_str; use serde_json::from_str;
use super::*; use super::*;

View File

@@ -315,6 +315,18 @@ impl From<Mailboxes> for Vec<Mailbox> {
} }
} }
impl FromIterator<Mailbox> for Mailboxes {
fn from_iter<T: IntoIterator<Item = Mailbox>>(iter: T) -> Self {
Self(Vec::from_iter(iter))
}
}
impl Extend<Mailbox> for Mailboxes {
fn extend<T: IntoIterator<Item = Mailbox>>(&mut self, iter: T) {
self.0.extend(iter);
}
}
impl IntoIterator for Mailboxes { impl IntoIterator for Mailboxes {
type Item = Mailbox; type Item = Mailbox;
type IntoIter = ::std::vec::IntoIter<Mailbox>; type IntoIter = ::std::vec::IntoIter<Mailbox>;
@@ -324,14 +336,6 @@ impl IntoIterator for Mailboxes {
} }
} }
impl Extend<Mailbox> for Mailboxes {
fn extend<T: IntoIterator<Item = Mailbox>>(&mut self, iter: T) {
for elem in iter {
self.0.push(elem);
}
}
}
impl Display for Mailboxes { impl Display for Mailboxes {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let mut iter = self.iter(); let mut iter = self.iter();
@@ -352,11 +356,37 @@ 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))
} }
} }
@@ -445,6 +475,8 @@ fn write_quoted_string_char(f: &mut Formatter<'_>, c: u8) -> FmtResult {
mod test { mod test {
use std::convert::TryInto; use std::convert::TryInto;
use pretty_assertions::assert_eq;
use super::Mailbox; use super::Mailbox;
#[test] #[test]

View File

@@ -398,6 +398,8 @@ impl EmailFormat for MultiPart {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq;
use super::*; use super::*;
use crate::message::header; use crate::message::header;
@@ -477,7 +479,7 @@ mod test {
assert_eq!( assert_eq!(
String::from_utf8(part.formatted()).unwrap(), String::from_utf8(part.formatted()).unwrap(),
concat!( concat!(
"Content-Type: multipart/mixed; \r\n", "Content-Type: multipart/mixed;\r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n", " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
"\r\n", "\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
@@ -524,8 +526,8 @@ mod test {
assert_eq!( assert_eq!(
String::from_utf8(part.formatted()).unwrap(), String::from_utf8(part.formatted()).unwrap(),
concat!( concat!(
"Content-Type: multipart/encrypted; \r\n", "Content-Type: multipart/encrypted;\r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"; \r\n", " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\";\r\n",
" protocol=\"application/pgp-encrypted\"\r\n", " protocol=\"application/pgp-encrypted\"\r\n",
"\r\n", "\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
@@ -580,8 +582,8 @@ mod test {
assert_eq!( assert_eq!(
String::from_utf8(part.formatted()).unwrap(), String::from_utf8(part.formatted()).unwrap(),
concat!( concat!(
"Content-Type: multipart/signed; \r\n", "Content-Type: multipart/signed;\r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"; \r\n", " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\";\r\n",
" protocol=\"application/pgp-signature\";", " protocol=\"application/pgp-signature\";",
" micalg=\"pgp-sha256\"\r\n", " micalg=\"pgp-sha256\"\r\n",
"\r\n", "\r\n",
@@ -622,7 +624,7 @@ mod test {
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>"))); .body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")));
assert_eq!(String::from_utf8(part.formatted()).unwrap(), assert_eq!(String::from_utf8(part.formatted()).unwrap(),
concat!("Content-Type: multipart/alternative; \r\n", concat!("Content-Type: multipart/alternative;\r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n", " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
"\r\n", "\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
@@ -660,11 +662,11 @@ mod test {
.body(String::from("int main() { return 0; }"))); .body(String::from("int main() { return 0; }")));
assert_eq!(String::from_utf8(part.formatted()).unwrap(), assert_eq!(String::from_utf8(part.formatted()).unwrap(),
concat!("Content-Type: multipart/mixed; \r\n", concat!("Content-Type: multipart/mixed;\r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n", " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
"\r\n", "\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: multipart/related; \r\n", "Content-Type: multipart/related;\r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n", " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
"\r\n", "\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",

View File

@@ -232,6 +232,7 @@ trait EmailFormat {
pub struct MessageBuilder { pub struct MessageBuilder {
headers: Headers, headers: Headers,
envelope: Option<Envelope>, envelope: Option<Envelope>,
drop_bcc: bool,
} }
impl MessageBuilder { impl MessageBuilder {
@@ -240,24 +241,26 @@ impl MessageBuilder {
Self { Self {
headers: Headers::new(), headers: Headers::new(),
envelope: None, envelope: None,
drop_bcc: true,
} }
} }
/// Set custom header to message /// Set or add mailbox to `From` header
pub fn header<H: Header>(mut self, header: H) -> Self { ///
self.headers.set(header); /// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
self ///
/// Shortcut for `self.mailbox(header::From(mbox))`.
pub fn from(self, mbox: Mailbox) -> Self {
self.mailbox(header::From::from(Mailboxes::from(mbox)))
} }
/// Add mailbox to header /// Set `Sender` header. Should be used when providing several `From` mailboxes.
pub fn mailbox<H: Header + MailboxesHeader>(self, header: H) -> Self { ///
match self.headers.get::<H>() { /// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
Some(mut header_) => { ///
header_.join_mailboxes(header); /// Shortcut for `self.header(header::Sender(mbox))`.
self.header(header_) pub fn sender(self, mbox: Mailbox) -> Self {
} self.header(header::Sender::from(mbox))
None => self.header(header),
}
} }
/// Add `Date` header to message /// Add `Date` header to message
@@ -275,41 +278,6 @@ impl MessageBuilder {
self.date(SystemTime::now()) self.date(SystemTime::now())
} }
/// Set `Subject` header to message
///
/// Shortcut for `self.header(header::Subject(subject.into()))`.
pub fn subject<S: Into<String>>(self, subject: S) -> Self {
let s: String = subject.into();
self.header(header::Subject::from(s))
}
/// Set `MIME-Version` header to 1.0
///
/// Shortcut for `self.header(header::MIME_VERSION_1_0)`.
///
/// Not exposed as it is set by body methods
fn mime_1_0(self) -> Self {
self.header(header::MIME_VERSION_1_0)
}
/// Set `Sender` header. Should be used when providing several `From` mailboxes.
///
/// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
///
/// Shortcut for `self.header(header::Sender(mbox))`.
pub fn sender(self, mbox: Mailbox) -> Self {
self.header(header::Sender::from(mbox))
}
/// Set or add mailbox to `From` header
///
/// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
///
/// Shortcut for `self.mailbox(header::From(mbox))`.
pub fn from(self, mbox: Mailbox) -> Self {
self.mailbox(header::From::from(Mailboxes::from(mbox)))
}
/// Set or add mailbox to `ReplyTo` header /// Set or add mailbox to `ReplyTo` header
/// ///
/// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2). /// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
@@ -352,6 +320,14 @@ impl MessageBuilder {
self.header(header::References::from(id)) self.header(header::References::from(id))
} }
/// Set `Subject` header to message
///
/// Shortcut for `self.header(header::Subject(subject.into()))`.
pub fn subject<S: Into<String>>(self, subject: S) -> Self {
let s: String = subject.into();
self.header(header::Subject::from(s))
}
/// Set [Message-ID /// Set [Message-ID
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4) /// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
/// ///
@@ -385,12 +361,43 @@ impl MessageBuilder {
self.header(header::UserAgent::from(id)) self.header(header::UserAgent::from(id))
} }
/// Set custom header to message
pub fn header<H: Header>(mut self, header: H) -> Self {
self.headers.set(header);
self
}
/// Add mailbox to header
pub fn mailbox<H: Header + MailboxesHeader>(self, header: H) -> Self {
match self.headers.get::<H>() {
Some(mut header_) => {
header_.join_mailboxes(header);
self.header(header_)
}
None => self.header(header),
}
}
/// Force specific envelope (by default it is derived from headers) /// Force specific envelope (by default it is derived from headers)
pub fn envelope(mut self, envelope: Envelope) -> Self { pub fn envelope(mut self, envelope: Envelope) -> Self {
self.envelope = Some(envelope); self.envelope = Some(envelope);
self self
} }
/// Keep the `Bcc` header
///
/// By default the `Bcc` header is removed from the email after
/// using it to generate the message envelope. In some cases though,
/// like when saving the email as an `.eml`, or sending through
/// some transports (like the Gmail API) that don't take a separate
/// envelope value, it becomes necessary to keep the `Bcc` header.
///
/// Calling this method overrides the default behaviour.
pub fn keep_bcc(mut self) -> Self {
self.drop_bcc = false;
self
}
// TODO: High-level methods for attachments and embedded files // TODO: High-level methods for attachments and embedded files
/// Create message from body /// Create message from body
@@ -423,8 +430,10 @@ impl MessageBuilder {
None => Envelope::try_from(&res.headers)?, None => Envelope::try_from(&res.headers)?,
}; };
if res.drop_bcc {
// Remove `Bcc` headers now the envelope is set // Remove `Bcc` headers now the envelope is set
res.headers.remove::<header::Bcc>(); res.headers.remove::<header::Bcc>();
}
Ok(Message { Ok(Message {
headers: res.headers, headers: res.headers,
@@ -455,6 +464,15 @@ impl MessageBuilder {
pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> { pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> {
self.mime_1_0().build(MessageBody::Mime(Part::Single(part))) self.mime_1_0().build(MessageBody::Mime(Part::Single(part)))
} }
/// Set `MIME-Version` header to 1.0
///
/// Shortcut for `self.header(header::MIME_VERSION_1_0)`.
///
/// Not exposed as it is set by body methods
fn mime_1_0(self) -> Self {
self.header(header::MIME_VERSION_1_0)
}
} }
/// Email message which can be formatted /// Email message which can be formatted
@@ -550,7 +568,7 @@ impl Message {
/// JcaBbL6ZSBIMA3AdaIjtvNRiomueHqh0GspTgOeCE2585TSFnw6vEOJ8RlR4A0Mw /// JcaBbL6ZSBIMA3AdaIjtvNRiomueHqh0GspTgOeCE2585TSFnw6vEOJ8RlR4A0Mw
/// I45fbR4l+3D/30WMfZlM6bzZbwPXEnr2s1mirmuQpjumY9wLhK25 /// I45fbR4l+3D/30WMfZlM6bzZbwPXEnr2s1mirmuQpjumY9wLhK25
/// -----END RSA PRIVATE KEY-----"; /// -----END RSA PRIVATE KEY-----";
/// let signing_key = DkimSigningKey::new(key.to_string(), DkimSigningAlgorithm::Rsa).unwrap(); /// let signing_key = DkimSigningKey::new(key, DkimSigningAlgorithm::Rsa).unwrap();
/// message.sign(&DkimConfig::default_config( /// message.sign(&DkimConfig::default_config(
/// "dkimtest".to_string(), /// "dkimtest".to_string(),
/// "example.org".to_string(), /// "example.org".to_string(),
@@ -598,6 +616,8 @@ fn make_message_id() -> String {
mod test { mod test {
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
use pretty_assertions::assert_eq;
use super::{header, mailbox::Mailbox, make_message_id, Message, MultiPart, SinglePart}; use super::{header, mailbox::Mailbox, make_message_id, Message, MultiPart, SinglePart};
#[test] #[test]
@@ -626,7 +646,7 @@ mod test {
} }
#[test] #[test]
fn email_message() { fn email_message_no_bcc() {
// Tue, 15 Nov 1994 08:12:31 GMT // Tue, 15 Nov 1994 08:12:31 GMT
let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151); let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151);
@@ -650,10 +670,48 @@ 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/So9CwINC10Lsg0LHQtdC705nQvQ==?=!\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Happy new year!"
)
);
}
#[test]
fn email_message_keep_bcc() {
// Tue, 15 Nov 1994 08:12:31 GMT
let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151);
let email = Message::builder()
.date(date)
.bcc("hidden@example.com".parse().unwrap())
.keep_bcc()
.header(header::From(
vec![Mailbox::new(
Some("Каи".into()),
"kayo@example.com".parse().unwrap(),
)]
.into(),
))
.header(header::To(
vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(),
))
.header(header::Subject::from(String::from("яңа ел белән!")))
.body(String::from("Happy new year!"))
.unwrap();
assert_eq!(
String::from_utf8(email.formatted()).unwrap(),
concat!(
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
"Bcc: hidden@example.com\r\n",
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
"To: \"Pony O.P.\" <pony@domain.tld>\r\n",
"Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvQ==?=!\r\n",
"Content-Transfer-Encoding: 7bit\r\n", "Content-Transfer-Encoding: 7bit\r\n",
"\r\n", "\r\n",
"Happy new year!" "Happy new year!"

View File

@@ -198,6 +198,9 @@ where
{ {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self { Self {
#[cfg(feature = "pool")]
inner: Arc::clone(&self.inner),
#[cfg(not(feature = "pool"))]
inner: self.inner.clone(), inner: self.inner.clone(),
} }
} }

View File

@@ -98,12 +98,12 @@ impl Mechanism {
let decoded_challenge = challenge let decoded_challenge = challenge
.ok_or_else(|| error::client("This mechanism does expect a challenge"))?; .ok_or_else(|| error::client("This mechanism does expect a challenge"))?;
if vec!["User Name", "Username:", "Username"].contains(&decoded_challenge) { if ["User Name", "Username:", "Username"].contains(&decoded_challenge) {
return Ok(credentials.authentication_identity.to_string()); return Ok(credentials.authentication_identity.clone());
} }
if vec!["Password", "Password:"].contains(&decoded_challenge) { if ["Password", "Password:"].contains(&decoded_challenge) {
return Ok(credentials.secret.to_string()); return Ok(credentials.secret.clone());
} }
Err(error::client("Unrecognized challenge")) Err(error::client("Unrecognized challenge"))

View File

@@ -1,4 +1,4 @@
use std::{fmt::Display, time::Duration}; use std::{fmt::Display, net::IpAddr, time::Duration};
use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
@@ -8,7 +8,7 @@ use super::{AsyncNetworkStream, ClientCodec, TlsParameters};
use crate::{ use crate::{
transport::smtp::{ transport::smtp::{
authentication::{Credentials, Mechanism}, authentication::{Credentials, Mechanism},
commands::*, commands::{Auth, Data, Ehlo, Mail, Noop, Quit, Rcpt, Starttls},
error, error,
error::Error, error::Error,
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo}, extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
@@ -41,6 +41,7 @@ pub struct AsyncSmtpConnection {
} }
impl AsyncSmtpConnection { impl AsyncSmtpConnection {
/// Get information about the server
pub fn server_info(&self) -> &ServerInfo { pub fn server_info(&self) -> &ServerInfo {
&self.server_info &self.server_info
} }
@@ -54,8 +55,11 @@ impl AsyncSmtpConnection {
timeout: Option<Duration>, timeout: Option<Duration>,
hello_name: &ClientId, hello_name: &ClientId,
tls_parameters: Option<TlsParameters>, tls_parameters: Option<TlsParameters>,
local_address: Option<IpAddr>,
) -> Result<AsyncSmtpConnection, Error> { ) -> Result<AsyncSmtpConnection, Error> {
let stream = AsyncNetworkStream::connect_tokio1(server, timeout, tls_parameters).await?; let stream =
AsyncNetworkStream::connect_tokio1(server, timeout, tls_parameters, local_address)
.await?;
Self::connect_impl(stream, hello_name).await Self::connect_impl(stream, hello_name).await
} }
@@ -190,6 +194,7 @@ impl AsyncSmtpConnection {
self.panic = true; self.panic = true;
let _ = self.command(Quit).await; let _ = self.command(Quit).await;
} }
let _ = self.stream.close().await;
} }
/// Sets the underlying stream /// Sets the underlying stream
@@ -316,7 +321,7 @@ impl AsyncSmtpConnection {
} }
/// The X509 certificate of the server (DER encoded) /// The X509 certificate of the server (DER encoded)
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> { pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
self.stream.get_ref().peer_certificate() self.stream.get_ref().peer_certificate()
} }

View File

@@ -1,6 +1,6 @@
use std::{ use std::{
io, mem, io, mem,
net::SocketAddr, net::{IpAddr, SocketAddr},
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
time::Duration, time::Duration,
@@ -16,10 +16,15 @@ use futures_io::{
}; };
#[cfg(feature = "async-std1-rustls-tls")] #[cfg(feature = "async-std1-rustls-tls")]
use futures_rustls::client::TlsStream as AsyncStd1RustlsTlsStream; use futures_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
#[cfg(feature = "tokio1-boring-tls")]
use tokio1_boring::SslStream as Tokio1SslStream;
#[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 = "tokio1")] #[cfg(feature = "tokio1")]
use tokio1_crate::net::{TcpStream as Tokio1TcpStream, ToSocketAddrs as Tokio1ToSocketAddrs}; use tokio1_crate::net::{
TcpSocket as Tokio1TcpSocket, TcpStream as Tokio1TcpStream,
ToSocketAddrs as Tokio1ToSocketAddrs,
};
#[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 = "tokio1-rustls-tls")] #[cfg(feature = "tokio1-rustls-tls")]
@@ -28,11 +33,14 @@ use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
#[cfg(any( #[cfg(any(
feature = "tokio1-native-tls", feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls", feature = "tokio1-rustls-tls",
feature = "tokio1-boring-tls",
feature = "async-std1-native-tls", feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls" feature = "async-std1-rustls-tls"
))] ))]
use super::InnerTlsParameters; use super::InnerTlsParameters;
use super::TlsParameters; use super::TlsParameters;
#[cfg(feature = "tokio1")]
use crate::transport::smtp::client::net::resolved_address_filter;
use crate::transport::smtp::{error, Error}; use crate::transport::smtp::{error, Error};
/// A network stream /// A network stream
@@ -55,6 +63,9 @@ enum InnerAsyncNetworkStream {
/// Encrypted Tokio 1.x TCP stream /// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "tokio1-rustls-tls")] #[cfg(feature = "tokio1-rustls-tls")]
Tokio1RustlsTls(Tokio1RustlsTlsStream<Tokio1TcpStream>), Tokio1RustlsTls(Tokio1RustlsTlsStream<Tokio1TcpStream>),
/// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "tokio1-boring-tls")]
Tokio1BoringTls(Tokio1SslStream<Tokio1TcpStream>),
/// Plain Tokio 1.x TCP stream /// Plain Tokio 1.x TCP stream
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
AsyncStd1Tcp(AsyncStd1TcpStream), AsyncStd1Tcp(AsyncStd1TcpStream),
@@ -88,6 +99,8 @@ impl AsyncNetworkStream {
} }
#[cfg(feature = "tokio1-rustls-tls")] #[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(ref s) => s.get_ref().0.peer_addr(), InnerAsyncNetworkStream::Tokio1RustlsTls(ref s) => s.get_ref().0.peer_addr(),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(ref s) => s.get_ref().peer_addr(),
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(ref s) => s.peer_addr(), InnerAsyncNetworkStream::AsyncStd1Tcp(ref s) => s.peer_addr(),
#[cfg(feature = "async-std1-native-tls")] #[cfg(feature = "async-std1-native-tls")]
@@ -109,19 +122,34 @@ impl AsyncNetworkStream {
server: T, server: T,
timeout: Option<Duration>, timeout: Option<Duration>,
tls_parameters: Option<TlsParameters>, tls_parameters: Option<TlsParameters>,
local_addr: Option<IpAddr>,
) -> Result<AsyncNetworkStream, Error> { ) -> Result<AsyncNetworkStream, Error> {
async fn try_connect_timeout<T: Tokio1ToSocketAddrs>( async fn try_connect<T: Tokio1ToSocketAddrs>(
server: T, server: T,
timeout: Duration, timeout: Option<Duration>,
local_addr: Option<IpAddr>,
) -> Result<Tokio1TcpStream, Error> { ) -> Result<Tokio1TcpStream, Error> {
let addrs = tokio1_crate::net::lookup_host(server) let addrs = tokio1_crate::net::lookup_host(server)
.await .await
.map_err(error::connection)?; .map_err(error::connection)?
.filter(|resolved_addr| resolved_address_filter(resolved_addr, local_addr));
let mut last_err = None; let mut last_err = None;
for addr in addrs { for addr in addrs {
let connect_future = Tokio1TcpStream::connect(&addr); let socket = match addr.ip() {
IpAddr::V4(_) => Tokio1TcpSocket::new_v4(),
IpAddr::V6(_) => Tokio1TcpSocket::new_v6(),
}
.map_err(error::connection)?;
if let Some(local_addr) = local_addr {
socket
.bind(SocketAddr::new(local_addr, 0))
.map_err(error::connection)?;
}
let connect_future = socket.connect(addr);
if let Some(timeout) = timeout {
match tokio1_crate::time::timeout(timeout, connect_future).await { match tokio1_crate::time::timeout(timeout, connect_future).await {
Ok(Ok(stream)) => return Ok(stream), Ok(Ok(stream)) => return Ok(stream),
Ok(Err(err)) => last_err = Some(err), Ok(Err(err)) => last_err = Some(err),
@@ -132,21 +160,21 @@ impl AsyncNetworkStream {
)) ))
} }
} }
} else {
match connect_future.await {
Ok(stream) => return Ok(stream),
Err(err) => last_err = Some(err),
}
}
} }
Err(match last_err { Err(match last_err {
Some(last_err) => error::connection(last_err), Some(last_err) => error::connection(last_err),
None => error::connection("could not resolve to any address"), None => error::connection("could not resolve to any supported address"),
}) })
} }
let tcp_stream = match timeout { let tcp_stream = try_connect(server, timeout, local_addr).await?;
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 {
stream.upgrade_tls(tls_parameters).await?; stream.upgrade_tls(tls_parameters).await?;
@@ -160,6 +188,9 @@ impl AsyncNetworkStream {
timeout: Option<Duration>, timeout: Option<Duration>,
tls_parameters: Option<TlsParameters>, tls_parameters: Option<TlsParameters>,
) -> Result<AsyncNetworkStream, Error> { ) -> Result<AsyncNetworkStream, Error> {
// Unfortunately there doesn't currently seem to be a way to set the local address
// Whilst we can create a AsyncStd1TcpStream from an existing socket, it needs to first have
// connected which is a blocking operation.
async fn try_connect_timeout<T: AsyncStd1ToSocketAddrs>( async fn try_connect_timeout<T: AsyncStd1ToSocketAddrs>(
server: T, server: T,
timeout: Duration, timeout: Duration,
@@ -206,14 +237,22 @@ impl AsyncNetworkStream {
match &self.inner { match &self.inner {
#[cfg(all( #[cfg(all(
feature = "tokio1", feature = "tokio1",
not(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls")) not(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "tokio1-boring-tls"
))
))] ))]
InnerAsyncNetworkStream::Tokio1Tcp(_) => { InnerAsyncNetworkStream::Tokio1Tcp(_) => {
let _ = tls_parameters; let _ = tls_parameters;
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio1-native-tls or the tokio1-rustls-tls feature"); panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio1-native-tls or the tokio1-rustls-tls feature");
} }
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))] #[cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "tokio1-boring-tls"
))]
InnerAsyncNetworkStream::Tokio1Tcp(_) => { InnerAsyncNetworkStream::Tokio1Tcp(_) => {
// get owned TcpStream // get owned TcpStream
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None); let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
@@ -255,7 +294,11 @@ impl AsyncNetworkStream {
} }
#[allow(unused_variables)] #[allow(unused_variables)]
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))] #[cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "tokio1-boring-tls"
))]
async fn upgrade_tokio1_tls( async fn upgrade_tokio1_tls(
tcp_stream: Tokio1TcpStream, tcp_stream: Tokio1TcpStream,
tls_parameters: TlsParameters, tls_parameters: TlsParameters,
@@ -301,11 +344,31 @@ impl AsyncNetworkStream {
Ok(InnerAsyncNetworkStream::Tokio1RustlsTls(stream)) Ok(InnerAsyncNetworkStream::Tokio1RustlsTls(stream))
}; };
} }
#[cfg(feature = "boring-tls")]
InnerTlsParameters::BoringTls(connector) => {
#[cfg(not(feature = "tokio1-boring-tls"))]
panic!("built without the tokio1-boring-tls feature");
#[cfg(feature = "tokio1-boring-tls")]
return {
let mut config = connector.configure().map_err(error::connection)?;
config.set_verify_hostname(tls_parameters.accept_invalid_hostnames);
let stream = tokio1_boring::connect(config, &domain, tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::Tokio1BoringTls(stream))
};
}
} }
} }
#[allow(unused_variables)] #[allow(unused_variables)]
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))] #[cfg(any(
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls",
feature = "async-std1-boring-tls"
))]
async fn upgrade_asyncstd1_tls( async fn upgrade_asyncstd1_tls(
tcp_stream: AsyncStd1TcpStream, tcp_stream: AsyncStd1TcpStream,
mut tls_parameters: TlsParameters, mut tls_parameters: TlsParameters,
@@ -354,6 +417,10 @@ impl AsyncNetworkStream {
Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream)) Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream))
}; };
} }
#[cfg(feature = "boring-tls")]
InnerTlsParameters::BoringTls(connector) => {
panic!("boring-tls isn't supported with async-std yet.");
}
} }
} }
@@ -365,6 +432,8 @@ impl AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio1NativeTls(_) => true, InnerAsyncNetworkStream::Tokio1NativeTls(_) => true,
#[cfg(feature = "tokio1-rustls-tls")] #[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(_) => true, InnerAsyncNetworkStream::Tokio1RustlsTls(_) => true,
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(_) => true,
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => false, InnerAsyncNetworkStream::AsyncStd1Tcp(_) => false,
#[cfg(feature = "async-std1-native-tls")] #[cfg(feature = "async-std1-native-tls")]
@@ -399,6 +468,13 @@ impl AsyncNetworkStream {
.unwrap() .unwrap()
.clone() .clone()
.0), .0),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => Ok(stream
.ssl()
.peer_certificate()
.unwrap()
.to_der()
.map_err(error::tls)?),
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => { InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
Err(error::client("Connection is not encrypted")) Err(error::client("Connection is not encrypted"))
@@ -454,6 +530,15 @@ impl FuturesAsyncRead for AsyncNetworkStream {
Poll::Pending => Poll::Pending, Poll::Pending => Poll::Pending,
} }
} }
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(ref mut s) => {
let mut b = Tokio1ReadBuf::new(buf);
match Pin::new(s).poll_read(cx, &mut b) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
Poll::Pending => Poll::Pending,
}
}
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_read(cx, buf), InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "async-std1-native-tls")] #[cfg(feature = "async-std1-native-tls")]
@@ -485,6 +570,8 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf), InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio1-rustls-tls")] #[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_write(cx, buf), InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf), InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "async-std1-native-tls")] #[cfg(feature = "async-std1-native-tls")]
@@ -510,6 +597,8 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_flush(cx), InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio1-rustls-tls")] #[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx), InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_flush(cx), InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "async-std1-native-tls")] #[cfg(feature = "async-std1-native-tls")]
@@ -531,6 +620,8 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_shutdown(cx), InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio1-rustls-tls")] #[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_shutdown(cx), InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_close(cx), InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_close(cx),
#[cfg(feature = "async-std1-native-tls")] #[cfg(feature = "async-std1-native-tls")]

View File

@@ -1,7 +1,7 @@
use std::{ use std::{
fmt::Display, fmt::Display,
io::{self, BufRead, BufReader, Write}, io::{self, BufRead, BufReader, Write},
net::ToSocketAddrs, net::{IpAddr, ToSocketAddrs},
time::Duration, time::Duration,
}; };
@@ -12,7 +12,7 @@ use crate::{
address::Envelope, address::Envelope,
transport::smtp::{ transport::smtp::{
authentication::{Credentials, Mechanism}, authentication::{Credentials, Mechanism},
commands::*, commands::{Auth, Data, Ehlo, Mail, Noop, Quit, Rcpt, Starttls},
error, error,
error::Error, error::Error,
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo}, extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
@@ -44,6 +44,7 @@ pub struct SmtpConnection {
} }
impl SmtpConnection { impl SmtpConnection {
/// Get information about the server
pub fn server_info(&self) -> &ServerInfo { pub fn server_info(&self) -> &ServerInfo {
&self.server_info &self.server_info
} }
@@ -58,8 +59,9 @@ impl SmtpConnection {
timeout: Option<Duration>, timeout: Option<Duration>,
hello_name: &ClientId, hello_name: &ClientId,
tls_parameters: Option<&TlsParameters>, tls_parameters: Option<&TlsParameters>,
local_address: Option<IpAddr>,
) -> Result<SmtpConnection, Error> { ) -> Result<SmtpConnection, Error> {
let stream = NetworkStream::connect(server, timeout, tls_parameters)?; let stream = NetworkStream::connect(server, timeout, tls_parameters, local_address)?;
let stream = BufReader::new(stream); let stream = BufReader::new(stream);
let mut conn = SmtpConnection { let mut conn = SmtpConnection {
stream, stream,
@@ -141,7 +143,7 @@ impl SmtpConnection {
hello_name: &ClientId, hello_name: &ClientId,
) -> Result<(), Error> { ) -> Result<(), Error> {
if self.server_info.supports_feature(Extension::StartTls) { if self.server_info.supports_feature(Extension::StartTls) {
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
{ {
try_smtp!(self.command(Starttls), self); try_smtp!(self.command(Starttls), self);
self.stream.get_mut().upgrade_tls(tls_parameters)?; self.stream.get_mut().upgrade_tls(tls_parameters)?;
@@ -151,7 +153,11 @@ impl SmtpConnection {
try_smtp!(self.ehlo(hello_name), self); try_smtp!(self.ehlo(hello_name), self);
Ok(()) Ok(())
} }
#[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))] #[cfg(not(any(
feature = "native-tls",
feature = "rustls-tls",
feature = "boring-tls"
)))]
// This should never happen as `Tls` can only be created // This should never happen as `Tls` can only be created
// when a TLS library is enabled // when a TLS library is enabled
unreachable!("TLS support required but not supported"); unreachable!("TLS support required but not supported");
@@ -177,6 +183,7 @@ impl SmtpConnection {
self.panic = true; self.panic = true;
let _ = self.command(Quit); let _ = self.command(Quit);
} }
let _ = self.stream.get_mut().shutdown(std::net::Shutdown::Both);
} }
/// Sets the underlying stream /// Sets the underlying stream
@@ -236,11 +243,12 @@ impl SmtpConnection {
/// Sends the message content /// Sends the message content
pub fn message(&mut self, message: &[u8]) -> Result<Response, Error> { pub fn message(&mut self, message: &[u8]) -> Result<Response, Error> {
let mut out_buf: Vec<u8> = vec![];
let mut codec = ClientCodec::new(); let mut codec = ClientCodec::new();
let mut out_buf = Vec::with_capacity(message.len());
codec.encode(message, &mut out_buf); codec.encode(message, &mut out_buf);
self.write(out_buf.as_slice())?; self.write(out_buf.as_slice())?;
self.write(b"\r\n.\r\n")?; self.write(b"\r\n.\r\n")?;
self.read_response() self.read_response()
} }
@@ -295,7 +303,7 @@ impl SmtpConnection {
} }
/// The X509 certificate of the server (DER encoded) /// The X509 certificate of the server (DER encoded)
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> { pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
self.stream.get_ref().peer_certificate() self.stream.get_ref().peer_certificate()
} }

View File

@@ -12,7 +12,7 @@
//! }; //! };
//! //!
//! 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, None)?;
//! client.command(Mail::new(Some("user@example.com".parse()?), vec![]))?; //! client.command(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)?;
@@ -30,8 +30,10 @@ pub use self::async_connection::AsyncSmtpConnection;
#[cfg(any(feature = "tokio1", feature = "async-std1"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub 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", feature = "boring-tls"))]
pub(super) use self::tls::InnerTlsParameters; pub(super) use self::tls::InnerTlsParameters;
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub use self::tls::TlsVersion;
pub use self::{ pub use self::{
connection::SmtpConnection, connection::SmtpConnection,
tls::{Certificate, Tls, TlsParameters, TlsParametersBuilder}, tls::{Certificate, Tls, TlsParameters, TlsParametersBuilder},
@@ -46,60 +48,57 @@ mod net;
mod tls; mod tls;
/// The codec used for transparency /// The codec used for transparency
#[derive(Default, Clone, Copy, Debug)] #[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
struct ClientCodec { struct ClientCodec {
escape_count: u8, status: CodecStatus,
} }
impl ClientCodec { impl ClientCodec {
/// Creates a new client codec /// Creates a new client codec
pub fn new() -> Self { pub fn new() -> Self {
ClientCodec::default() Self {
status: CodecStatus::StartOfNewLine,
}
} }
/// Adds transparency /// Adds transparency
fn encode(&mut self, frame: &[u8], buf: &mut Vec<u8>) { fn encode(&mut self, frame: &[u8], buf: &mut Vec<u8>) {
match frame.len() { for &b in frame {
0 => { buf.push(b);
match self.escape_count { match (b, self.status) {
0 => buf.extend_from_slice(b"\r\n.\r\n"), (b'\r', _) => {
1 => buf.extend_from_slice(b"\n.\r\n"), self.status = CodecStatus::StartingNewLine;
2 => buf.extend_from_slice(b".\r\n"),
_ => unreachable!(),
} }
self.escape_count = 0; (b'\n', CodecStatus::StartingNewLine) => {
self.status = CodecStatus::StartOfNewLine;
} }
_ => { (_, CodecStatus::StartingNewLine) => {
let mut start = 0; self.status = CodecStatus::MiddleOfLine;
for (idx, byte) in frame.iter().enumerate() {
match self.escape_count {
0 => self.escape_count = if *byte == b'\r' { 1 } else { 0 },
1 => self.escape_count = if *byte == b'\n' { 2 } else { 0 },
2 => {
self.escape_count = if *byte == b'.' {
3
} else if *byte == b'\r' {
1
} else {
0
} }
(b'.', CodecStatus::StartOfNewLine) => {
self.status = CodecStatus::MiddleOfLine;
buf.push(b'.');
} }
_ => unreachable!(), (_, CodecStatus::StartOfNewLine) => {
self.status = CodecStatus::MiddleOfLine;
} }
if self.escape_count == 3 { _ => {}
self.escape_count = 0;
buf.extend_from_slice(&frame[start..idx]);
buf.extend_from_slice(b".");
start = idx;
}
}
buf.extend_from_slice(&frame[start..]);
} }
} }
} }
} }
#[derive(Debug, Copy, Clone)]
#[allow(clippy::enum_variant_names)]
enum CodecStatus {
/// We are past the first character of the current line
MiddleOfLine,
/// We just read a `\r` character
StartingNewLine,
/// We are at the start of a new line
StartOfNewLine,
}
/// Returns the string replacing all the CRLF with "\<CRLF\>" /// Returns the string replacing all the CRLF with "\<CRLF\>"
/// Used for debug displays /// Used for debug displays
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
@@ -113,9 +112,10 @@ mod test {
#[test] #[test]
fn test_codec() { fn test_codec() {
let mut buf = Vec::new();
let mut codec = ClientCodec::new(); let mut codec = ClientCodec::new();
let mut buf: Vec<u8> = vec![];
codec.encode(b".\r\n", &mut buf);
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"test\r\n\r\n", &mut buf);
codec.encode(b".\r\n", &mut buf); codec.encode(b".\r\n", &mut buf);
@@ -126,9 +126,13 @@ mod test {
codec.encode(b"test\n", &mut buf); codec.encode(b"test\n", &mut buf);
codec.encode(b".test\n", &mut buf); codec.encode(b".test\n", &mut buf);
codec.encode(b"test", &mut buf); codec.encode(b"test", &mut buf);
codec.encode(b"test", &mut buf);
codec.encode(b"test\r\n", &mut buf);
codec.encode(b".test\r\n", &mut buf);
codec.encode(b"test.\r\n", &mut buf);
assert_eq!( assert_eq!(
String::from_utf8(buf).unwrap(), String::from_utf8(buf).unwrap(),
"test\r\ntest\r\n\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntest" "..\r\ntest\r\ntest\r\n\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntesttesttest\r\n..test\r\ntest.\r\n"
); );
} }

View File

@@ -1,16 +1,20 @@
use std::{ use std::{
io::{self, Read, Write}, io::{self, Read, Write},
mem, mem,
net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs}, net::{IpAddr, Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs},
sync::Arc,
time::Duration, time::Duration,
}; };
#[cfg(feature = "boring-tls")]
use boring::ssl::SslStream;
#[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::{ClientConnection, ServerName, StreamOwned}; use rustls::{ClientConnection, ServerName, StreamOwned};
use socket2::{Domain, Protocol, Type};
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
use super::InnerTlsParameters; use super::InnerTlsParameters;
use super::TlsParameters; use super::TlsParameters;
use crate::transport::smtp::{error, Error}; use crate::transport::smtp::{error, Error};
@@ -33,6 +37,8 @@ enum InnerNetworkStream {
/// Encrypted TCP stream /// Encrypted TCP stream
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
RustlsTls(StreamOwned<ClientConnection, TcpStream>), RustlsTls(StreamOwned<ClientConnection, TcpStream>),
#[cfg(feature = "boring-tls")]
BoringTls(SslStream<TcpStream>),
/// Can't be built /// Can't be built
None, None,
} }
@@ -54,6 +60,8 @@ impl NetworkStream {
InnerNetworkStream::NativeTls(ref s) => s.get_ref().peer_addr(), InnerNetworkStream::NativeTls(ref s) => s.get_ref().peer_addr(),
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().peer_addr(), InnerNetworkStream::RustlsTls(ref s) => s.get_ref().peer_addr(),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref s) => s.get_ref().peer_addr(),
InnerNetworkStream::None => { InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built"); debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(SocketAddr::V4(SocketAddrV4::new( Ok(SocketAddr::V4(SocketAddrV4::new(
@@ -72,6 +80,8 @@ impl NetworkStream {
InnerNetworkStream::NativeTls(ref s) => s.get_ref().shutdown(how), InnerNetworkStream::NativeTls(ref s) => s.get_ref().shutdown(how),
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().shutdown(how), InnerNetworkStream::RustlsTls(ref s) => s.get_ref().shutdown(how),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref s) => s.get_ref().shutdown(how),
InnerNetworkStream::None => { InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built"); debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(()) Ok(())
@@ -83,20 +93,40 @@ impl NetworkStream {
server: T, server: T,
timeout: Option<Duration>, timeout: Option<Duration>,
tls_parameters: Option<&TlsParameters>, tls_parameters: Option<&TlsParameters>,
local_addr: Option<IpAddr>,
) -> Result<NetworkStream, Error> { ) -> Result<NetworkStream, Error> {
fn try_connect_timeout<T: ToSocketAddrs>( fn try_connect<T: ToSocketAddrs>(
server: T, server: T,
timeout: Duration, timeout: Option<Duration>,
local_addr: Option<IpAddr>,
) -> 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)?
.filter(|resolved_addr| resolved_address_filter(resolved_addr, local_addr));
let mut last_err = None; let mut last_err = None;
for addr in addrs { for addr in addrs {
match TcpStream::connect_timeout(&addr, timeout) { let socket = socket2::Socket::new(
Ok(stream) => return Ok(stream), Domain::for_address(addr),
Type::STREAM,
Some(Protocol::TCP),
)
.map_err(error::connection)?;
bind_local_address(&socket, &addr, local_addr)?;
if let Some(timeout) = timeout {
match socket.connect_timeout(&addr.into(), timeout) {
Ok(_) => return Ok(socket.into()),
Err(err) => last_err = Some(err), Err(err) => last_err = Some(err),
} }
} else {
match socket.connect(&addr.into()) {
Ok(_) => return Ok(socket.into()),
Err(err) => last_err = Some(err),
}
}
} }
Err(match last_err { Err(match last_err {
@@ -105,11 +135,7 @@ impl NetworkStream {
}) })
} }
let tcp_stream = match timeout { let tcp_stream = try_connect(server, timeout, local_addr)?;
Some(t) => try_connect_timeout(server, t)?,
None => TcpStream::connect(server).map_err(error::connection)?,
};
let mut stream = NetworkStream::new(InnerNetworkStream::Tcp(tcp_stream)); let mut stream = NetworkStream::new(InnerNetworkStream::Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters { if let Some(tls_parameters) = tls_parameters {
stream.upgrade_tls(tls_parameters)?; stream.upgrade_tls(tls_parameters)?;
@@ -119,13 +145,17 @@ impl NetworkStream {
pub fn upgrade_tls(&mut self, tls_parameters: &TlsParameters) -> Result<(), Error> { pub fn upgrade_tls(&mut self, tls_parameters: &TlsParameters) -> Result<(), Error> {
match &self.inner { match &self.inner {
#[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))] #[cfg(not(any(
feature = "native-tls",
feature = "rustls-tls",
feature = "boring-tls"
)))]
InnerNetworkStream::Tcp(_) => { InnerNetworkStream::Tcp(_) => {
let _ = tls_parameters; let _ = tls_parameters;
panic!("Trying to upgrade an NetworkStream without having enabled either the native-tls or the rustls-tls feature"); panic!("Trying to upgrade an NetworkStream without having enabled either the native-tls or the rustls-tls feature");
} }
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
InnerNetworkStream::Tcp(_) => { InnerNetworkStream::Tcp(_) => {
// get owned TcpStream // get owned TcpStream
let tcp_stream = mem::replace(&mut self.inner, InnerNetworkStream::None); let tcp_stream = mem::replace(&mut self.inner, InnerNetworkStream::None);
@@ -141,7 +171,7 @@ impl NetworkStream {
} }
} }
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
fn upgrade_tls_impl( fn upgrade_tls_impl(
tcp_stream: TcpStream, tcp_stream: TcpStream,
tls_parameters: &TlsParameters, tls_parameters: &TlsParameters,
@@ -158,11 +188,21 @@ impl NetworkStream {
InnerTlsParameters::RustlsTls(connector) => { InnerTlsParameters::RustlsTls(connector) => {
let domain = ServerName::try_from(tls_parameters.domain()) let domain = ServerName::try_from(tls_parameters.domain())
.map_err(|_| error::connection("domain isn't a valid DNS name"))?; .map_err(|_| error::connection("domain isn't a valid DNS name"))?;
let connection = let connection = ClientConnection::new(Arc::clone(connector), domain)
ClientConnection::new(connector.clone(), domain).map_err(error::connection)?; .map_err(error::connection)?;
let stream = StreamOwned::new(connection, tcp_stream); let stream = StreamOwned::new(connection, tcp_stream);
InnerNetworkStream::RustlsTls(stream) InnerNetworkStream::RustlsTls(stream)
} }
#[cfg(feature = "boring-tls")]
InnerTlsParameters::BoringTls(connector) => {
let stream = connector
.configure()
.map_err(error::connection)?
.verify_hostname(tls_parameters.accept_invalid_hostnames)
.connect(tls_parameters.domain(), tcp_stream)
.map_err(error::connection)?;
InnerNetworkStream::BoringTls(stream)
}
}) })
} }
@@ -173,6 +213,8 @@ impl NetworkStream {
InnerNetworkStream::NativeTls(_) => true, InnerNetworkStream::NativeTls(_) => true,
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(_) => true, InnerNetworkStream::RustlsTls(_) => true,
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(_) => true,
InnerNetworkStream::None => { InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built"); debug_assert!(false, "InnerNetworkStream::None must never be built");
false false
@@ -180,7 +222,7 @@ impl NetworkStream {
} }
} }
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> { pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
match &self.inner { match &self.inner {
InnerNetworkStream::Tcp(_) => Err(error::client("Connection is not encrypted")), InnerNetworkStream::Tcp(_) => Err(error::client("Connection is not encrypted")),
@@ -200,6 +242,13 @@ impl NetworkStream {
.unwrap() .unwrap()
.clone() .clone()
.0), .0),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(stream) => Ok(stream
.ssl()
.peer_certificate()
.unwrap()
.to_der()
.map_err(error::tls)?),
InnerNetworkStream::None => panic!("InnerNetworkStream::None must never be built"), InnerNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
} }
} }
@@ -215,6 +264,10 @@ impl NetworkStream {
InnerNetworkStream::RustlsTls(ref mut stream) => { InnerNetworkStream::RustlsTls(ref mut stream) => {
stream.get_ref().set_read_timeout(duration) stream.get_ref().set_read_timeout(duration)
} }
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref mut stream) => {
stream.get_ref().set_read_timeout(duration)
}
InnerNetworkStream::None => { InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built"); debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(()) Ok(())
@@ -235,7 +288,10 @@ impl NetworkStream {
InnerNetworkStream::RustlsTls(ref mut stream) => { InnerNetworkStream::RustlsTls(ref mut stream) => {
stream.get_ref().set_write_timeout(duration) stream.get_ref().set_write_timeout(duration)
} }
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref mut stream) => {
stream.get_ref().set_write_timeout(duration)
}
InnerNetworkStream::None => { InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built"); debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(()) Ok(())
@@ -252,6 +308,8 @@ impl Read for NetworkStream {
InnerNetworkStream::NativeTls(ref mut s) => s.read(buf), InnerNetworkStream::NativeTls(ref mut s) => s.read(buf),
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.read(buf), InnerNetworkStream::RustlsTls(ref mut s) => s.read(buf),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref mut s) => s.read(buf),
InnerNetworkStream::None => { InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built"); debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(0) Ok(0)
@@ -268,6 +326,8 @@ impl Write for NetworkStream {
InnerNetworkStream::NativeTls(ref mut s) => s.write(buf), InnerNetworkStream::NativeTls(ref mut s) => s.write(buf),
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.write(buf), InnerNetworkStream::RustlsTls(ref mut s) => s.write(buf),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref mut s) => s.write(buf),
InnerNetworkStream::None => { InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built"); debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(0) Ok(0)
@@ -282,6 +342,8 @@ impl Write for NetworkStream {
InnerNetworkStream::NativeTls(ref mut s) => s.flush(), InnerNetworkStream::NativeTls(ref mut s) => s.flush(),
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.flush(), InnerNetworkStream::RustlsTls(ref mut s) => s.flush(),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref mut s) => s.flush(),
InnerNetworkStream::None => { InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built"); debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(()) Ok(())
@@ -289,3 +351,47 @@ impl Write for NetworkStream {
} }
} }
} }
/// If the local address is set, binds the socket to this address.
/// If local address is not set, then destination address is required to determine to the default
/// local address on some platforms.
/// See: https://github.com/hyperium/hyper/blob/faf24c6ad8eee1c3d5ccc9a4d4835717b8e2903f/src/client/connect/http.rs#L560
fn bind_local_address(
socket: &socket2::Socket,
dst_addr: &SocketAddr,
local_addr: Option<IpAddr>,
) -> Result<(), Error> {
match local_addr {
Some(local_addr) => {
socket
.bind(&SocketAddr::new(local_addr, 0).into())
.map_err(error::connection)?;
}
_ => {
if cfg!(windows) {
// Windows requires a socket be bound before calling connect
let any: SocketAddr = match dst_addr {
SocketAddr::V4(_) => ([0, 0, 0, 0], 0).into(),
SocketAddr::V6(_) => ([0, 0, 0, 0, 0, 0, 0, 0], 0).into(),
};
socket.bind(&any.into()).map_err(error::connection)?;
}
}
}
Ok(())
}
/// When we have an iterator of resolved remote addresses, we must filter them to be the same
/// protocol as the local address binding. If no local address is set, then all will be matched.
pub(crate) fn resolved_address_filter(
resolved_addr: &SocketAddr,
local_addr: Option<IpAddr>,
) -> bool {
match local_addr {
Some(local_addr) => match resolved_addr.ip() {
IpAddr::V4(_) => local_addr.is_ipv4(),
IpAddr::V6(_) => local_addr.is_ipv6(),
},
None => true,
}
}

View File

@@ -2,6 +2,8 @@ use std::fmt::{self, Debug};
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
use std::{sync::Arc, time::SystemTime}; use std::{sync::Arc, time::SystemTime};
#[cfg(feature = "boring-tls")]
use boring::ssl::{SslConnector, SslVersion};
#[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")]
@@ -10,14 +12,44 @@ use rustls::{
ClientConfig, Error as TlsError, OwnedTrustAnchor, RootCertStore, ServerName, ClientConfig, Error as TlsError, OwnedTrustAnchor, RootCertStore, ServerName,
}; };
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
use crate::transport::smtp::{error, Error}; use crate::transport::smtp::{error, Error};
/// Accepted protocols by default. /// TLS protocol versions.
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults. #[derive(Debug, Copy, Clone)]
// This is also rustls' default behavior #[non_exhaustive]
#[cfg(feature = "native-tls")] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
const DEFAULT_TLS_MIN_PROTOCOL: Protocol = Protocol::Tlsv12; pub enum TlsVersion {
/// TLS 1.0
///
/// Should only be used when trying to support legacy
/// SMTP servers that haven't updated to
/// at least TLS 1.2 yet.
///
/// Supported by `native-tls` and `boring-tls`.
Tlsv10,
/// TLS 1.1
///
/// Should only be used when trying to support legacy
/// SMTP servers that haven't updated to
/// at least TLS 1.2 yet.
///
/// Supported by `native-tls` and `boring-tls`.
Tlsv11,
/// TLS 1.2
///
/// A good option for most SMTP servers.
///
/// Supported by all TLS backends.
Tlsv12,
/// TLS 1.3
///
/// The most secure option, altough not supported by all SMTP servers.
///
/// Altough it is technically supported by all TLS backends,
/// trying to set it for `native-tls` will give a runtime error.
Tlsv13,
}
/// How to apply TLS to a client connection /// How to apply TLS to a client connection
#[derive(Clone)] #[derive(Clone)]
@@ -26,16 +58,25 @@ pub enum Tls {
/// Insecure connection only (for testing purposes) /// Insecure connection only (for testing purposes)
None, None,
/// Start with insecure connection and use `STARTTLS` when available /// Start with insecure connection and use `STARTTLS` when available
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))] #[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
Opportunistic(TlsParameters), Opportunistic(TlsParameters),
/// Start with insecure connection and require `STARTTLS` /// Start with insecure connection and require `STARTTLS`
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))] #[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
Required(TlsParameters), Required(TlsParameters),
/// Use TLS wrapped connection /// Use TLS wrapped connection
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))] #[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
Wrapper(TlsParameters), Wrapper(TlsParameters),
} }
@@ -43,11 +84,11 @@ impl Debug for Tls {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self { match &self {
Self::None => f.pad("None"), Self::None => f.pad("None"),
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Self::Opportunistic(_) => f.pad("Opportunistic"), Self::Opportunistic(_) => f.pad("Opportunistic"),
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Self::Required(_) => f.pad("Required"), Self::Required(_) => f.pad("Required"),
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Self::Wrapper(_) => f.pad("Wrapper"), Self::Wrapper(_) => f.pad("Wrapper"),
} }
} }
@@ -59,6 +100,7 @@ pub struct TlsParameters {
pub(crate) connector: InnerTlsParameters, pub(crate) connector: InnerTlsParameters,
/// The domain name which is expected in the TLS certificate from the server /// The domain name which is expected in the TLS certificate from the server
pub(super) domain: String, pub(super) domain: String,
pub(super) accept_invalid_hostnames: bool,
} }
/// Builder for `TlsParameters` /// Builder for `TlsParameters`
@@ -68,6 +110,8 @@ pub struct TlsParametersBuilder {
root_certs: Vec<Certificate>, root_certs: Vec<Certificate>,
accept_invalid_hostnames: bool, accept_invalid_hostnames: bool,
accept_invalid_certs: bool, accept_invalid_certs: bool,
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
min_tls_version: TlsVersion,
} }
impl TlsParametersBuilder { impl TlsParametersBuilder {
@@ -78,6 +122,8 @@ impl TlsParametersBuilder {
root_certs: Vec::new(), root_certs: Vec::new(),
accept_invalid_hostnames: false, accept_invalid_hostnames: false,
accept_invalid_certs: false, accept_invalid_certs: false,
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
min_tls_version: TlsVersion::Tlsv12,
} }
} }
@@ -102,13 +148,22 @@ impl TlsParametersBuilder {
/// This method introduces significant vulnerabilities to man-in-the-middle attacks. /// This method introduces significant vulnerabilities to man-in-the-middle attacks.
/// ///
/// Hostname verification can only be disabled with the `native-tls` TLS backend. /// Hostname verification can only be disabled with the `native-tls` TLS backend.
#[cfg(feature = "native-tls")] #[cfg(any(feature = "native-tls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))] #[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "boring-tls"))))]
pub fn dangerous_accept_invalid_hostnames(mut self, accept_invalid_hostnames: bool) -> Self { pub fn dangerous_accept_invalid_hostnames(mut self, accept_invalid_hostnames: bool) -> Self {
self.accept_invalid_hostnames = accept_invalid_hostnames; self.accept_invalid_hostnames = accept_invalid_hostnames;
self self
} }
/// Controls which minimum TLS version is allowed
///
/// Defaults to [`Tlsv12`][TlsVersion::Tlsv12].
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub fn set_min_tls_version(mut self, min_tls_version: TlsVersion) -> Self {
self.min_tls_version = min_tls_version;
self
}
/// Controls whether invalid certificates are accepted /// Controls whether invalid certificates are accepted
/// ///
/// Defaults to `false`. /// Defaults to `false`.
@@ -130,16 +185,20 @@ impl TlsParametersBuilder {
self self
} }
/// Creates a new `TlsParameters` using native-tls or rustls /// Creates a new `TlsParameters` using native-tls, boring-tls or rustls
/// depending on which one is available /// depending on which one is available
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))] #[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
pub fn build(self) -> Result<TlsParameters, Error> { pub fn build(self) -> Result<TlsParameters, Error> {
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
return self.build_rustls(); return self.build_rustls();
#[cfg(all(not(feature = "rustls-tls"), feature = "native-tls"))]
#[cfg(not(feature = "rustls-tls"))]
return self.build_native(); return self.build_native();
#[cfg(all(not(feature = "rustls-tls"), feature = "boring-tls"))]
return self.build_boring();
} }
/// Creates a new `TlsParameters` using native-tls with the provided configuration /// Creates a new `TlsParameters` using native-tls with the provided configuration
@@ -154,11 +213,59 @@ impl TlsParametersBuilder {
tls_builder.danger_accept_invalid_hostnames(self.accept_invalid_hostnames); tls_builder.danger_accept_invalid_hostnames(self.accept_invalid_hostnames);
tls_builder.danger_accept_invalid_certs(self.accept_invalid_certs); tls_builder.danger_accept_invalid_certs(self.accept_invalid_certs);
tls_builder.min_protocol_version(Some(DEFAULT_TLS_MIN_PROTOCOL)); let min_tls_version = match self.min_tls_version {
TlsVersion::Tlsv10 => Protocol::Tlsv10,
TlsVersion::Tlsv11 => Protocol::Tlsv11,
TlsVersion::Tlsv12 => Protocol::Tlsv12,
TlsVersion::Tlsv13 => {
return Err(error::tls(
"min tls version Tlsv13 not supported in native tls",
))
}
};
tls_builder.min_protocol_version(Some(min_tls_version));
let connector = tls_builder.build().map_err(error::tls)?; let connector = tls_builder.build().map_err(error::tls)?;
Ok(TlsParameters { Ok(TlsParameters {
connector: InnerTlsParameters::NativeTls(connector), connector: InnerTlsParameters::NativeTls(connector),
domain: self.domain, domain: self.domain,
accept_invalid_hostnames: self.accept_invalid_hostnames,
})
}
/// Creates a new `TlsParameters` using boring-tls with the provided configuration
#[cfg(feature = "boring-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
pub fn build_boring(self) -> Result<TlsParameters, Error> {
use boring::ssl::{SslMethod, SslVerifyMode};
let mut tls_builder = SslConnector::builder(SslMethod::tls_client()).map_err(error::tls)?;
if self.accept_invalid_certs {
tls_builder.set_verify(SslVerifyMode::NONE);
} else {
let cert_store = tls_builder.cert_store_mut();
for cert in self.root_certs {
cert_store.add_cert(cert.boring_tls).map_err(error::tls)?;
}
}
let min_tls_version = match self.min_tls_version {
TlsVersion::Tlsv10 => SslVersion::TLS1,
TlsVersion::Tlsv11 => SslVersion::TLS1_1,
TlsVersion::Tlsv12 => SslVersion::TLS1_2,
TlsVersion::Tlsv13 => SslVersion::TLS1_3,
};
tls_builder
.set_min_proto_version(Some(min_tls_version))
.map_err(error::tls)?;
let connector = tls_builder.build();
Ok(TlsParameters {
connector: InnerTlsParameters::BoringTls(connector),
domain: self.domain,
accept_invalid_hostnames: self.accept_invalid_hostnames,
}) })
} }
@@ -167,7 +274,24 @@ impl TlsParametersBuilder {
#[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> {
let tls = ClientConfig::builder(); let tls = ClientConfig::builder();
let tls = tls.with_safe_defaults();
let just_version3 = &[&rustls::version::TLS13];
let supported_versions = match self.min_tls_version {
TlsVersion::Tlsv10 => {
return Err(error::tls("min tls version Tlsv10 not supported in rustls"))
}
TlsVersion::Tlsv11 => {
return Err(error::tls("min tls version Tlsv11 not supported in rustls"))
}
TlsVersion::Tlsv12 => rustls::ALL_VERSIONS,
TlsVersion::Tlsv13 => just_version3,
};
let tls = tls
.with_safe_default_cipher_suites()
.with_safe_default_kx_groups()
.with_protocol_versions(supported_versions)
.map_err(error::tls)?;
let tls = if self.accept_invalid_certs { let tls = if self.accept_invalid_certs {
tls.with_custom_certificate_verifier(Arc::new(InvalidCertsVerifier {})) tls.with_custom_certificate_verifier(Arc::new(InvalidCertsVerifier {}))
@@ -198,27 +322,35 @@ impl TlsParametersBuilder {
Ok(TlsParameters { Ok(TlsParameters {
connector: InnerTlsParameters::RustlsTls(Arc::new(tls)), connector: InnerTlsParameters::RustlsTls(Arc::new(tls)),
domain: self.domain, domain: self.domain,
accept_invalid_hostnames: self.accept_invalid_hostnames,
}) })
} }
} }
#[derive(Clone)] #[derive(Clone)]
#[allow(clippy::enum_variant_names)]
pub enum InnerTlsParameters { pub enum InnerTlsParameters {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
NativeTls(TlsConnector), NativeTls(TlsConnector),
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
RustlsTls(Arc<ClientConfig>), RustlsTls(Arc<ClientConfig>),
#[cfg(feature = "boring-tls")]
BoringTls(SslConnector),
} }
impl TlsParameters { impl TlsParameters {
/// Creates a new `TlsParameters` using native-tls or rustls /// Creates a new `TlsParameters` using native-tls or rustls
/// depending on which one is available /// depending on which one is available
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))] #[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
pub fn new(domain: String) -> Result<Self, Error> { pub fn new(domain: String) -> Result<Self, Error> {
TlsParametersBuilder::new(domain).build() TlsParametersBuilder::new(domain).build()
} }
/// Creates a new `TlsParameters` builder
pub fn builder(domain: String) -> TlsParametersBuilder { pub fn builder(domain: String) -> TlsParametersBuilder {
TlsParametersBuilder::new(domain) TlsParametersBuilder::new(domain)
} }
@@ -237,6 +369,13 @@ impl TlsParameters {
TlsParametersBuilder::new(domain).build_rustls() TlsParametersBuilder::new(domain).build_rustls()
} }
/// Creates a new `TlsParameters` using boring
#[cfg(feature = "boring-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
pub fn new_boring(domain: String) -> Result<Self, Error> {
TlsParametersBuilder::new(domain).build_boring()
}
pub fn domain(&self) -> &str { pub fn domain(&self) -> &str {
&self.domain &self.domain
} }
@@ -250,20 +389,27 @@ pub struct Certificate {
native_tls: native_tls::Certificate, native_tls: native_tls::Certificate,
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
rustls: Vec<rustls::Certificate>, rustls: Vec<rustls::Certificate>,
#[cfg(feature = "boring-tls")]
boring_tls: boring::x509::X509,
} }
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
impl Certificate { impl Certificate {
/// Create a `Certificate` from a DER encoded certificate /// Create a `Certificate` from a DER encoded certificate
pub fn from_der(der: Vec<u8>) -> Result<Self, Error> { pub fn from_der(der: Vec<u8>) -> Result<Self, Error> {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
let native_tls_cert = native_tls::Certificate::from_der(&der).map_err(error::tls)?; let native_tls_cert = native_tls::Certificate::from_der(&der).map_err(error::tls)?;
#[cfg(feature = "boring-tls")]
let boring_tls_cert = boring::x509::X509::from_der(&der).map_err(error::tls)?;
Ok(Self { Ok(Self {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
native_tls: native_tls_cert, native_tls: native_tls_cert,
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
rustls: vec![rustls::Certificate(der)], rustls: vec![rustls::Certificate(der)],
#[cfg(feature = "boring-tls")]
boring_tls: boring_tls_cert,
}) })
} }
@@ -272,6 +418,9 @@ impl Certificate {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
let native_tls_cert = native_tls::Certificate::from_pem(pem).map_err(error::tls)?; let native_tls_cert = native_tls::Certificate::from_pem(pem).map_err(error::tls)?;
#[cfg(feature = "boring-tls")]
let boring_tls_cert = boring::x509::X509::from_pem(pem).map_err(error::tls)?;
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
let rustls_cert = { let rustls_cert = {
use std::io::Cursor; use std::io::Cursor;
@@ -289,6 +438,8 @@ impl Certificate {
native_tls: native_tls_cert, native_tls: native_tls_cert,
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
rustls: rustls_cert, rustls: rustls_cert,
#[cfg(feature = "boring-tls")]
boring_tls: boring_tls_cert,
}) })
} }
} }

View File

@@ -13,7 +13,7 @@ use crate::{
}; };
/// EHLO command /// EHLO command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Ehlo { pub struct Ehlo {
client_id: ClientId, client_id: ClientId,
@@ -33,7 +33,7 @@ impl Ehlo {
} }
/// STARTTLS command /// STARTTLS command
#[derive(PartialEq, Clone, Debug, Copy)] #[derive(PartialEq, Eq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Starttls; pub struct Starttls;
@@ -44,7 +44,7 @@ impl Display for Starttls {
} }
/// MAIL command /// MAIL command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Mail { pub struct Mail {
sender: Option<Address>, sender: Option<Address>,
@@ -73,7 +73,7 @@ impl Mail {
} }
/// RCPT command /// RCPT command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Rcpt { pub struct Rcpt {
recipient: Address, recipient: Address,
@@ -101,7 +101,7 @@ impl Rcpt {
} }
/// DATA command /// DATA command
#[derive(PartialEq, Clone, Debug, Copy)] #[derive(PartialEq, Eq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Data; pub struct Data;
@@ -112,7 +112,7 @@ impl Display for Data {
} }
/// QUIT command /// QUIT command
#[derive(PartialEq, Clone, Debug, Copy)] #[derive(PartialEq, Eq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Quit; pub struct Quit;
@@ -123,7 +123,7 @@ impl Display for Quit {
} }
/// NOOP command /// NOOP command
#[derive(PartialEq, Clone, Debug, Copy)] #[derive(PartialEq, Eq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Noop; pub struct Noop;
@@ -134,7 +134,7 @@ impl Display for Noop {
} }
/// HELP command /// HELP command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Help { pub struct Help {
argument: Option<String>, argument: Option<String>,
@@ -158,7 +158,7 @@ impl Help {
} }
/// VRFY command /// VRFY command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Vrfy { pub struct Vrfy {
argument: String, argument: String,
@@ -178,7 +178,7 @@ impl Vrfy {
} }
/// EXPN command /// EXPN command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Expn { pub struct Expn {
argument: String, argument: String,
@@ -198,7 +198,7 @@ impl Expn {
} }
/// RSET command /// RSET command
#[derive(PartialEq, Clone, Debug, Copy)] #[derive(PartialEq, Eq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Rset; pub struct Rset;
@@ -209,7 +209,7 @@ impl Display for Rset {
} }
/// AUTH command /// AUTH command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Auth { pub struct Auth {
mechanism: Mechanism, mechanism: Mechanism,

View File

@@ -68,8 +68,11 @@ impl Error {
} }
/// Returns true if the error is from TLS /// Returns true if the error is from TLS
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))] #[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
pub fn is_tls(&self) -> bool { pub fn is_tls(&self) -> bool {
matches!(self.inner.kind, Kind::Tls) matches!(self.inner.kind, Kind::Tls)
} }
@@ -102,8 +105,11 @@ pub(crate) enum Kind {
/// Underlying network i/o error /// Underlying network i/o error
Network, Network,
/// TLS error /// TLS error
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))] #[cfg_attr(
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Tls, Tls,
} }
@@ -128,7 +134,7 @@ impl fmt::Display for Error {
Kind::Client => f.write_str("internal client error")?, Kind::Client => f.write_str("internal client error")?,
Kind::Network => f.write_str("network error")?, Kind::Network => f.write_str("network error")?,
Kind::Connection => f.write_str("Connection error")?, Kind::Connection => f.write_str("Connection error")?,
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Kind::Tls => f.write_str("tls error")?, Kind::Tls => f.write_str("tls error")?,
Kind::Transient(ref code) => { Kind::Transient(ref code) => {
write!(f, "transient error ({})", code)?; write!(f, "transient error ({})", code)?;
@@ -179,7 +185,7 @@ pub(crate) fn connection<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Connection, Some(e)) Error::new(Kind::Connection, Some(e))
} }
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub(crate) fn tls<E: Into<BoxError>>(e: E) -> Error { pub(crate) fn tls<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Tls, Some(e)) Error::new(Kind::Tls, Some(e))
} }

View File

@@ -140,7 +140,7 @@ pub use self::{
error::Error, error::Error,
transport::{SmtpTransport, SmtpTransportBuilder}, transport::{SmtpTransport, SmtpTransportBuilder},
}; };
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
use crate::transport::smtp::client::TlsParameters; use crate::transport::smtp::client::TlsParameters;
use crate::transport::smtp::{ use crate::transport::smtp::{
authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS}, authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS},

View File

@@ -158,14 +158,14 @@ impl<E: Executor> Pool<E> {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("reusing a pooled connection"); tracing::debug!("reusing a pooled connection");
return Ok(PooledConnection::wrap(conn, self.clone())); return Ok(PooledConnection::wrap(conn, Arc::clone(self)));
} }
None => { None => {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("creating a new connection"); tracing::debug!("creating a new connection");
let conn = self.client.connection().await?; let conn = self.client.connection().await?;
return Ok(PooledConnection::wrap(conn, self.clone())); return Ok(PooledConnection::wrap(conn, Arc::clone(self)));
} }
} }
} }

View File

@@ -141,14 +141,14 @@ impl Pool {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("reusing a pooled connection"); tracing::debug!("reusing a pooled connection");
return Ok(PooledConnection::wrap(conn, self.clone())); return Ok(PooledConnection::wrap(conn, Arc::clone(self)));
} }
None => { None => {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("creating a new connection"); tracing::debug!("creating a new connection");
let conn = self.client.connection()?; let conn = self.client.connection()?;
return Ok(PooledConnection::wrap(conn, self.clone())); return Ok(PooledConnection::wrap(conn, Arc::clone(self)));
} }
} }
} }

View File

@@ -7,7 +7,7 @@ use super::pool::sync_impl::Pool;
#[cfg(feature = "pool")] #[cfg(feature = "pool")]
use super::PoolConfig; use super::PoolConfig;
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"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
use super::{Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT}; use super::{Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
use crate::{address::Envelope, Transport}; use crate::{address::Envelope, Transport};
@@ -45,8 +45,11 @@ impl SmtpTransport {
/// ///
/// Creates an encrypted transport over submissions port, using the provided domain /// Creates an encrypted transport over submissions port, using the provided domain
/// to validate TLS certificates. /// to validate TLS certificates.
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))] #[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
pub fn relay(relay: &str) -> Result<SmtpTransportBuilder, Error> { pub fn relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
let tls_parameters = TlsParameters::new(relay.into())?; let tls_parameters = TlsParameters::new(relay.into())?;
@@ -66,8 +69,11 @@ impl SmtpTransport {
/// ///
/// An error is returned if the connection can't be upgraded. No credentials /// An error is returned if the connection can't be upgraded. No credentials
/// or emails will be sent to the server, protecting from downgrade attacks. /// or emails will be sent to the server, protecting from downgrade attacks.
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))] #[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
pub fn starttls_relay(relay: &str) -> Result<SmtpTransportBuilder, Error> { pub fn starttls_relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
let tls_parameters = TlsParameters::new(relay.into())?; let tls_parameters = TlsParameters::new(relay.into())?;
@@ -166,8 +172,11 @@ impl SmtpTransportBuilder {
} }
/// Set the TLS settings to use /// Set the TLS settings to use
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))] #[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
pub fn tls(mut self, tls: Tls) -> Self { pub fn tls(mut self, tls: Tls) -> Self {
self.info.tls = tls; self.info.tls = tls;
self self
@@ -210,7 +219,7 @@ impl SmtpClient {
pub fn connection(&self) -> Result<SmtpConnection, Error> { pub fn connection(&self) -> Result<SmtpConnection, Error> {
#[allow(clippy::match_single_binding)] #[allow(clippy::match_single_binding)]
let tls_parameters = match self.info.tls { let tls_parameters = match self.info.tls {
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters), Tls::Wrapper(ref tls_parameters) => Some(tls_parameters),
_ => None, _ => None,
}; };
@@ -221,9 +230,10 @@ impl SmtpClient {
self.info.timeout, self.info.timeout,
&self.info.hello_name, &self.info.hello_name,
tls_parameters, tls_parameters,
None,
)?; )?;
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
match self.info.tls { match self.info.tls {
Tls::Opportunistic(ref tls_parameters) => { Tls::Opportunistic(ref tls_parameters) => {
if conn.can_starttls() { if conn.can_starttls() {

View File

@@ -41,7 +41,7 @@ mod tests {
] ]
.iter() .iter()
{ {
assert_eq!(format!("{}", XText(input)), expect.to_string()); assert_eq!(format!("{}", XText(input)), (*expect).to_string());
} }
} }
} }

View File

@@ -53,6 +53,8 @@ use futures_util::lock::Mutex as FuturesMutex;
use crate::AsyncTransport; use crate::AsyncTransport;
use crate::{address::Envelope, Transport}; use crate::{address::Envelope, Transport};
/// An error returned by the stub transport
#[non_exhaustive]
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub struct Error; pub struct Error;

View File

@@ -1,4 +1,4 @@
Date: Tue, 15 Nov 1994 08:12:31 -0000 Date: Tue, 15 Nov 1994 08:12:31 +0000
From: NoBody <nobody@domain.tld> From: NoBody <nobody@domain.tld>
Reply-To: Yuin <yuin@domain.tld> Reply-To: Yuin <yuin@domain.tld>
To: Hei <hei@domain.tld> To: Hei <hei@domain.tld>

View File

@@ -43,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!"
@@ -81,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!"
@@ -141,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!"
@@ -192,7 +192,7 @@ mod asyncstd_1 {
"Reply-To: Yuin <yuin@domain.tld>\r\n", "Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\r\n", "To: Hei <hei@domain.tld>\r\n",
"Subject: Happy new year\r\n", "Subject: Happy new year\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n", "Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
"Content-Transfer-Encoding: 7bit\r\n", "Content-Transfer-Encoding: 7bit\r\n",
"\r\n", "\r\n",
"Be happy!" "Be happy!"

View File

@@ -18,7 +18,7 @@ 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![( let expected_messages = [(
email.envelope().clone(), email.envelope().clone(),
String::from_utf8(email.formatted()).unwrap(), String::from_utf8(email.formatted()).unwrap(),
)]; )];
@@ -47,7 +47,7 @@ mod tokio_1 {
sender_ok.send(email.clone()).await.unwrap(); sender_ok.send(email.clone()).await.unwrap();
sender_ko.send(email.clone()).await.unwrap_err(); sender_ko.send(email.clone()).await.unwrap_err();
let expected_messages = vec![( let expected_messages = [(
email.envelope().clone(), email.envelope().clone(),
String::from_utf8(email.formatted()).unwrap(), String::from_utf8(email.formatted()).unwrap(),
)]; )];
@@ -75,7 +75,7 @@ mod asyncstd_1 {
sender_ok.send(email.clone()).await.unwrap(); sender_ok.send(email.clone()).await.unwrap();
sender_ko.send(email.clone()).await.unwrap_err(); sender_ko.send(email.clone()).await.unwrap_err();
let expected_messages = vec![( let expected_messages = [(
email.envelope().clone(), email.envelope().clone(),
String::from_utf8(email.formatted()).unwrap(), String::from_utf8(email.formatted()).unwrap(),
)]; )];