adnv 1.5.0

CLI that tracks, checks and auto-updates infrastructure dependency versions across Terraform, Helm, Docker, Git and npm
Documentation
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>,
    /// default true
    pub tls: Option<bool>,
    /// default false
    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 {
        // STARTTLS is opportunistic: an attacker who strips the server's
        // announcement gets the password in clear. Do not force PLAIN on top of
        // that — let lettre negotiate the strongest mechanism the server offers.
        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));
}