use crate::mailet::{Mailet, MailetAction, MailetConfig};
use async_trait::async_trait;
use rusmes_proto::{Mail, MailState, Username};
use rusmes_storage::{MailboxPath, StorageBackend};
use std::sync::Arc;
pub struct LocalDeliveryMailet {
name: String,
storage: Option<Arc<dyn StorageBackend>>,
}
impl LocalDeliveryMailet {
pub fn new() -> Self {
Self {
name: "LocalDelivery".to_string(),
storage: None,
}
}
pub fn with_storage(storage: Arc<dyn StorageBackend>) -> Self {
Self {
name: "LocalDelivery".to_string(),
storage: Some(storage),
}
}
}
impl Default for LocalDeliveryMailet {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Mailet for LocalDeliveryMailet {
async fn init(&mut self, _config: MailetConfig) -> anyhow::Result<()> {
tracing::info!("Initialized LocalDeliveryMailet");
Ok(())
}
async fn service(&self, mail: &mut Mail) -> anyhow::Result<MailetAction> {
tracing::info!(
"LocalDeliveryMailet: Delivering mail {} to {} local recipients",
mail.id(),
mail.recipients().len()
);
let storage = match &self.storage {
Some(s) => {
tracing::info!("LocalDeliveryMailet: Storage backend is available");
s
}
None => {
tracing::warn!(
"LocalDeliveryMailet: No storage backend configured, mail will be dropped"
);
return Ok(MailetAction::ChangeState(MailState::Ghost));
}
};
tracing::info!("LocalDeliveryMailet: Getting mailbox and message stores");
let mailbox_store = storage.mailbox_store();
let message_store = storage.message_store();
tracing::info!("LocalDeliveryMailet: Got stores successfully");
let mut delivered_count = 0;
let mut failed_recipients = Vec::new();
for recipient in mail.recipients() {
let username = Username::new(recipient.local_part())?;
let mailbox_path = MailboxPath::new(username.clone(), vec!["INBOX".to_string()]);
let mailboxes = mailbox_store.list_mailboxes(&username).await?;
let inbox = mailboxes.iter().find(|mb| {
if let Some(name) = mb.path().name() {
name == "INBOX"
} else {
false
}
});
let mailbox_id = if let Some(inbox) = inbox {
*inbox.id()
} else {
tracing::info!("Creating INBOX for user {}", username.as_str());
match mailbox_store.create_mailbox(&mailbox_path).await {
Ok(id) => id,
Err(e) => {
tracing::error!("Failed to create INBOX for {}: {}", recipient, e);
failed_recipients.push(recipient.clone());
continue;
}
}
};
match message_store
.append_message(&mailbox_id, mail.clone())
.await
{
Ok(metadata) => {
tracing::info!(
"Delivered mail {} to {} (message_id: {})",
mail.id(),
recipient,
metadata.message_id()
);
delivered_count += 1;
}
Err(e) => {
tracing::error!("Failed to deliver to {}: {}", recipient, e);
failed_recipients.push(recipient.clone());
}
}
}
if delivered_count == 0 {
tracing::error!("Failed to deliver mail {} to any recipients", mail.id());
Ok(MailetAction::ChangeState(MailState::Error))
} else if !failed_recipients.is_empty() {
tracing::warn!(
"Partially delivered mail {}: {}/{} successful",
mail.id(),
delivered_count,
mail.recipients().len()
);
Ok(MailetAction::ChangeState(MailState::Ghost))
} else {
tracing::info!(
"Successfully delivered mail {} to all {} recipients",
mail.id(),
delivered_count
);
Ok(MailetAction::ChangeState(MailState::Ghost))
}
}
fn name(&self) -> &str {
&self.name
}
}
#[cfg(test)]
mod tests {
use super::*;
use bytes::Bytes;
use rusmes_proto::{HeaderMap, MailAddress, MessageBody, MimeMessage};
use std::str::FromStr;
fn create_test_mail(sender: &str, recipients: Vec<&str>) -> Mail {
let sender_addr = MailAddress::from_str(sender).ok();
let recipient_addrs: Vec<MailAddress> = recipients
.iter()
.filter_map(|r| MailAddress::from_str(r).ok())
.collect();
let message = MimeMessage::new(
HeaderMap::new(),
MessageBody::Small(Bytes::from("Test message")),
);
Mail::new(sender_addr, recipient_addrs, message, None, None)
}
#[tokio::test]
async fn test_local_delivery_mailet_creation() {
let mailet = LocalDeliveryMailet::new();
assert_eq!(mailet.name(), "LocalDelivery");
}
#[tokio::test]
async fn test_local_delivery_mailet_default() {
let mailet = LocalDeliveryMailet::default();
assert_eq!(mailet.name(), "LocalDelivery");
}
#[tokio::test]
async fn test_local_delivery_init() {
let mut mailet = LocalDeliveryMailet::new();
let config = MailetConfig::new("LocalDelivery");
let result = mailet.init(config).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_local_delivery_service() {
let mailet = LocalDeliveryMailet::new();
let mut mail = create_test_mail(
"sender@example.com",
vec!["user1@local.com", "user2@local.com"],
);
let action = mailet.service(&mut mail).await.unwrap();
assert!(matches!(
action,
MailetAction::ChangeState(MailState::Ghost)
));
}
#[tokio::test]
async fn test_local_delivery_single_recipient() {
let mailet = LocalDeliveryMailet::new();
let mut mail = create_test_mail("sender@example.com", vec!["user@local.com"]);
let action = mailet.service(&mut mail).await.unwrap();
assert!(matches!(
action,
MailetAction::ChangeState(MailState::Ghost)
));
}
#[tokio::test]
async fn test_local_delivery_multiple_recipients() {
let mailet = LocalDeliveryMailet::new();
let mut mail = create_test_mail(
"sender@example.com",
vec![
"user1@local.com",
"user2@local.com",
"user3@local.com",
"user4@local.com",
],
);
let action = mailet.service(&mut mail).await.unwrap();
assert!(matches!(
action,
MailetAction::ChangeState(MailState::Ghost)
));
}
}