Compare commits

...

29 Commits

Author SHA1 Message Date
Manuel Pelloni
2ac6a72a8e chore(all): Remove rarely used top level re-exports 2020-10-21 22:08:56 +02:00
Alexis Mousset
f1e86c809d chore(all): Prepare 0.10.0-alpha.3 release 2020-10-21 22:08:56 +02:00
Manuel Pelloni
0b8d5d20ad Refactor address module and move Envelope into it (#488) 2020-10-21 17:44:51 +02:00
Paolo Barbolini
313eb74ea5 Fix MSRV in docs 2020-10-20 09:31:27 +02:00
ghizzo01
6afc078545 Avoid boxing the rustls stream (#486) 2020-10-19 21:21:22 +02:00
Paolo Barbolini
697da9f7db Tokio 0.3 support (#485)
* Tokio 0.3 support

* Tokio 0.3 TLS support

* Tokio 0.3 sendmail transport

* Tokio 0.3 file transport

* Forgotten re-exports

* Tokio 0.3 examples

* fix tokio 0.2 file-transport

* It works
2020-10-19 10:07:36 +02:00
Paolo Barbolini
1ea562b15a Stop exposing internal encoder implementation 2020-10-13 23:52:45 +02:00
Paolo Barbolini
b43c69af47 Allow changing defaults for the connection pool configuration 2020-10-13 09:33:00 +02:00
RotationMatrix
449f317246 Add doc_cfg attributes (#483)
* docs(Cargo.toml): Add `cfg(docsrs)` to `rustdoc` args

* docs(all): Enable doc_cfg crate feature

* docs(transport): Add doc_cfg to Transport traits

* docs(all): Add doc_cfg to public modules

* docs(transport-async): Add doc_cfg to Tokio02Connector

* docs(transport-smtp): Add doc_cfg to Smtp Error

* docs(transport-smtp): Add doc_cfg to TlsParameters
2020-10-13 09:18:41 +02:00
Paolo Barbolini
17d644181a Fix building with no default features 2020-10-13 09:10:46 +02:00
Paolo Barbolini
583df6af18 Fix Rust 2018 Idioms 2020-10-13 09:03:32 +02:00
Paolo Barbolini
ed9ca92de8 Bump our MSRV to 1.45.2 2020-10-13 09:03:32 +02:00
dvermd
0174a29a45 bump base64 version to 0.13.0 2020-10-06 19:23:02 +02:00
Paolo Barbolini
bfd3300df3 Improve comments in smtp selfsigned example 2020-10-04 10:17:09 +02:00
Paolo Barbolini
6526eff5b2 Allow configuring a custom root certificate in TlsParametersBuilder 2020-10-04 10:17:09 +02:00
dvermd
e00eff8b2a Implement From for Credentials 2020-10-02 08:52:56 +02:00
Alexander Jackson
5beef57c18 fix(email): Make doctest rustc-1.40 compatible 2020-09-29 19:48:10 +02:00
Alexander Jackson
b10b04ada8 docs(email): Add doctests/examples
Add doctests/examples for `Address`, `Mailbox`, `Mailboxes` and
`Envelope`.
2020-09-29 19:48:10 +02:00
Paolo Barbolini
30a8797acf Add TlsParametersBuilder with dangerous options 2020-09-23 19:18:24 +02:00
Julien Blatecky
c5fef28ac9 Add Content-ID header for attachments 2020-09-23 19:17:34 +02:00
Paolo Barbolini
bf32554e51 chore: fix unused mut warning 2020-09-09 13:12:26 +02:00
Paolo Barbolini
9983bb53c3 chore(Cargo.toml): try to tidy up the list of features and dependencies 2020-09-09 13:12:26 +02:00
Paolo Barbolini
8e49c60ff8 chore: fix benches
Makes running cargo check --all-features --all-targets work again
2020-09-09 13:12:26 +02:00
Paolo Barbolini
e156520feb clippy: fix warnings in tests
Makes running 'cargo clippy --all-features --tests' work
2020-09-09 13:12:26 +02:00
Paolo Barbolini
ec7d63c8de chore: AsRef is part of Rust's prelude 2020-09-09 13:12:26 +02:00
Paolo Barbolini
e5460c4ba1 Implement Clone for SendmailTransport 2020-09-09 13:12:26 +02:00
Paolo Barbolini
d2912a3e3f fix: Default impl for SendmailTransport 2020-09-09 13:12:26 +02:00
Alexis Mousset
9b3bd00a61 chore(all): Move from lettre.at to lettre.rs 2020-09-09 10:19:39 +02:00
Alexis Mousset
990de687aa chore(all): Prepare 0.10.0-alpha.2 release 2020-09-09 09:53:54 +02:00
47 changed files with 1480 additions and 474 deletions

View File

@@ -11,7 +11,7 @@ jobs:
rust:
- stable
- beta
- 1.40.0
- 1.45.2
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
@@ -37,7 +37,6 @@ jobs:
args: --no-default-features --features=builder,smtp-transport,file-transport,sendmail-transport
- run: rm target/debug/deps/liblettre-*
- uses: actions-rs/cargo@v1
if: matrix.rust != '1.40.0'
with:
command: test
args: --features=async-std1
@@ -45,6 +44,10 @@ jobs:
with:
command: test
args: --features=tokio02
- uses: actions-rs/cargo@v1
with:
command: test
args: --features=tokio03
check:
name: Check
@@ -54,7 +57,7 @@ jobs:
rust:
- stable
- beta
- 1.40.0
- 1.45.2
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1

View File

@@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@lettre.at. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@lettre.rs. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.

View File

@@ -1,9 +1,9 @@
[package]
name = "lettre"
version = "0.10.0-alpha.0" # remember to update html_root_url and README.md
version = "0.10.0-alpha.3" # remember to update html_root_url and README.md
description = "Email client"
readme = "README.md"
homepage = "https://lettre.at"
homepage = "https://lettre.rs"
repository = "https://github.com/lettre/lettre"
license = "MIT"
authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo565.org>"]
@@ -17,33 +17,50 @@ is-it-maintained-open-issues = { repository = "lettre/lettre" }
maintenance = { status = "actively-developed" }
[dependencies]
idna = "0.2"
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
# builder
hyperx = { version = "1", optional = true, features = ["headers"] }
mime = { version = "0.3", optional = true }
uuid = { version = "0.8", features = ["v4"] }
rand = { version = "0.7", optional = true }
quoted_printable = { version = "0.4", optional = true }
base64 = { version = "0.13", optional = true }
once_cell = "1"
regex = "1"
# file transport
serde = { version = "1", optional = true, features = ["derive"] }
serde_json = { version = "1", optional = true }
# smtp
nom = { version = "5", optional = true }
r2d2 = { version = "0.8", optional = true } # feature
hostname = { version = "0.3", optional = true } # feature
## tls
native-tls = { version = "0.2", optional = true } # feature
rustls = { version = "0.18", features = ["dangerous_configuration"], optional = true }
webpki = { version = "0.21", optional = true }
webpki-roots = { version = "0.20", optional = true }
# async
futures-io = { version = "0.3", optional = true }
futures-util = { version = "0.3", features = ["io"], optional = true }
## async-std
async-attributes = { version = "1.1", optional = true }
async-std = { version = "1.5", optional = true, features = ["unstable"] }
async-trait = { version = "0.1", optional = true }
## tokio
tokio02_crate = { package = "tokio", version = "0.2.7", features = ["fs", "process", "tcp", "dns", "io-util"], optional = true }
tokio02_native_tls_crate = { package = "tokio-native-tls", version = "0.1", optional = true }
tokio02_rustls = { package = "tokio-rustls", version = "0.14", optional = true }
futures-io = { version = "0.3", optional = true }
futures-util = { version = "0.3", features = ["io"], optional = true }
base64 = { version = "0.12", optional = true }
hostname = { version = "0.3", optional = true }
hyperx = { version = "1", optional = true, features = ["headers"] }
idna = "0.2"
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true }
mime = { version = "0.3", optional = true }
native-tls = { version = "0.2", optional = true }
nom = { version = "5", optional = true }
once_cell = "1"
quoted_printable = { version = "0.4", optional = true }
r2d2 = { version = "0.8", optional = true }
rand = { version = "0.7", optional = true }
regex = "1"
rustls = { version = "0.18", optional = true }
serde = { version = "1", optional = true, features = ["derive"] }
serde_json = { version = "1", optional = true }
uuid = { version = "0.8", features = ["v4"] }
webpki = { version = "0.21", optional = true }
webpki-roots = { version = "0.20", optional = true }
tokio03_crate = { package = "tokio", version = "0.3", features = ["fs", "process", "net", "io-util"], optional = true }
tokio03_native_tls_crate = { package = "tokio-native-tls", version = "0.2", optional = true }
tokio03_rustls = { package = "tokio-rustls", version = "0.20", optional = true }
[dev-dependencies]
criterion = "0.3"
@@ -51,26 +68,36 @@ tracing-subscriber = "0.2.10"
glob = "0.3"
walkdir = "2"
tokio02_crate = { package = "tokio", version = "0.2.7", features = ["macros", "rt-threaded"] }
tokio03_crate = { package = "tokio", version = "0.3", features = ["macros", "rt-multi-thread"] }
[[bench]]
harness = false
name = "transport_smtp"
[features]
default = ["file-transport", "smtp-transport", "native-tls", "hostname", "r2d2", "sendmail-transport", "builder"]
builder = ["mime", "base64", "hyperx", "rand", "quoted_printable"]
# transports
file-transport = ["serde", "serde_json"]
sendmail-transport = []
smtp-transport = ["base64", "nom"]
rustls-tls = ["webpki", "webpki-roots", "rustls"]
# async
async-std1 = ["async-std", "async-trait", "async-attributes"]
tokio02 = ["tokio02_crate", "async-trait", "futures-io", "futures-util"]
tokio02-native-tls = ["tokio02", "native-tls", "tokio02_native_tls_crate"]
tokio02-rustls-tls = ["tokio02", "rustls-tls", "tokio02_rustls"]
builder = ["mime", "base64", "hyperx", "rand", "quoted_printable"]
default = ["file-transport", "smtp-transport", "native-tls", "hostname", "r2d2", "sendmail-transport", "builder"]
file-transport = ["serde", "serde_json"]
# native-tls
rustls-tls = ["webpki", "webpki-roots", "rustls"]
sendmail-transport = []
smtp-transport = ["base64", "nom"]
tokio03 = ["tokio03_crate", "async-trait", "futures-io", "futures-util"]
tokio03-native-tls = ["tokio03", "native-tls", "tokio03_native_tls_crate"]
tokio03-rustls-tls = ["tokio03", "rustls-tls", "tokio03_rustls"]
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[[example]]
name = "smtp"
@@ -84,6 +111,10 @@ required-features = ["smtp-transport", "native-tls"]
name = "smtp_starttls"
required-features = ["smtp-transport", "native-tls"]
[[example]]
name = "smtp_selfsigned"
required-features = ["smtp-transport", "native-tls"]
[[example]]
name = "tokio02_smtp_tls"
required-features = ["smtp-transport", "tokio02", "tokio02-native-tls"]
@@ -91,3 +122,12 @@ required-features = ["smtp-transport", "tokio02", "tokio02-native-tls"]
[[example]]
name = "tokio02_smtp_starttls"
required-features = ["smtp-transport", "tokio02", "tokio02-native-tls"]
[[example]]
name = "tokio03_smtp_tls"
required-features = ["smtp-transport", "tokio03", "tokio03-native-tls"]
[[example]]
name = "tokio03_smtp_starttls"
required-features = ["smtp-transport", "tokio03", "tokio03-native-tls"]

View File

@@ -21,7 +21,7 @@
<img src="https://badges.gitter.im/lettre/lettre.svg"
alt="chat on gitter" />
</a>
<a href="https://lettre.at">
<a href="https://lettre.rs">
<img src="https://img.shields.io/badge/visit-website-blueviolet"
alt="website" />
</a>
@@ -53,13 +53,13 @@ Lettre does not provide (for now):
## Example
This library requires Rust 1.40 or newer.
This library requires Rust 1.45 or newer.
To use this library, add the following to your `Cargo.toml`:
```toml
[dependencies]
lettre = "0.10.0-alpha.1"
lettre = "0.10.0-alpha.3"
```
```rust,no_run

View File

@@ -1,7 +1,7 @@
## Report a security issue
The lettre project team welcomes security reports and is committed to providing prompt attention to security issues.
Security issues should be reported privately via [security@lettre.at](mailto:security@lettre.at). Security issues
Security issues should be reported privately via [security@lettre.rs](mailto:security@lettre.rs). Security issues
should not be reported via the public Github Issue tracker.
## Security advisories

View File

@@ -2,7 +2,9 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion};
use lettre::{Message, SmtpTransport, Transport};
fn bench_simple_send(c: &mut Criterion) {
let sender = SmtpTransport::builder("127.0.0.1").port(2525).build();
let sender = SmtpTransport::builder_dangerous("127.0.0.1")
.port(2525)
.build();
c.bench_function("send email", move |b| {
b.iter(|| {
@@ -20,7 +22,9 @@ fn bench_simple_send(c: &mut Criterion) {
}
fn bench_reuse_send(c: &mut Criterion) {
let sender = SmtpTransport::builder("127.0.0.1").port(2525).build();
let sender = SmtpTransport::builder_dangerous("127.0.0.1")
.port(2525)
.build();
c.bench_function("send email with connection reuse", move |b| {
b.iter(|| {
let email = Message::builder()

View File

@@ -0,0 +1,41 @@
use std::fs;
use lettre::{
transport::smtp::authentication::Credentials,
transport::smtp::client::{Certificate, Tls, TlsParameters},
Message, SmtpTransport, Transport,
};
fn main() {
tracing_subscriber::fmt::init();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.body("Be happy!")
.unwrap();
// Use a custom certificate stored on disk to securely verify the server's certificate
let pem_cert = fs::read("certificate.pem").unwrap();
let cert = Certificate::from_pem(&pem_cert).unwrap();
let mut tls = TlsParameters::builder("smtp.server.com".to_string());
tls.add_root_certificate(cert);
let tls = tls.build().unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
// Open a remote connection to the smtp server
let mailer = SmtpTransport::builder_dangerous("smtp.server.com")
.port(465)
.tls(Tls::Wrapper(tls))
.credentials(creds)
.build();
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
}
}

View File

@@ -0,0 +1,36 @@
// This line is only to make it compile from lettre's examples folder,
// since it uses Rust 2018 crate renaming to import tokio.
// Won't be needed in user's code.
use tokio03_crate as tokio;
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio03Connector,
Tokio03Transport,
};
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year")
.body("Be happy with async!")
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
// Open a remote connection to gmail using STARTTLS
let mailer = AsyncSmtpTransport::<Tokio03Connector>::starttls_relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
}
}

View File

@@ -0,0 +1,36 @@
// This line is only to make it compile from lettre's examples folder,
// since it uses Rust 2018 crate renaming to import tokio.
// Won't be needed in user's code.
use tokio03_crate as tokio;
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio03Connector,
Tokio03Transport,
};
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year")
.body("Be happy with async!")
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
// Open a remote connection to gmail
let mailer = AsyncSmtpTransport::<Tokio03Connector>::relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
}
}

134
src/address/envelope.rs Normal file
View File

@@ -0,0 +1,134 @@
#[cfg(feature = "builder")]
use std::convert::TryFrom;
use super::Address;
#[cfg(feature = "builder")]
use crate::message::header::{self, Headers};
#[cfg(feature = "builder")]
use crate::message::{Mailbox, Mailboxes};
use crate::Error;
/// Simple email envelope representation
///
/// We only accept mailboxes, and do not support source routes (as per RFC).
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Envelope {
/// The envelope recipients' addresses
///
/// This can not be empty.
forward_path: Vec<Address>,
/// The envelope sender address
reverse_path: Option<Address>,
}
impl Envelope {
/// Creates a new envelope, which may fail if `to` is empty.
///
/// # Examples
///
/// ```
/// use std::str::FromStr;
/// # use lettre::Address;
/// # use lettre::address::Envelope;
///
/// let sender = Address::from_str("from@email.com").unwrap();
/// let recipients = vec![Address::from_str("to@email.com").unwrap()];
///
/// let envelope = Envelope::new(Some(sender), recipients);
/// ```
///
/// # Errors
///
/// If `to` has no elements in it.
pub fn new(from: Option<Address>, to: Vec<Address>) -> Result<Envelope, Error> {
if to.is_empty() {
return Err(Error::MissingTo);
}
Ok(Envelope {
forward_path: to,
reverse_path: from,
})
}
/// Gets the destination addresses of the envelope.
///
/// # Examples
///
/// ```
/// use std::str::FromStr;
/// # use lettre::Address;
/// # use lettre::address::Envelope;
///
/// let sender = Address::from_str("from@email.com").unwrap();
/// let recipients = vec![Address::from_str("to@email.com").unwrap()];
///
/// let envelope = Envelope::new(Some(sender), recipients.clone()).unwrap();
/// assert_eq!(envelope.to(), recipients.as_slice());
/// ```
pub fn to(&self) -> &[Address] {
self.forward_path.as_slice()
}
/// Gets the sender of the envelope.
///
/// # Examples
///
/// ```
/// use std::str::FromStr;
/// # use lettre::Address;
/// # use lettre::address::Envelope;
///
/// let sender = Address::from_str("from@email.com").unwrap();
/// let recipients = vec![Address::from_str("to@email.com").unwrap()];
///
/// let envelope = Envelope::new(Some(sender), recipients.clone()).unwrap();
/// assert!(envelope.from().is_some());
///
/// let senderless = Envelope::new(None, recipients.clone()).unwrap();
/// assert!(senderless.from().is_none());
/// ```
pub fn from(&self) -> Option<&Address> {
self.reverse_path.as_ref()
}
}
#[cfg(feature = "builder")]
impl TryFrom<&Headers> for Envelope {
type Error = Error;
fn try_from(headers: &Headers) -> Result<Self, Self::Error> {
let from = match headers.get::<header::Sender>() {
// If there is a Sender, use it
Some(header::Sender(a)) => Some(a.email.clone()),
// ... else try From
None => match headers.get::<header::From>() {
Some(header::From(a)) => {
let from: Vec<Mailbox> = a.clone().into();
if from.len() > 1 {
return Err(Error::TooManyFrom);
}
Some(from[0].email.clone())
}
None => None,
},
};
fn add_addresses_from_mailboxes(
addresses: &mut Vec<Address>,
mailboxes: Option<&Mailboxes>,
) {
if let Some(mailboxes) = mailboxes {
for mailbox in mailboxes.iter() {
addresses.push(mailbox.email.clone());
}
}
}
let mut to = vec![];
add_addresses_from_mailboxes(&mut to, headers.get::<header::To>().map(|h| &h.0));
add_addresses_from_mailboxes(&mut to, headers.get::<header::Cc>().map(|h| &h.0));
add_addresses_from_mailboxes(&mut to, headers.get::<header::Bcc>().map(|h| &h.0));
Self::new(from, to)
}
}

8
src/address/mod.rs Normal file
View File

@@ -0,0 +1,8 @@
#[cfg(feature = "serde")]
mod serde;
mod envelope;
mod types;
pub use self::envelope::Envelope;
pub use self::types::{Address, AddressError};

112
src/address/serde.rs Normal file
View File

@@ -0,0 +1,112 @@
use std::fmt::{Formatter, Result as FmtResult};
use serde::{
de::{Deserializer, Error as DeError, MapAccess, Visitor},
ser::Serializer,
Deserialize, Serialize,
};
use super::Address;
impl Serialize for Address {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_ref())
}
}
impl<'de> Deserialize<'de> for Address {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
enum Field {
User,
Domain,
};
const FIELDS: &[&str] = &["user", "domain"];
impl<'de> Deserialize<'de> for Field {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct FieldVisitor;
impl<'de> Visitor<'de> for FieldVisitor {
type Value = Field;
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
formatter.write_str("'user' or 'domain'")
}
fn visit_str<E>(self, value: &str) -> Result<Field, E>
where
E: DeError,
{
match value {
"user" => Ok(Field::User),
"domain" => Ok(Field::Domain),
_ => Err(DeError::unknown_field(value, FIELDS)),
}
}
}
deserializer.deserialize_identifier(FieldVisitor)
}
}
struct AddressVisitor;
impl<'de> Visitor<'de> for AddressVisitor {
type Value = Address;
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
formatter.write_str("email address string or object")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: DeError,
{
s.parse().map_err(DeError::custom)
}
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
where
V: MapAccess<'de>,
{
let mut user = None;
let mut domain = None;
while let Some(key) = map.next_key()? {
match key {
Field::User => {
if user.is_some() {
return Err(DeError::duplicate_field("user"));
}
let val = map.next_value()?;
Address::check_user(val).map_err(DeError::custom)?;
user = Some(val);
}
Field::Domain => {
if domain.is_some() {
return Err(DeError::duplicate_field("domain"));
}
let val = map.next_value()?;
Address::check_domain(val).map_err(DeError::custom)?;
domain = Some(val);
}
}
}
let user: &str = user.ok_or_else(|| DeError::missing_field("user"))?;
let domain: &str = domain.ok_or_else(|| DeError::missing_field("domain"))?;
Ok(Address::new(user, domain).unwrap())
}
}
deserializer.deserialize_any(AddressVisitor)
}
}

