use serde::Deserialize;
use tracing::warn;
use crate::Error;
use crate::ops::check::Result;
use crate::ops::check::output::{
DEFAULT_MESSAGE_TEMPLATE, TableFormat, render_message, should_skip_empty,
};
use crate::ops::check::{Options, VersionedSource};
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct OutputConfig {
pub smtp_server: String,
pub sender: String,
pub sender_password: String,
pub recipients: Vec<String>,
pub subject: Option<String>,
pub message_template: Option<String>,
pub tls: Option<bool>,
pub send_empty: Option<bool>,
}
pub async fn output_mail(
opts: &Options,
sources: &[VersionedSource],
config: &OutputConfig,
) -> Result<()> {
if should_skip_empty(config.send_empty, sources) {
println!("No updates, no mail has been sent.");
return Ok(());
}
let msg = render_message(
opts,
sources,
config
.message_template
.as_deref()
.unwrap_or(DEFAULT_MESSAGE_TEMPLATE),
TableFormat::Html,
)?;
send_message(opts, config, msg).await?;
Ok(())
}
async fn send_message(_opts: &Options, config: &OutputConfig, msg: impl AsRef<str>) -> Result<()> {
use lettre::message::Mailbox;
use lettre::message::header::ContentType;
use lettre::transport::smtp::PoolConfig;
use lettre::transport::smtp::authentication::Credentials;
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
let from: Mailbox = config
.sender
.parse()
.map_err(|err| Error::InvalidSenderMail {
mail: config.sender.clone(),
err,
})?;
let creds = Credentials::new(config.sender.clone(), config.sender_password.clone());
let mailer: AsyncSmtpTransport<Tokio1Executor> = if config.tls.unwrap_or(true) {
AsyncSmtpTransport::<Tokio1Executor>::relay(&config.smtp_server)?
.credentials(creds)
.build()
} else {
tracing::warn!(
"`tls: false` for {}: the connection starts in clear and upgrades \
opportunistically. Only use it for a trusted local relay.",
config.smtp_server
);
AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&config.smtp_server)?
.credentials(creds)
.pool_config(PoolConfig::new().max_size(20))
.build()
};
let mail_number = config.recipients.len();
for (i, recipient) in config.recipients.iter().enumerate() {
let to: Result<Mailbox> = recipient
.parse()
.map_err(|err| Error::InvalidRecipientMail {
mail: recipient.clone(),
err,
});
match to {
Ok(to) => {
let email = Message::builder()
.from(from.clone())
.to(to.clone())
.subject(config.subject.clone().unwrap_or_default())
.header(ContentType::TEXT_HTML)
.body(String::from(msg.as_ref()))
.map_err(|err| {
Error::io(
std::io::Error::new(std::io::ErrorKind::InvalidData, err),
"Could not build email message",
)
})?;
match mailer.send(email).await {
Ok(_) => println!("Email {}/{} sent successfully", i + 1, mail_number),
Err(err) => print_error(i + 1, mail_number, Error::SMTP(err)),
}
}
Err(err) => print_error(i + 1, mail_number, err),
}
}
Ok(())
}
fn print_error<E>(counter: usize, max: usize, error: E)
where
E: miette::Diagnostic + Sync + Send + 'static,
{
warn!("Could not send email {}/{}: ", counter, max);
println!("{:?}", miette::Report::new(error));
}