Compare commits

...

10 Commits

Author SHA1 Message Date
Alexis Mousset
fe8dc4967d Prepare 0.10.0-beta.3 (#578) 2021-03-18 20:28:02 +00:00
Paolo Barbolini
137566a4e4 Improve docs in lib.rs (#574)
* Improve docs in lib.rs

* Typos
2021-03-18 08:02:21 +00:00
Paolo Barbolini
216c612931 Fix missing re-export of AsyncSmtpTransport when using async-std1 feature (#573) 2021-03-17 06:52:57 +00:00
Paolo Barbolini
a429a24913 Add missing Debug implementations (#570) 2021-03-14 10:17:07 +01:00
Paolo Barbolini
648bf2b2f6 chore: remove some uses of * (#569) 2021-03-14 09:22:59 +01:00
Alexis Mousset
509a623a27 feat(transport): Seal file and sendmail error types (#567) 2021-03-14 07:42:52 +00:00
Paolo Barbolini
a681c6b49d Remove From implementations on Error for file and sendmail transport (#566) 2021-03-13 17:41:44 +00:00
Alexis Mousset
22efe341fe feat(builder): Seal SMTP error type (#564)
* feat(builder): Seal SMTP error type

* More precise error types
2021-03-13 17:15:21 +00:00
Paolo Barbolini
97fba6a47e docs: improve docs for lettre::transport (#565) 2021-03-13 16:03:05 +00:00
Paolo Barbolini
f7066ac858 Fix various parts of the docs (#563) 2021-03-12 20:02:31 +01:00
27 changed files with 913 additions and 485 deletions

View File

@@ -1,7 +1,7 @@
[package]
name = "lettre"
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
version = "0.10.0-beta.2"
version = "0.10.0-beta.3"
description = "Email client"
readme = "README.md"
homepage = "https://lettre.rs"
@@ -36,7 +36,7 @@ serde = { version = "1", optional = true, features = ["derive"] }
serde_json = { version = "1", optional = true }
# smtp
nom = { version = "6", default-features = false, features = ["alloc"], optional = true }
nom = { version = "6", default-features = false, features = ["alloc", "std"], optional = true }
r2d2 = { version = "0.8", optional = true } # feature
hostname = { version = "0.3", optional = true } # feature

View File

@@ -28,8 +28,8 @@
</div>
<div align="center">
<a href="https://deps.rs/crate/lettre/0.10.0-beta.2">
<img src="https://deps.rs/crate/lettre/0.10.0-beta.2/status.svg"
<a href="https://deps.rs/crate/lettre/0.10.0-beta.3">
<img src="https://deps.rs/crate/lettre/0.10.0-beta.3/status.svg"
alt="dependency status" />
</a>
</div>
@@ -66,7 +66,7 @@ To use this library, add the following to your `Cargo.toml`:
```toml
[dependencies]
lettre = "0.10.0-beta.2"
lettre = "0.10.0-beta.3"
```
```rust,no_run

View File

@@ -27,15 +27,12 @@ impl Envelope {
///
/// # Examples
///
/// ```
/// use std::str::FromStr;
/// # use lettre::Address;
/// # use lettre::address::Envelope;
///
/// ```rust
/// # use lettre::address::{Address, Envelope};
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let sender = Address::from_str("from@email.com")?;
/// let recipients = vec![Address::from_str("to@email.com")?];
/// let sender = "sender@email.com".parse::<Address>()?;
/// let recipients = vec!["to@email.com".parse::<Address>()?];
///
/// let envelope = Envelope::new(Some(sender), recipients);
/// # Ok(())
@@ -59,15 +56,12 @@ impl Envelope {
///
/// # Examples
///
/// ```
/// use std::str::FromStr;
/// # use lettre::Address;
/// # use lettre::address::Envelope;
///
/// ```rust
/// # use lettre::address::{Address, Envelope};
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let sender = Address::from_str("from@email.com")?;
/// let recipients = vec![Address::from_str("to@email.com")?];
/// let sender = "from@email.com".parse::<Address>()?;
/// let recipients = vec!["to@email.com".parse::<Address>()?];
///
/// let envelope = Envelope::new(Some(sender), recipients.clone())?;
/// assert_eq!(envelope.to(), recipients.as_slice());
@@ -82,15 +76,12 @@ impl Envelope {
///
/// # Examples
///
/// ```
/// use std::str::FromStr;
/// # use lettre::Address;
/// # use lettre::address::Envelope;
///
/// ```rust
/// # use lettre::address::{Address, Envelope};
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let sender = Address::from_str("from@email.com")?;
/// let recipients = vec![Address::from_str("to@email.com")?];
/// let sender = "from@email.com".parse::<Address>()?;
/// let recipients = vec!["to@email.com".parse::<Address>()?];
///
/// let envelope = Envelope::new(Some(sender), recipients.clone())?;
/// assert!(envelope.from().is_some());
@@ -104,6 +95,7 @@ impl Envelope {
self.reverse_path.as_ref()
}
#[cfg(feature = "smtp-transport")]
/// Check if any of the addresses in the envelope contains non-ascii chars
pub(crate) fn has_non_ascii_addresses(&self) -> bool {
self.reverse_path

View File

@@ -23,10 +23,13 @@ use std::{
/// You can create an `Address` from a user and a domain:
///
/// ```
/// # use lettre::Address;
/// use lettre::Address;
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("example", "email.com")?;
/// let address = Address::new("user", "email.com")?;
/// assert_eq!(address.user(), "user");
/// assert_eq!(address.domain(), "email.com");
/// # Ok(())
/// # }
/// ```
@@ -34,11 +37,13 @@ use std::{
/// You can also create an `Address` from a string literal by parsing it:
///
/// ```
/// use std::str::FromStr;
/// # use lettre::Address;
/// use lettre::Address;
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::from_str("example@email.com")?;
/// let address = "user@email.com".parse::<Address>()?;
/// assert_eq!(address.user(), "user");
/// assert_eq!(address.domain(), "email.com");
/// # Ok(())
/// # }
/// ```
@@ -50,28 +55,6 @@ pub struct Address {
at_start: usize,
}
impl<U, D> TryFrom<(U, D)> for Address
where
U: AsRef<str>,
D: AsRef<str>,
{
type Error = AddressError;
fn try_from((user, domain): (U, D)) -> Result<Self, Self::Error> {
let user = user.as_ref();
Address::check_user(user)?;
let domain = domain.as_ref();
Address::check_domain(domain)?;
let serialized = format!("{}@{}", user, domain);
Ok(Address {
serialized,
at_start: user.len(),
})
}
}
// Regex from the specs
// https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address
// It will mark esoteric email addresses like quoted string as invalid
@@ -96,8 +79,8 @@ impl Address {
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("example", "email.com")?;
/// let expected: Address = "example@email.com".parse()?;
/// let address = Address::new("user", "email.com")?;
/// let expected = "user@email.com".parse::<Address>()?;
/// assert_eq!(expected, address);
/// # Ok(())
/// # }
@@ -115,8 +98,8 @@ impl Address {
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("example", "email.com")?;
/// assert_eq!("example", address.user());
/// let address = Address::new("user", "email.com")?;
/// assert_eq!(address.user(), "user");
/// # Ok(())
/// # }
/// ```
@@ -133,8 +116,8 @@ impl Address {
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("example", "email.com")?;
/// assert_eq!("email.com", address.domain());
/// let address = Address::new("user", "email.com")?;
/// assert_eq!(address.domain(), "email.com");
/// # Ok(())
/// # }
/// ```
@@ -174,6 +157,7 @@ impl Address {
Err(AddressError::InvalidDomain)
}
#[cfg(feature = "smtp-transport")]
/// Check if the address contains non-ascii chars
pub(super) fn is_ascii(&self) -> bool {
self.serialized.is_ascii()
@@ -203,6 +187,28 @@ impl FromStr for Address {
}
}
impl<U, D> TryFrom<(U, D)> for Address
where
U: AsRef<str>,
D: AsRef<str>,
{
type Error = AddressError;
fn try_from((user, domain): (U, D)) -> Result<Self, Self::Error> {
let user = user.as_ref();
Address::check_user(user)?;
let domain = domain.as_ref();
Address::check_domain(domain)?;
let serialized = format!("{}@{}", user, domain);
Ok(Address {
serialized,
at_start: user.len(),
})
}
}
impl AsRef<str> for Address {
fn as_ref(&self) -> &str {
&self.serialized

View File

@@ -1,5 +1,6 @@
use async_trait::async_trait;
use std::fmt::Debug;
#[cfg(feature = "file-transport")]
use std::io::Result as IoResult;
#[cfg(feature = "file-transport")]
@@ -26,8 +27,20 @@ use crate::transport::smtp::extension::ClientId;
))]
use crate::transport::smtp::Error;
/// Async executor abstraction trait
///
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
/// in order to be able to work with different async runtimes.
///
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))
)]
#[async_trait]
pub trait Executor: Send + Sync + private::Sealed {
pub trait Executor: Debug + Send + Sync + private::Sealed {
#[doc(hidden)]
#[cfg(feature = "smtp-transport")]
async fn connect(
@@ -46,10 +59,19 @@ pub trait Executor: Send + Sync + private::Sealed {
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()>;
}
/// Async [`Executor`] using `tokio` `0.2.x`
///
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
/// in order to be able to work with different async runtimes.
///
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
#[allow(missing_copy_implementations)]
#[non_exhaustive]
#[cfg(feature = "tokio02")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio02")))]
#[derive(Debug)]
pub struct Tokio02Executor;
#[async_trait]
@@ -103,10 +125,19 @@ impl Executor for Tokio02Executor {
}
}
/// Async [`Executor`] using `tokio` `1.x`
///
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
/// in order to be able to work with different async runtimes.
///
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
#[allow(missing_copy_implementations)]
#[non_exhaustive]
#[cfg(feature = "tokio1")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
#[derive(Debug)]
pub struct Tokio1Executor;
#[async_trait]
@@ -159,10 +190,19 @@ impl Executor for Tokio1Executor {
}
}
/// Async [`Executor`] using `async-std` `1.x`
///
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
/// in order to be able to work with different async runtimes.
///
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
#[allow(missing_copy_implementations)]
#[non_exhaustive]
#[cfg(feature = "async-std1")]
#[cfg_attr(docsrs, doc(cfg(feature = "async-std1")))]
#[derive(Debug)]
pub struct AsyncStd1Executor;
#[async_trait]

View File

@@ -4,33 +4,102 @@
//! * Pluggable email transports
//! * Unicode support
//! * Secure defaults
//! * Async support
//!
//! Lettre requires Rust 1.45 or newer.
//!
//! ## Optional features
//! ## Features
//!
//! This section lists each lettre feature and briefly explains it.
//! More info about each module can be found in the corresponding module page.
//!
//! Features with `📫` near them are enabled by default.
//!
//! ### Typed message builder
//!
//! _Strongly typed [`message`] builder_
//!
//! * **builder** 📫: Enable the [`Message`] builder
//! * **hostname** 📫: Try to use the actual system hostname in the `Message-ID` header
//!
//! ### SMTP transport
//!
//! _Send emails using [`SMTP`]_
//!
//! * **smtp-transport** 📫: Enable the SMTP transport
//! * **r2d2** 📫: Connection pool for SMTP transport
//! * **hostname** 📫: Try to use the actual system hostname for the SMTP `CLIENTID`
//!
//! #### SMTP over TLS via the native-tls crate
//!
//! _Secure SMTP connections using TLS from the `native-tls` crate_
//!
//! Uses schannel on Windows, Security-Framework on macOS, and OpenSSL on Linux.
//!
//! * **native-tls** 📫: TLS support for the synchronous version of the API
//! * **tokio02-native-tls**: TLS support for the `tokio02` async version of the API
//! * **tokio1-native-tls**: TLS support for the `tokio1` async version of the API
//!
//! NOTE: native-tls isn't supported with `async-std`
//!
//! #### SMTP over TLS via the rustls crate
//!
//! _Secure SMTP connections using TLS from the `rustls-tls` crate_
//!
//! Rustls uses [ring] as the cryptography implementation. As a result, [not all Rust's targets are supported][ring-support].
//!
//! * **rustls-tls**: TLS support for the synchronous version of the API
//! * **tokio02-rustls-tls**: TLS support for the `tokio02` async version of the API
//! * **tokio1-rustls-tls**: TLS support for the `tokio1` async version of the API
//! * **async-std1-rustls-tls**: TLS support for the `async-std1` async version of the API
//!
//! ### Sendmail transport
//!
//! _Send emails using the [`sendmail`] command_
//!
//! * **sendmail-transport**: Enable the `sendmail` transport
//!
//! ### File transport
//!
//! _Save emails as an `.eml` [`file`]_
//!
//! * **file-transport**: Enable the file transport (saves emails into an `.eml` file)
//! * **file-transport-envelope**: Allow writing the envelope into a JSON file (additionally saves envelopes into a `.json` file)
//!
//! ### Async execution runtimes
//!
//! _Use [tokio] or [async-std] as an async execution runtime for sending emails_
//!
//! The correct runtime version must be chosen in order for lettre to work correctly.
//! For example, when sending emails from a Tokio 1.3.0 context, the Tokio 1.x executor
//! ([`Tokio1Executor`]) must be used. Using a different version (for example Tokio 0.2.x),
//! or async-std, would result in a runtime panic.
//!
//! * **tokio02**: Allow to asynchronously send emails using [Tokio 0.2.x]
//! * **tokio1**: Allow to asynchronously send emails using [Tokio 1.x]
//! * **async-std1**: Allow to asynchronously send emails using [async-std 1.x]
//!
//! NOTE: native-tls isn't supported with `async-std`
//!
//! ### Misc features
//!
//! _Additional features_
//!
//! * **builder**: Message builder
//! * **file-transport**: Transport that write messages into a file
//! * **file-transport-envelope**: Allow writing the envelope into a JSON file
//! * **smtp-transport**: Transport over SMTP
//! * **sendmail-transport**: Transport over SMTP
//! * **rustls-tls**: TLS support with the `rustls` crate
//! * **native-tls**: TLS support with the `native-tls` crate
//! * **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
//! * **tokio1**: Allow to asyncronously send emails using tokio 1.x
//! * **tokio1-rustls-tls**: Async TLS support with the `rustls` crate using tokio 1.x
//! * **tokio1-native-tls**: Async TLS support with the `native-tls` crate using tokio 1.x
//! * **async-std1**: Allow to asynchronously send emails using async-std 1.x
//! * NOTE: native-tls isn't supported with async-std at the moment
//! * **async-std1-rustls-tls**: Async TLS support with the `rustls` crate using async-std 1.x
//! * **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
//! * **tracing**: Logging using the `tracing` crate
//!
//! [`SMTP`]: crate::transport::smtp
//! [`sendmail`]: crate::transport::sendmail
//! [`file`]: crate::transport::file
//! [tokio]: https://docs.rs/tokio/1
//! [async-std]: https://docs.rs/async-std/1
//! [ring]: https://github.com/briansmith/ring#ring
//! [ring-support]: https://github.com/briansmith/ring#online-automated-testing
//! [Tokio 0.2.x]: https://docs.rs/tokio/0.2
//! [Tokio 1.x]: https://docs.rs/tokio/1
//! [async-std 1.x]: https://docs.rs/async-std/1
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.0-beta.2")]
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.0-beta.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)]
@@ -46,7 +115,7 @@
pub mod address;
pub mod error;
#[cfg(all(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))]
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
mod executor;
#[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
@@ -66,34 +135,44 @@ pub use self::executor::Tokio02Executor;
#[cfg(feature = "tokio1")]
pub use self::executor::Tokio1Executor;
#[cfg(all(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))]
#[doc(inline)]
pub use self::transport::AsyncTransport;
pub use crate::address::Address;
#[cfg(feature = "builder")]
#[doc(inline)]
pub use crate::message::Message;
#[cfg(all(
feature = "file-transport",
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
))]
#[doc(inline)]
pub use crate::transport::file::AsyncFileTransport;
#[cfg(feature = "file-transport")]
#[doc(inline)]
pub use crate::transport::file::FileTransport;
#[cfg(all(
feature = "sendmail-transport",
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
))]
#[doc(inline)]
pub use crate::transport::sendmail::AsyncSendmailTransport;
#[cfg(feature = "sendmail-transport")]
#[doc(inline)]
pub use crate::transport::sendmail::SendmailTransport;
#[cfg(all(
feature = "smtp-transport",
any(feature = "tokio02", feature = "tokio1")
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
))]
pub use crate::transport::smtp::AsyncSmtpTransport;
#[doc(inline)]
pub use crate::transport::Transport;
use crate::{address::Envelope, error::Error};
#[cfg(feature = "smtp-transport")]
pub use crate::transport::smtp::SmtpTransport;
use std::error::Error as StdError;
pub(crate) type BoxError = Box<dyn StdError + Send + Sync>;
#[cfg(test)]
#[cfg(feature = "builder")]

View File

@@ -177,20 +177,19 @@ impl MultiPartKind {
fn to_mime<S: Into<String>>(&self, boundary: Option<S>) -> Mime {
let boundary = boundary.map_or_else(make_boundary, |s| s.into());
use self::MultiPartKind::*;
format!(
"multipart/{}; boundary=\"{}\"{}",
match self {
Mixed => "mixed",
Alternative => "alternative",
Related => "related",
Encrypted { .. } => "encrypted",
Signed { .. } => "signed",
Self::Mixed => "mixed",
Self::Alternative => "alternative",
Self::Related => "related",
Self::Encrypted { .. } => "encrypted",
Self::Signed { .. } => "signed",
},
boundary,
match self {
Encrypted { protocol } => format!("; protocol=\"{}\"", protocol),
Signed { protocol, micalg } =>
Self::Encrypted { protocol } => format!("; protocol=\"{}\"", protocol),
Self::Signed { protocol, micalg } =>
format!("; protocol=\"{}\"; micalg=\"{}\"", protocol, micalg),
_ => String::new(),
}
@@ -200,18 +199,17 @@ impl MultiPartKind {
}
fn from_mime(m: &Mime) -> Option<Self> {
use self::MultiPartKind::*;
match m.subtype().as_ref() {
"mixed" => Some(Mixed),
"alternative" => Some(Alternative),
"related" => Some(Related),
"mixed" => Some(Self::Mixed),
"alternative" => Some(Self::Alternative),
"related" => Some(Self::Related),
"signed" => m.get_param("protocol").and_then(|p| {
m.get_param("micalg").map(|micalg| Signed {
m.get_param("micalg").map(|micalg| Self::Signed {
protocol: p.as_str().to_owned(),
micalg: micalg.as_str().to_owned(),
})
}),
"encrypted" => m.get_param("protocol").map(|p| Encrypted {
"encrypted" => m.get_param("protocol").map(|p| Self::Encrypted {
protocol: p.as_str().to_owned(),
}),
_ => None,

View File

@@ -492,6 +492,7 @@ impl MessageBuilder {
}
/// Email message which can be formatted
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
#[derive(Clone, Debug)]
pub struct Message {
headers: Headers,

View File

@@ -1,61 +1,96 @@
//! Error and result type for file transport
use self::Error::*;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
io,
};
use crate::BoxError;
use std::{error::Error as StdError, fmt};
/// An enum of all error kinds.
#[derive(Debug)]
pub enum Error {
/// Internal client error
Client(&'static str),
/// IO error
Io(io::Error),
/// JSON error
#[cfg(feature = "file-transport-envelope")]
Json(serde_json::Error),
/// The Errors that may occur when sending an email over SMTP
pub struct Error {
inner: Box<Inner>,
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
match *self {
Client(err) => fmt.write_str(err),
Io(ref err) => err.fmt(fmt),
#[cfg(feature = "file-transport-envelope")]
Json(ref err) => err.fmt(fmt),
struct Inner {
kind: Kind,
source: Option<BoxError>,
}
impl Error {
pub(crate) fn new<E>(kind: Kind, source: Option<E>) -> Error
where
E: Into<BoxError>,
{
Error {
inner: Box::new(Inner {
kind,
source: source.map(Into::into),
}),
}
}
/// Returns true if the error is a file I/O error
pub fn is_io(&self) -> bool {
matches!(self.inner.kind, Kind::Io)
}
/// Returns true if the error is an envelope serialization or deserialization error
#[cfg(feature = "file-transport-envelope")]
pub fn is_envelope(&self) -> bool {
matches!(self.inner.kind, Kind::Envelope)
}
}
#[derive(Debug)]
pub(crate) enum Kind {
/// File I/O error
Io,
/// Envelope serialization/deserialization error
#[cfg(feature = "file-transport-envelope")]
Envelope,
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut builder = f.debug_struct("lettre::transport::file::Error");
builder.field("kind", &self.inner.kind);
if let Some(ref source) = self.inner.source {
builder.field("source", source);
}
builder.finish()
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.inner.kind {
Kind::Io => f.write_str("response error")?,
#[cfg(feature = "file-transport-envelope")]
Kind::Envelope => f.write_str("internal client error")?,
};
if let Some(ref e) = self.inner.source {
write!(f, ": {}", e)?;
}
Ok(())
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match *self {
Io(ref err) => Some(&*err),
#[cfg(feature = "file-transport-envelope")]
Json(ref err) => Some(&*err),
_ => None,
}
self.inner.source.as_ref().map(|e| {
let r: &(dyn std::error::Error + 'static) = &**e;
r
})
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::Io(err)
}
pub(crate) fn io<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Io, Some(e))
}
#[cfg(feature = "file-transport-envelope")]
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Error {
Error::Json(err)
}
}
impl From<&'static str> for Error {
fn from(string: &'static str) -> Error {
Error::Client(string)
}
pub(crate) fn envelope<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Envelope, Some(e))
}

View File

@@ -6,7 +6,7 @@
//!
//! ```rust
//! # use std::error::Error;
//!
//! #
//! # #[cfg(all(feature = "file-transport", feature = "builder"))]
//! # fn main() -> Result<(), Box<dyn Error>> {
//! use lettre::{FileTransport, Message, Transport};
@@ -38,7 +38,7 @@
//!
//! ```rust
//! # use std::error::Error;
//!
//! #
//! # #[cfg(all(feature = "file-transport-envelope", feature = "builder"))]
//! # fn main() -> Result<(), Box<dyn Error>> {
//! use lettre::{FileTransport, Message, Transport};
@@ -66,7 +66,7 @@
//!
//! ```rust,no_run
//! # use std::error::Error;
//!
//! #
//! # #[cfg(all(feature = "tokio1", feature = "file-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> {
//! use std::env::temp_dir;
@@ -91,7 +91,7 @@
//!
//! ```rust,no_run
//! # use std::error::Error;
//!
//! #
//! # #[cfg(all(feature = "async-std1", feature = "file-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> {
//! use std::env::temp_dir;
@@ -153,14 +153,20 @@ type Id = String;
/// Writes the content and the envelope information to a file
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport")))]
pub struct FileTransport {
path: PathBuf,
#[cfg(feature = "file-transport-envelope")]
save_envelope: bool,
}
/// Asynchronously writes the content and the envelope information to a file
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))
)]
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
pub struct AsyncFileTransport<E: Executor> {
inner: FileTransport,
@@ -200,11 +206,11 @@ impl FileTransport {
use std::fs;
let eml_file = self.path.join(format!("{}.eml", email_id));
let eml = fs::read(eml_file)?;
let eml = fs::read(eml_file).map_err(error::io)?;
let json_file = self.path.join(format!("{}.json", email_id));
let json = fs::read(&json_file)?;
let envelope = serde_json::from_slice(&json)?;
let json = fs::read(&json_file).map_err(error::io)?;
let envelope = serde_json::from_slice(&json).map_err(error::envelope)?;
Ok((envelope, eml))
}
@@ -247,11 +253,11 @@ where
#[cfg(feature = "file-transport-envelope")]
pub async fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
let eml_file = self.inner.path.join(format!("{}.eml", email_id));
let eml = E::fs_read(&eml_file).await?;
let eml = E::fs_read(&eml_file).await.map_err(error::io)?;
let json_file = self.inner.path.join(format!("{}.json", email_id));
let json = E::fs_read(&json_file).await?;
let envelope = serde_json::from_slice(&json)?;
let json = E::fs_read(&json_file).await.map_err(error::io)?;
let envelope = serde_json::from_slice(&json).map_err(error::envelope)?;
Ok((envelope, eml))
}
@@ -267,13 +273,14 @@ impl Transport for FileTransport {
let email_id = Uuid::new_v4();
let file = self.path(&email_id, "eml");
fs::write(file, email)?;
fs::write(file, email).map_err(error::io)?;
#[cfg(feature = "file-transport-envelope")]
{
if self.save_envelope {
let file = self.path(&email_id, "json");
fs::write(file, serde_json::to_string(&envelope)?)?;
let buf = serde_json::to_string(&envelope).map_err(error::envelope)?;
fs::write(file, buf).map_err(error::io)?;
}
}
// use envelope anyway
@@ -296,14 +303,14 @@ where
let email_id = Uuid::new_v4();
let file = self.inner.path(&email_id, "eml");
E::fs_write(&file, email).await?;
E::fs_write(&file, email).await.map_err(error::io)?;
#[cfg(feature = "file-transport-envelope")]
{
if self.inner.save_envelope {
let file = self.inner.path(&email_id, "json");
let buf = serde_json::to_vec(&envelope)?;
E::fs_write(&file, &buf).await?;
let buf = serde_json::to_vec(&envelope).map_err(error::envelope)?;
E::fs_write(&file, &buf).await.map_err(error::io)?;
}
}
// use envelope anyway

View File

@@ -1,37 +1,81 @@
//! ### Sending Messages
//! ## Transports for sending emails
//!
//! This section explains how to manipulate emails you have created.
//!
//! This mailer contains several different transports for your emails. To be sendable, the
//! emails have to implement `Email`, which is the case for emails created with `lettre::builder`.
//! This module contains `Transport`s for sending emails. A `Transport` implements a high-level API
//! for sending emails. It automatically manages the underlying resources and doesn't require any
//! specific knowledge of email protocols in order to be used.
//!
//! The following transports are available:
//!
//! * The `SmtpTransport` uses the SMTP protocol to send the message over the network. It is
//! the preferred way of sending emails.
//! * The `SendmailTransport` uses the sendmail command to send messages. It is an alternative to
//! the SMTP transport.
//! * The `FileTransport` creates a file containing the email content to be sent. It can be used
//! for debugging or if you want to keep all sent emails.
//! * The `StubTransport` is useful for debugging, and only prints the content of the email in the
//! logs.
//! | Module | Protocol | Sync API | Async API | Description |
//! | ------------ | -------- | --------------------- | -------------------------- | ------------------------------------------------------- |
//! | [`smtp`] | SMTP | [`SmtpTransport`] | [`AsyncSmtpTransport`] | Uses the SMTP protocol to send emails to a relay server |
//! | [`sendmail`] | Sendmail | [`SendmailTransport`] | [`AsyncSendmailTransport`] | Uses the `sendmail` command to send emails |
//! | [`file`] | File | [`FileTransport`] | [`AsyncFileTransport`] | Saves the email as an `.eml` file |
//! | [`stub`] | Debug | [`StubTransport`] | [`StubTransport`] | Drops the email - Useful for debugging |
//!
//! ## Building an email
//!
//! Emails can either be built though [`Message`], which is a typed API for constructing emails
//! (find out more about it by going over the [`message`][crate::message] module),
//! or via external means.
//!
//! [`Message`]s can be sent via [`Transport::send`] or [`AsyncTransport::send`], while messages
//! built without lettre's [`message`][crate::message] APIs can be sent via [`Transport::send_raw`]
//! or [`AsyncTransport::send_raw`].
//!
//! ## Brief example
//!
//! This example shows how to build an email and send it via an SMTP relay server.
//! It is in no way a complete example, but it shows how to get started with lettre.
//! More examples can be found by looking at the specific modules, linked in the _Module_ column
//! of the [table above](#transports-for-sending-emails).
//!
//! ```rust,no_run
//! # use std::error::Error;
//! #
//! # #[cfg(all(feature = "builder", feature = "smtp-transport"))]
//! # fn main() -> Result<(), Box<dyn Error>> {
//! use lettre::transport::smtp::authentication::Credentials;
//! use lettre::{Message, SmtpTransport, Transport};
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body(String::from("Be happy!"))?;
//!
//! let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
//!
//! // Open a remote connection to the SMTP relay server
//! let mailer = SmtpTransport::relay("smtp.gmail.com")?
//! .credentials(creds)
//! .build();
//!
//! // Send the email
//! match mailer.send(&email) {
//! Ok(_) => println!("Email sent successfully!"),
//! Err(e) => panic!("Could not send email: {:?}", e),
//! }
//! # Ok(())
//! # }
//! # #[cfg(not(all(feature = "builder", feature = "smtp-transport")))]
//! # fn main() {}
//! ```
//!
//! [`Message`]: crate::Message
//! [`file`]: self::file
//! [`SmtpTransport`]: crate::SmtpTransport
//! [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
//! [`SendmailTransport`]: crate::SendmailTransport
//! [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
//! [`FileTransport`]: crate::FileTransport
//! [`AsyncFileTransport`]: crate::AsyncFileTransport
//! [`StubTransport`]: crate::transport::stub::StubTransport
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
use async_trait::async_trait;
#[doc(hidden)]
#[deprecated(note = "use lettre::AsyncStd1Transport")]
#[cfg(feature = "async-std1")]
pub use self::AsyncTransport as AsyncStd1Transport;
#[doc(hidden)]
#[deprecated(note = "use lettre::Tokio1Transport")]
#[cfg(feature = "tokio1")]
pub use self::AsyncTransport as Tokio1Transport;
#[doc(hidden)]
#[deprecated(note = "use lettre::Tokio02Transport")]
#[cfg(feature = "tokio02")]
pub use self::AsyncTransport as Tokio02Transport;
use crate::Envelope;
#[cfg(feature = "builder")]
use crate::Message;

View File

@@ -1,52 +1,92 @@
//! Error and result type for sendmail transport
use self::Error::*;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
io,
string::FromUtf8Error,
};
use crate::BoxError;
use std::{error::Error as StdError, fmt};
/// An enum of all error kinds.
#[derive(Debug)]
pub enum Error {
/// Internal client error
Client(String),
/// Error parsing UTF8 in response
Utf8Parsing(FromUtf8Error),
/// IO error
Io(io::Error),
/// The Errors that may occur when sending an email over sendmail
pub struct Error {
inner: Box<Inner>,
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
match *self {
Client(ref err) => err.fmt(fmt),
Utf8Parsing(ref err) => err.fmt(fmt),
Io(ref err) => err.fmt(fmt),
struct Inner {
kind: Kind,
source: Option<BoxError>,
}
impl Error {
pub(crate) fn new<E>(kind: Kind, source: Option<E>) -> Error
where
E: Into<BoxError>,
{
Error {
inner: Box::new(Inner {
kind,
source: source.map(Into::into),
}),
}
}
/// Returns true if the error is from client
pub fn is_client(&self) -> bool {
matches!(self.inner.kind, Kind::Client)
}
/// Returns true if the error comes from the response
pub fn is_response(&self) -> bool {
matches!(self.inner.kind, Kind::Response)
}
}
#[derive(Debug)]
pub(crate) enum Kind {
/// Error parsing a response
Response,
/// Internal client error
Client,
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut builder = f.debug_struct("lettre::transport::sendmail::Error");
builder.field("kind", &self.inner.kind);
if let Some(ref source) = self.inner.source {
builder.field("source", source);
}
builder.finish()
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.inner.kind {
Kind::Response => f.write_str("response error")?,
Kind::Client => f.write_str("internal client error")?,
};
if let Some(ref e) = self.inner.source {
write!(f, ": {}", e)?;
}
Ok(())
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match *self {
Io(ref err) => Some(&*err),
Utf8Parsing(ref err) => Some(&*err),
_ => None,
}
self.inner.source.as_ref().map(|e| {
let r: &(dyn std::error::Error + 'static) = &**e;
r
})
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::Io(err)
}
pub(crate) fn response<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Response, Some(e))
}
impl From<FromUtf8Error> for Error {
fn from(err: FromUtf8Error) -> Error {
Utf8Parsing(err)
}
pub(crate) fn client<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Client, Some(e))
}

View File

@@ -1,13 +1,13 @@
//! The sendmail transport sends the email using the local sendmail command.
//! The sendmail transport sends the email using the local `sendmail` command.
//!
//! ## Sync example
//!
//! ```rust
//! # use std::error::Error;
//!
//! #
//! # #[cfg(all(feature = "sendmail-transport", feature = "builder"))]
//! # fn main() -> Result<(), Box<dyn Error>> {
//! # use lettre::{Message, Transport, SendmailTransport};
//! use lettre::{Message, SendmailTransport, Transport};
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
@@ -30,7 +30,7 @@
//!
//! ```rust,no_run
//! # use std::error::Error;
//!
//! #
//! # #[cfg(all(feature = "tokio02", feature = "sendmail-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> {
//! use lettre::{Message, AsyncTransport, Tokio02Executor, AsyncSendmailTransport, SendmailTransport};
@@ -53,7 +53,7 @@
//!
//! ```rust,no_run
//! # use std::error::Error;
//!
//! #
//! # #[cfg(all(feature = "tokio1", feature = "sendmail-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> {
//! use lettre::{Message, AsyncTransport, Tokio1Executor, AsyncSendmailTransport, SendmailTransport};
@@ -76,7 +76,7 @@
//!
//!```rust,no_run
//! # use std::error::Error;
//!
//! #
//! # #[cfg(all(feature = "async-std1", feature = "sendmail-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> {
//! use lettre::{Message, AsyncTransport, AsyncStd1Executor, AsyncSendmailTransport};
@@ -111,24 +111,30 @@ use async_trait::async_trait;
use std::marker::PhantomData;
use std::{
ffi::OsString,
io::prelude::*,
io::Write,
process::{Command, Stdio},
};
mod error;
const DEFAUT_SENDMAIL: &str = "/usr/sbin/sendmail";
const DEFAULT_SENDMAIL: &str = "/usr/sbin/sendmail";
/// Sends an email using the `sendmail` command
/// Sends emails using the `sendmail` command
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(docsrs, doc(cfg(feature = "sendmail-transport")))]
pub struct SendmailTransport {
command: OsString,
}
/// Asynchronously sends emails using the `sendmail` command
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))
)]
pub struct AsyncSendmailTransport<E: Executor> {
inner: SendmailTransport,
marker_: PhantomData<E>,
@@ -138,7 +144,7 @@ impl SendmailTransport {
/// Creates a new transport with the default `/usr/sbin/sendmail` command
pub fn new() -> SendmailTransport {
SendmailTransport {
command: DEFAUT_SENDMAIL.into(),
command: DEFAULT_SENDMAIL.into(),
}
}
@@ -263,15 +269,21 @@ impl Transport for SendmailTransport {
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
// Spawn the sendmail command
let mut process = self.command(envelope).spawn()?;
let mut process = self.command(envelope).spawn().map_err(error::client)?;
process.stdin.as_mut().unwrap().write_all(email)?;
let output = process.wait_with_output()?;
process
.stdin
.as_mut()
.unwrap()
.write_all(email)
.map_err(error::client)?;
let output = process.wait_with_output().map_err(error::client)?;
if output.status.success() {
Ok(())
} else {
Err(error::Error::Client(String::from_utf8(output.stderr)?))
let stderr = String::from_utf8(output.stderr).map_err(error::response)?;
Err(error::client(stderr))
}
}
}
@@ -288,15 +300,22 @@ impl AsyncTransport for AsyncSendmailTransport<AsyncStd1Executor> {
let mut command = self.async_std_command(envelope);
// Spawn the sendmail command
let mut process = command.spawn()?;
let mut process = command.spawn().map_err(error::client)?;
process.stdin.as_mut().unwrap().write_all(&email).await?;
let output = process.output().await?;
process
.stdin
.as_mut()
.unwrap()
.write_all(&email)
.await
.map_err(error::client)?;
let output = process.output().await.map_err(error::client)?;
if output.status.success() {
Ok(())
} else {
Err(Error::Client(String::from_utf8(output.stderr)?))
let stderr = String::from_utf8(output.stderr).map_err(error::response)?;
Err(error::client(stderr))
}
}
}
@@ -313,15 +332,22 @@ impl AsyncTransport for AsyncSendmailTransport<Tokio02Executor> {
let mut command = self.tokio02_command(envelope);
// Spawn the sendmail command
let mut process = command.spawn()?;
let mut process = command.spawn().map_err(error::client)?;
process.stdin.as_mut().unwrap().write_all(&email).await?;
let output = process.wait_with_output().await?;
process
.stdin
.as_mut()
.unwrap()
.write_all(&email)
.await
.map_err(error::client)?;
let output = process.wait_with_output().await.map_err(error::client)?;
if output.status.success() {
Ok(())
} else {
Err(Error::Client(String::from_utf8(output.stderr)?))
let stderr = String::from_utf8(output.stderr).map_err(error::response)?;
Err(error::client(stderr))
}
}
}
@@ -338,15 +364,22 @@ impl AsyncTransport for AsyncSendmailTransport<Tokio1Executor> {
let mut command = self.tokio1_command(envelope);
// Spawn the sendmail command
let mut process = command.spawn()?;
let mut process = command.spawn().map_err(error::client)?;
process.stdin.as_mut().unwrap().write_all(&email).await?;
let output = process.wait_with_output().await?;
process
.stdin
.as_mut()
.unwrap()
.write_all(&email)
.await
.map_err(error::client)?;
let output = process.wait_with_output().await.map_err(error::client)?;
if output.status.success() {
Ok(())
} else {
Err(Error::Client(String::from_utf8(output.stderr)?))
let stderr = String::from_utf8(output.stderr).map_err(error::response)?;
Err(error::client(stderr))
}
}
}

View File

@@ -1,3 +1,4 @@
use std::fmt::{self, Debug};
use std::marker::PhantomData;
use async_trait::async_trait;
@@ -15,7 +16,11 @@ use crate::Tokio02Executor;
use crate::Tokio1Executor;
use crate::{Envelope, Executor};
#[allow(missing_debug_implementations)]
/// Asynchronously sends emails using the SMTP protocol
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))
)]
pub struct AsyncSmtpTransport<E> {
// TODO: pool
inner: AsyncSmtpClient<E>,
@@ -93,6 +98,16 @@ where
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-rustls-tls"
)))
)]
pub fn relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
use super::{Tls, TlsParameters, SUBMISSIONS_PORT};
@@ -122,6 +137,16 @@ where
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-rustls-tls"
)))
)]
pub fn starttls_relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
use super::{Tls, TlsParameters, SUBMISSION_PORT};
@@ -159,6 +184,14 @@ where
}
}
impl<E> Debug for AsyncSmtpTransport<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut builder = f.debug_struct("AsyncSmtpTransport");
builder.field("inner", &self.inner);
builder.finish()
}
}
impl<E> Clone for AsyncSmtpTransport<E>
where
E: Executor,
@@ -172,8 +205,11 @@ where
/// Contains client configuration.
/// Instances of this struct can be created using functions of [`AsyncSmtpTransport`].
#[allow(missing_debug_implementations)]
#[derive(Clone)]
#[derive(Debug, Clone)]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))
)]
pub struct AsyncSmtpTransportBuilder {
info: SmtpInfo,
}
@@ -213,12 +249,22 @@ impl AsyncSmtpTransportBuilder {
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-rustls-tls"
)))
)]
pub fn tls(mut self, tls: super::Tls) -> Self {
self.info.tls = tls;
self
}
/// Build the transport (with default pool if enabled)
/// Build the transport
pub fn build<E>(self) -> AsyncSmtpTransport<E>
where
E: Executor,
@@ -232,9 +278,9 @@ impl AsyncSmtpTransportBuilder {
}
/// Build client
pub struct AsyncSmtpClient<C> {
pub struct AsyncSmtpClient<E> {
info: SmtpInfo,
marker_: PhantomData<C>,
marker_: PhantomData<E>,
}
impl<E> AsyncSmtpClient<E>
@@ -260,6 +306,14 @@ where
}
}
impl<E> Debug for AsyncSmtpClient<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut builder = f.debug_struct("AsyncSmtpClient");
builder.field("info", &self.info);
builder.finish()
}
}
impl<E> AsyncSmtpClient<E>
where
E: Executor,