View File

@@ -12,11 +12,28 @@ use std::{
str::FromStr,
};
/// Email address
/// Represents an email address with a user and a domain name.
///
/// This type contains email in canonical form (_user@domain.tld_).
///
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/).
///
/// # Examples
///
/// You can create an `Address` from a user and a domain:
///
/// ```
/// # use lettre::Address;
/// let address = Address::new("example", "email.com").unwrap();
/// ```
///
/// You can also create an `Address` from a string literal by parsing it:
///
/// ```
/// use std::str::FromStr;
/// # use lettre::Address;
/// let address = Address::from_str("example@email.com").unwrap();
/// ```
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct Address {
/// Complete address
@@ -62,22 +79,50 @@ static DOMAIN_RE: Lazy<Regex> = Lazy::new(|| {
static LITERAL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)\[([A-f0-9:\.]+)\]\z").unwrap());
impl Address {
/// Create email address from parts
/// Creates a new email address from a user and domain.
///
/// # Examples
///
/// ```
/// use lettre::Address;
///
/// let address = Address::new("example", "email.com").unwrap();
/// let expected: Address = "example@email.com".parse().unwrap();
/// assert_eq!(expected, address);
/// ```
pub fn new<U: AsRef<str>, D: AsRef<str>>(user: U, domain: D) -> Result<Self, AddressError> {
(user, domain).try_into()
}
/// Get the user part of this `Address`
/// Gets the user portion of the `Address`.
///
/// # Examples
///
/// ```
/// use lettre::Address;
///
/// let address = Address::new("example", "email.com").unwrap();
/// assert_eq!("example", address.user());
/// ```
pub fn user(&self) -> &str {
&self.serialized[..self.at_start]
}
/// Get the domain part of this `Address`
/// Gets the domain portion of the `Address`.
///
/// # Examples
///
/// ```
/// use lettre::Address;
///
/// let address = Address::new("example", "email.com").unwrap();
/// assert_eq!("email.com", address.domain());
/// ```
pub fn domain(&self) -> &str {
&self.serialized[self.at_start + 1..]
}
fn check_user(user: &str) -> Result<(), AddressError> {
pub(super) fn check_user(user: &str) -> Result<(), AddressError> {
if USER_RE.is_match(user) {
Ok(())
} else {
@@ -85,7 +130,7 @@ impl Address {
}
}
fn check_domain(domain: &str) -> Result<(), AddressError> {
pub(super) fn check_domain(domain: &str) -> Result<(), AddressError> {
Address::check_domain_ascii(domain).or_else(|_| {
domain_to_ascii(domain)
.map_err(|_| AddressError::InvalidDomain)
@@ -111,7 +156,7 @@ impl Address {
}
impl Display for Address {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
f.write_str(&self.serialized)
}
}
@@ -157,7 +202,7 @@ pub enum AddressError {
impl Error for AddressError {}
impl Display for AddressError {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
AddressError::MissingParts => f.write_str("Missing domain or user"),
AddressError::Unbalanced => f.write_str("Unbalanced angle bracket"),
@@ -168,120 +213,6 @@ impl Display for AddressError {
}
}
#[cfg(feature = "serde")]
mod serde {
use crate::address::Address;
use serde::{
de::{Deserializer, Error as DeError, MapAccess, Visitor},
ser::Serializer,
Deserialize, Serialize,
};
use std::fmt::{Formatter, Result as FmtResult};
impl Serialize for Address {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_ref())
}
}
impl<'de> Deserialize<'de> for Address {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
enum Field {
User,
Domain,
};
const FIELDS: &[&str] = &["user", "domain"];
impl<'de> Deserialize<'de> for Field {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct FieldVisitor;
impl<'de> Visitor<'de> for FieldVisitor {
type Value = Field;
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
formatter.write_str("'user' or 'domain'")
}
fn visit_str<E>(self, value: &str) -> Result<Field, E>
where
E: DeError,
{
match value {
"user" => Ok(Field::User),
"domain" => Ok(Field::Domain),
_ => Err(DeError::unknown_field(value, FIELDS)),
}
}
}
deserializer.deserialize_identifier(FieldVisitor)
}
}
struct AddressVisitor;
impl<'de> Visitor<'de> for AddressVisitor {
type Value = Address;
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
formatter.write_str("email address string or object")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: DeError,
{
s.parse().map_err(DeError::custom)
}
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
where
V: MapAccess<'de>,
{
let mut user = None;
let mut domain = None;
while let Some(key) = map.next_key()? {
match key {
Field::User => {
if user.is_some() {
return Err(DeError::duplicate_field("user"));
}
let val = map.next_value()?;
Address::check_user(val).map_err(DeError::custom)?;
user = Some(val);
}
Field::Domain => {
if domain.is_some() {
return Err(DeError::duplicate_field("domain"));
}
let val = map.next_value()?;
Address::check_domain(val).map_err(DeError::custom)?;
domain = Some(val);
}
}
}
let user: &str = user.ok_or_else(|| DeError::missing_field("user"))?;
let domain: &str = domain.ok_or_else(|| DeError::missing_field("domain"))?;
Ok(Address::new(user, domain).unwrap())
}
}
deserializer.deserialize_any(AddressVisitor)
}
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -5,7 +5,7 @@
//! * Unicode support
//! * Secure defaults
//!
//! Lettre requires Rust 1.40 or newer.
//! Lettre requires Rust 1.45 or newer.
//!
//! ## Optional features
//!
@@ -18,127 +18,62 @@
//! * **tokio02**: Allow to asyncronously send emails using tokio 0.2.x
//! * **tokio02-rustls-tls**: Async TLS support with the `rustls` crate using tokio 0.2
//! * **tokio02-native-tls**: Async TLS support with the `native-tls` crate using tokio 0.2
//! * **tokio03**: Allow to asyncronously send emails using tokio 0.3.x
//! * **tokio03-rustls-tls**: Async TLS support with the `rustls` crate using tokio 0.3
//! * **tokio03-native-tls**: Async TLS support with the `native-tls` crate using tokio 0.3
//! * **async-std1**: Allow to asyncronously send emails using async-std 1.x (SMTP isn't supported yet)
//! * **r2d2**: Connection pool for SMTP transport
//! * **tracing**: Logging using the `tracing` crate
//! * **serde**: Serialization/Deserialization of entities
//! * **hostname**: Ability to try to use actual hostname in SMTP transaction
#![doc(html_root_url = "https://docs.rs/lettre/0.10.0")]
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.0-alpha.3")]
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
#![forbid(unsafe_code)]
#![deny(
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unstable_features,
unused_import_braces,
unsafe_code
rust_2018_idioms
)]
#![cfg_attr(docsrs, feature(doc_cfg))]
pub mod address;
pub mod error;
#[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
pub mod message;
pub mod transport;
#[cfg(feature = "builder")]
#[macro_use]
extern crate hyperx;
pub use crate::address::Address;
use crate::address::Envelope;
use crate::error::Error;
#[cfg(feature = "builder")]
pub use crate::message::{
header::{self, Headers},
EmailFormat, Mailbox, Mailboxes, Message,
};
pub use crate::message::Message;
#[cfg(feature = "file-transport")]
pub use crate::transport::file::FileTransport;
#[cfg(feature = "sendmail-transport")]
pub use crate::transport::sendmail::SendmailTransport;
#[cfg(all(feature = "smtp-transport", feature = "connection-pool"))]
pub use crate::transport::smtp::r2d2::SmtpConnectionManager;
#[cfg(all(
feature = "smtp-transport",
any(feature = "tokio02", feature = "tokio03")
))]
pub use crate::transport::smtp::AsyncSmtpTransport;
#[cfg(feature = "smtp-transport")]
pub use crate::transport::smtp::SmtpTransport;
#[cfg(all(feature = "smtp-transport", feature = "tokio02"))]
pub use crate::transport::smtp::{AsyncSmtpTransport, Tokio02Connector};
pub use crate::{address::Address, transport::stub::StubTransport};
#[cfg(any(feature = "async-std1", feature = "tokio02"))]
pub use crate::transport::smtp::Tokio02Connector;
#[cfg(all(feature = "smtp-transport", feature = "tokio03"))]
pub use crate::transport::smtp::Tokio03Connector;
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio03"))]
use async_trait::async_trait;
#[cfg(feature = "builder")]
use std::convert::TryFrom;
/// Simple email envelope representation
///
/// We only accept mailboxes, and do not support source routes (as per RFC).
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Envelope {
/// The envelope recipients' addresses
///
/// This can not be empty.
forward_path: Vec<Address>,
/// The envelope sender address
reverse_path: Option<Address>,
}
impl Envelope {
/// Creates a new envelope, which may fail if `to` is empty.
pub fn new(from: Option<Address>, to: Vec<Address>) -> Result<Envelope, Error> {
if to.is_empty() {
return Err(Error::MissingTo);
}
Ok(Envelope {
forward_path: to,
reverse_path: from,
})
}
/// Destination addresses of the envelope
pub fn to(&self) -> &[Address] {
self.forward_path.as_slice()
}
/// Source address of the envelope
pub fn from(&self) -> Option<&Address> {
self.reverse_path.as_ref()
}
}
impl TryFrom<&Headers> for Envelope {
type Error = Error;
fn try_from(headers: &Headers) -> Result<Self, Self::Error> {
let from = match headers.get::<header::Sender>() {
// If there is a Sender, use it
Some(header::Sender(a)) => Some(a.email.clone()),
// ... else try From
None => match headers.get::<header::From>() {
Some(header::From(a)) => {
let from: Vec<Mailbox> = a.clone().into();
if from.len() > 1 {
return Err(Error::TooManyFrom);
}
Some(from[0].email.clone())
}
None => None,
},
};
fn add_addresses_from_mailboxes(
addresses: &mut Vec<Address>,
mailboxes: Option<&Mailboxes>,
) {
if let Some(mailboxes) = mailboxes {
for mailbox in mailboxes.iter() {
addresses.push(mailbox.email.clone());
}
}
}
let mut to = vec![];
add_addresses_from_mailboxes(&mut to, headers.get::<header::To>().map(|h| &h.0));
add_addresses_from_mailboxes(&mut to, headers.get::<header::Cc>().map(|h| &h.0));
add_addresses_from_mailboxes(&mut to, headers.get::<header::Bcc>().map(|h| &h.0));
Self::new(from, to)
}
}
/// Blocking Transport method for emails
pub trait Transport {
@@ -149,6 +84,7 @@ pub trait Transport {
/// Sends the email
#[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
fn send(&self, message: &Message) -> Result<Self::Ok, Self::Error> {
let raw = message.formatted();
self.send_raw(message.envelope(), &raw)
@@ -159,6 +95,7 @@ pub trait Transport {
/// async-std 1.x based Transport method for emails
#[cfg(feature = "async-std1")]
#[cfg_attr(docsrs, doc(cfg(feature = "async-std1")))]
#[async_trait]
pub trait AsyncStd1Transport {
/// Response produced by the Transport
@@ -168,6 +105,7 @@ pub trait AsyncStd1Transport {
/// Sends the email
#[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
// TODO take &Message
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
let raw = message.formatted();
@@ -180,6 +118,7 @@ pub trait AsyncStd1Transport {
/// tokio 0.2.x based Transport method for emails
#[cfg(feature = "tokio02")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio02")))]
#[async_trait]
pub trait Tokio02Transport {
/// Response produced by the Transport
@@ -189,6 +128,30 @@ pub trait Tokio02Transport {
/// Sends the email
#[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
// TODO take &Message
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
let raw = message.formatted();
let envelope = message.envelope();
self.send_raw(&envelope, &raw).await
}
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
}
/// tokio 0.3.x based Transport method for emails
#[cfg(feature = "tokio03")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio03")))]
#[async_trait]
pub trait Tokio03Transport {
/// Response produced by the Transport
type Ok;
/// Error produced by the Transport
type Error;
/// Sends the email
#[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
// TODO take &Message
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
let raw = message.formatted();
@@ -204,6 +167,7 @@ mod test {
use super::*;
use crate::message::{header, Mailbox, Mailboxes};
use hyperx::header::Headers;
use std::convert::TryFrom;
#[test]
fn envelope_from_headers() {

View File

@@ -143,7 +143,7 @@ mod test {
let mut c = SevenBitCodec::new();
assert_eq!(
&String::from_utf8(c.encode("Hello, world!".as_bytes())).unwrap(),
&String::from_utf8(c.encode(b"Hello, world!")).unwrap(),
"Hello, world!"
);
}
@@ -212,10 +212,7 @@ mod test {
fn base64_encodeed() {
let mut c = Base64Codec::new();
assert_eq!(
&String::from_utf8(c.encode("Chunk.".as_bytes())).unwrap(),
"Q2h1bmsu"
);
assert_eq!(&String::from_utf8(c.encode(b"Chunk.")).unwrap(), "Q2h1bmsu");
}
#[test]
@@ -223,7 +220,7 @@ mod test {
let mut c = EightBitCodec::new();
assert_eq!(
&String::from_utf8(c.encode("Hello, world!".as_bytes())).unwrap(),
&String::from_utf8(c.encode(b"Hello, world!")).unwrap(),
"Hello, world!"
);
@@ -238,7 +235,7 @@ mod test {
let mut c = BinaryCodec::new();
assert_eq!(
&String::from_utf8(c.encode("Hello, world!".as_bytes())).unwrap(),
&String::from_utf8(c.encode(b"Hello, world!")).unwrap(),
"Hello, world!"
);

View File

@@ -7,6 +7,8 @@ use std::{
str::{from_utf8, FromStr},
};
header! { (ContentId, "Content-ID") => [String] }
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ContentTransferEncoding {
SevenBit,
@@ -24,7 +26,7 @@ impl Default for ContentTransferEncoding {
}
impl Display for ContentTransferEncoding {
fn fmt(&self, f: &mut FmtFormatter) -> FmtResult {
fn fmt(&self, f: &mut FmtFormatter<'_>) -> FmtResult {
use self::ContentTransferEncoding::*;
f.write_str(match *self {
SevenBit => "7bit",
@@ -71,7 +73,7 @@ impl Header for ContentTransferEncoding {
})
}
fn fmt_header(&self, f: &mut HeaderFormatter) -> FmtResult {
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&format!("{}", self))
}
}

View File

@@ -35,7 +35,7 @@ macro_rules! mailbox_header {
}).map($type_name)
}
fn fmt_header(&self, f: &mut HeaderFormatter) -> FmtResult {
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&self.0.recode_name(utf8_b::encode))
}
}
@@ -70,7 +70,7 @@ macro_rules! mailboxes_header {
.map($type_name)
}
fn fmt_header(&self, f: &mut HeaderFormatter) -> FmtResult {
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
format_mailboxes(self.0.iter(), f)
}
}
@@ -155,7 +155,7 @@ fn parse_mailboxes(raw: &[u8]) -> HyperResult<Mailboxes> {
Err(HeaderError::Header)
}
fn format_mailboxes<'a>(mbs: Iter<'a, Mailbox>, f: &mut HeaderFormatter) -> FmtResult {
fn format_mailboxes<'a>(mbs: Iter<'a, Mailbox>, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&Mailboxes::from(
mbs.map(|mb| mb.recode_name(utf8_b::encode))
.collect::<Vec<_>>(),

View File

@@ -45,7 +45,7 @@ impl Header for MimeVersion {
})
}
fn fmt_header(&self, f: &mut HeaderFormatter) -> FmtResult {
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&format!("{}.{}", self.major, self.minor))
}
}

View File

@@ -26,7 +26,7 @@ macro_rules! text_header {
.map($type_name)
}
fn fmt_header(&self, f: &mut HeaderFormatter) -> FmtResult {
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
fmt_text(&self.0, f)
}
}
@@ -50,7 +50,7 @@ fn parse_text(raw: &[u8]) -> HyperResult<String> {
Err(HeaderError::Header)
}
fn fmt_text(s: &str, f: &mut HeaderFormatter) -> FmtResult {
fn fmt_text(s: &str, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&utf8_b::encode(s))
}

View File

@@ -37,7 +37,7 @@ impl<'de> Deserialize<'de> for Mailbox {
impl<'de> Visitor<'de> for FieldVisitor {
type Value = Field;
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
formatter.write_str("'name' or 'email'")
}
@@ -62,7 +62,7 @@ impl<'de> Deserialize<'de> for Mailbox {
impl<'de> Visitor<'de> for MailboxVisitor {
type Value = Mailbox;
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
formatter.write_str("mailbox string or object")
}
@@ -123,7 +123,7 @@ impl<'de> Deserialize<'de> for Mailboxes {
impl<'de> Visitor<'de> for MailboxesVisitor {
type Value = Mailboxes;
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
formatter.write_str("mailboxes string or sequence")
}

View File

@@ -9,22 +9,48 @@ use std::{
str::FromStr,
};
/// Email address with optional addressee name
/// Represents an email address with an optional name for the sender/recipient.
///
/// This type contains email address and the sender/recipient name (_Some Name \<user@domain.tld\>_ or _withoutname@domain.tld_).
///
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/).
///
/// # Examples
///
/// You can create a `Mailbox` from a string and an [`Address`]:
///
/// ```
/// # use lettre::{Address, message::Mailbox};
/// let address = Address::new("example", "email.com").unwrap();
/// let mailbox = Mailbox::new(None, address);
/// ```
///
/// You can also create one from a string literal:
///
/// ```
/// # use lettre::message::Mailbox;
/// let mailbox: Mailbox = "John Smith <example@email.com>".parse().unwrap();
/// ```
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct Mailbox {
/// User name part
/// The name associated with the address.
pub name: Option<String>,
/// Email address part
/// The email address itself.
pub email: Address,
}
impl Mailbox {
/// Create new mailbox using email address and addressee name
/// Creates a new `Mailbox` using an email address and the name of the recipient if there is one.
///
/// # Examples
///
/// ```
/// use lettre::{Address, message::Mailbox};
///
/// let address = Address::new("example", "email.com").unwrap();
/// let mailbox = Mailbox::new(None, address);
/// ```
pub fn new(name: Option<String>, email: Address) -> Self {
Mailbox { name, email }
}
@@ -39,7 +65,7 @@ impl Mailbox {
}
impl Display for Mailbox {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
if let Some(ref name) = self.name {
let name = name.trim();
if !name.is_empty() {
@@ -99,7 +125,7 @@ impl FromStr for Mailbox {
}
}
/// List or email mailboxes
/// Represents a sequence of [`Mailbox`] instances.
///
/// This type contains a sequence of mailboxes (_Some Name \<user@domain.tld\>, Another Name \<other@domain.tld\>, withoutname@domain.tld, ..._).
///
@@ -108,29 +134,91 @@ impl FromStr for Mailbox {
pub struct Mailboxes(Vec<Mailbox>);
impl Mailboxes {
/// Create mailboxes list
/// Creates a new list of [`Mailbox`] instances.
///
/// # Examples
///
/// ```
/// use lettre::message::Mailboxes;
/// let mailboxes = Mailboxes::new();
/// ```
pub fn new() -> Self {
Mailboxes(Vec::new())
}
/// Add mailbox to a list
/// Adds a new [`Mailbox`] to the list, in a builder style pattern.
///
/// # Examples
///
/// ```
/// use lettre::{Address, message::{Mailbox, Mailboxes}};
///
/// let address = Address::new("example", "email.com").unwrap();
/// let mut mailboxes = Mailboxes::new().with(Mailbox::new(None, address));
/// ```
pub fn with(mut self, mbox: Mailbox) -> Self {
self.0.push(mbox);
self
}
/// Add mailbox to a list
/// Adds a new [`Mailbox`] to the list, in a Vec::push style pattern.
///
/// # Examples
///
/// ```
/// use lettre::{Address, message::{Mailbox, Mailboxes}};
///
/// let address = Address::new("example", "email.com").unwrap();
/// let mut mailboxes = Mailboxes::new();
/// mailboxes.push(Mailbox::new(None, address));
/// ```
pub fn push(&mut self, mbox: Mailbox) {
self.0.push(mbox);
}
/// Extract first mailbox
/// Extracts the first [`Mailbox`] if it exists.
///
/// # Examples
///
/// ```
/// use lettre::{Address, message::{Mailbox, Mailboxes}};
///
/// let empty = Mailboxes::new();
/// assert!(empty.into_single().is_none());
///
/// let mut mailboxes = Mailboxes::new();
/// let address = Address::new("example", "email.com").unwrap();
///
/// mailboxes.push(Mailbox::new(None, address));
/// assert!(mailboxes.into_single().is_some());
/// ```
pub fn into_single(self) -> Option<Mailbox> {
self.into()
}
/// Iterate over mailboxes
pub fn iter(&self) -> Iter<Mailbox> {
/// Creates an iterator over the [`Mailbox`] instances that are currently stored.
///
/// # Examples
///
/// ```
/// use lettre::{Address, message::{Mailbox, Mailboxes}};
///
/// let mut mailboxes = Mailboxes::new();
///
/// let address = Address::new("example", "email.com").unwrap();
/// mailboxes.push(Mailbox::new(None, address));
///
/// let address = Address::new("example", "email.com").unwrap();
/// mailboxes.push(Mailbox::new(None, address));
///
/// let mut iter = mailboxes.iter();
///
/// assert!(iter.next().is_some());
/// assert!(iter.next().is_some());
///
/// assert!(iter.next().is_none());
/// ```
pub fn iter(&self) -> Iter<'_, Mailbox> {
self.0.iter()
}
}
@@ -183,7 +271,7 @@ impl Extend<Mailbox> for Mailboxes {
}
impl Display for Mailboxes {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let mut iter = self.iter();
if let Some(mbox) = iter.next() {

View File

@@ -535,7 +535,7 @@ mod test {
parameters: vec![header::DispositionParam::Filename(
header::Charset::Ext("utf-8".into()),
None,
"example.c".as_bytes().into(),
"example.c".into(),
)],
})
.header(header::ContentTransferEncoding::Binary)
@@ -582,7 +582,7 @@ mod test {
parameters: vec![header::DispositionParam::Filename(
header::Charset::Ext("utf-8".into()),
None,
"encrypted.asc".as_bytes().into(),
"encrypted.asc".into(),
)],
})
.body(String::from(concat!(
@@ -637,7 +637,7 @@ mod test {
parameters: vec![header::DispositionParam::Filename(
header::Charset::Ext("utf-8".into()),
None,
"signature.asc".as_bytes().into(),
"signature.asc".into(),
)],
})
.body(String::from(concat!(
@@ -725,7 +725,7 @@ mod test {
.header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
.header(header::ContentDisposition {
disposition: header::DispositionType::Attachment,
parameters: vec![header::DispositionParam::Filename(header::Charset::Ext("utf-8".into()), None, "example.c".as_bytes().into())]
parameters: vec![header::DispositionParam::Filename(header::Charset::Ext("utf-8".into()), None, "example.c".into())]
})
.header(header::ContentTransferEncoding::Binary)
.body(String::from("int main() { return 0; }")));

View File

@@ -174,7 +174,6 @@
//!
//! ```
pub use encoder::*;
pub use mailbox::*;
pub use mimebody::*;
@@ -187,8 +186,9 @@ mod mimebody;
mod utf8_b;
use crate::{
address::Envelope,
message::header::{EmailDate, Header, Headers, MailboxesHeader},
Envelope, Error as EmailError,
Error as EmailError,
};
use std::{convert::TryFrom, time::SystemTime};
use uuid::Uuid;

View File

@@ -16,19 +16,16 @@ pub fn encode(s: &str) -> String {
}
pub fn decode(s: &str) -> Option<String> {
const PREFIX: &str = "=?utf-8?b?";
const SUFFIX: &str = "?=";
let s = s.trim();
if s.starts_with(PREFIX) && s.ends_with(SUFFIX) {
let s = &s[PREFIX.len()..];
let s = &s[..s.len() - SUFFIX.len()];
base64::decode(s)
.ok()
.and_then(|v| String::from_utf8(v).ok())
} else {
Some(s.into())
}
s.strip_prefix("=?utf-8?b?")
.and_then(|stripped| stripped.strip_suffix("?="))
.map_or_else(
|| Some(s.into()),
|stripped| {
let decoded = base64::decode(stripped).ok()?;
let decoded = String::from_utf8(decoded).ok()?;
Some(decoded)
},
)
}
#[cfg(test)]

View File

@@ -19,7 +19,7 @@ pub enum Error {
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
match *self {
Client(err) => fmt.write_str(err),
Io(ref err) => err.fmt(fmt),

View File

@@ -6,7 +6,7 @@
//!
//! ```rust
//! use std::env::temp_dir;
//! use lettre::{Transport, Envelope, Message, FileTransport};
//! use lettre::{Transport, Message, FileTransport};
//!
//! // Write to the local temp directory
//! let sender = FileTransport::new(temp_dir());
@@ -28,7 +28,7 @@
//! # #[cfg(feature = "tokio02")]
//! # async fn run() {
//! use std::env::temp_dir;
//! use lettre::{Tokio02Transport, Envelope, Message, FileTransport};
//! use lettre::{Tokio02Transport, Message, FileTransport};
//!
//! // Write to the local temp directory
//! let sender = FileTransport::new(temp_dir());
@@ -51,7 +51,7 @@
//! # #[cfg(feature = "async-std1")]
//! # async fn run() {
//! use std::env::temp_dir;
//! use lettre::{AsyncStd1Transport, Envelope, Message, FileTransport};
//! use lettre::{AsyncStd1Transport, Message, FileTransport};
//!
//! // Write to the local temp directory
//! let sender = FileTransport::new(temp_dir());
@@ -86,12 +86,15 @@
//! ```
pub use self::error::Error;
use crate::address::Envelope;
#[cfg(feature = "async-std1")]
use crate::AsyncStd1Transport;
#[cfg(feature = "tokio02")]
use crate::Tokio02Transport;
use crate::{Envelope, Transport};
#[cfg(any(feature = "async-std1", feature = "tokio02"))]
#[cfg(feature = "tokio03")]
use crate::Tokio03Transport;
use crate::Transport;
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio03"))]
use async_trait::async_trait;
use std::{
path::{Path, PathBuf},
@@ -199,3 +202,19 @@ impl Tokio02Transport for FileTransport {
Ok(email_id.to_string())
}
}
#[cfg(feature = "tokio03")]
#[async_trait]
impl Tokio03Transport for FileTransport {
type Ok = Id;
type Error = Error;
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
use tokio03_crate::fs;
let (email_id, file, serialized) = self.send_raw_impl(envelope, email)?;
fs::write(file, serialized).await?;
Ok(email_id.to_string())
}
}

View File

@@ -17,9 +17,12 @@
//! logs.
#[cfg(feature = "file-transport")]
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport")))]
pub mod file;
#[cfg(feature = "sendmail-transport")]
#[cfg_attr(docsrs, doc(cfg(feature = "sendmail-transport")))]
pub mod sendmail;
#[cfg(feature = "smtp-transport")]
#[cfg_attr(docsrs, doc(cfg(feature = "smtp-transport")))]
pub mod smtp;
pub mod stub;

View File

@@ -20,7 +20,7 @@ pub enum Error {
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
match *self {
Client(ref err) => err.fmt(fmt),
Utf8Parsing(ref err) => err.fmt(fmt),

View File

@@ -3,7 +3,7 @@
//! ## Sync example
//!
//! ```rust
//! use lettre::{Message, Envelope, Transport, SendmailTransport};
//! use lettre::{Message, Transport, SendmailTransport};
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
@@ -23,7 +23,7 @@
//! ```rust
//! # #[cfg(feature = "tokio02")]
//! # async fn run() {
//! use lettre::{Message, Envelope, Tokio02Transport, SendmailTransport};
//! use lettre::{Message, Tokio02Transport, SendmailTransport};
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
@@ -44,7 +44,7 @@
//!```rust
//! # #[cfg(feature = "async-std1")]
//! # async fn run() {
//! use lettre::{Message, Envelope, AsyncStd1Transport, SendmailTransport};
//! use lettre::{Message, AsyncStd1Transport, SendmailTransport};
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
@@ -61,15 +61,17 @@
//! ```
pub use self::error::Error;
use crate::address::Envelope;
#[cfg(feature = "async-std1")]
use crate::AsyncStd1Transport;
#[cfg(feature = "tokio02")]
use crate::Tokio02Transport;
use crate::{Envelope, Transport};
#[cfg(any(feature = "async-std1", feature = "tokio02"))]
#[cfg(feature = "tokio03")]
use crate::Tokio03Transport;
use crate::Transport;
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio03"))]
use async_trait::async_trait;
use std::{
convert::AsRef,
ffi::OsString,
io::prelude::*,
process::{Command, Stdio},
@@ -80,7 +82,7 @@ mod error;
const DEFAUT_SENDMAIL: &str = "/usr/sbin/sendmail";
/// Sends an email using the `sendmail` command
#[derive(Debug, Default)]
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SendmailTransport {
command: OsString,
@@ -126,6 +128,27 @@ impl SendmailTransport {
.stdout(Stdio::piped());
c
}
#[cfg(feature = "tokio03")]
fn tokio03_command(&self, envelope: &Envelope) -> tokio03_crate::process::Command {
use tokio03_crate::process::Command;
let mut c = Command::new(&self.command);
c.kill_on_drop(true);
c.arg("-i")
.arg("-f")
.arg(envelope.from().map(|f| f.as_ref()).unwrap_or("\"\""))
.args(envelope.to())
.stdin(Stdio::piped())
.stdout(Stdio::piped());
c
}
}
impl Default for SendmailTransport {
fn default() -> Self {
Self::new()
}
}
impl Transport for SendmailTransport {
@@ -199,3 +222,28 @@ impl Tokio02Transport for SendmailTransport {
}
}
}
#[cfg(feature = "tokio03")]
#[async_trait]
impl Tokio03Transport for SendmailTransport {
type Ok = ();
type Error = Error;
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
use tokio03_crate::io::AsyncWriteExt;
let mut command = self.tokio03_command(envelope);
// Spawn the sendmail command
let mut process = command.spawn()?;
process.stdin.as_mut().unwrap().write_all(&email).await?;
let output = process.wait_with_output().await?;
if output.status.success() {
Ok(())
} else {
Err(Error::Client(String::from_utf8(output.stderr)?))
}
}
}

View File

@@ -1,11 +1,15 @@
use async_trait::async_trait;
#[cfg(feature = "tokio02")]
#[cfg(any(feature = "tokio02", feature = "tokio03"))]
use super::Tls;
use super::{
client::AsyncSmtpConnection, ClientId, Credentials, Error, Mechanism, Response, SmtpInfo,
};
use crate::{Envelope, Tokio02Transport};
use crate::Envelope;
#[cfg(feature = "tokio02")]
use crate::Tokio02Transport;
#[cfg(feature = "tokio03")]
use crate::Tokio03Transport;
#[allow(missing_debug_implementations)]
#[derive(Clone)]
@@ -14,6 +18,7 @@ pub struct AsyncSmtpTransport<C> {
inner: AsyncSmtpClient<C>,
}
#[cfg(feature = "tokio02")]
#[async_trait]
impl Tokio02Transport for AsyncSmtpTransport<Tokio02Connector> {
type Ok = Response;
@@ -31,6 +36,24 @@ impl Tokio02Transport for AsyncSmtpTransport<Tokio02Connector> {
}
}
#[cfg(feature = "tokio03")]
#[async_trait]
impl Tokio03Transport for AsyncSmtpTransport<Tokio03Connector> {
type Ok = Response;
type Error = Error;
/// Sends an email
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
let mut conn = self.inner.connection().await?;
let result = conn.send(envelope, email).await?;
conn.quit().await?;
Ok(result)
}
}
impl<C> AsyncSmtpTransport<C>
where
C: AsyncSmtpConnector,
@@ -41,11 +64,16 @@ where
///
/// Creates an encrypted transport over submissions port, using the provided domain
/// to validate TLS certificates.
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
#[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio03-native-tls",
feature = "tokio03-rustls-tls"
))]
pub fn relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
use super::{TlsParameters, SUBMISSIONS_PORT};
let tls_parameters = TlsParameters::new_tokio02(relay.into())?;
let tls_parameters = TlsParameters::new(relay.into())?;
Ok(Self::builder_dangerous(relay)
.port(SUBMISSIONS_PORT)
@@ -63,7 +91,12 @@ where
///
/// An error is returned if the connection can't be upgraded. No credentials
/// or emails will be sent to the server, protecting from downgrade attacks.
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
#[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio03-native-tls",
feature = "tokio03-rustls-tls"
))]
pub fn starttls_relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
use super::{TlsParameters, SUBMISSION_PORT};
@@ -133,7 +166,12 @@ impl AsyncSmtpTransportBuilder {
}
/// Set the TLS settings to use
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
#[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio03-native-tls",
feature = "tokio03-rustls-tls"
))]
pub fn tls(mut self, tls: Tls) -> Self {
self.info.tls = tls;
self
@@ -195,6 +233,7 @@ pub trait AsyncSmtpConnector: Default + private::Sealed {
#[derive(Debug, Copy, Clone, Default)]
#[cfg(feature = "tokio02")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio02")))]
pub struct Tokio02Connector;
#[async_trait]
@@ -212,6 +251,7 @@ impl AsyncSmtpConnector for Tokio02Connector {
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()),
_ => None,
};
#[allow(unused_mut)]
let mut conn =
AsyncSmtpConnection::connect_tokio02(hostname, port, hello_name, tls_parameters)
.await?;
@@ -233,6 +273,48 @@ impl AsyncSmtpConnector for Tokio02Connector {
}
}
#[derive(Debug, Copy, Clone, Default)]
#[cfg(feature = "tokio03")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio03")))]
pub struct Tokio03Connector;
#[async_trait]
#[cfg(feature = "tokio03")]
impl AsyncSmtpConnector for Tokio03Connector {
async fn connect(
hostname: &str,
port: u16,
hello_name: &ClientId,
tls: &Tls,
) -> Result<AsyncSmtpConnection, Error> {
#[allow(clippy::match_single_binding)]
let tls_parameters = match tls {
#[cfg(any(feature = "tokio03-native-tls", feature = "tokio03-rustls-tls"))]
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()),
_ => None,
};
#[allow(unused_mut)]
let mut conn =
AsyncSmtpConnection::connect_tokio03(hostname, port, hello_name, tls_parameters)
.await?;
#[cfg(any(feature = "tokio03-native-tls", feature = "tokio03-rustls-tls"))]
match tls {
Tls::Opportunistic(ref tls_parameters) => {
if conn.can_starttls() {
conn.starttls(tls_parameters.clone(), hello_name).await?;
}
}
Tls::Required(ref tls_parameters) => {
conn.starttls(tls_parameters.clone(), hello_name).await?;
}
_ => (),
}
Ok(conn)
}
}
mod private {
use super::*;
@@ -240,4 +322,7 @@ mod private {
#[cfg(feature = "tokio02")]
impl Sealed for Tokio02Connector {}
#[cfg(feature = "tokio03")]
impl Sealed for Tokio03Connector {}
}

View File

@@ -7,25 +7,6 @@ use std::fmt::{self, Display, Formatter};
/// Trying LOGIN last as it is deprecated.
pub const DEFAULT_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
/// Convertible to user credentials
pub trait IntoCredentials {
/// Converts to a `Credentials` struct
fn into_credentials(self) -> Credentials;
}
impl IntoCredentials for Credentials {
fn into_credentials(self) -> Credentials {
self
}
}
impl<S: Into<String>, T: Into<String>> IntoCredentials for (S, T) {
fn into_credentials(self) -> Credentials {
let (username, password) = self;
Credentials::new(username.into(), password.into())
}
}
/// Contains user credentials
#[derive(PartialEq, Eq, Clone, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@@ -44,6 +25,16 @@ impl Credentials {
}
}
impl<S, T> From<(S, T)> for Credentials
where
S: Into<String>,
T: Into<String>,
{
fn from((username, password): (S, T)) -> Self {
Credentials::new(username.into(), password.into())
}
}
/// Represents authentication mechanisms
#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@@ -61,7 +52,7 @@ pub enum Mechanism {
}
impl Display for Mechanism {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(match *self {
Mechanism::Plain => "PLAIN",
Mechanism::Login => "LOGIN",
@@ -168,4 +159,12 @@ mod test {
);
assert!(mechanism.response(&credentials, Some("test")).is_err());
}
#[test]
fn test_from_user_pass_for_credentials() {
assert_eq!(
Credentials::new("alice".to_string(), "wonderland".to_string()),
Credentials::from(("alice", "wonderland"))
);
}
}

View File

@@ -45,11 +45,10 @@ impl AsyncSmtpConnection {
&self.server_info
}
// FIXME add simple connect and rename this one
/// Connects to the configured server
///
/// Sends EHLO and parses server information
#[cfg(feature = "tokio02")]
pub async fn connect_tokio02(
hostname: &str,
port: u16,
@@ -60,6 +59,20 @@ impl AsyncSmtpConnection {
Self::connect_impl(stream, hello_name).await
}
/// Connects to the configured server
///
/// Sends EHLO and parses server information
#[cfg(feature = "tokio03")]
pub async fn connect_tokio03(
hostname: &str,
port: u16,
hello_name: &ClientId,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncSmtpConnection, Error> {
let stream = AsyncNetworkStream::connect_tokio03(hostname, port, tls_parameters).await?;
Self::connect_impl(stream, hello_name).await
}
async fn connect_impl(
stream: AsyncNetworkStream,
hello_name: &ClientId,

View File

@@ -8,17 +8,30 @@ use std::{
use futures_io::{Error as IoError, ErrorKind, Result as IoResult};
#[cfg(feature = "tokio02")]
use tokio02_crate::io::{AsyncRead, AsyncWrite};
use tokio02_crate::io::{AsyncRead as _, AsyncWrite as _};
#[cfg(feature = "tokio02")]
use tokio02_crate::net::TcpStream;
use tokio02_crate::net::TcpStream as Tokio02TcpStream;
#[cfg(feature = "tokio03")]
use tokio03_crate::io::{AsyncRead as _, AsyncWrite as _, ReadBuf as Tokio03ReadBuf};
#[cfg(feature = "tokio03")]
use tokio03_crate::net::TcpStream as Tokio03TcpStream;
#[cfg(feature = "tokio02-native-tls")]
use tokio02_native_tls_crate::TlsStream;
use tokio02_native_tls_crate::TlsStream as Tokio02TlsStream;
#[cfg(feature = "tokio03-native-tls")]
use tokio03_native_tls_crate::TlsStream as Tokio03TlsStream;
#[cfg(feature = "tokio02-rustls-tls")]
use tokio02_rustls::client::TlsStream as RustlsTlsStream;
use tokio02_rustls::client::TlsStream as Tokio02RustlsTlsStream;
#[cfg(feature = "tokio03-rustls-tls")]
use tokio03_rustls::client::TlsStream as Tokio03RustlsTlsStream;
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
#[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio03-native-tls",
feature = "tokio03-rustls-tls"
))]
use super::InnerTlsParameters;
use super::TlsParameters;
use crate::transport::smtp::Error;
@@ -29,17 +42,29 @@ pub struct AsyncNetworkStream {
}
/// Represents the different types of underlying network streams
// usually only one TLS backend at a time is going to be enabled,
// so clippy::large_enum_variant doesn't make sense here
#[allow(clippy::large_enum_variant)]
#[allow(dead_code)]
enum InnerAsyncNetworkStream {
/// Plain TCP stream
/// Plain Tokio 0.2 TCP stream
#[cfg(feature = "tokio02")]
Tokio02Tcp(TcpStream),
/// Encrypted TCP stream
Tokio02Tcp(Tokio02TcpStream),
/// Encrypted Tokio 0.2 TCP stream
#[cfg(feature = "tokio02-native-tls")]
Tokio02NativeTls(TlsStream<TcpStream>),
/// Encrypted TCP stream
Tokio02NativeTls(Tokio02TlsStream<Tokio02TcpStream>),
/// Encrypted Tokio 0.2 TCP stream
#[cfg(feature = "tokio02-rustls-tls")]
Tokio02RustlsTls(Box<RustlsTlsStream<TcpStream>>),
Tokio02RustlsTls(Tokio02RustlsTlsStream<Tokio02TcpStream>),
/// Plain Tokio 0.3 TCP stream
#[cfg(feature = "tokio03")]
Tokio03Tcp(Tokio03TcpStream),
/// Encrypted Tokio 0.3 TCP stream
#[cfg(feature = "tokio03-native-tls")]
Tokio03NativeTls(Tokio03TlsStream<Tokio03TcpStream>),
/// Encrypted Tokio 0.3 TCP stream
#[cfg(feature = "tokio03-rustls-tls")]
Tokio03RustlsTls(Tokio03RustlsTlsStream<Tokio03TcpStream>),
/// Can't be built
None,
}
@@ -47,7 +72,7 @@ enum InnerAsyncNetworkStream {
impl AsyncNetworkStream {
fn new(inner: InnerAsyncNetworkStream) -> Self {
if let InnerAsyncNetworkStream::None = inner {
debug_assert!(false, "InnerAsyncNetworkStream::None should never be built");
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
}
AsyncNetworkStream { inner }
@@ -64,11 +89,19 @@ impl AsyncNetworkStream {
}
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref s) => s.get_ref().0.peer_addr(),
#[cfg(feature = "tokio03")]
InnerAsyncNetworkStream::Tokio03Tcp(ref s) => s.peer_addr(),
#[cfg(feature = "tokio03-native-tls")]
InnerAsyncNetworkStream::Tokio03NativeTls(ref s) => {
s.get_ref().get_ref().get_ref().peer_addr()
}
#[cfg(feature = "tokio03-rustls-tls")]
InnerAsyncNetworkStream::Tokio03RustlsTls(ref s) => s.get_ref().0.peer_addr(),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None should never be built");
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Err(IoError::new(
ErrorKind::Other,
"InnerAsyncNetworkStream::None should never be built",
"InnerAsyncNetworkStream::None must never be built",
))
}
}
@@ -85,8 +118,16 @@ impl AsyncNetworkStream {
}
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref s) => s.get_ref().0.shutdown(how),
#[cfg(feature = "tokio03")]
InnerAsyncNetworkStream::Tokio03Tcp(ref s) => s.shutdown(how),
#[cfg(feature = "tokio03-native-tls")]
InnerAsyncNetworkStream::Tokio03NativeTls(ref s) => {
s.get_ref().get_ref().get_ref().shutdown(how)
}
#[cfg(feature = "tokio03-rustls-tls")]
InnerAsyncNetworkStream::Tokio03RustlsTls(ref s) => s.get_ref().0.shutdown(how),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None should never be built");
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Ok(())
}
}
@@ -98,7 +139,7 @@ impl AsyncNetworkStream {
port: u16,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncNetworkStream, Error> {
let tcp_stream = TcpStream::connect((hostname, port)).await?;
let tcp_stream = Tokio02TcpStream::connect((hostname, port)).await?;
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio02Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters {
@@ -107,9 +148,27 @@ impl AsyncNetworkStream {
Ok(stream)
}
#[cfg(feature = "tokio03")]
pub async fn connect_tokio03(
hostname: &str,
port: u16,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncNetworkStream, Error> {
let tcp_stream = Tokio03TcpStream::connect((hostname, port)).await?;
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio03Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters {
stream.upgrade_tls(tls_parameters).await?;
}
Ok(stream)
}
pub async fn upgrade_tls(&mut self, tls_parameters: TlsParameters) -> Result<(), Error> {
match &self.inner {
#[cfg(not(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls")))]
#[cfg(all(
feature = "tokio02",
not(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))
))]
InnerAsyncNetworkStream::Tokio02Tcp(_) => {
let _ = tls_parameters;
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio02-native-tls or the tokio02-rustls-tls feature");
@@ -127,6 +186,27 @@ impl AsyncNetworkStream {
self.inner = Self::upgrade_tokio02_tls(tcp_stream, tls_parameters).await?;
Ok(())
}
#[cfg(all(
feature = "tokio03",
not(any(feature = "tokio03-native-tls", feature = "tokio03-rustls-tls"))
))]
InnerAsyncNetworkStream::Tokio03Tcp(_) => {
let _ = tls_parameters;
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio03-native-tls or the tokio03-rustls-tls feature");
}
#[cfg(any(feature = "tokio03-native-tls", feature = "tokio03-rustls-tls"))]
InnerAsyncNetworkStream::Tokio03Tcp(_) => {
// get owned TcpStream
let tcp_stream = std::mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
let tcp_stream = match tcp_stream {
InnerAsyncNetworkStream::Tokio03Tcp(tcp_stream) => tcp_stream,
_ => unreachable!(),
};
self.inner = Self::upgrade_tokio03_tls(tcp_stream, tls_parameters).await?;
Ok(())
}
_ => Ok(()),
}
}
@@ -134,7 +214,7 @@ impl AsyncNetworkStream {
#[allow(unused_variables)]
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
async fn upgrade_tokio02_tls(
tcp_stream: TcpStream,
tcp_stream: Tokio02TcpStream,
mut tls_parameters: TlsParameters,
) -> Result<InnerAsyncNetworkStream, Error> {
let domain = std::mem::take(&mut tls_parameters.domain);
@@ -167,7 +247,49 @@ impl AsyncNetworkStream {
let connector = TlsConnector::from(Arc::new(config));
let stream = connector.connect(domain, tcp_stream).await?;
Ok(InnerAsyncNetworkStream::Tokio02RustlsTls(Box::new(stream)))
Ok(InnerAsyncNetworkStream::Tokio02RustlsTls(stream))
};
}
}
}
#[allow(unused_variables)]
#[cfg(any(feature = "tokio03-native-tls", feature = "tokio03-rustls-tls"))]
async fn upgrade_tokio03_tls(
tcp_stream: Tokio03TcpStream,
mut tls_parameters: TlsParameters,
) -> Result<InnerAsyncNetworkStream, Error> {
let domain = std::mem::take(&mut tls_parameters.domain);
match tls_parameters.connector {
#[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls(connector) => {
#[cfg(not(feature = "tokio03-native-tls"))]
panic!("built without the tokio03-native-tls feature");
#[cfg(feature = "tokio03-native-tls")]
return {
use tokio03_native_tls_crate::TlsConnector;
let connector = TlsConnector::from(connector);
let stream = connector.connect(&domain, tcp_stream).await?;
Ok(InnerAsyncNetworkStream::Tokio03NativeTls(stream))
};
}
#[cfg(feature = "rustls-tls")]
InnerTlsParameters::RustlsTls(config) => {
#[cfg(not(feature = "tokio03-rustls-tls"))]
panic!("built without the tokio03-rustls-tls feature");
#[cfg(feature = "tokio03-rustls-tls")]
return {
use tokio03_rustls::{webpki::DNSNameRef, TlsConnector};
let domain = DNSNameRef::try_from_ascii_str(&domain)?;
let connector = TlsConnector::from(Arc::new(config));
let stream = connector.connect(domain, tcp_stream).await?;
Ok(InnerAsyncNetworkStream::Tokio03RustlsTls(stream))
};
}
}
@@ -181,6 +303,12 @@ impl AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio02NativeTls(_) => true,
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(_) => true,
#[cfg(feature = "tokio03")]
InnerAsyncNetworkStream::Tokio03Tcp(_) => false,
#[cfg(feature = "tokio03-native-tls")]
InnerAsyncNetworkStream::Tokio03NativeTls(_) => true,
#[cfg(feature = "tokio03-rustls-tls")]
InnerAsyncNetworkStream::Tokio03RustlsTls(_) => true,
InnerAsyncNetworkStream::None => false,
}
}
@@ -189,7 +317,7 @@ impl AsyncNetworkStream {
impl futures_io::AsyncRead for AsyncNetworkStream {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<IoResult<usize>> {
match self.inner {
@@ -199,8 +327,35 @@ impl futures_io::AsyncRead for AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "tokio03")]
InnerAsyncNetworkStream::Tokio03Tcp(ref mut s) => {
let mut b = Tokio03ReadBuf::new(buf);
match Pin::new(s).poll_read(cx, &mut b) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
Poll::Pending => Poll::Pending,
}
}
#[cfg(feature = "tokio03-native-tls")]
InnerAsyncNetworkStream::Tokio03NativeTls(ref mut s) => {
let mut b = Tokio03ReadBuf::new(buf);
match Pin::new(s).poll_read(cx, &mut b) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
Poll::Pending => Poll::Pending,
}
}
#[cfg(feature = "tokio03-rustls-tls")]
InnerAsyncNetworkStream::Tokio03RustlsTls(ref mut s) => {
let mut b = Tokio03ReadBuf::new(buf);
match Pin::new(s).poll_read(cx, &mut b) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
Poll::Pending => Poll::Pending,
}
}
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None should never be built");
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(0))
}
}
@@ -208,7 +363,11 @@ impl futures_io::AsyncRead for AsyncNetworkStream {
}
impl futures_io::AsyncWrite for AsyncNetworkStream {
fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context, buf: &[u8]) -> Poll<IoResult<usize>> {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<IoResult<usize>> {
match self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
@@ -216,14 +375,20 @@ impl futures_io::AsyncWrite for AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio03")]
InnerAsyncNetworkStream::Tokio03Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio03-native-tls")]
InnerAsyncNetworkStream::Tokio03NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio03-rustls-tls")]
InnerAsyncNetworkStream::Tokio03RustlsTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None should never be built");
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(0))
}
}
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<IoResult<()>> {
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
match self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
@@ -231,14 +396,20 @@ impl futures_io::AsyncWrite for AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio03")]
InnerAsyncNetworkStream::Tokio03Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio03-native-tls")]
InnerAsyncNetworkStream::Tokio03NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio03-rustls-tls")]
InnerAsyncNetworkStream::Tokio03RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None should never be built");
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(()))
}
}
}
fn poll_close(self: Pin<&mut Self>, _cx: &mut Context) -> Poll<IoResult<()>> {
fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<IoResult<()>> {
Poll::Ready(self.shutdown(Shutdown::Write))
}
}

