Compare commits

..

1 Commits

Author SHA1 Message Date
Paolo Barbolini
18d2d051ed Improve SMTP error handling 2024-10-09 22:14:27 +02:00
40 changed files with 744 additions and 1111 deletions

View File

@@ -75,8 +75,8 @@ jobs:
rust: stable
- name: beta
rust: beta
- name: '1.71'
rust: '1.71'
- name: '1.70'
rust: '1.70'
steps:
- name: Checkout

View File

@@ -1,82 +1,3 @@
<a name="v0.11.13"></a>
### v0.11.13 (2025-02-17)
#### Features
* Add WASM support ([#1037], [#1042])
* Add method to get the TLS verify result with BoringSSL ([#1039])
#### Bug fixes
* Synchronous pool shutdowns being arbitrarily delayed ([#1041])
[#1037]: https://github.com/lettre/lettre/pull/1037
[#1039]: https://github.com/lettre/lettre/pull/1039
[#1041]: https://github.com/lettre/lettre/pull/1041
[#1042]: https://github.com/lettre/lettre/pull/1042
<a name="v0.11.12"></a>
### v0.11.12 (2025-02-02)
#### Misc
* Warn against manually configuring `port` and `tls` on SMTP transport builder ([#1014])
* Document variants of `Tls` enum ([#1015])
* Fix rustdoc warnings ([#1016])
* Add `ContentType::TEXT_PLAIN` to `Message` builder examples ([#1017])
* Document `SmtpTransport` and `AsyncSmtpTransport` ([#1018])
* Fix typo in transport builder `credentials` method ([#1019])
* Document required system dependencies for OpenSSL ([#1030])
* Improve docs for the `transport::smtp` module ([#1031])
* Improve docs for smtp transport builder `from_url` ([#1032])
* Replace `assert!` with `?` on `send` examples ([#1033])
* Warn on more pedantic clippy lints and fix them ([#1035], [#1036])
[#1014]: https://github.com/lettre/lettre/pull/1014
[#1015]: https://github.com/lettre/lettre/pull/1015
[#1016]: https://github.com/lettre/lettre/pull/1016
[#1017]: https://github.com/lettre/lettre/pull/1017
[#1018]: https://github.com/lettre/lettre/pull/1018
[#1019]: https://github.com/lettre/lettre/pull/1019
[#1030]: https://github.com/lettre/lettre/pull/1030
[#1031]: https://github.com/lettre/lettre/pull/1031
[#1032]: https://github.com/lettre/lettre/pull/1032
[#1033]: https://github.com/lettre/lettre/pull/1033
[#1035]: https://github.com/lettre/lettre/pull/1035
[#1036]: https://github.com/lettre/lettre/pull/1036
<a name="v0.11.11"></a>
### v0.11.11 (2024-12-05)
#### Upgrade notes
* MSRV is now 1.71 ([#1008])
#### Bug fixes
* Fix off-by-one error reaching the minimum number of configured pooled connections ([#1012])
#### Misc
* Fix clippy warnings ([#1009])
* Fix `-Zminimal-versions` build ([#1007])
[#1007]: https://github.com/lettre/lettre/pull/1007
[#1008]: https://github.com/lettre/lettre/pull/1008
[#1009]: https://github.com/lettre/lettre/pull/1009
[#1012]: https://github.com/lettre/lettre/pull/1012
<a name="v0.11.10"></a>
### v0.11.10 (2024-10-23)
#### Bug fixes
* Ignore disconnect errors when `pool` feature of SMTP transport is disabled ([#999])
* Use case insensitive comparisons for matching login challenge requests ([#1000])
[#999]: https://github.com/lettre/lettre/pull/999
[#1000]: https://github.com/lettre/lettre/pull/1000
<a name="v0.11.9"></a>
### v0.11.9 (2024-09-13)
@@ -587,6 +508,6 @@ Several breaking changes were made between 0.9 and 0.10, but changes should be s
* multipart support
* add non-consuming methods for Email builders
* `add_header` does not return the builder anymore,
* `add_header` does not return the builder anymore,
for consistency with other methods. Use the `header`
method instead

473
Cargo.lock generated

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.13"
version = "0.11.9"
description = "Email client"
readme = "README.md"
homepage = "https://lettre.rs"
@@ -11,7 +11,7 @@ authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo
categories = ["email", "network-programming"]
keywords = ["email", "smtp", "mailer", "message", "sendmail"]
edition = "2021"
rust-version = "1.71"
rust-version = "1.70"
[badges]
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
@@ -44,7 +44,7 @@ url = { version = "2.4", optional = true }
percent-encoding = { version = "2.3", optional = true }
## tls
native-tls = { version = "0.2.9", optional = true } # feature
native-tls = { version = "0.2.5", optional = true } # feature
rustls = { version = "0.23.5", default-features = false, features = ["ring", "logging", "std", "tls12"], optional = true }
rustls-pemfile = { version = "2", optional = true }
rustls-native-certs = { version = "0.8", optional = true }
@@ -75,9 +75,6 @@ ed25519-dalek = { version = "2", optional = true }
# email formats
email_address = { version = "0.2.1", default-features = false }
# webtime for wasm support
web-time = { version = "1.1.0", optional = true }
[dev-dependencies]
pretty_assertions = "1"
criterion = "0.5"
@@ -125,9 +122,6 @@ tokio1-boring-tls = ["tokio1", "boring-tls", "dep:tokio1_boring"]
dkim = ["dep:base64", "dep:sha2", "dep:rsa", "dep:ed25519-dalek"]
# wasm support
web = ["dep:web-time"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(lettre_ignore_tls_mismatch)'] }

View File

@@ -28,8 +28,8 @@
</div>
<div align="center">
<a href="https://deps.rs/crate/lettre/0.11.13">
<img src="https://deps.rs/crate/lettre/0.11.13/status.svg"
<a href="https://deps.rs/crate/lettre/0.11.9">
<img src="https://deps.rs/crate/lettre/0.11.9/status.svg"
alt="dependency status" />
</a>
</div>
@@ -53,12 +53,12 @@ Lettre does not provide (for now):
## Supported Rust Versions
Lettre supports all Rust versions released in the last 6 months. At the time of writing
the minimum supported Rust version is 1.71, 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.
## Example
This library requires Rust 1.71 or newer.
This library requires Rust 1.70 or newer.
To use this library, add the following to your `Cargo.toml`:
```toml
@@ -107,7 +107,7 @@ cargo run --example autoconfigure SMTP_HOST
## Testing
The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command. If you have python installed
The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command. If you have python installed
such a server can be launched with `python -m smtpd -n -c DebuggingServer 127.0.0.1:2525`
Alternatively only unit tests can be run by doing `cargo test --lib`.

View File

@@ -1,5 +1,5 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use lettre::{message::header::ContentType, Message, SmtpTransport, Transport};
use lettre::{Message, SmtpTransport, Transport};
fn bench_simple_send(c: &mut Criterion) {
let sender = SmtpTransport::builder_dangerous("127.0.0.1")
@@ -13,7 +13,6 @@ fn bench_simple_send(c: &mut Criterion) {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!"))
.unwrap();
let result = black_box(sender.send(&email));
@@ -33,7 +32,6 @@ fn bench_reuse_send(c: &mut Criterion) {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!"))
.unwrap();
let result = black_box(sender.send(&email));

View File

@@ -29,7 +29,7 @@ pub struct Envelope {
mod serde_forward_path {
use super::Address;
/// dummy type required for serde
/// see example: <https://serde.rs/deserialize-map.html>
/// see example: https://serde.rs/deserialize-map.html
struct CustomVisitor;
impl<'de> serde::de::Visitor<'de> for CustomVisitor {
type Value = Vec<Address>;

View File

@@ -36,7 +36,7 @@ impl<'de> Deserialize<'de> for Address {
{
struct FieldVisitor;
impl Visitor<'_> for FieldVisitor {
impl<'de> Visitor<'de> for FieldVisitor {
type Value = Field;
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {

View File

@@ -6,7 +6,7 @@
//! * Secure defaults
//! * Async support
//!
//! Lettre requires Rust 1.71 or newer.
//! Lettre requires Rust 1.70 or newer.
//!
//! ## Features
//!
@@ -34,25 +34,13 @@
//!
//! _Secure SMTP connections using TLS from the `native-tls` crate_
//!
//! Uses schannel on Windows, Security-Framework on macOS, and OpenSSL
//! on all other platforms.
//! Uses schannel on Windows, Security-Framework on macOS, and OpenSSL on Linux.
//!
//! * **native-tls** 📫: TLS support for the synchronous version of the API
//! * **tokio1-native-tls**: TLS support for the `tokio1` async version of the API
//!
//! NOTE: native-tls isn't supported with `async-std`
//!
//! ##### Building lettre with OpenSSL
//!
//! When building lettre with native-tls on a system that makes
//! use of OpenSSL, the following packages will need to be installed
//! in order for the build and the compiled program to run properly.
//!
//! | Distro | Build-time packages | Runtime packages |
//! | ------------ | -------------------------- | ---------------------------- |
//! | Debian | `pkg-config`, `libssl-dev` | `libssl3`, `ca-certificates` |
//! | Alpine Linux | `pkgconf`, `openssl-dev` | `libssl3`, `ca-certificates` |
//!
//! #### SMTP over TLS via the boring crate (Boring TLS)
//!
//! _Secure SMTP connections using TLS from the `boring-tls` crate_
@@ -107,7 +95,6 @@
//! * **tracing**: Logging using the `tracing` crate
//! * **mime03**: Allow creating a [`ContentType`] from an existing [mime 0.3] `Mime` struct
//! * **dkim**: Add support for signing email with DKIM
//! * **web**: WebAssembly support using the `web-time` crate for time operations
//!
//! [`SMTP`]: crate::transport::smtp
//! [`sendmail`]: crate::transport::sendmail
@@ -122,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.13")]
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.9")]
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
#![forbid(unsafe_code)]
@@ -150,14 +137,7 @@
clippy::wildcard_imports,
clippy::str_to_string,
clippy::empty_structs_with_brackets,
clippy::zero_sized_map_values,
clippy::manual_let_else,
clippy::semicolon_if_nothing_returned,
clippy::unnecessary_wraps,
clippy::doc_markdown,
clippy::explicit_iter_loop,
clippy::redundant_closure_for_method_calls,
// Rust 1.86: clippy::unnecessary_semicolon,
clippy::zero_sized_map_values
)]
#![cfg_attr(docsrs, feature(doc_cfg))]
@@ -219,7 +199,6 @@ mod executor;
#[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
pub mod message;
mod time;
pub mod transport;
use std::error::Error as StdError;

View File

@@ -69,7 +69,7 @@ impl Display for DkimSigningAlgorithm {
}
}
/// Describe [`DkimSigningKey`] key error
/// Describe DkimSigning key error
#[derive(Debug)]
pub struct DkimSigningKeyError(InnerDkimSigningKeyError);
@@ -100,7 +100,7 @@ impl StdError for DkimSigningKeyError {
}
}
/// Describe a signing key to be carried by [`DkimConfig`] struct
/// Describe a signing key to be carried by DkimConfig struct
#[derive(Debug)]
pub struct DkimSigningKey(InnerDkimSigningKey);
@@ -183,7 +183,7 @@ impl DkimConfig {
}
}
/// Create a [`DkimConfig`]
/// Create a DkimConfig
pub fn new(
selector: String,
domain: String,
@@ -283,19 +283,19 @@ fn dkim_canonicalize_headers_relaxed(headers: &str) -> String {
// End of header.
[b'\r', b'\n', ..] => {
*out += "\r\n";
name(&h[2..], out);
name(&h[2..], out)
}
// Sequential whitespace.
[b' ' | b'\t', b' ' | b'\t' | b'\r', ..] => value(&h[1..], out),
// All whitespace becomes spaces.
[b'\t', ..] => {
out.push(' ');
value(&h[1..], out);
value(&h[1..], out)
}
[_, ..] => {
let mut chars = h.chars();
out.push(chars.next().unwrap());
value(chars.as_str(), out);
value(chars.as_str(), out)
}
[] => {}
}
@@ -317,7 +317,7 @@ fn dkim_canonicalize_header_tag(
}
}
/// Canonicalize signed headers passed as `headers_list` among `mail_headers` using canonicalization
/// Canonicalize signed headers passed as headers_list among mail_headers using canonicalization
fn dkim_canonicalize_headers<'a>(
headers_list: impl IntoIterator<Item = &'a str>,
mail_headers: &Headers,
@@ -344,12 +344,10 @@ fn dkim_canonicalize_headers<'a>(
}
/// Sign with Dkim a message by adding Dkim-Signature header created with configuration expressed by
/// `dkim_config`
/// dkim_config
pub fn dkim_sign(message: &mut Message, dkim_config: &DkimConfig) {
#[cfg(feature = "web")]
dkim_sign_fixed_time(message, dkim_config, crate::time::now());
#[cfg(not(feature = "web"))]
dkim_sign_fixed_time(message, dkim_config, SystemTime::now());
dkim_sign_fixed_time(message, dkim_config, SystemTime::now())
}
fn dkim_sign_fixed_time(message: &mut Message, dkim_config: &DkimConfig, timestamp: SystemTime) {
@@ -380,7 +378,7 @@ fn dkim_sign_fixed_time(message: &mut Message, dkim_config: &DkimConfig, timesta
}
let dkim_header = dkim_header_format(dkim_config, timestamp, &signed_headers_list, &bh, "");
let signed_headers = dkim_canonicalize_headers(
dkim_config.headers.iter().map(AsRef::as_ref),
dkim_config.headers.iter().map(|h| h.as_ref()),
headers,
dkim_config.canonicalization.header,
);
@@ -490,14 +488,14 @@ cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT
fn test_headers_simple_canonicalize() {
let message = test_message();
dbg!(message.headers.to_string());
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Simple), "From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\nTest: test test very very long with spaces and extra spaces \twill be\r\n folded to several lines \r\n");
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Simple), "From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\nTest: test test very very long with spaces and extra spaces \twill be\r\n folded to several lines \r\n")
}
#[test]
fn test_headers_relaxed_canonicalize() {
let message = test_message();
dbg!(message.headers.to_string());
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Relaxed),"from:=?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\ntest:test test very very long with spaces and extra spaces will be folded to several lines\r\n");
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Relaxed),"from:=?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\ntest:test test very very long with spaces and extra spaces will be folded to several lines\r\n")
}
#[test]

View File

@@ -119,7 +119,7 @@ mod serde {
{
struct ContentTypeVisitor;
impl Visitor<'_> for ContentTypeVisitor {
impl<'de> Visitor<'de> for ContentTypeVisitor {
type Value = ContentType;
// The error message which states what the Visitor expects to

View File

@@ -21,7 +21,7 @@ impl Date {
///
/// Shortcut for `Date::new(SystemTime::now())`
pub fn now() -> Self {
Self::new(crate::time::now())
Self::new(SystemTime::now())
}
}

View File

@@ -110,7 +110,7 @@ mailbox_header! {
`Sender` header
This header contains [`Mailbox`] associated with sender.
This header contains [`Mailbox`][self::Mailbox] associated with sender.
```no_test
header::Sender("Mr. Sender <sender@example.com>".parse().unwrap())
@@ -124,7 +124,7 @@ mailboxes_header! {
`From` header
This header contains [`Mailboxes`].
This header contains [`Mailboxes`][self::Mailboxes].
*/
(From, "From")
@@ -135,7 +135,7 @@ mailboxes_header! {
`Reply-To` header
This header contains [`Mailboxes`].
This header contains [`Mailboxes`][self::Mailboxes].
*/
(ReplyTo, "Reply-To")
@@ -146,7 +146,7 @@ mailboxes_header! {
`To` header
This header contains [`Mailboxes`].
This header contains [`Mailboxes`][self::Mailboxes].
*/
(To, "To")
@@ -157,7 +157,7 @@ mailboxes_header! {
`Cc` header
This header contains [`Mailboxes`].
This header contains [`Mailboxes`][self::Mailboxes].
*/
(Cc, "Cc")
@@ -168,7 +168,7 @@ mailboxes_header! {
`Bcc` header
This header contains [`Mailboxes`].
This header contains [`Mailboxes`][self::Mailboxes].
*/
(Bcc, "Bcc")

View File

@@ -415,7 +415,7 @@ mod tests {
#[test]
fn empty_headername() {
assert!(HeaderName::new_from_ascii("".to_owned()).is_err());
assert!(HeaderName::new_from_ascii(String::from("")).is_err());
}
#[test]

View File

@@ -36,7 +36,7 @@ impl<'de> Deserialize<'de> for Mailbox {
{
struct FieldVisitor;
impl Visitor<'_> for FieldVisitor {
impl<'de> Visitor<'de> for FieldVisitor {
type Value = Field;
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {

View File

@@ -174,7 +174,7 @@ impl Mailboxes {
self
}
/// Adds a new [`Mailbox`] to the list, in a `Vec::push` style pattern.
/// Adds a new [`Mailbox`] to the list, in a Vec::push style pattern.
///
/// # Examples
///
@@ -351,7 +351,7 @@ impl FromStr for Mailboxes {
})?;
for (name, (user, domain)) in parsed_mailboxes {
mailboxes.push(Mailbox::new(name, Address::new(user, domain)?));
mailboxes.push(Mailbox::new(name, Address::new(user, domain)?))
}
Ok(Mailboxes(mailboxes))
@@ -531,7 +531,7 @@ mod test {
assert_eq!(
format!(
"{}",
Mailbox::new(Some("".to_owned()), "kayo@example.com".parse().unwrap())
Mailbox::new(Some("".into()), "kayo@example.com".parse().unwrap())
),
"kayo@example.com"
);

View File

@@ -277,7 +277,7 @@ impl MessageBuilder {
/// Shortcut for `self.date(SystemTime::now())`, it is automatically inserted
/// if no date has been provided.
pub fn date_now(self) -> Self {
self.date(crate::time::now())
self.date(SystemTime::now())
}
/// Set or add mailbox to `ReplyTo` header
@@ -345,7 +345,7 @@ impl MessageBuilder {
let hostname = hostname::get()
.map_err(|_| ())
.and_then(|s| s.into_string().map_err(|_| ()))
.unwrap_or_else(|()| DEFAULT_MESSAGE_ID_DOMAIN.to_owned());
.unwrap_or_else(|_| DEFAULT_MESSAGE_ID_DOMAIN.to_owned());
#[cfg(not(feature = "hostname"))]
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_owned();
@@ -457,12 +457,12 @@ impl MessageBuilder {
self.build(MessageBody::Raw(body.into_vec()))
}
/// Create message using mime body ([`MultiPart`])
/// Create message using mime body ([`MultiPart`][self::MultiPart])
pub fn multipart(self, part: MultiPart) -> Result<Message, EmailError> {
self.mime_1_0().build(MessageBody::Mime(Part::Multi(part)))
}
/// Create message using mime body ([`SinglePart`])
/// Create message using mime body ([`SinglePart`][self::SinglePart])
pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> {
self.mime_1_0().build(MessageBody::Mime(Part::Single(part)))
}
@@ -527,7 +527,7 @@ impl Message {
match &self.body {
MessageBody::Mime(p) => p.format_body(&mut out),
MessageBody::Raw(r) => out.extend_from_slice(r),
}
};
out.extend_from_slice(b"\r\n");
out
}
@@ -537,10 +537,7 @@ impl Message {
/// Example:
/// ```rust
/// use lettre::{
/// message::{
/// dkim::{DkimConfig, DkimSigningAlgorithm, DkimSigningKey},
/// header::ContentType,
/// },
/// message::dkim::{DkimConfig, DkimSigningAlgorithm, DkimSigningKey},
/// Message,
/// };
///
@@ -549,7 +546,6 @@ impl Message {
/// .reply_to("Bob <bob@example.org>".parse().unwrap())
/// .to("Carla <carla@example.net>".parse().unwrap())
/// .subject("Hello")
/// .header(ContentType::TEXT_PLAIN)
/// .body("Hi there, it's a test email, with utf-8 chars ë!\n\n\n".to_owned())
/// .unwrap();
/// let key = "-----BEGIN RSA PRIVATE KEY-----
@@ -605,7 +601,7 @@ impl EmailFormat for Message {
MessageBody::Mime(p) => p.format(out),
MessageBody::Raw(r) => {
out.extend_from_slice(b"\r\n");
out.extend_from_slice(r);
out.extend_from_slice(r)
}
}
}
@@ -769,7 +765,7 @@ mod test {
continue;
}
assert_eq!(line.0, line.1);
assert_eq!(line.0, line.1)
}
}

View File

@@ -1,18 +0,0 @@
use std::time::SystemTime;
#[cfg(feature = "web")]
pub(crate) fn now() -> SystemTime {
fn to_std_systemtime(time: web_time::SystemTime) -> std::time::SystemTime {
let duration = time
.duration_since(web_time::SystemTime::UNIX_EPOCH)
.unwrap();
SystemTime::UNIX_EPOCH + duration
}
to_std_systemtime(web_time::SystemTime::now())
}
#[cfg(not(feature = "web"))]
pub(crate) fn now() -> SystemTime {
SystemTime::now()
}

View File

@@ -68,7 +68,7 @@ impl fmt::Display for Error {
Kind::Io => f.write_str("response error")?,
#[cfg(feature = "file-transport-envelope")]
Kind::Envelope => f.write_str("internal client error")?,
}
};
if let Some(e) = &self.inner.source {
write!(f, ": {e}")?;

View File

@@ -11,7 +11,7 @@
//! # fn main() -> Result<(), Box<dyn Error>> {
//! use std::env::temp_dir;
//!
//! use lettre::{message::header::ContentType, FileTransport, Message, Transport};
//! use lettre::{FileTransport, Message, Transport};
//!
//! // Write to the local temp directory
//! let sender = FileTransport::new(temp_dir());
@@ -20,10 +20,10 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .header(ContentType::TEXT_PLAIN)
//! .body(String::from("Be happy!"))?;
//!
//! sender.send(&email)?;
//! let result = sender.send(&email);
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//!
@@ -44,7 +44,7 @@
//! # fn main() -> Result<(), Box<dyn Error>> {
//! use std::env::temp_dir;
//!
//! use lettre::{message::header::ContentType, FileTransport, Message, Transport};
//! use lettre::{FileTransport, Message, Transport};
//!
//! // Write to the local temp directory
//! let sender = FileTransport::with_envelope(temp_dir());
@@ -53,10 +53,10 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .header(ContentType::TEXT_PLAIN)
//! .body(String::from("Be happy!"))?;
//!
//! sender.send(&email)?;
//! let result = sender.send(&email);
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//!
@@ -73,9 +73,7 @@
//! # async fn run() -> Result<(), Box<dyn Error>> {
//! use std::env::temp_dir;
//!
//! use lettre::{
//! message::header::ContentType, AsyncFileTransport, AsyncTransport, Message, Tokio1Executor,
//! };
//! use lettre::{AsyncFileTransport, AsyncTransport, Message, Tokio1Executor};
//!
//! // Write to the local temp directory
//! let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir());
@@ -84,10 +82,10 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .header(ContentType::TEXT_PLAIN)
//! .body(String::from("Be happy!"))?;
//!
//! sender.send(email).await?;
//! let result = sender.send(email).await;
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//! ```
@@ -101,10 +99,7 @@
//! # async fn run() -> Result<(), Box<dyn Error>> {
//! use std::env::temp_dir;
//!
//! use lettre::{
//! message::header::ContentType, AsyncFileTransport, AsyncStd1Executor, AsyncTransport,
//! Message,
//! };
//! use lettre::{AsyncFileTransport, AsyncStd1Executor, AsyncTransport, Message};
//!
//! // Write to the local temp directory
//! let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir());
@@ -113,10 +108,10 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .header(ContentType::TEXT_PLAIN)
//! .body(String::from("Be happy!"))?;
//!
//! sender.send(email).await?;
//! let result = sender.send(email).await;
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//! ```
@@ -130,7 +125,6 @@
//! Reply-To: Yuin <yuin@domain.tld>
//! To: Hei <hei@domain.tld>
//! Subject: Happy new year
//! Content-Type: text/plain; charset=utf-8
//! Date: Tue, 18 Aug 2020 22:50:17 GMT
//!
//! Be happy!

View File

@@ -56,17 +56,13 @@
//! #
//! # #[cfg(all(feature = "builder", feature = "smtp-transport"))]
//! # fn main() -> Result<(), Box<dyn Error>> {
//! use lettre::{
//! message::header::ContentType, transport::smtp::authentication::Credentials, Message,
//! SmtpTransport, Transport,
//! };
//! use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .header(ContentType::TEXT_PLAIN)
//! .body(String::from("Be happy!"))?;
//!
//! let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());

View File

@@ -65,7 +65,7 @@ impl fmt::Display for Error {
match self.inner.kind {
Kind::Response => f.write_str("response error")?,
Kind::Client => f.write_str("internal client error")?,
}
};
if let Some(e) = &self.inner.source {
write!(f, ": {e}")?;

View File

@@ -7,18 +7,18 @@
//! #
//! # #[cfg(all(feature = "sendmail-transport", feature = "builder"))]
//! # fn main() -> Result<(), Box<dyn Error>> {
//! use lettre::{message::header::ContentType, Message, SendmailTransport, Transport};
//! use lettre::{Message, SendmailTransport, Transport};
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .header(ContentType::TEXT_PLAIN)
//! .body(String::from("Be happy!"))?;
//!
//! let sender = SendmailTransport::new();
//! sender.send(&email)?;
//! let result = sender.send(&email);
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//!
@@ -34,8 +34,7 @@
//! # #[cfg(all(feature = "tokio1", feature = "sendmail-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> {
//! use lettre::{
//! message::header::ContentType, AsyncSendmailTransport, AsyncTransport, Message,
//! SendmailTransport, Tokio1Executor,
//! AsyncSendmailTransport, AsyncTransport, Message, SendmailTransport, Tokio1Executor,
//! };
//!
//! let email = Message::builder()
@@ -43,11 +42,11 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .header(ContentType::TEXT_PLAIN)
//! .body(String::from("Be happy!"))?;
//!
//! let sender = AsyncSendmailTransport::<Tokio1Executor>::new();
//! sender.send(email).await?;
//! let result = sender.send(email).await;
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//! ```
@@ -59,17 +58,18 @@
//! #
//! # #[cfg(all(feature = "async-std1", feature = "sendmail-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> {
//! use lettre::{Message, AsyncTransport, AsyncStd1Executor,message::header::ContentType, AsyncSendmailTransport};
//! use lettre::{Message, AsyncTransport, AsyncStd1Executor, AsyncSendmailTransport};
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year").header(ContentType::TEXT_PLAIN)
//! .subject("Happy new year")
//! .body(String::from("Be happy!"))?;
//!
//! let sender = AsyncSendmailTransport::<AsyncStd1Executor>::new();
//! sender.send(email).await?;
//! let result = sender.send(email).await;
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//! ```
@@ -120,7 +120,7 @@ impl SendmailTransport {
/// Creates a new transport with the `sendmail` command
///
/// Note: This uses the `sendmail` command in the current `PATH`. To use another command,
/// use [`SendmailTransport::new_with_command`].
/// use [SendmailTransport::new_with_command].
pub fn new() -> SendmailTransport {
SendmailTransport {
command: DEFAULT_SENDMAIL.into(),
@@ -157,7 +157,7 @@ where
/// Creates a new transport with the `sendmail` command
///
/// Note: This uses the `sendmail` command in the current `PATH`. To use another command,
/// use [`AsyncSendmailTransport::new_with_command`].
/// use [AsyncSendmailTransport::new_with_command].
pub fn new() -> Self {
Self {
inner: SendmailTransport::new(),

View File

@@ -12,12 +12,6 @@ use async_trait::async_trait;
use super::pool::async_impl::Pool;
#[cfg(feature = "pool")]
use super::PoolConfig;
#[cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-rustls-tls"
))]
use super::Tls;
use super::{
client::AsyncSmtpConnection, ClientId, Credentials, Error, Mechanism, Response, SmtpInfo,
};
@@ -30,30 +24,6 @@ use crate::Tokio1Executor;
use crate::{Envelope, Executor};
/// Asynchronously sends emails using the SMTP protocol
///
/// `AsyncSmtpTransport` is the primary way for communicating
/// with SMTP relay servers to send email messages. It holds the
/// client connect configuration and creates new connections
/// as necessary.
///
/// # Connection pool
///
/// When the `pool` feature is enabled (default), `AsyncSmtpTransport` maintains a
/// connection pool to manage SMTP connections. The pool:
///
/// - Establishes a new connection when sending a message.
/// - Recycles connections internally after a message is sent.
/// - Reuses connections for subsequent messages, reducing connection setup overhead.
///
/// The connection pool can grow to hold multiple SMTP connections if multiple
/// emails are sent concurrently, as SMTP does not support multiplexing within a
/// single connection.
///
/// However, **connection reuse is not possible** if the `SyncSmtpTransport` instance
/// is dropped after every email send operation. You must reuse the instance
/// of this struct for the connection pool to be of any use.
///
/// To customize connection pool settings, use [`AsyncSmtpTransportBuilder::pool_config`].
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
pub struct AsyncSmtpTransport<E: Executor> {
#[cfg(feature = "pool")]
@@ -75,7 +45,7 @@ impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> {
let result = conn.send(envelope, email).await?;
#[cfg(not(feature = "pool"))]
conn.abort().await;
conn.quit().await?;
Ok(result)
}
@@ -191,72 +161,54 @@ where
/// Creates a `AsyncSmtpTransportBuilder` from a connection URL
///
/// The protocol, credentials, host, port and EHLO name can be provided
/// in a single URL. This may be simpler than having to configure SMTP
/// through multiple configuration parameters and then having to pass
/// those options to lettre.
/// The protocol, credentials, host and port can be provided in a single URL.
/// Use the scheme `smtp` for an unencrypted relay (optionally in combination with the
/// `tls` parameter to allow/require STARTTLS) or `smtps` for SMTP over TLS.
/// The path section of the url can be used to set an alternative name for
/// the HELO / EHLO command.
/// For example `smtps://username:password@smtp.example.com/client.example.com:465`
/// will set the HELO / EHLO name `client.example.com`.
///
/// The URL is created in the following way:
/// `scheme://user:pass@hostname:port/ehlo-name?tls=TLS`.
///
/// `user` (Username) and `pass` (Password) are optional in case the
/// SMTP relay doesn't require authentication. When `port` is not
/// configured it is automatically determined based on the `scheme`.
/// `ehlo-name` optionally overwrites the hostname sent for the EHLO
/// command. `TLS` controls whether STARTTLS is simply enabled
/// (`opportunistic` - not enough to prevent man-in-the-middle attacks)
/// or `required` (require the server to upgrade the connection to
/// STARTTLS, otherwise fail on suspicion of main-in-the-middle attempt).
///
/// Use the following table to construct your SMTP url:
///
/// | scheme | `tls` query parameter | example | default port | remarks |
/// | ------- | --------------------- | -------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
/// | `smtps` | unset | `smtps://user:pass@hostname:port` | 465 | SMTP over TLS, recommended method |
/// | `smtp` | `required` | `smtp://user:pass@hostname:port?tls=required` | 587 | SMTP with STARTTLS required, when SMTP over TLS is not available |
/// | `smtp` | `opportunistic` | `smtp://user:pass@hostname:port?tls=opportunistic` | 587 | SMTP with optionally STARTTLS when supported by the server. Not suitable for production use: vulnerable to a man-in-the-middle attack |
/// | `smtp` | unset | `smtp://user:pass@hostname:port` | 587 | Always unencrypted SMTP. Not suitable for production use: sends all data unencrypted |
///
/// IMPORTANT: some parameters like `user` and `pass` cannot simply
/// be concatenated to construct the final URL because special characters
/// contained within the parameter may confuse the URL decoder.
/// Manually URL encode the parameters before concatenating them or use
/// a proper URL encoder, like the following cargo script:
///
/// ```rust
/// # let _ = r#"
/// #!/usr/bin/env cargo
///
/// //! ```cargo
/// //! [dependencies]
/// //! url = "2"
/// //! ```
/// # "#;
///
/// use url::Url;
///
/// fn main() {
/// // don't touch this line
/// let mut url = Url::parse("foo://bar").unwrap();
///
/// // configure the scheme (`smtp` or `smtps`) here.
/// url.set_scheme("smtps").unwrap();
/// // configure the username and password.
/// // remove the following two lines if unauthenticated.
/// url.set_username("username").unwrap();
/// url.set_password(Some("password")).unwrap();
/// // configure the hostname
/// url.set_host(Some("smtp.example.com")).unwrap();
/// // configure the port - only necessary if using a non-default port
/// url.set_port(Some(465)).unwrap();
/// // configure the EHLO name
/// url.set_path("ehlo-name");
///
/// println!("{url}");
/// }
/// ```
///
/// The connection URL can then be used in the following way:
/// <table>
/// <thead>
/// <tr>
/// <th>scheme</th>
/// <th>tls parameter</th>
/// <th>example</th>
/// <th>remarks</th>
/// </tr>
/// </thead>
/// <tbody>
/// <tr>
/// <td>smtps</td>
/// <td>-</td>
/// <td>smtps://smtp.example.com</td>
/// <td>SMTP over TLS, recommended method</td>
/// </tr>
/// <tr>
/// <td>smtp</td>
/// <td>required</td>
/// <td>smtp://smtp.example.com?tls=required</td>
/// <td>SMTP with STARTTLS required, when SMTP over TLS is not available</td>
/// </tr>
/// <tr>
/// <td>smtp</td>
/// <td>opportunistic</td>
/// <td>smtp://smtp.example.com?tls=opportunistic</td>
/// <td>
/// SMTP with optionally STARTTLS when supported by the server.
/// Caution: this method is vulnerable to a man-in-the-middle attack.
/// Not recommended for production use.
/// </td>
/// </tr>
/// <tr>
/// <td>smtp</td>
/// <td>-</td>
/// <td>smtp://smtp.example.com</td>
/// <td>Unencrypted SMTP, not recommended for production use.</td>
/// </tr>
/// </tbody>
/// </table>
///
/// ```rust,no_run
/// use lettre::{
@@ -280,11 +232,15 @@ where
/// let mailer: AsyncSmtpTransport<Tokio1Executor> =
/// AsyncSmtpTransport::<Tokio1Executor>::from_url(
/// "smtps://username:password@smtp.example.com:465",
/// )?
/// )
/// .unwrap()
/// .build();
///
/// // Send the email
/// mailer.send(email).await?;
/// match mailer.send(email).await {
/// Ok(_) => println!("Email sent successfully!"),
/// Err(e) => panic!("Could not send email: {e:?}"),
/// }
/// # Ok(())
/// # }
/// ```
@@ -367,7 +323,7 @@ impl AsyncSmtpTransportBuilder {
self
}
/// Set the authentication credentials to use
/// Set the authentication mechanism to use
pub fn credentials(mut self, credentials: Credentials) -> Self {
self.info.credentials = Some(credentials);
self
@@ -380,17 +336,6 @@ impl AsyncSmtpTransportBuilder {
}
/// Set the port to use
///
/// # ⚠️⚠️⚠️ You probably don't need to call this method ⚠️⚠️⚠️
///
/// lettre usually picks the correct `port` when building
/// [`AsyncSmtpTransport`] using [`AsyncSmtpTransport::relay`] or
/// [`AsyncSmtpTransport::starttls_relay`].
///
/// # Errors
///
/// Using the incorrect `port` and [`Self::tls`] combination may
/// lead to hard to debug IO errors coming from the TLS library.
pub fn port(mut self, port: u16) -> Self {
self.info.port = port;
self
@@ -403,17 +348,6 @@ impl AsyncSmtpTransportBuilder {
}
/// Set the TLS settings to use
///
/// # ⚠️⚠️⚠️ You probably don't need to call this method ⚠️⚠️⚠️
///
/// By default lettre chooses the correct `tls` configuration when
/// building [`AsyncSmtpTransport`] using [`AsyncSmtpTransport::relay`] or
/// [`AsyncSmtpTransport::starttls_relay`].
///
/// # Errors
///
/// Using the incorrect [`Tls`] and [`Self::port`] combination may
/// lead to hard to debug IO errors coming from the TLS library.
#[cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
@@ -427,7 +361,7 @@ impl AsyncSmtpTransportBuilder {
feature = "async-std1-rustls-tls"
)))
)]
pub fn tls(mut self, tls: Tls) -> Self {
pub fn tls(mut self, tls: super::Tls) -> Self {
self.info.tls = tls;
self
}

View File

@@ -98,17 +98,13 @@ impl Mechanism {
let decoded_challenge = challenge
.ok_or_else(|| error::client("This mechanism does expect a challenge"))?;
if contains_ignore_ascii_case(
decoded_challenge,
["User Name", "Username:", "Username", "User Name\0"],
) {
if ["User Name", "Username:", "Username", "User Name\0"]
.contains(&decoded_challenge)
{
return Ok(credentials.authentication_identity.clone());
}
if contains_ignore_ascii_case(
decoded_challenge,
["Password", "Password:", "Password\0"],
) {
if ["Password", "Password:", "Password\0"].contains(&decoded_challenge) {
return Ok(credentials.secret.clone());
}
@@ -125,15 +121,6 @@ impl Mechanism {
}
}
fn contains_ignore_ascii_case<'a>(
haystack: &str,
needles: impl IntoIterator<Item = &'a str>,
) -> bool {
needles
.into_iter()
.any(|item| item.eq_ignore_ascii_case(haystack))
}
#[cfg(test)]
mod test {
use super::{Credentials, Mechanism};
@@ -168,23 +155,6 @@ mod test {
assert!(mechanism.response(&credentials, None).is_err());
}
#[test]
fn test_login_case_insensitive() {
let mechanism = Mechanism::Login;
let credentials = Credentials::new("alice".to_owned(), "wonderland".to_owned());
assert_eq!(
mechanism.response(&credentials, Some("username")).unwrap(),
"alice"
);
assert_eq!(
mechanism.response(&credentials, Some("password")).unwrap(),
"wonderland"
);
assert!(mechanism.response(&credentials, None).is_err());
}
#[test]
fn test_xoauth2() {
let mechanism = Mechanism::Xoauth2;

View File

@@ -10,34 +10,21 @@ use super::{AsyncNetworkStream, ClientCodec, TlsParameters};
use crate::{
transport::smtp::{
authentication::{Credentials, Mechanism},
client::{ConnectionState, ConnectionWrapper},
commands::{Auth, Data, Ehlo, Mail, Noop, Quit, Rcpt, Starttls},
error,
error::Error,
error::{self, Error},
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
response::{parse_response, Response},
},
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
pub struct AsyncSmtpConnection {
/// TCP stream between client and server
/// Value is None before connection
stream: BufReader<AsyncNetworkStream>,
/// Panic state
panic: bool,
stream: ConnectionWrapper<BufReader<AsyncNetworkStream>>,
/// Whether QUIT has been sent
sent_quit: bool,
/// Information about the server
server_info: ServerInfo,
}
@@ -86,7 +73,8 @@ impl AsyncSmtpConnection {
/// Some(TlsParameters::new("example.com".to_owned())?),
/// None,
/// )
/// .await?;
/// .await
/// .unwrap();
/// # Ok(())
/// # }
/// ```
@@ -124,8 +112,8 @@ impl AsyncSmtpConnection {
) -> Result<AsyncSmtpConnection, Error> {
let stream = BufReader::new(stream);
let mut conn = AsyncSmtpConnection {
stream,
panic: false,
stream: ConnectionWrapper::new(stream),
sent_quit: false,
server_info: ServerInfo::default(),
};
// TODO log
@@ -169,30 +157,28 @@ impl AsyncSmtpConnection {
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
}
try_smtp!(
self.command(Mail::new(envelope.from().cloned(), mail_options))
.await,
self
);
self.command(Mail::new(envelope.from().cloned(), mail_options))
.await?;
// Recipient
for to_address in envelope.to() {
try_smtp!(
self.command(Rcpt::new(to_address.clone(), vec![])).await,
self
);
self.command(Rcpt::new(to_address.clone(), vec![])).await?;
}
// Data
try_smtp!(self.command(Data).await, self);
self.command(Data).await?;
// Message content
let result = try_smtp!(self.message(email).await, self);
let result = self.message(email).await?;
Ok(result)
}
pub fn has_broken(&self) -> bool {
self.panic
self.sent_quit
|| matches!(
self.stream.state(),
ConnectionState::BrokenConnection | ConnectionState::BrokenResponse
)
}
pub fn can_starttls(&self) -> bool {
@@ -212,12 +198,14 @@ impl AsyncSmtpConnection {
hello_name: &ClientId,
) -> Result<(), Error> {
if self.server_info.supports_feature(Extension::StartTls) {
try_smtp!(self.command(Starttls).await, self);
self.stream.get_mut().upgrade_tls(tls_parameters).await?;
self.command(Starttls).await?;
self.stream
.async_op(|stream| stream.get_mut().upgrade_tls(tls_parameters))
.await?;
#[cfg(feature = "tracing")]
tracing::debug!("connection encrypted");
// Send EHLO again
try_smtp!(self.ehlo(hello_name).await, self);
self.ehlo(hello_name).await?;
Ok(())
} else {
Err(error::client("STARTTLS is not supported on this server"))
@@ -226,32 +214,39 @@ impl AsyncSmtpConnection {
/// Send EHLO and update server info
async fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> {
let ehlo_response = try_smtp!(self.command(Ehlo::new(hello_name.clone())).await, self);
self.server_info = try_smtp!(ServerInfo::from_response(&ehlo_response), self);
let ehlo_response = self.command(Ehlo::new(hello_name.clone())).await?;
self.server_info = ServerInfo::from_response(&ehlo_response)?;
Ok(())
}
pub async fn quit(&mut self) -> Result<Response, Error> {
Ok(try_smtp!(self.command(Quit).await, self))
self.sent_quit = true;
self.command(Quit).await
}
pub async fn abort(&mut self) {
// Only try to quit if we are not already broken
if !self.panic {
self.panic = true;
let _ = self.command(Quit).await;
// `write` already rejects writes if the connection state if bad
if !self.sent_quit {
let _ = self.quit().await;
}
if !matches!(self.stream.state(), ConnectionState::BrokenConnection) {
let _ = self
.stream
.async_op(|stream| async { stream.close().await.map_err(error::network) })
.await;
}
let _ = self.stream.close().await;
}
/// Sets the underlying stream
pub fn set_stream(&mut self, stream: AsyncNetworkStream) {
self.stream = BufReader::new(stream);
self.stream = ConnectionWrapper::new(BufReader::new(stream));
}
/// Tells if the underlying stream is currently encrypted
pub fn is_encrypted(&self) -> bool {
self.stream.get_ref().is_encrypted()
self.stream.get_ref().get_ref().is_encrypted()
}
/// Checks if the server is connected using the NOOP SMTP command
@@ -278,15 +273,13 @@ impl AsyncSmtpConnection {
while challenges > 0 && response.has_code(334) {
challenges -= 1;
response = try_smtp!(
self.command(Auth::new_from_response(
response = self
.command(Auth::new_from_response(
mechanism,
credentials.clone(),
&response,
)?)
.await,
self
);
.await?;
}
if challenges == 0 {
@@ -315,15 +308,17 @@ impl AsyncSmtpConnection {
/// Writes a string to the server
async fn write(&mut self, string: &[u8]) -> Result<(), Error> {
self.stream
.get_mut()
.write_all(string)
.await
.map_err(error::network)?;
.async_op(|stream| async {
stream
.get_mut()
.write_all(string)
.await
.map_err(error::network)
})
.await?;
self.stream
.get_mut()
.flush()
.await
.map_err(error::network)?;
.async_op(|stream| async { stream.get_mut().flush().await.map_err(error::network) })
.await?;
#[cfg(feature = "tracing")]
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
@@ -336,9 +331,10 @@ impl AsyncSmtpConnection {
while self
.stream
.read_line(&mut buffer)
.await
.map_err(error::network)?
.async_op(|stream| async {
stream.read_line(&mut buffer).await.map_err(error::network)
})
.await?
> 0
{
#[cfg(feature = "tracing")]
@@ -355,10 +351,12 @@ impl AsyncSmtpConnection {
}
}
Err(nom::Err::Failure(e)) => {
self.stream.set_state(ConnectionState::BrokenResponse);
return Err(error::response(e.to_string()));
}
Err(nom::Err::Incomplete(_)) => { /* read more */ }
Err(nom::Err::Error(e)) => {
self.stream.set_state(ConnectionState::BrokenResponse);
return Err(error::response(e.to_string()));
}
}
@@ -370,28 +368,12 @@ impl AsyncSmtpConnection {
/// The X509 certificate of the server (DER encoded)
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
self.stream.get_ref().peer_certificate()
}
/// Currently this is only avaialable when using Boring TLS and
/// returns the result of the verification of the TLS certificate
/// presented by the peer, if any. Only the last error encountered
/// during verification is presented.
/// It can be useful when you don't want to fail outright the TLS
/// negotiation, for example when a self-signed certificate is
/// encountered, but still want to record metrics or log the fact.
/// When using DANE verification, the PKI root of trust moves from
/// the CAs to DNS, so self-signed certificates are permitted as long
/// as the TLSA records match the leaf or issuer certificates.
/// It cannot be called on non Boring TLS streams.
#[cfg(feature = "boring-tls")]
pub fn tls_verify_result(&self) -> Result<(), Error> {
self.stream.get_ref().tls_verify_result()
self.stream.get_ref().get_ref().peer_certificate()
}
/// All the X509 certificates of the chain (DER encoded)
#[cfg(any(feature = "rustls-tls", feature = "boring-tls"))]
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
self.stream.get_ref().certificate_chain()
self.stream.get_ref().get_ref().certificate_chain()
}
}

View File

@@ -170,7 +170,7 @@ impl AsyncNetworkStream {
last_err = Some(io::Error::new(
io::ErrorKind::TimedOut,
"connection timed out",
));
))
}
}
} else {
@@ -222,7 +222,7 @@ impl AsyncNetworkStream {
last_err = Some(io::Error::new(
io::ErrorKind::TimedOut,
"connection timed out",
));
))
}
}
}
@@ -270,8 +270,9 @@ impl AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
// get owned TcpStream
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
let InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) = tcp_stream else {
unreachable!()
let tcp_stream = match tcp_stream {
InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) => tcp_stream,
_ => unreachable!(),
};
self.inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters)
@@ -289,8 +290,9 @@ impl AsyncNetworkStream {
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
// get owned TcpStream
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
let InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream) = tcp_stream else {
unreachable!()
let tcp_stream = match tcp_stream {
InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream) => tcp_stream,
_ => unreachable!(),
};
self.inner = Self::upgrade_asyncstd1_tls(tcp_stream, tls_parameters)
@@ -429,30 +431,6 @@ impl AsyncNetworkStream {
}
}
#[cfg(feature = "boring-tls")]
pub fn tls_verify_result(&self) -> Result<(), Error> {
match &self.inner {
#[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
Err(error::client("Connection is not encrypted"))
}
#[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(_) => panic!("Unsupported"),
#[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(_) => panic!("Unsupported"),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => {
stream.ssl().verify_result().map_err(error::tls)
}
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
Err(error::client("Connection is not encrypted"))
}
#[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(_) => panic!("Unsupported"),
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
}
}
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
match &self.inner {
#[cfg(feature = "tokio1")]

View File

@@ -7,38 +7,25 @@ use std::{
#[cfg(feature = "tracing")]
use super::escape_crlf;
use super::{ClientCodec, NetworkStream, TlsParameters};
use super::{ClientCodec, ConnectionWrapper, NetworkStream, TlsParameters};
use crate::{
address::Envelope,
transport::smtp::{
authentication::{Credentials, Mechanism},
client::ConnectionState,
commands::{Auth, Data, Ehlo, Mail, Noop, Quit, Rcpt, Starttls},
error,
error::Error,
error::{self, Error},
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
response::{parse_response, Response},
},
};
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
pub struct SmtpConnection {
/// TCP stream between client and server
/// Value is None before connection
stream: BufReader<NetworkStream>,
/// Panic state
panic: bool,
stream: ConnectionWrapper<BufReader<NetworkStream>>,
/// Whether QUIT has been sent
sent_quit: bool,
/// Information about the server
server_info: ServerInfo,
}
@@ -64,8 +51,8 @@ impl SmtpConnection {
let stream = NetworkStream::connect(server, timeout, tls_parameters, local_address)?;
let stream = BufReader::new(stream);
let mut conn = SmtpConnection {
stream,
panic: false,
stream: ConnectionWrapper::new(stream),
sent_quit: false,
server_info: ServerInfo::default(),
};
conn.set_timeout(timeout).map_err(error::network)?;
@@ -110,26 +97,27 @@ impl SmtpConnection {
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
}
try_smtp!(
self.command(Mail::new(envelope.from().cloned(), mail_options)),
self
);
self.command(Mail::new(envelope.from().cloned(), mail_options))?;
// Recipient
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
try_smtp!(self.command(Data), self);
self.command(Data)?;
// Message content
let result = try_smtp!(self.message(email), self);
let result = self.message(email)?;
Ok(result)
}
pub fn has_broken(&self) -> bool {
self.panic
self.sent_quit
|| matches!(
self.stream.state(),
ConnectionState::BrokenConnection | ConnectionState::BrokenResponse
)
}
pub fn can_starttls(&self) -> bool {
@@ -145,12 +133,13 @@ impl SmtpConnection {
if self.server_info.supports_feature(Extension::StartTls) {
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
{
try_smtp!(self.command(Starttls), self);
self.stream.get_mut().upgrade_tls(tls_parameters)?;
self.command(Starttls)?;
self.stream
.sync_op(|stream| stream.get_mut().upgrade_tls(tls_parameters))?;
#[cfg(feature = "tracing")]
tracing::debug!("connection encrypted");
// Send EHLO again
try_smtp!(self.ehlo(hello_name), self);
self.ehlo(hello_name)?;
Ok(())
}
#[cfg(not(any(
@@ -168,38 +157,47 @@ impl SmtpConnection {
/// Send EHLO and update server info
fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> {
let ehlo_response = try_smtp!(self.command(Ehlo::new(hello_name.clone())), self);
self.server_info = try_smtp!(ServerInfo::from_response(&ehlo_response), self);
let ehlo_response = self.command(Ehlo::new(hello_name.clone()))?;
self.server_info = ServerInfo::from_response(&ehlo_response)?;
Ok(())
}
pub fn quit(&mut self) -> Result<Response, Error> {
Ok(try_smtp!(self.command(Quit), self))
self.sent_quit = true;
self.command(Quit)
}
pub fn abort(&mut self) {
// Only try to quit if we are not already broken
if !self.panic {
self.panic = true;
let _ = self.command(Quit);
// `write` already rejects writes if the connection state if bad
if !self.sent_quit {
let _ = self.quit();
}
if !matches!(self.stream.state(), ConnectionState::BrokenConnection) {
let _ = self.stream.sync_op(|stream| {
stream
.get_mut()
.shutdown(std::net::Shutdown::Both)
.map_err(error::network)
});
}
let _ = self.stream.get_mut().shutdown(std::net::Shutdown::Both);
}
/// Sets the underlying stream
pub fn set_stream(&mut self, stream: NetworkStream) {
self.stream = BufReader::new(stream);
self.stream = ConnectionWrapper::new(BufReader::new(stream));
}
/// Tells if the underlying stream is currently encrypted
pub fn is_encrypted(&self) -> bool {
self.stream.get_ref().is_encrypted()
self.stream.get_ref().get_ref().is_encrypted()
}
/// Set timeout
pub fn set_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
self.stream.get_mut().set_read_timeout(duration)?;
self.stream.get_mut().set_write_timeout(duration)
self.stream.get_mut().get_mut().set_read_timeout(duration)?;
self.stream.get_mut().get_mut().set_write_timeout(duration)
}
/// Checks if the server is connected using the NOOP SMTP command
@@ -224,14 +222,11 @@ impl SmtpConnection {
while challenges > 0 && response.has_code(334) {
challenges -= 1;
response = try_smtp!(
self.command(Auth::new_from_response(
mechanism,
credentials.clone(),
&response,
)?),
self
);
response = self.command(Auth::new_from_response(
mechanism,
credentials.clone(),
&response,
)?)?;
}
if challenges == 0 {
@@ -261,10 +256,9 @@ impl SmtpConnection {
/// Writes a string to the server
fn write(&mut self, string: &[u8]) -> Result<(), Error> {
self.stream
.get_mut()
.write_all(string)
.map_err(error::network)?;
self.stream.get_mut().flush().map_err(error::network)?;
.sync_op(|stream| stream.get_mut().write_all(string).map_err(error::network))?;
self.stream
.sync_op(|stream| stream.get_mut().flush().map_err(error::network))?;
#[cfg(feature = "tracing")]
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
@@ -275,7 +269,11 @@ impl SmtpConnection {
pub fn read_response(&mut self) -> Result<Response, Error> {
let mut buffer = String::with_capacity(100);
while self.stream.read_line(&mut buffer).map_err(error::network)? > 0 {
while self
.stream
.sync_op(|stream| stream.read_line(&mut buffer).map_err(error::network))?
> 0
{
#[cfg(feature = "tracing")]
tracing::debug!("<< {}", escape_crlf(&buffer));
match parse_response(&buffer) {
@@ -290,10 +288,12 @@ impl SmtpConnection {
};
}
Err(nom::Err::Failure(e)) => {
self.stream.set_state(ConnectionState::BrokenResponse);
return Err(error::response(e.to_string()));
}
Err(nom::Err::Incomplete(_)) => { /* read more */ }
Err(nom::Err::Error(e)) => {
self.stream.set_state(ConnectionState::BrokenResponse);
return Err(error::response(e.to_string()));
}
}
@@ -305,28 +305,12 @@ impl SmtpConnection {
/// The X509 certificate of the server (DER encoded)
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
self.stream.get_ref().peer_certificate()
}
/// Currently this is only avaialable when using Boring TLS and
/// returns the result of the verification of the TLS certificate
/// presented by the peer, if any. Only the last error encountered
/// during verification is presented.
/// It can be useful when you don't want to fail outright the TLS
/// negotiation, for example when a self-signed certificate is
/// encountered, but still want to record metrics or log the fact.
/// When using DANE verification, the PKI root of trust moves from
/// the CAs to DNS, so self-signed certificates are permitted as long
/// as the TLSA records match the leaf or issuer certificates.
/// It cannot be called on non Boring TLS streams.
#[cfg(feature = "boring-tls")]
pub fn tls_verify_result(&self) -> Result<(), Error> {
self.stream.get_ref().tls_verify_result()
self.stream.get_ref().get_ref().peer_certificate()
}
/// All the X509 certificates of the chain (DER encoded)
#[cfg(any(feature = "rustls-tls", feature = "boring-tls"))]
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
self.stream.get_ref().certificate_chain()
self.stream.get_ref().get_ref().certificate_chain()
}
}

View File

@@ -24,6 +24,8 @@
#[cfg(feature = "serde")]
use std::fmt::Debug;
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
use std::future::Future;
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub use self::async_connection::AsyncSmtpConnection;
@@ -40,6 +42,7 @@ pub use self::{
connection::SmtpConnection,
tls::{Certificate, CertificateStore, Identity, Tls, TlsParameters, TlsParametersBuilder},
};
use super::{error, Error};
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
mod async_connection;
@@ -49,6 +52,99 @@ mod connection;
mod net;
mod tls;
#[derive(Debug)]
pub(super) struct ConnectionWrapper<C> {
conn: C,
state: ConnectionState,
}
impl<C> ConnectionWrapper<C> {
pub(super) fn new(conn: C) -> Self {
Self {
conn,
state: ConnectionState::ProbablyConnected,
}
}
pub(super) fn get_ref(&self) -> &C {
&self.conn
}
pub(super) fn get_mut(&mut self) -> &mut C {
&mut self.conn
}
pub(super) fn state(&self) -> ConnectionState {
self.state
}
pub(super) fn set_state(&mut self, state: ConnectionState) {
self.state = state;
}
pub(super) fn sync_op<F, T>(&mut self, f: F) -> Result<T, Error>
where
F: FnOnce(&mut C) -> Result<T, Error>,
{
if !matches!(
self.state,
ConnectionState::ProbablyConnected | ConnectionState::BrokenResponse
) {
return Err(error::client(
"attempted to send operation to broken connection",
));
}
self.state = ConnectionState::Writing;
match f(&mut self.conn) {
Ok(t) => {
self.state = ConnectionState::ProbablyConnected;
Ok(t)
}
Err(err) => {
self.state = ConnectionState::BrokenConnection;
Err(err)
}
}
}
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub(super) async fn async_op<'a, F, Fut, T>(&'a mut self, f: F) -> Result<T, Error>
where
F: FnOnce(&'a mut C) -> Fut,
Fut: Future<Output = Result<T, Error>>,
{
if !matches!(
self.state,
ConnectionState::ProbablyConnected | ConnectionState::BrokenResponse
) {
return Err(error::client(
"attempted to send operation to broken connection",
));
}
self.state = ConnectionState::Writing;
match f(&mut self.conn).await {
Ok(t) => {
self.state = ConnectionState::ProbablyConnected;
Ok(t)
}
Err(err) => {
self.state = ConnectionState::BrokenConnection;
Err(err)
}
}
}
}
#[derive(Debug, Copy, Clone)]
pub(super) enum ConnectionState {
ProbablyConnected,
Writing,
BrokenResponse,
BrokenConnection,
}
/// The codec used for transparency
#[derive(Debug)]
struct ClientCodec {

View File

@@ -119,12 +119,12 @@ impl NetworkStream {
if let Some(timeout) = timeout {
match socket.connect_timeout(&addr.into(), timeout) {
Ok(()) => return Ok(socket.into()),
Ok(_) => return Ok(socket.into()),
Err(err) => last_err = Some(err),
}
} else {
match socket.connect(&addr.into()) {
Ok(()) => return Ok(socket.into()),
Ok(_) => return Ok(socket.into()),
Err(err) => last_err = Some(err),
}
}
@@ -160,8 +160,9 @@ impl NetworkStream {
InnerNetworkStream::Tcp(_) => {
// get owned TcpStream
let tcp_stream = mem::replace(&mut self.inner, InnerNetworkStream::None);
let InnerNetworkStream::Tcp(tcp_stream) = tcp_stream else {
unreachable!()
let tcp_stream = match tcp_stream {
InnerNetworkStream::Tcp(tcp_stream) => tcp_stream,
_ => unreachable!(),
};
self.inner = Self::upgrade_tls_impl(tcp_stream, tls_parameters)?;
@@ -222,22 +223,6 @@ impl NetworkStream {
}
}
#[cfg(feature = "boring-tls")]
pub fn tls_verify_result(&self) -> Result<(), Error> {
match &self.inner {
InnerNetworkStream::Tcp(_) => Err(error::client("Connection is not encrypted")),
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(_) => panic!("Unsupported"),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(_) => panic!("Unsupported"),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(stream) => {
stream.ssl().verify_result().map_err(error::tls)
}
InnerNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
}
}
#[cfg(any(feature = "rustls-tls", feature = "boring-tls"))]
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
match &self.inner {
@@ -384,7 +369,7 @@ impl Write for NetworkStream {
/// If the local address is set, binds the socket to this address.
/// If local address is not set, then destination address is required to determine the default
/// local address on some platforms.
/// See: <https://github.com/hyperium/hyper/blob/faf24c6ad8eee1c3d5ccc9a4d4835717b8e2903f/src/client/connect/http.rs#L560>
/// See: https://github.com/hyperium/hyper/blob/faf24c6ad8eee1c3d5ccc9a4d4835717b8e2903f/src/client/connect/http.rs#L560
fn bind_local_address(
socket: &socket2::Socket,
dst_addr: &SocketAddr,

View File

@@ -59,61 +59,27 @@ pub enum TlsVersion {
Tlsv13,
}
/// Specifies how to establish a TLS connection
///
/// TLDR: Use [`Tls::Wrapper`] or [`Tls::Required`] when
/// connecting to a remote server, [`Tls::None`] when
/// connecting to a local server.
/// How to apply TLS to a client connection
#[derive(Clone)]
#[allow(missing_copy_implementations)]
pub enum Tls {
/// Insecure (plaintext) connection only.
///
/// This option **always** uses a plaintext connection and should only
/// be used for trusted local relays. It is **highly discouraged**
/// for remote servers, as it exposes credentials and emails to potential
/// interception.
///
/// Note: Servers requiring credentials or emails to be sent over TLS
/// may reject connections when this option is used.
/// Insecure connection only (for testing purposes)
None,
/// Begin with a plaintext connection and attempt to use `STARTTLS` if available.
///
/// lettre will try to upgrade to a TLS-secured connection but will fall back
/// to plaintext if the server does not support TLS. This option is provided for
/// compatibility but is **strongly discouraged**, as it exposes connections to
/// potential MITM (man-in-the-middle) attacks.
///
/// Warning: A malicious intermediary could intercept the `STARTTLS` flag,
/// causing lettre to believe the server only supports plaintext connections.
/// Start with insecure connection and use `STARTTLS` when available
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
Opportunistic(TlsParameters),
/// Begin with a plaintext connection and require `STARTTLS` for security.
///
/// lettre will upgrade plaintext TCP connections to TLS before transmitting
/// any sensitive data. If the server does not support TLS, the connection
/// attempt will fail, ensuring no credentials or emails are sent in plaintext.
///
/// Unlike [`Tls::Opportunistic`], this option is secure against MITM attacks.
/// For optimal security and performance, consider using [`Tls::Wrapper`] instead,
/// as it requires fewer roundtrips to establish a secure connection.
/// Start with insecure connection and require `STARTTLS`
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
Required(TlsParameters),
/// Establish a connection wrapped in TLS from the start.
///
/// lettre connects to the server and immediately performs a TLS handshake.
/// If the handshake fails, the connection attempt is aborted without
/// transmitting any sensitive data.
///
/// This is the fastest and most secure option for establishing a connection.
/// Use TLS wrapped connection
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
@@ -423,7 +389,7 @@ impl TlsParametersBuilder {
let mut root_cert_store = RootCertStore::empty();
#[cfg(feature = "rustls-native-certs")]
fn load_native_roots(store: &mut RootCertStore) {
fn load_native_roots(store: &mut RootCertStore) -> Result<(), Error> {
let rustls_native_certs::CertificateResult { certs, errors, .. } =
rustls_native_certs::load_native_certs();
let errors_len = errors.len();
@@ -433,6 +399,7 @@ impl TlsParametersBuilder {
tracing::debug!(
"loaded platform certs with {errors_len} failing to load, {added} valid and {ignored} ignored (invalid) certs"
);
Ok(())
}
#[cfg(feature = "rustls-tls")]
@@ -443,7 +410,7 @@ impl TlsParametersBuilder {
match self.cert_store {
CertificateStore::Default => {
#[cfg(feature = "rustls-native-certs")]
load_native_roots(&mut root_cert_store);
load_native_roots(&mut root_cert_store)?;
#[cfg(not(feature = "rustls-native-certs"))]
load_webpki_roots(&mut root_cert_store);
}
@@ -661,11 +628,10 @@ impl Identity {
#[cfg(feature = "rustls-tls")]
fn from_pem_rustls_tls(
pem: &[u8],
mut key: &[u8],
key: &[u8],
) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>), Error> {
let key = rustls_pemfile::private_key(&mut key)
.map_err(error::tls)?
.ok_or_else(|| error::tls("no private key found"))?;
let mut key = key;
let key = rustls_pemfile::private_key(&mut key).unwrap().unwrap();
Ok((vec![pem.to_owned().into()], key))
}

View File

@@ -1,5 +1,3 @@
use std::borrow::Cow;
use url::Url;
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
@@ -64,7 +62,7 @@ impl TransportBuilder for AsyncSmtpTransportBuilder {
}
}
/// Create a new `SmtpTransportBuilder` or `AsyncSmtpTransportBuilder` from a connection URL
/// Create a new SmtpTransportBuilder or AsyncSmtpTransportBuilder from a connection URL
pub(crate) fn from_connection_url<B: TransportBuilder>(connection_url: &str) -> Result<B, Error> {
let connection_url = Url::parse(connection_url).map_err(error::connection)?;
let tls: Option<String> = connection_url
@@ -86,26 +84,26 @@ pub(crate) fn from_connection_url<B: TransportBuilder>(connection_url: &str) ->
("smtp", Some("required")) => {
builder = builder
.port(connection_url.port().unwrap_or(SUBMISSION_PORT))
.tls(Tls::Required(TlsParameters::new(host.into())?));
.tls(Tls::Required(TlsParameters::new(host.into())?))
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
("smtp", Some("opportunistic")) => {
builder = builder
.port(connection_url.port().unwrap_or(SUBMISSION_PORT))
.tls(Tls::Opportunistic(TlsParameters::new(host.into())?));
.tls(Tls::Opportunistic(TlsParameters::new(host.into())?))
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
("smtps", _) => {
builder = builder
.port(connection_url.port().unwrap_or(SUBMISSIONS_PORT))
.tls(Tls::Wrapper(TlsParameters::new(host.into())?));
.tls(Tls::Wrapper(TlsParameters::new(host.into())?))
}
(scheme, tls) => {
return Err(error::connection(format!(
"Unknown scheme '{scheme}' or tls parameter '{tls:?}', note that a transport with TLS requires one of the TLS features"
)))
}
}
};
// use the path segment of the URL as name in the name in the HELO / EHLO command
if connection_url.path().len() > 1 {
@@ -117,7 +115,7 @@ pub(crate) fn from_connection_url<B: TransportBuilder>(connection_url: &str) ->
let percent_decode = |s: &str| {
percent_encoding::percent_decode_str(s)
.decode_utf8()
.map(Cow::into_owned)
.map(|cow| cow.into_owned())
.map_err(error::connection)
};
let credentials = Credentials::new(

View File

@@ -142,7 +142,7 @@ impl fmt::Display for Error {
Kind::Permanent(code) => {
write!(f, "permanent error ({code})")?;
}
}
};
if let Some(e) = &self.inner.source {
write!(f, ": {e}")?;

View File

@@ -129,8 +129,9 @@ impl Display for ServerInfo {
impl ServerInfo {
/// Parses a EHLO response to create a `ServerInfo`
pub fn from_response(response: &Response) -> Result<ServerInfo, Error> {
let Some(name) = response.first_word() else {
return Err(error::response("Could not read server name"));
let name = match response.first_word() {
Some(name) => name,
None => return Err(error::response("Could not read server name")),
};
let mut features: HashSet<Extension> = HashSet::new();
@@ -168,7 +169,7 @@ impl ServerInfo {
}
}
_ => (),
}
};
}
Ok(ServerInfo {

View File

@@ -26,17 +26,43 @@
//!
//! The relay server can be the local email server, a specific host or a third-party service.
//!
//! #### Simple example with authentication
//! #### Simple example
//!
//! A good starting point for sending emails via SMTP relay is to
//! do the following:
//! This is the most basic example of usage:
//!
//! ```rust,no_run
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
//! use lettre::{Message, SmtpTransport, Transport};
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body(String::from("Be happy!"))?;
//!
//! // Create TLS transport on port 465
//! let sender = SmtpTransport::relay("smtp.example.com")?.build();
//! // Send the email via remote relay
//! let result = sender.send(&email);
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//! ```
//!
//! #### Authentication
//!
//! Example with authentication and connection pool:
//!
//! ```rust,no_run
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
//! use lettre::{
//! message::header::ContentType,
//! transport::smtp::authentication::{Credentials, Mechanism},
//! transport::smtp::{
//! authentication::{Credentials, Mechanism},
//! PoolConfig,
//! },
//! Message, SmtpTransport, Transport,
//! };
//!
@@ -45,39 +71,35 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .header(ContentType::TEXT_PLAIN)
//! .body(String::from("Be happy!"))?;
//!
//! // Create the SMTPS transport
//! let sender = SmtpTransport::relay("smtp.example.com")?
//! // Create TLS transport on port 587 with STARTTLS
//! let sender = SmtpTransport::starttls_relay("smtp.example.com")?
//! // Add credentials for authentication
//! .credentials(Credentials::new(
//! "username".to_owned(),
//! "password".to_owned(),
//! ))
//! // Optionally configure expected authentication mechanism
//! // Configure expected authentication mechanism
//! .authentication(vec![Mechanism::Plain])
//! // Connection pool settings
//! .pool_config(PoolConfig::new().max_size(20))
//! .build();
//!
//! // Send the email via remote relay
//! sender.send(&email)?;
//! let result = sender.send(&email);
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//! ```
//!
//! #### Shortening configuration
//!
//! It can be very repetitive to ask the user for every SMTP connection parameter.
//! In some cases this can be simplified by using a connection URI instead.
//!
//! For more information take a look at [`SmtpTransport::from_url`] or [`AsyncSmtpTransport::from_url`].
//! You can specify custom TLS settings:
//!
//! ```rust,no_run
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
//! use lettre::{
//! message::header::ContentType,
//! transport::smtp::authentication::{Credentials, Mechanism},
//! transport::smtp::client::{Tls, TlsParameters},
//! Message, SmtpTransport, Transport,
//! };
//!
@@ -86,106 +108,25 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .header(ContentType::TEXT_PLAIN)
//! .body(String::from("Be happy!"))?;
//!
//! // Create the SMTPS transport
//! let sender = SmtpTransport::from_url("smtps://username:password@smtp.example.com")?.build();
//!
//! // Send the email via remote relay
//! sender.send(&email)?;
//! # Ok(())
//! # }
//! ```
//!
//! #### Advanced configuration with custom TLS settings
//!
//! ```rust,no_run
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
//! use std::fs;
//!
//! use lettre::{
//! message::header::ContentType,
//! transport::smtp::client::{Certificate, Tls, TlsParameters},
//! Message, SmtpTransport, Transport,
//! };
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .header(ContentType::TEXT_PLAIN)
//! .body(String::from("Be happy!"))?;
//!
//! // Custom TLS configuration - Use a self signed certificate
//! let cert = fs::read("self-signed.crt")?;
//! let cert = Certificate::from_pem(&cert)?;
//! let tls = TlsParameters::builder(/* TLS SNI value */ "smtp.example.com".to_owned())
//! .add_root_certificate(cert)
//! // Custom TLS configuration
//! let tls = TlsParameters::builder("smtp.example.com".to_owned())
//! .dangerous_accept_invalid_certs(true)
//! .build()?;
//!
//! // Create the SMTPS transport
//! // Create TLS transport on port 465
//! let sender = SmtpTransport::relay("smtp.example.com")?
//! .tls(Tls::Wrapper(tls))
//! // Custom TLS configuration
//! .tls(Tls::Required(tls))
//! .build();
//!
//! // Send the email via remote relay
//! sender.send(&email)?;
//! let result = sender.send(&email);
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//! ```
//!
//! #### Connection pooling
//!
//! [`SmtpTransport`] and [`AsyncSmtpTransport`] store connections in
//! a connection pool by default. This avoids connecting and disconnecting
//! from the relay server for every message the application tries to send. For the connection pool
//! to work the instance of the transport **must** be reused.
//! In a webserver context it may go about this:
//!
//! ```rust,no_run
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
//! # fn test() {
//! use lettre::{
//! message::header::ContentType,
//! transport::smtp::{authentication::Credentials, PoolConfig},
//! Message, SmtpTransport, Transport,
//! };
//! #
//! # type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
//!
//! /// The global application state
//! #[derive(Debug)]
//! struct AppState {
//! smtp: SmtpTransport,
//! // ... other global application parameters
//! }
//!
//! impl AppState {
//! pub fn new(smtp_url: &str) -> Result<Self> {
//! let smtp = SmtpTransport::from_url(smtp_url)?.build();
//! Ok(Self { smtp })
//! }
//! }
//!
//! fn handle_request(app_state: &AppState) -> Result<String> {
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .header(ContentType::TEXT_PLAIN)
//! .body(String::from("Be happy!"))?;
//!
//! // Send the email via remote relay
//! app_state.smtp.send(&email)?;
//!
//! Ok("The email has successfully been sent!".to_owned())
//! }
//! # }
//! ```
use std::time::Duration;

View File

@@ -78,7 +78,7 @@ impl<E: Executor> Pool<E> {
#[cfg(feature = "tracing")]
let mut created = 0;
for _ in count..(min_idle as usize) {
for _ in count..=(min_idle as usize) {
let conn = match pool.client.connection().await {
Ok(conn) => conn,
Err(err) => {
@@ -109,7 +109,7 @@ impl<E: Executor> Pool<E> {
#[cfg(feature = "tracing")]
tracing::debug!("dropped {} idle connections", dropped.len());
abort_concurrent(dropped.into_iter().map(ParkedConnection::unpark))
abort_concurrent(dropped.into_iter().map(|conn| conn.unpark()))
.await;
}
}
@@ -229,7 +229,7 @@ impl<E: Executor> Drop for Pool<E> {
handle.shutdown().await;
}
abort_concurrent(connections.into_iter().map(ParkedConnection::unpark)).await;
abort_concurrent(connections.into_iter().map(|conn| conn.unpark())).await;
});
}
}

View File

@@ -72,7 +72,7 @@ impl Pool {
#[cfg(feature = "tracing")]
let mut created = 0;
for _ in count..(min_idle as usize) {
for _ in count..=(min_idle as usize) {
let conn = match pool.client.connection() {
Ok(conn) => conn,
Err(err) => {
@@ -109,7 +109,6 @@ impl Pool {
}
}
drop(pool);
thread::sleep(idle_timeout);
}
})

View File

@@ -11,31 +11,7 @@ use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, S
use super::{Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
use crate::{address::Envelope, Transport};
/// Synchronously send emails using the SMTP protocol
///
/// `SmtpTransport` is the primary way for communicating
/// with SMTP relay servers to send email messages. It holds the
/// client connect configuration and creates new connections
/// as necessary.
///
/// # Connection pool
///
/// When the `pool` feature is enabled (default), `SmtpTransport` maintains a
/// connection pool to manage SMTP connections. The pool:
///
/// - Establishes a new connection when sending a message.
/// - Recycles connections internally after a message is sent.
/// - Reuses connections for subsequent messages, reducing connection setup overhead.
///
/// The connection pool can grow to hold multiple SMTP connections if multiple
/// emails are sent concurrently, as SMTP does not support multiplexing within a
/// single connection.
///
/// However, **connection reuse is not possible** if the `SmtpTransport` instance
/// is dropped after every email send operation. You must reuse the instance
/// of this struct for the connection pool to be of any use.
///
/// To customize connection pool settings, use [`SmtpTransportBuilder::pool_config`].
/// Sends emails using the SMTP protocol
#[cfg_attr(docsrs, doc(cfg(feature = "smtp-transport")))]
#[derive(Clone)]
pub struct SmtpTransport {
@@ -56,7 +32,7 @@ impl Transport for SmtpTransport {
let result = conn.send(envelope, email)?;
#[cfg(not(feature = "pool"))]
conn.abort();
conn.quit()?;
Ok(result)
}
@@ -139,72 +115,54 @@ impl SmtpTransport {
/// Creates a `SmtpTransportBuilder` from a connection URL
///
/// The protocol, credentials, host, port and EHLO name can be provided
/// in a single URL. This may be simpler than having to configure SMTP
/// through multiple configuration parameters and then having to pass
/// those options to lettre.
/// The protocol, credentials, host and port can be provided in a single URL.
/// Use the scheme `smtp` for an unencrypted relay (optionally in combination with the
/// `tls` parameter to allow/require STARTTLS) or `smtps` for SMTP over TLS.
/// The path section of the url can be used to set an alternative name for
/// the HELO / EHLO command.
/// For example `smtps://username:password@smtp.example.com/client.example.com:465`
/// will set the HELO / EHLO name `client.example.com`.
///
/// The URL is created in the following way:
/// `scheme://user:pass@hostname:port/ehlo-name?tls=TLS`.
///
/// `user` (Username) and `pass` (Password) are optional in case the
/// SMTP relay doesn't require authentication. When `port` is not
/// configured it is automatically determined based on the `scheme`.
/// `ehlo-name` optionally overwrites the hostname sent for the EHLO
/// command. `TLS` controls whether STARTTLS is simply enabled
/// (`opportunistic` - not enough to prevent man-in-the-middle attacks)
/// or `required` (require the server to upgrade the connection to
/// STARTTLS, otherwise fail on suspicion of main-in-the-middle attempt).
///
/// Use the following table to construct your SMTP url:
///
/// | scheme | `tls` query parameter | example | default port | remarks |
/// | ------- | --------------------- | -------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
/// | `smtps` | unset | `smtps://user:pass@hostname:port` | 465 | SMTP over TLS, recommended method |
/// | `smtp` | `required` | `smtp://user:pass@hostname:port?tls=required` | 587 | SMTP with STARTTLS required, when SMTP over TLS is not available |
/// | `smtp` | `opportunistic` | `smtp://user:pass@hostname:port?tls=opportunistic` | 587 | SMTP with optionally STARTTLS when supported by the server. Not suitable for production use: vulnerable to a man-in-the-middle attack |
/// | `smtp` | unset | `smtp://user:pass@hostname:port` | 587 | Always unencrypted SMTP. Not suitable for production use: sends all data unencrypted |
///
/// IMPORTANT: some parameters like `user` and `pass` cannot simply
/// be concatenated to construct the final URL because special characters
/// contained within the parameter may confuse the URL decoder.
/// Manually URL encode the parameters before concatenating them or use
/// a proper URL encoder, like the following cargo script:
///
/// ```rust
/// # let _ = r#"
/// #!/usr/bin/env cargo
///
/// //! ```cargo
/// //! [dependencies]
/// //! url = "2"
/// //! ```
/// # "#;
///
/// use url::Url;
///
/// fn main() {
/// // don't touch this line
/// let mut url = Url::parse("foo://bar").unwrap();
///
/// // configure the scheme (`smtp` or `smtps`) here.
/// url.set_scheme("smtps").unwrap();
/// // configure the username and password.
/// // remove the following two lines if unauthenticated.
/// url.set_username("username").unwrap();
/// url.set_password(Some("password")).unwrap();
/// // configure the hostname
/// url.set_host(Some("smtp.example.com")).unwrap();
/// // configure the port - only necessary if using a non-default port
/// url.set_port(Some(465)).unwrap();
/// // configure the EHLO name
/// url.set_path("ehlo-name");
///
/// println!("{url}");
/// }
/// ```
///
/// The connection URL can then be used in the following way:
/// <table>
/// <thead>
/// <tr>
/// <th>scheme</th>
/// <th>tls parameter</th>
/// <th>example</th>
/// <th>remarks</th>
/// </tr>
/// </thead>
/// <tbody>
/// <tr>
/// <td>smtps</td>
/// <td>-</td>
/// <td>smtps://smtp.example.com</td>
/// <td>SMTP over TLS, recommended method</td>
/// </tr>
/// <tr>
/// <td>smtp</td>
/// <td>required</td>
/// <td>smtp://smtp.example.com?tls=required</td>
/// <td>SMTP with STARTTLS required, when SMTP over TLS is not available</td>
/// </tr>
/// <tr>
/// <td>smtp</td>
/// <td>opportunistic</td>
/// <td>smtp://smtp.example.com?tls=opportunistic</td>
/// <td>
/// SMTP with optionally STARTTLS when supported by the server.
/// Caution: this method is vulnerable to a man-in-the-middle attack.
/// Not recommended for production use.
/// </td>
/// </tr>
/// <tr>
/// <td>smtp</td>
/// <td>-</td>
/// <td>smtp://smtp.example.com</td>
/// <td>Unencrypted SMTP, not recommended for production use.</td>
/// </tr>
/// </tbody>
/// </table>
///
/// ```rust,no_run
/// use lettre::{
@@ -212,7 +170,6 @@ impl SmtpTransport {
/// SmtpTransport, Transport,
/// };
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let email = Message::builder()
/// .from("NoBody <nobody@domain.tld>".parse().unwrap())
/// .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
@@ -223,12 +180,15 @@ impl SmtpTransport {
/// .unwrap();
///
/// // Open a remote connection to example
/// let mailer = SmtpTransport::from_url("smtps://username:password@smtp.example.com")?.build();
/// let mailer = SmtpTransport::from_url("smtps://username:password@smtp.example.com:465")
/// .unwrap()
/// .build();
///
/// // Send the email
/// mailer.send(&email)?;
/// # Ok(())
/// # }
/// match mailer.send(&email) {
/// Ok(_) => println!("Email sent successfully!"),
/// Err(e) => panic!("Could not send email: {e:?}"),
/// }
/// ```
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(
@@ -286,7 +246,7 @@ impl SmtpTransportBuilder {
self
}
/// Set the authentication credentials to use
/// Set the authentication mechanism to use
pub fn credentials(mut self, credentials: Credentials) -> Self {
self.info.credentials = Some(credentials);
self
@@ -305,34 +265,12 @@ impl SmtpTransportBuilder {
}
/// Set the port to use
///
/// # ⚠️⚠️⚠️ You probably don't need to call this method ⚠️⚠️⚠️
///
/// lettre usually picks the correct `port` when building
/// [`SmtpTransport`] using [`SmtpTransport::relay`] or
/// [`SmtpTransport::starttls_relay`].
///
/// # Errors
///
/// Using the incorrect `port` and [`Self::tls`] combination may
/// lead to hard to debug IO errors coming from the TLS library.
pub fn port(mut self, port: u16) -> Self {
self.info.port = port;
self
}
/// Set the TLS settings to use
///
/// # ⚠️⚠️⚠️ You probably don't need to call this method ⚠️⚠️⚠️
///
/// By default lettre chooses the correct `tls` configuration when
/// building [`SmtpTransport`] using [`SmtpTransport::relay`] or
/// [`SmtpTransport::starttls_relay`].
///
/// # Errors
///
/// Using the wrong [`Tls`] and [`Self::port`] combination may
/// lead to hard to debug IO errors coming from the TLS library.
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,

View File

@@ -6,7 +6,7 @@ use std::fmt::{Display, Formatter, Result as FmtResult};
#[derive(Debug)]
pub struct XText<'a>(pub &'a str);
impl Display for XText<'_> {
impl<'a> Display for XText<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let mut rest = self.0;
while let Some(idx) = rest.find(|c| c < '!' || c == '+' || c == '=') {
@@ -38,7 +38,9 @@ mod tests {
("bjørn", "bjørn"),
("Ø+= ❤️‰", "Ø+2B+3D+20❤"),
("+", "+2B"),
] {
]
.iter()
{
assert_eq!(format!("{}", XText(input)), (*expect).to_owned());
}
}

View File

@@ -11,9 +11,7 @@
//! ```rust
//! # #[cfg(feature = "builder")]
//! # {
//! use lettre::{
//! message::header::ContentType, transport::stub::StubTransport, Message, Transport,
//! };
//! use lettre::{transport::stub::StubTransport, Message, Transport};
//!
//! # use std::error::Error;
//! # fn try_main() -> Result<(), Box<dyn Error>> {
@@ -22,11 +20,11 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .header(ContentType::TEXT_PLAIN)
//! .body(String::from("Be happy!"))?;
//!
//! let mut sender = StubTransport::new_ok();
//! sender.send(&email)?;
//! let result = sender.send(&email);
//! assert!(result.is_ok());
//! assert_eq!(
//! sender.messages(),
//! vec![(