first commit
RustBox CI / rustbox (push) Waiting to run

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-29 15:24:59 +01:00
commit bc5fbd6ba9
180 files changed
+63750

No files matched your search

+157
View File
@@ -0,0 +1,157 @@
//! SMTP client abstraction and real implementation.
use async_trait::async_trait;
// Import and re-export types for backward compatibility
pub use crate::smtp::error::SmtpError;
pub use crate::smtp::types::{AttachmentData, SendableMessage, SmtpCredentials, SmtpSendResult};
// ---------------------------------------------------------------------------
// Trait definition
// ---------------------------------------------------------------------------
/// Abstraction over SMTP operations.
///
/// Every method receives explicit connection parameters so that the trait
/// remains stateless — no persistent connections are held.
///
/// The `Send + Sync` bounds allow implementations to be shared across
/// Tokio tasks and stored in `Arc`.
#[allow(clippy::double_must_use)]
#[async_trait]
pub trait SmtpClient: Send + Sync {
/// Send an email message. Returns the generated Message-ID and exact RFC822 bytes.
async fn send_message(
&self,
creds: &SmtpCredentials,
message: &SendableMessage,
) -> Result<SmtpSendResult, SmtpError>;
/// Verify that the configured SMTP endpoint accepts an authenticated NOOP.
async fn check_connection(&self, _creds: &SmtpCredentials) -> Result<(), SmtpError> {
Ok(())
}
}
// ---------------------------------------------------------------------------
// Real implementation backed by lettre
// ---------------------------------------------------------------------------
/// Production SMTP client that uses `lettre`.
///
/// This is a stateless unit struct — every method creates a fresh connection,
/// performs the operation, and disconnects.
pub struct RealSmtpClient;
#[async_trait]
impl SmtpClient for RealSmtpClient {
async fn send_message(
&self,
creds: &SmtpCredentials,
message: &SendableMessage,
) -> Result<SmtpSendResult, SmtpError> {
use lettre::transport::smtp::authentication::Credentials;
use lettre::{AsyncSmtpTransport, AsyncTransport, Tokio1Executor};
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();
let smtp_creds = Credentials::new(creds.email.clone(), creds.password.clone());
let transport: AsyncSmtpTransport<Tokio1Executor> = if creds.tls && creds.port == 587 {
AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&creds.host)
.map_err(|e| SmtpError::ConnectionFailed(e.to_string()))?
.port(creds.port)
.credentials(smtp_creds)
.build()
} else if creds.tls {
AsyncSmtpTransport::<Tokio1Executor>::relay(&creds.host)
.map_err(|e| SmtpError::ConnectionFailed(e.to_string()))?
.port(creds.port)
.credentials(smtp_creds)
.build()
} else {
AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(&creds.host)
.port(creds.port)
.credentials(smtp_creds)
.build()
};
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> {
use lettre::transport::smtp::authentication::Credentials;
use lettre::{AsyncSmtpTransport, Tokio1Executor};
let smtp_creds = Credentials::new(creds.email.clone(), creds.password.clone());
let transport: AsyncSmtpTransport<Tokio1Executor> = if creds.tls && creds.port == 587 {
AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&creds.host)
.map_err(|e| SmtpError::ConnectionFailed(e.to_string()))?
.port(creds.port)
.credentials(smtp_creds)
.build()
} else if creds.tls {
AsyncSmtpTransport::<Tokio1Executor>::relay(&creds.host)
.map_err(|e| SmtpError::ConnectionFailed(e.to_string()))?
.port(creds.port)
.credentials(smtp_creds)
.build()
} else {
AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(&creds.host)
.port(creds.port)
.credentials(smtp_creds)
.build()
};
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(),
))
}
}
}
// ---------------------------------------------------------------------------
// Mock implementation (test-only)
// ---------------------------------------------------------------------------
#[cfg(test)]
#[path = "mock.rs"]
pub mod mock;