#![allow(deprecated)]
use crate::backends::{EmailBackend, SmtpBackend, SmtpConfig};
use crate::message::EmailMessage;
use crate::{EmailError, EmailResult};
use std::sync::Arc;
use tokio::sync::Semaphore;
#[derive(Debug, Clone)]
pub struct PoolConfig {
pub max_connections: usize,
pub min_idle: usize,
pub max_messages_per_connection: usize,
}
impl Default for PoolConfig {
fn default() -> Self {
Self {
max_connections: 10,
min_idle: 2,
max_messages_per_connection: 100,
}
}
}
impl PoolConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_max_connections(mut self, max: usize) -> Self {
self.max_connections = max;
self
}
pub fn with_min_idle(mut self, min: usize) -> Self {
self.min_idle = min;
self
}
pub fn with_max_messages_per_connection(mut self, max: usize) -> Self {
self.max_messages_per_connection = max;
self
}
}
pub struct EmailPool {
smtp_config: SmtpConfig,
pool_config: PoolConfig,
semaphore: Arc<Semaphore>,
}
impl std::fmt::Debug for EmailPool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EmailPool")
.field("smtp_config", &self.smtp_config)
.field("pool_config", &self.pool_config)
.finish_non_exhaustive()
}
}
impl EmailPool {
pub fn new(smtp_config: SmtpConfig, pool_config: PoolConfig) -> EmailResult<Self> {
if pool_config.max_connections == 0 {
return Err(EmailError::BackendError(format!(
"Invalid configuration: max_connections must be at least 1, got {}",
pool_config.max_connections
)));
}
if pool_config.max_messages_per_connection == 0 {
return Err(EmailError::BackendError(format!(
"Invalid configuration: max_messages_per_connection must be at least 1, got {}",
pool_config.max_messages_per_connection
)));
}
let semaphore = Arc::new(Semaphore::new(pool_config.max_connections));
Ok(Self {
smtp_config,
pool_config,
semaphore,
})
}
pub async fn send_bulk(&self, messages: Vec<EmailMessage>) -> EmailResult<usize> {
if messages.is_empty() {
return Ok(0);
}
let chunk_size = self.pool_config.max_messages_per_connection;
let chunks: Vec<Vec<EmailMessage>> = messages
.chunks(chunk_size)
.map(|chunk| chunk.to_vec())
.collect();
let mut total_sent = 0;
let mut handles = Vec::new();
for chunk in chunks {
let permit = self.semaphore.clone().acquire_owned().await.map_err(|e| {
EmailError::BackendError(format!("Failed to acquire semaphore: {}", e))
})?;
let smtp_config = self.smtp_config.clone();
let handle = tokio::spawn(async move {
let backend = SmtpBackend::new(smtp_config)?;
let result = backend.send_messages(&chunk).await;
drop(permit); result
});
handles.push(handle);
}
for handle in handles {
let sent = handle
.await
.map_err(|e| EmailError::BackendError(format!("Task join error: {}", e)))??;
total_sent += sent;
}
Ok(total_sent)
}
pub async fn send(&self, message: EmailMessage) -> EmailResult<()> {
self.send_bulk(vec![message]).await?;
Ok(())
}
pub fn config(&self) -> &PoolConfig {
&self.pool_config
}
pub fn smtp_config(&self) -> &SmtpConfig {
&self.smtp_config
}
}
pub struct BatchSender {
pool: EmailPool,
batch_size: usize,
delay: std::time::Duration,
}
impl BatchSender {
pub fn new(smtp_config: SmtpConfig, pool_config: PoolConfig) -> EmailResult<Self> {
let pool = EmailPool::new(smtp_config, pool_config)?;
Ok(Self {
pool,
batch_size: 100,
delay: std::time::Duration::from_millis(0),
})
}
pub fn with_batch_size(mut self, size: usize) -> Self {
self.batch_size = size;
self
}
pub fn with_delay(mut self, delay: std::time::Duration) -> Self {
self.delay = delay;
self
}
pub async fn send_with_rate_limit(
&mut self,
messages: Vec<EmailMessage>,
) -> EmailResult<usize> {
let mut total_sent = 0;
let chunks: Vec<&[EmailMessage]> = messages.chunks(self.batch_size).collect();
let last_index = chunks.len().saturating_sub(1);
for (i, batch) in chunks.into_iter().enumerate() {
let sent = self.pool.send_bulk(batch.to_vec()).await?;
total_sent += sent;
if !self.delay.is_zero() && i < last_index {
tokio::time::sleep(self.delay).await;
}
}
Ok(total_sent)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
use std::sync::atomic::{AtomicUsize, Ordering};
#[rstest]
fn test_pool_config() {
let config = PoolConfig::new()
.with_max_connections(20)
.with_min_idle(5)
.with_max_messages_per_connection(50);
assert_eq!(config.max_connections, 20);
assert_eq!(config.min_idle, 5);
assert_eq!(config.max_messages_per_connection, 50);
}
#[rstest]
fn test_pool_config_default() {
let config = PoolConfig::default();
assert_eq!(config.max_connections, 10);
assert_eq!(config.min_idle, 2);
assert_eq!(config.max_messages_per_connection, 100);
}
#[rstest]
fn test_email_pool_rejects_zero_max_connections() {
let smtp_config = SmtpConfig::new("smtp.example.com", 587);
let pool_config = PoolConfig::new().with_max_connections(0);
let result = EmailPool::new(smtp_config, pool_config);
let err = result.unwrap_err();
assert!(
matches!(err, EmailError::BackendError(ref msg) if msg.contains("max_connections")),
"Expected BackendError for max_connections, got: {err}"
);
}
#[rstest]
fn test_email_pool_rejects_zero_max_messages_per_connection() {
let smtp_config = SmtpConfig::new("smtp.example.com", 587);
let pool_config = PoolConfig::new().with_max_messages_per_connection(0);
let result = EmailPool::new(smtp_config, pool_config);
let err = result.unwrap_err();
assert!(
matches!(err, EmailError::BackendError(ref msg) if msg.contains("max_messages_per_connection")),
"Expected BackendError for max_messages_per_connection, got: {err}"
);
}
#[tokio::test]
async fn test_semaphore_enforces_max_connections() {
let max_connections = 3;
let semaphore = Arc::new(Semaphore::new(max_connections));
let active_count = Arc::new(AtomicUsize::new(0));
let peak_count = Arc::new(AtomicUsize::new(0));
let total_tasks = 20;
let mut handles = Vec::new();
for _ in 0..total_tasks {
let sem = semaphore.clone();
let active = active_count.clone();
let peak = peak_count.clone();
handles.push(tokio::spawn(async move {
let _permit = sem.acquire().await.unwrap();
let current = active.fetch_add(1, Ordering::SeqCst) + 1;
peak.fetch_max(current, Ordering::SeqCst);
tokio::task::yield_now().await;
active.fetch_sub(1, Ordering::SeqCst);
}));
}
for handle in handles {
handle.await.unwrap();
}
let observed_peak = peak_count.load(Ordering::SeqCst);
assert!(
observed_peak <= max_connections,
"Peak concurrent count {} exceeded max_connections {}",
observed_peak,
max_connections
);
}
}