Compare commits

...

26 Commits

Author SHA1 Message Date
Paolo Barbolini
efee4b5d72 Fix warnings 2024-03-19 16:21:10 +01:00
Paolo Barbolini
57d7bf25cc Replace try_smtp macro with more resilient method 2024-03-19 16:21:10 +01:00
Paolo Barbolini
c52c596458 Drop ref syntax 2024-03-10 15:28:54 +01:00
Paolo Barbolini
53dee3e31f Drop need for NetworkStream::None variant 2024-03-10 15:28:54 +01:00
dependabot[bot]
c64cb0ff2e Bump mio from 0.8.10 to 0.8.11 (#946)
Bumps [mio](https://github.com/tokio-rs/mio) from 0.8.10 to 0.8.11.
- [Release notes](https://github.com/tokio-rs/mio/releases)
- [Changelog](https://github.com/tokio-rs/mio/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/mio/compare/v0.8.10...v0.8.11)

---
updated-dependencies:
- dependency-name: mio
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-04 22:38:54 +01:00
Paolo Barbolini
10d7b197ed chore(cargo): bump base64 to v0.22 (#945) 2024-03-02 11:34:57 +01:00
Viktor Szépe
fb54855d5f Fix typos (#944) 2024-02-21 14:49:42 +01:00
ciffelia
157c4fb5ae docs(transport): fix error in "Available transports" table (#943) 2024-02-18 17:19:27 +01:00
Hodu Mayo
1196e332ee feat(transport-smtp): Support to SASL draft login challenge (#911) 2024-02-14 21:18:56 +01:00
Alexis Mousset
75770f7bc6 Add conversion from SMTP code to integer (#941) 2024-02-14 21:01:21 +01:00
Alexis Mousset
76d0929c94 Add a Cargo.lock (#942) 2024-02-14 20:50:54 +01:00
Paolo Barbolini
c3d00051b2 Prepare 0.11.4 (#936) 2024-01-28 08:08:26 +01:00
Birk Tjelmeland
12580d82f4 style(email): Change Part::body_raw to Part::format_body 2024-01-28 07:54:16 +01:00
Birk Tjelmeland
f7849078b8 fix(email): Fix mimebody DKIM body-hash computation 2024-01-28 07:54:16 +01:00
Paolo Barbolini
f2c94cdf4d chore(cargo): bump maud to v0.26 (#935) 2024-01-25 20:08:32 +01:00
Paolo Barbolini
74f64b81ab test(transport/smtp): test credentials percent decoding from URL (#934) 2024-01-25 20:05:31 +01:00
42triangles
39c71dbfd2 transport/smtp: percent decode credentials in URL (#932) 2024-01-12 11:43:30 +00:00
Paolo Barbolini
c1bf5dfda1 Prepare 0.11.3 (#929) 2024-01-02 18:45:34 +01:00
Paolo Barbolini
1c1fef8055 Drop once_cell dependency in favor of OnceLock from std (#928) 2024-01-02 11:53:47 +01:00
Paolo Barbolini
1540f16015 Upgrade rustls to v0.22 (#921) 2024-01-02 11:41:16 +01:00
Tobias Bieniek
330daa1173 transport/smtp: Implement Debug trait (#925) 2023-12-17 09:20:51 +01:00
Tobias Bieniek
47f2fe0750 transport/file: Derive Clone impls (#924) 2023-12-16 21:30:57 +01:00
Paolo Barbolini
8b6cee30ee Prepare 0.11.2 (#919) 2023-11-23 09:49:21 +01:00
Paolo Barbolini
62c16e90ef Bump idna to v0.5 (#918) 2023-11-23 08:23:25 +00:00
Paolo Barbolini
e0494a5f9d Bump boringssl crates to v4 (#915) 2023-11-19 11:49:43 +01:00
Paolo Barbolini
8c3bffa728 Bump MSRV to 1.70 (#916) 2023-11-19 11:42:49 +01:00
29 changed files with 3166 additions and 462 deletions

View File

@@ -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
View File

@@ -4,4 +4,3 @@
lettre.sublime-* lettre.sublime-*
lettre.iml lettre.iml
target/ target/
/Cargo.lock

View File

@@ -1,3 +1,49 @@
<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 +244,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>

2660
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

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.11.1" version = "0.11.4"
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,7 +28,7 @@ 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.2", optional = true }
# file transport # file transport
@@ -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.22.1", 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.25", 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.25", 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"]

View File

@@ -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.4">
<img src="https://deps.rs/crate/lettre/0.11.1/status.svg" <img src="https://deps.rs/crate/lettre/0.11.4/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

View File

@@ -134,7 +134,7 @@ impl Executor for Tokio1Executor {
#[allow(clippy::match_single_binding)] #[allow(clippy::match_single_binding)]
let tls_parameters = match tls { let tls_parameters = match tls {
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))] #[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()), Tls::Wrapper(tls_parameters) => Some(tls_parameters.clone()),
_ => None, _ => None,
}; };
#[allow(unused_mut)] #[allow(unused_mut)]
@@ -149,13 +149,13 @@ impl Executor for Tokio1Executor {
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))] #[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
match tls { match tls {
Tls::Opportunistic(ref tls_parameters) => { Tls::Opportunistic(tls_parameters) => {
if conn.can_starttls() { if conn.can_starttls() {
conn.starttls(tls_parameters.clone(), hello_name).await?; conn = conn.starttls(tls_parameters.clone(), hello_name).await?;
} }
} }
Tls::Required(ref tls_parameters) => { Tls::Required(tls_parameters) => {
conn.starttls(tls_parameters.clone(), hello_name).await?; conn = conn.starttls(tls_parameters.clone(), hello_name).await?;
} }
_ => (), _ => (),
} }
@@ -231,7 +231,7 @@ impl Executor for AsyncStd1Executor {
#[allow(clippy::match_single_binding)] #[allow(clippy::match_single_binding)]
let tls_parameters = match tls { let tls_parameters = match tls {
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))] #[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()), Tls::Wrapper(tls_parameters) => Some(tls_parameters.clone()),
_ => None, _ => None,
}; };
#[allow(unused_mut)] #[allow(unused_mut)]
@@ -245,13 +245,13 @@ impl Executor for AsyncStd1Executor {
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))] #[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
match tls { match tls {
Tls::Opportunistic(ref tls_parameters) => { Tls::Opportunistic(tls_parameters) => {
if conn.can_starttls() { if conn.can_starttls() {
conn.starttls(tls_parameters.clone(), hello_name).await?; conn = conn.starttls(tls_parameters.clone(), hello_name).await?;
} }
} }
Tls::Required(ref tls_parameters) => { Tls::Required(tls_parameters) => {
conn.starttls(tls_parameters.clone(), hello_name).await?; conn = conn.starttls(tls_parameters.clone(), hello_name).await?;
} }
_ => (), _ => (),
} }

View File

@@ -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.4")]
#![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)]

View File

@@ -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,
}; };

View File

@@ -88,7 +88,7 @@ impl Mailbox {
impl Display for Mailbox { impl Display for Mailbox {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
if let Some(ref name) = self.name { if let Some(name) = &self.name {
let name = name.trim(); let name = name.trim();
if !name.is_empty() { if !name.is_empty() {
write_word(f, name)?; write_word(f, name)?;

View File

@@ -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,6 +406,15 @@ 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;

View File

@@ -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");

View File

@@ -54,7 +54,7 @@ impl fmt::Debug for Error {
builder.field("kind", &self.inner.kind); builder.field("kind", &self.inner.kind);
if let Some(ref source) = self.inner.source { if let Some(source) = &self.inner.source {
builder.field("source", source); builder.field("source", source);
} }
@@ -70,7 +70,7 @@ impl fmt::Display for Error {
Kind::Envelope => f.write_str("internal client error")?, Kind::Envelope => f.write_str("internal client error")?,
}; };
if let Some(ref e) = self.inner.source { if let Some(e) = &self.inner.source {
write!(f, ": {e}")?; write!(f, ": {e}")?;
} }

View File

@@ -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"))]

View File

@@ -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;

View File

@@ -52,7 +52,7 @@ impl fmt::Debug for Error {
builder.field("kind", &self.inner.kind); builder.field("kind", &self.inner.kind);
if let Some(ref source) = self.inner.source { if let Some(source) = &self.inner.source {
builder.field("source", source); builder.field("source", source);
} }
@@ -67,7 +67,7 @@ impl fmt::Display for Error {
Kind::Client => f.write_str("internal client error")?, Kind::Client => f.write_str("internal client error")?,
}; };
if let Some(ref e) = self.inner.source { if let Some(e) = &self.inner.source {
write!(f, ": {e}")?; write!(f, ": {e}")?;
} }

View File

@@ -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());
} }

View File

@@ -6,7 +6,7 @@ use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use super::async_net::AsyncTokioStream; use super::async_net::AsyncTokioStream;
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
use super::escape_crlf; use super::escape_crlf;
use super::{AsyncNetworkStream, ClientCodec, TlsParameters}; use super::{AsyncNetworkStream, ClientCodec, ConnectionState, TlsParameters};
use crate::{ use crate::{
transport::smtp::{ transport::smtp::{
authentication::{Credentials, Mechanism}, authentication::{Credentials, Mechanism},
@@ -19,25 +19,11 @@ use crate::{
Envelope, Envelope,
}; };
macro_rules! try_smtp (
($err: expr, $client: ident) => ({
match $err {
Ok(val) => val,
Err(err) => {
$client.abort().await;
return Err(From::from(err))
},
}
})
);
/// Structure that implements the SMTP client /// Structure that implements the SMTP client
pub struct AsyncSmtpConnection { pub struct AsyncSmtpConnection {
/// TCP stream between client and server /// TCP stream between client and server
/// Value is None before connection /// Value is None before connection
stream: BufReader<AsyncNetworkStream>, stream: BufReader<AsyncNetworkStream>,
/// Panic state
panic: bool,
/// Information about the server /// Information about the server
server_info: ServerInfo, server_info: ServerInfo,
} }
@@ -65,7 +51,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
@@ -126,7 +112,6 @@ impl AsyncSmtpConnection {
let stream = BufReader::new(stream); let stream = BufReader::new(stream);
let mut conn = AsyncSmtpConnection { let mut conn = AsyncSmtpConnection {
stream, stream,
panic: false,
server_info: ServerInfo::default(), server_info: ServerInfo::default(),
}; };
// TODO log // TODO log
@@ -170,30 +155,26 @@ impl AsyncSmtpConnection {
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime)); mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
} }
try_smtp!( self.command(Mail::new(envelope.from().cloned(), mail_options))
self.command(Mail::new(envelope.from().cloned(), mail_options)) .await?;
.await,
self
);
// Recipient // Recipient
for to_address in envelope.to() { for to_address in envelope.to() {
try_smtp!( self.command(Rcpt::new(to_address.clone(), vec![])).await?;
self.command(Rcpt::new(to_address.clone(), vec![])).await,
self
);
} }
// Data // Data
try_smtp!(self.command(Data).await, self); self.command(Data).await?;
// Message content // Message content
let result = try_smtp!(self.message(email).await, self); self.message(email).await
Ok(result)
} }
pub fn has_broken(&self) -> bool { pub fn has_broken(&self) -> bool {
self.panic match self.stream.get_ref().state() {
ConnectionState::Ok => false,
ConnectionState::Broken | ConnectionState::Closed => true,
}
} }
pub fn can_starttls(&self) -> bool { pub fn can_starttls(&self) -> bool {
@@ -208,18 +189,20 @@ impl AsyncSmtpConnection {
/// [rfc8314]: https://www.rfc-editor.org/rfc/rfc8314 /// [rfc8314]: https://www.rfc-editor.org/rfc/rfc8314
#[allow(unused_variables)] #[allow(unused_variables)]
pub async fn starttls( pub async fn starttls(
&mut self, mut self,
tls_parameters: TlsParameters, tls_parameters: TlsParameters,
hello_name: &ClientId, hello_name: &ClientId,
) -> Result<(), Error> { ) -> Result<Self, Error> {
if self.server_info.supports_feature(Extension::StartTls) { if self.server_info.supports_feature(Extension::StartTls) {
try_smtp!(self.command(Starttls).await, self); self.command(Starttls).await?;
self.stream.get_mut().upgrade_tls(tls_parameters).await?; let stream = self.stream.into_inner();
let stream = stream.upgrade_tls(tls_parameters).await?;
self.stream = BufReader::new(stream);
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("connection encrypted"); tracing::debug!("connection encrypted");
// Send EHLO again // Send EHLO again
try_smtp!(self.ehlo(hello_name).await, self); self.ehlo(hello_name).await?;
Ok(()) Ok(self)
} else { } else {
Err(error::client("STARTTLS is not supported on this server")) Err(error::client("STARTTLS is not supported on this server"))
} }
@@ -227,22 +210,24 @@ impl AsyncSmtpConnection {
/// Send EHLO and update server info /// Send EHLO and update server info
async fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> { async fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> {
let ehlo_response = try_smtp!(self.command(Ehlo::new(hello_name.clone())).await, self); let ehlo_response = self.command(Ehlo::new(hello_name.clone())).await?;
self.server_info = try_smtp!(ServerInfo::from_response(&ehlo_response), self); self.server_info = ServerInfo::from_response(&ehlo_response)?;
Ok(()) Ok(())
} }
pub async fn quit(&mut self) -> Result<Response, Error> { pub async fn quit(&mut self) -> Result<Response, Error> {
Ok(try_smtp!(self.command(Quit).await, self)) self.command(Quit).await
} }
pub async fn abort(&mut self) { pub async fn abort(&mut self) {
// Only try to quit if we are not already broken match self.stream.get_ref().state() {
if !self.panic { ConnectionState::Ok | ConnectionState::Broken => {
self.panic = true; let _ = self.command(Quit).await;
let _ = self.command(Quit).await; let _ = self.stream.close().await;
self.stream.get_mut().set_state(ConnectionState::Closed);
}
ConnectionState::Closed => {}
} }
let _ = self.stream.close().await;
} }
/// Sets the underlying stream /// Sets the underlying stream
@@ -279,15 +264,13 @@ impl AsyncSmtpConnection {
while challenges > 0 && response.has_code(334) { while challenges > 0 && response.has_code(334) {
challenges -= 1; challenges -= 1;
response = try_smtp!( response = self
self.command(Auth::new_from_response( .command(Auth::new_from_response(
mechanism, mechanism,
credentials.clone(), credentials.clone(),
&response, &response,
)?) )?)
.await, .await?;
self
);
} }
if challenges == 0 { if challenges == 0 {
@@ -315,6 +298,9 @@ impl AsyncSmtpConnection {
/// Writes a string to the server /// Writes a string to the server
async fn write(&mut self, string: &[u8]) -> Result<(), Error> { async fn write(&mut self, string: &[u8]) -> Result<(), Error> {
self.stream.get_ref().state().verify()?;
self.stream.get_mut().set_state(ConnectionState::Broken);
self.stream self.stream
.get_mut() .get_mut()
.write_all(string) .write_all(string)
@@ -326,6 +312,8 @@ impl AsyncSmtpConnection {
.await .await
.map_err(error::network)?; .map_err(error::network)?;
self.stream.get_mut().set_state(ConnectionState::Ok);
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string))); tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
Ok(()) Ok(())
@@ -333,6 +321,9 @@ impl AsyncSmtpConnection {
/// Gets the SMTP response /// Gets the SMTP response
pub async fn read_response(&mut self) -> Result<Response, Error> { pub async fn read_response(&mut self) -> Result<Response, Error> {
self.stream.get_ref().state().verify()?;
self.stream.get_mut().set_state(ConnectionState::Broken);
let mut buffer = String::with_capacity(100); let mut buffer = String::with_capacity(100);
while self while self
@@ -346,6 +337,8 @@ impl AsyncSmtpConnection {
tracing::debug!("<< {}", escape_crlf(&buffer)); tracing::debug!("<< {}", escape_crlf(&buffer));
match parse_response(&buffer) { match parse_response(&buffer) {
Ok((_remaining, response)) => { Ok((_remaining, response)) => {
self.stream.get_mut().set_state(ConnectionState::Ok);
return if response.is_positive() { return if response.is_positive() {
Ok(response) Ok(response)
} else { } else {
@@ -353,7 +346,7 @@ impl AsyncSmtpConnection {
response.code(), response.code(),
Some(response.message().collect()), Some(response.message().collect()),
)) ))
} };
} }
Err(nom::Err::Failure(e)) => { Err(nom::Err::Failure(e)) => {
return Err(error::response(e.to_string())); return Err(error::response(e.to_string()));

View File

@@ -11,11 +11,12 @@ use async_native_tls::TlsStream as AsyncStd1TlsStream;
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
use async_std::net::{TcpStream as AsyncStd1TcpStream, ToSocketAddrs as AsyncStd1ToSocketAddrs}; use async_std::net::{TcpStream as AsyncStd1TcpStream, ToSocketAddrs as AsyncStd1ToSocketAddrs};
use futures_io::{ use futures_io::{
AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Error as IoError, ErrorKind, AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Result as IoResult,
Result as IoResult,
}; };
#[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")]
@@ -38,7 +39,7 @@ use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
feature = "async-std1-rustls-tls" feature = "async-std1-rustls-tls"
))] ))]
use super::InnerTlsParameters; use super::InnerTlsParameters;
use super::TlsParameters; use super::{ConnectionState, TlsParameters};
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
use crate::transport::smtp::client::net::resolved_address_filter; use crate::transport::smtp::client::net::resolved_address_filter;
use crate::transport::smtp::{error, Error}; use crate::transport::smtp::{error, Error};
@@ -47,6 +48,7 @@ use crate::transport::smtp::{error, Error};
#[derive(Debug)] #[derive(Debug)]
pub struct AsyncNetworkStream { pub struct AsyncNetworkStream {
inner: InnerAsyncNetworkStream, inner: InnerAsyncNetworkStream,
state: ConnectionState,
} }
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
@@ -89,45 +91,43 @@ enum InnerAsyncNetworkStream {
/// Encrypted Tokio 1.x TCP stream /// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "async-std1-rustls-tls")] #[cfg(feature = "async-std1-rustls-tls")]
AsyncStd1RustlsTls(AsyncStd1RustlsTlsStream<AsyncStd1TcpStream>), AsyncStd1RustlsTls(AsyncStd1RustlsTlsStream<AsyncStd1TcpStream>),
/// Can't be built
None,
} }
impl AsyncNetworkStream { impl AsyncNetworkStream {
fn new(inner: InnerAsyncNetworkStream) -> Self { fn new(inner: InnerAsyncNetworkStream) -> Self {
if let InnerAsyncNetworkStream::None = inner { AsyncNetworkStream {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built"); inner,
state: ConnectionState::Ok,
} }
}
AsyncNetworkStream { inner } pub(super) fn state(&self) -> ConnectionState {
self.state
}
pub(super) fn set_state(&mut self, state: ConnectionState) {
self.state = state;
} }
/// Returns peer's address /// Returns peer's address
pub fn peer_addr(&self) -> IoResult<SocketAddr> { pub fn peer_addr(&self) -> IoResult<SocketAddr> {
match self.inner { match &self.inner {
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(ref s) => s.peer_addr(), InnerAsyncNetworkStream::Tokio1Tcp(s) => s.peer_addr(),
#[cfg(feature = "tokio1-native-tls")] #[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(ref s) => { InnerAsyncNetworkStream::Tokio1NativeTls(s) => {
s.get_ref().get_ref().get_ref().peer_addr() s.get_ref().get_ref().get_ref().peer_addr()
} }
#[cfg(feature = "tokio1-rustls-tls")] #[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(ref s) => s.get_ref().0.peer_addr(), InnerAsyncNetworkStream::Tokio1RustlsTls(s) => s.get_ref().0.peer_addr(),
#[cfg(feature = "tokio1-boring-tls")] #[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(ref s) => s.get_ref().peer_addr(), InnerAsyncNetworkStream::Tokio1BoringTls(s) => s.get_ref().peer_addr(),
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(ref s) => s.peer_addr(), InnerAsyncNetworkStream::AsyncStd1Tcp(s) => s.peer_addr(),
#[cfg(feature = "async-std1-native-tls")] #[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref s) => s.get_ref().peer_addr(), InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => s.get_ref().peer_addr(),
#[cfg(feature = "async-std1-rustls-tls")] #[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref s) => s.get_ref().0.peer_addr(), InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => s.get_ref().0.peer_addr(),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Err(IoError::new(
ErrorKind::Other,
"InnerAsyncNetworkStream::None must never be built",
))
}
} }
} }
@@ -197,7 +197,7 @@ impl AsyncNetworkStream {
let mut stream = let mut stream =
AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(Box::new(tcp_stream))); AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(Box::new(tcp_stream)));
if let Some(tls_parameters) = tls_parameters { if let Some(tls_parameters) = tls_parameters {
stream.upgrade_tls(tls_parameters).await?; stream = stream.upgrade_tls(tls_parameters).await?;
} }
Ok(stream) Ok(stream)
} }
@@ -248,13 +248,13 @@ impl AsyncNetworkStream {
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream)); let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters { if let Some(tls_parameters) = tls_parameters {
stream.upgrade_tls(tls_parameters).await?; stream = stream.upgrade_tls(tls_parameters).await?;
} }
Ok(stream) Ok(stream)
} }
pub async fn upgrade_tls(&mut self, tls_parameters: TlsParameters) -> Result<(), Error> { pub async fn upgrade_tls(self, tls_parameters: TlsParameters) -> Result<Self, Error> {
match &self.inner { match self.inner {
#[cfg(all( #[cfg(all(
feature = "tokio1", feature = "tokio1",
not(any( not(any(
@@ -273,18 +273,14 @@ impl AsyncNetworkStream {
feature = "tokio1-rustls-tls", feature = "tokio1-rustls-tls",
feature = "tokio1-boring-tls" feature = "tokio1-boring-tls"
))] ))]
InnerAsyncNetworkStream::Tokio1Tcp(_) => { InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) => {
// get owned TcpStream let inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters)
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
let tcp_stream = match tcp_stream {
InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) => tcp_stream,
_ => unreachable!(),
};
self.inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters)
.await .await
.map_err(error::connection)?; .map_err(error::connection)?;
Ok(()) Ok(Self {
inner,
state: ConnectionState::Ok,
})
} }
#[cfg(all( #[cfg(all(
feature = "async-std1", feature = "async-std1",
@@ -296,20 +292,16 @@ impl AsyncNetworkStream {
} }
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))] #[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => { InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream) => {
// get owned TcpStream let inner = Self::upgrade_asyncstd1_tls(tcp_stream, tls_parameters)
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
let tcp_stream = match tcp_stream {
InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream) => tcp_stream,
_ => unreachable!(),
};
self.inner = Self::upgrade_asyncstd1_tls(tcp_stream, tls_parameters)
.await .await
.map_err(error::connection)?; .map_err(error::connection)?;
Ok(()) Ok(Self {
inner,
state: ConnectionState::Ok,
})
} }
_ => Ok(()), _ => Ok(self),
} }
} }
@@ -350,7 +342,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 +349,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 +415,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))
@@ -445,7 +435,7 @@ impl AsyncNetworkStream {
} }
pub fn is_encrypted(&self) -> bool { pub fn is_encrypted(&self) -> bool {
match self.inner { match &self.inner {
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(_) => false, InnerAsyncNetworkStream::Tokio1Tcp(_) => false,
#[cfg(feature = "tokio1-native-tls")] #[cfg(feature = "tokio1-native-tls")]
@@ -460,7 +450,6 @@ impl AsyncNetworkStream {
InnerAsyncNetworkStream::AsyncStd1NativeTls(_) => true, InnerAsyncNetworkStream::AsyncStd1NativeTls(_) => true,
#[cfg(feature = "async-std1-rustls-tls")] #[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(_) => true, InnerAsyncNetworkStream::AsyncStd1RustlsTls(_) => true,
InnerAsyncNetworkStream::None => false,
} }
} }
@@ -486,8 +475,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,9 +497,7 @@ impl AsyncNetworkStream {
.unwrap() .unwrap()
.first() .first()
.unwrap() .unwrap()
.clone() .to_vec()),
.0),
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
} }
} }
} }
@@ -522,9 +508,9 @@ impl FuturesAsyncRead for AsyncNetworkStream {
cx: &mut Context<'_>, cx: &mut Context<'_>,
buf: &mut [u8], buf: &mut [u8],
) -> Poll<IoResult<usize>> { ) -> Poll<IoResult<usize>> {
match self.inner { match &mut self.inner {
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => { InnerAsyncNetworkStream::Tokio1Tcp(s) => {
let mut b = Tokio1ReadBuf::new(buf); let mut b = Tokio1ReadBuf::new(buf);
match Pin::new(s).poll_read(cx, &mut b) { match Pin::new(s).poll_read(cx, &mut b) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())), Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
@@ -533,7 +519,7 @@ impl FuturesAsyncRead for AsyncNetworkStream {
} }
} }
#[cfg(feature = "tokio1-native-tls")] #[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => { InnerAsyncNetworkStream::Tokio1NativeTls(s) => {
let mut b = Tokio1ReadBuf::new(buf); let mut b = Tokio1ReadBuf::new(buf);
match Pin::new(s).poll_read(cx, &mut b) { match Pin::new(s).poll_read(cx, &mut b) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())), Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
@@ -542,7 +528,7 @@ impl FuturesAsyncRead for AsyncNetworkStream {
} }
} }
#[cfg(feature = "tokio1-rustls-tls")] #[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => { InnerAsyncNetworkStream::Tokio1RustlsTls(s) => {
let mut b = Tokio1ReadBuf::new(buf); let mut b = Tokio1ReadBuf::new(buf);
match Pin::new(s).poll_read(cx, &mut b) { match Pin::new(s).poll_read(cx, &mut b) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())), Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
@@ -551,7 +537,7 @@ impl FuturesAsyncRead for AsyncNetworkStream {
} }
} }
#[cfg(feature = "tokio1-boring-tls")] #[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(ref mut s) => { InnerAsyncNetworkStream::Tokio1BoringTls(s) => {
let mut b = Tokio1ReadBuf::new(buf); let mut b = Tokio1ReadBuf::new(buf);
match Pin::new(s).poll_read(cx, &mut b) { match Pin::new(s).poll_read(cx, &mut b) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())), Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
@@ -560,19 +546,11 @@ impl FuturesAsyncRead for AsyncNetworkStream {
} }
} }
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_read(cx, buf), InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "async-std1-native-tls")] #[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => { InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => Pin::new(s).poll_read(cx, buf),
Pin::new(s).poll_read(cx, buf)
}
#[cfg(feature = "async-std1-rustls-tls")] #[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => { InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_read(cx, buf),
Pin::new(s).poll_read(cx, buf)
}
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(0))
}
} }
} }
} }
@@ -583,75 +561,61 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
cx: &mut Context<'_>, cx: &mut Context<'_>,
buf: &[u8], buf: &[u8],
) -> Poll<IoResult<usize>> { ) -> Poll<IoResult<usize>> {
match self.inner { match &mut self.inner {
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf), InnerAsyncNetworkStream::Tokio1Tcp(s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio1-native-tls")] #[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf), InnerAsyncNetworkStream::Tokio1NativeTls(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(s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio1-boring-tls")] #[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(ref mut s) => Pin::new(s).poll_write(cx, buf), InnerAsyncNetworkStream::Tokio1BoringTls(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(s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "async-std1-native-tls")] #[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => { InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => Pin::new(s).poll_write(cx, buf),
Pin::new(s).poll_write(cx, buf)
}
#[cfg(feature = "async-std1-rustls-tls")] #[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => { InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_write(cx, buf),
Pin::new(s).poll_write(cx, buf)
}
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(0))
}
} }
} }
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> { fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
match self.inner { match &mut self.inner {
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => Pin::new(s).poll_flush(cx), InnerAsyncNetworkStream::Tokio1Tcp(s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio1-native-tls")] #[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_flush(cx), InnerAsyncNetworkStream::Tokio1NativeTls(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(s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio1-boring-tls")] #[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(ref mut s) => Pin::new(s).poll_flush(cx), InnerAsyncNetworkStream::Tokio1BoringTls(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(s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "async-std1-native-tls")] #[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => Pin::new(s).poll_flush(cx), InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "async-std1-rustls-tls")] #[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx), InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_flush(cx),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(()))
}
} }
} }
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> { fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
match self.inner { self.state = ConnectionState::Closed;
match &mut self.inner {
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => Pin::new(s).poll_shutdown(cx), InnerAsyncNetworkStream::Tokio1Tcp(s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio1-native-tls")] #[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_shutdown(cx), InnerAsyncNetworkStream::Tokio1NativeTls(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(s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio1-boring-tls")] #[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(ref mut s) => Pin::new(s).poll_shutdown(cx), InnerAsyncNetworkStream::Tokio1BoringTls(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(s) => Pin::new(s).poll_close(cx),
#[cfg(feature = "async-std1-native-tls")] #[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => Pin::new(s).poll_close(cx), InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => Pin::new(s).poll_close(cx),
#[cfg(feature = "async-std1-rustls-tls")] #[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => Pin::new(s).poll_close(cx), InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_close(cx),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(()))
}
} }
} }
} }

