Compare commits
22 Commits
smtp-error
...
v0.11.12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fb89e23ad | ||
|
|
097f7d5aaa | ||
|
|
32e066464a | ||
|
|
55c7f57f25 | ||
|
|
3f7a57a417 | ||
|
|
bb64baec67 | ||
|
|
5f13636b49 | ||
|
|
4513e602d6 | ||
|
|
382e15013a | ||
|
|
3ce31c5a6a | ||
|
|
a48cf92a5b | ||
|
|
43f6f139d2 | ||
|
|
fd1425666d | ||
|
|
de075153b0 | ||
|
|
02dfc7dd4a | ||
|
|
83ce5872d7 | ||
|
|
272efeca74 | ||
|
|
ec6f5f3920 | ||
|
|
b62d23bd87 | ||
|
|
51794aa912 | ||
|
|
eb42651401 | ||
|
|
99c6dc2a87 |
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -75,8 +75,8 @@ jobs:
|
||||
rust: stable
|
||||
- name: beta
|
||||
rust: beta
|
||||
- name: '1.70'
|
||||
rust: '1.70'
|
||||
- name: '1.71'
|
||||
rust: '1.71'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
64
CHANGELOG.md
64
CHANGELOG.md
@@ -1,3 +1,65 @@
|
||||
<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)
|
||||
|
||||
@@ -508,6 +570,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
|
||||
|
||||
460
Cargo.lock
generated
460
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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.9"
|
||||
version = "0.11.12"
|
||||
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.70"
|
||||
rust-version = "1.71"
|
||||
|
||||
[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.5", optional = true } # feature
|
||||
native-tls = { version = "0.2.9", 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 }
|
||||
|
||||
10
README.md
10
README.md
@@ -28,8 +28,8 @@
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://deps.rs/crate/lettre/0.11.9">
|
||||
<img src="https://deps.rs/crate/lettre/0.11.9/status.svg"
|
||||
<a href="https://deps.rs/crate/lettre/0.11.12">
|
||||
<img src="https://deps.rs/crate/lettre/0.11.12/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.70, but this could change at any time either from
|
||||
the minimum supported Rust version is 1.71, 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.70 or newer.
|
||||
This library requires Rust 1.71 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`.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use lettre::{Message, SmtpTransport, Transport};
|
||||
use lettre::{message::header::ContentType, Message, SmtpTransport, Transport};
|
||||
|
||||
fn bench_simple_send(c: &mut Criterion) {
|
||||
let sender = SmtpTransport::builder_dangerous("127.0.0.1")
|
||||
@@ -13,6 +13,7 @@ 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));
|
||||
@@ -32,6 +33,7 @@ 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));
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -36,7 +36,7 @@ impl<'de> Deserialize<'de> for Address {
|
||||
{
|
||||
struct FieldVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for FieldVisitor {
|
||||
impl Visitor<'_> for FieldVisitor {
|
||||
type Value = Field;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
|
||||
|
||||
27
src/lib.rs
27
src/lib.rs
@@ -6,7 +6,7 @@
|
||||
//! * Secure defaults
|
||||
//! * Async support
|
||||
//!
|
||||
//! Lettre requires Rust 1.70 or newer.
|
||||
//! Lettre requires Rust 1.71 or newer.
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
@@ -34,13 +34,25 @@
|
||||
//!
|
||||
//! _Secure SMTP connections using TLS from the `native-tls` crate_
|
||||
//!
|
||||
//! Uses schannel on Windows, Security-Framework on macOS, and OpenSSL on Linux.
|
||||
//! Uses schannel on Windows, Security-Framework on macOS, and OpenSSL
|
||||
//! on all other platforms.
|
||||
//!
|
||||
//! * **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_
|
||||
@@ -109,7 +121,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.9")]
|
||||
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.12")]
|
||||
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
|
||||
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -137,7 +149,14 @@
|
||||
clippy::wildcard_imports,
|
||||
clippy::str_to_string,
|
||||
clippy::empty_structs_with_brackets,
|
||||
clippy::zero_sized_map_values
|
||||
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,
|
||||
)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ impl Display for DkimSigningAlgorithm {
|
||||
}
|
||||
}
|
||||
|
||||
/// Describe DkimSigning key error
|
||||
/// Describe [`DkimSigningKey`] 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,10 +344,9 @@ 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) {
|
||||
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) {
|
||||
@@ -378,7 +377,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(|h| h.as_ref()),
|
||||
dkim_config.headers.iter().map(AsRef::as_ref),
|
||||
headers,
|
||||
dkim_config.canonicalization.header,
|
||||
);
|
||||
@@ -488,14 +487,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]
|
||||
|
||||
@@ -119,7 +119,7 @@ mod serde {
|
||||
{
|
||||
struct ContentTypeVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ContentTypeVisitor {
|
||||
impl Visitor<'_> for ContentTypeVisitor {
|
||||
type Value = ContentType;
|
||||
|
||||
// The error message which states what the Visitor expects to
|
||||
|
||||
@@ -110,7 +110,7 @@ mailbox_header! {
|
||||
|
||||
`Sender` header
|
||||
|
||||
This header contains [`Mailbox`][self::Mailbox] associated with sender.
|
||||
This header contains [`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`][self::Mailboxes].
|
||||
This header contains [`Mailboxes`].
|
||||
|
||||
*/
|
||||
(From, "From")
|
||||
@@ -135,7 +135,7 @@ mailboxes_header! {
|
||||
|
||||
`Reply-To` header
|
||||
|
||||
This header contains [`Mailboxes`][self::Mailboxes].
|
||||
This header contains [`Mailboxes`].
|
||||
|
||||
*/
|
||||
(ReplyTo, "Reply-To")
|
||||
@@ -146,7 +146,7 @@ mailboxes_header! {
|
||||
|
||||
`To` header
|
||||
|
||||
This header contains [`Mailboxes`][self::Mailboxes].
|
||||
This header contains [`Mailboxes`].
|
||||
|
||||
*/
|
||||
(To, "To")
|
||||
@@ -157,7 +157,7 @@ mailboxes_header! {
|
||||
|
||||
`Cc` header
|
||||
|
||||
This header contains [`Mailboxes`][self::Mailboxes].
|
||||
This header contains [`Mailboxes`].
|
||||
|
||||
*/
|
||||
(Cc, "Cc")
|
||||
@@ -168,7 +168,7 @@ mailboxes_header! {
|
||||
|
||||
`Bcc` header
|
||||
|
||||
This header contains [`Mailboxes`][self::Mailboxes].
|
||||
This header contains [`Mailboxes`].
|
||||
|
||||
*/
|
||||
(Bcc, "Bcc")
|
||||
|
||||
@@ -415,7 +415,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn empty_headername() {
|
||||
assert!(HeaderName::new_from_ascii(String::from("")).is_err());
|
||||
assert!(HeaderName::new_from_ascii("".to_owned()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -36,7 +36,7 @@ impl<'de> Deserialize<'de> for Mailbox {
|
||||
{
|
||||
struct FieldVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for FieldVisitor {
|
||||
impl Visitor<'_> for FieldVisitor {
|
||||
type Value = Field;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
|
||||
|
||||
@@ -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("".into()), "kayo@example.com".parse().unwrap())
|
||||
Mailbox::new(Some("".to_owned()), "kayo@example.com".parse().unwrap())
|
||||
),
|
||||
"kayo@example.com"
|
||||
);
|
||||
|
||||
@@ -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`][self::MultiPart])
|
||||
/// Create message using mime body ([`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`][self::SinglePart])
|
||||
/// Create message using mime body ([`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,7 +537,10 @@ impl Message {
|
||||
/// Example:
|
||||
/// ```rust
|
||||
/// use lettre::{
|
||||
/// message::dkim::{DkimConfig, DkimSigningAlgorithm, DkimSigningKey},
|
||||
/// message::{
|
||||
/// dkim::{DkimConfig, DkimSigningAlgorithm, DkimSigningKey},
|
||||
/// header::ContentType,
|
||||
/// },
|
||||
/// Message,
|
||||
/// };
|
||||
///
|
||||
@@ -546,6 +549,7 @@ 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-----
|
||||
@@ -601,7 +605,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -765,7 +769,7 @@ mod test {
|
||||
continue;
|
||||
}
|
||||
|
||||
assert_eq!(line.0, line.1)
|
||||
assert_eq!(line.0, line.1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}")?;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! use std::env::temp_dir;
|
||||
//!
|
||||
//! use lettre::{FileTransport, Message, Transport};
|
||||
//! use lettre::{message::header::ContentType, 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!"))?;
|
||||
//!
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! sender.send(&email)?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//!
|
||||
@@ -44,7 +44,7 @@
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! use std::env::temp_dir;
|
||||
//!
|
||||
//! use lettre::{FileTransport, Message, Transport};
|
||||
//! use lettre::{message::header::ContentType, 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!"))?;
|
||||
//!
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! sender.send(&email)?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//!
|
||||
@@ -73,7 +73,9 @@
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use std::env::temp_dir;
|
||||
//!
|
||||
//! use lettre::{AsyncFileTransport, AsyncTransport, Message, Tokio1Executor};
|
||||
//! use lettre::{
|
||||
//! message::header::ContentType, AsyncFileTransport, AsyncTransport, Message, Tokio1Executor,
|
||||
//! };
|
||||
//!
|
||||
//! // Write to the local temp directory
|
||||
//! let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir());
|
||||
@@ -82,10 +84,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!"))?;
|
||||
//!
|
||||
//! let result = sender.send(email).await;
|
||||
//! assert!(result.is_ok());
|
||||
//! sender.send(email).await?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
@@ -99,7 +101,10 @@
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use std::env::temp_dir;
|
||||
//!
|
||||
//! use lettre::{AsyncFileTransport, AsyncStd1Executor, AsyncTransport, Message};
|
||||
//! use lettre::{
|
||||
//! message::header::ContentType, AsyncFileTransport, AsyncStd1Executor, AsyncTransport,
|
||||
//! Message,
|
||||
//! };
|
||||
//!
|
||||
//! // Write to the local temp directory
|
||||
//! let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir());
|
||||
@@ -108,10 +113,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!"))?;
|
||||
//!
|
||||
//! let result = sender.send(email).await;
|
||||
//! assert!(result.is_ok());
|
||||
//! sender.send(email).await?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
@@ -125,6 +130,7 @@
|
||||
//! 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!
|
||||
|
||||
@@ -56,13 +56,17 @@
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "builder", feature = "smtp-transport"))]
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
|
||||
//! use lettre::{
|
||||
//! message::header::ContentType, 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());
|
||||
|
||||
@@ -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}")?;
|
||||
|
||||
@@ -7,18 +7,18 @@
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "sendmail-transport", feature = "builder"))]
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{Message, SendmailTransport, Transport};
|
||||
//! use lettre::{message::header::ContentType, 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();
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! sender.send(&email)?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//!
|
||||
@@ -34,7 +34,8 @@
|
||||
//! # #[cfg(all(feature = "tokio1", feature = "sendmail-transport", feature = "builder"))]
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{
|
||||
//! AsyncSendmailTransport, AsyncTransport, Message, SendmailTransport, Tokio1Executor,
|
||||
//! message::header::ContentType, AsyncSendmailTransport, AsyncTransport, Message,
|
||||
//! SendmailTransport, Tokio1Executor,
|
||||
//! };
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
@@ -42,11 +43,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();
|
||||
//! let result = sender.send(email).await;
|
||||
//! assert!(result.is_ok());
|
||||
//! sender.send(email).await?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
@@ -58,18 +59,17 @@
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "async-std1", feature = "sendmail-transport", feature = "builder"))]
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{Message, AsyncTransport, AsyncStd1Executor, AsyncSendmailTransport};
|
||||
//! use lettre::{Message, AsyncTransport, AsyncStd1Executor,message::header::ContentType, 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")
|
||||
//! .subject("Happy new year").header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let sender = AsyncSendmailTransport::<AsyncStd1Executor>::new();
|
||||
//! let result = sender.send(email).await;
|
||||
//! assert!(result.is_ok());
|
||||
//! sender.send(email).await?;
|
||||
//! # 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(),
|
||||
|
||||
@@ -12,6 +12,12 @@ 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,
|
||||
};
|
||||
@@ -24,6 +30,30 @@ 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")]
|
||||
@@ -45,7 +75,7 @@ impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> {
|
||||
let result = conn.send(envelope, email).await?;
|
||||
|
||||
#[cfg(not(feature = "pool"))]
|
||||
conn.quit().await?;
|
||||
conn.abort().await;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -161,54 +191,72 @@ where
|
||||
|
||||
/// Creates a `AsyncSmtpTransportBuilder` from a connection URL
|
||||
///
|
||||
/// 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 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.
|
||||
///
|
||||
/// <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>
|
||||
/// 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:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use lettre::{
|
||||
@@ -232,15 +280,11 @@ where
|
||||
/// let mailer: AsyncSmtpTransport<Tokio1Executor> =
|
||||
/// AsyncSmtpTransport::<Tokio1Executor>::from_url(
|
||||
/// "smtps://username:password@smtp.example.com:465",
|
||||
/// )
|
||||
/// .unwrap()
|
||||
/// )?
|
||||
/// .build();
|
||||
///
|
||||
/// // Send the email
|
||||
/// match mailer.send(email).await {
|
||||
/// Ok(_) => println!("Email sent successfully!"),
|
||||
/// Err(e) => panic!("Could not send email: {e:?}"),
|
||||
/// }
|
||||
/// mailer.send(email).await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
@@ -323,7 +367,7 @@ impl AsyncSmtpTransportBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication mechanism to use
|
||||
/// Set the authentication credentials to use
|
||||
pub fn credentials(mut self, credentials: Credentials) -> Self {
|
||||
self.info.credentials = Some(credentials);
|
||||
self
|
||||
@@ -336,6 +380,17 @@ 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
|
||||
@@ -348,6 +403,17 @@ 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",
|
||||
@@ -361,7 +427,7 @@ impl AsyncSmtpTransportBuilder {
|
||||
feature = "async-std1-rustls-tls"
|
||||
)))
|
||||
)]
|
||||
pub fn tls(mut self, tls: super::Tls) -> Self {
|
||||
pub fn tls(mut self, tls: Tls) -> Self {
|
||||
self.info.tls = tls;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -98,13 +98,17 @@ impl Mechanism {
|
||||
let decoded_challenge = challenge
|
||||
.ok_or_else(|| error::client("This mechanism does expect a challenge"))?;
|
||||
|
||||
if ["User Name", "Username:", "Username", "User Name\0"]
|
||||
.contains(&decoded_challenge)
|
||||
{
|
||||
if contains_ignore_ascii_case(
|
||||
decoded_challenge,
|
||||
["User Name", "Username:", "Username", "User Name\0"],
|
||||
) {
|
||||
return Ok(credentials.authentication_identity.clone());
|
||||
}
|
||||
|
||||
if ["Password", "Password:", "Password\0"].contains(&decoded_challenge) {
|
||||
if contains_ignore_ascii_case(
|
||||
decoded_challenge,
|
||||
["Password", "Password:", "Password\0"],
|
||||
) {
|
||||
return Ok(credentials.secret.clone());
|
||||
}
|
||||
|
||||
@@ -121,6 +125,15 @@ 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};
|
||||
@@ -155,6 +168,23 @@ 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;
|
||||
|
||||
@@ -86,8 +86,7 @@ impl AsyncSmtpConnection {
|
||||
/// Some(TlsParameters::new("example.com".to_owned())?),
|
||||
/// None,
|
||||
/// )
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
/// .await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
|
||||
@@ -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,9 +270,8 @@ impl AsyncNetworkStream {
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
|
||||
// get owned TcpStream
|
||||
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
|
||||
let tcp_stream = match tcp_stream {
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) => tcp_stream,
|
||||
_ => unreachable!(),
|
||||
let InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) = tcp_stream else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
self.inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters)
|
||||
@@ -290,9 +289,8 @@ impl AsyncNetworkStream {
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
|
||||
// get owned TcpStream
|
||||
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
|
||||
let tcp_stream = match tcp_stream {
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream) => tcp_stream,
|
||||
_ => unreachable!(),
|
||||
let InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream) = tcp_stream else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
self.inner = Self::upgrade_asyncstd1_tls(tcp_stream, tls_parameters)
|
||||
|
||||
@@ -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,9 +160,8 @@ impl NetworkStream {
|
||||
InnerNetworkStream::Tcp(_) => {
|
||||
// get owned TcpStream
|
||||
let tcp_stream = mem::replace(&mut self.inner, InnerNetworkStream::None);
|
||||
let tcp_stream = match tcp_stream {
|
||||
InnerNetworkStream::Tcp(tcp_stream) => tcp_stream,
|
||||
_ => unreachable!(),
|
||||
let InnerNetworkStream::Tcp(tcp_stream) = tcp_stream else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
self.inner = Self::upgrade_tls_impl(tcp_stream, tls_parameters)?;
|
||||
@@ -369,7 +368,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,
|
||||
|
||||
@@ -59,27 +59,61 @@ pub enum TlsVersion {
|
||||
Tlsv13,
|
||||
}
|
||||
|
||||
/// How to apply TLS to a client connection
|
||||
/// 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.
|
||||
#[derive(Clone)]
|
||||
#[allow(missing_copy_implementations)]
|
||||
pub enum Tls {
|
||||
/// Insecure connection only (for testing purposes)
|
||||
/// 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.
|
||||
None,
|
||||
/// Start with insecure connection and use `STARTTLS` when available
|
||||
/// 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.
|
||||
#[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),
|
||||
/// Start with insecure connection and require `STARTTLS`
|
||||
/// 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.
|
||||
#[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),
|
||||
/// Use TLS wrapped connection
|
||||
/// 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.
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
@@ -389,7 +423,7 @@ impl TlsParametersBuilder {
|
||||
let mut root_cert_store = RootCertStore::empty();
|
||||
|
||||
#[cfg(feature = "rustls-native-certs")]
|
||||
fn load_native_roots(store: &mut RootCertStore) -> Result<(), Error> {
|
||||
fn load_native_roots(store: &mut RootCertStore) {
|
||||
let rustls_native_certs::CertificateResult { certs, errors, .. } =
|
||||
rustls_native_certs::load_native_certs();
|
||||
let errors_len = errors.len();
|
||||
@@ -399,7 +433,6 @@ 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")]
|
||||
@@ -410,7 +443,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);
|
||||
}
|
||||
@@ -628,10 +661,11 @@ impl Identity {
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
fn from_pem_rustls_tls(
|
||||
pem: &[u8],
|
||||
key: &[u8],
|
||||
mut key: &[u8],
|
||||
) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>), Error> {
|
||||
let mut key = key;
|
||||
let key = rustls_pemfile::private_key(&mut key).unwrap().unwrap();
|
||||
let key = rustls_pemfile::private_key(&mut key)
|
||||
.map_err(error::tls)?
|
||||
.ok_or_else(|| error::tls("no private key found"))?;
|
||||
Ok((vec![pem.to_owned().into()], key))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use url::Url;
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
@@ -62,7 +64,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
|
||||
@@ -84,26 +86,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 {
|
||||
@@ -115,7 +117,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| cow.into_owned())
|
||||
.map(Cow::into_owned)
|
||||
.map_err(error::connection)
|
||||
};
|
||||
let credentials = Credentials::new(
|
||||
|
||||
@@ -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}")?;
|
||||
|
||||
@@ -129,9 +129,8 @@ impl Display for ServerInfo {
|
||||
impl ServerInfo {
|
||||
/// Parses a EHLO response to create a `ServerInfo`
|
||||
pub fn from_response(response: &Response) -> Result<ServerInfo, Error> {
|
||||
let name = match response.first_word() {
|
||||
Some(name) => name,
|
||||
None => return Err(error::response("Could not read server name")),
|
||||
let Some(name) = response.first_word() else {
|
||||
return Err(error::response("Could not read server name"));
|
||||
};
|
||||
|
||||
let mut features: HashSet<Extension> = HashSet::new();
|
||||
@@ -169,7 +168,7 @@ impl ServerInfo {
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ServerInfo {
|
||||
|
||||
@@ -26,43 +26,17 @@
|
||||
//!
|
||||
//! The relay server can be the local email server, a specific host or a third-party service.
|
||||
//!
|
||||
//! #### Simple example
|
||||
//! #### Simple example with authentication
|
||||
//!
|
||||
//! 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:
|
||||
//! A good starting point for sending emails via SMTP relay is to
|
||||
//! do the following:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
|
||||
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! use lettre::{
|
||||
//! transport::smtp::{
|
||||
//! authentication::{Credentials, Mechanism},
|
||||
//! PoolConfig,
|
||||
//! },
|
||||
//! message::header::ContentType,
|
||||
//! transport::smtp::authentication::{Credentials, Mechanism},
|
||||
//! Message, SmtpTransport, Transport,
|
||||
//! };
|
||||
//!
|
||||
@@ -71,35 +45,39 @@
|
||||
//! .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 TLS transport on port 587 with STARTTLS
|
||||
//! let sender = SmtpTransport::starttls_relay("smtp.example.com")?
|
||||
//! // Create the SMTPS transport
|
||||
//! let sender = SmtpTransport::relay("smtp.example.com")?
|
||||
//! // Add credentials for authentication
|
||||
//! .credentials(Credentials::new(
|
||||
//! "username".to_owned(),
|
||||
//! "password".to_owned(),
|
||||
//! ))
|
||||
//! // Configure expected authentication mechanism
|
||||
//! // Optionally 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
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! sender.send(&email)?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! You can specify custom TLS settings:
|
||||
//! #### 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`].
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
|
||||
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! use lettre::{
|
||||
//! transport::smtp::client::{Tls, TlsParameters},
|
||||
//! message::header::ContentType,
|
||||
//! transport::smtp::authentication::{Credentials, Mechanism},
|
||||
//! Message, SmtpTransport, Transport,
|
||||
//! };
|
||||
//!
|
||||
@@ -108,25 +86,106 @@
|
||||
//! .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
|
||||
//! let tls = TlsParameters::builder("smtp.example.com".to_owned())
|
||||
//! .dangerous_accept_invalid_certs(true)
|
||||
//! // 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)
|
||||
//! .build()?;
|
||||
//!
|
||||
//! // Create TLS transport on port 465
|
||||
//! // Create the SMTPS transport
|
||||
//! let sender = SmtpTransport::relay("smtp.example.com")?
|
||||
//! // Custom TLS configuration
|
||||
//! .tls(Tls::Required(tls))
|
||||
//! .tls(Tls::Wrapper(tls))
|
||||
//! .build();
|
||||
//!
|
||||
//! // Send the email via remote relay
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! sender.send(&email)?;
|
||||
//! # 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;
|
||||
|
||||
|
||||
@@ -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(|conn| conn.unpark()))
|
||||
abort_concurrent(dropped.into_iter().map(ParkedConnection::unpark))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
@@ -229,7 +229,7 @@ impl<E: Executor> Drop for Pool<E> {
|
||||
handle.shutdown().await;
|
||||
}
|
||||
|
||||
abort_concurrent(connections.into_iter().map(|conn| conn.unpark())).await;
|
||||
abort_concurrent(connections.into_iter().map(ParkedConnection::unpark)).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -11,7 +11,31 @@ use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, S
|
||||
use super::{Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
|
||||
use crate::{address::Envelope, Transport};
|
||||
|
||||
/// Sends emails using the SMTP protocol
|
||||
/// 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`].
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "smtp-transport")))]
|
||||
#[derive(Clone)]
|
||||
pub struct SmtpTransport {
|
||||
@@ -32,7 +56,7 @@ impl Transport for SmtpTransport {
|
||||
let result = conn.send(envelope, email)?;
|
||||
|
||||
#[cfg(not(feature = "pool"))]
|
||||
conn.quit()?;
|
||||
conn.abort();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -115,54 +139,72 @@ impl SmtpTransport {
|
||||
|
||||
/// Creates a `SmtpTransportBuilder` from a connection URL
|
||||
///
|
||||
/// 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 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.
|
||||
///
|
||||
/// <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>
|
||||
/// 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:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use lettre::{
|
||||
@@ -170,6 +212,7 @@ 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())
|
||||
@@ -180,15 +223,12 @@ impl SmtpTransport {
|
||||
/// .unwrap();
|
||||
///
|
||||
/// // Open a remote connection to example
|
||||
/// let mailer = SmtpTransport::from_url("smtps://username:password@smtp.example.com:465")
|
||||
/// .unwrap()
|
||||
/// .build();
|
||||
/// let mailer = SmtpTransport::from_url("smtps://username:password@smtp.example.com")?.build();
|
||||
///
|
||||
/// // Send the email
|
||||
/// match mailer.send(&email) {
|
||||
/// Ok(_) => println!("Email sent successfully!"),
|
||||
/// Err(e) => panic!("Could not send email: {e:?}"),
|
||||
/// }
|
||||
/// mailer.send(&email)?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
@@ -246,7 +286,7 @@ impl SmtpTransportBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication mechanism to use
|
||||
/// Set the authentication credentials to use
|
||||
pub fn credentials(mut self, credentials: Credentials) -> Self {
|
||||
self.info.credentials = Some(credentials);
|
||||
self
|
||||
@@ -265,12 +305,34 @@ 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,
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::fmt::{Display, Formatter, Result as FmtResult};
|
||||
#[derive(Debug)]
|
||||
pub struct XText<'a>(pub &'a str);
|
||||
|
||||
impl<'a> Display for XText<'a> {
|
||||
impl Display for XText<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
let mut rest = self.0;
|
||||
while let Some(idx) = rest.find(|c| c < '!' || c == '+' || c == '=') {
|
||||
@@ -38,9 +38,7 @@ mod tests {
|
||||
("bjørn", "bjørn"),
|
||||
("Ø+= ❤️‰", "Ø+2B+3D+20❤️‰"),
|
||||
("+", "+2B"),
|
||||
]
|
||||
.iter()
|
||||
{
|
||||
] {
|
||||
assert_eq!(format!("{}", XText(input)), (*expect).to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
//! ```rust
|
||||
//! # #[cfg(feature = "builder")]
|
||||
//! # {
|
||||
//! use lettre::{transport::stub::StubTransport, Message, Transport};
|
||||
//! use lettre::{
|
||||
//! message::header::ContentType, transport::stub::StubTransport, Message, Transport,
|
||||
//! };
|
||||
//!
|
||||
//! # use std::error::Error;
|
||||
//! # fn try_main() -> Result<(), Box<dyn Error>> {
|
||||
@@ -20,11 +22,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();
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! sender.send(&email)?;
|
||||
//! assert_eq!(
|
||||
//! sender.messages(),
|
||||
//! vec![(
|
||||
|
||||
Reference in New Issue
Block a user