Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7d1f35676 | ||
|
|
eebea56f16 | ||
|
|
851d6ae164 | ||
|
|
6f38e6b9a9 | ||
|
|
c40af78809 | ||
|
|
6d2e0d5046 | ||
|
|
c64cb0ff2e | ||
|
|
10d7b197ed | ||
|
|
fb54855d5f | ||
|
|
157c4fb5ae | ||
|
|
1196e332ee | ||
|
|
75770f7bc6 | ||
|
|
76d0929c94 | ||
|
|
c3d00051b2 | ||
|
|
12580d82f4 | ||
|
|
f7849078b8 | ||
|
|
f2c94cdf4d | ||
|
|
74f64b81ab | ||
|
|
39c71dbfd2 | ||
|
|
c1bf5dfda1 | ||
|
|
1c1fef8055 | ||
|
|
1540f16015 | ||
|
|
330daa1173 | ||
|
|
47f2fe0750 | ||
|
|
8b6cee30ee | ||
|
|
62c16e90ef | ||
|
|
e0494a5f9d | ||
|
|
8c3bffa728 |
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -75,8 +75,8 @@ jobs:
|
|||||||
rust: stable
|
rust: stable
|
||||||
- name: beta
|
- name: beta
|
||||||
rust: beta
|
rust: beta
|
||||||
- name: 1.65.0
|
- name: '1.70'
|
||||||
rust: 1.65.0
|
rust: '1.70'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,4 +4,3 @@
|
|||||||
lettre.sublime-*
|
lettre.sublime-*
|
||||||
lettre.iml
|
lettre.iml
|
||||||
target/
|
target/
|
||||||
/Cargo.lock
|
|
||||||
|
|||||||
85
CHANGELOG.md
85
CHANGELOG.md
@@ -1,3 +1,86 @@
|
|||||||
|
<a name="v0.11.6"></a>
|
||||||
|
### v0.11.6 (2024-03-28)
|
||||||
|
|
||||||
|
#### Bug fixes
|
||||||
|
|
||||||
|
* Upgraded `email-encoding` to v0.3 - fixing multiple encoding bugs in the process ([#952])
|
||||||
|
|
||||||
|
#### Misc
|
||||||
|
|
||||||
|
* Updated copyright year in license ([#954])
|
||||||
|
|
||||||
|
[#952]: https://github.com/lettre/lettre/pull/952
|
||||||
|
[#954]: https://github.com/lettre/lettre/pull/954
|
||||||
|
|
||||||
|
<a name="v0.11.5"></a>
|
||||||
|
### v0.11.5 (2024-03-25)
|
||||||
|
|
||||||
|
#### Features
|
||||||
|
|
||||||
|
* Support SMTP SASL draft login challenge ([#911])
|
||||||
|
* Add conversion from SMTP response code to integer ([#941])
|
||||||
|
|
||||||
|
#### Misc
|
||||||
|
|
||||||
|
* Upgrade `rustls` to v0.23 ([#950])
|
||||||
|
* Bump `base64` to v0.22 ([#945])
|
||||||
|
* Fix typos in documentation ([#943], [#944])
|
||||||
|
* Add `Cargo.lock` ([#942])
|
||||||
|
|
||||||
|
[#911]: https://github.com/lettre/lettre/pull/911
|
||||||
|
[#941]: https://github.com/lettre/lettre/pull/941
|
||||||
|
[#942]: https://github.com/lettre/lettre/pull/942
|
||||||
|
[#943]: https://github.com/lettre/lettre/pull/943
|
||||||
|
[#944]: https://github.com/lettre/lettre/pull/944
|
||||||
|
[#945]: https://github.com/lettre/lettre/pull/945
|
||||||
|
[#950]: https://github.com/lettre/lettre/pull/950
|
||||||
|
|
||||||
|
<a name="v0.11.4"></a>
|
||||||
|
### v0.11.4 (2024-01-28)
|
||||||
|
|
||||||
|
#### Bug fixes
|
||||||
|
|
||||||
|
* Percent decode credentials in SMTP connect URL ([#932], [#934])
|
||||||
|
* Fix mimebody DKIM body-hash computation ([#923])
|
||||||
|
|
||||||
|
[#923]: https://github.com/lettre/lettre/pull/923
|
||||||
|
[#932]: https://github.com/lettre/lettre/pull/932
|
||||||
|
[#934]: https://github.com/lettre/lettre/pull/934
|
||||||
|
|
||||||
|
<a name="v0.11.3"></a>
|
||||||
|
### v0.11.3 (2024-01-02)
|
||||||
|
|
||||||
|
#### Features
|
||||||
|
|
||||||
|
* Derive `Clone` for `FileTransport` and `AsyncFileTransport` ([#924])
|
||||||
|
* Derive `Debug` for `SmtpTransport` ([#925])
|
||||||
|
|
||||||
|
#### Misc
|
||||||
|
|
||||||
|
* Upgrade `rustls` to v0.22 ([#921])
|
||||||
|
* Drop once_cell dependency in favor of OnceLock from std ([#928])
|
||||||
|
|
||||||
|
[#921]: https://github.com/lettre/lettre/pull/921
|
||||||
|
[#924]: https://github.com/lettre/lettre/pull/924
|
||||||
|
[#925]: https://github.com/lettre/lettre/pull/925
|
||||||
|
[#928]: https://github.com/lettre/lettre/pull/928
|
||||||
|
|
||||||
|
<a name="v0.11.2"></a>
|
||||||
|
### v0.11.2 (2023-11-23)
|
||||||
|
|
||||||
|
#### Upgrade notes
|
||||||
|
|
||||||
|
* MSRV is now 1.70 ([#916])
|
||||||
|
|
||||||
|
#### Misc
|
||||||
|
|
||||||
|
* Bump `idna` to v0.5 ([#918])
|
||||||
|
* Bump `boring` and `tokio-boring` to v4 ([#915])
|
||||||
|
|
||||||
|
[#915]: https://github.com/lettre/lettre/pull/915
|
||||||
|
[#916]: https://github.com/lettre/lettre/pull/916
|
||||||
|
[#918]: https://github.com/lettre/lettre/pull/918
|
||||||
|
|
||||||
<a name="v0.11.1"></a>
|
<a name="v0.11.1"></a>
|
||||||
### v0.11.1 (2023-10-24)
|
### v0.11.1 (2023-10-24)
|
||||||
|
|
||||||
@@ -198,7 +281,7 @@ Several breaking changes were made between 0.9 and 0.10, but changes should be s
|
|||||||
* Update `hostname` to 0.3
|
* Update `hostname` to 0.3
|
||||||
* Update to `nom` 6
|
* Update to `nom` 6
|
||||||
* Replace `log` with `tracing`
|
* Replace `log` with `tracing`
|
||||||
* Move CI to Github Actions
|
* Move CI to GitHub Actions
|
||||||
* Use criterion for benchmarks
|
* Use criterion for benchmarks
|
||||||
|
|
||||||
<a name="v0.9.2"></a>
|
<a name="v0.9.2"></a>
|
||||||
|
|||||||
2661
Cargo.lock
generated
Normal file
2661
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
Cargo.toml
32
Cargo.toml
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lettre"
|
name = "lettre"
|
||||||
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
|
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
|
||||||
version = "0.11.1"
|
version = "0.11.6"
|
||||||
description = "Email client"
|
description = "Email client"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
homepage = "https://lettre.rs"
|
homepage = "https://lettre.rs"
|
||||||
@@ -11,7 +11,7 @@ authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo
|
|||||||
categories = ["email", "network-programming"]
|
categories = ["email", "network-programming"]
|
||||||
keywords = ["email", "smtp", "mailer", "message", "sendmail"]
|
keywords = ["email", "smtp", "mailer", "message", "sendmail"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.65"
|
rust-version = "1.70"
|
||||||
|
|
||||||
[badges]
|
[badges]
|
||||||
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
|
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
|
||||||
@@ -20,8 +20,7 @@ maintenance = { status = "actively-developed" }
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chumsky = "0.9"
|
chumsky = "0.9"
|
||||||
idna = "0.4"
|
idna = "0.5"
|
||||||
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,8 +28,8 @@ httpdate = { version = "1", optional = true }
|
|||||||
mime = { version = "0.3.4", optional = true }
|
mime = { version = "0.3.4", optional = true }
|
||||||
fastrand = { version = "2.0", optional = true }
|
fastrand = { version = "2.0", optional = true }
|
||||||
quoted_printable = { version = "0.5", optional = true }
|
quoted_printable = { version = "0.5", optional = true }
|
||||||
base64 = { version = "0.21", optional = true }
|
base64 = { version = "0.22", optional = true }
|
||||||
email-encoding = { version = "0.2", optional = true }
|
email-encoding = { version = "0.3", optional = true }
|
||||||
|
|
||||||
# file transport
|
# file transport
|
||||||
uuid = { version = "1", features = ["v4"], optional = true }
|
uuid = { version = "1", features = ["v4"], optional = true }
|
||||||
@@ -42,14 +41,15 @@ nom = { version = "7", optional = true }
|
|||||||
hostname = { version = "0.3", optional = true } # feature
|
hostname = { version = "0.3", optional = true } # feature
|
||||||
socket2 = { version = "0.5.1", optional = true }
|
socket2 = { version = "0.5.1", optional = true }
|
||||||
url = { version = "2.4", optional = true }
|
url = { version = "2.4", optional = true }
|
||||||
|
percent-encoding = { version = "2.3", optional = true }
|
||||||
|
|
||||||
## tls
|
## tls
|
||||||
native-tls = { version = "0.2.5", optional = true } # feature
|
native-tls = { version = "0.2.5", optional = true } # feature
|
||||||
rustls = { version = "0.21", features = ["dangerous_configuration"], optional = true }
|
rustls = { version = "0.23.3", default-features = false, features = ["ring", "logging", "std", "tls12"], optional = true }
|
||||||
rustls-pemfile = { version = "1", optional = true }
|
rustls-pemfile = { version = "2", optional = true }
|
||||||
rustls-native-certs = { version = "0.6.2", optional = true }
|
rustls-native-certs = { version = "0.7", optional = true }
|
||||||
webpki-roots = { version = "0.25", optional = true }
|
webpki-roots = { version = "0.26", optional = true }
|
||||||
boring = { version = "3", optional = true }
|
boring = { version = "4", optional = true }
|
||||||
|
|
||||||
# async
|
# async
|
||||||
futures-io = { version = "0.3.7", optional = true }
|
futures-io = { version = "0.3.7", optional = true }
|
||||||
@@ -59,13 +59,13 @@ async-trait = { version = "0.1", optional = true }
|
|||||||
## async-std
|
## async-std
|
||||||
async-std = { version = "1.8", optional = true }
|
async-std = { version = "1.8", optional = true }
|
||||||
#async-native-tls = { version = "0.3.3", optional = true }
|
#async-native-tls = { version = "0.3.3", optional = true }
|
||||||
futures-rustls = { version = "0.24", optional = true }
|
futures-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12", "ring"], optional = true }
|
||||||
|
|
||||||
## tokio
|
## tokio
|
||||||
tokio1_crate = { package = "tokio", version = "1", optional = true }
|
tokio1_crate = { package = "tokio", version = "1", 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.24", optional = true }
|
tokio1_rustls = { package = "tokio-rustls", version = "0.26", default-features = false, features = ["logging", "tls12", "ring"], optional = true }
|
||||||
tokio1_boring = { package = "tokio-boring", version = "3", optional = true }
|
tokio1_boring = { package = "tokio-boring", version = "4", optional = true }
|
||||||
|
|
||||||
## dkim
|
## dkim
|
||||||
sha2 = { version = "0.10", optional = true, features = ["oid"] }
|
sha2 = { version = "0.10", optional = true, features = ["oid"] }
|
||||||
@@ -85,7 +85,7 @@ 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.25"
|
maud = "0.26"
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
harness = false
|
harness = false
|
||||||
@@ -104,7 +104,7 @@ mime03 = ["dep:mime"]
|
|||||||
file-transport = ["dep:uuid", "tokio1_crate?/fs", "tokio1_crate?/io-util"]
|
file-transport = ["dep:uuid", "tokio1_crate?/fs", "tokio1_crate?/io-util"]
|
||||||
file-transport-envelope = ["serde", "dep:serde_json", "file-transport"]
|
file-transport-envelope = ["serde", "dep:serde_json", "file-transport"]
|
||||||
sendmail-transport = ["tokio1_crate?/process", "tokio1_crate?/io-util", "async-std?/unstable"]
|
sendmail-transport = ["tokio1_crate?/process", "tokio1_crate?/io-util", "async-std?/unstable"]
|
||||||
smtp-transport = ["dep:base64", "dep:nom", "dep:socket2", "dep:once_cell", "dep:url", "tokio1_crate?/rt", "tokio1_crate?/time", "tokio1_crate?/net"]
|
smtp-transport = ["dep:base64", "dep:nom", "dep:socket2", "dep:url", "dep:percent-encoding", "tokio1_crate?/rt", "tokio1_crate?/time", "tokio1_crate?/net"]
|
||||||
|
|
||||||
pool = ["dep:futures-util"]
|
pool = ["dep:futures-util"]
|
||||||
|
|
||||||
|
|||||||
4
LICENSE
4
LICENSE
@@ -1,5 +1,5 @@
|
|||||||
Copyright (c) 2014-2022 Alexis Mousset <contact@amousset.me>
|
Copyright (c) 2014-2024 Alexis Mousset <contact@amousset.me>
|
||||||
Copyright (c) 2019-2022 Paolo Barbolini <paolo@paolo565.org>
|
Copyright (c) 2019-2024 Paolo Barbolini <paolo@paolo565.org>
|
||||||
Copyright (c) 2018 K. <kayo@illumium.org>
|
Copyright (c) 2018 K. <kayo@illumium.org>
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any
|
Permission is hereby granted, free of charge, to any
|
||||||
|
|||||||
@@ -28,8 +28,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://deps.rs/crate/lettre/0.11.1">
|
<a href="https://deps.rs/crate/lettre/0.11.6">
|
||||||
<img src="https://deps.rs/crate/lettre/0.11.1/status.svg"
|
<img src="https://deps.rs/crate/lettre/0.11.6/status.svg"
|
||||||
alt="dependency status" />
|
alt="dependency status" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,12 +53,12 @@ Lettre does not provide (for now):
|
|||||||
## Supported Rust Versions
|
## Supported Rust Versions
|
||||||
|
|
||||||
Lettre supports all Rust versions released in the last 6 months. At the time of writing
|
Lettre supports all Rust versions released in the last 6 months. At the time of writing
|
||||||
the minimum supported Rust version is 1.65, but this could change at any time either from
|
the minimum supported Rust version is 1.70, but this could change at any time either from
|
||||||
one of our dependencies bumping their MSRV or by a new patch release of lettre.
|
one of our dependencies bumping their MSRV or by a new patch release of lettre.
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
This library requires Rust 1.65 or newer.
|
This library requires Rust 1.70 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
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
//! * Secure defaults
|
//! * Secure defaults
|
||||||
//! * Async support
|
//! * Async support
|
||||||
//!
|
//!
|
||||||
//! Lettre requires Rust 1.65 or newer.
|
//! Lettre requires Rust 1.70 or newer.
|
||||||
//!
|
//!
|
||||||
//! ## Features
|
//! ## Features
|
||||||
//!
|
//!
|
||||||
@@ -109,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.11.1")]
|
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.6")]
|
||||||
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
|
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
|
||||||
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
|
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use std::{
|
|||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
error::Error as StdError,
|
error::Error as StdError,
|
||||||
fmt::{self, Display},
|
fmt::{self, Display},
|
||||||
iter::IntoIterator,
|
|
||||||
time::SystemTime,
|
time::SystemTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
|
||||||
use email_encoding::headers::EmailWriter;
|
use email_encoding::headers::writer::EmailWriter;
|
||||||
|
|
||||||
use super::{Header, HeaderName, HeaderValue};
|
use super::{Header, HeaderName, HeaderValue};
|
||||||
use crate::BoxError;
|
use crate::BoxError;
|
||||||
@@ -38,10 +38,10 @@ impl ContentDisposition {
|
|||||||
let mut encoded_value = String::new();
|
let mut encoded_value = String::new();
|
||||||
let line_len = "Content-Disposition: ".len();
|
let line_len = "Content-Disposition: ".len();
|
||||||
{
|
{
|
||||||
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false, false);
|
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false);
|
||||||
w.write_str(kind).expect("writing `kind` returned an error");
|
w.write_str(kind).expect("writing `kind` returned an error");
|
||||||
w.write_char(';').expect("writing `;` returned an error");
|
w.write_char(';').expect("writing `;` returned an error");
|
||||||
w.optional_breakpoint();
|
w.space();
|
||||||
|
|
||||||
email_encoding::headers::rfc2231::encode("filename", file_name, &mut w)
|
email_encoding::headers::rfc2231::encode("filename", file_name, &mut w)
|
||||||
.expect("some Write implementation returned an error");
|
.expect("some Write implementation returned an error");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use email_encoding::headers::EmailWriter;
|
use email_encoding::headers::writer::EmailWriter;
|
||||||
|
|
||||||
use super::{Header, HeaderName, HeaderValue};
|
use super::{Header, HeaderName, HeaderValue};
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -31,7 +31,7 @@ macro_rules! mailbox_header {
|
|||||||
let mut encoded_value = String::new();
|
let mut encoded_value = String::new();
|
||||||
let line_len = $header_name.len() + ": ".len();
|
let line_len = $header_name.len() + ": ".len();
|
||||||
{
|
{
|
||||||
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false, false);
|
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false);
|
||||||
self.0.encode(&mut w).expect("writing `Mailbox` returned an error");
|
self.0.encode(&mut w).expect("writing `Mailbox` returned an error");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ macro_rules! mailboxes_header {
|
|||||||
let mut encoded_value = String::new();
|
let mut encoded_value = String::new();
|
||||||
let line_len = $header_name.len() + ": ".len();
|
let line_len = $header_name.len() + ": ".len();
|
||||||
{
|
{
|
||||||
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false, false);
|
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false);
|
||||||
self.0.encode(&mut w).expect("writing `Mailboxes` returned an error");
|
self.0.encode(&mut w).expect("writing `Mailboxes` returned an error");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::{
|
|||||||
ops::Deref,
|
ops::Deref,
|
||||||
};
|
};
|
||||||
|
|
||||||
use email_encoding::headers::EmailWriter;
|
use email_encoding::headers::writer::EmailWriter;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
content::*,
|
content::*,
|
||||||
@@ -348,7 +348,7 @@ impl<'a> HeaderValueEncoder<'a> {
|
|||||||
|
|
||||||
fn new(name: &str, writer: &'a mut dyn Write) -> Self {
|
fn new(name: &str, writer: &'a mut dyn Write) -> Self {
|
||||||
let line_len = name.len() + ": ".len();
|
let line_len = name.len() + ": ".len();
|
||||||
let writer = EmailWriter::new(writer, line_len, 0, false, false);
|
let writer = EmailWriter::new(writer, line_len, 0, false);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
writer,
|
writer,
|
||||||
@@ -612,17 +612,14 @@ mod tests {
|
|||||||
"🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_owned(),
|
"🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_owned(),
|
||||||
));
|
));
|
||||||
|
|
||||||
// TODO: fix the fact that the encoder doesn't know that
|
|
||||||
// the space between the name and the address should be
|
|
||||||
// removed when wrapping.
|
|
||||||
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==?=\r\n",
|
||||||
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9?=\r\n",
|
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9?=\r\n",
|
||||||
" =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
|
" =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
|
||||||
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>,\r\n",
|
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, =?utf-8?b?U2U=?=\r\n",
|
||||||
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n",
|
" =?utf-8?b?w6FuIMOTIFJ1ZGHDrQ==?= <sean@example.com>\r\n",
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -687,9 +684,6 @@ mod tests {
|
|||||||
"quoted-printable".to_owned(),
|
"quoted-printable".to_owned(),
|
||||||
));
|
));
|
||||||
|
|
||||||
// TODO: fix the fact that the encoder doesn't know that
|
|
||||||
// the space between the name and the address should be
|
|
||||||
// removed when wrapping.
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
concat!(
|
concat!(
|
||||||
@@ -699,8 +693,8 @@ mod tests {
|
|||||||
"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==?=\r\n",
|
||||||
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9?=\r\n",
|
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9?=\r\n",
|
||||||
" =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
|
" =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
|
||||||
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>,\r\n",
|
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, =?utf-8?b?U2U=?=\r\n",
|
||||||
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n",
|
" =?utf-8?b?w6FuIMOTIFJ1ZGHDrQ==?= <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",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use chumsky::prelude::*;
|
use chumsky::prelude::*;
|
||||||
use email_encoding::headers::EmailWriter;
|
use email_encoding::headers::writer::EmailWriter;
|
||||||
|
|
||||||
use super::parsers;
|
use super::parsers;
|
||||||
use crate::address::{Address, AddressError};
|
use crate::address::{Address, AddressError};
|
||||||
@@ -72,7 +72,7 @@ impl Mailbox {
|
|||||||
pub(crate) fn encode(&self, w: &mut EmailWriter<'_>) -> FmtResult {
|
pub(crate) fn encode(&self, w: &mut EmailWriter<'_>) -> FmtResult {
|
||||||
if let Some(name) = &self.name {
|
if let Some(name) = &self.name {
|
||||||
email_encoding::headers::quoted_string::encode(name, w)?;
|
email_encoding::headers::quoted_string::encode(name, w)?;
|
||||||
w.optional_breakpoint();
|
w.space();
|
||||||
w.write_char('<')?;
|
w.write_char('<')?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,7 +261,7 @@ impl Mailboxes {
|
|||||||
for mailbox in self.iter() {
|
for mailbox in self.iter() {
|
||||||
if !mem::take(&mut first) {
|
if !mem::take(&mut first) {
|
||||||
w.write_char(',')?;
|
w.write_char(',')?;
|
||||||
w.optional_breakpoint();
|
w.space();
|
||||||
}
|
}
|
||||||
|
|
||||||
mailbox.encode(w)?;
|
mailbox.encode(w)?;
|
||||||
@@ -444,8 +444,6 @@ fn write_quoted_string_char(f: &mut Formatter<'_>, c: char) -> FmtResult {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use std::convert::TryInto;
|
|
||||||
|
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::Mailbox;
|
use super::Mailbox;
|
||||||
|
|||||||
@@ -17,6 +17,16 @@ pub(super) enum Part {
|
|||||||
Multi(MultiPart),
|
Multi(MultiPart),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Part {
|
||||||
|
#[cfg(feature = "dkim")]
|
||||||
|
pub(super) fn format_body(&self, out: &mut Vec<u8>) {
|
||||||
|
match self {
|
||||||
|
Part::Single(part) => part.format_body(out),
|
||||||
|
Part::Multi(part) => part.format_body(out),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl EmailFormat for Part {
|
impl EmailFormat for Part {
|
||||||
fn format(&self, out: &mut Vec<u8>) {
|
fn format(&self, out: &mut Vec<u8>) {
|
||||||
match self {
|
match self {
|
||||||
@@ -132,6 +142,12 @@ impl SinglePart {
|
|||||||
self.format(&mut out);
|
self.format(&mut out);
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Format only the signlepart body
|
||||||
|
fn format_body(&self, out: &mut Vec<u8>) {
|
||||||
|
out.extend_from_slice(&self.body);
|
||||||
|
out.extend_from_slice(b"\r\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EmailFormat for SinglePart {
|
impl EmailFormat for SinglePart {
|
||||||
@@ -139,8 +155,7 @@ impl EmailFormat for SinglePart {
|
|||||||
write!(out, "{}", self.headers)
|
write!(out, "{}", self.headers)
|
||||||
.expect("A Write implementation panicked while formatting headers");
|
.expect("A Write implementation panicked while formatting headers");
|
||||||
out.extend_from_slice(b"\r\n");
|
out.extend_from_slice(b"\r\n");
|
||||||
out.extend_from_slice(&self.body);
|
self.format_body(out);
|
||||||
out.extend_from_slice(b"\r\n");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,14 +388,9 @@ impl MultiPart {
|
|||||||
self.format(&mut out);
|
self.format(&mut out);
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl EmailFormat for MultiPart {
|
|
||||||
fn format(&self, out: &mut Vec<u8>) {
|
|
||||||
write!(out, "{}", self.headers)
|
|
||||||
.expect("A Write implementation panicked while formatting headers");
|
|
||||||
out.extend_from_slice(b"\r\n");
|
|
||||||
|
|
||||||
|
/// Format only the multipart body
|
||||||
|
fn format_body(&self, out: &mut Vec<u8>) {
|
||||||
let boundary = self.boundary();
|
let boundary = self.boundary();
|
||||||
|
|
||||||
for part in &self.parts {
|
for part in &self.parts {
|
||||||
@@ -396,12 +406,20 @@ impl EmailFormat for MultiPart {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl EmailFormat for MultiPart {
|
||||||
|
fn format(&self, out: &mut Vec<u8>) {
|
||||||
|
write!(out, "{}", self.headers)
|
||||||
|
.expect("A Write implementation panicked while formatting headers");
|
||||||
|
out.extend_from_slice(b"\r\n");
|
||||||
|
self.format_body(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::message::header;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn single_part_binary() {
|
fn single_part_binary() {
|
||||||
|
|||||||
@@ -525,7 +525,7 @@ impl Message {
|
|||||||
pub(crate) fn body_raw(&self) -> Vec<u8> {
|
pub(crate) fn body_raw(&self) -> Vec<u8> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
match &self.body {
|
match &self.body {
|
||||||
MessageBody::Mime(p) => p.format(&mut out),
|
MessageBody::Mime(p) => p.format_body(&mut out),
|
||||||
MessageBody::Raw(r) => out.extend_from_slice(r),
|
MessageBody::Raw(r) => out.extend_from_slice(r),
|
||||||
};
|
};
|
||||||
out.extend_from_slice(b"\r\n");
|
out.extend_from_slice(b"\r\n");
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ mod error;
|
|||||||
type Id = String;
|
type Id = String;
|
||||||
|
|
||||||
/// Writes the content and the envelope information to a file
|
/// Writes the content and the envelope information to a file
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport")))]
|
||||||
pub struct FileTransport {
|
pub struct FileTransport {
|
||||||
@@ -167,7 +167,7 @@ pub struct FileTransport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Asynchronously writes the content and the envelope information to a file
|
/// Asynchronously writes the content and the envelope information to a file
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
//! | [`smtp`] | SMTP | [`SmtpTransport`] | [`AsyncSmtpTransport`] | Uses the SMTP protocol to send emails to a relay server |
|
//! | [`smtp`] | SMTP | [`SmtpTransport`] | [`AsyncSmtpTransport`] | Uses the SMTP protocol to send emails to a relay server |
|
||||||
//! | [`sendmail`] | Sendmail | [`SendmailTransport`] | [`AsyncSendmailTransport`] | Uses the `sendmail` command to send emails |
|
//! | [`sendmail`] | Sendmail | [`SendmailTransport`] | [`AsyncSendmailTransport`] | Uses the `sendmail` command to send emails |
|
||||||
//! | [`file`] | File | [`FileTransport`] | [`AsyncFileTransport`] | Saves the email as an `.eml` file |
|
//! | [`file`] | File | [`FileTransport`] | [`AsyncFileTransport`] | Saves the email as an `.eml` file |
|
||||||
//! | [`stub`] | Debug | [`StubTransport`] | [`StubTransport`] | Drops the email - Useful for debugging |
|
//! | [`stub`] | Debug | [`StubTransport`] | [`AsyncStubTransport`] | Drops the email - Useful for debugging |
|
||||||
//!
|
//!
|
||||||
//! ## Building an email
|
//! ## Building an email
|
||||||
//!
|
//!
|
||||||
@@ -97,6 +97,7 @@
|
|||||||
//! [`FileTransport`]: crate::FileTransport
|
//! [`FileTransport`]: crate::FileTransport
|
||||||
//! [`AsyncFileTransport`]: crate::AsyncFileTransport
|
//! [`AsyncFileTransport`]: crate::AsyncFileTransport
|
||||||
//! [`StubTransport`]: crate::transport::stub::StubTransport
|
//! [`StubTransport`]: crate::transport::stub::StubTransport
|
||||||
|
//! [`AsyncStubTransport`]: crate::transport::stub::AsyncStubTransport
|
||||||
|
|
||||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|||||||
@@ -98,11 +98,13 @@ 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 ["User Name", "Username:", "Username"].contains(&decoded_challenge) {
|
if ["User Name", "Username:", "Username", "User Name\0"]
|
||||||
|
.contains(&decoded_challenge)
|
||||||
|
{
|
||||||
return Ok(credentials.authentication_identity.clone());
|
return Ok(credentials.authentication_identity.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
if ["Password", "Password:"].contains(&decoded_challenge) {
|
if ["Password", "Password:", "Password\0"].contains(&decoded_challenge) {
|
||||||
return Ok(credentials.secret.clone());
|
return Ok(credentials.secret.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ impl AsyncSmtpConnection {
|
|||||||
/// If `tls_parameters` is `Some`, then the connection will use Implicit TLS (sometimes
|
/// If `tls_parameters` is `Some`, then the connection will use Implicit TLS (sometimes
|
||||||
/// referred to as `SMTPS`). See also [`AsyncSmtpConnection::starttls`].
|
/// referred to as `SMTPS`). See also [`AsyncSmtpConnection::starttls`].
|
||||||
///
|
///
|
||||||
/// If `local_addres` is `Some`, then the address provided shall be used to bind the
|
/// If `local_address` is `Some`, then the address provided shall be used to bind the
|
||||||
/// connection to a specific local address using [`tokio1_crate::net::TcpSocket::bind`].
|
/// connection to a specific local address using [`tokio1_crate::net::TcpSocket::bind`].
|
||||||
///
|
///
|
||||||
/// Sends EHLO and parses server information
|
/// Sends EHLO and parses server information
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ 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(any(feature = "tokio1-rustls-tls", feature = "async-std1-rustls-tls"))]
|
||||||
|
use rustls::pki_types::ServerName;
|
||||||
#[cfg(feature = "tokio1-boring-tls")]
|
#[cfg(feature = "tokio1-boring-tls")]
|
||||||
use tokio1_boring::SslStream as Tokio1SslStream;
|
use tokio1_boring::SslStream as Tokio1SslStream;
|
||||||
#[cfg(feature = "tokio1")]
|
#[cfg(feature = "tokio1")]
|
||||||
@@ -350,7 +352,6 @@ impl AsyncNetworkStream {
|
|||||||
|
|
||||||
#[cfg(feature = "tokio1-rustls-tls")]
|
#[cfg(feature = "tokio1-rustls-tls")]
|
||||||
return {
|
return {
|
||||||
use rustls::ServerName;
|
|
||||||
use tokio1_rustls::TlsConnector;
|
use tokio1_rustls::TlsConnector;
|
||||||
|
|
||||||
let domain = ServerName::try_from(domain.as_str())
|
let domain = ServerName::try_from(domain.as_str())
|
||||||
@@ -358,7 +359,7 @@ impl AsyncNetworkStream {
|
|||||||
|
|
||||||
let connector = TlsConnector::from(config);
|
let connector = TlsConnector::from(config);
|
||||||
let stream = connector
|
let stream = connector
|
||||||
.connect(domain, tcp_stream)
|
.connect(domain.to_owned(), tcp_stream)
|
||||||
.await
|
.await
|
||||||
.map_err(error::connection)?;
|
.map_err(error::connection)?;
|
||||||
Ok(InnerAsyncNetworkStream::Tokio1RustlsTls(stream))
|
Ok(InnerAsyncNetworkStream::Tokio1RustlsTls(stream))
|
||||||
@@ -424,14 +425,13 @@ impl AsyncNetworkStream {
|
|||||||
#[cfg(feature = "async-std1-rustls-tls")]
|
#[cfg(feature = "async-std1-rustls-tls")]
|
||||||
return {
|
return {
|
||||||
use futures_rustls::TlsConnector;
|
use futures_rustls::TlsConnector;
|
||||||
use rustls::ServerName;
|
|
||||||
|
|
||||||
let domain = ServerName::try_from(domain.as_str())
|
let domain = ServerName::try_from(domain.as_str())
|
||||||
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
|
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
|
||||||
|
|
||||||
let connector = TlsConnector::from(config);
|
let connector = TlsConnector::from(config);
|
||||||
let stream = connector
|
let stream = connector
|
||||||
.connect(domain, tcp_stream)
|
.connect(domain.to_owned(), tcp_stream)
|
||||||
.await
|
.await
|
||||||
.map_err(error::connection)?;
|
.map_err(error::connection)?;
|
||||||
Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream))
|
Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream))
|
||||||
@@ -486,8 +486,7 @@ impl AsyncNetworkStream {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.first()
|
.first()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.clone()
|
.to_vec()),
|
||||||
.0),
|
|
||||||
#[cfg(feature = "tokio1-boring-tls")]
|
#[cfg(feature = "tokio1-boring-tls")]
|
||||||
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => Ok(stream
|
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => Ok(stream
|
||||||
.ssl()
|
.ssl()
|
||||||
@@ -509,8 +508,7 @@ impl AsyncNetworkStream {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.first()
|
.first()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.clone()
|
.to_vec()),
|
||||||
.0),
|
|
||||||
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
|
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ 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::{pki_types::ServerName, ClientConnection, StreamOwned};
|
||||||
use socket2::{Domain, Protocol, Type};
|
use socket2::{Domain, Protocol, Type};
|
||||||
|
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
@@ -189,7 +189,7 @@ 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 = ClientConnection::new(Arc::clone(connector), domain)
|
let connection = ClientConnection::new(Arc::clone(connector), domain.to_owned())
|
||||||
.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)
|
||||||
@@ -241,8 +241,7 @@ impl NetworkStream {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.first()
|
.first()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.clone()
|
.to_vec()),
|
||||||
.0),
|
|
||||||
#[cfg(feature = "boring-tls")]
|
#[cfg(feature = "boring-tls")]
|
||||||
InnerNetworkStream::BoringTls(stream) => Ok(stream
|
InnerNetworkStream::BoringTls(stream) => Ok(stream
|
||||||
.ssl()
|
.ssl()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::fmt::{self, Debug};
|
use std::fmt::{self, Debug};
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls-tls")]
|
||||||
use std::{sync::Arc, time::SystemTime};
|
use std::{io, sync::Arc};
|
||||||
|
|
||||||
#[cfg(feature = "boring-tls")]
|
#[cfg(feature = "boring-tls")]
|
||||||
use boring::{
|
use boring::{
|
||||||
@@ -11,8 +11,10 @@ use boring::{
|
|||||||
use native_tls::{Protocol, TlsConnector};
|
use native_tls::{Protocol, TlsConnector};
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls-tls")]
|
||||||
use rustls::{
|
use rustls::{
|
||||||
client::{ServerCertVerified, ServerCertVerifier, WebPkiVerifier},
|
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
|
||||||
ClientConfig, Error as TlsError, RootCertStore, ServerName,
|
crypto::{verify_tls12_signature, verify_tls13_signature},
|
||||||
|
pki_types::{CertificateDer, ServerName, UnixTime},
|
||||||
|
ClientConfig, DigitallySignedStruct, Error as TlsError, RootCertStore, SignatureScheme,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
@@ -337,8 +339,6 @@ impl TlsParametersBuilder {
|
|||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls-tls")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
|
||||||
pub fn build_rustls(self) -> Result<TlsParameters, Error> {
|
pub fn build_rustls(self) -> Result<TlsParameters, Error> {
|
||||||
let tls = ClientConfig::builder();
|
|
||||||
|
|
||||||
let just_version3 = &[&rustls::version::TLS13];
|
let just_version3 = &[&rustls::version::TLS13];
|
||||||
let supported_versions = match self.min_tls_version {
|
let supported_versions = match self.min_tls_version {
|
||||||
TlsVersion::Tlsv10 => {
|
TlsVersion::Tlsv10 => {
|
||||||
@@ -351,50 +351,28 @@ impl TlsParametersBuilder {
|
|||||||
TlsVersion::Tlsv13 => just_version3,
|
TlsVersion::Tlsv13 => just_version3,
|
||||||
};
|
};
|
||||||
|
|
||||||
let tls = tls
|
let tls = ClientConfig::builder_with_protocol_versions(supported_versions);
|
||||||
.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.dangerous()
|
||||||
|
.with_custom_certificate_verifier(Arc::new(InvalidCertsVerifier {}))
|
||||||
} else {
|
} else {
|
||||||
let mut root_cert_store = RootCertStore::empty();
|
let mut root_cert_store = RootCertStore::empty();
|
||||||
|
|
||||||
#[cfg(feature = "rustls-native-certs")]
|
#[cfg(feature = "rustls-native-certs")]
|
||||||
fn load_native_roots(store: &mut RootCertStore) -> Result<(), Error> {
|
fn load_native_roots(store: &mut RootCertStore) -> Result<(), Error> {
|
||||||
let native_certs = rustls_native_certs::load_native_certs().map_err(error::tls)?;
|
let native_certs = rustls_native_certs::load_native_certs().map_err(error::tls)?;
|
||||||
let mut valid_count = 0;
|
let (added, ignored) = store.add_parsable_certificates(native_certs);
|
||||||
let mut invalid_count = 0;
|
|
||||||
for cert in native_certs {
|
|
||||||
match store.add(&rustls::Certificate(cert.0)) {
|
|
||||||
Ok(_) => valid_count += 1,
|
|
||||||
Err(err) => {
|
|
||||||
#[cfg(feature = "tracing")]
|
|
||||||
tracing::debug!("certificate parsing failed: {:?}", err);
|
|
||||||
invalid_count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"loaded platform certs with {valid_count} valid and {invalid_count} invalid certs"
|
"loaded platform certs with {added} valid and {ignored} ignored (invalid) certs"
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls-tls")]
|
||||||
fn load_webpki_roots(store: &mut RootCertStore) {
|
fn load_webpki_roots(store: &mut RootCertStore) {
|
||||||
// TODO: handle this in the rustls 0.22 upgrade
|
store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||||
#[allow(deprecated)]
|
|
||||||
store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.iter().map(|ta| {
|
|
||||||
rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
|
|
||||||
ta.subject,
|
|
||||||
ta.spki,
|
|
||||||
ta.name_constraints,
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.cert_store {
|
match self.cert_store {
|
||||||
@@ -412,14 +390,11 @@ impl TlsParametersBuilder {
|
|||||||
}
|
}
|
||||||
for cert in self.root_certs {
|
for cert in self.root_certs {
|
||||||
for rustls_cert in cert.rustls {
|
for rustls_cert in cert.rustls {
|
||||||
root_cert_store.add(&rustls_cert).map_err(error::tls)?;
|
root_cert_store.add(rustls_cert).map_err(error::tls)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tls.with_custom_certificate_verifier(Arc::new(WebPkiVerifier::new(
|
tls.with_root_certificates(root_cert_store)
|
||||||
root_cert_store,
|
|
||||||
None,
|
|
||||||
)))
|
|
||||||
};
|
};
|
||||||
let tls = tls.with_no_client_auth();
|
let tls = tls.with_no_client_auth();
|
||||||
|
|
||||||
@@ -493,7 +468,7 @@ pub struct Certificate {
|
|||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
native_tls: native_tls::Certificate,
|
native_tls: native_tls::Certificate,
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls-tls")]
|
||||||
rustls: Vec<rustls::Certificate>,
|
rustls: Vec<CertificateDer<'static>>,
|
||||||
#[cfg(feature = "boring-tls")]
|
#[cfg(feature = "boring-tls")]
|
||||||
boring_tls: boring::x509::X509,
|
boring_tls: boring::x509::X509,
|
||||||
}
|
}
|
||||||
@@ -512,7 +487,7 @@ impl Certificate {
|
|||||||
#[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![der.into()],
|
||||||
#[cfg(feature = "boring-tls")]
|
#[cfg(feature = "boring-tls")]
|
||||||
boring_tls: boring_tls_cert,
|
boring_tls: boring_tls_cert,
|
||||||
})
|
})
|
||||||
@@ -532,10 +507,8 @@ impl Certificate {
|
|||||||
|
|
||||||
let mut pem = Cursor::new(pem);
|
let mut pem = Cursor::new(pem);
|
||||||
rustls_pemfile::certs(&mut pem)
|
rustls_pemfile::certs(&mut pem)
|
||||||
|
.collect::<io::Result<Vec<_>>>()
|
||||||
.map_err(|_| error::tls("invalid certificates"))?
|
.map_err(|_| error::tls("invalid certificates"))?
|
||||||
.into_iter()
|
|
||||||
.map(rustls::Certificate)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
@@ -556,19 +529,53 @@ impl Debug for Certificate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls-tls")]
|
||||||
|
#[derive(Debug)]
|
||||||
struct InvalidCertsVerifier;
|
struct InvalidCertsVerifier;
|
||||||
|
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls-tls")]
|
||||||
impl ServerCertVerifier for InvalidCertsVerifier {
|
impl ServerCertVerifier for InvalidCertsVerifier {
|
||||||
fn verify_server_cert(
|
fn verify_server_cert(
|
||||||
&self,
|
&self,
|
||||||
_end_entity: &rustls::Certificate,
|
_end_entity: &CertificateDer<'_>,
|
||||||
_intermediates: &[rustls::Certificate],
|
_intermediates: &[CertificateDer<'_>],
|
||||||
_server_name: &ServerName,
|
_server_name: &ServerName<'_>,
|
||||||
_scts: &mut dyn Iterator<Item = &[u8]>,
|
|
||||||
_ocsp_response: &[u8],
|
_ocsp_response: &[u8],
|
||||||
_now: SystemTime,
|
_now: UnixTime,
|
||||||
) -> Result<ServerCertVerified, TlsError> {
|
) -> Result<ServerCertVerified, TlsError> {
|
||||||
Ok(ServerCertVerified::assertion())
|
Ok(ServerCertVerified::assertion())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn verify_tls12_signature(
|
||||||
|
&self,
|
||||||
|
message: &[u8],
|
||||||
|
cert: &CertificateDer<'_>,
|
||||||
|
dss: &DigitallySignedStruct,
|
||||||
|
) -> Result<HandshakeSignatureValid, TlsError> {
|
||||||
|
verify_tls12_signature(
|
||||||
|
message,
|
||||||
|
cert,
|
||||||
|
dss,
|
||||||
|
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_tls13_signature(
|
||||||
|
&self,
|
||||||
|
message: &[u8],
|
||||||
|
cert: &CertificateDer<'_>,
|
||||||
|
dss: &DigitallySignedStruct,
|
||||||
|
) -> Result<HandshakeSignatureValid, TlsError> {
|
||||||
|
verify_tls13_signature(
|
||||||
|
message,
|
||||||
|
cert,
|
||||||
|
dss,
|
||||||
|
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||||
|
rustls::crypto::ring::default_provider()
|
||||||
|
.signature_verification_algorithms
|
||||||
|
.supported_schemes()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,16 @@ pub(crate) fn from_connection_url<B: TransportBuilder>(connection_url: &str) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(password) = connection_url.password() {
|
if let Some(password) = connection_url.password() {
|
||||||
let credentials = Credentials::new(connection_url.username().into(), password.into());
|
let percent_decode = |s: &str| {
|
||||||
|
percent_encoding::percent_decode_str(s)
|
||||||
|
.decode_utf8()
|
||||||
|
.map(|cow| cow.into_owned())
|
||||||
|
.map_err(error::connection)
|
||||||
|
};
|
||||||
|
let credentials = Credentials::new(
|
||||||
|
percent_decode(connection_url.username())?,
|
||||||
|
percent_decode(password)?,
|
||||||
|
);
|
||||||
builder = builder.credentials(credentials);
|
builder = builder.credentials(credentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ use std::{
|
|||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
fmt::{self, Display, Formatter},
|
fmt::{self, Display, Formatter},
|
||||||
net::{Ipv4Addr, Ipv6Addr},
|
net::{Ipv4Addr, Ipv6Addr},
|
||||||
result::Result,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::transport::smtp::{
|
use crate::transport::smtp::{
|
||||||
@@ -292,14 +291,8 @@ impl Display for RcptParameter {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::transport::smtp::{
|
use crate::transport::smtp::response::{Category, Code, Detail, Severity};
|
||||||
authentication::Mechanism,
|
|
||||||
response::{Category, Code, Detail, Response, Severity},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_clientid_fmt() {
|
fn test_clientid_fmt() {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::{
|
|||||||
fmt::{self, Debug},
|
fmt::{self, Debug},
|
||||||
mem,
|
mem,
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
sync::Arc,
|
sync::{Arc, OnceLock},
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -10,7 +10,6 @@ use futures_util::{
|
|||||||
lock::Mutex,
|
lock::Mutex,
|
||||||
stream::{self, StreamExt},
|
stream::{self, StreamExt},
|
||||||
};
|
};
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
super::{client::AsyncSmtpConnection, Error},
|
super::{client::AsyncSmtpConnection, Error},
|
||||||
@@ -22,7 +21,7 @@ pub struct Pool<E: Executor> {
|
|||||||
config: PoolConfig,
|
config: PoolConfig,
|
||||||
connections: Mutex<Vec<ParkedConnection>>,
|
connections: Mutex<Vec<ParkedConnection>>,
|
||||||
client: AsyncSmtpClient<E>,
|
client: AsyncSmtpClient<E>,
|
||||||
handle: OnceCell<E::Handle>,
|
handle: OnceLock<E::Handle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ParkedConnection {
|
struct ParkedConnection {
|
||||||
@@ -41,7 +40,7 @@ impl<E: Executor> Pool<E> {
|
|||||||
config,
|
config,
|
||||||
connections: Mutex::new(Vec::new()),
|
connections: Mutex::new(Vec::new()),
|
||||||
client,
|
client,
|
||||||
handle: OnceCell::new(),
|
handle: OnceLock::new(),
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ use std::{
|
|||||||
fmt::{Display, Formatter, Result},
|
fmt::{Display, Formatter, Result},
|
||||||
result,
|
result,
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
string::ToString,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use nom::{
|
use nom::{
|
||||||
@@ -132,6 +131,12 @@ impl Code {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Code> for u16 {
|
||||||
|
fn from(code: Code) -> Self {
|
||||||
|
code.detail as u16 + 10 * code.category as u16 + 100 * code.severity as u16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Contains an SMTP reply, with separated code and message
|
/// Contains an SMTP reply, with separated code and message
|
||||||
///
|
///
|
||||||
/// The text message is optional, only the code is mandatory
|
/// The text message is optional, only the code is mandatory
|
||||||
@@ -317,6 +322,17 @@ mod test {
|
|||||||
assert_eq!(code.to_string(), "421");
|
assert_eq!(code.to_string(), "421");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_code_to_u16() {
|
||||||
|
let code = Code {
|
||||||
|
severity: Severity::TransientNegativeCompletion,
|
||||||
|
category: Category::Connections,
|
||||||
|
detail: Detail::One,
|
||||||
|
};
|
||||||
|
let c: u16 = code.into();
|
||||||
|
assert_eq!(c, 421);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_response_from_str() {
|
fn test_response_from_str() {
|
||||||
let raw_response = "250-me\r\n250-8BITMIME\r\n250-SIZE 42\r\n250 AUTH PLAIN CRAM-MD5\r\n";
|
let raw_response = "250-me\r\n250-8BITMIME\r\n250-SIZE 42\r\n250 AUTH PLAIN CRAM-MD5\r\n";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#[cfg(feature = "pool")]
|
#[cfg(feature = "pool")]
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::{fmt::Debug, time::Duration};
|
||||||
|
|
||||||
#[cfg(feature = "pool")]
|
#[cfg(feature = "pool")]
|
||||||
use super::pool::sync_impl::Pool;
|
use super::pool::sync_impl::Pool;
|
||||||
@@ -38,6 +38,14 @@ impl Transport for SmtpTransport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Debug for SmtpTransport {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let mut builder = f.debug_struct("SmtpTransport");
|
||||||
|
builder.field("inner", &self.inner);
|
||||||
|
builder.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SmtpTransport {
|
impl SmtpTransport {
|
||||||
/// Simple and secure transport, using TLS connections to communicate with the SMTP server
|
/// Simple and secure transport, using TLS connections to communicate with the SMTP server
|
||||||
///
|
///
|
||||||
@@ -373,6 +381,22 @@ mod tests {
|
|||||||
assert!(matches!(builder.info.tls, Tls::Wrapper(_)));
|
assert!(matches!(builder.info.tls, Tls::Wrapper(_)));
|
||||||
assert_eq!(builder.info.server, "smtp.example.com");
|
assert_eq!(builder.info.server, "smtp.example.com");
|
||||||
|
|
||||||
|
let builder = SmtpTransport::from_url(
|
||||||
|
"smtps://user%40example.com:pa$$word%3F%22!@smtp.example.com:465",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(builder.info.port, 465);
|
||||||
|
assert_eq!(
|
||||||
|
builder.info.credentials,
|
||||||
|
Some(Credentials::new(
|
||||||
|
"user@example.com".to_owned(),
|
||||||
|
"pa$$word?\"!".to_owned()
|
||||||
|
))
|
||||||
|
);
|
||||||
|
assert!(matches!(builder.info.tls, Tls::Wrapper(_)));
|
||||||
|
assert_eq!(builder.info.server, "smtp.example.com");
|
||||||
|
|
||||||
let builder =
|
let builder =
|
||||||
SmtpTransport::from_url("smtp://username:password@smtp.example.com:587?tls=required")
|
SmtpTransport::from_url("smtp://username:password@smtp.example.com:587?tls=required")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user