View File

@@ -7,7 +7,7 @@ use std::{
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
use super::escape_crlf; use super::escape_crlf;
use super::{ClientCodec, NetworkStream, TlsParameters}; use super::{ClientCodec, ConnectionState, NetworkStream, TlsParameters};
use crate::{ use crate::{
address::Envelope, address::Envelope,
transport::smtp::{ transport::smtp::{
@@ -20,25 +20,11 @@ use crate::{
}, },
}; };
macro_rules! try_smtp (
($err: expr, $client: ident) => ({
match $err {
Ok(val) => val,
Err(err) => {
$client.abort();
return Err(From::from(err))
},
}
})
);
/// Structure that implements the SMTP client /// Structure that implements the SMTP client
pub struct SmtpConnection { pub struct SmtpConnection {
/// TCP stream between client and server /// TCP stream between client and server
/// Value is None before connection /// Value is None before connection
stream: BufReader<NetworkStream>, stream: BufReader<NetworkStream>,
/// Panic state
panic: bool,
/// Information about the server /// Information about the server
server_info: ServerInfo, server_info: ServerInfo,
} }
@@ -65,7 +51,6 @@ impl SmtpConnection {
let stream = BufReader::new(stream); let stream = BufReader::new(stream);
let mut conn = SmtpConnection { let mut conn = SmtpConnection {
stream, stream,
panic: false,
server_info: ServerInfo::default(), server_info: ServerInfo::default(),
}; };
conn.set_timeout(timeout).map_err(error::network)?; conn.set_timeout(timeout).map_err(error::network)?;
@@ -110,26 +95,25 @@ impl SmtpConnection {
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime)); mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
} }
try_smtp!( self.command(Mail::new(envelope.from().cloned(), mail_options))?;
self.command(Mail::new(envelope.from().cloned(), mail_options)),
self
);
// Recipient // Recipient
for to_address in envelope.to() { for to_address in envelope.to() {
try_smtp!(self.command(Rcpt::new(to_address.clone(), vec![])), self); self.command(Rcpt::new(to_address.clone(), vec![]))?;
} }
// Data // Data
try_smtp!(self.command(Data), self); self.command(Data)?;
// Message content // Message content
let result = try_smtp!(self.message(email), self); self.message(email)
Ok(result)
} }
pub fn has_broken(&self) -> bool { pub fn has_broken(&self) -> bool {
self.panic match self.stream.get_ref().state() {
ConnectionState::Ok => false,
ConnectionState::Broken | ConnectionState::Closed => true,
}
} }
pub fn can_starttls(&self) -> bool { pub fn can_starttls(&self) -> bool {
@@ -138,20 +122,22 @@ impl SmtpConnection {
#[allow(unused_variables)] #[allow(unused_variables)]
pub fn starttls( pub fn starttls(
&mut self, mut self,
tls_parameters: &TlsParameters, tls_parameters: &TlsParameters,
hello_name: &ClientId, hello_name: &ClientId,
) -> Result<(), Error> { ) -> Result<Self, 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", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
{ {
try_smtp!(self.command(Starttls), self); self.command(Starttls)?;
self.stream.get_mut().upgrade_tls(tls_parameters)?; let stream = self.stream.into_inner();
let stream = stream.upgrade_tls(tls_parameters)?;
self.stream = BufReader::new(stream);
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("connection encrypted"); tracing::debug!("connection encrypted");
// Send EHLO again // Send EHLO again
try_smtp!(self.ehlo(hello_name), self); self.ehlo(hello_name)?;
Ok(()) Ok(self)
} }
#[cfg(not(any( #[cfg(not(any(
feature = "native-tls", feature = "native-tls",
@@ -168,22 +154,24 @@ impl SmtpConnection {
/// Send EHLO and update server info /// Send EHLO and update server info
fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> { fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> {
let ehlo_response = try_smtp!(self.command(Ehlo::new(hello_name.clone())), self); let ehlo_response = self.command(Ehlo::new(hello_name.clone()))?;
self.server_info = try_smtp!(ServerInfo::from_response(&ehlo_response), self); self.server_info = ServerInfo::from_response(&ehlo_response)?;
Ok(()) Ok(())
} }
pub fn quit(&mut self) -> Result<Response, Error> { pub fn quit(&mut self) -> Result<Response, Error> {
Ok(try_smtp!(self.command(Quit), self)) self.command(Quit)
} }
pub fn abort(&mut self) { pub fn abort(&mut self) {
// Only try to quit if we are not already broken match self.stream.get_ref().state() {
if !self.panic { ConnectionState::Ok | ConnectionState::Broken => {
self.panic = true; let _ = self.command(Quit);
let _ = self.command(Quit); let _ = self.stream.get_mut().shutdown(std::net::Shutdown::Both);
self.stream.get_mut().set_state(ConnectionState::Closed);
}
ConnectionState::Closed => {}
} }
let _ = self.stream.get_mut().shutdown(std::net::Shutdown::Both);
} }
/// Sets the underlying stream /// Sets the underlying stream
@@ -224,14 +212,11 @@ impl SmtpConnection {
while challenges > 0 && response.has_code(334) { while challenges > 0 && response.has_code(334) {
challenges -= 1; challenges -= 1;
response = try_smtp!( response = self.command(Auth::new_from_response(
self.command(Auth::new_from_response( mechanism,
mechanism, credentials.clone(),
credentials.clone(), &response,
&response, )?)?;
)?),
self
);
} }
if challenges == 0 { if challenges == 0 {
@@ -260,12 +245,17 @@ impl SmtpConnection {
/// Writes a string to the server /// Writes a string to the server
fn write(&mut self, string: &[u8]) -> Result<(), Error> { fn write(&mut self, string: &[u8]) -> Result<(), Error> {
self.stream.get_ref().state().verify()?;
self.stream.get_mut().set_state(ConnectionState::Broken);
self.stream self.stream
.get_mut() .get_mut()
.write_all(string) .write_all(string)
.map_err(error::network)?; .map_err(error::network)?;
self.stream.get_mut().flush().map_err(error::network)?; self.stream.get_mut().flush().map_err(error::network)?;
self.stream.get_mut().set_state(ConnectionState::Ok);
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string))); tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
Ok(()) Ok(())
@@ -273,6 +263,9 @@ impl SmtpConnection {
/// Gets the SMTP response /// Gets the SMTP response
pub fn read_response(&mut self) -> Result<Response, Error> { pub fn read_response(&mut self) -> Result<Response, Error> {
self.stream.get_ref().state().verify()?;
self.stream.get_mut().set_state(ConnectionState::Broken);
let mut buffer = String::with_capacity(100); let mut buffer = String::with_capacity(100);
while self.stream.read_line(&mut buffer).map_err(error::network)? > 0 { while self.stream.read_line(&mut buffer).map_err(error::network)? > 0 {
@@ -280,6 +273,8 @@ impl SmtpConnection {
tracing::debug!("<< {}", escape_crlf(&buffer)); tracing::debug!("<< {}", escape_crlf(&buffer));
match parse_response(&buffer) { match parse_response(&buffer) {
Ok((_remaining, response)) => { Ok((_remaining, response)) => {
self.stream.get_mut().set_state(ConnectionState::Ok);
return if response.is_positive() { return if response.is_positive() {
Ok(response) Ok(response)
} else { } else {

View File

@@ -40,6 +40,7 @@ pub use self::{
connection::SmtpConnection, connection::SmtpConnection,
tls::{Certificate, CertificateStore, Tls, TlsParameters, TlsParametersBuilder}, tls::{Certificate, CertificateStore, Tls, TlsParameters, TlsParametersBuilder},
}; };
use super::{error, Error};
#[cfg(any(feature = "tokio1", feature = "async-std1"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
mod async_connection; mod async_connection;
@@ -49,6 +50,23 @@ mod connection;
mod net; mod net;
mod tls; mod tls;
#[derive(Debug, Copy, Clone)]
enum ConnectionState {
Ok,
Broken,
Closed,
}
impl ConnectionState {
fn verify(&mut self) -> Result<(), Error> {
match self {
Self::Ok => Ok(()),
Self::Broken => Err(error::connection("connection broken")),
Self::Closed => Err(error::connection("connection closed")),
}
}
}
/// The codec used for transparency /// The codec used for transparency
#[derive(Debug)] #[derive(Debug)]
struct ClientCodec { struct ClientCodec {

View File

@@ -2,8 +2,7 @@
use std::sync::Arc; use std::sync::Arc;
use std::{ use std::{
io::{self, Read, Write}, io::{self, Read, Write},
mem, net::{IpAddr, Shutdown, SocketAddr, TcpStream, ToSocketAddrs},
net::{IpAddr, Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs},
time::Duration, time::Duration,
}; };
@@ -12,17 +11,18 @@ 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"))]
use super::InnerTlsParameters; use super::InnerTlsParameters;
use super::TlsParameters; use super::{ConnectionState, TlsParameters};
use crate::transport::smtp::{error, Error}; use crate::transport::smtp::{error, Error};
/// A network stream /// A network stream
pub struct NetworkStream { pub struct NetworkStream {
inner: InnerNetworkStream, inner: InnerNetworkStream,
state: ConnectionState,
} }
/// Represents the different types of underlying network streams /// Represents the different types of underlying network streams
@@ -40,53 +40,49 @@ enum InnerNetworkStream {
RustlsTls(StreamOwned<ClientConnection, TcpStream>), RustlsTls(StreamOwned<ClientConnection, TcpStream>),
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
BoringTls(SslStream<TcpStream>), BoringTls(SslStream<TcpStream>),
/// Can't be built
None,
} }
impl NetworkStream { impl NetworkStream {
fn new(inner: InnerNetworkStream) -> Self { fn new(inner: InnerNetworkStream) -> Self {
if let InnerNetworkStream::None = inner { NetworkStream {
debug_assert!(false, "InnerNetworkStream::None must never be built"); inner,
state: ConnectionState::Ok,
} }
}
NetworkStream { inner } pub(super) fn state(&self) -> ConnectionState {
self.state
}
pub(super) fn set_state(&mut self, state: ConnectionState) {
self.state = state;
} }
/// Returns peer's address /// Returns peer's address
pub fn peer_addr(&self) -> io::Result<SocketAddr> { pub fn peer_addr(&self) -> io::Result<SocketAddr> {
match self.inner { match &self.inner {
InnerNetworkStream::Tcp(ref s) => s.peer_addr(), InnerNetworkStream::Tcp(s) => s.peer_addr(),
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref s) => s.get_ref().peer_addr(), InnerNetworkStream::NativeTls(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(s) => s.get_ref().peer_addr(),
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref s) => s.get_ref().peer_addr(), InnerNetworkStream::BoringTls(s) => s.get_ref().peer_addr(),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::new(127, 0, 0, 1),
80,
)))
}
} }
} }
/// Shutdowns the connection /// Shutdowns the connection
pub fn shutdown(&self, how: Shutdown) -> io::Result<()> { pub fn shutdown(&mut self, how: Shutdown) -> io::Result<()> {
match self.inner { self.state = ConnectionState::Closed;
InnerNetworkStream::Tcp(ref s) => s.shutdown(how),
match &self.inner {
InnerNetworkStream::Tcp(s) => s.shutdown(how),
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref s) => s.get_ref().shutdown(how), InnerNetworkStream::NativeTls(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(s) => s.get_ref().shutdown(how),
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref s) => s.get_ref().shutdown(how), InnerNetworkStream::BoringTls(s) => s.get_ref().shutdown(how),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(())
}
} }
} }
@@ -139,13 +135,13 @@ impl NetworkStream {
let tcp_stream = try_connect(server, timeout, local_addr)?; let tcp_stream = try_connect(server, timeout, local_addr)?;
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 = stream.upgrade_tls(tls_parameters)?;
} }
Ok(stream) Ok(stream)
} }
pub fn upgrade_tls(&mut self, tls_parameters: &TlsParameters) -> Result<(), Error> { pub fn upgrade_tls(self, tls_parameters: &TlsParameters) -> Result<Self, Error> {
match &self.inner { match self.inner {
#[cfg(not(any( #[cfg(not(any(
feature = "native-tls", feature = "native-tls",
feature = "rustls-tls", feature = "rustls-tls",
@@ -157,18 +153,14 @@ impl NetworkStream {
} }
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
InnerNetworkStream::Tcp(_) => { InnerNetworkStream::Tcp(tcp_stream) => {
// get owned TcpStream let inner = Self::upgrade_tls_impl(tcp_stream, tls_parameters)?;
let tcp_stream = mem::replace(&mut self.inner, InnerNetworkStream::None); Ok(Self {
let tcp_stream = match tcp_stream { inner,
InnerNetworkStream::Tcp(tcp_stream) => tcp_stream, state: ConnectionState::Ok,
_ => unreachable!(), })
};
self.inner = Self::upgrade_tls_impl(tcp_stream, tls_parameters)?;
Ok(())
} }
_ => Ok(()), _ => Ok(self),
} }
} }
@@ -189,7 +181,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)
@@ -208,7 +200,7 @@ impl NetworkStream {
} }
pub fn is_encrypted(&self) -> bool { pub fn is_encrypted(&self) -> bool {
match self.inner { match &self.inner {
InnerNetworkStream::Tcp(_) => false, InnerNetworkStream::Tcp(_) => false,
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(_) => true, InnerNetworkStream::NativeTls(_) => true,
@@ -216,10 +208,6 @@ impl NetworkStream {
InnerNetworkStream::RustlsTls(_) => true, InnerNetworkStream::RustlsTls(_) => true,
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(_) => true, InnerNetworkStream::BoringTls(_) => true,
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
false
}
} }
} }
@@ -241,8 +229,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()
@@ -250,105 +237,72 @@ impl NetworkStream {
.unwrap() .unwrap()
.to_der() .to_der()
.map_err(error::tls)?), .map_err(error::tls)?),
InnerNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
} }
} }
pub fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> { pub fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
match self.inner { match &mut self.inner {
InnerNetworkStream::Tcp(ref mut stream) => stream.set_read_timeout(duration), InnerNetworkStream::Tcp(stream) => stream.set_read_timeout(duration),
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref mut stream) => { InnerNetworkStream::NativeTls(stream) => stream.get_ref().set_read_timeout(duration),
stream.get_ref().set_read_timeout(duration)
}
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut stream) => { InnerNetworkStream::RustlsTls(stream) => stream.get_ref().set_read_timeout(duration),
stream.get_ref().set_read_timeout(duration)
}
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref mut stream) => { InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_read_timeout(duration),
stream.get_ref().set_read_timeout(duration)
}
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(())
}
} }
} }
/// Set write timeout for IO calls /// Set write timeout for IO calls
pub fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> { pub fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
match self.inner { match &mut self.inner {
InnerNetworkStream::Tcp(ref mut stream) => stream.set_write_timeout(duration), InnerNetworkStream::Tcp(stream) => stream.set_write_timeout(duration),
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref mut stream) => { InnerNetworkStream::NativeTls(stream) => stream.get_ref().set_write_timeout(duration),
stream.get_ref().set_write_timeout(duration)
}
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut stream) => { InnerNetworkStream::RustlsTls(stream) => stream.get_ref().set_write_timeout(duration),
stream.get_ref().set_write_timeout(duration)
}
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref mut stream) => { InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_write_timeout(duration),
stream.get_ref().set_write_timeout(duration)
}
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(())
}
} }
} }
} }
impl Read for NetworkStream { impl Read for NetworkStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self.inner { match &mut self.inner {
InnerNetworkStream::Tcp(ref mut s) => s.read(buf), InnerNetworkStream::Tcp(s) => s.read(buf),
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref mut s) => s.read(buf), InnerNetworkStream::NativeTls(s) => s.read(buf),
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.read(buf), InnerNetworkStream::RustlsTls(s) => s.read(buf),
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref mut s) => s.read(buf), InnerNetworkStream::BoringTls(s) => s.read(buf),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(0)
}
} }
} }
} }
impl Write for NetworkStream { impl Write for NetworkStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> { fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self.inner { match &mut self.inner {
InnerNetworkStream::Tcp(ref mut s) => s.write(buf), InnerNetworkStream::Tcp(s) => s.write(buf),
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref mut s) => s.write(buf), InnerNetworkStream::NativeTls(s) => s.write(buf),
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.write(buf), InnerNetworkStream::RustlsTls(s) => s.write(buf),
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref mut s) => s.write(buf), InnerNetworkStream::BoringTls(s) => s.write(buf),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(0)
}
} }
} }
fn flush(&mut self) -> io::Result<()> { fn flush(&mut self) -> io::Result<()> {
match self.inner { match &mut self.inner {
InnerNetworkStream::Tcp(ref mut s) => s.flush(), InnerNetworkStream::Tcp(s) => s.flush(),
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref mut s) => s.flush(), InnerNetworkStream::NativeTls(s) => s.flush(),
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.flush(), InnerNetworkStream::RustlsTls(s) => s.flush(),
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref mut s) => s.flush(), InnerNetworkStream::BoringTls(s) => s.flush(),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(())
}
} }
} }
} }

