1use 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
16pub 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 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 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 #[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 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 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 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 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 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 if let Some(parent) = metadata_file.parent() {
144 std::fs::create_dir_all(parent)?;
145 }
146
147 let json = serde_json::to_string_pretty(mailboxes)?;
149
150 std::fs::write(&metadata_file, json)?;
152
153 tracing::debug!("Saved {} mailboxes for user {}", mailboxes.len(), user);
154 Ok(())
155 }
156}
157
158struct MaildirFilename;
160
161impl MaildirFilename {
162 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 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 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 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 fn base_name(filename: &str) -> &str {
226 filename.split(":2,").next().unwrap_or(filename)
227 }
228
229 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
265struct 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 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 let mailbox_dir = self.base_path.join("mailboxes").join(id.to_string());
298 tokio::fs::create_dir_all(&mailbox_dir).await?;
299
300 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 self.mailboxes
307 .write()
308 .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?
309 .insert(id, mailbox);
310
311 self.persist_user_mailboxes(&user)?;
313
314 Ok(id)
315 }
316
317 async fn delete_mailbox(&self, id: &MailboxId) -> anyhow::Result<()> {
318 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 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 mailbox.set_path(new_path.clone());
354 drop(mailboxes); 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 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 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 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 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 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
484struct 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 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 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)) };
528
529 if quota.used + (message_size as u64) > quota.limit {
531 return Err(anyhow::anyhow!("Quota exceeded: cannot append message"));
532 }
533
534 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 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 let tmp_path = mailbox_dir.join("tmp").join(&filename);
554 tokio::fs::create_dir_all(mailbox_dir.join("tmp")).await?;
555
556 let message_data = serialize_message_to_bytes(&message)?;
558 tokio::fs::write(&tmp_path, &message_data).await?;
559
560 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 self.messages
568 .write()
569 .map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?
570 .insert(message_id, message.clone());
571
572 {
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 let metadata = MessageMetadata::new(
586 message_id,
587 *mailbox_id,
588 1, 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 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 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 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 let mailbox_dir = entry.path();
625
626 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 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 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 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 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 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 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 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 let messages = self.get_mailbox_messages(mailbox_id).await?;
760
761 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 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 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 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 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 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 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 #[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 #[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 #[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 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 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 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 let data = tokio::fs::read(file_path).await?;
979
980 let mime_message = rusmes_proto::MimeMessage::parse_from_bytes(&data)?;
982
983 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 let mail = if let Some(msg_id) = stored_message_id {
991 rusmes_proto::Mail::with_message_id(
992 None, Vec::new(), mime_message,
995 None, None, msg_id,
998 )
999 } else {
1000 rusmes_proto::Mail::new(None, Vec::new(), mime_message, None, None)
1002 };
1003
1004 let size = data.len();
1006
1007 let flags = MaildirFilename::decode_flags(filename);
1009
1010 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 let metadata = MessageMetadata::new(message_id, *mailbox_id, uid, flags, size);
1019
1020 Ok(metadata)
1021 }
1022}
1023
1024struct 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))) }
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 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 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, })
1094 }
1095}
1096
1097fn 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 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 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
1151async fn matches_header_pattern_helper(
1153 store: &FilesystemMessageStore,
1154 metadata: &MessageMetadata,
1155 header_name: &str,
1156 pattern: &str,
1157) -> anyhow::Result<bool> {
1158 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 return Ok(header_value
1163 .to_lowercase()
1164 .contains(&pattern.to_lowercase()));
1165 }
1166 }
1167 Ok(false)
1168}
1169
1170async fn matches_body_pattern_helper(
1172 store: &FilesystemMessageStore,
1173 metadata: &MessageMetadata,
1174 pattern: &str,
1175) -> anyhow::Result<bool> {
1176 if let Some(mail) = store.get_message(metadata.message_id()).await? {
1178 if let Ok(text) = mail.message().extract_text() {
1180 return Ok(text.to_lowercase().contains(&pattern.to_lowercase()));
1182 }
1183 }
1184 Ok(false)
1185}
1186
1187fn 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 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 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 output.extend_from_slice(b"\r\n");
1213
1214 match body {
1216 rusmes_proto::MessageBody::Small(bytes) => {
1217 output.extend_from_slice(bytes);
1218 }
1219 rusmes_proto::MessageBody::Large(_) => {
1220 }
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 let mailbox_id = mailbox_store.create_mailbox(&path).await.unwrap();
1261
1262 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 let metadata = message_store
1273 .append_message(&mailbox_id, mail)
1274 .await
1275 .unwrap();
1276 assert_eq!(metadata.mailbox_id(), &mailbox_id);
1277
1278 let messages = message_store
1280 .get_mailbox_messages(&mailbox_id)
1281 .await
1282 .unwrap();
1283
1284 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 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 let mailbox_id = mailbox_store.create_mailbox(&path).await.unwrap();
1309
1310 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 let messages = message_store
1335 .get_mailbox_messages(&mailbox_id)
1336 .await
1337 .unwrap();
1338
1339 assert_eq!(messages.len(), 5, "Should have exactly 5 messages");
1341
1342 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 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 let mailbox_id = mailbox_store.create_mailbox(&path).await.unwrap();
1373
1374 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 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 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 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 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 tokio::fs::rename(&old_path, &new_path).await.unwrap();
1418 }
1419
1420 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 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 let mailbox_id = mailbox_store.create_mailbox(&path).await.unwrap();
1450
1451 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 let message_id = *mail.message_id();
1463
1464 let _metadata = message_store
1466 .append_message(&mailbox_id, mail)
1467 .await
1468 .unwrap();
1469
1470 let backend2 = FilesystemBackend::new(&temp_dir);
1472 let message_store2 = backend2.message_store();
1473
1474 let retrieved_mail = message_store2.get_message(&message_id).await.unwrap();
1476
1477 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 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 {
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 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 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 {
1535 let backend = FilesystemBackend::new(&temp_dir);
1536 let mailbox_store = backend.mailbox_store();
1537
1538 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 let mailboxes = mailbox_store.list_mailboxes(&user).await.unwrap();
1549 assert_eq!(mailboxes.len(), 2, "Should have 2 mailboxes after restart");
1550
1551 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 {
1559 let backend = FilesystemBackend::new(&temp_dir);
1560 let mailbox_store = backend.mailbox_store();
1561
1562 let mailbox = mailbox_store.get_mailbox(&mailbox_id).await.unwrap();
1564 assert!(mailbox.is_some(), "INBOX should still exist");
1565
1566 let sent_mailbox = mailbox_store.get_mailbox(&sent_id).await.unwrap();
1568 assert!(sent_mailbox.is_none(), "Sent should still be deleted");
1569
1570 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 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 {
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 {
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 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 {
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 {
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 let _ = tokio::fs::remove_dir_all(&temp_dir).await;
1651 }
1652}