mod code_msg;
use std::{str::FromStr, time::Duration};
pub use code_msg::*;
use lettre::{
AsyncSmtpTransport, AsyncTransport, SmtpTransport, Tokio1Executor, Transport,
transport::smtp::{
PoolConfig, SMTP_PORT, SUBMISSION_PORT,
authentication::{Credentials, DEFAULT_MECHANISMS, Mechanism},
client::{Tls, TlsParameters},
extension::ClientId,
},
};
pub use lettre::{
message::*,
transport::smtp::{Error, response::Response},
};
#[derive(Debug)]
pub struct MailerBuilder {
username: String,
password: String,
tls_option: TlsOption,
hello_name: ClientId,
server: String,
port: Option<u16>,
authentication: Vec<Mechanism>,
timeout: Option<Duration>,
pool_config: PoolConfig,
}
#[derive(Debug, Clone, Copy, Default)]
#[allow(missing_copy_implementations)]
#[allow(clippy::exhaustive_enums)]
pub enum TlsOption {
None,
Opportunistic,
Required,
#[default]
Wrapper,
}
impl TlsOption {
pub const fn is_none(self) -> bool {
matches!(self, Self::None)
}
}
impl FromStr for TlsOption {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"none" => Ok(Self::None),
"opportunistic" => Ok(Self::Opportunistic),
"required" => Ok(Self::Required),
"wrapper" => Ok(Self::Wrapper),
_ => Err("TlsOption string must be one of none, opportunistic, required and wrapper."),
}
}
}
impl MailerBuilder {
pub fn new(username: String, password: String, server: String) -> MailerBuilder {
Self {
username,
password,
server,
port: None,
tls_option: TlsOption::default(),
hello_name: ClientId::default(),
authentication: DEFAULT_MECHANISMS.into(),
timeout: Some(Duration::from_secs(60)),
pool_config: PoolConfig::default(),
}
}
pub const fn with_port(mut self, port: u16) -> Self {
self.port = Some(port);
self
}
pub const fn with_tls_option(mut self, tls_option: TlsOption) -> Self {
self.tls_option = tls_option;
self
}
pub fn with_hello_name(mut self, hello_name: ClientId) -> Self {
self.hello_name = hello_name;
self
}
pub fn with_authentication(mut self, authentication: Vec<Mechanism>) -> Self {
self.authentication = authentication;
self
}
pub const fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn mailer(self) -> Result<Mailer, Error> {
let creds = Credentials::new(self.username, self.password);
let port = self.port.unwrap_or_else(|| {
if self.tls_option.is_none() {
SMTP_PORT
} else {
SUBMISSION_PORT
}
});
let mut builder = SmtpTransport::builder_dangerous(self.server.clone())
.port(port)
.credentials(creds)
.authentication(self.authentication)
.hello_name(self.hello_name)
.pool_config(self.pool_config)
.timeout(self.timeout);
if !self.tls_option.is_none() {
let tls_parameters = TlsParameters::new(self.server)?;
builder = builder.tls(match self.tls_option {
TlsOption::None => unreachable!(),
TlsOption::Opportunistic => Tls::Opportunistic(tls_parameters),
TlsOption::Required => Tls::Required(tls_parameters),
TlsOption::Wrapper => Tls::Wrapper(tls_parameters),
})
}
Ok(Mailer {
mailer: builder.build(),
})
}
pub fn async_mailer(self) -> Result<AsnycMailer, Error> {
let creds = Credentials::new(self.username, self.password);
let port = self.port.unwrap_or_else(|| {
if self.tls_option.is_none() {
SMTP_PORT
} else {
SUBMISSION_PORT
}
});
let mut builder = AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(self.server.clone())
.port(port)
.credentials(creds)
.authentication(self.authentication)
.hello_name(self.hello_name)
.pool_config(self.pool_config)
.timeout(self.timeout);
if !self.tls_option.is_none() {
let tls_parameters = TlsParameters::new(self.server)?;
builder = builder.tls(match self.tls_option {
TlsOption::None => unreachable!(),
TlsOption::Opportunistic => Tls::Opportunistic(tls_parameters),
TlsOption::Required => Tls::Required(tls_parameters),
TlsOption::Wrapper => Tls::Wrapper(tls_parameters),
})
}
Ok(AsnycMailer {
mailer: builder.build(),
})
}
}
#[derive(Clone)]
pub struct Mailer {
mailer: SmtpTransport,
}
impl Mailer {
pub fn send(&self, message: &Message) -> Result<Response, Error> {
Transport::send(&self.mailer, message)
}
pub fn test_connection(&self) -> Result<bool, Error> {
self.mailer.test_connection()
}
}
#[derive(Clone)]
#[allow(clippy::exhaustive_structs)]
pub struct AsnycMailer {
mailer: AsyncSmtpTransport<Tokio1Executor>,
}
impl AsnycMailer {
pub async fn send(&self, message: Message) -> Result<Response, Error> {
AsyncTransport::send(&self.mailer, message).await
}
pub async fn test_connection(&self) -> Result<bool, Error> {
self.mailer.test_connection().await
}
}
#[allow(clippy::exhaustive_structs)]
pub struct VerificationCode {}
#[cfg(test)]
mod tests {
use std::env;
use dotenv::dotenv;
use super::*;
#[test]
fn main() -> anyhow::Result<()> {
dotenv().ok();
let username: String = env::var("SMTP_USERNAME").expect("SMTP_USERNAME must be set");
let password: String = env::var("SMTP_PASSWORD").expect("SMTP_PASSWORD must be set");
let server: String = env::var("SMTP_SERVER").expect("SMTP_SERVER must be set");
let port: u16 = env::var("SMTP_PORT")
.expect("SMTP_PORT must be set")
.parse()
.expect("SMTP_PORT must be a number");
let tls: TlsOption = env::var("SMTP_TLS")
.expect("SMTP_TLS must be set")
.parse()
.expect("SMTP_TLS must be one of none, opportunistic, required and wrapper.");
let builder = MailerBuilder::new(username, password, server)
.with_port(port)
.with_tls_option(tls);
println!("{builder:?}");
let mailer = builder.mailer().unwrap();
let from_email = env::var("FROM_EMAIL").expect("FROM_EMAIL must be set");
let to_email = env::var("TO_EMAIL").expect("TO_EMAIL must be set");
let totp_msg = DefaultTotpMessageCN::new("Dipper".to_owned(), true);
let email = totp_msg.email_message(
from_email.parse().unwrap(),
to_email.parse().unwrap(),
TOTP::gen_secret_base32(),
10 * 60,
)?;
println!("{:?}", mailer.test_connection());
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => println!("Could not send email: {:?}", e),
}
Ok(())
}
}