Skip to main content

rusmes_storage/backends/
filesystem.rs

1//! Filesystem-based storage backend using maildir format
2
3use crate::traits::{MailboxStore, MessageStore, MetadataStore, StorageBackend};
4use crate::types::{
5    Mailbox, MailboxCounters, MailboxId, MailboxPath, MessageFlags, MessageMetadata, Quota,
6    SearchCriteria,
7};
8use async_trait::async_trait;
9use rusmes_proto::{Mail, MessageId, Username};
10use std::collections::{HashMap, HashSet};
11use std::path::PathBuf;
12use std::str::FromStr;
13use std::sync::{Arc, RwLock};
14use std::time::{SystemTime, UNIX_EPOCH};
15
16/// Filesystem storage backend
17pub struct FilesystemBackend {
18    base_path: PathBuf,
19    mailboxes: Arc<RwLock<HashMap<MailboxId, Mailbox>>>,
20    messages: Arc<RwLock<HashMap<MessageId, Mail>>>,
21    quotas: Arc<RwLock<HashMap<Username, Quota>>>,
22    subscriptions: Arc<RwLock<HashMap<Username, HashSet<String>>>>,
23    hostname: String,
24    pid: u32,
25    delivery_counter: Arc<RwLock<u64>>,
26}
27
28impl FilesystemBackend {
29    /// Create a new filesystem backend
30    pub fn new(base_path: impl Into<PathBuf>) -> Self {
31        let hostname = hostname::get()
32            .ok()
33            .and_then(|h| h.into_string().ok())
34            .unwrap_or_else(|| "localhost".to_string());
35
36        let pid = std::process::id();
37
38        let base_path_buf = base_path.into();
39
40        // Load existing mailbox metadata from disk
41        let mailboxes = Self::load_all_mailbox_metadata(&base_path_buf).unwrap_or_else(|e| {
42            tracing::warn!("Failed to load mailbox metadata: {}", e);
43            HashMap::new()
44        });
45
46        Self {
47            base_path: base_path_buf,
48            mailboxes: Arc::new(RwLock::new(mailboxes)),
49            messages: Arc::new(RwLock::new(HashMap::new())),
50            quotas: Arc::new(RwLock::new(HashMap::new())),
51            subscriptions: Arc::new(RwLock::new(HashMap::new())),
52            hostname,
53            pid,
54            delivery_counter: Arc::new(RwLock::new(0)),
55        }
56    }
57
58    /// Get the path for a mailbox
59    #[allow(dead_code)]
60    fn mailbox_path(&self, mailbox_id: &MailboxId) -> PathBuf {
61        self.base_path
62            .join("mailboxes")
63            .join(mailbox_id.to_string())
64    }
65
66    /// Get the metadata file path for a user
67    fn metadata_file_path(base_path: &std::path::Path, user: &Username) -> PathBuf {
68        base_path
69            .join("users")
70            .join(user.as_str())
71            .join("mailboxes.json")
72    }
73
74    /// Load mailbox metadata for a specific user from disk
75    fn load_user_mailbox_metadata(
76        base_path: &std::path::Path,
77        user: &Username,
78    ) -> anyhow::Result<HashMap<MailboxId, Mailbox>> {
79        let metadata_file = Self::metadata_file_path(base_path, user);
80
81        if !metadata_file.exists() {
82            return Ok(HashMap::new());
83        }
84
85        let content = std::fs::read_to_string(&metadata_file)?;
86        let mailboxes: Vec<Mailbox> = serde_json::from_str(&content)?;
87
88        let mut map = HashMap::new();
89        for mailbox in mailboxes {
90            map.insert(*mailbox.id(), mailbox);
91        }
92
93        tracing::info!("Loaded {} mailboxes for user {}", map.len(), user);
94        Ok(map)
95    }
96
97    /// Load all mailbox metadata from disk (all users)
98    fn load_all_mailbox_metadata(
99        base_path: &std::path::Path,
100    ) -> anyhow::Result<HashMap<MailboxId, Mailbox>> {
101        let users_dir = base_path.join("users");
102
103        if !users_dir.exists() {
104            return Ok(HashMap::new());
105        }
106
107        let mut all_mailboxes = HashMap::new();
108
109        // Iterate through all user directories
110        for entry in std::fs::read_dir(&users_dir)? {
111            let entry = entry?;
112            let path = entry.path();
113
114            if path.is_dir() {
115                if let Some(username) = path.file_name().and_then(|n| n.to_str()) {
116                    if let Ok(user) = username.parse::<Username>() {
117                        match Self::load_user_mailbox_metadata(base_path, &user) {
118                            Ok(mailboxes) => {
119                                all_mailboxes.extend(mailboxes);
120                            }
121                            Err(e) => {
122                                tracing::warn!("Failed to load mailboxes for user {}: {}", user, e);
123                            }
124                        }
125                    }
126                }
127            }
128        }
129
130        tracing::info!("Loaded {} total mailboxes from disk", all_mailboxes.len());
131        Ok(all_mailboxes)
132    }
133
134    /// Save mailbox metadata for a specific user to disk
135    fn save_user_mailbox_metadata(
136        base_path: &std::path::Path,
137        user: &Username,
138        mailboxes: &[Mailbox],
139    ) -> anyhow::Result<()> {
140        let metadata_file = Self::metadata_file_path(base_path, user);
141
142        // Create parent directory if it doesn't exist
143        if let Some(parent) = metadata_file.parent() {
144            std::fs::create_dir_all(parent)?;
145        }
146
147        // Serialize mailboxes to JSON
148        let json = serde_json::to_string_pretty(mailboxes)?;
149
150        // Write to disk
151        std::fs::write(&metadata_file, json)?;
152
153        tracing::debug!("Saved {} mailboxes for user {}", mailboxes.len(), user);
154        Ok(())
155    }
156}
157
158/// Maildir filename utilities
159struct MaildirFilename;
160
161impl MaildirFilename {
162    /// Generate a unique maildir filename
163    /// Format: timestamp.M<microseconds>P<pid>Q<counter>.<hostname>
164    fn generate(hostname: &str, pid: u32, counter: u64) -> String {
165        let now = SystemTime::now()
166            .duration_since(UNIX_EPOCH)
167            .unwrap_or_default();
168        let secs = now.as_secs();
169        let micros = now.subsec_micros();
170
171        format!("{}.M{}P{}Q{}.{}", secs, micros, pid, counter, hostname)
172    }
173
174    /// Encode flags into maildir format
175    /// Format: :2,<flags> where flags are sorted: DFPRST (and 'd' for draft in some implementations)
176    /// D=Draft, F=Flagged, P=Passed, R=Replied, S=Seen, T=Trashed
177    fn encode_flags(flags: &MessageFlags) -> String {
178        let mut flag_chars = String::new();
179
180        if flags.is_draft() {
181            flag_chars.push('D');
182        }
183        if flags.is_flagged() {
184            flag_chars.push('F');
185        }
186        // P (Passed) - forwarded/redirected - not in standard IMAP flags
187        if flags.is_answered() {
188            flag_chars.push('R');
189        }
190        if flags.is_seen() {
191            flag_chars.push('S');
192        }
193        if flags.is_deleted() {
194            flag_chars.push('T');
195        }
196
197        if flag_chars.is_empty() {
198            ":2,".to_string()
199        } else {
200            format!(":2,{}", flag_chars)
201        }
202    }
203
204    /// Decode flags from maildir filename
205    fn decode_flags(filename: &str) -> MessageFlags {
206        let mut flags = MessageFlags::new();
207
208        if let Some(flag_part) = filename.split(":2,").nth(1) {
209            for ch in flag_part.chars() {
210                match ch {
211                    'D' => flags.set_draft(true),
212                    'F' => flags.set_flagged(true),
213                    'R' => flags.set_answered(true),
214                    'S' => flags.set_seen(true),
215                    'T' => flags.set_deleted(true),
216                    _ => {}
217                }
218            }
219        }
220
221        flags
222    }
223
224    /// Extract the base filename without flags
225    fn base_name(filename: &str) -> &str {
226        filename.split(":2,").next().unwrap_or(filename)
227    }
228
229    /// Update flags in a filename
230    fn with_flags(filename: &str, flags: &MessageFlags) -> String {
231        let base = Self::base_name(filename);
232        format!("{}{}", base, Self::encode_flags(flags))
233    }
234}
235
236impl StorageBackend for FilesystemBackend {
237    fn mailbox_store(&self) -> Arc<dyn MailboxStore> {
238        Arc::new(FilesystemMailboxStore {
239            base_path: self.base_path.clone(),
240            mailboxes: self.mailboxes.clone(),
241            subscriptions: self.subscriptions.clone(),
242        })
243    }
244
245    fn message_store(&self) -> Arc<dyn MessageStore> {
246        Arc::new(FilesystemMessageStore {
247            base_path: self.base_path.clone(),
248            messages: self.messages.clone(),
249            mailboxes: self.mailboxes.clone(),
250            quotas: self.quotas.clone(),
251            hostname: self.hostname.clone(),
252            pid: self.pid,
253            delivery_counter: self.delivery_counter.clone(),
254        })
255    }
256
257    fn metadata_store(&self) -> Arc<dyn MetadataStore> {
258        Arc::new(FilesystemMetadataStore {
259            base_path: self.base_path.clone(),
260            quotas: self.quotas.clone(),
261        })
262    }
263}
264
265/// Filesystem mailbox store
266struct FilesystemMailboxStore {
267    base_path: PathBuf,
268    mailboxes: Arc<RwLock<HashMap<MailboxId, Mailbox>>>,
269    subscriptions: Arc<RwLock<HashMap<Username, HashSet<String>>>>,
270}
271
272impl FilesystemMailboxStore {
273    /// Persist mailbox metadata for a user to disk
274    fn persist_user_mailboxes(&self, user: &Username) -> anyhow::Result<()> {
275        let mailboxes = self
276            .mailboxes
277            .read()
278            .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?;
279        let user_mailboxes: Vec<Mailbox> = mailboxes
280            .values()
281            .filter(|m| m.path().user() == user)
282            .cloned()
283            .collect();
284
285        FilesystemBackend::save_user_mailbox_metadata(&self.base_path, user, &user_mailboxes)
286    }
287}
288
289#[async_trait]
290impl MailboxStore for FilesystemMailboxStore {
291    async fn create_mailbox(&self, path: &MailboxPath) -> anyhow::Result<MailboxId> {
292        let mailbox = Mailbox::new(path.clone());
293        let id = *mailbox.id();
294        let user = path.user().clone();
295
296        // Create directory
297        let mailbox_dir = self.base_path.join("mailboxes").join(id.to_string());
298        tokio::fs::create_dir_all(&mailbox_dir).await?;
299
300        // Create maildir subdirectories
301        tokio::fs::create_dir_all(mailbox_dir.join("cur")).await?;
302        tokio::fs::create_dir_all(mailbox_dir.join("new")).await?;
303        tokio::fs::create_dir_all(mailbox_dir.join("tmp")).await?;
304
305        // Store in memory
306        self.mailboxes
307            .write()
308            .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?
309            .insert(id, mailbox);
310
311        // Persist metadata to disk
312        self.persist_user_mailboxes(&user)?;
313
314        Ok(id)
315    }
316
317    async fn delete_mailbox(&self, id: &MailboxId) -> anyhow::Result<()> {
318        // Get user before deleting from memory
319        let user = {
320            let mailboxes = self
321                .mailboxes
322                .read()
323                .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?;
324            mailboxes
325                .get(id)
326                .map(|m| m.path().user().clone())
327                .ok_or_else(|| anyhow::anyhow!("Mailbox not found"))?
328        };
329
330        let mailbox_dir = self.base_path.join("mailboxes").join(id.to_string());
331        tokio::fs::remove_dir_all(mailbox_dir).await?;
332
333        self.mailboxes
334            .write()
335            .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?
336            .remove(id);
337
338        // Persist updated metadata to disk
339        self.persist_user_mailboxes(&user)?;
340
341        Ok(())
342    }
343
344    async fn rename_mailbox(&self, id: &MailboxId, new_path: &MailboxPath) -> anyhow::Result<()> {
345        let user = new_path.user().clone();
346
347        let mut mailboxes = self
348            .mailboxes
349            .write()
350            .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?;
351        if let Some(mailbox) = mailboxes.get_mut(id) {
352            // Update the mailbox path in memory while preserving the ID
353            mailbox.set_path(new_path.clone());
354            drop(mailboxes); // Release the lock before persisting
355
356            // Persist updated metadata to disk
357            self.persist_user_mailboxes(&user)?;
358
359            Ok(())
360        } else {
361            Err(anyhow::anyhow!("Mailbox not found"))
362        }
363    }
364
365    async fn get_mailbox(&self, id: &MailboxId) -> anyhow::Result<Option<Mailbox>> {
366        let mailboxes = self
367            .mailboxes
368            .read()
369            .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?;
370        Ok(mailboxes.get(id).cloned())
371    }
372
373    async fn list_mailboxes(&self, user: &Username) -> anyhow::Result<Vec<Mailbox>> {
374        let mailboxes: Vec<Mailbox> = self
375            .mailboxes
376            .read()
377            .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?
378            .values()
379            .filter(|m| m.path().user() == user)
380            .cloned()
381            .collect();
382        Ok(mailboxes)
383    }
384
385    async fn get_user_inbox(&self, user: &Username) -> anyhow::Result<Option<MailboxId>> {
386        let mailbox_id = self
387            .mailboxes
388            .read()
389            .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?
390            .values()
391            .find(|m| {
392                m.path().user() == user
393                    && m.path().name().map(|name| name == "INBOX").unwrap_or(false)
394            })
395            .map(|m| *m.id());
396
397        Ok(mailbox_id)
398    }
399
400    async fn subscribe_mailbox(&self, user: &Username, mailbox_name: String) -> anyhow::Result<()> {
401        let user_subs = {
402            let mut subs = self
403                .subscriptions
404                .write()
405                .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?;
406            subs.entry(user.clone()).or_default().insert(mailbox_name);
407
408            subs.get(user)
409                .map(|s| s.iter().cloned().collect::<Vec<String>>())
410                .unwrap_or_default()
411        };
412
413        // Persist subscriptions to disk
414        let subs_dir = self.base_path.join("users").join(user.as_str());
415        tokio::fs::create_dir_all(&subs_dir).await?;
416        let subs_file = subs_dir.join("subscriptions");
417
418        let content = user_subs.join("\n");
419        tokio::fs::write(subs_file, content).await?;
420
421        Ok(())
422    }
423
424    async fn unsubscribe_mailbox(&self, user: &Username, mailbox_name: &str) -> anyhow::Result<()> {
425        let user_subs = {
426            let mut subs = self
427                .subscriptions
428                .write()
429                .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?;
430            if let Some(user_subs) = subs.get_mut(user) {
431                user_subs.remove(mailbox_name);
432            }
433
434            subs.get(user)
435                .map(|s| s.iter().cloned().collect::<Vec<String>>())
436                .unwrap_or_default()
437        };
438
439        // Persist subscriptions to disk
440        let subs_dir = self.base_path.join("users").join(user.as_str());
441        tokio::fs::create_dir_all(&subs_dir).await?;
442        let subs_file = subs_dir.join("subscriptions");
443
444        let content = user_subs.join("\n");
445        tokio::fs::write(subs_file, content).await?;
446
447        Ok(())
448    }
449
450    async fn list_subscriptions(&self, user: &Username) -> anyhow::Result<Vec<String>> {
451        // Try to load from disk first
452        let subs_file = self
453            .base_path
454            .join("users")
455            .join(user.as_str())
456            .join("subscriptions");
457
458        if tokio::fs::try_exists(&subs_file).await.unwrap_or(false) {
459            let content = tokio::fs::read_to_string(&subs_file).await?;
460            let subs: Vec<String> = content.lines().map(|s| s.to_string()).collect();
461
462            // Update in-memory cache
463            let mut cache = self
464                .subscriptions
465                .write()
466                .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?;
467            cache.insert(user.clone(), subs.iter().cloned().collect());
468
469            Ok(subs)
470        } else {
471            // Fall back to in-memory
472            let subs = self
473                .subscriptions
474                .read()
475                .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?;
476            Ok(subs
477                .get(user)
478                .map(|s| s.iter().cloned().collect())
479                .unwrap_or_default())
480        }
481    }
482}
483
484/// Filesystem message store
485struct FilesystemMessageStore {
486    base_path: PathBuf,
487    messages: Arc<RwLock<HashMap<MessageId, Mail>>>,
488    mailboxes: Arc<RwLock<HashMap<MailboxId, Mailbox>>>,
489    quotas: Arc<RwLock<HashMap<Username, Quota>>>,
490    hostname: String,
491    pid: u32,
492    delivery_counter: Arc<RwLock<u64>>,
493}
494
495#[async_trait]
496impl MessageStore for FilesystemMessageStore {
497    async fn append_message(
498        &self,
499        mailbox_id: &MailboxId,
500        message: Mail,
501    ) -> anyhow::Result<MessageMetadata> {
502        let message_id = *message.message_id();
503        let message_size = message.size();
504
505        // Get the mailbox to determine the user
506        let user = {
507            let mailboxes = self
508                .mailboxes
509                .read()
510                .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?;
511            let mailbox = mailboxes
512                .get(mailbox_id)
513                .ok_or_else(|| anyhow::anyhow!("Mailbox not found"))?;
514            mailbox.path().user().clone()
515        };
516
517        // Check quota before appending
518        let quota = {
519            let quotas = self
520                .quotas
521                .read()
522                .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?;
523            quotas
524                .get(&user)
525                .cloned()
526                .unwrap_or(Quota::new(0, 1024 * 1024 * 1024)) // Default 1GB
527        };
528
529        // Check if adding this message would exceed the quota
530        if quota.used + (message_size as u64) > quota.limit {
531            return Err(anyhow::anyhow!("Quota exceeded: cannot append message"));
532        }
533
534        // Get unique counter for this delivery
535        let counter = {
536            let mut c = self
537                .delivery_counter
538                .write()
539                .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?;
540            *c += 1;
541            *c
542        };
543
544        // Generate unique maildir filename
545        let filename = MaildirFilename::generate(&self.hostname, self.pid, counter);
546
547        let mailbox_dir = self
548            .base_path
549            .join("mailboxes")
550            .join(mailbox_id.to_string());
551
552        // Step 1: Write to tmp/ directory (atomic delivery part 1)
553        let tmp_path = mailbox_dir.join("tmp").join(&filename);
554        tokio::fs::create_dir_all(mailbox_dir.join("tmp")).await?;
555
556        // Serialize message to disk
557        let message_data = serialize_message_to_bytes(&message)?;
558        tokio::fs::write(&tmp_path, &message_data).await?;
559
560        // Step 2: Rename to new/ directory (atomic delivery part 2)
561        // This is atomic on most filesystems
562        let new_path = mailbox_dir.join("new").join(&filename);
563        tokio::fs::create_dir_all(mailbox_dir.join("new")).await?;
564        tokio::fs::rename(&tmp_path, &new_path).await?;
565
566        // Store message in memory
567        self.messages
568            .write()
569            .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?
570            .insert(message_id, message.clone());
571
572        // Update quota after successful append
573        {
574            let mut quotas = self
575                .quotas
576                .write()
577                .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?;
578            let user_quota = quotas
579                .entry(user)
580                .or_insert(Quota::new(0, 1024 * 1024 * 1024));
581            user_quota.used += message_size as u64;
582        }
583
584        // Create metadata
585        let metadata = MessageMetadata::new(
586            message_id,
587            *mailbox_id,
588            1, // UID would be properly generated from mailbox state
589            MessageFlags::new(),
590            message.size(),
591        );
592
593        Ok(metadata)
594    }
595
596    async fn get_message(&self, message_id: &MessageId) -> anyhow::Result<Option<Mail>> {
597        // First check in-memory cache
598        if let Some(mail) = self
599            .messages
600            .read()
601            .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?
602            .get(message_id)
603            .cloned()
604        {
605            return Ok(Some(mail));
606        }
607
608        // If not in cache, search on disk
609        // We need to scan all mailboxes since MessageId doesn't contain mailbox info
610        tracing::debug!("Message {} not in cache, scanning disk", message_id);
611
612        let mailboxes_dir = self.base_path.join("mailboxes");
613        if !tokio::fs::try_exists(&mailboxes_dir).await.unwrap_or(false) {
614            tracing::debug!("Mailboxes directory doesn't exist: {:?}", mailboxes_dir);
615            return Ok(None);
616        }
617
618        // Scan all mailbox directories
619        let mut entries = tokio::fs::read_dir(&mailboxes_dir).await?;
620        while let Some(entry) = entries.next_entry().await? {
621            if let Ok(file_type) = entry.file_type().await {
622                if file_type.is_dir() {
623                    // This is a mailbox directory - check its messages
624                    let mailbox_dir = entry.path();
625
626                    // Try both new/ and cur/ subdirectories
627                    for subdir in ["new", "cur"] {
628                        let msg_dir = mailbox_dir.join(subdir);
629                        if !tokio::fs::try_exists(&msg_dir).await.unwrap_or(false) {
630                            continue;
631                        }
632
633                        let mut msg_entries = tokio::fs::read_dir(&msg_dir).await?;
634                        while let Some(msg_entry) = msg_entries.next_entry().await? {
635                            if let Ok(msg_file_type) = msg_entry.file_type().await {
636                                if msg_file_type.is_file() {
637                                    let file_path = msg_entry.path();
638
639                                    // Read and parse the message to get its MessageId
640                                    match tokio::fs::read(&file_path).await {
641                                        Ok(data) => {
642                                            match rusmes_proto::MimeMessage::parse_from_bytes(&data)
643                                            {
644                                                Ok(mime_message) => {
645                                                    // Extract the stored MessageId from X-Rusmes-Message-Id header
646                                                    let stored_message_id = mime_message
647                                                        .headers()
648                                                        .get_first("x-rusmes-message-id")
649                                                        .and_then(|id_str| {
650                                                            let trimmed = id_str.trim();
651                                                            uuid::Uuid::from_str(trimmed)
652                                                                .ok()
653                                                                .map(MessageId::from_uuid)
654                                                        });
655
656                                                    // Check if this is the message we're looking for
657                                                    if let Some(stored_id) = stored_message_id {
658                                                        if &stored_id == message_id {
659                                                            tracing::debug!(
660                                                                "Found message {} on disk at {:?}",
661                                                                message_id,
662                                                                file_path
663                                                            );
664
665                                                            // Create a Mail object with the correct MessageId
666                                                            let mail =
667                                                                rusmes_proto::Mail::with_message_id(
668                                                                    None,
669                                                                    Vec::new(),
670                                                                    mime_message,
671                                                                    None,
672                                                                    None,
673                                                                    stored_id,
674                                                                );
675
676                                                            // Cache it for future lookups
677                                                            if let Ok(mut msgs) =
678                                                                self.messages.write()
679                                                            {
680                                                                msgs.insert(
681                                                                    *message_id,
682                                                                    mail.clone(),
683                                                                );
684                                                            }
685
686                                                            return Ok(Some(mail));
687                                                        }
688                                                    }
689                                                }
690                                                Err(e) => {
691                                                    tracing::warn!(
692                                                        "Failed to parse message file {:?}: {}",
693                                                        file_path,
694                                                        e
695                                                    );
696                                                }
697                                            }
698                                        }
699                                        Err(e) => {
700                                            tracing::warn!(
701                                                "Failed to read message file {:?}: {}",
702                                                file_path,
703                                                e
704                                            );
705                                        }
706                                    }
707                                }
708                            }
709                        }
710                    }
711                }
712            }
713        }
714
715        tracing::debug!("Message {} not found on disk", message_id);
716        Ok(None)
717    }
718
719    async fn delete_messages(&self, message_ids: &[MessageId]) -> anyhow::Result<()> {
720        let mut messages = self
721            .messages
722            .write()
723            .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?;
724        for id in message_ids {
725            messages.remove(id);
726        }
727        Ok(())
728    }
729
730    async fn set_flags(
731        &self,
732        message_ids: &[MessageId],
733        flags: MessageFlags,
734    ) -> anyhow::Result<()> {
735        // For each message, update its filename to reflect the new flags
736        for message_id in message_ids {
737            let messages = self
738                .messages
739                .read()
740                .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?;
741            if messages.get(message_id).is_some() {
742                // In a full implementation, we would:
743                // 1. Find the file in cur/ or new/
744                // 2. Rename it with the new flags encoding
745                // 3. Update in-memory metadata
746                // For now, just store the flags intention
747                tracing::debug!("Setting flags {:?} for message {:?}", flags, message_id);
748            }
749        }
750        Ok(())
751    }
752
753    async fn search(
754        &self,
755        mailbox_id: &MailboxId,
756        criteria: SearchCriteria,
757    ) -> anyhow::Result<Vec<MessageId>> {
758        // Get all messages in the mailbox
759        let messages = self.get_mailbox_messages(mailbox_id).await?;
760
761        // Filter messages based on criteria
762        let mut results = Vec::new();
763        for metadata in messages {
764            if matches_criteria_helper(self, &metadata, &criteria).await? {
765                results.push(*metadata.message_id());
766            }
767        }
768        Ok(results)
769    }
770
771    async fn copy_messages(
772        &self,
773        message_ids: &[MessageId],
774        dest_mailbox_id: &MailboxId,
775    ) -> anyhow::Result<Vec<MessageMetadata>> {
776        let mut result_metadata = Vec::new();
777
778        for message_id in message_ids {
779            // Clone the message before the await to avoid holding the lock across await
780            let message = {
781                let messages = self
782                    .messages
783                    .read()
784                    .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?;
785                messages.get(message_id).cloned()
786            };
787
788            if let Some(message) = message {
789                // Append the message to the destination mailbox
790                let metadata = self.append_message(dest_mailbox_id, message).await?;
791                result_metadata.push(metadata);
792            }
793        }
794
795        Ok(result_metadata)
796    }
797
798    async fn get_mailbox_messages(
799        &self,
800        mailbox_id: &MailboxId,
801    ) -> anyhow::Result<Vec<MessageMetadata>> {
802        let mailbox_dir = self
803            .base_path
804            .join("mailboxes")
805            .join(mailbox_id.to_string());
806
807        let mut results = Vec::new();
808        let mut uid_counter = 1u32;
809
810        // Read from new/ directory (unread messages)
811        let new_dir = mailbox_dir.join("new");
812        if tokio::fs::try_exists(&new_dir).await.unwrap_or(false) {
813            let mut entries = tokio::fs::read_dir(&new_dir).await?;
814            while let Some(entry) = entries.next_entry().await? {
815                if let Ok(file_type) = entry.file_type().await {
816                    if file_type.is_file() {
817                        if let Some(filename) = entry.file_name().to_str() {
818                            let file_path = entry.path();
819
820                            // Parse the message from disk
821                            match self
822                                .parse_message_from_file(
823                                    &file_path,
824                                    mailbox_id,
825                                    uid_counter,
826                                    filename,
827                                )
828                                .await
829                            {
830                                Ok(metadata) => {
831                                    results.push(metadata);
832                                    uid_counter += 1;
833                                }
834                                Err(e) => {
835                                    tracing::warn!("Failed to parse message {}: {}", filename, e);
836                                }
837                            }
838                        }
839                    }
840                }
841            }
842        }
843
844        // Read from cur/ directory (messages that have been seen/flagged)
845        let cur_dir = mailbox_dir.join("cur");
846        if tokio::fs::try_exists(&cur_dir).await.unwrap_or(false) {
847            let mut entries = tokio::fs::read_dir(&cur_dir).await?;
848            while let Some(entry) = entries.next_entry().await? {
849                if let Ok(file_type) = entry.file_type().await {
850                    if file_type.is_file() {
851                        if let Some(filename) = entry.file_name().to_str() {
852                            let file_path = entry.path();
853
854                            // Parse the message from disk
855                            match self
856                                .parse_message_from_file(
857                                    &file_path,
858                                    mailbox_id,
859                                    uid_counter,
860                                    filename,
861                                )
862                                .await
863                            {
864                                Ok(metadata) => {
865                                    results.push(metadata);
866                                    uid_counter += 1;
867                                }
868                                Err(e) => {
869                                    tracing::warn!("Failed to parse message {}: {}", filename, e);
870                                }
871                            }
872                        }
873                    }
874                }
875            }
876        }
877
878        Ok(results)
879    }
880}
881
882impl FilesystemMessageStore {
883    /// Move a message from new/ to cur/ and apply flags
884    #[allow(dead_code)]
885    async fn mark_as_seen(
886        &self,
887        mailbox_id: &MailboxId,
888        filename: &str,
889        flags: &MessageFlags,
890    ) -> anyhow::Result<()> {
891        let mailbox_dir = self
892            .base_path
893            .join("mailboxes")
894            .join(mailbox_id.to_string());
895
896        let old_path = mailbox_dir.join("new").join(filename);
897        let new_filename = MaildirFilename::with_flags(filename, flags);
898        let new_path = mailbox_dir.join("cur").join(&new_filename);
899
900        tokio::fs::create_dir_all(mailbox_dir.join("cur")).await?;
901        tokio::fs::rename(&old_path, &new_path).await?;
902
903        Ok(())
904    }
905
906    /// Update flags on an existing message in cur/
907    #[allow(dead_code)]
908    async fn update_flags(
909        &self,
910        mailbox_id: &MailboxId,
911        old_filename: &str,
912        new_flags: &MessageFlags,
913    ) -> anyhow::Result<()> {
914        let mailbox_dir = self
915            .base_path
916            .join("mailboxes")
917            .join(mailbox_id.to_string());
918
919        let old_path = mailbox_dir.join("cur").join(old_filename);
920        let new_filename = MaildirFilename::with_flags(old_filename, new_flags);
921        let new_path = mailbox_dir.join("cur").join(&new_filename);
922
923        if old_path != new_path {
924            tokio::fs::rename(&old_path, &new_path).await?;
925        }
926
927        Ok(())
928    }
929
930    /// List all messages in a mailbox
931    #[allow(dead_code)]
932    async fn list_messages(
933        &self,
934        mailbox_id: &MailboxId,
935    ) -> anyhow::Result<Vec<(String, MessageFlags)>> {
936        let mailbox_dir = self
937            .base_path
938            .join("mailboxes")
939            .join(mailbox_id.to_string());
940        let mut results = Vec::new();
941
942        // Read from new/ directory
943        let new_dir = mailbox_dir.join("new");
944        if tokio::fs::try_exists(&new_dir).await.unwrap_or(false) {
945            let mut entries = tokio::fs::read_dir(&new_dir).await?;
946            while let Some(entry) = entries.next_entry().await? {
947                if let Some(filename) = entry.file_name().to_str() {
948                    let flags = MaildirFilename::decode_flags(filename);
949                    results.push((filename.to_string(), flags));
950                }
951            }
952        }
953
954        // Read from cur/ directory
955        let cur_dir = mailbox_dir.join("cur");
956        if tokio::fs::try_exists(&cur_dir).await.unwrap_or(false) {
957            let mut entries = tokio::fs::read_dir(&cur_dir).await?;
958            while let Some(entry) = entries.next_entry().await? {
959                if let Some(filename) = entry.file_name().to_str() {
960                    let flags = MaildirFilename::decode_flags(filename);
961                    results.push((filename.to_string(), flags));
962                }
963            }
964        }
965
966        Ok(results)
967    }
968
969    /// Parse a message from a maildir file and return metadata
970    async fn parse_message_from_file(
971        &self,
972        file_path: &std::path::Path,
973        mailbox_id: &MailboxId,
974        uid: u32,
975        filename: &str,
976    ) -> anyhow::Result<MessageMetadata> {
977        // Read the file contents
978        let data = tokio::fs::read(file_path).await?;
979
980        // Parse the message
981        let mime_message = rusmes_proto::MimeMessage::parse_from_bytes(&data)?;
982
983        // Extract the stored MessageId from X-Rusmes-Message-Id header if present
984        let stored_message_id = mime_message
985            .headers()
986            .get_first("x-rusmes-message-id")
987            .and_then(|id_str| uuid::Uuid::from_str(id_str).ok().map(MessageId::from_uuid));
988
989        // Create a Mail object with the correct MessageId
990        let mail = if let Some(msg_id) = stored_message_id {
991            rusmes_proto::Mail::with_message_id(
992                None,       // sender - would need to parse from headers
993                Vec::new(), // recipients - would need to parse from headers
994                mime_message,
995                None, // remote_addr
996                None, // remote_host
997                msg_id,
998            )
999        } else {
1000            // Fallback to generating a new ID if not stored
1001            rusmes_proto::Mail::new(None, Vec::new(), mime_message, None, None)
1002        };
1003
1004        // Get the message size
1005        let size = data.len();
1006
1007        // Decode flags from filename
1008        let flags = MaildirFilename::decode_flags(filename);
1009
1010        // Store the message in memory cache so it can be retrieved later
1011        let message_id = *mail.message_id();
1012        self.messages
1013            .write()
1014            .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?
1015            .insert(message_id, mail);
1016
1017        // Create metadata
1018        let metadata = MessageMetadata::new(message_id, *mailbox_id, uid, flags, size);
1019
1020        Ok(metadata)
1021    }
1022}
1023
1024/// Filesystem metadata store
1025struct FilesystemMetadataStore {
1026    base_path: PathBuf,
1027    quotas: Arc<RwLock<HashMap<Username, Quota>>>,
1028}
1029
1030#[async_trait]
1031impl MetadataStore for FilesystemMetadataStore {
1032    async fn get_user_quota(&self, user: &Username) -> anyhow::Result<Quota> {
1033        Ok(self
1034            .quotas
1035            .read()
1036            .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?
1037            .get(user)
1038            .cloned()
1039            .unwrap_or(Quota::new(0, 1024 * 1024 * 1024))) // Default 1GB
1040    }
1041
1042    async fn set_user_quota(&self, user: &Username, quota: Quota) -> anyhow::Result<()> {
1043        self.quotas
1044            .write()
1045            .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?
1046            .insert(user.clone(), quota);
1047        Ok(())
1048    }
1049
1050    async fn get_mailbox_counters(
1051        &self,
1052        mailbox_id: &MailboxId,
1053    ) -> anyhow::Result<MailboxCounters> {
1054        let mailbox_dir = self
1055            .base_path
1056            .join("mailboxes")
1057            .join(mailbox_id.to_string());
1058
1059        let mut total = 0;
1060        let mut recent = 0;
1061
1062        // Count messages in new/ directory (these are "recent")
1063        let new_dir = mailbox_dir.join("new");
1064        if tokio::fs::try_exists(&new_dir).await.unwrap_or(false) {
1065            let mut entries = tokio::fs::read_dir(&new_dir).await?;
1066            while let Some(entry) = entries.next_entry().await? {
1067                if let Ok(file_type) = entry.file_type().await {
1068                    if file_type.is_file() {
1069                        total += 1;
1070                        recent += 1;
1071                    }
1072                }
1073            }
1074        }
1075
1076        // Count messages in cur/ directory
1077        let cur_dir = mailbox_dir.join("cur");
1078        if tokio::fs::try_exists(&cur_dir).await.unwrap_or(false) {
1079            let mut entries = tokio::fs::read_dir(&cur_dir).await?;
1080            while let Some(entry) = entries.next_entry().await? {
1081                if let Ok(file_type) = entry.file_type().await {
1082                    if file_type.is_file() {
1083                        total += 1;
1084                    }
1085                }
1086            }
1087        }
1088
1089        Ok(MailboxCounters {
1090            exists: total,
1091            recent,
1092            unseen: 0, // Would need to parse flags to determine
1093        })
1094    }
1095}
1096
1097/// Helper: Check if a message matches search criteria
1098fn matches_criteria_helper<'a>(
1099    store: &'a FilesystemMessageStore,
1100    metadata: &'a MessageMetadata,
1101    criteria: &'a SearchCriteria,
1102) -> std::pin::Pin<Box<dyn std::future::Future<Output = anyhow::Result<bool>> + Send + 'a>> {
1103    Box::pin(async move {
1104        match criteria {
1105            SearchCriteria::All => Ok(true),
1106            SearchCriteria::Unseen => Ok(!metadata.flags().is_seen()),
1107            SearchCriteria::Seen => Ok(metadata.flags().is_seen()),
1108            SearchCriteria::Flagged => Ok(metadata.flags().is_flagged()),
1109            SearchCriteria::Unflagged => Ok(!metadata.flags().is_flagged()),
1110            SearchCriteria::Deleted => Ok(metadata.flags().is_deleted()),
1111            SearchCriteria::Undeleted => Ok(!metadata.flags().is_deleted()),
1112
1113            // Content-based searches require loading the full message
1114            SearchCriteria::From(pattern) => {
1115                matches_header_pattern_helper(store, metadata, "from", pattern).await
1116            }
1117            SearchCriteria::To(pattern) => {
1118                matches_header_pattern_helper(store, metadata, "to", pattern).await
1119            }
1120            SearchCriteria::Subject(pattern) => {
1121                matches_header_pattern_helper(store, metadata, "subject", pattern).await
1122            }
1123            SearchCriteria::Body(pattern) => {
1124                matches_body_pattern_helper(store, metadata, pattern).await
1125            }
1126
1127            // Logical operators
1128            SearchCriteria::And(sub_criteria) => {
1129                for sub in sub_criteria {
1130                    if !matches_criteria_helper(store, metadata, sub).await? {
1131                        return Ok(false);
1132                    }
1133                }
1134                Ok(true)
1135            }
1136            SearchCriteria::Or(sub_criteria) => {
1137                for sub in sub_criteria {
1138                    if matches_criteria_helper(store, metadata, sub).await? {
1139                        return Ok(true);
1140                    }
1141                }
1142                Ok(false)
1143            }
1144            SearchCriteria::Not(sub_criteria) => {
1145                Ok(!matches_criteria_helper(store, metadata, sub_criteria).await?)
1146            }
1147        }
1148    })
1149}
1150
1151/// Helper: Check if a header matches a pattern
1152async fn matches_header_pattern_helper(
1153    store: &FilesystemMessageStore,
1154    metadata: &MessageMetadata,
1155    header_name: &str,
1156    pattern: &str,
1157) -> anyhow::Result<bool> {
1158    // Load the message from storage
1159    if let Some(mail) = store.get_message(metadata.message_id()).await? {
1160        if let Some(header_value) = mail.message().headers().get_first(header_name) {
1161            // Case-insensitive substring match
1162            return Ok(header_value
1163                .to_lowercase()
1164                .contains(&pattern.to_lowercase()));
1165        }
1166    }
1167    Ok(false)
1168}
1169
1170/// Helper: Check if message body matches a pattern
1171async fn matches_body_pattern_helper(
1172    store: &FilesystemMessageStore,
1173    metadata: &MessageMetadata,
1174    pattern: &str,
1175) -> anyhow::Result<bool> {
1176    // Load the message from storage
1177    if let Some(mail) = store.get_message(metadata.message_id()).await? {
1178        // Extract text from message body
1179        if let Ok(text) = mail.message().extract_text() {
1180            // Case-insensitive substring match
1181            return Ok(text.to_lowercase().contains(&pattern.to_lowercase()));
1182        }
1183    }
1184    Ok(false)
1185}
1186
1187/// Helper function to serialize a Mail object to bytes for storage
1188fn serialize_message_to_bytes(mail: &Mail) -> anyhow::Result<Vec<u8>> {
1189    let message = mail.message();
1190    let headers = message.headers();
1191    let body = message.body();
1192
1193    let mut output = Vec::new();
1194
1195    // Write custom header with MessageId for retrieval
1196    // This is stored as X-Rusmes-Message-Id to avoid conflicts
1197    output.extend_from_slice(b"X-Rusmes-Message-Id: ");
1198    output.extend_from_slice(mail.message_id().to_string().as_bytes());
1199    output.extend_from_slice(b"\r\n");
1200
1201    // Write original headers
1202    for (name, values) in headers.iter() {
1203        for value in values {
1204            output.extend_from_slice(name.as_bytes());
1205            output.extend_from_slice(b": ");
1206            output.extend_from_slice(value.as_bytes());
1207            output.extend_from_slice(b"\r\n");
1208        }
1209    }
1210
1211    // Blank line separating headers from body
1212    output.extend_from_slice(b"\r\n");
1213
1214    // Write body
1215    match body {
1216        rusmes_proto::MessageBody::Small(bytes) => {
1217            output.extend_from_slice(bytes);
1218        }
1219        rusmes_proto::MessageBody::Large(_) => {
1220            // For large messages, we'd need to stream
1221            // For now, just return empty
1222        }
1223    }
1224
1225    Ok(output)
1226}
1227
1228#[cfg(test)]
1229mod tests {
1230    use super::*;
1231
1232    #[tokio::test]
1233    async fn test_filesystem_backend() {
1234        let backend = FilesystemBackend::new("/tmp/rusmes-test");
1235        let mailbox_store = backend.mailbox_store();
1236
1237        let user: Username = "testuser".parse().unwrap();
1238        let path = MailboxPath::new(user.clone(), vec!["INBOX".to_string()]);
1239
1240        let mailbox_id = mailbox_store.create_mailbox(&path).await.unwrap();
1241        let mailbox = mailbox_store.get_mailbox(&mailbox_id).await.unwrap();
1242
1243        assert!(mailbox.is_some());
1244        assert_eq!(mailbox.unwrap().path().user(), &user);
1245    }
1246
1247    #[tokio::test]
1248    async fn test_get_mailbox_messages() {
1249        use rusmes_proto::{MailAddress, MimeMessage};
1250
1251        let temp_dir = std::env::temp_dir().join(format!("rusmes-test-{}", uuid::Uuid::new_v4()));
1252        let backend = FilesystemBackend::new(&temp_dir);
1253        let mailbox_store = backend.mailbox_store();
1254        let message_store = backend.message_store();
1255
1256        let user: Username = "testuser".parse().unwrap();
1257        let path = MailboxPath::new(user.clone(), vec!["INBOX".to_string()]);
1258
1259        // Create mailbox
1260        let mailbox_id = mailbox_store.create_mailbox(&path).await.unwrap();
1261
1262        // Create and append a test message
1263        let headers = rusmes_proto::HeaderMap::new();
1264        let body = rusmes_proto::MessageBody::Small(bytes::Bytes::from("Test message body"));
1265        let mime_message = MimeMessage::new(headers, body);
1266
1267        let sender = Some("sender@example.com".parse::<MailAddress>().unwrap());
1268        let recipients = vec!["testuser@localhost".parse::<MailAddress>().unwrap()];
1269        let mail = rusmes_proto::Mail::new(sender, recipients, mime_message, None, None);
1270
1271        // Append message
1272        let metadata = message_store
1273            .append_message(&mailbox_id, mail)
1274            .await
1275            .unwrap();
1276        assert_eq!(metadata.mailbox_id(), &mailbox_id);
1277
1278        // Get mailbox messages
1279        let messages = message_store
1280            .get_mailbox_messages(&mailbox_id)
1281            .await
1282            .unwrap();
1283
1284        // Verify we got the message back
1285        assert_eq!(messages.len(), 1, "Should have exactly 1 message");
1286        let msg = &messages[0];
1287        assert_eq!(msg.mailbox_id(), &mailbox_id);
1288        assert_eq!(msg.uid(), 1, "First message should have UID 1");
1289        assert!(msg.size() > 0, "Message should have non-zero size");
1290
1291        // Clean up
1292        let _ = tokio::fs::remove_dir_all(&temp_dir).await;
1293    }
1294
1295    #[tokio::test]
1296    async fn test_get_mailbox_messages_multiple() {
1297        use rusmes_proto::{MailAddress, MimeMessage};
1298
1299        let temp_dir = std::env::temp_dir().join(format!("rusmes-test-{}", uuid::Uuid::new_v4()));
1300        let backend = FilesystemBackend::new(&temp_dir);
1301        let mailbox_store = backend.mailbox_store();
1302        let message_store = backend.message_store();
1303
1304        let user: Username = "testuser".parse().unwrap();
1305        let path = MailboxPath::new(user.clone(), vec!["INBOX".to_string()]);
1306
1307        // Create mailbox
1308        let mailbox_id = mailbox_store.create_mailbox(&path).await.unwrap();
1309
1310        // Append multiple messages
1311        for i in 0..5 {
1312            let headers = rusmes_proto::HeaderMap::new();
1313            let body = rusmes_proto::MessageBody::Small(bytes::Bytes::from(format!(
1314                "Test message body {}",
1315                i
1316            )));
1317            let mime_message = MimeMessage::new(headers, body);
1318
1319            let sender = Some(
1320                format!("sender{}@example.com", i)
1321                    .parse::<MailAddress>()
1322                    .unwrap(),
1323            );
1324            let recipients = vec!["testuser@localhost".parse::<MailAddress>().unwrap()];
1325            let mail = rusmes_proto::Mail::new(sender, recipients, mime_message, None, None);
1326
1327            message_store
1328                .append_message(&mailbox_id, mail)
1329                .await
1330                .unwrap();
1331        }
1332
1333        // Get mailbox messages
1334        let messages = message_store
1335            .get_mailbox_messages(&mailbox_id)
1336            .await
1337            .unwrap();
1338
1339        // Verify we got all messages
1340        assert_eq!(messages.len(), 5, "Should have exactly 5 messages");
1341
1342        // Verify UIDs are sequential
1343        for (i, msg) in messages.iter().enumerate() {
1344            assert_eq!(
1345                msg.uid(),
1346                (i + 1) as u32,
1347                "Message {} should have UID {}",
1348                i,
1349                i + 1
1350            );
1351            assert_eq!(msg.mailbox_id(), &mailbox_id);
1352            assert!(msg.size() > 0, "Message should have non-zero size");
1353        }
1354
1355        // Clean up
1356        let _ = tokio::fs::remove_dir_all(&temp_dir).await;
1357    }
1358
1359    #[tokio::test]
1360    async fn test_get_mailbox_messages_with_flags() {
1361        use rusmes_proto::{MailAddress, MimeMessage};
1362
1363        let temp_dir = std::env::temp_dir().join(format!("rusmes-test-{}", uuid::Uuid::new_v4()));
1364        let backend = FilesystemBackend::new(&temp_dir);
1365        let mailbox_store = backend.mailbox_store();
1366        let message_store = backend.message_store();
1367
1368        let user: Username = "testuser".parse().unwrap();
1369        let path = MailboxPath::new(user.clone(), vec!["INBOX".to_string()]);
1370
1371        // Create mailbox
1372        let mailbox_id = mailbox_store.create_mailbox(&path).await.unwrap();
1373
1374        // Append a message
1375        let headers = rusmes_proto::HeaderMap::new();
1376        let body = rusmes_proto::MessageBody::Small(bytes::Bytes::from("Test message with flags"));
1377        let mime_message = MimeMessage::new(headers, body);
1378
1379        let sender = Some("sender@example.com".parse::<MailAddress>().unwrap());
1380        let recipients = vec!["testuser@localhost".parse::<MailAddress>().unwrap()];
1381        let mail = rusmes_proto::Mail::new(sender, recipients, mime_message, None, None);
1382
1383        let _metadata = message_store
1384            .append_message(&mailbox_id, mail)
1385            .await
1386            .unwrap();
1387
1388        // Initially, message should be in new/ directory with no flags
1389        let messages = message_store
1390            .get_mailbox_messages(&mailbox_id)
1391            .await
1392            .unwrap();
1393        assert_eq!(messages.len(), 1);
1394        let initial_flags = messages[0].flags();
1395        assert!(
1396            !initial_flags.is_seen(),
1397            "New message should not be marked as seen"
1398        );
1399
1400        // Manually move the message to cur/ with flags to simulate IMAP flag setting
1401        let mailbox_dir = temp_dir.join("mailboxes").join(mailbox_id.to_string());
1402        let new_dir = mailbox_dir.join("new");
1403        let cur_dir = mailbox_dir.join("cur");
1404
1405        // Find the message file
1406        let mut entries = tokio::fs::read_dir(&new_dir).await.unwrap();
1407        if let Some(entry) = entries.next_entry().await.unwrap() {
1408            let old_filename = entry.file_name();
1409            let old_path = new_dir.join(&old_filename);
1410
1411            // Create new filename with Seen flag (:2,S)
1412            let base_name = old_filename.to_str().unwrap();
1413            let new_filename = format!("{}:2,S", base_name.split(":2,").next().unwrap());
1414            let new_path = cur_dir.join(&new_filename);
1415
1416            // Move the file
1417            tokio::fs::rename(&old_path, &new_path).await.unwrap();
1418        }
1419
1420        // Re-read messages - should now see the Seen flag
1421        let messages = message_store
1422            .get_mailbox_messages(&mailbox_id)
1423            .await
1424            .unwrap();
1425        assert_eq!(messages.len(), 1);
1426        let updated_flags = messages[0].flags();
1427        assert!(
1428            updated_flags.is_seen(),
1429            "Message should now be marked as seen"
1430        );
1431
1432        // Clean up
1433        let _ = tokio::fs::remove_dir_all(&temp_dir).await;
1434    }
1435
1436    #[tokio::test]
1437    async fn test_get_message_from_disk() {
1438        use rusmes_proto::{MailAddress, MimeMessage};
1439
1440        let temp_dir = std::env::temp_dir().join(format!("rusmes-test-{}", uuid::Uuid::new_v4()));
1441        let backend = FilesystemBackend::new(&temp_dir);
1442        let mailbox_store = backend.mailbox_store();
1443        let message_store = backend.message_store();
1444
1445        let user: Username = "testuser".parse().unwrap();
1446        let path = MailboxPath::new(user.clone(), vec!["INBOX".to_string()]);
1447
1448        // Create mailbox
1449        let mailbox_id = mailbox_store.create_mailbox(&path).await.unwrap();
1450
1451        // Create and append a test message
1452        let headers = rusmes_proto::HeaderMap::new();
1453        let body =
1454            rusmes_proto::MessageBody::Small(bytes::Bytes::from("Test message for disk retrieval"));
1455        let mime_message = MimeMessage::new(headers, body);
1456
1457        let sender = Some("sender@example.com".parse::<MailAddress>().unwrap());
1458        let recipients = vec!["testuser@localhost".parse::<MailAddress>().unwrap()];
1459        let mail = rusmes_proto::Mail::new(sender, recipients, mime_message, None, None);
1460
1461        // Store the message ID before appending
1462        let message_id = *mail.message_id();
1463
1464        // Append message
1465        let _metadata = message_store
1466            .append_message(&mailbox_id, mail)
1467            .await
1468            .unwrap();
1469
1470        // Create a new backend instance to simulate a fresh start (empty cache)
1471        let backend2 = FilesystemBackend::new(&temp_dir);
1472        let message_store2 = backend2.message_store();
1473
1474        // Try to retrieve the message - should load from disk
1475        let retrieved_mail = message_store2.get_message(&message_id).await.unwrap();
1476
1477        // Verify we got the message back
1478        assert!(
1479            retrieved_mail.is_some(),
1480            "Should retrieve message from disk"
1481        );
1482        let retrieved = retrieved_mail.unwrap();
1483        assert_eq!(
1484            retrieved.message_id(),
1485            &message_id,
1486            "Message ID should match"
1487        );
1488        assert!(
1489            retrieved.size() > 0,
1490            "Retrieved message should have non-zero size"
1491        );
1492
1493        // Clean up
1494        let _ = tokio::fs::remove_dir_all(&temp_dir).await;
1495    }
1496
1497    #[tokio::test]
1498    async fn test_mailbox_metadata_persistence() {
1499        let temp_dir = std::env::temp_dir().join(format!("rusmes-test-{}", uuid::Uuid::new_v4()));
1500
1501        let user: Username = "testuser".parse().unwrap();
1502        let inbox_path = MailboxPath::new(user.clone(), vec!["INBOX".to_string()]);
1503        let sent_path = MailboxPath::new(user.clone(), vec!["Sent".to_string()]);
1504
1505        let mailbox_id;
1506        let sent_id;
1507
1508        // Create mailboxes in first backend instance
1509        {
1510            let backend = FilesystemBackend::new(&temp_dir);
1511            let mailbox_store = backend.mailbox_store();
1512
1513            mailbox_id = mailbox_store.create_mailbox(&inbox_path).await.unwrap();
1514            sent_id = mailbox_store.create_mailbox(&sent_path).await.unwrap();
1515
1516            // Verify mailboxes exist
1517            let mailbox = mailbox_store.get_mailbox(&mailbox_id).await.unwrap();
1518            assert!(mailbox.is_some());
1519            assert_eq!(mailbox.unwrap().path().name(), Some("INBOX"));
1520
1521            let sent_mailbox = mailbox_store.get_mailbox(&sent_id).await.unwrap();
1522            assert!(sent_mailbox.is_some());
1523            assert_eq!(sent_mailbox.unwrap().path().name(), Some("Sent"));
1524
1525            // Verify metadata file was created
1526            let metadata_file = temp_dir
1527                .join("users")
1528                .join(user.as_str())
1529                .join("mailboxes.json");
1530            assert!(tokio::fs::try_exists(&metadata_file).await.unwrap());
1531        }
1532
1533        // Create new backend instance (simulates server restart)
1534        {
1535            let backend = FilesystemBackend::new(&temp_dir);
1536            let mailbox_store = backend.mailbox_store();
1537
1538            // Verify mailboxes still exist after "restart"
1539            let mailbox = mailbox_store.get_mailbox(&mailbox_id).await.unwrap();
1540            assert!(mailbox.is_some(), "INBOX should be restored from disk");
1541            assert_eq!(mailbox.unwrap().path().name(), Some("INBOX"));
1542
1543            let sent_mailbox = mailbox_store.get_mailbox(&sent_id).await.unwrap();
1544            assert!(sent_mailbox.is_some(), "Sent should be restored from disk");
1545            assert_eq!(sent_mailbox.unwrap().path().name(), Some("Sent"));
1546
1547            // List mailboxes should return both
1548            let mailboxes = mailbox_store.list_mailboxes(&user).await.unwrap();
1549            assert_eq!(mailboxes.len(), 2, "Should have 2 mailboxes after restart");
1550
1551            // Test delete and verify persistence
1552            mailbox_store.delete_mailbox(&sent_id).await.unwrap();
1553            let deleted_mailbox = mailbox_store.get_mailbox(&sent_id).await.unwrap();
1554            assert!(deleted_mailbox.is_none(), "Sent mailbox should be deleted");
1555        }
1556
1557        // Create third backend instance to verify deletion was persisted
1558        {
1559            let backend = FilesystemBackend::new(&temp_dir);
1560            let mailbox_store = backend.mailbox_store();
1561
1562            // INBOX should still exist
1563            let mailbox = mailbox_store.get_mailbox(&mailbox_id).await.unwrap();
1564            assert!(mailbox.is_some(), "INBOX should still exist");
1565
1566            // Sent should not exist
1567            let sent_mailbox = mailbox_store.get_mailbox(&sent_id).await.unwrap();
1568            assert!(sent_mailbox.is_none(), "Sent should still be deleted");
1569
1570            // List should only return INBOX
1571            let mailboxes = mailbox_store.list_mailboxes(&user).await.unwrap();
1572            assert_eq!(mailboxes.len(), 1, "Should have 1 mailbox after restart");
1573            assert_eq!(mailboxes[0].path().name(), Some("INBOX"));
1574        }
1575
1576        // Clean up
1577        let _ = tokio::fs::remove_dir_all(&temp_dir).await;
1578    }
1579
1580    #[tokio::test]
1581    async fn test_mailbox_metadata_persistence_multiple_users() {
1582        let temp_dir = std::env::temp_dir().join(format!("rusmes-test-{}", uuid::Uuid::new_v4()));
1583
1584        let user1: Username = "user1".parse().unwrap();
1585        let user2: Username = "user2".parse().unwrap();
1586
1587        let user1_inbox = MailboxPath::new(user1.clone(), vec!["INBOX".to_string()]);
1588        let user2_inbox = MailboxPath::new(user2.clone(), vec!["INBOX".to_string()]);
1589
1590        // Create mailboxes for both users
1591        {
1592            let backend = FilesystemBackend::new(&temp_dir);
1593            let mailbox_store = backend.mailbox_store();
1594
1595            mailbox_store.create_mailbox(&user1_inbox).await.unwrap();
1596            mailbox_store.create_mailbox(&user2_inbox).await.unwrap();
1597        }
1598
1599        // Verify both users' mailboxes are restored
1600        {
1601            let backend = FilesystemBackend::new(&temp_dir);
1602            let mailbox_store = backend.mailbox_store();
1603
1604            let user1_mailboxes = mailbox_store.list_mailboxes(&user1).await.unwrap();
1605            assert_eq!(user1_mailboxes.len(), 1);
1606            assert_eq!(user1_mailboxes[0].path().user(), &user1);
1607
1608            let user2_mailboxes = mailbox_store.list_mailboxes(&user2).await.unwrap();
1609            assert_eq!(user2_mailboxes.len(), 1);
1610            assert_eq!(user2_mailboxes[0].path().user(), &user2);
1611        }
1612
1613        // Clean up
1614        let _ = tokio::fs::remove_dir_all(&temp_dir).await;
1615    }
1616
1617    #[tokio::test]
1618    async fn test_mailbox_metadata_rename_persistence() {
1619        let temp_dir = std::env::temp_dir().join(format!("rusmes-test-{}", uuid::Uuid::new_v4()));
1620
1621        let user: Username = "testuser".parse().unwrap();
1622        let original_path = MailboxPath::new(user.clone(), vec!["OldName".to_string()]);
1623        let new_path = MailboxPath::new(user.clone(), vec!["NewName".to_string()]);
1624
1625        let mailbox_id;
1626
1627        // Create and rename mailbox
1628        {
1629            let backend = FilesystemBackend::new(&temp_dir);
1630            let mailbox_store = backend.mailbox_store();
1631
1632            mailbox_id = mailbox_store.create_mailbox(&original_path).await.unwrap();
1633            mailbox_store
1634                .rename_mailbox(&mailbox_id, &new_path)
1635                .await
1636                .unwrap();
1637        }
1638
1639        // Verify rename was persisted
1640        {
1641            let backend = FilesystemBackend::new(&temp_dir);
1642            let mailbox_store = backend.mailbox_store();
1643
1644            let mailbox = mailbox_store.get_mailbox(&mailbox_id).await.unwrap();
1645            assert!(mailbox.is_some());
1646            assert_eq!(mailbox.unwrap().path().name(), Some("NewName"));
1647        }
1648
1649        // Clean up
1650        let _ = tokio::fs::remove_dir_all(&temp_dir).await;
1651    }
1652}