Merge pull request #413 from amousset/refactor-net
Refactor smtp client
This commit is contained in:
@@ -37,6 +37,7 @@ serde = { version = "1", optional = true, features = ["derive"] }
|
|||||||
serde_json = { version = "1", optional = true }
|
serde_json = { version = "1", optional = true }
|
||||||
textnonce = { version = "0.7", optional = true }
|
textnonce = { version = "0.7", optional = true }
|
||||||
webpki = { version = "0.21", optional = true }
|
webpki = { version = "0.21", optional = true }
|
||||||
|
webpki-roots = { version = "0.19", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = "0.3"
|
criterion = "0.3"
|
||||||
@@ -50,10 +51,9 @@ name = "transport_smtp"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
builder = ["mime", "base64", "hyperx", "textnonce", "quoted_printable"]
|
builder = ["mime", "base64", "hyperx", "textnonce", "quoted_printable"]
|
||||||
connection-pool = ["r2d2"]
|
default = ["file-transport", "smtp-transport", "hostname", "sendmail-transport", "rustls-tls", "builder", "r2d2"]
|
||||||
default = ["file-transport", "smtp-transport", "hostname", "sendmail-transport", "native-tls", "builder"]
|
|
||||||
file-transport = ["serde", "serde_json"]
|
file-transport = ["serde", "serde_json"]
|
||||||
rustls-tls = ["webpki", "rustls"]
|
rustls-tls = ["webpki", "webpki-roots", "rustls"]
|
||||||
sendmail-transport = []
|
sendmail-transport = []
|
||||||
smtp-transport = ["bufstream", "base64", "nom"]
|
smtp-transport = ["bufstream", "base64", "nom"]
|
||||||
unstable = []
|
unstable = []
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
use lettre::{
|
use lettre::{Message, SmtpTransport, Transport};
|
||||||
transport::smtp::ConnectionReuseParameters, ClientSecurity, Message, SmtpClient, Transport,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn bench_simple_send(c: &mut Criterion) {
|
fn bench_simple_send(c: &mut Criterion) {
|
||||||
let mut sender = SmtpClient::new("127.0.0.1:2525", ClientSecurity::None)
|
let sender = SmtpTransport::new("127.0.0.1").port(2525);
|
||||||
.unwrap()
|
|
||||||
.transport();
|
|
||||||
|
|
||||||
c.bench_function("send email", move |b| {
|
c.bench_function("send email", move |b| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
@@ -24,10 +20,7 @@ fn bench_simple_send(c: &mut Criterion) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn bench_reuse_send(c: &mut Criterion) {
|
fn bench_reuse_send(c: &mut Criterion) {
|
||||||
let mut sender = SmtpClient::new("127.0.0.1:2525", ClientSecurity::None)
|
let sender = SmtpTransport::new("127.0.0.1").port(2525);
|
||||||
.unwrap()
|
|
||||||
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
|
|
||||||
.transport();
|
|
||||||
c.bench_function("send email with connection reuse", move |b| {
|
c.bench_function("send email with connection reuse", move |b| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
let email = Message::builder()
|
let email = Message::builder()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
extern crate env_logger;
|
extern crate env_logger;
|
||||||
extern crate lettre;
|
extern crate lettre;
|
||||||
|
|
||||||
use lettre::{Message, SmtpClient, Transport};
|
use lettre::{Message, SmtpTransport, Transport};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
@@ -14,7 +14,7 @@ fn main() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Open a local connection on port 25
|
// Open a local connection on port 25
|
||||||
let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport();
|
let mailer = SmtpTransport::unencrypted_localhost();
|
||||||
// Send the email
|
// Send the email
|
||||||
let result = mailer.send(&email);
|
let result = mailer.send(&email);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
extern crate lettre;
|
extern crate lettre;
|
||||||
|
|
||||||
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpClient, Transport};
|
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let email = Message::builder()
|
let email = Message::builder()
|
||||||
@@ -17,10 +17,9 @@ fn main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Open a remote connection to gmail
|
// Open a remote connection to gmail
|
||||||
let mut mailer = SmtpClient::new_simple("smtp.gmail.com")
|
let mailer = SmtpTransport::relay("smtp.gmail.com")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.credentials(creds)
|
.credentials(creds);
|
||||||
.transport();
|
|
||||||
|
|
||||||
// Send the email
|
// Send the email
|
||||||
let result = mailer.send(&email);
|
let result = mailer.send(&email);
|
||||||
|
|||||||
@@ -270,7 +270,6 @@ pub mod serde {
|
|||||||
}
|
}
|
||||||
let user: &str = user.ok_or_else(|| DeError::missing_field("user"))?;
|
let user: &str = user.ok_or_else(|| DeError::missing_field("user"))?;
|
||||||
let domain: &str = domain.ok_or_else(|| DeError::missing_field("domain"))?;
|
let domain: &str = domain.ok_or_else(|| DeError::missing_field("domain"))?;
|
||||||
// FIXME avoid unwrap here
|
|
||||||
Ok(Address::new(user, domain).unwrap())
|
Ok(Address::new(user, domain).unwrap())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,5 +278,3 @@ pub mod serde {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME test serializer deserializer
|
|
||||||
|
|||||||
10
src/lib.rs
10
src/lib.rs
@@ -31,11 +31,11 @@ pub use crate::transport::file::FileTransport;
|
|||||||
#[cfg(feature = "sendmail-transport")]
|
#[cfg(feature = "sendmail-transport")]
|
||||||
pub use crate::transport::sendmail::SendmailTransport;
|
pub use crate::transport::sendmail::SendmailTransport;
|
||||||
#[cfg(feature = "smtp-transport")]
|
#[cfg(feature = "smtp-transport")]
|
||||||
pub use crate::transport::smtp::client::net::ClientTlsParameters;
|
pub use crate::transport::smtp::client::net::TlsParameters;
|
||||||
#[cfg(all(feature = "smtp-transport", feature = "connection-pool"))]
|
#[cfg(all(feature = "smtp-transport", feature = "connection-pool"))]
|
||||||
pub use crate::transport::smtp::r2d2::SmtpConnectionManager;
|
pub use crate::transport::smtp::r2d2::SmtpConnectionManager;
|
||||||
#[cfg(feature = "smtp-transport")]
|
#[cfg(feature = "smtp-transport")]
|
||||||
pub use crate::transport::smtp::{ClientSecurity, SmtpClient, SmtpTransport};
|
pub use crate::transport::smtp::{SmtpTransport, Tls};
|
||||||
#[cfg(feature = "builder")]
|
#[cfg(feature = "builder")]
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
@@ -115,14 +115,12 @@ pub trait Transport<'a> {
|
|||||||
type Result;
|
type Result;
|
||||||
|
|
||||||
/// Sends the email
|
/// Sends the email
|
||||||
/// FIXME not mut
|
fn send(&self, message: &Message) -> Self::Result {
|
||||||
|
|
||||||
fn send(&mut self, message: &Message) -> Self::Result {
|
|
||||||
let raw = message.formatted();
|
let raw = message.formatted();
|
||||||
self.send_raw(message.envelope(), &raw)
|
self.send_raw(message.envelope(), &raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_raw(&mut self, envelope: &Envelope, email: &[u8]) -> Self::Result;
|
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Self::Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ struct SerializableEmail<'a> {
|
|||||||
impl<'a> Transport<'a> for FileTransport {
|
impl<'a> Transport<'a> for FileTransport {
|
||||||
type Result = FileResult;
|
type Result = FileResult;
|
||||||
|
|
||||||
fn send_raw(&mut self, envelope: &Envelope, email: &[u8]) -> Self::Result {
|
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Self::Result {
|
||||||
let email_id = Uuid::new_v4();
|
let email_id = Uuid::new_v4();
|
||||||
|
|
||||||
let mut file = self.path.clone();
|
let mut file = self.path.clone();
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ impl SendmailTransport {
|
|||||||
impl<'a> Transport<'a> for SendmailTransport {
|
impl<'a> Transport<'a> for SendmailTransport {
|
||||||
type Result = SendmailResult;
|
type Result = SendmailResult;
|
||||||
|
|
||||||
fn send_raw(&mut self, envelope: &Envelope, email: &[u8]) -> Self::Result {
|
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Self::Result {
|
||||||
let email_id = Uuid::new_v4();
|
let email_id = Uuid::new_v4();
|
||||||
|
|
||||||
// Spawn the sendmail command
|
// Spawn the sendmail command
|
||||||
|
|||||||
@@ -3,13 +3,9 @@
|
|||||||
use crate::transport::smtp::error::Error;
|
use crate::transport::smtp::error::Error;
|
||||||
use std::fmt::{self, Display, Formatter};
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
|
||||||
/// Accepted authentication mechanisms on an encrypted connection
|
/// Accepted authentication mechanisms
|
||||||
/// Trying LOGIN last as it is deprecated.
|
/// Trying LOGIN last as it is deprecated.
|
||||||
pub const DEFAULT_ENCRYPTED_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
|
pub const DEFAULT_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
|
||||||
|
|
||||||
/// Accepted authentication mechanisms on an unencrypted connection
|
|
||||||
// FIXME remove
|
|
||||||
pub const DEFAULT_UNENCRYPTED_MECHANISMS: &[Mechanism] = &[];
|
|
||||||
|
|
||||||
/// Convertible to user credentials
|
/// Convertible to user credentials
|
||||||
pub trait IntoCredentials {
|
pub trait IntoCredentials {
|
||||||
|
|||||||
@@ -2,18 +2,20 @@
|
|||||||
|
|
||||||
use crate::transport::smtp::{
|
use crate::transport::smtp::{
|
||||||
authentication::{Credentials, Mechanism},
|
authentication::{Credentials, Mechanism},
|
||||||
client::net::{ClientTlsParameters, Connector, NetworkStream, Timeout},
|
client::net::{NetworkStream, TlsParameters},
|
||||||
commands::*,
|
commands::*,
|
||||||
error::{Error, SmtpResult},
|
error::{Error, SmtpResult},
|
||||||
|
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
|
||||||
response::Response,
|
response::Response,
|
||||||
};
|
};
|
||||||
|
use crate::Envelope;
|
||||||
use bufstream::BufStream;
|
use bufstream::BufStream;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::{
|
use std::{
|
||||||
fmt::Display,
|
fmt::Display,
|
||||||
io::{self, BufRead, Read, Write},
|
io::{self, BufRead, Write},
|
||||||
net::ToSocketAddrs,
|
net::ToSocketAddrs,
|
||||||
string::String,
|
string::String,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
@@ -77,12 +79,27 @@ fn escape_crlf(string: &str) -> String {
|
|||||||
string.replace("\r\n", "<CRLF>")
|
string.replace("\r\n", "<CRLF>")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! try_smtp (
|
||||||
|
($err: expr, $client: ident) => ({
|
||||||
|
match $err {
|
||||||
|
Ok(val) => val,
|
||||||
|
Err(err) => {
|
||||||
|
$client.abort();
|
||||||
|
return Err(From::from(err))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
/// Structure that implements the SMTP client
|
/// Structure that implements the SMTP client
|
||||||
#[derive(Default)]
|
pub struct SmtpConnection {
|
||||||
pub struct InnerClient<S: Write + Read = NetworkStream> {
|
|
||||||
/// TCP stream between client and server
|
/// TCP stream between client and server
|
||||||
/// Value is None before connection
|
/// Value is None before connection
|
||||||
stream: Option<BufStream<S>>,
|
stream: BufStream<NetworkStream>,
|
||||||
|
/// Panic state
|
||||||
|
panic: bool,
|
||||||
|
/// Information about the server
|
||||||
|
server_info: ServerInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! return_err (
|
macro_rules! return_err (
|
||||||
@@ -91,102 +108,176 @@ macro_rules! return_err (
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
impl<S: Write + Read> InnerClient<S> {
|
impl SmtpConnection {
|
||||||
/// Creates a new SMTP client
|
pub fn server_info(&self) -> &ServerInfo {
|
||||||
///
|
&self.server_info
|
||||||
/// It does not connects to the server, but only creates the `Client`
|
|
||||||
pub fn new() -> InnerClient<S> {
|
|
||||||
InnerClient { stream: None }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S: Connector + Write + Read + Timeout> InnerClient<S> {
|
|
||||||
/// Closes the SMTP transaction if possible
|
|
||||||
pub fn close(&mut self) {
|
|
||||||
let _ = self.command(QuitCommand);
|
|
||||||
self.stream = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the underlying stream
|
|
||||||
pub fn set_stream(&mut self, stream: S) {
|
|
||||||
self.stream = Some(BufStream::new(stream));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Upgrades the underlying connection to SSL/TLS
|
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
|
||||||
pub fn upgrade_tls_stream(
|
|
||||||
&mut self,
|
|
||||||
tls_parameters: &ClientTlsParameters,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
match self.stream {
|
|
||||||
Some(ref mut stream) => stream.get_mut().upgrade_tls(tls_parameters),
|
|
||||||
None => Ok(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tells if the underlying stream is currently encrypted
|
|
||||||
pub fn is_encrypted(&self) -> bool {
|
|
||||||
self.stream
|
|
||||||
.as_ref()
|
|
||||||
.map(|s| s.get_ref().is_encrypted())
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set timeout
|
|
||||||
pub fn set_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
|
||||||
if let Some(ref mut stream) = self.stream {
|
|
||||||
stream.get_mut().set_read_timeout(duration)?;
|
|
||||||
stream.get_mut().set_write_timeout(duration)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connects to the configured server
|
/// Connects to the configured server
|
||||||
|
///
|
||||||
|
/// Sends EHLO and parses server information
|
||||||
pub fn connect<A: ToSocketAddrs>(
|
pub fn connect<A: ToSocketAddrs>(
|
||||||
&mut self,
|
server: A,
|
||||||
addr: &A,
|
|
||||||
timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
tls_parameters: Option<&ClientTlsParameters>,
|
hello_name: &ClientId,
|
||||||
) -> Result<(), Error> {
|
tls_parameters: Option<&TlsParameters>,
|
||||||
// Connect should not be called when the client is already connected
|
) -> Result<SmtpConnection, Error> {
|
||||||
if self.stream.is_some() {
|
let mut addresses = server.to_socket_addrs()?;
|
||||||
return_err!("The connection is already established", self);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut addresses = addr.to_socket_addrs()?;
|
|
||||||
|
|
||||||
|
// FIXME try all
|
||||||
let server_addr = match addresses.next() {
|
let server_addr = match addresses.next() {
|
||||||
Some(addr) => addr,
|
Some(addr) => addr,
|
||||||
None => return_err!("Could not resolve hostname", self),
|
None => return_err!("Could not resolve hostname", self),
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!("connecting to {}", server_addr);
|
debug!("connecting to {}", server_addr);
|
||||||
|
|
||||||
// Try to connect
|
let stream = BufStream::new(NetworkStream::connect(
|
||||||
self.set_stream(Connector::connect(&server_addr, timeout, tls_parameters)?);
|
&server_addr,
|
||||||
|
timeout,
|
||||||
|
tls_parameters,
|
||||||
|
)?);
|
||||||
|
let mut conn = SmtpConnection {
|
||||||
|
stream,
|
||||||
|
panic: false,
|
||||||
|
server_info: ServerInfo::default(),
|
||||||
|
};
|
||||||
|
conn.set_timeout(timeout)?;
|
||||||
|
// TODO log
|
||||||
|
let _response = conn.read_response()?;
|
||||||
|
|
||||||
|
conn.ehlo(hello_name)?;
|
||||||
|
|
||||||
|
// Print server information
|
||||||
|
debug!("server {}", conn.server_info);
|
||||||
|
Ok(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send(&mut self, envelope: &Envelope, email: &[u8]) -> SmtpResult {
|
||||||
|
// Mail
|
||||||
|
let mut mail_options = vec![];
|
||||||
|
|
||||||
|
if self.server_info().supports_feature(Extension::EightBitMime) {
|
||||||
|
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
|
||||||
|
}
|
||||||
|
try_smtp!(
|
||||||
|
self.command(Mail::new(envelope.from().cloned(), mail_options,)),
|
||||||
|
self
|
||||||
|
);
|
||||||
|
|
||||||
|
// Recipient
|
||||||
|
for to_address in envelope.to() {
|
||||||
|
try_smtp!(self.command(Rcpt::new(to_address.clone(), vec![])), self);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data
|
||||||
|
try_smtp!(self.command(Data), self);
|
||||||
|
|
||||||
|
// Message content
|
||||||
|
let result = try_smtp!(self.message(email), self);
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_broken(&self) -> bool {
|
||||||
|
self.panic
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_starttls(&self) -> bool {
|
||||||
|
!self.stream.get_ref().is_encrypted()
|
||||||
|
&& self.server_info.supports_feature(Extension::StartTls)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn starttls(
|
||||||
|
&mut self,
|
||||||
|
tls_parameters: &TlsParameters,
|
||||||
|
hello_name: &ClientId,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if self.server_info.supports_feature(Extension::StartTls) {
|
||||||
|
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||||
|
{
|
||||||
|
try_smtp!(self.command(Starttls), self);
|
||||||
|
try_smtp!(self.stream.get_mut().upgrade_tls(tls_parameters), self);
|
||||||
|
debug!("connection encrypted");
|
||||||
|
// Send EHLO again
|
||||||
|
try_smtp!(self.ehlo(hello_name), self);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(not(any(feature = "native-tls", feature = "rustls")))]
|
||||||
|
// This should never happen as `Tls` can only be created
|
||||||
|
// when a TLS library is enabled
|
||||||
|
unreachable!("TLS support required but not supported");
|
||||||
|
} else {
|
||||||
|
Err(Error::Client("STARTTLS is not supported on this server"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send EHLO and update server info
|
||||||
|
fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> {
|
||||||
|
let ehlo_response = try_smtp!(
|
||||||
|
self.command(Ehlo::new(ClientId::new(hello_name.to_string()))),
|
||||||
|
self
|
||||||
|
);
|
||||||
|
self.server_info = try_smtp!(ServerInfo::from_response(&ehlo_response), self);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn quit(&mut self) -> SmtpResult {
|
||||||
|
Ok(try_smtp!(self.command(Quit), self))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn abort(&mut self) {
|
||||||
|
// Only try to quit if we are not already broken
|
||||||
|
if !self.panic {
|
||||||
|
self.panic = true;
|
||||||
|
let _ = self.command(Quit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the underlying stream
|
||||||
|
pub fn set_stream(&mut self, stream: NetworkStream) {
|
||||||
|
self.stream = BufStream::new(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tells if the underlying stream is currently encrypted
|
||||||
|
pub fn is_encrypted(&self) -> bool {
|
||||||
|
self.stream.get_ref().is_encrypted()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set timeout
|
||||||
|
pub fn set_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||||
|
self.stream.get_mut().set_read_timeout(duration)?;
|
||||||
|
self.stream.get_mut().set_write_timeout(duration)
|
||||||
|
}
|
||||||
|
|
||||||
/// Checks if the server is connected using the NOOP SMTP command
|
/// Checks if the server is connected using the NOOP SMTP command
|
||||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::wrong_self_convention))]
|
pub fn test_connected(&mut self) -> bool {
|
||||||
pub fn is_connected(&mut self) -> bool {
|
self.command(Noop).is_ok()
|
||||||
self.stream.is_some() && self.command(NoopCommand).is_ok()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends an AUTH command with the given mechanism, and handles challenge if needed
|
/// Sends an AUTH command with the given mechanism, and handles challenge if needed
|
||||||
pub fn auth(&mut self, mechanism: Mechanism, credentials: &Credentials) -> SmtpResult {
|
pub fn auth(&mut self, mechanisms: &[Mechanism], credentials: &Credentials) -> SmtpResult {
|
||||||
|
let mechanism = match self.server_info.get_auth_mechanism(mechanisms) {
|
||||||
|
Some(m) => m,
|
||||||
|
None => {
|
||||||
|
return Err(Error::Client(
|
||||||
|
"No compatible authentication mechanism was found",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Limit challenges to avoid blocking
|
// Limit challenges to avoid blocking
|
||||||
let mut challenges = 10;
|
let mut challenges = 10;
|
||||||
let mut response = self.command(AuthCommand::new(mechanism, credentials.clone(), None)?)?;
|
let mut response = self.command(Auth::new(mechanism, credentials.clone(), None)?)?;
|
||||||
|
|
||||||
while challenges > 0 && response.has_code(334) {
|
while challenges > 0 && response.has_code(334) {
|
||||||
challenges -= 1;
|
challenges -= 1;
|
||||||
response = self.command(AuthCommand::new_from_response(
|
response = try_smtp!(
|
||||||
mechanism,
|
self.command(Auth::new_from_response(
|
||||||
credentials.clone(),
|
mechanism,
|
||||||
&response,
|
credentials.clone(),
|
||||||
)?)?;
|
&response,
|
||||||
|
)?),
|
||||||
|
self
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if challenges == 0 {
|
if challenges == 0 {
|
||||||
@@ -214,12 +305,8 @@ impl<S: Connector + Write + Read + Timeout> InnerClient<S> {
|
|||||||
|
|
||||||
/// Writes a string to the server
|
/// Writes a string to the server
|
||||||
fn write(&mut self, string: &[u8]) -> Result<(), Error> {
|
fn write(&mut self, string: &[u8]) -> Result<(), Error> {
|
||||||
if self.stream.is_none() {
|
self.stream.write_all(string)?;
|
||||||
return Err(From::from("Connection closed"));
|
self.stream.flush()?;
|
||||||
}
|
|
||||||
|
|
||||||
self.stream.as_mut().unwrap().write_all(string)?;
|
|
||||||
self.stream.as_mut().unwrap().flush()?;
|
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"Wrote: {}",
|
"Wrote: {}",
|
||||||
@@ -240,7 +327,7 @@ impl<S: Connector + Write + Read + Timeout> InnerClient<S> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// TODO read more than one line
|
// TODO read more than one line
|
||||||
let read_count = self.stream.as_mut().unwrap().read_line(&mut raw_response)?;
|
let read_count = self.stream.read_line(&mut raw_response)?;
|
||||||
|
|
||||||
// EOF is reached
|
// EOF is reached
|
||||||
if read_count == 0 {
|
if read_count == 0 {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use std::{
|
|||||||
/// Parameters to use for secure clients
|
/// Parameters to use for secure clients
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
#[allow(missing_debug_implementations)]
|
#[allow(missing_debug_implementations)]
|
||||||
pub struct ClientTlsParameters {
|
pub struct TlsParameters {
|
||||||
/// A connector from `native-tls`
|
/// A connector from `native-tls`
|
||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
connector: TlsConnector,
|
connector: TlsConnector,
|
||||||
@@ -30,17 +30,17 @@ pub struct ClientTlsParameters {
|
|||||||
domain: String,
|
domain: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientTlsParameters {
|
impl TlsParameters {
|
||||||
/// Creates a `ClientTlsParameters`
|
/// Creates a `TlsParameters`
|
||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
pub fn new(domain: String, connector: TlsConnector) -> Self {
|
pub fn new(domain: String, connector: TlsConnector) -> Self {
|
||||||
ClientTlsParameters { connector, domain }
|
Self { connector, domain }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a `ClientTlsParameters`
|
/// Creates a `TlsParameters`
|
||||||
#[cfg(feature = "rustls")]
|
#[cfg(feature = "rustls")]
|
||||||
pub fn new(domain: String, connector: ClientConfig) -> Self {
|
pub fn new(domain: String, connector: ClientConfig) -> Self {
|
||||||
ClientTlsParameters {
|
Self {
|
||||||
connector: Box::new(connector),
|
connector: Box::new(connector),
|
||||||
domain,
|
domain,
|
||||||
}
|
}
|
||||||
@@ -87,6 +87,85 @@ impl NetworkStream {
|
|||||||
NetworkStream::Mock(_) => Ok(()),
|
NetworkStream::Mock(_) => Ok(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn connect(
|
||||||
|
addr: &SocketAddr,
|
||||||
|
timeout: Option<Duration>,
|
||||||
|
tls_parameters: Option<&TlsParameters>,
|
||||||
|
) -> Result<NetworkStream, Error> {
|
||||||
|
let tcp_stream = match timeout {
|
||||||
|
Some(t) => TcpStream::connect_timeout(addr, t)?,
|
||||||
|
None => TcpStream::connect(addr)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
match tls_parameters {
|
||||||
|
#[cfg(feature = "native-tls")]
|
||||||
|
Some(context) => context
|
||||||
|
.connector
|
||||||
|
.connect(context.domain.as_ref(), tcp_stream)
|
||||||
|
.map(|tls| NetworkStream::Tls(Box::new(tls)))
|
||||||
|
.map_err(|e| Error::Io(io::Error::new(ErrorKind::Other, e))),
|
||||||
|
#[cfg(feature = "rustls")]
|
||||||
|
Some(context) => {
|
||||||
|
let domain = webpki::DNSNameRef::try_from_ascii_str(&context.domain)?;
|
||||||
|
|
||||||
|
Ok(NetworkStream::Tls(Box::new(rustls::StreamOwned::new(
|
||||||
|
ClientSession::new(&Arc::new(*context.connector.clone()), domain),
|
||||||
|
tcp_stream,
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
None => Ok(NetworkStream::Tcp(tcp_stream)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn upgrade_tls(&mut self, tls_parameters: &TlsParameters) -> Result<(), Error> {
|
||||||
|
*self = match *self {
|
||||||
|
#[cfg(feature = "native-tls")]
|
||||||
|
NetworkStream::Tcp(ref mut stream) => match tls_parameters
|
||||||
|
.connector
|
||||||
|
.connect(tls_parameters.domain.as_ref(), stream.try_clone().unwrap())
|
||||||
|
{
|
||||||
|
Ok(tls_stream) => NetworkStream::Tls(Box::new(tls_stream)),
|
||||||
|
Err(err) => return Err(Error::Io(io::Error::new(ErrorKind::Other, err))),
|
||||||
|
},
|
||||||
|
#[cfg(feature = "rustls")]
|
||||||
|
NetworkStream::Tcp(ref mut stream) => {
|
||||||
|
let domain = webpki::DNSNameRef::try_from_ascii_str(&tls_parameters.domain)?;
|
||||||
|
|
||||||
|
NetworkStream::Tls(Box::new(rustls::StreamOwned::new(
|
||||||
|
ClientSession::new(&Arc::new(*tls_parameters.connector.clone()), domain),
|
||||||
|
stream.try_clone().unwrap(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
NetworkStream::Tls(_) | NetworkStream::Mock(_) => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_encrypted(&self) -> bool {
|
||||||
|
match *self {
|
||||||
|
NetworkStream::Tcp(_) | NetworkStream::Mock(_) => false,
|
||||||
|
NetworkStream::Tls(_) => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||||
|
match *self {
|
||||||
|
NetworkStream::Tcp(ref mut stream) => stream.set_read_timeout(duration),
|
||||||
|
NetworkStream::Tls(ref mut stream) => stream.get_ref().set_read_timeout(duration),
|
||||||
|
NetworkStream::Mock(_) => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set write timeout for IO calls
|
||||||
|
pub fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||||
|
match *self {
|
||||||
|
NetworkStream::Tcp(ref mut stream) => stream.set_write_timeout(duration),
|
||||||
|
NetworkStream::Tls(ref mut stream) => stream.get_ref().set_write_timeout(duration),
|
||||||
|
NetworkStream::Mock(_) => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Read for NetworkStream {
|
impl Read for NetworkStream {
|
||||||
@@ -125,108 +204,3 @@ impl Write for NetworkStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A trait for the concept of opening a stream
|
|
||||||
pub trait Connector: Sized {
|
|
||||||
/// Opens a connection to the given IP socket
|
|
||||||
fn connect(
|
|
||||||
addr: &SocketAddr,
|
|
||||||
timeout: Option<Duration>,
|
|
||||||
tls_parameters: Option<&ClientTlsParameters>,
|
|
||||||
) -> Result<Self, Error>;
|
|
||||||
/// Upgrades to TLS connection
|
|
||||||
fn upgrade_tls(&mut self, tls_parameters: &ClientTlsParameters) -> Result<(), Error>;
|
|
||||||
/// Is the NetworkStream encrypted
|
|
||||||
fn is_encrypted(&self) -> bool;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Connector for NetworkStream {
|
|
||||||
fn connect(
|
|
||||||
addr: &SocketAddr,
|
|
||||||
timeout: Option<Duration>,
|
|
||||||
tls_parameters: Option<&ClientTlsParameters>,
|
|
||||||
) -> Result<NetworkStream, Error> {
|
|
||||||
let tcp_stream = match timeout {
|
|
||||||
Some(duration) => TcpStream::connect_timeout(addr, duration)?,
|
|
||||||
None => TcpStream::connect(addr)?,
|
|
||||||
};
|
|
||||||
|
|
||||||
match tls_parameters {
|
|
||||||
#[cfg(feature = "native-tls")]
|
|
||||||
Some(context) => context
|
|
||||||
.connector
|
|
||||||
.connect(context.domain.as_ref(), tcp_stream)
|
|
||||||
.map(|tls| NetworkStream::Tls(Box::new(tls)))
|
|
||||||
.map_err(|e| Error::Io(io::Error::new(ErrorKind::Other, e))),
|
|
||||||
#[cfg(feature = "rustls")]
|
|
||||||
Some(context) => {
|
|
||||||
let domain = webpki::DNSNameRef::try_from_ascii_str(&context.domain)?;
|
|
||||||
|
|
||||||
Ok(NetworkStream::Tls(Box::new(rustls::StreamOwned::new(
|
|
||||||
ClientSession::new(&Arc::new(*context.connector.clone()), domain),
|
|
||||||
tcp_stream,
|
|
||||||
))))
|
|
||||||
}
|
|
||||||
None => Ok(NetworkStream::Tcp(tcp_stream)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn upgrade_tls(&mut self, tls_parameters: &ClientTlsParameters) -> Result<(), Error> {
|
|
||||||
*self = match *self {
|
|
||||||
#[cfg(feature = "native-tls")]
|
|
||||||
NetworkStream::Tcp(ref mut stream) => match tls_parameters
|
|
||||||
.connector
|
|
||||||
.connect(tls_parameters.domain.as_ref(), stream.try_clone().unwrap())
|
|
||||||
{
|
|
||||||
Ok(tls_stream) => NetworkStream::Tls(Box::new(tls_stream)),
|
|
||||||
Err(err) => return Err(Error::Io(io::Error::new(ErrorKind::Other, err))),
|
|
||||||
},
|
|
||||||
#[cfg(feature = "rustls")]
|
|
||||||
NetworkStream::Tcp(ref mut stream) => {
|
|
||||||
let domain = webpki::DNSNameRef::try_from_ascii_str(&tls_parameters.domain)?;
|
|
||||||
|
|
||||||
NetworkStream::Tls(Box::new(rustls::StreamOwned::new(
|
|
||||||
ClientSession::new(&Arc::new(*tls_parameters.connector.clone()), domain),
|
|
||||||
stream.try_clone().unwrap(),
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
NetworkStream::Tls(_) | NetworkStream::Mock(_) => return Ok(()),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_encrypted(&self) -> bool {
|
|
||||||
match *self {
|
|
||||||
NetworkStream::Tcp(_) | NetworkStream::Mock(_) => false,
|
|
||||||
NetworkStream::Tls(_) => true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A trait for read and write timeout support
|
|
||||||
pub trait Timeout: Sized {
|
|
||||||
/// Set read timeout for IO calls
|
|
||||||
fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()>;
|
|
||||||
/// Set write timeout for IO calls
|
|
||||||
fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Timeout for NetworkStream {
|
|
||||||
fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
|
||||||
match *self {
|
|
||||||
NetworkStream::Tcp(ref mut stream) => stream.set_read_timeout(duration),
|
|
||||||
NetworkStream::Tls(ref mut stream) => stream.get_ref().set_read_timeout(duration),
|
|
||||||
NetworkStream::Mock(_) => Ok(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set write timeout for IO calls
|
|
||||||
fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
|
||||||
match *self {
|
|
||||||
NetworkStream::Tcp(ref mut stream) => stream.set_write_timeout(duration),
|
|
||||||
NetworkStream::Tls(ref mut stream) => stream.get_ref().set_write_timeout(duration),
|
|
||||||
NetworkStream::Mock(_) => Ok(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -18,30 +18,29 @@ use std::{
|
|||||||
/// EHLO command
|
/// EHLO command
|
||||||
#[derive(PartialEq, Clone, Debug)]
|
#[derive(PartialEq, Clone, Debug)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct EhloCommand {
|
pub struct Ehlo {
|
||||||
client_id: ClientId,
|
client_id: ClientId,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for EhloCommand {
|
impl Display for Ehlo {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
#[allow(clippy::write_with_newline)]
|
|
||||||
write!(f, "EHLO {}\r\n", self.client_id)
|
write!(f, "EHLO {}\r\n", self.client_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EhloCommand {
|
impl Ehlo {
|
||||||
/// Creates a EHLO command
|
/// Creates a EHLO command
|
||||||
pub fn new(client_id: ClientId) -> EhloCommand {
|
pub fn new(client_id: ClientId) -> Ehlo {
|
||||||
EhloCommand { client_id }
|
Ehlo { client_id }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// STARTTLS command
|
/// STARTTLS command
|
||||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct StarttlsCommand;
|
pub struct Starttls;
|
||||||
|
|
||||||
impl Display for StarttlsCommand {
|
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")
|
f.write_str("STARTTLS\r\n")
|
||||||
}
|
}
|
||||||
@@ -50,12 +49,12 @@ impl Display for StarttlsCommand {
|
|||||||
/// MAIL command
|
/// MAIL command
|
||||||
#[derive(PartialEq, Clone, Debug)]
|
#[derive(PartialEq, Clone, Debug)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct MailCommand {
|
pub struct Mail {
|
||||||
sender: Option<Address>,
|
sender: Option<Address>,
|
||||||
parameters: Vec<MailParameter>,
|
parameters: Vec<MailParameter>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for MailCommand {
|
impl Display for Mail {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
@@ -69,22 +68,22 @@ impl Display for MailCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MailCommand {
|
impl Mail {
|
||||||
/// Creates a MAIL command
|
/// Creates a MAIL command
|
||||||
pub fn new(sender: Option<Address>, parameters: Vec<MailParameter>) -> MailCommand {
|
pub fn new(sender: Option<Address>, parameters: Vec<MailParameter>) -> Mail {
|
||||||
MailCommand { sender, parameters }
|
Mail { sender, parameters }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RCPT command
|
/// RCPT command
|
||||||
#[derive(PartialEq, Clone, Debug)]
|
#[derive(PartialEq, Clone, Debug)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct RcptCommand {
|
pub struct Rcpt {
|
||||||
recipient: Address,
|
recipient: Address,
|
||||||
parameters: Vec<RcptParameter>,
|
parameters: Vec<RcptParameter>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for RcptCommand {
|
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)?;
|
write!(f, "RCPT TO:<{}>", self.recipient)?;
|
||||||
for parameter in &self.parameters {
|
for parameter in &self.parameters {
|
||||||
@@ -94,10 +93,10 @@ impl Display for RcptCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RcptCommand {
|
impl Rcpt {
|
||||||
/// Creates an RCPT command
|
/// Creates an RCPT command
|
||||||
pub fn new(recipient: Address, parameters: Vec<RcptParameter>) -> RcptCommand {
|
pub fn new(recipient: Address, parameters: Vec<RcptParameter>) -> Rcpt {
|
||||||
RcptCommand {
|
Rcpt {
|
||||||
recipient,
|
recipient,
|
||||||
parameters,
|
parameters,
|
||||||
}
|
}
|
||||||
@@ -107,9 +106,9 @@ impl RcptCommand {
|
|||||||
/// DATA command
|
/// DATA command
|
||||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct DataCommand;
|
pub struct Data;
|
||||||
|
|
||||||
impl Display for DataCommand {
|
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")
|
f.write_str("DATA\r\n")
|
||||||
}
|
}
|
||||||
@@ -118,9 +117,9 @@ impl Display for DataCommand {
|
|||||||
/// QUIT command
|
/// QUIT command
|
||||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct QuitCommand;
|
pub struct Quit;
|
||||||
|
|
||||||
impl Display for QuitCommand {
|
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")
|
f.write_str("QUIT\r\n")
|
||||||
}
|
}
|
||||||
@@ -129,9 +128,9 @@ impl Display for QuitCommand {
|
|||||||
/// NOOP command
|
/// NOOP command
|
||||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct NoopCommand;
|
pub struct Noop;
|
||||||
|
|
||||||
impl Display for NoopCommand {
|
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")
|
f.write_str("NOOP\r\n")
|
||||||
}
|
}
|
||||||
@@ -140,11 +139,11 @@ impl Display for NoopCommand {
|
|||||||
/// HELP command
|
/// HELP command
|
||||||
#[derive(PartialEq, Clone, Debug)]
|
#[derive(PartialEq, Clone, Debug)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct HelpCommand {
|
pub struct Help {
|
||||||
argument: Option<String>,
|
argument: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for HelpCommand {
|
impl Display for Help {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
f.write_str("HELP")?;
|
f.write_str("HELP")?;
|
||||||
if self.argument.is_some() {
|
if self.argument.is_some() {
|
||||||
@@ -154,61 +153,61 @@ impl Display for HelpCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HelpCommand {
|
impl Help {
|
||||||
/// Creates an HELP command
|
/// Creates an HELP command
|
||||||
pub fn new(argument: Option<String>) -> HelpCommand {
|
pub fn new(argument: Option<String>) -> Help {
|
||||||
HelpCommand { argument }
|
Help { argument }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// VRFY command
|
/// VRFY command
|
||||||
#[derive(PartialEq, Clone, Debug)]
|
#[derive(PartialEq, Clone, Debug)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct VrfyCommand {
|
pub struct Vrfy {
|
||||||
argument: String,
|
argument: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for VrfyCommand {
|
impl Display for Vrfy {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
#[allow(clippy::write_with_newline)]
|
#[allow(clippy::write_with_newline)]
|
||||||
write!(f, "VRFY {}\r\n", self.argument)
|
write!(f, "VRFY {}\r\n", self.argument)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VrfyCommand {
|
impl Vrfy {
|
||||||
/// Creates a VRFY command
|
/// Creates a VRFY command
|
||||||
pub fn new(argument: String) -> VrfyCommand {
|
pub fn new(argument: String) -> Vrfy {
|
||||||
VrfyCommand { argument }
|
Vrfy { argument }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// EXPN command
|
/// EXPN command
|
||||||
#[derive(PartialEq, Clone, Debug)]
|
#[derive(PartialEq, Clone, Debug)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct ExpnCommand {
|
pub struct Expn {
|
||||||
argument: String,
|
argument: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for ExpnCommand {
|
impl Display for Expn {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
#[allow(clippy::write_with_newline)]
|
#[allow(clippy::write_with_newline)]
|
||||||
write!(f, "EXPN {}\r\n", self.argument)
|
write!(f, "EXPN {}\r\n", self.argument)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExpnCommand {
|
impl Expn {
|
||||||
/// Creates an EXPN command
|
/// Creates an EXPN command
|
||||||
pub fn new(argument: String) -> ExpnCommand {
|
pub fn new(argument: String) -> Expn {
|
||||||
ExpnCommand { argument }
|
Expn { argument }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RSET command
|
/// RSET command
|
||||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct RsetCommand;
|
pub struct Rset;
|
||||||
|
|
||||||
impl Display for RsetCommand {
|
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")
|
f.write_str("RSET\r\n")
|
||||||
}
|
}
|
||||||
@@ -217,14 +216,14 @@ impl Display for RsetCommand {
|
|||||||
/// AUTH command
|
/// AUTH command
|
||||||
#[derive(PartialEq, Clone, Debug)]
|
#[derive(PartialEq, Clone, Debug)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct AuthCommand {
|
pub struct Auth {
|
||||||
mechanism: Mechanism,
|
mechanism: Mechanism,
|
||||||
credentials: Credentials,
|
credentials: Credentials,
|
||||||
challenge: Option<String>,
|
challenge: Option<String>,
|
||||||
response: Option<String>,
|
response: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for AuthCommand {
|
impl Display for Auth {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
let encoded_response = self
|
let encoded_response = self
|
||||||
.response
|
.response
|
||||||
@@ -243,19 +242,19 @@ impl Display for AuthCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AuthCommand {
|
impl Auth {
|
||||||
/// Creates an AUTH command (from a challenge if provided)
|
/// Creates an AUTH command (from a challenge if provided)
|
||||||
pub fn new(
|
pub fn new(
|
||||||
mechanism: Mechanism,
|
mechanism: Mechanism,
|
||||||
credentials: Credentials,
|
credentials: Credentials,
|
||||||
challenge: Option<String>,
|
challenge: Option<String>,
|
||||||
) -> Result<AuthCommand, Error> {
|
) -> Result<Auth, Error> {
|
||||||
let response = if mechanism.supports_initial_response() || challenge.is_some() {
|
let response = if mechanism.supports_initial_response() || challenge.is_some() {
|
||||||
Some(mechanism.response(&credentials, challenge.as_deref())?)
|
Some(mechanism.response(&credentials, challenge.as_deref())?)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
Ok(AuthCommand {
|
Ok(Auth {
|
||||||
mechanism,
|
mechanism,
|
||||||
credentials,
|
credentials,
|
||||||
challenge,
|
challenge,
|
||||||
@@ -269,7 +268,7 @@ impl AuthCommand {
|
|||||||
mechanism: Mechanism,
|
mechanism: Mechanism,
|
||||||
credentials: Credentials,
|
credentials: Credentials,
|
||||||
response: &Response,
|
response: &Response,
|
||||||
) -> Result<AuthCommand, Error> {
|
) -> Result<Auth, Error> {
|
||||||
if !response.has_code(334) {
|
if !response.has_code(334) {
|
||||||
return Err(Error::ResponseParsing("Expecting a challenge"));
|
return Err(Error::ResponseParsing("Expecting a challenge"));
|
||||||
}
|
}
|
||||||
@@ -284,7 +283,7 @@ impl AuthCommand {
|
|||||||
|
|
||||||
let response = Some(mechanism.response(&credentials, Some(decoded_challenge.as_ref()))?);
|
let response = Some(mechanism.response(&credentials, Some(decoded_challenge.as_ref()))?);
|
||||||
|
|
||||||
Ok(AuthCommand {
|
Ok(Auth {
|
||||||
mechanism,
|
mechanism,
|
||||||
credentials,
|
credentials,
|
||||||
challenge: Some(decoded_challenge),
|
challenge: Some(decoded_challenge),
|
||||||
@@ -311,26 +310,23 @@ mod test {
|
|||||||
keyword: "TEST".to_string(),
|
keyword: "TEST".to_string(),
|
||||||
value: Some("value".to_string()),
|
value: Some("value".to_string()),
|
||||||
};
|
};
|
||||||
assert_eq!(format!("{}", EhloCommand::new(id)), "EHLO localhost\r\n");
|
assert_eq!(format!("{}", Ehlo::new(id)), "EHLO localhost\r\n");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!("{}", MailCommand::new(Some(email.clone()), vec![])),
|
format!("{}", Mail::new(Some(email.clone()), vec![])),
|
||||||
"MAIL FROM:<test@example.com>\r\n"
|
"MAIL FROM:<test@example.com>\r\n"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(format!("{}", Mail::new(None, vec![])), "MAIL FROM:<>\r\n");
|
||||||
format!("{}", MailCommand::new(None, vec![])),
|
|
||||||
"MAIL FROM:<>\r\n"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!(
|
format!(
|
||||||
"{}",
|
"{}",
|
||||||
MailCommand::new(Some(email.clone()), vec![MailParameter::Size(42)])
|
Mail::new(Some(email.clone()), vec![MailParameter::Size(42)])
|
||||||
),
|
),
|
||||||
"MAIL FROM:<test@example.com> SIZE=42\r\n"
|
"MAIL FROM:<test@example.com> SIZE=42\r\n"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!(
|
format!(
|
||||||
"{}",
|
"{}",
|
||||||
MailCommand::new(
|
Mail::new(
|
||||||
Some(email.clone()),
|
Some(email.clone()),
|
||||||
vec![
|
vec![
|
||||||
MailParameter::Size(42),
|
MailParameter::Size(42),
|
||||||
@@ -342,42 +338,42 @@ mod test {
|
|||||||
"MAIL FROM:<test@example.com> SIZE=42 BODY=8BITMIME TEST=value\r\n"
|
"MAIL FROM:<test@example.com> SIZE=42 BODY=8BITMIME TEST=value\r\n"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!("{}", RcptCommand::new(email.clone(), vec![])),
|
format!("{}", Rcpt::new(email.clone(), vec![])),
|
||||||
"RCPT TO:<test@example.com>\r\n"
|
"RCPT TO:<test@example.com>\r\n"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!("{}", RcptCommand::new(email.clone(), vec![rcpt_parameter])),
|
format!("{}", Rcpt::new(email.clone(), vec![rcpt_parameter])),
|
||||||
"RCPT TO:<test@example.com> TEST=value\r\n"
|
"RCPT TO:<test@example.com> TEST=value\r\n"
|
||||||
);
|
);
|
||||||
assert_eq!(format!("{}", QuitCommand), "QUIT\r\n");
|
assert_eq!(format!("{}", Quit), "QUIT\r\n");
|
||||||
assert_eq!(format!("{}", DataCommand), "DATA\r\n");
|
assert_eq!(format!("{}", Data), "DATA\r\n");
|
||||||
assert_eq!(format!("{}", NoopCommand), "NOOP\r\n");
|
assert_eq!(format!("{}", Noop), "NOOP\r\n");
|
||||||
assert_eq!(format!("{}", HelpCommand::new(None)), "HELP\r\n");
|
assert_eq!(format!("{}", Help::new(None)), "HELP\r\n");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!("{}", HelpCommand::new(Some("test".to_string()))),
|
format!("{}", Help::new(Some("test".to_string()))),
|
||||||
"HELP test\r\n"
|
"HELP test\r\n"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!("{}", VrfyCommand::new("test".to_string())),
|
format!("{}", Vrfy::new("test".to_string())),
|
||||||
"VRFY test\r\n"
|
"VRFY test\r\n"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!("{}", ExpnCommand::new("test".to_string())),
|
format!("{}", Expn::new("test".to_string())),
|
||||||
"EXPN test\r\n"
|
"EXPN test\r\n"
|
||||||
);
|
);
|
||||||
assert_eq!(format!("{}", RsetCommand), "RSET\r\n");
|
assert_eq!(format!("{}", Rset), "RSET\r\n");
|
||||||
let credentials = Credentials::new("user".to_string(), "password".to_string());
|
let credentials = Credentials::new("user".to_string(), "password".to_string());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!(
|
format!(
|
||||||
"{}",
|
"{}",
|
||||||
AuthCommand::new(Mechanism::Plain, credentials.clone(), None).unwrap()
|
Auth::new(Mechanism::Plain, credentials.clone(), None).unwrap()
|
||||||
),
|
),
|
||||||
"AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n"
|
"AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!(
|
format!(
|
||||||
"{}",
|
"{}",
|
||||||
AuthCommand::new(Mechanism::Login, credentials.clone(), None).unwrap()
|
Auth::new(Mechanism::Login, credentials.clone(), None).unwrap()
|
||||||
),
|
),
|
||||||
"AUTH LOGIN\r\n"
|
"AUTH LOGIN\r\n"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,11 +3,9 @@
|
|||||||
use self::Error::*;
|
use self::Error::*;
|
||||||
use crate::transport::smtp::response::{Response, Severity};
|
use crate::transport::smtp::response::{Response, Severity};
|
||||||
use base64::DecodeError;
|
use base64::DecodeError;
|
||||||
#[cfg(feature = "native-tls")]
|
|
||||||
use std::{
|
use std::{
|
||||||
error::Error as StdError,
|
error::Error as StdError,
|
||||||
fmt,
|
fmt::{self, Display, Formatter},
|
||||||
fmt::{Display, Formatter},
|
|
||||||
io,
|
io,
|
||||||
string::FromUtf8Error,
|
string::FromUtf8Error,
|
||||||
};
|
};
|
||||||
@@ -43,10 +41,11 @@ pub enum Error {
|
|||||||
/// Invalid hostname
|
/// Invalid hostname
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls-tls")]
|
||||||
InvalidDNSName(webpki::InvalidDNSNameError),
|
InvalidDNSName(webpki::InvalidDNSNameError),
|
||||||
|
#[cfg(feature = "r2d2")]
|
||||||
|
Pool(r2d2::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Error {
|
impl Display for Error {
|
||||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))]
|
|
||||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
||||||
match *self {
|
match *self {
|
||||||
// Try to display the first line of the server's response that usually
|
// Try to display the first line of the server's response that usually
|
||||||
@@ -70,6 +69,7 @@ impl Display for Error {
|
|||||||
Parsing(ref err) => fmt.write_str(err.description()),
|
Parsing(ref err) => fmt.write_str(err.description()),
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls-tls")]
|
||||||
InvalidDNSName(ref err) => err.fmt(fmt),
|
InvalidDNSName(ref err) => err.fmt(fmt),
|
||||||
|
Pool(ref err) => err.fmt(fmt),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,6 +129,13 @@ impl From<webpki::InvalidDNSNameError> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "r2d2")]
|
||||||
|
impl From<r2d2::Error> for Error {
|
||||||
|
fn from(err: r2d2::Error) -> Error {
|
||||||
|
Pool(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<Response> for Error {
|
impl From<Response> for Error {
|
||||||
fn from(response: Response) -> Error {
|
fn from(response: Response) -> Error {
|
||||||
match response.code.severity {
|
match response.code.severity {
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ impl ClientId {
|
|||||||
.unwrap_or_else(|_| DEFAULT_DOMAIN_CLIENT_ID.to_string()),
|
.unwrap_or_else(|_| DEFAULT_DOMAIN_CLIENT_ID.to_string()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
#[cfg(not(feature = "hostname"))]
|
||||||
|
pub fn hostname() -> ClientId {
|
||||||
|
ClientId::Domain(DEFAULT_DOMAIN_CLIENT_ID.to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Supported ESMTP keywords
|
/// Supported ESMTP keywords
|
||||||
@@ -86,7 +90,7 @@ impl Display for Extension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Contains information about an SMTP server
|
/// Contains information about an SMTP server
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq, Default)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct ServerInfo {
|
pub struct ServerInfo {
|
||||||
/// Server name
|
/// Server name
|
||||||
@@ -176,6 +180,16 @@ impl ServerInfo {
|
|||||||
self.features
|
self.features
|
||||||
.contains(&Extension::Authentication(mechanism))
|
.contains(&Extension::Authentication(mechanism))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets a compatible mechanism from list
|
||||||
|
pub fn get_auth_mechanism(&self, mechanisms: &[Mechanism]) -> Option<Mechanism> {
|
||||||
|
for mechanism in mechanisms {
|
||||||
|
if self.supports_auth_mechanism(*mechanism) {
|
||||||
|
return Some(*mechanism);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A `MAIL FROM` extension parameter
|
/// A `MAIL FROM` extension parameter
|
||||||
|
|||||||
@@ -10,32 +10,25 @@
|
|||||||
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
|
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
|
||||||
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and XOAUTH2 mechanisms
|
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and XOAUTH2 mechanisms
|
||||||
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
|
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
|
||||||
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
|
|
||||||
//!
|
//!
|
||||||
|
|
||||||
use crate::Envelope;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
transport::smtp::{
|
transport::smtp::{
|
||||||
authentication::{
|
authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS},
|
||||||
Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS, DEFAULT_UNENCRYPTED_MECHANISMS,
|
client::{net::TlsParameters, SmtpConnection},
|
||||||
},
|
|
||||||
client::{net::ClientTlsParameters, InnerClient},
|
|
||||||
commands::*,
|
|
||||||
error::{Error, SmtpResult},
|
error::{Error, SmtpResult},
|
||||||
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
|
extension::ClientId,
|
||||||
},
|
},
|
||||||
Transport,
|
Envelope, Transport,
|
||||||
};
|
};
|
||||||
use log::{debug, info};
|
|
||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
use native_tls::{Protocol, TlsConnector};
|
use native_tls::{Protocol, TlsConnector};
|
||||||
|
#[cfg(feature = "r2d2")]
|
||||||
|
use r2d2::Pool;
|
||||||
#[cfg(feature = "rustls")]
|
#[cfg(feature = "rustls")]
|
||||||
use rustls::ClientConfig;
|
use rustls::ClientConfig;
|
||||||
use std::{
|
use std::ops::DerefMut;
|
||||||
net::{SocketAddr, ToSocketAddrs},
|
use std::time::Duration;
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
use uuid::Uuid;
|
|
||||||
#[cfg(feature = "rustls")]
|
#[cfg(feature = "rustls")]
|
||||||
use webpki_roots::TLS_SERVER_ROOTS;
|
use webpki_roots::TLS_SERVER_ROOTS;
|
||||||
|
|
||||||
@@ -44,8 +37,8 @@ pub mod client;
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod extension;
|
pub mod extension;
|
||||||
#[cfg(feature = "connection-pool")]
|
#[cfg(feature = "r2d2")]
|
||||||
pub mod r2d2;
|
pub mod pool;
|
||||||
pub mod response;
|
pub mod response;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
@@ -62,378 +55,192 @@ pub const SUBMISSIONS_PORT: u16 = 465;
|
|||||||
|
|
||||||
/// Accepted protocols by default.
|
/// Accepted protocols by default.
|
||||||
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults.
|
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults.
|
||||||
/// This is also rustls' default behavior
|
// This is also rustls' default behavior
|
||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
const DEFAULT_TLS_MIN_PROTOCOL: Protocol = Protocol::Tlsv12;
|
const DEFAULT_TLS_MIN_PROTOCOL: Protocol = Protocol::Tlsv12;
|
||||||
|
|
||||||
/// How to apply TLS to a client connection
|
/// How to apply TLS to a client connection
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
#[allow(missing_debug_implementations)]
|
#[allow(missing_debug_implementations)]
|
||||||
pub enum ClientSecurity {
|
pub enum Tls {
|
||||||
/// Insecure connection only (for testing purposes)
|
/// Insecure connection only (for testing purposes)
|
||||||
None,
|
None,
|
||||||
/// Start with insecure connection and use `STARTTLS` when available
|
/// Start with insecure connection and use `STARTTLS` when available
|
||||||
Opportunistic(ClientTlsParameters),
|
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||||
|
Opportunistic(TlsParameters),
|
||||||
/// Start with insecure connection and require `STARTTLS`
|
/// Start with insecure connection and require `STARTTLS`
|
||||||
Required(ClientTlsParameters),
|
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||||
|
Required(TlsParameters),
|
||||||
/// Use TLS wrapped connection
|
/// Use TLS wrapped connection
|
||||||
Wrapper(ClientTlsParameters),
|
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||||
}
|
Wrapper(TlsParameters),
|
||||||
|
|
||||||
/// Configures connection reuse behavior
|
|
||||||
#[derive(Clone, Debug, Copy)]
|
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
|
||||||
pub enum ConnectionReuseParameters {
|
|
||||||
/// Unlimited connection reuse
|
|
||||||
ReuseUnlimited,
|
|
||||||
/// Maximum number of connection reuse
|
|
||||||
ReuseLimited(u16),
|
|
||||||
/// Disable connection reuse, close connection after each transaction
|
|
||||||
NoReuse,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Contains client configuration
|
/// Contains client configuration
|
||||||
#[allow(missing_debug_implementations)]
|
#[allow(missing_debug_implementations)]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SmtpClient {
|
pub struct SmtpTransport {
|
||||||
/// Enable connection reuse
|
|
||||||
connection_reuse: ConnectionReuseParameters,
|
|
||||||
/// Name sent during EHLO
|
/// Name sent during EHLO
|
||||||
hello_name: ClientId,
|
hello_name: ClientId,
|
||||||
|
/// Server we are connecting to
|
||||||
|
server: String,
|
||||||
|
/// Port to connect to
|
||||||
|
port: u16,
|
||||||
|
/// TLS security configuration
|
||||||
|
tls: Tls,
|
||||||
|
/// Optional enforced authentication mechanism
|
||||||
|
authentication: Vec<Mechanism>,
|
||||||
/// Credentials
|
/// Credentials
|
||||||
credentials: Option<Credentials>,
|
credentials: Option<Credentials>,
|
||||||
/// Socket we are connecting to
|
|
||||||
server_addr: SocketAddr,
|
|
||||||
/// TLS security configuration
|
|
||||||
security: ClientSecurity,
|
|
||||||
/// Enable UTF8 mailboxes in envelope or headers
|
|
||||||
smtp_utf8: bool,
|
|
||||||
/// Optional enforced authentication mechanism
|
|
||||||
authentication_mechanism: Option<Mechanism>,
|
|
||||||
/// Force use of the set authentication mechanism even if server does not report to support it
|
|
||||||
force_set_auth: bool,
|
|
||||||
/// Define network timeout
|
/// Define network timeout
|
||||||
/// It can be changed later for specific needs (like a different timeout for each SMTP command)
|
/// It can be changed later for specific needs (like a different timeout for each SMTP command)
|
||||||
timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
|
/// Connection pool
|
||||||
|
#[cfg(feature = "r2d2")]
|
||||||
|
pool: Option<Pool<SmtpTransport>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builder for the SMTP `SmtpTransport`
|
/// Builder for the SMTP `SmtpTransport`
|
||||||
impl SmtpClient {
|
impl SmtpTransport {
|
||||||
/// Creates a new SMTP client
|
/// Creates a new SMTP client
|
||||||
///
|
///
|
||||||
/// Defaults are:
|
/// Defaults are:
|
||||||
///
|
///
|
||||||
/// * No connection reuse
|
|
||||||
/// * No authentication
|
/// * No authentication
|
||||||
/// * No SMTPUTF8 support
|
|
||||||
/// * A 60 seconds timeout for smtp commands
|
/// * A 60 seconds timeout for smtp commands
|
||||||
|
/// * Port 587
|
||||||
///
|
///
|
||||||
/// Consider using [`SmtpClient::new_simple`] instead, if possible.
|
/// Consider using [`SmtpTransport::new`] instead, if possible.
|
||||||
pub fn new<A: ToSocketAddrs>(addr: A, security: ClientSecurity) -> Result<SmtpClient, Error> {
|
pub fn new<T: Into<String>>(server: T) -> Self {
|
||||||
let mut addresses = addr.to_socket_addrs()?;
|
Self {
|
||||||
|
server: server.into(),
|
||||||
match addresses.next() {
|
port: SUBMISSION_PORT,
|
||||||
Some(addr) => Ok(SmtpClient {
|
hello_name: ClientId::hostname(),
|
||||||
server_addr: addr,
|
credentials: None,
|
||||||
security,
|
authentication: DEFAULT_MECHANISMS.into(),
|
||||||
smtp_utf8: false,
|
timeout: Some(Duration::new(60, 0)),
|
||||||
credentials: None,
|
tls: Tls::None,
|
||||||
connection_reuse: ConnectionReuseParameters::NoReuse,
|
#[cfg(feature = "r2d2")]
|
||||||
#[cfg(feature = "hostname")]
|
pool: None,
|
||||||
hello_name: ClientId::hostname(),
|
|
||||||
#[cfg(not(feature = "hostname"))]
|
|
||||||
hello_name: ClientId::new("localhost".to_string()),
|
|
||||||
authentication_mechanism: None,
|
|
||||||
force_set_auth: false,
|
|
||||||
timeout: Some(Duration::new(60, 0)),
|
|
||||||
}),
|
|
||||||
None => Err(Error::Resolution),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Simple and secure transport, should be used when possible.
|
/// Simple and secure transport, should be used when possible.
|
||||||
/// Creates an encrypted transport over submissions port, using the provided domain
|
/// Creates an encrypted transport over submissions port, using the provided domain
|
||||||
/// to validate TLS certificates.
|
/// to validate TLS certificates.
|
||||||
#[cfg(feature = "native-tls")]
|
pub fn relay(relay: &str) -> Result<Self, Error> {
|
||||||
pub fn new_simple(domain: &str) -> Result<SmtpClient, Error> {
|
#[cfg(feature = "native-tls")]
|
||||||
let mut tls_builder = TlsConnector::builder();
|
let mut tls_builder = TlsConnector::builder();
|
||||||
|
#[cfg(feature = "native-tls")]
|
||||||
tls_builder.min_protocol_version(Some(DEFAULT_TLS_MIN_PROTOCOL));
|
tls_builder.min_protocol_version(Some(DEFAULT_TLS_MIN_PROTOCOL));
|
||||||
|
#[cfg(feature = "native-tls")]
|
||||||
|
let tls_parameters = TlsParameters::new(relay.to_string(), tls_builder.build().unwrap());
|
||||||
|
|
||||||
let tls_parameters =
|
#[cfg(feature = "rustls")]
|
||||||
ClientTlsParameters::new(domain.to_string(), tls_builder.build().unwrap());
|
|
||||||
|
|
||||||
SmtpClient::new(
|
|
||||||
(domain, SUBMISSIONS_PORT),
|
|
||||||
ClientSecurity::Wrapper(tls_parameters),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "rustls")]
|
|
||||||
pub fn new_simple(domain: &str) -> Result<SmtpClient, Error> {
|
|
||||||
let mut tls = ClientConfig::new();
|
let mut tls = ClientConfig::new();
|
||||||
tls.config
|
#[cfg(feature = "rustls")]
|
||||||
.root_store
|
tls.root_store.add_server_trust_anchors(&TLS_SERVER_ROOTS);
|
||||||
.add_server_trust_anchors(&TLS_SERVER_ROOTS);
|
#[cfg(feature = "rustls")]
|
||||||
|
let tls_parameters = TlsParameters::new(relay.to_string(), tls);
|
||||||
|
|
||||||
let tls_parameters = ClientTlsParameters::new(domain.to_string(), tls);
|
let new = Self::new(relay)
|
||||||
|
.port(SUBMISSIONS_PORT)
|
||||||
|
.tls(Tls::Wrapper(tls_parameters));
|
||||||
|
|
||||||
SmtpClient::new(
|
#[cfg(feature = "r2d2")]
|
||||||
(domain, SUBMISSIONS_PORT),
|
// Pool with default configuration
|
||||||
ClientSecurity::Wrapper(tls_parameters),
|
// FIXME avoid clone
|
||||||
)
|
let tpool = new.clone();
|
||||||
|
let new = new.pool(Pool::new(tpool)?);
|
||||||
|
Ok(new)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new local SMTP client to port 25
|
/// Creates a new local SMTP client to port 25
|
||||||
pub fn new_unencrypted_localhost() -> Result<SmtpClient, Error> {
|
///
|
||||||
SmtpClient::new(("localhost", SMTP_PORT), ClientSecurity::None)
|
/// Shortcut for local unencrypted relay (typical local email daemon that will handle relaying)
|
||||||
}
|
pub fn unencrypted_localhost() -> Self {
|
||||||
|
Self::new("localhost").port(SMTP_PORT)
|
||||||
/// Enable SMTPUTF8 if the server supports it
|
|
||||||
pub fn smtp_utf8(mut self, enabled: bool) -> SmtpClient {
|
|
||||||
self.smtp_utf8 = enabled;
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the name used during EHLO
|
/// Set the name used during EHLO
|
||||||
pub fn hello_name(mut self, name: ClientId) -> SmtpClient {
|
pub fn hello_name(mut self, name: ClientId) -> Self {
|
||||||
self.hello_name = name;
|
self.hello_name = name;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enable connection reuse
|
/// Set the authentication mechanism to use
|
||||||
pub fn connection_reuse(mut self, parameters: ConnectionReuseParameters) -> SmtpClient {
|
pub fn credentials(mut self, credentials: Credentials) -> Self {
|
||||||
self.connection_reuse = parameters;
|
self.credentials = Some(credentials);
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the client credentials
|
|
||||||
pub fn credentials<S: Into<Credentials>>(mut self, credentials: S) -> SmtpClient {
|
|
||||||
self.credentials = Some(credentials.into());
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the authentication mechanism to use
|
/// Set the authentication mechanism to use
|
||||||
pub fn authentication_mechanism(mut self, mechanism: Mechanism) -> SmtpClient {
|
pub fn authentication(mut self, mechanisms: Vec<Mechanism>) -> Self {
|
||||||
self.authentication_mechanism = Some(mechanism);
|
self.authentication = mechanisms;
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set if the set authentication mechanism should be force
|
|
||||||
pub fn force_set_auth(mut self, force: bool) -> SmtpClient {
|
|
||||||
self.force_set_auth = force;
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the timeout duration
|
/// Set the timeout duration
|
||||||
pub fn timeout(mut self, timeout: Option<Duration>) -> SmtpClient {
|
pub fn timeout(mut self, timeout: Option<Duration>) -> Self {
|
||||||
self.timeout = timeout;
|
self.timeout = timeout;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the SMTP client
|
/// Set the port to use
|
||||||
///
|
pub fn port(mut self, port: u16) -> Self {
|
||||||
/// It does not connect to the server, but only creates the `SmtpTransport`
|
self.port = port;
|
||||||
pub fn transport(self) -> SmtpTransport {
|
self
|
||||||
SmtpTransport::new(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the state of a client
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct State {
|
|
||||||
/// Panic state
|
|
||||||
pub panic: bool,
|
|
||||||
/// Connection reuse counter
|
|
||||||
pub connection_reuse_count: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Structure that implements the high level SMTP client
|
|
||||||
#[allow(missing_debug_implementations)]
|
|
||||||
pub struct SmtpTransport {
|
|
||||||
/// Information about the server
|
|
||||||
/// Value is None before EHLO
|
|
||||||
server_info: Option<ServerInfo>,
|
|
||||||
/// SmtpTransport variable states
|
|
||||||
state: State,
|
|
||||||
/// Information about the client
|
|
||||||
client_info: SmtpClient,
|
|
||||||
/// Low level client
|
|
||||||
client: InnerClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! try_smtp (
|
|
||||||
($err: expr, $client: ident) => ({
|
|
||||||
match $err {
|
|
||||||
Ok(val) => val,
|
|
||||||
Err(err) => {
|
|
||||||
if !$client.state.panic {
|
|
||||||
$client.state.panic = true;
|
|
||||||
$client.close();
|
|
||||||
}
|
|
||||||
return Err(From::from(err))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
impl<'a> SmtpTransport {
|
|
||||||
/// Creates a new SMTP client
|
|
||||||
///
|
|
||||||
/// It does not connect to the server, but only creates the `SmtpTransport`
|
|
||||||
pub fn new(builder: SmtpClient) -> SmtpTransport {
|
|
||||||
let client = InnerClient::new();
|
|
||||||
|
|
||||||
SmtpTransport {
|
|
||||||
client,
|
|
||||||
server_info: None,
|
|
||||||
client_info: builder,
|
|
||||||
state: State {
|
|
||||||
panic: false,
|
|
||||||
connection_reuse_count: 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn connect(&mut self) -> Result<(), Error> {
|
/// Set the TLS settings to use
|
||||||
// Check if the connection is still available
|
pub fn tls(mut self, tls: Tls) -> Self {
|
||||||
if (self.state.connection_reuse_count > 0) && (!self.client.is_connected()) {
|
self.tls = tls;
|
||||||
self.close();
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.state.connection_reuse_count > 0 {
|
/// Set the TLS settings to use
|
||||||
info!(
|
#[cfg(feature = "r2d2")]
|
||||||
"connection already established to {}",
|
pub fn pool(mut self, pool: Pool<SmtpTransport>) -> Self {
|
||||||
self.client_info.server_addr
|
self.pool = Some(pool);
|
||||||
);
|
self
|
||||||
return Ok(());
|
}
|
||||||
}
|
|
||||||
|
|
||||||
self.client.connect(
|
/// Creates a new connection directly usable to send emails
|
||||||
&self.client_info.server_addr,
|
///
|
||||||
self.client_info.timeout,
|
/// Handles encryption and authentication
|
||||||
match self.client_info.security {
|
fn connection(&self) -> Result<SmtpConnection, Error> {
|
||||||
ClientSecurity::Wrapper(ref tls_parameters) => Some(tls_parameters),
|
let mut conn = SmtpConnection::connect::<(&str, u16)>(
|
||||||
|
(self.server.as_ref(), self.port),
|
||||||
|
self.timeout,
|
||||||
|
&self.hello_name,
|
||||||
|
match self.tls {
|
||||||
|
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters),
|
||||||
_ => None,
|
_ => None,
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
self.client.set_timeout(self.client_info.timeout)?;
|
match self.tls {
|
||||||
let _response = self.client.read_response()?;
|
|
||||||
|
|
||||||
// Log the connection
|
|
||||||
info!("connection established to {}", self.client_info.server_addr);
|
|
||||||
|
|
||||||
self.ehlo()?;
|
|
||||||
|
|
||||||
match (
|
|
||||||
&self.client_info.security.clone(),
|
|
||||||
self.server_info
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.supports_feature(Extension::StartTls),
|
|
||||||
) {
|
|
||||||
(&ClientSecurity::Required(_), false) => {
|
|
||||||
return Err(From::from("Could not encrypt connection, aborting"));
|
|
||||||
}
|
|
||||||
(&ClientSecurity::Opportunistic(_), false) => (),
|
|
||||||
(&ClientSecurity::None, _) => (),
|
|
||||||
(&ClientSecurity::Wrapper(_), _) => (),
|
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||||
(&ClientSecurity::Opportunistic(ref tls_parameters), true)
|
Tls::Opportunistic(ref tls_parameters) => {
|
||||||
| (&ClientSecurity::Required(ref tls_parameters), true) => {
|
if conn.can_starttls() {
|
||||||
try_smtp!(self.client.command(StarttlsCommand), self);
|
conn.starttls(tls_parameters, &self.hello_name)?;
|
||||||
try_smtp!(self.client.upgrade_tls_stream(tls_parameters), self);
|
|
||||||
debug!("connection encrypted");
|
|
||||||
// Send EHLO again
|
|
||||||
self.ehlo()?;
|
|
||||||
}
|
|
||||||
#[cfg(not(any(feature = "native-tls", feature = "rustls")))]
|
|
||||||
(&ClientSecurity::Opportunistic(_), true) | (&ClientSecurity::Required(_), true) => {
|
|
||||||
// This should never happen as `ClientSecurity` can only be created
|
|
||||||
// when a TLS library is enabled
|
|
||||||
unreachable!("TLS support required but not supported");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.client_info.credentials.is_some() {
|
|
||||||
let mut found = false;
|
|
||||||
|
|
||||||
if !self.client_info.force_set_auth {
|
|
||||||
// Compute accepted mechanism
|
|
||||||
let accepted_mechanisms = match self.client_info.authentication_mechanism {
|
|
||||||
Some(mechanism) => vec![mechanism],
|
|
||||||
None => {
|
|
||||||
if self.client.is_encrypted() {
|
|
||||||
DEFAULT_ENCRYPTED_MECHANISMS.to_vec()
|
|
||||||
} else {
|
|
||||||
DEFAULT_UNENCRYPTED_MECHANISMS.to_vec()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for mechanism in accepted_mechanisms {
|
|
||||||
if self
|
|
||||||
.server_info
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.supports_auth_mechanism(mechanism)
|
|
||||||
{
|
|
||||||
found = true;
|
|
||||||
try_smtp!(
|
|
||||||
self.client
|
|
||||||
.auth(mechanism, self.client_info.credentials.as_ref().unwrap(),),
|
|
||||||
self
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
try_smtp!(
|
|
||||||
self.client.auth(
|
|
||||||
self.client_info.authentication_mechanism.expect(
|
|
||||||
"force_set_auth set to true, but no authentication mechanism set"
|
|
||||||
),
|
|
||||||
self.client_info.credentials.as_ref().unwrap(),
|
|
||||||
),
|
|
||||||
self
|
|
||||||
);
|
|
||||||
found = true;
|
|
||||||
}
|
}
|
||||||
|
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||||
if !found {
|
Tls::Required(ref tls_parameters) => {
|
||||||
info!("No supported authentication mechanisms available");
|
conn.starttls(tls_parameters, &self.hello_name)?;
|
||||||
}
|
}
|
||||||
|
_ => (),
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the EHLO response and updates server information
|
match &self.credentials {
|
||||||
fn ehlo(&mut self) -> SmtpResult {
|
Some(credentials) => {
|
||||||
// Extended Hello
|
conn.auth(self.authentication.as_slice(), &credentials)?;
|
||||||
let ehlo_response = try_smtp!(
|
}
|
||||||
self.client.command(EhloCommand::new(ClientId::new(
|
None => (),
|
||||||
self.client_info.hello_name.to_string()
|
}
|
||||||
),)),
|
|
||||||
self
|
|
||||||
);
|
|
||||||
|
|
||||||
self.server_info = Some(try_smtp!(ServerInfo::from_response(&ehlo_response), self));
|
Ok(conn)
|
||||||
|
|
||||||
// Print server information
|
|
||||||
debug!("server {}", self.server_info.as_ref().unwrap());
|
|
||||||
|
|
||||||
Ok(ehlo_response)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reset the client state
|
|
||||||
pub fn close(&mut self) {
|
|
||||||
// Close the SMTP transaction if needed
|
|
||||||
self.client.close();
|
|
||||||
|
|
||||||
// Reset the client state
|
|
||||||
self.server_info = None;
|
|
||||||
self.state.panic = false;
|
|
||||||
self.state.connection_reuse_count = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,101 +248,26 @@ impl<'a> Transport<'a> for SmtpTransport {
|
|||||||
type Result = SmtpResult;
|
type Result = SmtpResult;
|
||||||
|
|
||||||
/// Sends an email
|
/// Sends an email
|
||||||
#[cfg_attr(
|
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Self::Result {
|
||||||
feature = "cargo-clippy",
|
#[cfg(feature = "r2d2")]
|
||||||
allow(clippy::match_same_arms, clippy::cyclomatic_complexity)
|
let mut conn: Box<dyn DerefMut<Target = SmtpConnection>> = match self.pool {
|
||||||
)]
|
Some(ref p) => Box::new(p.get()?),
|
||||||
fn send_raw(&mut self, envelope: &Envelope, email: &[u8]) -> Self::Result {
|
None => Box::new(Box::new(self.connection()?)),
|
||||||
let email_id = Uuid::new_v4();
|
};
|
||||||
let envelope = envelope;
|
#[cfg(not(feature = "r2d2"))]
|
||||||
|
let mut conn = self.connection()?;
|
||||||
|
|
||||||
if !self.client.is_connected() {
|
let result = conn.send(envelope, email)?;
|
||||||
self.connect()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mail
|
#[cfg(feature = "r2d2")]
|
||||||
let mut mail_options = vec![];
|
|
||||||
|
|
||||||
if self
|
|
||||||
.server_info
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.supports_feature(Extension::EightBitMime)
|
|
||||||
{
|
{
|
||||||
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
|
if self.pool.is_none() {
|
||||||
}
|
conn.quit()?;
|
||||||
|
|
||||||
if self
|
|
||||||
.server_info
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.supports_feature(Extension::SmtpUtfEight)
|
|
||||||
&& self.client_info.smtp_utf8
|
|
||||||
{
|
|
||||||
mail_options.push(MailParameter::SmtpUtfEight);
|
|
||||||
}
|
|
||||||
|
|
||||||
try_smtp!(
|
|
||||||
self.client
|
|
||||||
.command(MailCommand::new(envelope.from().cloned(), mail_options,)),
|
|
||||||
self
|
|
||||||
);
|
|
||||||
|
|
||||||
// Log the mail command
|
|
||||||
info!(
|
|
||||||
"{}: from=<{}>",
|
|
||||||
email_id,
|
|
||||||
match envelope.from() {
|
|
||||||
Some(address) => address.to_string(),
|
|
||||||
None => "".to_string(),
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
// Recipient
|
|
||||||
for to_address in envelope.to() {
|
|
||||||
try_smtp!(
|
|
||||||
self.client
|
|
||||||
.command(RcptCommand::new(to_address.clone(), vec![])),
|
|
||||||
self
|
|
||||||
);
|
|
||||||
// Log the rcpt command
|
|
||||||
info!("{}: to=<{}>", email_id, to_address);
|
|
||||||
}
|
}
|
||||||
|
#[cfg(not(feature = "r2d2"))]
|
||||||
|
conn.quit()?;
|
||||||
|
|
||||||
// Data
|
Ok(result)
|
||||||
try_smtp!(self.client.command(DataCommand), self);
|
|
||||||
|
|
||||||
// Message content
|
|
||||||
let result = self.client.message(email);
|
|
||||||
|
|
||||||
if let Ok(ref result) = result {
|
|
||||||
// Increment the connection reuse counter
|
|
||||||
self.state.connection_reuse_count += 1;
|
|
||||||
|
|
||||||
// Log the message
|
|
||||||
info!(
|
|
||||||
"{}: conn_use={}, status=sent ({})",
|
|
||||||
email_id,
|
|
||||||
self.state.connection_reuse_count,
|
|
||||||
result
|
|
||||||
.message
|
|
||||||
.iter()
|
|
||||||
.next()
|
|
||||||
.unwrap_or(&"no response".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test if we can reuse the existing connection
|
|
||||||
match self.client_info.connection_reuse {
|
|
||||||
ConnectionReuseParameters::ReuseLimited(limit)
|
|
||||||
if self.state.connection_reuse_count >= limit =>
|
|
||||||
{
|
|
||||||
self.close()
|
|
||||||
}
|
|
||||||
ConnectionReuseParameters::NoReuse => self.close(),
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
src/transport/smtp/pool.rs
Normal file
22
src/transport/smtp/pool.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
use crate::transport::smtp::{client::SmtpConnection, error::Error, SmtpTransport};
|
||||||
|
use r2d2::ManageConnection;
|
||||||
|
|
||||||
|
impl ManageConnection for SmtpTransport {
|
||||||
|
type Connection = SmtpConnection;
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn connect(&self) -> Result<Self::Connection, Error> {
|
||||||
|
self.connection()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Error> {
|
||||||
|
if conn.test_connected() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(Error::Client("is not connected anymore"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_broken(&self, conn: &mut Self::Connection) -> bool {
|
||||||
|
conn.has_broken()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
use crate::transport::smtp::{error::Error, ConnectionReuseParameters, SmtpClient, SmtpTransport};
|
|
||||||
use r2d2::ManageConnection;
|
|
||||||
|
|
||||||
pub struct SmtpConnectionManager {
|
|
||||||
transport_builder: SmtpClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SmtpConnectionManager {
|
|
||||||
pub fn new(transport_builder: SmtpClient) -> Result<SmtpConnectionManager, Error> {
|
|
||||||
Ok(SmtpConnectionManager {
|
|
||||||
transport_builder: transport_builder
|
|
||||||
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ManageConnection for SmtpConnectionManager {
|
|
||||||
type Connection = SmtpTransport;
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn connect(&self) -> Result<Self::Connection, Error> {
|
|
||||||
let mut transport = SmtpTransport::new(self.transport_builder.clone());
|
|
||||||
transport.connect()?;
|
|
||||||
Ok(transport)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Error> {
|
|
||||||
if conn.client.is_connected() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
Err(Error::Client("is not connected anymore"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_broken(&self, conn: &mut Self::Connection) -> bool {
|
|
||||||
conn.state.panic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -30,7 +30,7 @@ pub type StubResult = Result<(), ()>;
|
|||||||
impl<'a> Transport<'a> for StubTransport {
|
impl<'a> Transport<'a> for StubTransport {
|
||||||
type Result = StubResult;
|
type Result = StubResult;
|
||||||
|
|
||||||
fn send_raw(&mut self, envelope: &Envelope, _email: &[u8]) -> Self::Result {
|
fn send_raw(&self, envelope: &Envelope, _email: &[u8]) -> Self::Result {
|
||||||
info!(
|
info!(
|
||||||
"from=<{}> to=<{:?}>",
|
"from=<{}> to=<{:?}>",
|
||||||
match envelope.from() {
|
match envelope.from() {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#[cfg(all(test, feature = "smtp-transport", feature = "connection-pool"))]
|
#[cfg(all(test, feature = "smtp-transport", feature = "connection-pool"))]
|
||||||
mod test {
|
mod test {
|
||||||
use lettre::{
|
use lettre::{
|
||||||
ClientSecurity, Email, EmailAddress, Envelope, SmtpClient, SmtpConnectionManager, Transport,
|
Email, EmailAddress, Envelope, SmtpConnectionManager, SmtpTransport, Tls, Transport,
|
||||||
};
|
};
|
||||||
use r2d2::Pool;
|
use r2d2::Pool;
|
||||||
use std::{sync::mpsc, thread};
|
use std::{sync::mpsc, thread};
|
||||||
@@ -20,7 +20,7 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn send_one() {
|
fn send_one() {
|
||||||
let client = SmtpClient::new("127.0.0.1:2525", ClientSecurity::None).unwrap();
|
let client = SmtpTransport::new("127.0.0.1:2525", Tls::None).unwrap();
|
||||||
let manager = SmtpConnectionManager::new(client).unwrap();
|
let manager = SmtpConnectionManager::new(client).unwrap();
|
||||||
let pool = Pool::builder().max_size(1).build(manager).unwrap();
|
let pool = Pool::builder().max_size(1).build(manager).unwrap();
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn send_from_thread() {
|
fn send_from_thread() {
|
||||||
let client = SmtpClient::new("127.0.0.1:2525", ClientSecurity::None).unwrap();
|
let client = SmtpTransport::new("127.0.0.1:2525", Tls::None).unwrap();
|
||||||
let manager = SmtpConnectionManager::new(client).unwrap();
|
let manager = SmtpConnectionManager::new(client).unwrap();
|
||||||
let pool = Pool::builder().max_size(2).build(manager).unwrap();
|
let pool = Pool::builder().max_size(2).build(manager).unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn file_transport() {
|
fn file_transport() {
|
||||||
let mut sender = FileTransport::new(temp_dir());
|
let sender = FileTransport::new(temp_dir());
|
||||||
let email = Message::builder()
|
let email = Message::builder()
|
||||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sendmail_transport_simple() {
|
fn sendmail_transport_simple() {
|
||||||
let mut sender = SendmailTransport::new();
|
let sender = SendmailTransport::new();
|
||||||
let email = Message::builder()
|
let email = Message::builder()
|
||||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[cfg(feature = "smtp-transport")]
|
#[cfg(feature = "smtp-transport")]
|
||||||
mod test {
|
mod test {
|
||||||
use lettre::{ClientSecurity, Message, SmtpClient, Transport};
|
use lettre::{Message, SmtpTransport, Transport};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn smtp_transport_simple() {
|
fn smtp_transport_simple() {
|
||||||
@@ -12,9 +12,8 @@ mod test {
|
|||||||
.subject("Happy new year")
|
.subject("Happy new year")
|
||||||
.body("Be happy!")
|
.body("Be happy!")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
SmtpClient::new("127.0.0.1:2525", ClientSecurity::None)
|
SmtpTransport::new("127.0.0.1")
|
||||||
.unwrap()
|
.port(2525)
|
||||||
.transport()
|
|
||||||
.send(&email)
|
.send(&email)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use lettre::{transport::stub::StubTransport, Message, Transport};
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stub_transport() {
|
fn stub_transport() {
|
||||||
let mut sender_ok = StubTransport::new_positive();
|
let sender_ok = StubTransport::new_positive();
|
||||||
let mut sender_ko = StubTransport::new(Err(()));
|
let sender_ko = StubTransport::new(Err(()));
|
||||||
let email = Message::builder()
|
let email = Message::builder()
|
||||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||||
|
|||||||
Reference in New Issue
Block a user