View File

@@ -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()
}
} }

View File

@@ -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);
} }

View File

@@ -119,7 +119,7 @@ impl fmt::Debug for Error {
builder.field("kind", &self.inner.kind); builder.field("kind", &self.inner.kind);
if let Some(ref source) = self.inner.source { if let Some(source) = &self.inner.source {
builder.field("source", source); builder.field("source", source);
} }
@@ -129,22 +129,22 @@ impl fmt::Debug for Error {
impl fmt::Display for Error { impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.inner.kind { match &self.inner.kind {
Kind::Response => f.write_str("response error")?, Kind::Response => f.write_str("response 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", feature = "boring-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(code) => {
write!(f, "transient error ({code})")?; write!(f, "transient error ({code})")?;
} }
Kind::Permanent(ref code) => { Kind::Permanent(code) => {
write!(f, "permanent error ({code})")?; write!(f, "permanent error ({code})")?;
} }
}; };
if let Some(ref e) = self.inner.source { if let Some(e) = &self.inner.source {
write!(f, ": {e}")?; write!(f, ": {e}")?;
} }

View File

@@ -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::{
@@ -53,10 +52,10 @@ impl Default for ClientId {
impl Display for ClientId { impl Display for ClientId {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self { match self {
Self::Domain(ref value) => f.write_str(value), Self::Domain(value) => f.write_str(value),
Self::Ipv4(ref value) => write!(f, "[{value}]"), Self::Ipv4(value) => write!(f, "[{value}]"),
Self::Ipv6(ref value) => write!(f, "[IPv6:{value}]"), Self::Ipv6(value) => write!(f, "[IPv6:{value}]"),
} }
} }
} }
@@ -93,11 +92,11 @@ pub enum Extension {
impl Display for Extension { impl Display for Extension {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self { match self {
Extension::EightBitMime => f.write_str("8BITMIME"), Extension::EightBitMime => f.write_str("8BITMIME"),
Extension::SmtpUtfEight => f.write_str("SMTPUTF8"), Extension::SmtpUtfEight => f.write_str("SMTPUTF8"),
Extension::StartTls => f.write_str("STARTTLS"), Extension::StartTls => f.write_str("STARTTLS"),
Extension::Authentication(ref mechanism) => write!(f, "AUTH {mechanism}"), Extension::Authentication(mechanism) => write!(f, "AUTH {mechanism}"),
} }
} }
} }
@@ -227,16 +226,16 @@ pub enum MailParameter {
impl Display for MailParameter { impl Display for MailParameter {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self { match self {
MailParameter::Body(ref value) => write!(f, "BODY={value}"), MailParameter::Body(value) => write!(f, "BODY={value}"),
MailParameter::Size(size) => write!(f, "SIZE={size}"), MailParameter::Size(size) => write!(f, "SIZE={size}"),
MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"), MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"),
MailParameter::Other { MailParameter::Other {
ref keyword, keyword,
value: Some(ref value), value: Some(value),
} => write!(f, "{}={}", keyword, XText(value)), } => write!(f, "{}={}", keyword, XText(value)),
MailParameter::Other { MailParameter::Other {
ref keyword, keyword,
value: None, value: None,
} => f.write_str(keyword), } => f.write_str(keyword),
} }
@@ -277,13 +276,13 @@ pub enum RcptParameter {
impl Display for RcptParameter { impl Display for RcptParameter {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self { match &self {
RcptParameter::Other { RcptParameter::Other {
ref keyword, keyword,
value: Some(ref value), value: Some(value),
} => write!(f, "{keyword}={}", XText(value)), } => write!(f, "{keyword}={}", XText(value)),
RcptParameter::Other { RcptParameter::Other {
ref keyword, keyword,
value: None, value: None,
} => f.write_str(keyword), } => f.write_str(keyword),
} }

View File

@@ -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(),
}); });
{ {

View File

@@ -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";

View File

@@ -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
/// ///
@@ -309,9 +317,9 @@ impl SmtpClient {
/// Handles encryption and authentication /// Handles encryption and authentication
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", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters), Tls::Wrapper(tls_parameters) => Some(tls_parameters),
_ => None, _ => None,
}; };
@@ -325,14 +333,14 @@ impl SmtpClient {
)?; )?;
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-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(tls_parameters) => {
if conn.can_starttls() { if conn.can_starttls() {
conn.starttls(tls_parameters, &self.info.hello_name)?; conn = conn.starttls(tls_parameters, &self.info.hello_name)?;
} }
} }
Tls::Required(ref tls_parameters) => { Tls::Required(tls_parameters) => {
conn.starttls(tls_parameters, &self.info.hello_name)?; conn = conn.starttls(tls_parameters, &self.info.hello_name)?;
} }
_ => (), _ => (),
} }
@@ -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();