Compare commits

...

24 Commits

Author SHA1 Message Date
Paolo Barbolini
c7d1f35676 Prepare 0.11.6 (#955) 2024-03-28 16:19:10 +01:00
Paolo Barbolini
eebea56f16 Upgrade email-encoding to v0.3 (#952) 2024-03-28 15:22:55 +01:00
Paolo Barbolini
851d6ae164 Bump license year (#954) 2024-03-28 14:50:53 +01:00
Paolo Barbolini
6f38e6b9a9 Fix latest clippy warnings (#953) 2024-03-28 05:12:13 +01:00
Paolo Barbolini
c40af78809 Prepare 0.11.5 (#951) 2024-03-25 17:59:47 +01:00
Paolo Barbolini
6d2e0d5046 Bump rustls to v0.23 (#950) 2024-03-22 20:09:27 +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
26 changed files with 2919 additions and 135 deletions

1
.gitignore vendored
View File

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

View File

@@ -1,3 +1,70 @@
<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)
@@ -214,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 to `nom` 6
* Replace `log` with `tracing`
* Move CI to Github Actions
* Move CI to GitHub Actions
* Use criterion for benchmarks
<a name="v0.9.2"></a>

2661
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
[package]
name = "lettre"
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
version = "0.11.2"
version = "0.11.6"
description = "Email client"
readme = "README.md"
homepage = "https://lettre.rs"
@@ -21,7 +21,6 @@ maintenance = { status = "actively-developed" }
[dependencies]
chumsky = "0.9"
idna = "0.5"
once_cell = { version = "1", optional = true }
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
# builder
@@ -29,8 +28,8 @@ httpdate = { version = "1", optional = true }
mime = { version = "0.3.4", optional = true }
fastrand = { version = "2.0", optional = true }
quoted_printable = { version = "0.5", optional = true }
base64 = { version = "0.21", optional = true }
email-encoding = { version = "0.2", optional = true }
base64 = { version = "0.22", optional = true }
email-encoding = { version = "0.3", optional = true }
# file transport
uuid = { version = "1", features = ["v4"], optional = true }
@@ -42,13 +41,14 @@ nom = { version = "7", optional = true }
hostname = { version = "0.3", optional = true } # feature
socket2 = { version = "0.5.1", optional = true }
url = { version = "2.4", optional = true }
percent-encoding = { version = "2.3", optional = true }
## tls
native-tls = { version = "0.2.5", optional = true } # feature
rustls = { version = "0.21", features = ["dangerous_configuration"], optional = true }
rustls-pemfile = { version = "1", optional = true }
rustls-native-certs = { version = "0.6.2", optional = true }
webpki-roots = { version = "0.25", optional = true }
rustls = { version = "0.23.3", default-features = false, features = ["ring", "logging", "std", "tls12"], optional = true }
rustls-pemfile = { version = "2", optional = true }
rustls-native-certs = { version = "0.7", optional = true }
webpki-roots = { version = "0.26", optional = true }
boring = { version = "4", optional = true }
# async
@@ -59,12 +59,12 @@ async-trait = { version = "0.1", optional = true }
## async-std
async-std = { version = "1.8", 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
tokio1_crate = { package = "tokio", version = "1", 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 = "4", optional = true }
## dkim
@@ -85,7 +85,7 @@ walkdir = "2"
tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] }
async-std = { version = "1.8", features = ["attributes"] }
serde_json = "1"
maud = "0.25"
maud = "0.26"
[[bench]]
harness = false
@@ -104,7 +104,7 @@ mime03 = ["dep:mime"]
file-transport = ["dep:uuid", "tokio1_crate?/fs", "tokio1_crate?/io-util"]
file-transport-envelope = ["serde", "dep:serde_json", "file-transport"]
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"]

View File

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

View File

@@ -28,8 +28,8 @@
</div>
<div align="center">
<a href="https://deps.rs/crate/lettre/0.11.2">
<img src="https://deps.rs/crate/lettre/0.11.2/status.svg"
<a href="https://deps.rs/crate/lettre/0.11.6">
<img src="https://deps.rs/crate/lettre/0.11.6/status.svg"
alt="dependency status" />
</a>
</div>

View File

@@ -109,7 +109,7 @@
//! [mime 0.3]: https://docs.rs/mime/0.3
//! [DKIM]: https://datatracker.ietf.org/doc/html/rfc6376
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.2")]
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.6")]
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
#![forbid(unsafe_code)]

View File

@@ -2,7 +2,6 @@ use std::{
borrow::Cow,
error::Error as StdError,
fmt::{self, Display},
iter::IntoIterator,
time::SystemTime,
};

View File

@@ -1,6 +1,6 @@
use std::fmt::Write;
use email_encoding::headers::EmailWriter;
use email_encoding::headers::writer::EmailWriter;
use super::{Header, HeaderName, HeaderValue};
use crate::BoxError;
@@ -38,10 +38,10 @@ impl ContentDisposition {
let mut encoded_value = String::new();
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_char(';').expect("writing `;` returned an error");
w.optional_breakpoint();
w.space();
email_encoding::headers::rfc2231::encode("filename", file_name, &mut w)
.expect("some Write implementation returned an error");

View File

@@ -1,4 +1,4 @@
use email_encoding::headers::EmailWriter;
use email_encoding::headers::writer::EmailWriter;
use super::{Header, HeaderName, HeaderValue};
use crate::{
@@ -31,7 +31,7 @@ macro_rules! mailbox_header {
let mut encoded_value = String::new();
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");
}
@@ -81,7 +81,7 @@ macro_rules! mailboxes_header {
let mut encoded_value = String::new();
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");
}

View File

@@ -7,7 +7,7 @@ use std::{
ops::Deref,
};
use email_encoding::headers::EmailWriter;
use email_encoding::headers::writer::EmailWriter;
pub use self::{
content::*,
@@ -348,7 +348,7 @@ impl<'a> HeaderValueEncoder<'a> {
fn new(name: &str, writer: &'a mut dyn Write) -> Self {
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 {
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(),
));
// 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!(
headers.to_string(),
concat!(
"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",
" =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>,\r\n",
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, =?utf-8?b?U2U=?=\r\n",
" =?utf-8?b?w6FuIMOTIFJ1ZGHDrQ==?= <sean@example.com>\r\n",
)
);
}
@@ -687,9 +684,6 @@ mod tests {
"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!(
headers.to_string(),
concat!(
@@ -699,8 +693,8 @@ mod tests {
"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",
" =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>,\r\n",
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, =?utf-8?b?U2U=?=\r\n",
" =?utf-8?b?w6FuIMOTIFJ1ZGHDrQ==?= <sean@example.com>\r\n",
"From: Someone <somewhere@example.com>\r\n",
"Content-Transfer-Encoding: quoted-printable\r\n",
)

View File

@@ -6,7 +6,7 @@ use std::{
};
use chumsky::prelude::*;
use email_encoding::headers::EmailWriter;
use email_encoding::headers::writer::EmailWriter;
use super::parsers;
use crate::address::{Address, AddressError};
@@ -72,7 +72,7 @@ impl Mailbox {
pub(crate) fn encode(&self, w: &mut EmailWriter<'_>) -> FmtResult {
if let Some(name) = &self.name {
email_encoding::headers::quoted_string::encode(name, w)?;
w.optional_breakpoint();
w.space();
w.write_char('<')?;
}
@@ -261,7 +261,7 @@ impl Mailboxes {
for mailbox in self.iter() {
if !mem::take(&mut first) {
w.write_char(',')?;
w.optional_breakpoint();
w.space();
}
mailbox.encode(w)?;
@@ -444,8 +444,6 @@ fn write_quoted_string_char(f: &mut Formatter<'_>, c: char) -> FmtResult {
#[cfg(test)]
mod test {
use std::convert::TryInto;
use pretty_assertions::assert_eq;
use super::Mailbox;

View File

@@ -17,6 +17,16 @@ pub(super) enum Part {
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 {
fn format(&self, out: &mut Vec<u8>) {
match self {
@@ -132,6 +142,12 @@ impl SinglePart {
self.format(&mut 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 {
@@ -139,8 +155,7 @@ impl EmailFormat for SinglePart {
write!(out, "{}", self.headers)
.expect("A Write implementation panicked while formatting headers");
out.extend_from_slice(b"\r\n");
out.extend_from_slice(&self.body);
out.extend_from_slice(b"\r\n");
self.format_body(out);
}
}
@@ -373,14 +388,9 @@ impl MultiPart {
self.format(&mut 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();
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)]
mod test {
use pretty_assertions::assert_eq;
use super::*;
use crate::message::header;
#[test]
fn single_part_binary() {

View File

@@ -525,7 +525,7 @@ impl Message {
pub(crate) fn body_raw(&self) -> Vec<u8> {
let mut out = Vec::new();
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),
};
out.extend_from_slice(b"\r\n");

View File

@@ -157,7 +157,7 @@ mod error;
type Id = String;
/// 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(docsrs, doc(cfg(feature = "file-transport")))]
pub struct FileTransport {
@@ -167,7 +167,7 @@ pub struct FileTransport {
}
/// 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(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
#[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 |
//! | [`sendmail`] | Sendmail | [`SendmailTransport`] | [`AsyncSendmailTransport`] | Uses the `sendmail` command to send emails |
//! | [`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
//!
@@ -97,6 +97,7 @@
//! [`FileTransport`]: crate::FileTransport
//! [`AsyncFileTransport`]: crate::AsyncFileTransport
//! [`StubTransport`]: crate::transport::stub::StubTransport
//! [`AsyncStubTransport`]: crate::transport::stub::AsyncStubTransport
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
use async_trait::async_trait;

View File

@@ -98,11 +98,13 @@ impl Mechanism {
let decoded_challenge = 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());
}
if ["Password", "Password:"].contains(&decoded_challenge) {
if ["Password", "Password:", "Password\0"].contains(&decoded_challenge) {
return Ok(credentials.secret.clone());
}

View File

@@ -65,7 +65,7 @@ impl AsyncSmtpConnection {
/// If `tls_parameters` is `Some`, then the connection will use Implicit TLS (sometimes
/// 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`].
///
/// Sends EHLO and parses server information

View File

@@ -16,6 +16,8 @@ use futures_io::{
};
#[cfg(feature = "async-std1-rustls-tls")]
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")]
use tokio1_boring::SslStream as Tokio1SslStream;
#[cfg(feature = "tokio1")]
@@ -350,7 +352,6 @@ impl AsyncNetworkStream {
#[cfg(feature = "tokio1-rustls-tls")]
return {
use rustls::ServerName;
use tokio1_rustls::TlsConnector;
let domain = ServerName::try_from(domain.as_str())
@@ -358,7 +359,7 @@ impl AsyncNetworkStream {
let connector = TlsConnector::from(config);
let stream = connector
.connect(domain, tcp_stream)
.connect(domain.to_owned(), tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::Tokio1RustlsTls(stream))
@@ -424,14 +425,13 @@ impl AsyncNetworkStream {
#[cfg(feature = "async-std1-rustls-tls")]
return {
use futures_rustls::TlsConnector;
use rustls::ServerName;
let domain = ServerName::try_from(domain.as_str())
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
let connector = TlsConnector::from(config);
let stream = connector
.connect(domain, tcp_stream)
.connect(domain.to_owned(), tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream))
@@ -486,8 +486,7 @@ impl AsyncNetworkStream {
.unwrap()
.first()
.unwrap()
.clone()
.0),
.to_vec()),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => Ok(stream
.ssl()
@@ -509,8 +508,7 @@ impl AsyncNetworkStream {
.unwrap()
.first()
.unwrap()
.clone()
.0),
.to_vec()),
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
}
}

View File

@@ -12,7 +12,7 @@ use boring::ssl::SslStream;
#[cfg(feature = "native-tls")]
use native_tls::TlsStream;
#[cfg(feature = "rustls-tls")]
use rustls::{ClientConnection, ServerName, StreamOwned};
use rustls::{pki_types::ServerName, ClientConnection, StreamOwned};
use socket2::{Domain, Protocol, Type};
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
@@ -189,7 +189,7 @@ impl NetworkStream {
InnerTlsParameters::RustlsTls(connector) => {
let domain = ServerName::try_from(tls_parameters.domain())
.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)?;
let stream = StreamOwned::new(connection, tcp_stream);
InnerNetworkStream::RustlsTls(stream)
@@ -241,8 +241,7 @@ impl NetworkStream {
.unwrap()
.first()
.unwrap()
.clone()
.0),
.to_vec()),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(stream) => Ok(stream
.ssl()

View File

@@ -1,6 +1,6 @@
use std::fmt::{self, Debug};
#[cfg(feature = "rustls-tls")]
use std::{sync::Arc, time::SystemTime};
use std::{io, sync::Arc};
#[cfg(feature = "boring-tls")]
use boring::{
@@ -11,8 +11,10 @@ use boring::{
use native_tls::{Protocol, TlsConnector};
#[cfg(feature = "rustls-tls")]
use rustls::{
client::{ServerCertVerified, ServerCertVerifier, WebPkiVerifier},
ClientConfig, Error as TlsError, RootCertStore, ServerName,
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
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"))]
@@ -337,8 +339,6 @@ impl TlsParametersBuilder {
#[cfg(feature = "rustls-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
pub fn build_rustls(self) -> Result<TlsParameters, Error> {
let tls = ClientConfig::builder();
let just_version3 = &[&rustls::version::TLS13];
let supported_versions = match self.min_tls_version {
TlsVersion::Tlsv10 => {
@@ -351,50 +351,28 @@ impl TlsParametersBuilder {
TlsVersion::Tlsv13 => just_version3,
};
let tls = tls
.with_safe_default_cipher_suites()
.with_safe_default_kx_groups()
.with_protocol_versions(supported_versions)
.map_err(error::tls)?;
let tls = ClientConfig::builder_with_protocol_versions(supported_versions);
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 {
let mut root_cert_store = RootCertStore::empty();
#[cfg(feature = "rustls-native-certs")]
fn load_native_roots(store: &mut RootCertStore) -> Result<(), Error> {
let native_certs = rustls_native_certs::load_native_certs().map_err(error::tls)?;
let mut valid_count = 0;
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;
}
}
}
let (added, ignored) = store.add_parsable_certificates(native_certs);
#[cfg(feature = "tracing")]
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(())
}
#[cfg(feature = "rustls-tls")]
fn load_webpki_roots(store: &mut RootCertStore) {
// TODO: handle this in the rustls 0.22 upgrade
#[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,
)
}));
store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
}
match self.cert_store {
@@ -412,14 +390,11 @@ impl TlsParametersBuilder {
}
for cert in self.root_certs {
for rustls_cert in cert.rustls {
root_cert_store.add(&rustls_cert).map_err(error::tls)?;
root_cert_store.add(rustls_cert).map_err(error::tls)?;
}
}
tls.with_custom_certificate_verifier(Arc::new(WebPkiVerifier::new(
root_cert_store,
None,
)))
tls.with_root_certificates(root_cert_store)
};
let tls = tls.with_no_client_auth();
@@ -493,7 +468,7 @@ pub struct Certificate {
#[cfg(feature = "native-tls")]
native_tls: native_tls::Certificate,
#[cfg(feature = "rustls-tls")]
rustls: Vec<rustls::Certificate>,
rustls: Vec<CertificateDer<'static>>,
#[cfg(feature = "boring-tls")]
boring_tls: boring::x509::X509,
}
@@ -512,7 +487,7 @@ impl Certificate {
#[cfg(feature = "native-tls")]
native_tls: native_tls_cert,
#[cfg(feature = "rustls-tls")]
rustls: vec![rustls::Certificate(der)],
rustls: vec![der.into()],
#[cfg(feature = "boring-tls")]
boring_tls: boring_tls_cert,
})
@@ -532,10 +507,8 @@ impl Certificate {
let mut pem = Cursor::new(pem);
rustls_pemfile::certs(&mut pem)
.collect::<io::Result<Vec<_>>>()
.map_err(|_| error::tls("invalid certificates"))?
.into_iter()
.map(rustls::Certificate)
.collect::<Vec<_>>()
};
Ok(Self {
@@ -556,19 +529,53 @@ impl Debug for Certificate {
}
#[cfg(feature = "rustls-tls")]
#[derive(Debug)]
struct InvalidCertsVerifier;
#[cfg(feature = "rustls-tls")]
impl ServerCertVerifier for InvalidCertsVerifier {
fn verify_server_cert(
&self,
_end_entity: &rustls::Certificate,
_intermediates: &[rustls::Certificate],
_server_name: &ServerName,
_scts: &mut dyn Iterator<Item = &[u8]>,
_end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp_response: &[u8],
_now: SystemTime,
_now: UnixTime,
) -> Result<ServerCertVerified, TlsError> {
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() {
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);
}

View File

@@ -4,7 +4,6 @@ use std::{
collections::HashSet,
fmt::{self, Display, Formatter},
net::{Ipv4Addr, Ipv6Addr},
result::Result,
};
use crate::transport::smtp::{
@@ -292,14 +291,8 @@ impl Display for RcptParameter {
#[cfg(test)]
mod test {
use std::collections::HashSet;
use super::*;
use crate::transport::smtp::{
authentication::Mechanism,
response::{Category, Code, Detail, Response, Severity},
};
use crate::transport::smtp::response::{Category, Code, Detail, Severity};
#[test]
fn test_clientid_fmt() {

View File

@@ -2,7 +2,7 @@ use std::{
fmt::{self, Debug},
mem,
ops::{Deref, DerefMut},
sync::Arc,
sync::{Arc, OnceLock},
time::{Duration, Instant},
};
@@ -10,7 +10,6 @@ use futures_util::{
lock::Mutex,
stream::{self, StreamExt},
};
use once_cell::sync::OnceCell;
use super::{
super::{client::AsyncSmtpConnection, Error},
@@ -22,7 +21,7 @@ pub struct Pool<E: Executor> {
config: PoolConfig,
connections: Mutex<Vec<ParkedConnection>>,
client: AsyncSmtpClient<E>,
handle: OnceCell<E::Handle>,
handle: OnceLock<E::Handle>,
}
struct ParkedConnection {
@@ -41,7 +40,7 @@ impl<E: Executor> Pool<E> {
config,
connections: Mutex::new(Vec::new()),
client,
handle: OnceCell::new(),
handle: OnceLock::new(),
});
{

View File

@@ -5,7 +5,6 @@ use std::{
fmt::{Display, Formatter, Result},
result,
str::FromStr,
string::ToString,
};
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
///
/// The text message is optional, only the code is mandatory
@@ -317,6 +322,17 @@ mod test {
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]
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";

View File

@@ -1,6 +1,6 @@
#[cfg(feature = "pool")]
use std::sync::Arc;
use std::time::Duration;
use std::{fmt::Debug, time::Duration};
#[cfg(feature = "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 {
/// 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_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 =
SmtpTransport::from_url("smtp://username:password@smtp.example.com:587?tls=required")
.unwrap();