Files
RustBox/src/smtp/client.rs
T
Mateusz Gruszczyński a643e7e3ab
RustBox CI / rustbox (push) Waiting to run
v0.0.5
2026-10-30 12:06:44 +01:00

124 lines
3.9 KiB
Rust

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<SmtpSendResult, SmtpError>;
async fn check_connection(&self, _creds: &SmtpCredentials) -> Result<(), SmtpError> {
Ok(())
}
}
pub struct RealSmtpClient;
fn smtp_transport(
creds: &SmtpCredentials,
) -> Result<lettre::AsyncSmtpTransport<lettre::Tokio1Executor>, 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::<Tokio1Executor>::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<SmtpSendResult, SmtpError> {
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;