mail-list 0.3.0-alpha

Email subscriptions list library. Unicast, narrowcast and broadcast lists
Documentation
use lettre::AsyncSmtpTransport;
#[cfg(feature = "tokio-runtime")]
use lettre::transport::smtp::authentication::{Credentials, Mechanism};
use percent_encoding::percent_decode_str;

use crate::{MailListError, MailListResult, Smtps};

#[derive(Default, Debug)]
pub struct SmtpsBuilder {
    mail_server: String,
    username: String,
    password: String,
    port: u16,
}

impl SmtpsBuilder {
    pub fn new(mail_server: &str) -> Self {
        Self {
            mail_server: mail_server.to_string(),
            ..Default::default()
        }
    }

    pub fn set_username(mut self, username: &str) -> Self {
        self.username = username.to_string();

        self
    }

    pub fn set_password(mut self, password: &str) -> Self {
        self.password = password.to_string();

        self
    }

    pub fn set_port(mut self, port: u16) -> Self {
        self.port = port;

        self
    }

    pub fn build(self) -> MailListResult<Smtps> {
        #[cfg(feature = "tokio-runtime")]
        let mailer = AsyncSmtpTransport::<lettre::Tokio1Executor>::relay(&self.mail_server)
            .map_err(|error| MailListError::Mailer(error.to_string()))?
            .credentials(Credentials::new(self.username, self.password))
            .authentication(vec![Mechanism::Plain])
            .build();

        Ok(Smtps {
            domain: self.mail_server.into(),
            ehlo_name: None,
            mailer,
        })
    }

    pub fn parse(smtps_uri: &str) -> MailListResult<Smtps> {
        use url::Url;

        let url =
            Url::parse(smtps_uri).or(Err(MailListError::Smtps("Invalid smtps URI".to_string())))?;

        if url.scheme() != "smtps" {
            return Err(MailListError::Smtps("Invalid SMTPs scheme".to_string()));
        };
        let domain = url
            .host_str()
            .ok_or(MailListError::Smtps(
                "Domain not found in SMTPs".to_string(),
            ))?
            .to_string();
        let domain = percent_decode_str(&domain)
            .decode_utf8()
            .or(Err(MailListError::Smtps(
                "Domain is not valid UTF-8".to_string(),
            )))?
            .to_string();

        let ehlo_name = url.path().trim_start_matches('/');
        let ehlo_name = percent_decode_str(ehlo_name)
            .decode_utf8()
            .or(Err(MailListError::Smtps(
                "EHLO name is not valid UTF-8".to_string(),
            )))?
            .to_string();
        let ehlo_name: Option<std::borrow::Cow<'_, str>> = if ehlo_name.is_empty() {
            None
        } else {
            Some(std::borrow::Cow::Owned(ehlo_name))
        };

        #[cfg(feature = "tokio-runtime")]
        let transport = AsyncSmtpTransport::<lettre::Tokio1Executor>::from_url(smtps_uri)
            .map_err(|error| MailListError::Mailer(error.to_string()))?;

        let mailer = if let Some(hello_name) = ehlo_name.as_ref() {
            transport.hello_name(lettre::transport::smtp::extension::ClientId::Domain(
                hello_name.to_string(),
            ))
        } else {
            transport
        };

        let mailer = mailer.build();

        let outcome = Smtps {
            domain: domain.into(),
            ehlo_name,
            mailer,
        };

        Ok(outcome)
    }
}

#[cfg(test)]
mod sanity_checks {
    use crate::SmtpsBuilder;

    #[test]
    fn smtps_builder() {
        let smtps_uri = "smtps://support@example.com:password@smtp.example.com:465/foo.example.com";

        SmtpsBuilder::parse(smtps_uri).unwrap();
    }
}