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 }
|
||||
textnonce = { version = "0.7", optional = true }
|
||||
webpki = { version = "0.21", optional = true }
|
||||
webpki-roots = { version = "0.19", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.3"
|
||||
@@ -50,10 +51,9 @@ name = "transport_smtp"
|
||||
|
||||
[features]
|
||||
builder = ["mime", "base64", "hyperx", "textnonce", "quoted_printable"]
|
||||
connection-pool = ["r2d2"]
|
||||
default = ["file-transport", "smtp-transport", "hostname", "sendmail-transport", "native-tls", "builder"]
|
||||
default = ["file-transport", "smtp-transport", "hostname", "sendmail-transport", "rustls-tls", "builder", "r2d2"]
|
||||
file-transport = ["serde", "serde_json"]
|
||||
rustls-tls = ["webpki", "rustls"]
|
||||
rustls-tls = ["webpki", "webpki-roots", "rustls"]
|
||||
sendmail-transport = []
|
||||
smtp-transport = ["bufstream", "base64", "nom"]
|
||||
unstable = []
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use lettre::{
|
||||
transport::smtp::ConnectionReuseParameters, ClientSecurity, Message, SmtpClient, Transport,
|
||||
};
|
||||
use lettre::{Message, SmtpTransport, Transport};
|
||||
|
||||
fn bench_simple_send(c: &mut Criterion) {
|
||||
let mut sender = SmtpClient::new("127.0.0.1:2525", ClientSecurity::None)
|
||||
.unwrap()
|
||||
.transport();
|
||||
let sender = SmtpTransport::new("127.0.0.1").port(2525);
|
||||
|
||||
c.bench_function("send email", move |b| {
|
||||
b.iter(|| {
|
||||
@@ -24,10 +20,7 @@ fn bench_simple_send(c: &mut Criterion) {
|
||||
}
|
||||
|
||||
fn bench_reuse_send(c: &mut Criterion) {
|
||||
let mut sender = SmtpClient::new("127.0.0.1:2525", ClientSecurity::None)
|
||||
.unwrap()
|
||||
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
|
||||
.transport();
|
||||
let sender = SmtpTransport::new("127.0.0.1").port(2525);
|
||||
c.bench_function("send email with connection reuse", move |b| {
|
||||
b.iter(|| {
|
||||
let email = Message::builder()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
extern crate env_logger;
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::{Message, SmtpClient, Transport};
|
||||
use lettre::{Message, SmtpTransport, Transport};
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
@@ -14,7 +14,7 @@ fn main() {
|
||||
.unwrap();
|
||||
|
||||
// Open a local connection on port 25
|
||||
let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport();
|
||||
let mailer = SmtpTransport::unencrypted_localhost();
|
||||
// Send the email
|
||||
let result = mailer.send(&email);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpClient, Transport};
|
||||
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
|
||||
|
||||
fn main() {
|
||||
let email = Message::builder()
|
||||
@@ -17,10 +17,9 @@ fn main() {
|
||||
);
|
||||
|
||||
// Open a remote connection to gmail
|
||||
let mut mailer = SmtpClient::new_simple("smtp.gmail.com")
|
||||
let mailer = SmtpTransport::relay("smtp.gmail.com")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.transport();
|
||||
.credentials(creds);
|
||||
|
||||
// Send the 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 domain: &str = domain.ok_or_else(|| DeError::missing_field("domain"))?;
|
||||
// FIXME avoid unwrap here
|
||||
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")]
|
||||
pub use crate::transport::sendmail::SendmailTransport;
|
||||
#[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"))]
|
||||
pub use crate::transport::smtp::r2d2::SmtpConnectionManager;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub use crate::transport::smtp::{ClientSecurity, SmtpClient, SmtpTransport};
|
||||
pub use crate::transport::smtp::{SmtpTransport, Tls};
|
||||
#[cfg(feature = "builder")]
|
||||
use std::convert::TryFrom;
|
||||
|
||||
@@ -115,14 +115,12 @@ pub trait Transport<'a> {
|
||||
type Result;
|
||||
|
||||
/// Sends the email
|
||||
/// FIXME not mut
|
||||
|
||||
fn send(&mut self, message: &Message) -> Self::Result {
|
||||
fn send(&self, message: &Message) -> Self::Result {
|
||||
let raw = message.formatted();
|
||||
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)]
|
||||
|
||||
@@ -41,7 +41,7 @@ struct SerializableEmail<'a> {
|
||||
impl<'a> Transport<'a> for FileTransport {
|
||||
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 mut file = self.path.clone();
|
||||
|
||||
@@ -39,7 +39,7 @@ impl SendmailTransport {
|
||||
impl<'a> Transport<'a> for SendmailTransport {
|
||||
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();
|
||||
|
||||
// Spawn the sendmail command
|
||||
|
||||
@@ -3,13 +3,9 @@
|
||||
use crate::transport::smtp::error::Error;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
/// Accepted authentication mechanisms on an encrypted connection
|
||||
/// Accepted authentication mechanisms
|
||||
/// Trying LOGIN last as it is deprecated.
|
||||
pub const DEFAULT_ENCRYPTED_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
|
||||
|
||||
/// Accepted authentication mechanisms on an unencrypted connection
|
||||
// FIXME remove
|
||||
pub const DEFAULT_UNENCRYPTED_MECHANISMS: &[Mechanism] = &[];
|
||||
pub const DEFAULT_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
|
||||
|
||||
/// Convertible to user credentials
|
||||
pub trait IntoCredentials {
|
||||
|
||||
@@ -2,18 +2,20 @@
|
||||
|
||||
use crate::transport::smtp::{
|
||||
authentication::{Credentials, Mechanism},
|
||||
client::net::{ClientTlsParameters, Connector, NetworkStream, Timeout},
|
||||
client::net::{NetworkStream, TlsParameters},
|
||||
commands::*,
|
||||
error::{Error, SmtpResult},
|
||||
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
|
||||
response::Response,
|
||||
};
|
||||
use crate::Envelope;
|
||||
use bufstream::BufStream;
|
||||
use log::debug;
|
||||
#[cfg(feature = "serde")]
|
||||
use std::fmt::Debug;
|
||||
use std::{
|
||||
fmt::Display,
|
||||
io::{self, BufRead, Read, Write},
|
||||
io::{self, BufRead, Write},
|
||||
net::ToSocketAddrs,
|
||||
string::String,
|
||||
time::Duration,
|
||||
@@ -77,12 +79,27 @@ fn escape_crlf(string: &str) -> String {
|
||||
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
|
||||
#[derive(Default)]
|
||||
pub struct InnerClient<S: Write + Read = NetworkStream> {
|
||||
pub struct SmtpConnection {
|
||||
/// TCP stream between client and server
|
||||
/// 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 (
|
||||
@@ -91,102 +108,176 @@ macro_rules! return_err (
|
||||
})
|
||||
);
|
||||
|
||||
impl<S: Write + Read> InnerClient<S> {
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// 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(())
|
||||
impl SmtpConnection {
|
||||
pub fn server_info(&self) -> &ServerInfo {
|
||||
&self.server_info
|
||||
}
|
||||
|
||||
/// Connects to the configured server
|
||||
///
|
||||
/// Sends EHLO and parses server information
|
||||
pub fn connect<A: ToSocketAddrs>(
|
||||
&mut self,
|
||||
addr: &A,
|
||||
server: A,
|
||||
timeout: Option<Duration>,
|
||||
tls_parameters: Option<&ClientTlsParameters>,
|
||||
) -> Result<(), Error> {
|
||||
// Connect should not be called when the client is already connected
|
||||
if self.stream.is_some() {
|
||||
return_err!("The connection is already established", self);
|
||||
}
|
||||
|
||||
let mut addresses = addr.to_socket_addrs()?;
|
||||
hello_name: &ClientId,
|
||||
tls_parameters: Option<&TlsParameters>,
|
||||
) -> Result<SmtpConnection, Error> {
|
||||
let mut addresses = server.to_socket_addrs()?;
|
||||
|
||||
// FIXME try all
|
||||
let server_addr = match addresses.next() {
|
||||
Some(addr) => addr,
|
||||
None => return_err!("Could not resolve hostname", self),
|
||||
};
|
||||
|
||||
debug!("connecting to {}", server_addr);
|
||||
|
||||
// Try to connect
|
||||
self.set_stream(Connector::connect(&server_addr, timeout, tls_parameters)?);
|
||||
let stream = BufStream::new(NetworkStream::connect(
|
||||
&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(())
|
||||
}
|
||||
|
||||
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
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::wrong_self_convention))]
|
||||
pub fn is_connected(&mut self) -> bool {
|
||||
self.stream.is_some() && self.command(NoopCommand).is_ok()
|
||||
pub fn test_connected(&mut self) -> bool {
|
||||
self.command(Noop).is_ok()
|
||||
}
|
||||
|
||||
/// 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
|
||||
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) {
|
||||
challenges -= 1;
|
||||
response = self.command(AuthCommand::new_from_response(
|
||||
mechanism,
|
||||
credentials.clone(),
|
||||
&response,
|
||||
)?)?;
|
||||
response = try_smtp!(
|
||||
self.command(Auth::new_from_response(
|
||||
mechanism,
|
||||
credentials.clone(),
|
||||
&response,
|
||||
)?),
|
||||
self
|
||||
);
|
||||
}
|
||||
|
||||
if challenges == 0 {
|
||||
@@ -214,12 +305,8 @@ impl<S: Connector + Write + Read + Timeout> InnerClient<S> {
|
||||
|
||||
/// Writes a string to the server
|
||||
fn write(&mut self, string: &[u8]) -> Result<(), Error> {
|
||||
if self.stream.is_none() {
|
||||
return Err(From::from("Connection closed"));
|
||||
}
|
||||
|
||||
self.stream.as_mut().unwrap().write_all(string)?;
|
||||
self.stream.as_mut().unwrap().flush()?;
|
||||
self.stream.write_all(string)?;
|
||||
self.stream.flush()?;
|
||||
|
||||
debug!(
|
||||
"Wrote: {}",
|
||||
@@ -240,7 +327,7 @@ impl<S: Connector + Write + Read + Timeout> InnerClient<S> {
|
||||
break;
|
||||
}
|
||||
// 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
|
||||
if read_count == 0 {
|
||||
|
||||
@@ -18,7 +18,7 @@ use std::{
|
||||
/// Parameters to use for secure clients
|
||||
#[derive(Clone)]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct ClientTlsParameters {
|
||||
pub struct TlsParameters {
|
||||
/// A connector from `native-tls`
|
||||
#[cfg(feature = "native-tls")]
|
||||
connector: TlsConnector,
|
||||
@@ -30,17 +30,17 @@ pub struct ClientTlsParameters {
|
||||
domain: String,
|
||||
}
|
||||
|
||||
impl ClientTlsParameters {
|
||||
/// Creates a `ClientTlsParameters`
|
||||
impl TlsParameters {
|
||||
/// Creates a `TlsParameters`
|
||||
#[cfg(feature = "native-tls")]
|
||||
pub fn new(domain: String, connector: TlsConnector) -> Self {
|
||||
ClientTlsParameters { connector, domain }
|
||||
Self { connector, domain }
|
||||
}
|
||||
|
||||
/// Creates a `ClientTlsParameters`
|
||||
/// Creates a `TlsParameters`
|
||||
#[cfg(feature = "rustls")]
|
||||
pub fn new(domain: String, connector: ClientConfig) -> Self {
|
||||
ClientTlsParameters {
|
||||
Self {
|
||||
connector: Box::new(connector),
|
||||
domain,
|
||||
}
|
||||
@@ -87,6 +87,85 @@ impl NetworkStream {
|
||||
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 {
|
||||
@@ -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
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct EhloCommand {
|
||||
pub struct Ehlo {
|
||||
client_id: ClientId,
|
||||
}
|
||||
|
||||
impl Display for EhloCommand {
|
||||
impl Display for Ehlo {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
#[allow(clippy::write_with_newline)]
|
||||
write!(f, "EHLO {}\r\n", self.client_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl EhloCommand {
|
||||
impl Ehlo {
|
||||
/// Creates a EHLO command
|
||||
pub fn new(client_id: ClientId) -> EhloCommand {
|
||||
EhloCommand { client_id }
|
||||
pub fn new(client_id: ClientId) -> Ehlo {
|
||||
Ehlo { client_id }
|
||||
}
|
||||
}
|
||||
|
||||
/// STARTTLS command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[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 {
|
||||
f.write_str("STARTTLS\r\n")
|
||||
}
|
||||
@@ -50,12 +49,12 @@ impl Display for StarttlsCommand {
|
||||
/// MAIL command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct MailCommand {
|
||||
pub struct Mail {
|
||||
sender: Option<Address>,
|
||||
parameters: Vec<MailParameter>,
|
||||
}
|
||||
|
||||
impl Display for MailCommand {
|
||||
impl Display for Mail {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
@@ -69,22 +68,22 @@ impl Display for MailCommand {
|
||||
}
|
||||
}
|
||||
|
||||
impl MailCommand {
|
||||
impl Mail {
|
||||
/// Creates a MAIL command
|
||||
pub fn new(sender: Option<Address>, parameters: Vec<MailParameter>) -> MailCommand {
|
||||
MailCommand { sender, parameters }
|
||||
pub fn new(sender: Option<Address>, parameters: Vec<MailParameter>) -> Mail {
|
||||
Mail { sender, parameters }
|
||||
}
|
||||
}
|
||||
|
||||
/// RCPT command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct RcptCommand {
|
||||
pub struct Rcpt {
|
||||
recipient: Address,
|
||||
parameters: Vec<RcptParameter>,
|
||||
}
|
||||
|
||||
impl Display for RcptCommand {
|
||||
impl Display for Rcpt {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "RCPT TO:<{}>", self.recipient)?;
|
||||
for parameter in &self.parameters {
|
||||
@@ -94,10 +93,10 @@ impl Display for RcptCommand {
|
||||
}
|
||||
}
|
||||
|
||||
impl RcptCommand {
|
||||
impl Rcpt {
|
||||
/// Creates an RCPT command
|
||||
pub fn new(recipient: Address, parameters: Vec<RcptParameter>) -> RcptCommand {
|
||||
RcptCommand {
|
||||
pub fn new(recipient: Address, parameters: Vec<RcptParameter>) -> Rcpt {
|
||||
Rcpt {
|
||||
recipient,
|
||||
parameters,
|
||||
}
|
||||
@@ -107,9 +106,9 @@ impl RcptCommand {
|
||||
/// DATA command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[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 {
|
||||
f.write_str("DATA\r\n")
|
||||
}
|
||||
@@ -118,9 +117,9 @@ impl Display for DataCommand {
|
||||
/// QUIT command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[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 {
|
||||
f.write_str("QUIT\r\n")
|
||||
}
|
||||
@@ -129,9 +128,9 @@ impl Display for QuitCommand {
|
||||
/// NOOP command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[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 {
|
||||
f.write_str("NOOP\r\n")
|
||||
}
|
||||
@@ -140,11 +139,11 @@ impl Display for NoopCommand {
|
||||
/// HELP command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct HelpCommand {
|
||||
pub struct Help {
|
||||
argument: Option<String>,
|
||||
}
|
||||
|
||||
impl Display for HelpCommand {
|
||||
impl Display for Help {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("HELP")?;
|
||||
if self.argument.is_some() {
|
||||
@@ -154,61 +153,61 @@ impl Display for HelpCommand {
|
||||
}
|
||||
}
|
||||
|
||||
impl HelpCommand {
|
||||
impl Help {
|
||||
/// Creates an HELP command
|
||||
pub fn new(argument: Option<String>) -> HelpCommand {
|
||||
HelpCommand { argument }
|
||||
pub fn new(argument: Option<String>) -> Help {
|
||||
Help { argument }
|
||||
}
|
||||
}
|
||||
|
||||
/// VRFY command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct VrfyCommand {
|
||||
pub struct Vrfy {
|
||||
argument: String,
|
||||
}
|
||||
|
||||
impl Display for VrfyCommand {
|
||||
impl Display for Vrfy {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
#[allow(clippy::write_with_newline)]
|
||||
write!(f, "VRFY {}\r\n", self.argument)
|
||||
}
|
||||
}
|
||||
|
||||
impl VrfyCommand {
|
||||
impl Vrfy {
|
||||
/// Creates a VRFY command
|
||||
pub fn new(argument: String) -> VrfyCommand {
|
||||
VrfyCommand { argument }
|
||||
pub fn new(argument: String) -> Vrfy {
|
||||
Vrfy { argument }
|
||||
}
|
||||
}
|
||||
|
||||
/// EXPN command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct ExpnCommand {
|
||||
pub struct Expn {
|
||||
argument: String,
|
||||
}
|
||||
|
||||
impl Display for ExpnCommand {
|
||||
impl Display for Expn {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
#[allow(clippy::write_with_newline)]
|
||||
write!(f, "EXPN {}\r\n", self.argument)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExpnCommand {
|
||||
impl Expn {
|
||||
/// Creates an EXPN command
|
||||
pub fn new(argument: String) -> ExpnCommand {
|
||||
ExpnCommand { argument }
|
||||
pub fn new(argument: String) -> Expn {
|
||||
Expn { argument }
|
||||
}
|
||||
}
|
||||
|
||||
/// RSET command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[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 {
|
||||
f.write_str("RSET\r\n")
|
||||
}
|
||||
@@ -217,14 +216,14 @@ impl Display for RsetCommand {
|
||||
/// AUTH command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct AuthCommand {
|
||||
pub struct Auth {
|
||||
mechanism: Mechanism,
|
||||
credentials: Credentials,
|
||||
challenge: Option<String>,
|
||||
response: Option<String>,
|
||||
}
|
||||
|
||||
impl Display for AuthCommand {
|
||||
impl Display for Auth {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
let encoded_response = self
|
||||
.response
|
||||
@@ -243,19 +242,19 @@ impl Display for AuthCommand {
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthCommand {
|
||||
impl Auth {
|
||||
/// Creates an AUTH command (from a challenge if provided)
|
||||
pub fn new(
|
||||
mechanism: Mechanism,
|
||||
credentials: Credentials,
|
||||
challenge: Option<String>,
|
||||
) -> Result<AuthCommand, Error> {
|
||||
) -> Result<Auth, Error> {
|
||||
let response = if mechanism.supports_initial_response() || challenge.is_some() {
|
||||
Some(mechanism.response(&credentials, challenge.as_deref())?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(AuthCommand {
|
||||
Ok(Auth {
|
||||
mechanism,
|
||||
credentials,
|
||||
challenge,
|
||||
@@ -269,7 +268,7 @@ impl AuthCommand {
|
||||
mechanism: Mechanism,
|
||||
credentials: Credentials,
|
||||
response: &Response,
|
||||
) -> Result<AuthCommand, Error> {
|
||||
) -> Result<Auth, Error> {
|
||||
if !response.has_code(334) {
|
||||
return Err(Error::ResponseParsing("Expecting a challenge"));
|
||||
}
|
||||
@@ -284,7 +283,7 @@ impl AuthCommand {
|
||||
|
||||
let response = Some(mechanism.response(&credentials, Some(decoded_challenge.as_ref()))?);
|
||||
|
||||
Ok(AuthCommand {
|
||||
Ok(Auth {
|
||||
mechanism,
|
||||
credentials,
|
||||
challenge: Some(decoded_challenge),
|
||||
@@ -311,26 +310,23 @@ mod test {
|
||||
keyword: "TEST".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!(
|
||||
format!("{}", MailCommand::new(Some(email.clone()), vec![])),
|
||||
format!("{}", Mail::new(Some(email.clone()), vec![])),
|
||||
"MAIL FROM:<test@example.com>\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", MailCommand::new(None, vec![])),
|
||||
"MAIL FROM:<>\r\n"
|
||||
);
|
||||
assert_eq!(format!("{}", Mail::new(None, vec![])), "MAIL FROM:<>\r\n");
|
||||
assert_eq!(
|
||||
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"
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
MailCommand::new(
|
||||
Mail::new(
|
||||
Some(email.clone()),
|
||||
vec![
|
||||
MailParameter::Size(42),
|
||||
@@ -342,42 +338,42 @@ mod test {
|
||||
"MAIL FROM:<test@example.com> SIZE=42 BODY=8BITMIME TEST=value\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", RcptCommand::new(email.clone(), vec![])),
|
||||
format!("{}", Rcpt::new(email.clone(), vec![])),
|
||||
"RCPT TO:<test@example.com>\r\n"
|
||||
);
|
||||
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"
|
||||
);
|
||||
assert_eq!(format!("{}", QuitCommand), "QUIT\r\n");
|
||||
assert_eq!(format!("{}", DataCommand), "DATA\r\n");
|
||||
assert_eq!(format!("{}", NoopCommand), "NOOP\r\n");
|
||||
assert_eq!(format!("{}", HelpCommand::new(None)), "HELP\r\n");
|
||||
assert_eq!(format!("{}", Quit), "QUIT\r\n");
|
||||
assert_eq!(format!("{}", Data), "DATA\r\n");
|
||||
assert_eq!(format!("{}", Noop), "NOOP\r\n");
|
||||
assert_eq!(format!("{}", Help::new(None)), "HELP\r\n");
|
||||
assert_eq!(
|
||||
format!("{}", HelpCommand::new(Some("test".to_string()))),
|
||||
format!("{}", Help::new(Some("test".to_string()))),
|
||||
"HELP test\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", VrfyCommand::new("test".to_string())),
|
||||
format!("{}", Vrfy::new("test".to_string())),
|
||||
"VRFY test\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", ExpnCommand::new("test".to_string())),
|
||||
format!("{}", Expn::new("test".to_string())),
|
||||
"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());
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
AuthCommand::new(Mechanism::Plain, credentials.clone(), None).unwrap()
|
||||
Auth::new(Mechanism::Plain, credentials.clone(), None).unwrap()
|
||||
),
|
||||
"AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
AuthCommand::new(Mechanism::Login, credentials.clone(), None).unwrap()
|
||||
Auth::new(Mechanism::Login, credentials.clone(), None).unwrap()
|
||||
),
|
||||
"AUTH LOGIN\r\n"
|
||||
);
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
use self::Error::*;
|
||||
use crate::transport::smtp::response::{Response, Severity};
|
||||
use base64::DecodeError;
|
||||
#[cfg(feature = "native-tls")]
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt,
|
||||
fmt::{Display, Formatter},
|
||||
fmt::{self, Display, Formatter},
|
||||
io,
|
||||
string::FromUtf8Error,
|
||||
};
|
||||
@@ -43,10 +41,11 @@ pub enum Error {
|
||||
/// Invalid hostname
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InvalidDNSName(webpki::InvalidDNSNameError),
|
||||
#[cfg(feature = "r2d2")]
|
||||
Pool(r2d2::Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))]
|
||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
match *self {
|
||||
// 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()),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
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 {
|
||||
fn from(response: Response) -> Error {
|
||||
match response.code.severity {
|
||||
|
||||
@@ -52,6 +52,10 @@ impl ClientId {
|
||||
.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
|
||||
@@ -86,7 +90,7 @@ impl Display for Extension {
|
||||
}
|
||||
|
||||
/// 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))]
|
||||
pub struct ServerInfo {
|
||||
/// Server name
|
||||
@@ -176,6 +180,16 @@ impl ServerInfo {
|
||||
self.features
|
||||
.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
|
||||
|
||||
@@ -10,32 +10,25 @@
|
||||
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
|
||||
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and XOAUTH2 mechanisms
|
||||
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
|
||||
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
|
||||
//!
|
||||
|
||||
use crate::Envelope;
|
||||
use crate::{
|
||||
transport::smtp::{
|
||||
authentication::{
|
||||
Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS, DEFAULT_UNENCRYPTED_MECHANISMS,
|
||||
},
|
||||
client::{net::ClientTlsParameters, InnerClient},
|
||||
commands::*,
|
||||
authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS},
|
||||
client::{net::TlsParameters, SmtpConnection},
|
||||
error::{Error, SmtpResult},
|
||||
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
|
||||
extension::ClientId,
|
||||
},
|
||||
Transport,
|
||||
Envelope, Transport,
|
||||
};
|
||||
use log::{debug, info};
|
||||
#[cfg(feature = "native-tls")]
|
||||
use native_tls::{Protocol, TlsConnector};
|
||||
#[cfg(feature = "r2d2")]
|
||||
use r2d2::Pool;
|
||||
#[cfg(feature = "rustls")]
|
||||
use rustls::ClientConfig;
|
||||
use std::{
|
||||
net::{SocketAddr, ToSocketAddrs},
|
||||
time::Duration,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
use std::ops::DerefMut;
|
||||
use std::time::Duration;
|
||||
#[cfg(feature = "rustls")]
|
||||
use webpki_roots::TLS_SERVER_ROOTS;
|
||||
|
||||
@@ -44,8 +37,8 @@ pub mod client;
|
||||
pub mod commands;
|
||||
pub mod error;
|
||||
pub mod extension;
|
||||
#[cfg(feature = "connection-pool")]
|
||||
pub mod r2d2;
|
||||
#[cfg(feature = "r2d2")]
|
||||
pub mod pool;
|
||||
pub mod response;
|
||||
pub mod util;
|
||||
|
||||
@@ -62,378 +55,192 @@ pub const SUBMISSIONS_PORT: u16 = 465;
|
||||
|
||||
/// Accepted protocols by default.
|
||||
/// 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")]
|
||||
const DEFAULT_TLS_MIN_PROTOCOL: Protocol = Protocol::Tlsv12;
|
||||
|
||||
/// How to apply TLS to a client connection
|
||||
#[derive(Clone)]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub enum ClientSecurity {
|
||||
pub enum Tls {
|
||||
/// Insecure connection only (for testing purposes)
|
||||
None,
|
||||
/// 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`
|
||||
Required(ClientTlsParameters),
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
Required(TlsParameters),
|
||||
/// Use TLS wrapped connection
|
||||
Wrapper(ClientTlsParameters),
|
||||
}
|
||||
|
||||
/// 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,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
Wrapper(TlsParameters),
|
||||
}
|
||||
|
||||
/// Contains client configuration
|
||||
#[allow(missing_debug_implementations)]
|
||||
#[derive(Clone)]
|
||||
pub struct SmtpClient {
|
||||
/// Enable connection reuse
|
||||
connection_reuse: ConnectionReuseParameters,
|
||||
pub struct SmtpTransport {
|
||||
/// Name sent during EHLO
|
||||
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: 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
|
||||
/// It can be changed later for specific needs (like a different timeout for each SMTP command)
|
||||
timeout: Option<Duration>,
|
||||
/// Connection pool
|
||||
#[cfg(feature = "r2d2")]
|
||||
pool: Option<Pool<SmtpTransport>>,
|
||||
}
|
||||
|
||||
/// Builder for the SMTP `SmtpTransport`
|
||||
impl SmtpClient {
|
||||
impl SmtpTransport {
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// Defaults are:
|
||||
///
|
||||
/// * No connection reuse
|
||||
/// * No authentication
|
||||
/// * No SMTPUTF8 support
|
||||
/// * A 60 seconds timeout for smtp commands
|
||||
/// * Port 587
|
||||
///
|
||||
/// Consider using [`SmtpClient::new_simple`] instead, if possible.
|
||||
pub fn new<A: ToSocketAddrs>(addr: A, security: ClientSecurity) -> Result<SmtpClient, Error> {
|
||||
let mut addresses = addr.to_socket_addrs()?;
|
||||
|
||||
match addresses.next() {
|
||||
Some(addr) => Ok(SmtpClient {
|
||||
server_addr: addr,
|
||||
security,
|
||||
smtp_utf8: false,
|
||||
credentials: None,
|
||||
connection_reuse: ConnectionReuseParameters::NoReuse,
|
||||
#[cfg(feature = "hostname")]
|
||||
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),
|
||||
/// Consider using [`SmtpTransport::new`] instead, if possible.
|
||||
pub fn new<T: Into<String>>(server: T) -> Self {
|
||||
Self {
|
||||
server: server.into(),
|
||||
port: SUBMISSION_PORT,
|
||||
hello_name: ClientId::hostname(),
|
||||
credentials: None,
|
||||
authentication: DEFAULT_MECHANISMS.into(),
|
||||
timeout: Some(Duration::new(60, 0)),
|
||||
tls: Tls::None,
|
||||
#[cfg(feature = "r2d2")]
|
||||
pool: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple and secure transport, should be used when possible.
|
||||
/// Creates an encrypted transport over submissions port, using the provided domain
|
||||
/// to validate TLS certificates.
|
||||
#[cfg(feature = "native-tls")]
|
||||
pub fn new_simple(domain: &str) -> Result<SmtpClient, Error> {
|
||||
pub fn relay(relay: &str) -> Result<Self, Error> {
|
||||
#[cfg(feature = "native-tls")]
|
||||
let mut tls_builder = TlsConnector::builder();
|
||||
#[cfg(feature = "native-tls")]
|
||||
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 =
|
||||
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> {
|
||||
#[cfg(feature = "rustls")]
|
||||
let mut tls = ClientConfig::new();
|
||||
tls.config
|
||||
.root_store
|
||||
.add_server_trust_anchors(&TLS_SERVER_ROOTS);
|
||||
#[cfg(feature = "rustls")]
|
||||
tls.root_store.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(
|
||||
(domain, SUBMISSIONS_PORT),
|
||||
ClientSecurity::Wrapper(tls_parameters),
|
||||
)
|
||||
#[cfg(feature = "r2d2")]
|
||||
// Pool with default configuration
|
||||
// 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
|
||||
pub fn new_unencrypted_localhost() -> Result<SmtpClient, Error> {
|
||||
SmtpClient::new(("localhost", SMTP_PORT), ClientSecurity::None)
|
||||
}
|
||||
|
||||
/// Enable SMTPUTF8 if the server supports it
|
||||
pub fn smtp_utf8(mut self, enabled: bool) -> SmtpClient {
|
||||
self.smtp_utf8 = enabled;
|
||||
self
|
||||
///
|
||||
/// Shortcut for local unencrypted relay (typical local email daemon that will handle relaying)
|
||||
pub fn unencrypted_localhost() -> Self {
|
||||
Self::new("localhost").port(SMTP_PORT)
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Enable connection reuse
|
||||
pub fn connection_reuse(mut self, parameters: ConnectionReuseParameters) -> SmtpClient {
|
||||
self.connection_reuse = parameters;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the client credentials
|
||||
pub fn credentials<S: Into<Credentials>>(mut self, credentials: S) -> SmtpClient {
|
||||
self.credentials = Some(credentials.into());
|
||||
/// Set the authentication mechanism to use
|
||||
pub fn credentials(mut self, credentials: Credentials) -> Self {
|
||||
self.credentials = Some(credentials);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication mechanism to use
|
||||
pub fn authentication_mechanism(mut self, mechanism: Mechanism) -> SmtpClient {
|
||||
self.authentication_mechanism = Some(mechanism);
|
||||
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;
|
||||
pub fn authentication(mut self, mechanisms: Vec<Mechanism>) -> Self {
|
||||
self.authentication = mechanisms;
|
||||
self
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Build the SMTP client
|
||||
///
|
||||
/// It does not connect to the server, but only creates the `SmtpTransport`
|
||||
pub fn transport(self) -> SmtpTransport {
|
||||
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,
|
||||
},
|
||||
}
|
||||
/// Set the port to use
|
||||
pub fn port(mut self, port: u16) -> Self {
|
||||
self.port = port;
|
||||
self
|
||||
}
|
||||
|
||||
fn connect(&mut self) -> Result<(), Error> {
|
||||
// Check if the connection is still available
|
||||
if (self.state.connection_reuse_count > 0) && (!self.client.is_connected()) {
|
||||
self.close();
|
||||
}
|
||||
/// Set the TLS settings to use
|
||||
pub fn tls(mut self, tls: Tls) -> Self {
|
||||
self.tls = tls;
|
||||
self
|
||||
}
|
||||
|
||||
if self.state.connection_reuse_count > 0 {
|
||||
info!(
|
||||
"connection already established to {}",
|
||||
self.client_info.server_addr
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
/// Set the TLS settings to use
|
||||
#[cfg(feature = "r2d2")]
|
||||
pub fn pool(mut self, pool: Pool<SmtpTransport>) -> Self {
|
||||
self.pool = Some(pool);
|
||||
self
|
||||
}
|
||||
|
||||
self.client.connect(
|
||||
&self.client_info.server_addr,
|
||||
self.client_info.timeout,
|
||||
match self.client_info.security {
|
||||
ClientSecurity::Wrapper(ref tls_parameters) => Some(tls_parameters),
|
||||
/// Creates a new connection directly usable to send emails
|
||||
///
|
||||
/// Handles encryption and authentication
|
||||
fn connection(&self) -> Result<SmtpConnection, Error> {
|
||||
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,
|
||||
},
|
||||
)?;
|
||||
|
||||
self.client.set_timeout(self.client_info.timeout)?;
|
||||
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(_), _) => (),
|
||||
match self.tls {
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
(&ClientSecurity::Opportunistic(ref tls_parameters), true)
|
||||
| (&ClientSecurity::Required(ref tls_parameters), true) => {
|
||||
try_smtp!(self.client.command(StarttlsCommand), self);
|
||||
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;
|
||||
}
|
||||
Tls::Opportunistic(ref tls_parameters) => {
|
||||
if conn.can_starttls() {
|
||||
conn.starttls(tls_parameters, &self.hello_name)?;
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
|
||||
if !found {
|
||||
info!("No supported authentication mechanisms available");
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
Tls::Required(ref tls_parameters) => {
|
||||
conn.starttls(tls_parameters, &self.hello_name)?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the EHLO response and updates server information
|
||||
fn ehlo(&mut self) -> SmtpResult {
|
||||
// Extended Hello
|
||||
let ehlo_response = try_smtp!(
|
||||
self.client.command(EhloCommand::new(ClientId::new(
|
||||
self.client_info.hello_name.to_string()
|
||||
),)),
|
||||
self
|
||||
);
|
||||
match &self.credentials {
|
||||
Some(credentials) => {
|
||||
conn.auth(self.authentication.as_slice(), &credentials)?;
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
|
||||
self.server_info = Some(try_smtp!(ServerInfo::from_response(&ehlo_response), self));
|
||||
|
||||
// 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;
|
||||
Ok(conn)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,101 +248,26 @@ impl<'a> Transport<'a> for SmtpTransport {
|
||||
type Result = SmtpResult;
|
||||
|
||||
/// Sends an email
|
||||
#[cfg_attr(
|
||||
feature = "cargo-clippy",
|
||||
allow(clippy::match_same_arms, clippy::cyclomatic_complexity)
|
||||
)]
|
||||
fn send_raw(&mut self, envelope: &Envelope, email: &[u8]) -> Self::Result {
|
||||
let email_id = Uuid::new_v4();
|
||||
let envelope = envelope;
|
||||
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Self::Result {
|
||||
#[cfg(feature = "r2d2")]
|
||||
let mut conn: Box<dyn DerefMut<Target = SmtpConnection>> = match self.pool {
|
||||
Some(ref p) => Box::new(p.get()?),
|
||||
None => Box::new(Box::new(self.connection()?)),
|
||||
};
|
||||
#[cfg(not(feature = "r2d2"))]
|
||||
let mut conn = self.connection()?;
|
||||
|
||||
if !self.client.is_connected() {
|
||||
self.connect()?;
|
||||
}
|
||||
let result = conn.send(envelope, email)?;
|
||||
|
||||
// Mail
|
||||
let mut mail_options = vec![];
|
||||
|
||||
if self
|
||||
.server_info
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.supports_feature(Extension::EightBitMime)
|
||||
#[cfg(feature = "r2d2")]
|
||||
{
|
||||
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
|
||||
}
|
||||
|
||||
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(),
|
||||
if self.pool.is_none() {
|
||||
conn.quit()?;
|
||||
}
|
||||
);
|
||||
|
||||
// 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
|
||||
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
|
||||
Ok(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 {
|
||||
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!(
|
||||
"from=<{}> to=<{:?}>",
|
||||
match envelope.from() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#[cfg(all(test, feature = "smtp-transport", feature = "connection-pool"))]
|
||||
mod test {
|
||||
use lettre::{
|
||||
ClientSecurity, Email, EmailAddress, Envelope, SmtpClient, SmtpConnectionManager, Transport,
|
||||
Email, EmailAddress, Envelope, SmtpConnectionManager, SmtpTransport, Tls, Transport,
|
||||
};
|
||||
use r2d2::Pool;
|
||||
use std::{sync::mpsc, thread};
|
||||
@@ -20,7 +20,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
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 pool = Pool::builder().max_size(1).build(manager).unwrap();
|
||||
|
||||
@@ -31,7 +31,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
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 pool = Pool::builder().max_size(2).build(manager).unwrap();
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn file_transport() {
|
||||
let mut sender = FileTransport::new(temp_dir());
|
||||
let sender = FileTransport::new(temp_dir());
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
|
||||
@@ -5,7 +5,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn sendmail_transport_simple() {
|
||||
let mut sender = SendmailTransport::new();
|
||||
let sender = SendmailTransport::new();
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
mod test {
|
||||
use lettre::{ClientSecurity, Message, SmtpClient, Transport};
|
||||
use lettre::{Message, SmtpTransport, Transport};
|
||||
|
||||
#[test]
|
||||
fn smtp_transport_simple() {
|
||||
@@ -12,9 +12,8 @@ mod test {
|
||||
.subject("Happy new year")
|
||||
.body("Be happy!")
|
||||
.unwrap();
|
||||
SmtpClient::new("127.0.0.1:2525", ClientSecurity::None)
|
||||
.unwrap()
|
||||
.transport()
|
||||
SmtpTransport::new("127.0.0.1")
|
||||
.port(2525)
|
||||
.send(&email)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ use lettre::{transport::stub::StubTransport, Message, Transport};
|
||||
|
||||
#[test]
|
||||
fn stub_transport() {
|
||||
let mut sender_ok = StubTransport::new_positive();
|
||||
let mut sender_ko = StubTransport::new(Err(()));
|
||||
let sender_ok = StubTransport::new_positive();
|
||||
let sender_ko = StubTransport::new(Err(()));
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
|
||||
Reference in New Issue
Block a user