use async_trait::async_trait; pub use crate::smtp::error::SmtpError; pub use crate::smtp::types::{AttachmentData, SendableMessage, SmtpCredentials, SmtpSendResult}; #[allow(clippy::double_must_use)] #[async_trait] pub trait SmtpClient: Send + Sync { async fn send_message( &self, creds: &SmtpCredentials, message: &SendableMessage, ) -> Result; async fn check_connection(&self, _creds: &SmtpCredentials) -> Result<(), SmtpError> { Ok(()) } } pub struct RealSmtpClient; fn smtp_transport( creds: &SmtpCredentials, ) -> Result, SmtpError> { use lettre::transport::smtp::authentication::Credentials; use lettre::transport::smtp::client::{Tls, TlsParameters}; use lettre::{AsyncSmtpTransport, Tokio1Executor}; let smtp_creds = Credentials::new(creds.email.clone(), creds.password.clone()); let mut builder = AsyncSmtpTransport::::builder_dangerous(&creds.host) .port(creds.port) .credentials(smtp_creds); if creds.tls { let parameters = TlsParameters::builder(creds.host.clone()) .dangerous_accept_invalid_certs(!creds.tls_verify) .dangerous_accept_invalid_hostnames(!creds.tls_verify) .build() .map_err(|e| SmtpError::ConnectionFailed(e.to_string()))?; builder = builder.tls(if creds.port == 587 { Tls::Required(parameters) } else { Tls::Wrapper(parameters) }); } else { builder = builder.tls(Tls::None); } Ok(builder.build()) } #[async_trait] impl SmtpClient for RealSmtpClient { async fn send_message( &self, creds: &SmtpCredentials, message: &SendableMessage, ) -> Result { use lettre::AsyncTransport; let message_id = format!( "<{}.{}@{}>", uuid::Uuid::new_v4(), uuid::Uuid::new_v4(), creds.host ); let email = crate::smtp::builder::build_message(message, &message_id) .map_err(SmtpError::SendFailed)?; let rfc822 = email.formatted(); if message.max_message_size_bytes > 0 && rfc822.len() > message.max_message_size_bytes { return Err(SmtpError::MessageTooLarge { actual: rfc822.len(), limit: message.max_message_size_bytes, }); } let transport = smtp_transport(creds)?; transport.send(email).await.map_err(|e| { let msg = e.to_string(); let lower = msg.to_lowercase(); if lower.contains("authentication") || lower.contains("credentials") || lower.contains("auth") { SmtpError::AuthenticationFailed } else if lower.contains("connect") || lower.contains("dns") || lower.contains("resolve") || lower.contains("timeout") || lower.contains("tls") { SmtpError::ConnectionFailed(msg) } else { SmtpError::SendFailed(msg) } })?; Ok(SmtpSendResult { message_id, rfc822 }) } async fn check_connection(&self, creds: &SmtpCredentials) -> Result<(), SmtpError> { let transport = smtp_transport(creds)?; let accepted = transport.test_connection().await.map_err(|e| { let message = e.to_string(); if message.to_ascii_lowercase().contains("auth") { SmtpError::AuthenticationFailed } else { SmtpError::ConnectionFailed(message) } })?; if accepted { Ok(()) } else { Err(SmtpError::ConnectionFailed( "SMTP NOOP was not accepted".to_string(), )) } } } #[cfg(test)] #[path = "mock.rs"] pub mod mock;