View File

@@ -6,15 +6,13 @@ use std::{
};
use super::{ClientCodec, NetworkStream, TlsParameters};
use crate::{
transport::smtp::{
authentication::{Credentials, Mechanism},
commands::*,
error::Error,
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
response::{parse_response, Response},
},
Envelope,
use crate::address::Envelope;
use crate::transport::smtp::{
authentication::{Credentials, Mechanism},
commands::*,
error::Error,
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
response::{parse_response, Response},
};
#[cfg(feature = "tracing")]

View File

@@ -24,9 +24,9 @@
#[cfg(feature = "serde")]
use std::fmt::Debug;
#[cfg(feature = "tokio02")]
#[cfg(any(feature = "tokio02", feature = "tokio03"))]
pub(crate) use self::async_connection::AsyncSmtpConnection;
#[cfg(feature = "tokio02")]
#[cfg(any(feature = "tokio02", feature = "tokio03"))]
pub(crate) use self::async_net::AsyncNetworkStream;
use self::net::NetworkStream;
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
@@ -34,12 +34,12 @@ pub(super) use self::tls::InnerTlsParameters;
pub use self::{
connection::SmtpConnection,
mock::MockStream,
tls::{Tls, TlsParameters},
tls::{Certificate, Tls, TlsParameters, TlsParametersBuilder},
};
#[cfg(feature = "tokio02")]
#[cfg(any(feature = "tokio02", feature = "tokio03"))]
mod async_connection;
#[cfg(feature = "tokio02")]
#[cfg(any(feature = "tokio02", feature = "tokio03"))]
mod async_net;
mod connection;
mod mock;

View File

@@ -23,6 +23,9 @@ pub struct NetworkStream {
}
/// Represents the different types of underlying network streams
// usually only one TLS backend at a time is going to be enabled,
// so clippy::large_enum_variant doesn't make sense here
#[allow(clippy::large_enum_variant)]
enum InnerNetworkStream {
/// Plain TCP stream
Tcp(TcpStream),
@@ -31,7 +34,7 @@ enum InnerNetworkStream {
NativeTls(TlsStream<TcpStream>),
/// Encrypted TCP stream
#[cfg(feature = "rustls-tls")]
RustlsTls(Box<StreamOwned<ClientSession, TcpStream>>),
RustlsTls(StreamOwned<ClientSession, TcpStream>),
/// Mock stream
Mock(MockStream),
}
@@ -150,7 +153,7 @@ impl NetworkStream {
tcp_stream,
);
InnerNetworkStream::RustlsTls(Box::new(stream))
InnerNetworkStream::RustlsTls(stream)
}
})
}

