use std::fmt::Debug;
use log::{debug, info};
use crate::authentication::{Credentials, Mechanism};
use crate::commands::*;
use crate::error::{Error, SmtpResult};
use crate::extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo};
use crate::stream::SmtpStream;
use crate::SendableEmail;
#[cfg(feature = "runtime-async-std")]
use async_std::io::{BufRead, Write};
#[cfg(feature = "runtime-tokio")]
use tokio::io::{AsyncBufRead as BufRead, AsyncWrite as Write};
#[derive(Debug)]
pub struct SmtpClient {
hello_name: ClientId,
smtp_utf8: bool,
expect_greeting: bool,
pipelining: bool,
}
impl Default for SmtpClient {
fn default() -> Self {
Self::new()
}
}
impl SmtpClient {
pub fn new() -> Self {
SmtpClient {
smtp_utf8: false,
hello_name: Default::default(),
expect_greeting: true,
pipelining: true,
}
}
pub fn smtp_utf8(self, enabled: bool) -> SmtpClient {
Self {
smtp_utf8: enabled,
..self
}
}
pub fn pipelining(self, enabled: bool) -> SmtpClient {
Self {
pipelining: enabled,
..self
}
}
pub fn hello_name(self, name: ClientId) -> SmtpClient {
Self {
hello_name: name,
..self
}
}
pub fn without_greeting(self) -> SmtpClient {
Self {
expect_greeting: false,
..self
}
}
}
#[derive(Debug)]
pub struct SmtpTransport<S: BufRead + Write + Unpin> {
server_info: ServerInfo,
client_info: SmtpClient,
stream: SmtpStream<S>,
}
impl<S: BufRead + Write + Unpin> SmtpTransport<S> {
pub async fn new(builder: SmtpClient, stream: S) -> Result<Self, Error> {
let mut stream = SmtpStream::new(stream);
if builder.expect_greeting {
let _greeting = stream.read_response().await?;
}
let ehlo_response = stream
.ehlo(ClientId::new(builder.hello_name.to_string()))
.await?;
let server_info = ServerInfo::from_response(&ehlo_response)?;
debug!("server {}", server_info);
let transport = SmtpTransport {
server_info,
client_info: builder,
stream,
};
Ok(transport)
}
pub async fn try_login(
&mut self,
credentials: &Credentials,
accepted_mechanisms: &[Mechanism],
) -> Result<(), Error> {
if let Some(mechanism) = accepted_mechanisms
.iter()
.find(|mechanism| self.server_info.supports_auth_mechanism(**mechanism))
{
self.auth(*mechanism, credentials).await?;
} else {
info!("No supported authentication mechanisms available");
}
Ok(())
}
pub async fn starttls(mut self) -> Result<S, Error> {
if !self.supports_feature(Extension::StartTls) {
return Err(From::from("server does not support STARTTLS"));
}
self.stream.command(StarttlsCommand).await?;
Ok(self.stream.into_inner())
}
fn supports_feature(&self, keyword: Extension) -> bool {
self.server_info.supports_feature(keyword)
}
pub async fn quit(&mut self) -> Result<(), Error> {
self.stream.command(QuitCommand).await?;
Ok(())
}
pub async fn auth(&mut self, mechanism: Mechanism, credentials: &Credentials) -> SmtpResult {
let mut challenges = 10;
let mut response = self
.stream
.command(AuthCommand::new(mechanism, credentials.clone(), None)?)
.await?;
while challenges > 0 && response.has_code(334) {
challenges -= 1;
response = self
.stream
.command(AuthCommand::new_from_response(
mechanism,
credentials.clone(),
&response,
)?)
.await?;
}
if challenges == 0 {
Err(Error::ResponseParsing("Unexpected number of challenges"))
} else {
Ok(response)
}
}
pub async fn send(&mut self, email: SendableEmail) -> SmtpResult {
let mut mail_options = vec![];
if self.supports_feature(Extension::EightBitMime) {
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
}
if self.supports_feature(Extension::SmtpUtfEight) && self.client_info.smtp_utf8 {
mail_options.push(MailParameter::SmtpUtfEight);
}
let pipelining =
self.supports_feature(Extension::Pipelining) && self.client_info.pipelining;
if pipelining {
self.stream
.send_command(MailCommand::new(
email.envelope().from().cloned(),
mail_options,
))
.await?;
let mut sent_commands = 1;
for to_address in email.envelope().to() {
self.stream
.send_command(RcptCommand::new(to_address.clone(), vec![]))
.await?;
sent_commands += 1;
}
self.stream.send_command(DataCommand).await?;
sent_commands += 1;
for _ in 0..sent_commands {
self.stream.read_response().await?;
}
} else {
self.stream
.command(MailCommand::new(
email.envelope().from().cloned(),
mail_options,
))
.await?;
for to_address in email.envelope().to() {
self.stream
.command(RcptCommand::new(to_address.clone(), vec![]))
.await?;
debug!("to=<{}>", to_address);
}
self.stream.command(DataCommand).await?;
}
let res = self.stream.message(email.message()).await;
if let Ok(result) = &res {
debug!(
"status=sent ({})",
result.message.first().unwrap_or(&"no response".to_string())
);
}
res
}
}