Skip to main content

mailsis_utils/
storage.rs

1//! Storage engine traits and implementations for persisting email messages.
2//!
3//! Provides [`StorageEngine`] as the abstract interface, with
4//! [`FileStorageEngine`] (filesystem-backed) and [`MemoryStorageEngine`]
5//! (in-memory, for testing) as concrete implementations.
6
7use std::{collections::HashMap, error::Error, fmt::Display, io, path::PathBuf, sync::RwLock};
8
9use chrono::Utc;
10use tokio::{
11    fs::{self, File},
12    io::AsyncWriteExt,
13};
14
15use crate::{EmailMessage, EmailMetadata};
16
17/// Result type for storage operations.
18pub type StorageResult<T> = Result<T, StorageError>;
19
20/// Errors that can occur during storage operations.
21#[derive(Debug)]
22pub enum StorageError {
23    /// The message was not found.
24    NotFound,
25    /// An I/O error occurred.
26    Io(io::Error),
27    /// A storage engine error occurred.
28    EngineError(String),
29}
30
31impl Display for StorageError {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        match self {
34            StorageError::NotFound => write!(f, "Message not found"),
35            StorageError::Io(error) => write!(f, "I/O error: {error}"),
36            StorageError::EngineError(msg) => write!(f, "Storage error: {msg}"),
37        }
38    }
39}
40
41impl Error for StorageError {}
42
43impl From<io::Error> for StorageError {
44    fn from(error: io::Error) -> Self {
45        if error.kind() == io::ErrorKind::NotFound {
46            StorageError::NotFound
47        } else {
48            StorageError::Io(error)
49        }
50    }
51}
52
53/// Trait for storage engines.
54///
55/// Implementations of this trait provide different storage backends,
56/// such as filesystem, database, S3, etc.
57pub trait StorageEngine: Send + Sync {
58    /// Stores an email message for a recipient.
59    ///
60    /// Returns the message ID on success.
61    fn store(
62        &self,
63        message: &EmailMessage,
64    ) -> impl std::future::Future<Output = StorageResult<String>> + Send;
65
66    /// Retrieves an email message by ID for a user.
67    fn retrieve(
68        &self,
69        user: &str,
70        message_id: &str,
71    ) -> impl std::future::Future<Output = StorageResult<String>> + Send;
72
73    /// Lists all message IDs for a user.
74    fn list(
75        &self,
76        user: &str,
77    ) -> impl std::future::Future<Output = StorageResult<Vec<String>>> + Send;
78
79    /// Deletes a message by ID for a user.
80    fn delete(
81        &self,
82        user: &str,
83        message_id: &str,
84    ) -> impl std::future::Future<Output = StorageResult<()>> + Send;
85
86    /// Creates a mailbox for a user.
87    fn create_mailbox(
88        &self,
89        user: &str,
90        mailbox: &str,
91    ) -> impl std::future::Future<Output = StorageResult<()>> + Send;
92}
93
94/// Filesystem-based storage engine.
95///
96/// Stores emails as .eml files in a directory structure:
97/// `{base_path}/{user}/{message_id}.eml`
98#[derive(Debug, Clone)]
99pub struct FileStorageEngine {
100    /// Base path for storing email messages.
101    base_path: PathBuf,
102
103    /// Path to the metadata database.
104    db_path: PathBuf,
105
106    /// Whether to store metadata in the database.
107    store_metadata: bool,
108}
109
110impl FileStorageEngine {
111    /// Creates a new FileStorageEngine with the given base path.
112    pub fn new(base_path: PathBuf) -> Self {
113        let db_path = base_path.join("metadata.db");
114        Self {
115            base_path,
116            db_path,
117            store_metadata: true,
118        }
119    }
120
121    /// Creates a FileStorageEngine without metadata storage.
122    pub fn without_metadata(base_path: PathBuf) -> Self {
123        Self {
124            base_path,
125            db_path: PathBuf::new(),
126            store_metadata: false,
127        }
128    }
129
130    /// Returns the path to a user's mailbox directory.
131    fn user_path(&self, user: &str) -> PathBuf {
132        let safe_user = user.replace(|c: char| !c.is_ascii_alphanumeric(), "_");
133        self.base_path.join(safe_user)
134    }
135
136    /// Returns the path to a specific message file.
137    fn message_path(&self, user: &str, message_id: &str) -> PathBuf {
138        self.user_path(user).join(format!("{message_id}.eml"))
139    }
140}
141
142impl StorageEngine for FileStorageEngine {
143    async fn store(&self, message: &EmailMessage) -> StorageResult<String> {
144        let user_dir = self.user_path(&message.to);
145        fs::create_dir_all(&user_dir).await?;
146
147        let file_path = self.message_path(&message.to, &message.message_id);
148        let mut file = File::create(&file_path).await?;
149
150        // Add minimal headers if not a valid MIME message
151        if !message.has_headers() {
152            file.write_all(format!("From: {}\r\n", message.from).as_bytes())
153                .await?;
154            file.write_all(format!("To: {}\r\n", message.to).as_bytes())
155                .await?;
156            file.write_all(format!("Date: {}\r\n\r\n", Utc::now().to_rfc2822()).as_bytes())
157                .await?;
158        }
159        file.write_all(message.raw().as_bytes()).await?;
160        file.flush().await?;
161
162        // Store metadata if enabled
163        if self.store_metadata {
164            let metadata = EmailMetadata::new(
165                message.message_id.clone(),
166                message.from.clone(),
167                message.to.clone(),
168                message.subject().to_string(),
169                file_path,
170            );
171            metadata
172                .store_sqlite(&self.db_path)
173                .await
174                .map_err(|error| StorageError::EngineError(error.to_string()))?;
175        }
176
177        Ok(message.message_id.clone())
178    }
179
180    async fn retrieve(&self, user: &str, message_id: &str) -> StorageResult<String> {
181        let path = self.message_path(user, message_id);
182        let content = fs::read_to_string(path).await?;
183        Ok(content)
184    }
185
186    async fn list(&self, user: &str) -> StorageResult<Vec<String>> {
187        let user_dir = self.user_path(user);
188
189        // Return empty list if directory doesn't exist
190        if !user_dir.exists() {
191            return Ok(Vec::new());
192        }
193
194        let mut messages = Vec::new();
195        let mut entries = fs::read_dir(&user_dir).await?;
196
197        while let Some(entry) = entries.next_entry().await? {
198            let path = entry.path();
199            if let Some(ext) = path.extension() {
200                if ext == "eml" {
201                    if let Some(stem) = path.file_stem() {
202                        messages.push(stem.to_string_lossy().to_string());
203                    }
204                }
205            }
206        }
207
208        Ok(messages)
209    }
210
211    async fn delete(&self, user: &str, message_id: &str) -> StorageResult<()> {
212        let path = self.message_path(user, message_id);
213        fs::remove_file(path).await?;
214        Ok(())
215    }
216
217    async fn create_mailbox(&self, user: &str, mailbox: &str) -> StorageResult<()> {
218        let path = self.user_path(user).join(format!("{mailbox}.mbox"));
219        fs::create_dir_all(path).await?;
220        Ok(())
221    }
222}
223
224impl Default for FileStorageEngine {
225    fn default() -> Self {
226        Self::new(PathBuf::from("mailbox"))
227    }
228}
229
230/// In-memory storage engine.
231///
232/// Stores emails in memory using a HashMap structure.
233/// Useful for testing and development.
234///
235/// Structure: `HashMap<user, HashMap<message_id, EmailMessage>>`
236#[derive(Debug, Default)]
237pub struct MemoryStorageEngine {
238    /// Storage for messages: user -> (message_id -> message)
239    messages: RwLock<HashMap<String, HashMap<String, EmailMessage>>>,
240
241    /// Storage for mailboxes: user -> list of mailbox names
242    mailboxes: RwLock<HashMap<String, Vec<String>>>,
243}
244
245impl MemoryStorageEngine {
246    /// Creates a new empty MemoryStorageEngine.
247    pub fn new() -> Self {
248        Self {
249            messages: RwLock::new(HashMap::new()),
250            mailboxes: RwLock::new(HashMap::new()),
251        }
252    }
253
254    /// Returns the number of messages stored for a user.
255    pub fn message_count(&self, user: &str) -> usize {
256        let safe_user = Self::safe_user(user);
257        self.messages
258            .read()
259            .unwrap()
260            .get(&safe_user)
261            .map(|m| m.len())
262            .unwrap_or(0)
263    }
264
265    /// Returns the total number of messages stored across all users.
266    pub fn total_message_count(&self) -> usize {
267        self.messages
268            .read()
269            .unwrap()
270            .values()
271            .map(|m| m.len())
272            .sum()
273    }
274
275    /// Clears all messages and mailboxes.
276    pub fn clear(&self) {
277        self.messages.write().unwrap().clear();
278        self.mailboxes.write().unwrap().clear();
279    }
280
281    /// Normalizes a user identifier to a safe key.
282    fn safe_user(user: &str) -> String {
283        user.replace(|c: char| !c.is_ascii_alphanumeric(), "_")
284    }
285}
286
287impl StorageEngine for MemoryStorageEngine {
288    async fn store(&self, message: &EmailMessage) -> StorageResult<String> {
289        let safe_user = Self::safe_user(&message.to);
290        let mut messages = self.messages.write().unwrap();
291        let user_messages = messages.entry(safe_user).or_default();
292        user_messages.insert(message.message_id.clone(), message.clone());
293        Ok(message.message_id.clone())
294    }
295
296    async fn retrieve(&self, user: &str, message_id: &str) -> StorageResult<String> {
297        let safe_user = Self::safe_user(user);
298        let messages = self.messages.read().unwrap();
299        let user_messages = messages.get(&safe_user).ok_or(StorageError::NotFound)?;
300        let message = user_messages
301            .get(message_id)
302            .ok_or(StorageError::NotFound)?;
303        Ok(message.raw().to_string())
304    }
305
306    async fn list(&self, user: &str) -> StorageResult<Vec<String>> {
307        let safe_user = Self::safe_user(user);
308        let messages = self.messages.read().unwrap();
309        match messages.get(&safe_user) {
310            Some(user_messages) => Ok(user_messages.keys().cloned().collect()),
311            None => Ok(Vec::new()),
312        }
313    }
314
315    async fn delete(&self, user: &str, message_id: &str) -> StorageResult<()> {
316        let safe_user = Self::safe_user(user);
317        let mut messages = self.messages.write().unwrap();
318        let user_messages = messages.get_mut(&safe_user).ok_or(StorageError::NotFound)?;
319        user_messages
320            .remove(message_id)
321            .ok_or(StorageError::NotFound)?;
322        Ok(())
323    }
324
325    async fn create_mailbox(&self, user: &str, mailbox: &str) -> StorageResult<()> {
326        let safe_user = Self::safe_user(user);
327        let mut mailboxes = self.mailboxes.write().unwrap();
328        let user_mailboxes = mailboxes.entry(safe_user).or_default();
329        if !user_mailboxes.contains(&mailbox.to_string()) {
330            user_mailboxes.push(mailbox.to_string());
331        }
332        Ok(())
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use tempfile::TempDir;
339
340    use super::*;
341
342    #[tokio::test]
343    async fn test_file_storage_store_and_retrieve() {
344        let temp_dir = TempDir::new().unwrap();
345        let engine = FileStorageEngine::without_metadata(temp_dir.path().to_path_buf());
346
347        let message = EmailMessage::new(
348            "sender@example.com".to_string(),
349            "recipient@example.com".to_string(),
350            "Hello, World!".to_string(),
351        );
352
353        let message_id = engine.store(&message).await.unwrap();
354        let content = engine
355            .retrieve("recipient@example.com", &message_id)
356            .await
357            .unwrap();
358
359        assert!(content.contains("Hello, World!"));
360        assert!(content.contains("From: sender@example.com"));
361    }
362
363    #[tokio::test]
364    async fn test_file_storage_list() {
365        let temp_dir = TempDir::new().unwrap();
366        let engine = FileStorageEngine::without_metadata(temp_dir.path().to_path_buf());
367
368        let msg1 = EmailMessage::new(
369            "sender@example.com".to_string(),
370            "user@example.com".to_string(),
371            "Message 1".to_string(),
372        );
373        let msg2 = EmailMessage::new(
374            "sender@example.com".to_string(),
375            "user@example.com".to_string(),
376            "Message 2".to_string(),
377        );
378
379        let id1 = engine.store(&msg1).await.unwrap();
380        let id2 = engine.store(&msg2).await.unwrap();
381
382        let messages = engine.list("user@example.com").await.unwrap();
383        assert_eq!(messages.len(), 2);
384        assert!(messages.contains(&id1));
385        assert!(messages.contains(&id2));
386    }
387
388    #[tokio::test]
389    async fn test_file_storage_delete() {
390        let temp_dir = TempDir::new().unwrap();
391        let engine = FileStorageEngine::without_metadata(temp_dir.path().to_path_buf());
392
393        let message = EmailMessage::new(
394            "sender@example.com".to_string(),
395            "recipient@example.com".to_string(),
396            "Test message".to_string(),
397        );
398
399        let message_id = engine.store(&message).await.unwrap();
400        assert!(engine
401            .retrieve("recipient@example.com", &message_id)
402            .await
403            .is_ok());
404
405        engine
406            .delete("recipient@example.com", &message_id)
407            .await
408            .unwrap();
409        assert!(matches!(
410            engine.retrieve("recipient@example.com", &message_id).await,
411            Err(StorageError::NotFound)
412        ));
413    }
414
415    #[tokio::test]
416    async fn test_file_storage_list_empty() {
417        let temp_dir = TempDir::new().unwrap();
418        let engine = FileStorageEngine::without_metadata(temp_dir.path().to_path_buf());
419
420        let messages = engine.list("nonexistent@example.com").await.unwrap();
421        assert!(messages.is_empty());
422    }
423
424    #[tokio::test]
425    async fn test_file_storage_create_mailbox() {
426        let temp_dir = TempDir::new().unwrap();
427        let engine = FileStorageEngine::without_metadata(temp_dir.path().to_path_buf());
428
429        engine
430            .create_mailbox("user@example.com", "Drafts")
431            .await
432            .unwrap();
433
434        let mailbox_path = temp_dir.path().join("user_example_com").join("Drafts.mbox");
435        assert!(mailbox_path.exists());
436    }
437
438    #[tokio::test]
439    async fn test_storage_error_display() {
440        assert_eq!(StorageError::NotFound.to_string(), "Message not found");
441        assert_eq!(
442            StorageError::EngineError("test".to_string()).to_string(),
443            "Storage error: test"
444        );
445    }
446
447    // MemoryStorageEngine tests
448
449    #[tokio::test]
450    async fn test_memory_storage_new() {
451        let engine = MemoryStorageEngine::new();
452        assert_eq!(engine.total_message_count(), 0);
453    }
454
455    #[tokio::test]
456    async fn test_memory_storage_store_and_retrieve() {
457        let engine = MemoryStorageEngine::new();
458
459        let message = EmailMessage::new(
460            "sender@example.com".to_string(),
461            "recipient@example.com".to_string(),
462            "Hello, World!".to_string(),
463        );
464
465        let message_id = engine.store(&message).await.unwrap();
466        let content = engine
467            .retrieve("recipient@example.com", &message_id)
468            .await
469            .unwrap();
470
471        assert_eq!(content, "Hello, World!");
472        assert_eq!(engine.message_count("recipient@example.com"), 1);
473    }
474
475    #[tokio::test]
476    async fn test_memory_storage_store_multiple_users() {
477        let engine = MemoryStorageEngine::new();
478
479        let msg1 = EmailMessage::new(
480            "sender@example.com".to_string(),
481            "user1@example.com".to_string(),
482            "Message for user1".to_string(),
483        );
484        let msg2 = EmailMessage::new(
485            "sender@example.com".to_string(),
486            "user2@example.com".to_string(),
487            "Message for user2".to_string(),
488        );
489
490        engine.store(&msg1).await.unwrap();
491        engine.store(&msg2).await.unwrap();
492
493        assert_eq!(engine.message_count("user1@example.com"), 1);
494        assert_eq!(engine.message_count("user2@example.com"), 1);
495        assert_eq!(engine.total_message_count(), 2);
496    }
497
498    #[tokio::test]
499    async fn test_memory_storage_list() {
500        let engine = MemoryStorageEngine::new();
501
502        let msg1 = EmailMessage::new(
503            "sender@example.com".to_string(),
504            "user@example.com".to_string(),
505            "Message 1".to_string(),
506        );
507        let msg2 = EmailMessage::new(
508            "sender@example.com".to_string(),
509            "user@example.com".to_string(),
510            "Message 2".to_string(),
511        );
512
513        let id1 = engine.store(&msg1).await.unwrap();
514        let id2 = engine.store(&msg2).await.unwrap();
515
516        let messages = engine.list("user@example.com").await.unwrap();
517        assert_eq!(messages.len(), 2);
518        assert!(messages.contains(&id1));
519        assert!(messages.contains(&id2));
520    }
521
522    #[tokio::test]
523    async fn test_memory_storage_list_empty() {
524        let engine = MemoryStorageEngine::new();
525
526        let messages = engine.list("nonexistent@example.com").await.unwrap();
527        assert!(messages.is_empty());
528    }
529
530    #[tokio::test]
531    async fn test_memory_storage_delete() {
532        let engine = MemoryStorageEngine::new();
533
534        let message = EmailMessage::new(
535            "sender@example.com".to_string(),
536            "recipient@example.com".to_string(),
537            "Test message".to_string(),
538        );
539
540        let message_id = engine.store(&message).await.unwrap();
541        assert_eq!(engine.message_count("recipient@example.com"), 1);
542
543        engine
544            .delete("recipient@example.com", &message_id)
545            .await
546            .unwrap();
547        assert_eq!(engine.message_count("recipient@example.com"), 0);
548
549        // Verify retrieve fails after delete
550        assert!(matches!(
551            engine.retrieve("recipient@example.com", &message_id).await,
552            Err(StorageError::NotFound)
553        ));
554    }
555
556    #[tokio::test]
557    async fn test_memory_storage_delete_nonexistent() {
558        let engine = MemoryStorageEngine::new();
559
560        // Delete from nonexistent user
561        let result = engine.delete("user@example.com", "fake-id").await;
562        assert!(matches!(result, Err(StorageError::NotFound)));
563
564        // Store a message, then try to delete wrong ID
565        let message = EmailMessage::new(
566            "sender@example.com".to_string(),
567            "user@example.com".to_string(),
568            "Test".to_string(),
569        );
570        engine.store(&message).await.unwrap();
571
572        let result = engine.delete("user@example.com", "wrong-id").await;
573        assert!(matches!(result, Err(StorageError::NotFound)));
574    }
575
576    #[tokio::test]
577    async fn test_memory_storage_retrieve_nonexistent() {
578        let engine = MemoryStorageEngine::new();
579
580        // Retrieve from nonexistent user
581        let result = engine.retrieve("user@example.com", "fake-id").await;
582        assert!(matches!(result, Err(StorageError::NotFound)));
583
584        // Store a message, then try to retrieve wrong ID
585        let message = EmailMessage::new(
586            "sender@example.com".to_string(),
587            "user@example.com".to_string(),
588            "Test".to_string(),
589        );
590        engine.store(&message).await.unwrap();
591
592        let result = engine.retrieve("user@example.com", "wrong-id").await;
593        assert!(matches!(result, Err(StorageError::NotFound)));
594    }
595
596    #[tokio::test]
597    async fn test_memory_storage_create_mailbox() {
598        let engine = MemoryStorageEngine::new();
599
600        engine
601            .create_mailbox("user@example.com", "Inbox")
602            .await
603            .unwrap();
604        engine
605            .create_mailbox("user@example.com", "Drafts")
606            .await
607            .unwrap();
608
609        // Creating the same mailbox again should not fail
610        engine
611            .create_mailbox("user@example.com", "Inbox")
612            .await
613            .unwrap();
614    }
615
616    #[tokio::test]
617    async fn test_memory_storage_clear() {
618        let engine = MemoryStorageEngine::new();
619
620        let msg1 = EmailMessage::new(
621            "sender@example.com".to_string(),
622            "user1@example.com".to_string(),
623            "Message 1".to_string(),
624        );
625        let msg2 = EmailMessage::new(
626            "sender@example.com".to_string(),
627            "user2@example.com".to_string(),
628            "Message 2".to_string(),
629        );
630
631        engine.store(&msg1).await.unwrap();
632        engine.store(&msg2).await.unwrap();
633        assert_eq!(engine.total_message_count(), 2);
634
635        engine.clear();
636        assert_eq!(engine.total_message_count(), 0);
637    }
638
639    #[tokio::test]
640    async fn test_memory_storage_with_id() {
641        let engine = MemoryStorageEngine::new();
642
643        let message = EmailMessage::with_id(
644            "custom-id-123".to_string(),
645            "sender@example.com".to_string(),
646            "recipient@example.com".to_string(),
647            "Test Subject".to_string(),
648            "Test body content".to_string(),
649        );
650
651        let message_id = engine.store(&message).await.unwrap();
652        assert_eq!(message_id, "custom-id-123");
653
654        let content = engine
655            .retrieve("recipient@example.com", "custom-id-123")
656            .await
657            .unwrap();
658        assert!(content.contains("Test body content"));
659        assert!(content.contains("Subject: Test Subject"));
660    }
661
662    #[tokio::test]
663    async fn test_memory_storage_special_characters_in_user() {
664        let engine = MemoryStorageEngine::new();
665
666        let message = EmailMessage::new(
667            "sender@example.com".to_string(),
668            "user+tag@example.com".to_string(),
669            "Test message".to_string(),
670        );
671
672        let message_id = engine.store(&message).await.unwrap();
673
674        // Should be able to retrieve with the same user string
675        let content = engine
676            .retrieve("user+tag@example.com", &message_id)
677            .await
678            .unwrap();
679        assert_eq!(content, "Test message");
680
681        // The internal safe_user normalization should handle special chars
682        assert_eq!(engine.message_count("user+tag@example.com"), 1);
683    }
684}