View File

@@ -1,10 +1,14 @@
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
use crate::transport::smtp::error::Error;
#[cfg(feature = "rustls-tls")]
use std::sync::Arc;
#[cfg(feature = "native-tls")]
use native_tls::{Protocol, TlsConnector};
#[cfg(feature = "rustls-tls")]
use rustls::ClientConfig;
use rustls::{ClientConfig, RootCertStore, ServerCertVerified, ServerCertVerifier, TLSError};
#[cfg(feature = "rustls-tls")]
use webpki::DNSNameRef;
use crate::transport::smtp::error::Error;
/// Accepted protocols by default.
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults.
@@ -38,6 +42,138 @@ pub struct TlsParameters {
pub(super) domain: String,
}
/// Builder for `TlsParameters`
#[derive(Clone)]
pub struct TlsParametersBuilder {
domain: String,
root_certs: Vec<Certificate>,
accept_invalid_hostnames: bool,
accept_invalid_certs: bool,
}
impl TlsParametersBuilder {
/// Creates a new builder for `TlsParameters`
pub fn new(domain: String) -> Self {
Self {
domain,
root_certs: Vec::new(),
accept_invalid_hostnames: false,
accept_invalid_certs: false,
}
}
/// Add a custom root certificate
///
/// Can be used to safely connect to a server using a self signed certificate, for example.
pub fn add_root_certificate(&mut self, cert: Certificate) -> &mut Self {
self.root_certs.push(cert);
self
}
/// Controls whether certificates with an invalid hostname are accepted
///
/// Defaults to `false`.
///
/// # Warning
///
/// You should think very carefully before using this method.
/// If hostname verification is disabled *any* valid certificate,
/// including those from other sites, are trusted.
///
/// This method introduces significant vulnerabilities to man-in-the-middle attacks.
///
/// Hostname verification can only be disabled with the `native-tls` TLS backend.
#[cfg(feature = "native-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
pub fn dangerous_accept_invalid_hostnames(
&mut self,
accept_invalid_hostnames: bool,
) -> &mut Self {
self.accept_invalid_hostnames = accept_invalid_hostnames;
self
}
/// Controls whether invalid certificates are accepted
///
/// Defaults to `false`.
///
/// # Warning
///
/// You should think very carefully before using this method.
/// If certificate verification is disabled, *any* certificate
/// is trusted for use, including:
///
/// * Self signed certificates
/// * Certificates from different hostnames
/// * Expired certificates
///
/// This method should only be used as a last resort, as it introduces
/// significant vulnerabilities to man-in-the-middle attacks.
pub fn dangerous_accept_invalid_certs(&mut self, accept_invalid_certs: bool) -> &mut Self {
self.accept_invalid_certs = accept_invalid_certs;
self
}
/// Creates a new `TlsParameters` using native-tls or rustls
/// depending on which one is available
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
pub fn build(self) -> Result<TlsParameters, Error> {
#[cfg(feature = "native-tls")]
return self.build_native();
#[cfg(not(feature = "native-tls"))]
return self.build_rustls();
}
/// Creates a new `TlsParameters` using native-tls with the provided configuration
#[cfg(feature = "native-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
pub fn build_native(self) -> Result<TlsParameters, Error> {
let mut tls_builder = TlsConnector::builder();
for cert in self.root_certs {
tls_builder.add_root_certificate(cert.native_tls);
}
tls_builder.danger_accept_invalid_hostnames(self.accept_invalid_hostnames);
tls_builder.danger_accept_invalid_certs(self.accept_invalid_certs);
tls_builder.min_protocol_version(Some(DEFAULT_TLS_MIN_PROTOCOL));
let connector = tls_builder.build()?;
Ok(TlsParameters {
connector: InnerTlsParameters::NativeTls(connector),
domain: self.domain,
})
}
/// Creates a new `TlsParameters` using rustls with the provided configuration
#[cfg(feature = "rustls-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
pub fn build_rustls(self) -> Result<TlsParameters, Error> {
use webpki_roots::TLS_SERVER_ROOTS;
let mut tls = ClientConfig::new();
for cert in self.root_certs {
for rustls_cert in cert.rustls {
tls.root_store
.add(&rustls_cert)
.map_err(|_| Error::InvalidCertificate)?;
}
}
if self.accept_invalid_certs {
tls.dangerous()
.set_certificate_verifier(Arc::new(InvalidCertsVerifier {}));
}
tls.root_store.add_server_trust_anchors(&TLS_SERVER_ROOTS);
Ok(TlsParameters {
connector: InnerTlsParameters::RustlsTls(tls),
domain: self.domain,
})
}
}
#[derive(Clone)]
pub enum InnerTlsParameters {
#[cfg(feature = "native-tls")]
@@ -50,49 +186,95 @@ impl TlsParameters {
/// Creates a new `TlsParameters` using native-tls or rustls
/// depending on which one is available
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
pub fn new(domain: String) -> Result<Self, Error> {
#[cfg(feature = "native-tls")]
return Self::new_native(domain);
#[cfg(not(feature = "native-tls"))]
return Self::new_rustls(domain);
TlsParametersBuilder::new(domain).build()
}
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
pub(crate) fn new_tokio02(domain: String) -> Result<Self, Error> {
#[cfg(feature = "tokio02-native-tls")]
return Self::new_native(domain);
#[cfg(not(feature = "tokio02-native-tls"))]
return Self::new_rustls(domain);
pub fn builder(domain: String) -> TlsParametersBuilder {
TlsParametersBuilder::new(domain)
}
/// Creates a new `TlsParameters` using native-tls
#[cfg(feature = "native-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
pub fn new_native(domain: String) -> Result<Self, Error> {
let mut tls_builder = TlsConnector::builder();
tls_builder.min_protocol_version(Some(DEFAULT_TLS_MIN_PROTOCOL));
let connector = tls_builder.build()?;
Ok(Self {
connector: InnerTlsParameters::NativeTls(connector),
domain,
})
TlsParametersBuilder::new(domain).build_native()
}
/// Creates a new `TlsParameters` using rustls
#[cfg(feature = "rustls-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
pub fn new_rustls(domain: String) -> Result<Self, Error> {
use webpki_roots::TLS_SERVER_ROOTS;
let mut tls = ClientConfig::new();
tls.root_store.add_server_trust_anchors(&TLS_SERVER_ROOTS);
Ok(Self {
connector: InnerTlsParameters::RustlsTls(tls),
domain,
})
TlsParametersBuilder::new(domain).build_rustls()
}
pub fn domain(&self) -> &str {
&self.domain
}
}
/// A client certificate that can be used with [`TlsParametersBuilder::add_root_certificate`]
#[derive(Clone)]
#[allow(missing_copy_implementations)]
pub struct Certificate {
#[cfg(feature = "native-tls")]
native_tls: native_tls::Certificate,
#[cfg(feature = "rustls-tls")]
rustls: Vec<rustls::Certificate>,
}
impl Certificate {
/// Create a `Certificate` from a DER encoded certificate
pub fn from_der(der: Vec<u8>) -> Result<Self, Error> {
#[cfg(feature = "native-tls")]
let native_tls_cert =
native_tls::Certificate::from_der(&der).map_err(|_| Error::InvalidCertificate)?;
Ok(Self {
#[cfg(feature = "native-tls")]
native_tls: native_tls_cert,
#[cfg(feature = "rustls-tls")]
rustls: vec![rustls::Certificate(der)],
})
}
/// Create a `Certificate` from a PEM encoded certificate
pub fn from_pem(pem: &[u8]) -> Result<Self, Error> {
#[cfg(feature = "native-tls")]
let native_tls_cert =
native_tls::Certificate::from_pem(pem).map_err(|_| Error::InvalidCertificate)?;
#[cfg(feature = "rustls-tls")]
let rustls_cert = {
use rustls::internal::pemfile;
use std::io::Cursor;
let mut pem = Cursor::new(pem);
pemfile::certs(&mut pem).map_err(|_| Error::InvalidCertificate)?
};
Ok(Self {
#[cfg(feature = "native-tls")]
native_tls: native_tls_cert,
#[cfg(feature = "rustls-tls")]
rustls: rustls_cert,
})
}
}
#[cfg(feature = "rustls-tls")]
struct InvalidCertsVerifier;
#[cfg(feature = "rustls-tls")]
impl ServerCertVerifier for InvalidCertsVerifier {
fn verify_server_cert(
&self,
_roots: &RootCertStore,
_presented_certs: &[rustls::Certificate],
_dns_name: DNSNameRef<'_>,
_ocsp_response: &[u8],
) -> Result<ServerCertVerified, TLSError> {
Ok(ServerCertVerified::assertion())
}
}

View File

@@ -9,10 +9,7 @@ use crate::{
},
Address,
};
use std::{
convert::AsRef,
fmt::{self, Display, Formatter},
};
use std::fmt::{self, Display, Formatter};
/// EHLO command
#[derive(PartialEq, Clone, Debug)]
@@ -22,7 +19,7 @@ pub struct Ehlo {
}
impl Display for Ehlo {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "EHLO {}\r\n", self.client_id)
}
}
@@ -40,7 +37,7 @@ impl Ehlo {
pub struct Starttls;
impl Display for Starttls {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("STARTTLS\r\n")
}
}
@@ -54,7 +51,7 @@ pub struct Mail {
}
impl Display for Mail {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"MAIL FROM:<{}>",
@@ -83,7 +80,7 @@ pub struct Rcpt {
}
impl Display for Rcpt {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "RCPT TO:<{}>", self.recipient)?;
for parameter in &self.parameters {
write!(f, " {}", parameter)?;
@@ -108,7 +105,7 @@ impl Rcpt {
pub struct Data;
impl Display for Data {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("DATA\r\n")
}
}
@@ -119,7 +116,7 @@ impl Display for Data {
pub struct Quit;
impl Display for Quit {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("QUIT\r\n")
}
}
@@ -130,7 +127,7 @@ impl Display for Quit {
pub struct Noop;
impl Display for Noop {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("NOOP\r\n")
}
}
@@ -143,7 +140,7 @@ pub struct Help {
}
impl Display for Help {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("HELP")?;
if let Some(argument) = &self.argument {
write!(f, " {}", argument)?;
@@ -167,7 +164,7 @@ pub struct Vrfy {
}
impl Display for Vrfy {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "VRFY {}\r\n", self.argument)
}
}
@@ -187,7 +184,7 @@ pub struct Expn {
}
impl Display for Expn {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "EXPN {}\r\n", self.argument)
}
}
@@ -205,7 +202,7 @@ impl Expn {
pub struct Rset;
impl Display for Rset {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("RSET\r\n")
}
}
@@ -221,7 +218,7 @@ pub struct Auth {
}
impl Display for Auth {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let encoded_response = self.response.as_ref().map(base64::encode);
if self.mechanism.supports_initial_response() {
@@ -338,7 +335,7 @@ mod test {
"RCPT TO:<test@example.com>\r\n"
);
assert_eq!(
format!("{}", Rcpt::new(email.clone(), vec![rcpt_parameter])),
format!("{}", Rcpt::new(email, vec![rcpt_parameter])),
"RCPT TO:<test@example.com> TEST=value\r\n"
);
assert_eq!(format!("{}", Quit), "QUIT\r\n");
@@ -369,7 +366,7 @@ mod test {
assert_eq!(
format!(
"{}",
Auth::new(Mechanism::Login, credentials.clone(), None).unwrap()
Auth::new(Mechanism::Login, credentials, None).unwrap()
),
"AUTH LOGIN\r\n"
);

View File

@@ -35,18 +35,24 @@ pub enum Error {
Io(io::Error),
/// TLS error
#[cfg(feature = "native-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
Tls(native_tls::Error),
/// Parsing error
Parsing(nom::error::ErrorKind),
/// Invalid hostname
#[cfg(feature = "rustls-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
InvalidDNSName(webpki::InvalidDNSNameError),
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
InvalidCertificate,
#[cfg(feature = "r2d2")]
#[cfg_attr(docsrs, doc(cfg(feature = "r2d2")))]
Pool(r2d2::Error),
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
match *self {
// Try to display the first line of the server's response that usually
// contains a short humanly readable error message
@@ -69,6 +75,8 @@ impl Display for Error {
Parsing(ref err) => fmt.write_str(err.description()),
#[cfg(feature = "rustls-tls")]
InvalidDNSName(ref err) => err.fmt(fmt),
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
InvalidCertificate => fmt.write_str("invalid certificate"),
#[cfg(feature = "r2d2")]
Pool(ref err) => err.fmt(fmt),
}

View File

@@ -48,7 +48,7 @@ impl Default for ClientId {
}
impl Display for ClientId {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
Self::Domain(ref value) => f.write_str(value),
Self::Ipv4(ref value) => write!(f, "[{}]", value),
@@ -86,7 +86,7 @@ pub enum Extension {
}
impl Display for Extension {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
Extension::EightBitMime => f.write_str("8BITMIME"),
Extension::SmtpUtfEight => f.write_str("SMTPUTF8"),
@@ -111,7 +111,7 @@ pub struct ServerInfo {
}
impl Display for ServerInfo {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let features = if self.features.is_empty() {
"no supported features".to_string()
} else {
@@ -215,7 +215,7 @@ pub enum MailParameter {
}
impl Display for MailParameter {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
MailParameter::Body(ref value) => write!(f, "BODY={}", value),
MailParameter::Size(size) => write!(f, "SIZE={}", size),
@@ -243,7 +243,7 @@ pub enum MailBodyParameter {
}
impl Display for MailBodyParameter {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
MailBodyParameter::SevenBit => f.write_str("7BIT"),
MailBodyParameter::EightBitMime => f.write_str("8BITMIME"),
@@ -265,7 +265,7 @@ pub enum RcptParameter {
}
impl Display for RcptParameter {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
RcptParameter::Other {
ref keyword,
@@ -320,7 +320,7 @@ mod test {
"{}",
ServerInfo {
name: "name".to_string(),
features: eightbitmime.clone(),
features: eightbitmime,
}
),
"name with {EightBitMime}".to_string()
@@ -347,7 +347,7 @@ mod test {
"{}",
ServerInfo {
name: "name".to_string(),
features: plain.clone(),
features: plain,
}
),
"name with {Authentication(Plain)}".to_string()

View File

@@ -155,9 +155,15 @@
//!
#[cfg(feature = "tokio02")]
pub use self::async_transport::Tokio02Connector;
#[cfg(feature = "tokio03")]
pub use self::async_transport::Tokio03Connector;
#[cfg(any(feature = "tokio02", feature = "tokio03"))]
pub use self::async_transport::{
AsyncSmtpConnector, AsyncSmtpTransport, AsyncSmtpTransportBuilder, Tokio02Connector,
AsyncSmtpConnector, AsyncSmtpTransport, AsyncSmtpTransportBuilder,
};
#[cfg(feature = "r2d2")]
pub use self::pool::PoolConfig;
pub(crate) use self::transport::SmtpClient;
pub use self::{
error::Error,
@@ -174,7 +180,7 @@ use crate::transport::smtp::{
use client::Tls;
use std::time::Duration;
#[cfg(feature = "tokio02")]
#[cfg(any(feature = "tokio02", feature = "tokio03"))]
mod async_transport;
pub mod authentication;
pub mod client;
@@ -182,7 +188,7 @@ pub mod commands;
mod error;
pub mod extension;
#[cfg(feature = "r2d2")]
pub mod pool;
mod pool;
pub mod response;
mod transport;
pub mod util;

View File

@@ -1,5 +1,73 @@
use std::time::Duration;
use crate::transport::smtp::{client::SmtpConnection, error::Error, SmtpClient};
use r2d2::ManageConnection;
use r2d2::{ManageConnection, Pool};
/// Configuration for a connection pool
#[derive(Debug, Clone)]
#[allow(missing_copy_implementations)]
#[cfg_attr(docsrs, doc(cfg(feature = "r2d2")))]
pub struct PoolConfig {
min_idle: u32,
max_size: u32,
connection_timeout: Duration,
idle_timeout: Duration,
}
impl PoolConfig {
/// Minimum number of idle connections
///
/// Defaults to `0`
pub fn min_idle(mut self, min_idle: u32) -> Self {
self.min_idle = min_idle;
self
}
/// Maximum number of pooled connections
///
/// Defaults to `10`
pub fn max_size(mut self, max_size: u32) -> Self {
self.min_idle = max_size;
self
}
/// Connection timeout
///
/// Defaults to `30 seconds`
pub fn connection_timeout(mut self, connection_timeout: Duration) -> Self {
self.connection_timeout = connection_timeout;
self
}
/// Connection idle timeout
///
/// Defaults to `60 seconds`
pub fn idle_timeout(mut self, idle_timeout: Duration) -> Self {
self.idle_timeout = idle_timeout;
self
}
pub(crate) fn build<C: ManageConnection>(&self, client: C) -> Pool<C> {
Pool::builder()
.min_idle(Some(self.min_idle))
.max_size(self.max_size)
.connection_timeout(self.connection_timeout)
.idle_timeout(Some(self.idle_timeout))
.build_unchecked(client)
}
}
impl Default for PoolConfig {
fn default() -> Self {
Self {
min_idle: 0,
max_size: 10,
connection_timeout: Duration::from_secs(30),
idle_timeout: Duration::from_secs(60),
}
}
}
impl ManageConnection for SmtpClient {
type Connection = SmtpConnection;

View File

@@ -32,7 +32,7 @@ pub enum Severity {
}
impl Display for Severity {
fn fmt(&self, f: &mut Formatter) -> Result {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(f, "{}", *self as u8)
}
}
@@ -56,7 +56,7 @@ pub enum Category {
}
impl Display for Category {
fn fmt(&self, f: &mut Formatter) -> Result {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(f, "{}", *self as u8)
}
}
@@ -88,7 +88,7 @@ pub enum Detail {
}
impl Display for Detail {
fn fmt(&self, f: &mut Formatter) -> Result {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(f, "{}", *self as u8)
}
}
@@ -106,7 +106,7 @@ pub struct Code {
}
impl Display for Code {
fn fmt(&self, f: &mut Formatter) -> Result {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(f, "{}{}{}", self.severity, self.category, self.detail)
}
}
@@ -151,10 +151,10 @@ impl Response {
/// Tells if the response is positive
pub fn is_positive(&self) -> bool {
match self.code.severity {
Severity::PositiveCompletion | Severity::PositiveIntermediate => true,
_ => false,
}
matches!(
self.code.severity,
Severity::PositiveCompletion | Severity::PositiveIntermediate
)
}
/// Tests code equality

View File

@@ -3,10 +3,13 @@ use std::time::Duration;
#[cfg(feature = "r2d2")]
use r2d2::Pool;
#[cfg(feature = "r2d2")]
use super::PoolConfig;
use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, SmtpInfo};
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
use super::{Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
use crate::{Envelope, Transport};
use crate::address::Envelope;
use crate::Transport;
#[allow(missing_debug_implementations)]
#[derive(Clone)]
@@ -95,7 +98,12 @@ impl SmtpTransport {
pub fn builder_dangerous<T: Into<String>>(server: T) -> SmtpTransportBuilder {
let mut new = SmtpInfo::default();
new.server = server.into();
SmtpTransportBuilder { info: new }
SmtpTransportBuilder {
info: new,
#[cfg(feature = "r2d2")]
pool_config: PoolConfig::default(),
}
}
}
@@ -104,6 +112,8 @@ impl SmtpTransport {
#[derive(Clone)]
pub struct SmtpTransportBuilder {
info: SmtpInfo,
#[cfg(feature = "r2d2")]
pool_config: PoolConfig,
}
/// Builder for the SMTP `SmtpTransport`
@@ -145,27 +155,25 @@ impl SmtpTransportBuilder {
self
}
/// Build the client
fn build_client(self) -> SmtpClient {
SmtpClient { info: self.info }
/// Use a custom configuration for the connection pool
///
/// Defaults can be found at [`PoolConfig`]
#[cfg(feature = "r2d2")]
#[cfg_attr(docsrs, doc(cfg(feature = "r2d2")))]
pub fn pool_config(mut self, pool_config: PoolConfig) -> Self {
self.pool_config = pool_config;
self
}
/// Build the transport
///
/// If the `r2d2` feature is enabled an `Arc` wrapped pool is be created.
/// Defaults:
///
/// * 60 seconds idle timeout
/// * 30 minutes max connection lifetime
/// * max pool size of 10 connections
/// Defaults can be found at [`PoolConfig`]
pub fn build(self) -> SmtpTransport {
let client = self.build_client();
let client = SmtpClient { info: self.info };
SmtpTransport {
#[cfg(feature = "r2d2")]
inner: Pool::builder()
.min_idle(Some(0))
.idle_timeout(Some(Duration::from_secs(60)))
.build_unchecked(client),
inner: self.pool_config.build(client),
#[cfg(not(feature = "r2d2"))]
inner: client,
}
@@ -190,6 +198,7 @@ impl SmtpClient {
_ => None,
};
#[allow(unused_mut)]
let mut conn = SmtpConnection::connect::<(&str, u16)>(
(self.info.server.as_ref(), self.info.port),
self.info.timeout,

View File

@@ -8,7 +8,7 @@ use std::fmt::{Display, Formatter, Result as FmtResult};
pub struct XText<'a>(pub &'a str);
impl<'a> Display for XText<'a> {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let mut rest = self.0;
while let Some(idx) = rest.find(|c| c < '!' || c == '+' || c == '=') {
let (start, end) = rest.split_at(idx);

View File

@@ -7,7 +7,8 @@
//! testing purposes.
//!
//! ```rust
//! use lettre::{Message, Envelope, Transport, StubTransport};
//! use lettre::{Message, Transport};
//! use lettre::transport::stub::StubTransport;
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
@@ -22,11 +23,12 @@
//! assert!(result.is_ok());
//! ```
use crate::address::Envelope;
#[cfg(feature = "async-std1")]
use crate::AsyncStd1Transport;
#[cfg(feature = "tokio02")]
use crate::Tokio02Transport;
use crate::{Envelope, Transport};
use crate::Transport;
#[cfg(any(feature = "async-std1", feature = "tokio02"))]
use async_trait::async_trait;
use std::{error::Error as StdError, fmt};

View File

@@ -1,6 +1,8 @@
#[cfg(all(test, feature = "smtp-transport", feature = "r2d2"))]
mod test {
use lettre::{Envelope, SmtpTransport, Transport};
use lettre::address::Envelope;
use lettre::{SmtpTransport, Transport};
use std::{sync::mpsc, thread};
fn envelope() -> Envelope {