View File

@@ -1,14 +1,14 @@
//! Provides limited SASL authentication mechanisms
use crate::transport::smtp::error::Error;
use std::fmt::{self, Display, Formatter};
use crate::transport::smtp::error::{self, Error};
use std::fmt::{self, Debug, Display, Formatter};
/// Accepted authentication mechanisms
/// Trying LOGIN last as it is deprecated.
pub const DEFAULT_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
/// Contains user credentials
#[derive(PartialEq, Eq, Clone, Hash, Debug)]
#[derive(PartialEq, Eq, Clone, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Credentials {
authentication_identity: String,
@@ -35,6 +35,12 @@ where
}
}
impl Debug for Credentials {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("Credentials").finish()
}
}
/// Represents authentication mechanisms
#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@@ -80,15 +86,15 @@ impl Mechanism {
) -> Result<String, Error> {
match self {
Mechanism::Plain => match challenge {
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
Some(_) => Err(error::client("This mechanism does not expect a challenge")),
None => Ok(format!(
"\u{0}{}\u{0}{}",
credentials.authentication_identity, credentials.secret
)),
},
Mechanism::Login => {
let decoded_challenge =
challenge.ok_or(Error::Client("This mechanism does expect a challenge"))?;
let decoded_challenge = challenge
.ok_or_else(|| error::client("This mechanism does expect a challenge"))?;
if vec!["User Name", "Username:", "Username"].contains(&decoded_challenge) {
return Ok(credentials.authentication_identity.to_string());
@@ -98,10 +104,10 @@ impl Mechanism {
return Ok(credentials.secret.to_string());
}
Err(Error::Client("Unrecognized challenge"))
Err(error::client("Unrecognized challenge"))
}
Mechanism::Xoauth2 => match challenge {
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
Some(_) => Err(error::client("This mechanism does not expect a challenge")),
None => Ok(format!(
"user={}\x01auth=Bearer {}\x01\x01",
credentials.authentication_identity, credentials.secret

View File

@@ -1,18 +1,17 @@
use std::{fmt::Display, io};
use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use super::{AsyncNetworkStream, ClientCodec, TlsParameters};
use crate::{
transport::smtp::{
authentication::{Credentials, Mechanism},
commands::*,
error,
error::Error,
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
response::{parse_response, Response},
},
Envelope,
};
use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use std::fmt::Display;
#[cfg(feature = "tracing")]
use super::escape_crlf;
@@ -121,7 +120,7 @@ impl AsyncSmtpConnection {
if envelope.has_non_ascii_addresses() {
if !self.server_info().supports_feature(Extension::SmtpUtfEight) {
// don't try to send non-ascii addresses (per RFC)
return Err(Error::Client(
return Err(error::client(
"Envelope contains non-ascii chars but server does not support SMTPUTF8",
));
}
@@ -131,7 +130,7 @@ impl AsyncSmtpConnection {
// Check for non-ascii content in message
if !email.is_ascii() {
if !self.server_info().supports_feature(Extension::EightBitMime) {
return Err(Error::Client(
return Err(error::client(
"Message contains non-ascii chars but server does not support 8BITMIME",
));
}
@@ -186,7 +185,7 @@ impl AsyncSmtpConnection {
try_smtp!(self.ehlo(hello_name).await, self);
Ok(())
} else {
Err(Error::Client("STARTTLS is not supported on this server"))
Err(error::client("STARTTLS is not supported on this server"))
}
}
@@ -233,12 +232,10 @@ impl AsyncSmtpConnection {
let mechanism = self
.server_info
.get_auth_mechanism(mechanisms)
.ok_or(Error::Client(
"No compatible authentication mechanism was found",
))?;
.ok_or_else(|| error::client("No compatible authentication mechanism was found"))?;
// Limit challenges to avoid blocking
let mut challenges = 10;
let mut challenges: u8 = 10;
let mut response = self
.command(Auth::new(mechanism, credentials.clone(), None)?)
.await?;
@@ -257,7 +254,7 @@ impl AsyncSmtpConnection {
}
if challenges == 0 {
Err(Error::ResponseParsing("Unexpected number of challenges"))
Err(error::response("Unexpected number of challenges"))
} else {
Ok(response)
}
@@ -281,8 +278,16 @@ impl AsyncSmtpConnection {
/// Writes a string to the server
async fn write(&mut self, string: &[u8]) -> Result<(), Error> {
self.stream.get_mut().write_all(string).await?;
self.stream.get_mut().flush().await?;
self.stream
.get_mut()
.write_all(string)
.await
.map_err(error::network)?;
self.stream
.get_mut()
.flush()
.await
.map_err(error::network)?;
#[cfg(feature = "tracing")]
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
@@ -293,27 +298,33 @@ impl AsyncSmtpConnection {
pub async fn read_response(&mut self) -> Result<Response, Error> {
let mut buffer = String::with_capacity(100);
while self.stream.read_line(&mut buffer).await? > 0 {
while self
.stream
.read_line(&mut buffer)
.await
.map_err(error::network)?
> 0
{
#[cfg(feature = "tracing")]
tracing::debug!("<< {}", escape_crlf(&buffer));
match parse_response(&buffer) {
Ok((_remaining, response)) => {
if response.is_positive() {
return Ok(response);
return if response.is_positive() {
Ok(response)
} else {
Err(error::code(response.code))
}
return Err(response.into());
}
Err(nom::Err::Failure(e)) => {
return Err(Error::Parsing(e.code));
return Err(error::response(e.to_string()));
}
Err(nom::Err::Incomplete(_)) => { /* read more */ }
Err(nom::Err::Error(e)) => {
return Err(Error::Parsing(e.code));
return Err(error::response(e.to_string()));
}
}
}
Err(io::Error::new(io::ErrorKind::Other, "incomplete").into())
Err(error::response("incomplete response"))
}
}

View File

@@ -50,7 +50,7 @@ use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
))]
use super::InnerTlsParameters;
use super::TlsParameters;
use crate::transport::smtp::Error;
use crate::transport::smtp::{error, Error};
/// A network stream
pub struct AsyncNetworkStream {
@@ -144,7 +144,9 @@ impl AsyncNetworkStream {
port: u16,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncNetworkStream, Error> {
let tcp_stream = Tokio02TcpStream::connect((hostname, port)).await?;
let tcp_stream = Tokio02TcpStream::connect((hostname, port))
.await
.map_err(error::connection)?;
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio02Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters {
@@ -159,7 +161,9 @@ impl AsyncNetworkStream {
port: u16,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncNetworkStream, Error> {
let tcp_stream = Tokio1TcpStream::connect((hostname, port)).await?;
let tcp_stream = Tokio1TcpStream::connect((hostname, port))
.await
.map_err(error::connection)?;
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters {
@@ -174,7 +178,9 @@ impl AsyncNetworkStream {
port: u16,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncNetworkStream, Error> {
let tcp_stream = AsyncStd1TcpStream::connect((hostname, port)).await?;
let tcp_stream = AsyncStd1TcpStream::connect((hostname, port))
.await
.map_err(error::connection)?;
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters {
@@ -203,7 +209,9 @@ impl AsyncNetworkStream {
_ => unreachable!(),
};
self.inner = Self::upgrade_tokio02_tls(tcp_stream, tls_parameters).await?;
self.inner = Self::upgrade_tokio02_tls(tcp_stream, tls_parameters)
.await
.map_err(error::connection)?;
Ok(())
}
#[cfg(all(
@@ -224,7 +232,9 @@ impl AsyncNetworkStream {
_ => unreachable!(),
};
self.inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters).await?;
self.inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters)
.await
.map_err(error::connection)?;
Ok(())
}
#[cfg(all(
@@ -245,7 +255,9 @@ impl AsyncNetworkStream {
_ => unreachable!(),
};
self.inner = Self::upgrade_asyncstd1_tls(tcp_stream, tls_parameters).await?;
self.inner = Self::upgrade_asyncstd1_tls(tcp_stream, tls_parameters)
.await
.map_err(error::connection)?;
Ok(())
}
_ => Ok(()),
@@ -271,7 +283,10 @@ impl AsyncNetworkStream {
use tokio02_native_tls_crate::TlsConnector;
let connector = TlsConnector::from(connector);
let stream = connector.connect(&domain, tcp_stream).await?;
let stream = connector
.connect(&domain, tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::Tokio02NativeTls(stream))
};
}
@@ -284,10 +299,14 @@ impl AsyncNetworkStream {
return {
use tokio02_rustls::{webpki::DNSNameRef, TlsConnector};
let domain = DNSNameRef::try_from_ascii_str(&domain)?;
let domain =
DNSNameRef::try_from_ascii_str(&domain).map_err(error::connection)?;
let connector = TlsConnector::from(Arc::new(config));
let stream = connector.connect(domain, tcp_stream).await?;
let stream = connector
.connect(domain, tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::Tokio02RustlsTls(stream))
};
}
@@ -313,7 +332,10 @@ impl AsyncNetworkStream {
use tokio1_native_tls_crate::TlsConnector;
let connector = TlsConnector::from(connector);
let stream = connector.connect(&domain, tcp_stream).await?;
let stream = connector
.connect(&domain, tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::Tokio1NativeTls(stream))
};
}
@@ -326,10 +348,14 @@ impl AsyncNetworkStream {
return {
use tokio1_rustls::{webpki::DNSNameRef, TlsConnector};
let domain = DNSNameRef::try_from_ascii_str(&domain)?;
let domain =
DNSNameRef::try_from_ascii_str(&domain).map_err(error::connection)?;
let connector = TlsConnector::from(Arc::new(config));
let stream = connector.connect(domain, tcp_stream).await?;
let stream = connector
.connect(domain, tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::Tokio1RustlsTls(stream))
};
}
@@ -374,10 +400,14 @@ impl AsyncNetworkStream {
return {
use async_rustls::{webpki::DNSNameRef, TlsConnector};
let domain = DNSNameRef::try_from_ascii_str(&domain)?;
let domain =
DNSNameRef::try_from_ascii_str(&domain).map_err(error::connection)?;
let connector = TlsConnector::from(Arc::new(config));
let stream = connector.connect(domain, tcp_stream).await?;
let stream = connector
.connect(domain, tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream))
};
}

View File

@@ -11,6 +11,7 @@ use crate::{
transport::smtp::{
authentication::{Credentials, Mechanism},
commands::*,
error,
error::Error,
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
response::{parse_response, Response},
@@ -66,7 +67,7 @@ impl SmtpConnection {
panic: false,
server_info: ServerInfo::default(),
};
conn.set_timeout(timeout)?;
conn.set_timeout(timeout).map_err(error::network)?;
// TODO log
let _response = conn.read_response()?;
@@ -91,7 +92,7 @@ impl SmtpConnection {
if envelope.has_non_ascii_addresses() {
if !self.server_info().supports_feature(Extension::SmtpUtfEight) {
// don't try to send non-ascii addresses (per RFC)
return Err(Error::Client(
return Err(error::client(
"Envelope contains non-ascii chars but server does not support SMTPUTF8",
));
}
@@ -101,7 +102,7 @@ impl SmtpConnection {
// Check for non-ascii content in message
if !email.is_ascii() {
if !self.server_info().supports_feature(Extension::EightBitMime) {
return Err(Error::Client(
return Err(error::client(
"Message contains non-ascii chars but server does not support 8BITMIME",
));
}
@@ -156,7 +157,7 @@ impl SmtpConnection {
// when a TLS library is enabled
unreachable!("TLS support required but not supported");
} else {
Err(Error::Client("STARTTLS is not supported on this server"))
Err(error::client("STARTTLS is not supported on this server"))
}
}
@@ -209,9 +210,7 @@ impl SmtpConnection {
let mechanism = self
.server_info
.get_auth_mechanism(mechanisms)
.ok_or(Error::Client(
"No compatible authentication mechanism was found",
))?;
.ok_or_else(|| error::client("No compatible authentication mechanism was found"))?;
// Limit challenges to avoid blocking
let mut challenges = 10;
@@ -230,7 +229,7 @@ impl SmtpConnection {
}
if challenges == 0 {
Err(Error::ResponseParsing("Unexpected number of challenges"))
Err(error::response("Unexpected number of challenges"))
} else {
Ok(response)
}
@@ -254,8 +253,11 @@ impl SmtpConnection {
/// Writes a string to the server
fn write(&mut self, string: &[u8]) -> Result<(), Error> {
self.stream.get_mut().write_all(string)?;
self.stream.get_mut().flush()?;
self.stream
.get_mut()
.write_all(string)
.map_err(error::network)?;
self.stream.get_mut().flush().map_err(error::network)?;
#[cfg(feature = "tracing")]
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
@@ -266,27 +268,27 @@ impl SmtpConnection {
pub fn read_response(&mut self) -> Result<Response, Error> {
let mut buffer = String::with_capacity(100);
while self.stream.read_line(&mut buffer)? > 0 {
while self.stream.read_line(&mut buffer).map_err(error::network)? > 0 {
#[cfg(feature = "tracing")]
tracing::debug!("<< {}", escape_crlf(&buffer));
match parse_response(&buffer) {
Ok((_remaining, response)) => {
if response.is_positive() {
return Ok(response);
}
return Err(response.into());
return if response.is_positive() {
Ok(response)
} else {
Err(error::code(response.code))
};
}
Err(nom::Err::Failure(e)) => {
return Err(Error::Parsing(e.code));
return Err(error::response(e.to_string()));
}
Err(nom::Err::Incomplete(_)) => { /* read more */ }
Err(nom::Err::Error(e)) => {
return Err(Error::Parsing(e.code));
return Err(error::response(e.to_string()));
}
}
}
Err(io::Error::new(io::ErrorKind::Other, "incomplete").into())
Err(error::response("incomplete response"))
}
}

View File

@@ -15,7 +15,7 @@ use rustls::{ClientSession, StreamOwned};
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
use super::InnerTlsParameters;
use super::{MockStream, TlsParameters};
use crate::transport::smtp::Error;
use crate::transport::smtp::{error, Error};
/// A network stream
pub struct NetworkStream {
@@ -84,18 +84,18 @@ impl NetworkStream {
server: T,
timeout: Duration,
) -> Result<TcpStream, Error> {
let addrs = server.to_socket_addrs()?;
let addrs = server.to_socket_addrs().map_err(error::connection)?;
for addr in addrs {
if let Ok(result) = TcpStream::connect_timeout(&addr, timeout) {
return Ok(result);
}
}
Err(Error::Client("Could not connect"))
Err(error::connection("Could not connect"))
}
let tcp_stream = match timeout {
Some(t) => try_connect_timeout(server, t)?,
None => TcpStream::connect(server)?,
None => TcpStream::connect(server).map_err(error::connection)?,
};
let mut stream = NetworkStream::new(InnerNetworkStream::Tcp(tcp_stream));
@@ -140,14 +140,15 @@ impl NetworkStream {
InnerTlsParameters::NativeTls(connector) => {
let stream = connector
.connect(tls_parameters.domain(), tcp_stream)
.map_err(|err| Error::Io(io::Error::new(io::ErrorKind::Other, err)))?;
.map_err(error::connection)?;
InnerNetworkStream::NativeTls(stream)
}
#[cfg(feature = "rustls-tls")]
InnerTlsParameters::RustlsTls(connector) => {
use webpki::DNSNameRef;
let domain = DNSNameRef::try_from_ascii_str(tls_parameters.domain())?;
let domain = DNSNameRef::try_from_ascii_str(tls_parameters.domain())
.map_err(error::connection)?;
let stream = StreamOwned::new(
ClientSession::new(&Arc::new(connector.clone()), domain),
tcp_stream,

View File

@@ -1,15 +1,15 @@
#[cfg(feature = "rustls-tls")]
use std::sync::Arc;
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
use crate::transport::smtp::{error, Error};
#[cfg(feature = "native-tls")]
use native_tls::{Protocol, TlsConnector};
#[cfg(feature = "rustls-tls")]
use rustls::{ClientConfig, RootCertStore, ServerCertVerified, ServerCertVerifier, TLSError};
use std::fmt::{self, Debug};
#[cfg(feature = "rustls-tls")]
use std::sync::Arc;
#[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.
// This is also rustls' default behavior
@@ -33,9 +33,22 @@ pub enum Tls {
Wrapper(TlsParameters),
}
impl Debug for Tls {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self {
Self::None => f.pad("None"),
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Self::Opportunistic(_) => f.pad("Opportunistic"),
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Self::Required(_) => f.pad("Required"),
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Self::Wrapper(_) => f.pad("Wrapper"),
}
}
}
/// Parameters to use for secure clients
#[derive(Clone)]
#[allow(missing_debug_implementations)]
pub struct TlsParameters {
pub(crate) connector: InnerTlsParameters,
/// The domain name which is expected in the TLS certificate from the server
@@ -43,7 +56,7 @@ pub struct TlsParameters {
}
/// Builder for `TlsParameters`
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct TlsParametersBuilder {
domain: String,
root_certs: Vec<Certificate>,
@@ -142,7 +155,7 @@ impl TlsParametersBuilder {
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()?;
let connector = tls_builder.build().map_err(error::tls)?;
Ok(TlsParameters {
connector: InnerTlsParameters::NativeTls(connector),
domain: self.domain,
@@ -159,9 +172,7 @@ impl TlsParametersBuilder {
for cert in self.root_certs {
for rustls_cert in cert.rustls {
tls.root_store
.add(&rustls_cert)
.map_err(|_| Error::InvalidCertificate)?;
tls.root_store.add(&rustls_cert).map_err(error::tls)?;
}
}
if self.accept_invalid_certs {
@@ -227,12 +238,12 @@ pub struct Certificate {
rustls: Vec<rustls::Certificate>,
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
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)?;
let native_tls_cert = native_tls::Certificate::from_der(&der).map_err(error::tls)?;
Ok(Self {
#[cfg(feature = "native-tls")]
@@ -245,8 +256,7 @@ impl Certificate {
/// 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)?;
let native_tls_cert = native_tls::Certificate::from_pem(pem).map_err(error::tls)?;
#[cfg(feature = "rustls-tls")]
let rustls_cert = {
@@ -254,7 +264,7 @@ impl Certificate {
use std::io::Cursor;
let mut pem = Cursor::new(pem);
pemfile::certs(&mut pem).map_err(|_| Error::InvalidCertificate)?
pemfile::certs(&mut pem).map_err(|_| error::tls("invalid certificates"))?
};
Ok(Self {
@@ -266,6 +276,12 @@ impl Certificate {
}
}
impl Debug for Certificate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Certificate").finish()
}
}
#[cfg(feature = "rustls-tls")]
struct InvalidCertsVerifier;

View File

@@ -1,13 +1,13 @@
//! SMTP commands
use crate::{
address::Address,
transport::smtp::{
authentication::{Credentials, Mechanism},
error::Error,
error::{self, Error},
extension::{ClientId, MailParameter, RcptParameter},
response::Response,
},
Address,
};
use std::fmt::{self, Display, Formatter};
@@ -261,16 +261,17 @@ impl Auth {
response: &Response,
) -> Result<Auth, Error> {
if !response.has_code(334) {
return Err(Error::ResponseParsing("Expecting a challenge"));
return Err(error::response("Expecting a challenge"));
}
let encoded_challenge = response
.first_word()
.ok_or(Error::ResponseParsing("Could not read auth challenge"))?;
.ok_or_else(|| error::response("Could not read auth challenge"))?;
#[cfg(feature = "tracing")]
tracing::debug!("auth encoded challenge: {}", encoded_challenge);
let decoded_challenge = String::from_utf8(base64::decode(&encoded_challenge)?)?;
let decoded_base64 = base64::decode(&encoded_challenge).map_err(error::response)?;
let decoded_challenge = String::from_utf8(decoded_base64).map_err(error::response)?;
#[cfg(feature = "tracing")]
tracing::debug!("auth decoded challenge: {}", decoded_challenge);

View File

@@ -1,162 +1,185 @@
//! Error and result type for SMTP clients
use self::Error::*;
use crate::transport::smtp::response::{Response, Severity};
use base64::DecodeError;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
io,
string::FromUtf8Error,
use crate::{
transport::smtp::response::{Code, Severity},
BoxError,
};
use std::{error::Error as StdError, fmt};
/// An enum of all error kinds.
#[derive(Debug)]
pub enum Error {
/// Transient SMTP error, 4xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
Transient(Response),
/// Permanent SMTP error, 5xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
Permanent(Response),
/// Error parsing a response
ResponseParsing(&'static str),
/// Error parsing a base64 string in response
ChallengeParsing(DecodeError),
/// Error parsing UTF8 in response
Utf8Parsing(FromUtf8Error),
/// Internal client error
Client(&'static str),
/// DNS resolution error
Resolution,
/// IO 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),
// Inspired by https://github.com/seanmonstar/reqwest/blob/a8566383168c0ef06c21f38cbc9213af6ff6db31/src/error.rs
/// The Errors that may occur when sending an email over SMTP
pub struct Error {
inner: Box<Inner>,
}
impl Display for 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
Transient(ref err) => fmt.write_str(
err.first_line()
.unwrap_or("transient error during SMTP transaction"),
),
Permanent(ref err) => fmt.write_str(
err.first_line()
.unwrap_or("permanent error during SMTP transaction"),
),
ResponseParsing(err) => fmt.write_str(err),
ChallengeParsing(ref err) => err.fmt(fmt),
Utf8Parsing(ref err) => err.fmt(fmt),
Resolution => fmt.write_str("could not resolve hostname"),
Client(err) => fmt.write_str(err),
Io(ref err) => err.fmt(fmt),
#[cfg(feature = "native-tls")]
Tls(ref err) => err.fmt(fmt),
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),
struct Inner {
kind: Kind,
source: Option<BoxError>,
}
impl Error {
pub(crate) fn new<E>(kind: Kind, source: Option<E>) -> Error
where
E: Into<BoxError>,
{
Error {
inner: Box::new(Inner {
kind,
source: source.map(Into::into),
}),
}
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match *self {
ChallengeParsing(ref err) => Some(&*err),
Utf8Parsing(ref err) => Some(&*err),
Io(ref err) => Some(&*err),
#[cfg(feature = "native-tls")]
Tls(ref err) => Some(&*err),
/// Returns true if the error is from response
pub fn is_response(&self) -> bool {
matches!(self.inner.kind, Kind::Response)
}
/// Returns true if the error is from client
pub fn is_client(&self) -> bool {
matches!(self.inner.kind, Kind::Client)
}
/// Returns true if the error is a transient SMTP error
pub fn is_transient(&self) -> bool {
matches!(self.inner.kind, Kind::Transient(_))
}
/// Returns true if the error is a permanent SMTP error
pub fn is_permanent(&self) -> bool {
matches!(self.inner.kind, Kind::Permanent(_))
}
/// Returns true if the error is caused by a timeout
pub fn is_timeout(&self) -> bool {
let mut source = self.source();
while let Some(err) = source {
if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
return io_err.kind() == std::io::ErrorKind::TimedOut;
}
source = err.source();
}
false
}
/// Returns true if the error is from TLS
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
pub fn is_tls(&self) -> bool {
matches!(self.inner.kind, Kind::Tls)
}
/// Returns the status code, if the error was generated from a response.
pub fn status(&self) -> Option<Code> {
match self.inner.kind {
Kind::Transient(code) => Some(code),
Kind::Permanent(code) => Some(code),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Io(err)
#[derive(Debug)]
pub(crate) enum Kind {
/// Transient SMTP error, 4xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
Transient(Code),
/// Permanent SMTP error, 5xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
Permanent(Code),
/// Error parsing a response
Response,
/// Internal client error
Client,
/// Connection error
Connection,
/// Underlying network i/o error
Network,
/// TLS error
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Tls,
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut builder = f.debug_struct("lettre::transport::smtp::Error");
builder.field("kind", &self.inner.kind);
if let Some(ref source) = self.inner.source {
builder.field("source", source);
}
builder.finish()
}
}
#[cfg(feature = "native-tls")]
impl From<native_tls::Error> for Error {
fn from(err: native_tls::Error) -> Error {
Tls(err)
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.inner.kind {
Kind::Response => f.write_str("response error")?,
Kind::Client => f.write_str("internal client error")?,
Kind::Network => f.write_str("network error")?,
Kind::Connection => f.write_str("Connection error")?,
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Kind::Tls => f.write_str("tls error")?,
Kind::Transient(ref code) => {
write!(f, "transient error ({})", code)?;
}
Kind::Permanent(ref code) => {
write!(f, "permanent error ({})", code)?;
}
};
if let Some(ref e) = self.inner.source {
write!(f, ": {}", e)?;
}
Ok(())
}
}
impl From<nom::Err<nom::error::Error<&str>>> for Error {
fn from(err: nom::Err<nom::error::Error<&str>>) -> Error {
Parsing(match err {
nom::Err::Incomplete(_) => nom::error::ErrorKind::Complete,
nom::Err::Failure(e) => e.code,
nom::Err::Error(e) => e.code,
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.inner.source.as_ref().map(|e| {
let r: &(dyn std::error::Error + 'static) = &**e;
r
})
}
}
impl From<DecodeError> for Error {
fn from(err: DecodeError) -> Error {
ChallengeParsing(err)
pub(crate) fn code(c: Code) -> Error {
match c.severity {
Severity::TransientNegativeCompletion => Error::new::<Error>(Kind::Transient(c), None),
Severity::PermanentNegativeCompletion => Error::new::<Error>(Kind::Permanent(c), None),
_ => client("Unknown error code"),
}
}
impl From<FromUtf8Error> for Error {
fn from(err: FromUtf8Error) -> Error {
Utf8Parsing(err)
}
pub(crate) fn response<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Response, Some(e))
}
#[cfg(feature = "rustls-tls")]
impl From<webpki::InvalidDNSNameError> for Error {
fn from(err: webpki::InvalidDNSNameError) -> Error {
InvalidDNSName(err)
}
pub(crate) fn client<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Client, Some(e))
}
#[cfg(feature = "r2d2")]
impl From<r2d2::Error> for Error {
fn from(err: r2d2::Error) -> Error {
Pool(err)
}
pub(crate) fn network<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Network, Some(e))
}
impl From<Response> for Error {
fn from(response: Response) -> Error {
match response.code.severity {
Severity::TransientNegativeCompletion => Transient(response),
Severity::PermanentNegativeCompletion => Permanent(response),
_ => Client("Unknown error code"),
}
}
pub(crate) fn connection<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Connection, Some(e))
}
impl From<&'static str> for Error {
fn from(string: &'static str) -> Error {
Client(string)
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
pub(crate) fn tls<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Tls, Some(e))
}

View File

@@ -1,7 +1,10 @@
//! ESMTP features
use crate::transport::smtp::{
authentication::Mechanism, error::Error, response::Response, util::XText,
authentication::Mechanism,
error::{self, Error},
response::Response,
util::XText,
};
use std::{
collections::HashSet,
@@ -58,6 +61,7 @@ impl Display for ClientId {
}
impl ClientId {
#[doc(hidden)]
#[deprecated(since = "0.10.0", note = "Please use ClientId::Domain(domain) instead")]
/// Creates a new `ClientId` from a fully qualified domain name
pub fn new(domain: String) -> Self {
@@ -126,7 +130,7 @@ impl ServerInfo {
pub fn from_response(response: &Response) -> Result<ServerInfo, Error> {
let name = match response.first_word() {
Some(name) => name,
None => return Err(Error::ResponseParsing("Could not read server name")),
None => return Err(error::response("Could not read server name")),
};
let mut features: HashSet<Extension> = HashSet::new();

View File

@@ -166,8 +166,7 @@ pub const SUBMISSIONS_PORT: u16 = 465;
/// Default timeout
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
#[allow(missing_debug_implementations)]
#[derive(Clone)]
#[derive(Debug, Clone)]
struct SmtpInfo {
/// Name sent during EHLO
hello_name: ClientId,

View File

@@ -1,6 +1,6 @@
use std::time::Duration;
use crate::transport::smtp::{client::SmtpConnection, error::Error, SmtpClient};
use crate::transport::smtp::{client::SmtpConnection, error, error::Error, SmtpClient};
use r2d2::{CustomizeConnection, ManageConnection, Pool};
@@ -90,7 +90,7 @@ impl ManageConnection for SmtpClient {
if conn.test_connected() {
return Ok(());
}
Err(Error::Client("is not connected anymore"))
Err(error::network("is not connected anymore"))
}
fn has_broken(&self, conn: &mut Self::Connection) -> bool {

View File

@@ -1,7 +1,7 @@
//! SMTP response, containing a mandatory return code and an optional text
//! message
use crate::transport::smtp::Error;
use crate::transport::smtp::{error, Error};
use nom::{
branch::alt,
bytes::streaming::{tag, take_until},
@@ -120,6 +120,14 @@ impl Code {
detail,
}
}
/// Tells if the response is positive
pub fn is_positive(&self) -> bool {
matches!(
self.severity,
Severity::PositiveCompletion | Severity::PositiveIntermediate
)
}
}
/// Contains an SMTP reply, with separated code and message
@@ -139,7 +147,9 @@ impl FromStr for Response {
type Err = Error;
fn from_str(s: &str) -> result::Result<Response, Error> {
parse_response(s).map(|(_, r)| r).map_err(|e| e.into())
parse_response(s)
.map(|(_, r)| r)
.map_err(|e| error::response(e.to_string()))
}
}
@@ -151,10 +161,7 @@ impl Response {
/// Tells if the response is positive
pub fn is_positive(&self) -> bool {
matches!(
self.code.severity,
Severity::PositiveCompletion | Severity::PositiveIntermediate
)
self.code.is_positive()
}
/// Tests code equality

View File

@@ -5,14 +5,14 @@ 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 super::{error, Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, SmtpInfo};
use crate::{address::Envelope, Transport};
#[allow(missing_debug_implementations)]
/// Sends emails using the SMTP protocol
#[cfg_attr(docsrs, doc(cfg(feature = "smtp-transport")))]
#[derive(Clone)]
/// Transport using the SMTP protocol
pub struct SmtpTransport {
#[cfg(feature = "r2d2")]
inner: Pool<SmtpClient>,
@@ -27,7 +27,7 @@ impl Transport for SmtpTransport {
/// Sends an email
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
#[cfg(feature = "r2d2")]
let mut conn = self.inner.get()?;
let mut conn = self.inner.get().map_err(error::client)?;
#[cfg(not(feature = "r2d2"))]
let mut conn = self.inner.connection()?;
@@ -111,8 +111,7 @@ impl SmtpTransport {
/// Contains client configuration.
/// Instances of this struct can be created using functions of [`SmtpTransport`].
#[allow(missing_debug_implementations)]
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct SmtpTransportBuilder {
info: SmtpInfo,
#[cfg(feature = "r2d2")]
@@ -184,7 +183,7 @@ impl SmtpTransportBuilder {
}
/// Build client
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct SmtpClient {
info: SmtpInfo,
}