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 sqlx::postgres::{PgPool, PgPoolOptions};
11use sqlx::{Executor, Row};
12use std::sync::Arc;
13
14pub struct PostgresCompleteBackend {
16 pool: PgPool,
17}
18
19impl PostgresCompleteBackend {
20 pub async fn new(database_url: &str) -> anyhow::Result<Self> {
22 let pool = PgPoolOptions::new()
23 .max_connections(20)
24 .connect(database_url)
25 .await?;
26
27 Ok(Self { pool })
28 }
29
30 pub async fn init_schema(&self) -> anyhow::Result<()> {
32 self.pool
34 .execute(
35 r#"
36 CREATE TABLE IF NOT EXISTS mailboxes (
37 id UUID PRIMARY KEY,
38 username TEXT NOT NULL,
39 path TEXT NOT NULL,
40 uid_validity INTEGER NOT NULL,
41 uid_next INTEGER NOT NULL,
42 special_use TEXT,
43 created_at TIMESTAMP NOT NULL DEFAULT NOW(),
44 updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
45 UNIQUE(username, path)
46 )
47 "#,
48 )
49 .await?;
50
51 self.pool
52 .execute("CREATE INDEX IF NOT EXISTS idx_mailboxes_username ON mailboxes(username)")
53 .await?;
54 self.pool
55 .execute("CREATE INDEX IF NOT EXISTS idx_mailboxes_path ON mailboxes(path)")
56 .await?;
57
58 self.pool
60 .execute(
61 r#"
62 CREATE TABLE IF NOT EXISTS messages (
63 id UUID PRIMARY KEY,
64 mailbox_id UUID NOT NULL REFERENCES mailboxes(id) ON DELETE CASCADE,
65 uid INTEGER NOT NULL,
66 sender TEXT,
67 recipients TEXT[] NOT NULL,
68 headers JSONB NOT NULL,
69 body BYTEA NOT NULL,
70 size INTEGER NOT NULL,
71 search_vector TSVECTOR,
72 created_at TIMESTAMP NOT NULL DEFAULT NOW(),
73 UNIQUE(mailbox_id, uid)
74 )
75 "#,
76 )
77 .await?;
78
79 self.pool
80 .execute("CREATE INDEX IF NOT EXISTS idx_messages_mailbox ON messages(mailbox_id)")
81 .await?;
82 self.pool
83 .execute("CREATE INDEX IF NOT EXISTS idx_messages_sender ON messages(sender)")
84 .await?;
85 self.pool.execute("CREATE INDEX IF NOT EXISTS idx_messages_search ON messages USING GIN(search_vector)").await?;
86
87 self.pool
89 .execute(
90 r#"
91 CREATE TABLE IF NOT EXISTS message_flags (
92 message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
93 flag_seen BOOLEAN NOT NULL DEFAULT FALSE,
94 flag_answered BOOLEAN NOT NULL DEFAULT FALSE,
95 flag_flagged BOOLEAN NOT NULL DEFAULT FALSE,
96 flag_deleted BOOLEAN NOT NULL DEFAULT FALSE,
97 flag_draft BOOLEAN NOT NULL DEFAULT FALSE,
98 flag_recent BOOLEAN NOT NULL DEFAULT FALSE,
99 custom_flags TEXT[] NOT NULL DEFAULT '{}',
100 PRIMARY KEY(message_id)
101 )
102 "#,
103 )
104 .await?;
105
106 self.pool
108 .execute(
109 r#"
110 CREATE TABLE IF NOT EXISTS subscriptions (
111 username TEXT NOT NULL,
112 mailbox_name TEXT NOT NULL,
113 created_at TIMESTAMP NOT NULL DEFAULT NOW(),
114 PRIMARY KEY(username, mailbox_name)
115 )
116 "#,
117 )
118 .await?;
119
120 self.pool
122 .execute(
123 r#"
124 CREATE TABLE IF NOT EXISTS user_quotas (
125 username TEXT PRIMARY KEY,
126 used BIGINT NOT NULL DEFAULT 0,
127 quota_limit BIGINT NOT NULL,
128 updated_at TIMESTAMP NOT NULL DEFAULT NOW()
129 )
130 "#,
131 )
132 .await?;
133
134 self.pool.execute(
136 r#"
137 CREATE OR REPLACE FUNCTION messages_search_vector_update() RETURNS trigger AS $$
138 BEGIN
139 NEW.search_vector :=
140 setweight(to_tsvector('english', COALESCE(NEW.sender, '')), 'A') ||
141 setweight(to_tsvector('english', COALESCE(array_to_string(NEW.recipients, ' '), '')), 'B') ||
142 setweight(to_tsvector('english', COALESCE(NEW.headers::text, '')), 'C');
143 RETURN NEW;
144 END
145 $$ LANGUAGE plpgsql
146 "#,
147 ).await?;
148
149 self.pool
150 .execute(
151 r#"
152 DROP TRIGGER IF EXISTS messages_search_vector_trigger ON messages;
153 CREATE TRIGGER messages_search_vector_trigger
154 BEFORE INSERT OR UPDATE ON messages
155 FOR EACH ROW EXECUTE FUNCTION messages_search_vector_update()
156 "#,
157 )
158 .await?;
159
160 Ok(())
161 }
162
163 pub fn pool(&self) -> &PgPool {
165 &self.pool
166 }
167}
168
169impl StorageBackend for PostgresCompleteBackend {
170 fn mailbox_store(&self) -> Arc<dyn MailboxStore> {
171 Arc::new(PostgresCompleteMailboxStore {
172 pool: self.pool.clone(),
173 })
174 }
175
176 fn message_store(&self) -> Arc<dyn MessageStore> {
177 Arc::new(PostgresCompleteMessageStore {
178 pool: self.pool.clone(),
179 })
180 }
181
182 fn metadata_store(&self) -> Arc<dyn MetadataStore> {
183 Arc::new(PostgresCompleteMetadataStore {
184 pool: self.pool.clone(),
185 })
186 }
187}
188
189struct PostgresCompleteMailboxStore {
191 pool: PgPool,
192}
193
194#[async_trait]
195impl MailboxStore for PostgresCompleteMailboxStore {
196 async fn create_mailbox(&self, path: &MailboxPath) -> anyhow::Result<MailboxId> {
197 let mailbox = Mailbox::new(path.clone());
198 let id = *mailbox.id();
199
200 sqlx::query(
201 r#"
202 INSERT INTO mailboxes (id, username, path, uid_validity, uid_next, special_use)
203 VALUES ($1, $2, $3, $4, $5, $6)
204 "#,
205 )
206 .bind(*id.as_uuid())
207 .bind(path.user().to_string())
208 .bind(path.path().join("/"))
209 .bind(mailbox.uid_validity() as i32)
210 .bind(mailbox.uid_next() as i32)
211 .bind(mailbox.special_use())
212 .execute(&self.pool)
213 .await?;
214
215 Ok(id)
216 }
217
218 async fn delete_mailbox(&self, id: &MailboxId) -> anyhow::Result<()> {
219 sqlx::query("DELETE FROM mailboxes WHERE id = $1")
220 .bind(*id.as_uuid())
221 .execute(&self.pool)
222 .await?;
223
224 Ok(())
225 }
226
227 async fn rename_mailbox(&self, id: &MailboxId, new_path: &MailboxPath) -> anyhow::Result<()> {
228 sqlx::query("UPDATE mailboxes SET path = $1, updated_at = NOW() WHERE id = $2")
229 .bind(new_path.path().join("/"))
230 .bind(*id.as_uuid())
231 .execute(&self.pool)
232 .await?;
233
234 Ok(())
235 }
236
237 async fn get_mailbox(&self, id: &MailboxId) -> anyhow::Result<Option<Mailbox>> {
238 let row = sqlx::query(
239 "SELECT id, username, path, uid_validity, uid_next, special_use FROM mailboxes WHERE id = $1"
240 )
241 .bind(*id.as_uuid())
242 .fetch_optional(&self.pool)
243 .await?;
244
245 let row = match row {
246 Some(r) => r,
247 None => return Ok(None),
248 };
249
250 let username: String = row.try_get("username")?;
251 let path_str: String = row.try_get("path")?;
252 let path_parts: Vec<String> = path_str.split('/').map(|s| s.to_string()).collect();
253 let username_obj =
254 Username::new(username).map_err(|e| anyhow::anyhow!("Invalid username: {}", e))?;
255 let path = MailboxPath::new(username_obj, path_parts);
256
257 let mut mailbox = Mailbox::new(path);
258 let special_use: Option<String> = row.try_get("special_use")?;
259 mailbox.set_special_use(special_use);
260
261 Ok(Some(mailbox))
262 }
263
264 async fn list_mailboxes(&self, user: &Username) -> anyhow::Result<Vec<Mailbox>> {
265 let rows = sqlx::query(
266 "SELECT id, username, path, uid_validity, uid_next, special_use FROM mailboxes WHERE username = $1 ORDER BY path"
267 )
268 .bind(user.to_string())
269 .fetch_all(&self.pool)
270 .await?;
271
272 let mailboxes = rows
273 .into_iter()
274 .filter_map(|row| {
275 let username: String = row.try_get("username").ok()?;
276 let path_str: String = row.try_get("path").ok()?;
277 let path_parts: Vec<String> = path_str.split('/').map(|s| s.to_string()).collect();
278 let username_obj = Username::new(username).ok()?;
279 let path = MailboxPath::new(username_obj, path_parts);
280
281 let mut mailbox = Mailbox::new(path);
282 if let Ok(Some(special_use)) = row.try_get("special_use") {
283 mailbox.set_special_use(Some(special_use));
284 }
285 Some(mailbox)
286 })
287 .collect();
288
289 Ok(mailboxes)
290 }
291
292 async fn get_user_inbox(&self, user: &Username) -> anyhow::Result<Option<MailboxId>> {
293 let row =
294 sqlx::query("SELECT id FROM mailboxes WHERE username = $1 AND path = 'INBOX' LIMIT 1")
295 .bind(user.to_string())
296 .fetch_optional(&self.pool)
297 .await?;
298
299 if let Some(row) = row {
300 let uuid: uuid::Uuid = row.get(0);
301 Ok(Some(MailboxId::from_uuid(uuid)))
302 } else {
303 Ok(None)
304 }
305 }
306
307 async fn subscribe_mailbox(&self, user: &Username, mailbox_name: String) -> anyhow::Result<()> {
308 sqlx::query(
309 "INSERT INTO subscriptions (username, mailbox_name) VALUES ($1, $2) ON CONFLICT DO NOTHING"
310 )
311 .bind(user.to_string())
312 .bind(mailbox_name)
313 .execute(&self.pool)
314 .await?;
315
316 Ok(())
317 }
318
319 async fn unsubscribe_mailbox(&self, user: &Username, mailbox_name: &str) -> anyhow::Result<()> {
320 sqlx::query("DELETE FROM subscriptions WHERE username = $1 AND mailbox_name = $2")
321 .bind(user.to_string())
322 .bind(mailbox_name)
323 .execute(&self.pool)
324 .await?;
325
326 Ok(())
327 }
328
329 async fn list_subscriptions(&self, user: &Username) -> anyhow::Result<Vec<String>> {
330 let rows = sqlx::query("SELECT mailbox_name FROM subscriptions WHERE username = $1")
331 .bind(user.to_string())
332 .fetch_all(&self.pool)
333 .await?;
334
335 let subscriptions = rows
336 .into_iter()
337 .filter_map(|row| row.try_get("mailbox_name").ok())
338 .collect();
339
340 Ok(subscriptions)
341 }
342}
343
344struct PostgresCompleteMessageStore {
346 pool: PgPool,
347}
348
349#[async_trait]
350impl MessageStore for PostgresCompleteMessageStore {
351 async fn append_message(
352 &self,
353 mailbox_id: &MailboxId,
354 message: Mail,
355 ) -> anyhow::Result<MessageMetadata> {
356 let uid_row = sqlx::query("SELECT uid_next FROM mailboxes WHERE id = $1 FOR UPDATE")
358 .bind(*mailbox_id.as_uuid())
359 .fetch_one(&self.pool)
360 .await?;
361 let uid: i32 = uid_row.try_get("uid_next")?;
362
363 let headers_json = serde_json::json!({});
366 let body_bytes: &[u8] = b"";
367
368 sqlx::query(
370 r#"
371 INSERT INTO messages (id, mailbox_id, uid, sender, recipients, headers, body, size)
372 VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
373 "#,
374 )
375 .bind(*message.message_id().as_uuid())
376 .bind(*mailbox_id.as_uuid())
377 .bind(uid)
378 .bind(message.sender().map(|s| s.to_string()))
379 .bind(
380 message
381 .recipients()
382 .iter()
383 .map(|r| r.to_string())
384 .collect::<Vec<_>>(),
385 )
386 .bind(&headers_json)
387 .bind(body_bytes)
388 .bind(message.size() as i32)
389 .execute(&self.pool)
390 .await?;
391
392 sqlx::query("INSERT INTO message_flags (message_id, flag_recent) VALUES ($1, TRUE)")
394 .bind(*message.message_id().as_uuid())
395 .execute(&self.pool)
396 .await?;
397
398 sqlx::query("UPDATE mailboxes SET uid_next = $1 WHERE id = $2")
400 .bind(uid + 1)
401 .bind(*mailbox_id.as_uuid())
402 .execute(&self.pool)
403 .await?;
404
405 let metadata = MessageMetadata::new(
406 *message.message_id(),
407 *mailbox_id,
408 uid as u32,
409 MessageFlags::new(),
410 message.size(),
411 );
412
413 Ok(metadata)
414 }
415
416 async fn get_message(&self, _message_id: &MessageId) -> anyhow::Result<Option<Mail>> {
417 Ok(None)
420 }
421
422 async fn delete_messages(&self, message_ids: &[MessageId]) -> anyhow::Result<()> {
423 let uuids: Vec<uuid::Uuid> = message_ids.iter().map(|id| *id.as_uuid()).collect();
424
425 sqlx::query("DELETE FROM messages WHERE id = ANY($1)")
426 .bind(&uuids)
427 .execute(&self.pool)
428 .await?;
429
430 Ok(())
431 }
432
433 async fn set_flags(
434 &self,
435 message_ids: &[MessageId],
436 flags: MessageFlags,
437 ) -> anyhow::Result<()> {
438 let uuids: Vec<uuid::Uuid> = message_ids.iter().map(|id| *id.as_uuid()).collect();
439
440 sqlx::query(
441 r#"
442 UPDATE message_flags SET
443 flag_seen = $1,
444 flag_answered = $2,
445 flag_flagged = $3,
446 flag_deleted = $4,
447 flag_draft = $5
448 WHERE message_id = ANY($6)
449 "#,
450 )
451 .bind(flags.is_seen())
452 .bind(flags.is_answered())
453 .bind(flags.is_flagged())
454 .bind(flags.is_deleted())
455 .bind(flags.is_draft())
456 .bind(&uuids)
457 .execute(&self.pool)
458 .await?;
459
460 Ok(())
461 }
462
463 async fn search(
464 &self,
465 mailbox_id: &MailboxId,
466 criteria: SearchCriteria,
467 ) -> anyhow::Result<Vec<MessageId>> {
468 let query = match criteria {
469 SearchCriteria::All => {
470 sqlx::query("SELECT id FROM messages WHERE mailbox_id = $1")
471 .bind(*mailbox_id.as_uuid())
472 .fetch_all(&self.pool)
473 .await?
474 }
475 SearchCriteria::Unseen => {
476 sqlx::query(
477 r#"
478 SELECT m.id FROM messages m
479 JOIN message_flags f ON m.id = f.message_id
480 WHERE m.mailbox_id = $1 AND f.flag_seen = FALSE
481 "#,
482 )
483 .bind(*mailbox_id.as_uuid())
484 .fetch_all(&self.pool)
485 .await?
486 }
487 SearchCriteria::Seen => {
488 sqlx::query(
489 r#"
490 SELECT m.id FROM messages m
491 JOIN message_flags f ON m.id = f.message_id
492 WHERE m.mailbox_id = $1 AND f.flag_seen = TRUE
493 "#,
494 )
495 .bind(*mailbox_id.as_uuid())
496 .fetch_all(&self.pool)
497 .await?
498 }
499 SearchCriteria::Subject(text) => {
500 sqlx::query(
501 r#"
502 SELECT id FROM messages
503 WHERE mailbox_id = $1 AND search_vector @@ plainto_tsquery('english', $2)
504 "#,
505 )
506 .bind(*mailbox_id.as_uuid())
507 .bind(text)
508 .fetch_all(&self.pool)
509 .await?
510 }
511 SearchCriteria::From(email) => {
512 sqlx::query("SELECT id FROM messages WHERE mailbox_id = $1 AND sender ILIKE $2")
513 .bind(*mailbox_id.as_uuid())
514 .bind(format!("%{}%", email))
515 .fetch_all(&self.pool)
516 .await?
517 }
518 _ => Vec::new(),
519 };
520
521 let _message_ids: Vec<MessageId> = query
522 .into_iter()
523 .filter_map(|row| {
524 let _uuid: uuid::Uuid = row.try_get("id").ok()?;
525 Some(MessageId::new())
526 })
527 .collect();
528
529 Ok(vec![])
530 }
531
532 async fn copy_messages(
533 &self,
534 message_ids: &[MessageId],
535 dest_mailbox_id: &MailboxId,
536 ) -> anyhow::Result<Vec<MessageMetadata>> {
537 let mut metadata_list = Vec::new();
538
539 for message_id in message_ids {
540 let message = self.get_message(message_id).await?;
541 if let Some(msg) = message {
542 let metadata = self.append_message(dest_mailbox_id, msg).await?;
543 metadata_list.push(metadata);
544 }
545 }
546
547 Ok(metadata_list)
548 }
549
550 async fn get_mailbox_messages(
551 &self,
552 mailbox_id: &MailboxId,
553 ) -> anyhow::Result<Vec<MessageMetadata>> {
554 let rows = sqlx::query(
555 r#"
556 SELECT m.id, m.mailbox_id, m.uid, m.size,
557 f.flag_seen, f.flag_answered, f.flag_flagged,
558 f.flag_deleted, f.flag_draft, f.flag_recent
559 FROM messages m
560 LEFT JOIN message_flags f ON m.id = f.message_id
561 WHERE m.mailbox_id = $1
562 ORDER BY m.uid
563 "#,
564 )
565 .bind(*mailbox_id.as_uuid())
566 .fetch_all(&self.pool)
567 .await?;
568
569 let metadata_list = rows
570 .into_iter()
571 .filter_map(|row| {
572 let _msg_id: uuid::Uuid = row.try_get("id").ok()?;
573 let uid: i32 = row.try_get("uid").ok()?;
574 let size: i32 = row.try_get("size").ok()?;
575
576 let mut flags = MessageFlags::new();
577 if let Ok(seen) = row.try_get("flag_seen") {
578 flags.set_seen(seen);
579 }
580 if let Ok(answered) = row.try_get("flag_answered") {
581 flags.set_answered(answered);
582 }
583 if let Ok(flagged) = row.try_get("flag_flagged") {
584 flags.set_flagged(flagged);
585 }
586 if let Ok(deleted) = row.try_get("flag_deleted") {
587 flags.set_deleted(deleted);
588 }
589 if let Ok(draft) = row.try_get("flag_draft") {
590 flags.set_draft(draft);
591 }
592 if let Ok(recent) = row.try_get("flag_recent") {
593 flags.set_recent(recent);
594 }
595
596 Some(MessageMetadata::new(
597 MessageId::new(),
598 *mailbox_id,
599 uid as u32,
600 flags,
601 size as usize,
602 ))
603 })
604 .collect();
605
606 Ok(metadata_list)
607 }
608}
609
610struct PostgresCompleteMetadataStore {
612 pool: PgPool,
613}
614
615#[async_trait]
616impl MetadataStore for PostgresCompleteMetadataStore {
617 async fn get_user_quota(&self, user: &Username) -> anyhow::Result<Quota> {
618 let row = sqlx::query("SELECT used, quota_limit FROM user_quotas WHERE username = $1")
619 .bind(user.to_string())
620 .fetch_optional(&self.pool)
621 .await?;
622
623 match row {
624 Some(r) => {
625 let used: i64 = r.try_get("used")?;
626 let limit: i64 = r.try_get("quota_limit")?;
627 Ok(Quota::new(used as u64, limit as u64))
628 }
629 None => Ok(Quota::new(0, 1024 * 1024 * 1024)), }
631 }
632
633 async fn set_user_quota(&self, user: &Username, quota: Quota) -> anyhow::Result<()> {
634 sqlx::query(
635 r#"
636 INSERT INTO user_quotas (username, used, quota_limit)
637 VALUES ($1, $2, $3)
638 ON CONFLICT (username) DO UPDATE
639 SET used = $2, quota_limit = $3, updated_at = NOW()
640 "#,
641 )
642 .bind(user.to_string())
643 .bind(quota.used as i64)
644 .bind(quota.limit as i64)
645 .execute(&self.pool)
646 .await?;
647
648 Ok(())
649 }
650
651 async fn get_mailbox_counters(
652 &self,
653 mailbox_id: &MailboxId,
654 ) -> anyhow::Result<MailboxCounters> {
655 let row = sqlx::query(
656 r#"
657 SELECT
658 COUNT(*) as total,
659 COUNT(*) FILTER (WHERE f.flag_recent = TRUE) as recent,
660 COUNT(*) FILTER (WHERE f.flag_seen = FALSE) as unseen
661 FROM messages m
662 LEFT JOIN message_flags f ON m.id = f.message_id
663 WHERE m.mailbox_id = $1
664 "#,
665 )
666 .bind(*mailbox_id.as_uuid())
667 .fetch_one(&self.pool)
668 .await?;
669
670 let total: i64 = row.try_get("total")?;
671 let recent: i64 = row.try_get("recent")?;
672 let unseen: i64 = row.try_get("unseen")?;
673
674 Ok(MailboxCounters {
675 exists: total as u32,
676 recent: recent as u32,
677 unseen: unseen as u32,
678 })
679 }
680}
681
682#[cfg(test)]
683mod tests {
684 use super::*;
685
686 #[test]
687 fn test_postgres_complete_backend_struct() {
688 let _ = std::mem::size_of::<PostgresCompleteBackend>();
690 }
691
692 #[tokio::test]
693 async fn test_init_schema_creates_tables() {
694 let schema = r#"
697 CREATE TABLE IF NOT EXISTS mailboxes (
698 id UUID PRIMARY KEY,
699 username TEXT NOT NULL,
700 path TEXT NOT NULL,
701 uid_validity INTEGER NOT NULL,
702 uid_next INTEGER NOT NULL,
703 special_use TEXT,
704 created_at TIMESTAMP NOT NULL DEFAULT NOW(),
705 updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
706 UNIQUE(username, path)
707 )
708 "#;
709 assert!(schema.contains("CREATE TABLE"));
710 }
711
712 #[test]
713 fn test_search_criteria_all() {
714 let criteria = SearchCriteria::All;
715 assert!(matches!(criteria, SearchCriteria::All));
716 }
717
718 #[test]
719 fn test_search_criteria_unseen() {
720 let criteria = SearchCriteria::Unseen;
721 assert!(matches!(criteria, SearchCriteria::Unseen));
722 }
723
724 #[test]
725 fn test_search_criteria_from() {
726 let criteria = SearchCriteria::From("test@example.com".to_string());
727 assert!(matches!(criteria, SearchCriteria::From(_)));
728 }
729
730 #[test]
731 fn test_search_criteria_subject() {
732 let criteria = SearchCriteria::Subject("test subject".to_string());
733 assert!(matches!(criteria, SearchCriteria::Subject(_)));
734 }
735
736 #[test]
737 fn test_message_flags_default() {
738 let flags = MessageFlags::new();
739 assert!(!flags.is_seen());
740 assert!(!flags.is_answered());
741 assert!(!flags.is_flagged());
742 assert!(!flags.is_deleted());
743 assert!(!flags.is_draft());
744 }
745
746 #[test]
747 fn test_message_flags_setters() {
748 let mut flags = MessageFlags::new();
749 flags.set_seen(true);
750 flags.set_answered(true);
751 flags.set_flagged(true);
752
753 assert!(flags.is_seen());
754 assert!(flags.is_answered());
755 assert!(flags.is_flagged());
756 }
757
758 #[test]
759 fn test_quota_new() {
760 let quota = Quota::new(1024, 2048);
761 assert_eq!(quota.used, 1024);
762 assert_eq!(quota.limit, 2048);
763 }
764
765 #[test]
766 fn test_quota_exceeded() {
767 let quota = Quota::new(2048, 1024);
768 assert!(quota.is_exceeded());
769
770 let quota_ok = Quota::new(512, 1024);
771 assert!(!quota_ok.is_exceeded());
772 }
773
774 #[test]
775 fn test_quota_remaining() {
776 let quota = Quota::new(256, 1024);
777 assert_eq!(quota.remaining(), 768);
778 }
779
780 #[test]
781 fn test_mailbox_counters_default() {
782 let counters = MailboxCounters::default();
783 assert_eq!(counters.exists, 0);
784 assert_eq!(counters.recent, 0);
785 assert_eq!(counters.unseen, 0);
786 }
787
788 #[test]
789 fn test_mailbox_id_new() {
790 let id1 = MailboxId::new();
791 let id2 = MailboxId::new();
792 assert_ne!(id1, id2);
793 }
794
795 #[test]
796 fn test_mailbox_id_display() {
797 let id = MailboxId::new();
798 let display = format!("{}", id);
799 assert!(!display.is_empty());
800 }
801
802 #[test]
803 fn test_mailbox_path_creation() {
804 let user = Username::new("test@example.com".to_string()).unwrap();
805 let path = MailboxPath::new(user.clone(), vec!["INBOX".to_string()]);
806 assert_eq!(path.user(), &user);
807 assert_eq!(path.path().len(), 1);
808 }
809
810 #[test]
811 fn test_mailbox_path_name() {
812 let user = Username::new("test@example.com".to_string()).unwrap();
813 let path = MailboxPath::new(user, vec!["INBOX".to_string(), "Sent".to_string()]);
814 assert_eq!(path.name(), Some("Sent"));
815 }
816
817 #[test]
818 fn test_mailbox_new() {
819 let user = Username::new("test@example.com".to_string()).unwrap();
820 let path = MailboxPath::new(user, vec!["INBOX".to_string()]);
821 let mailbox = Mailbox::new(path);
822
823 assert_eq!(mailbox.uid_validity(), 1);
824 assert_eq!(mailbox.uid_next(), 1);
825 assert!(mailbox.special_use().is_none());
826 }
827
828 #[test]
829 fn test_mailbox_special_use() {
830 let user = Username::new("test@example.com".to_string()).unwrap();
831 let path = MailboxPath::new(user, vec!["Sent".to_string()]);
832 let mut mailbox = Mailbox::new(path);
833
834 mailbox.set_special_use(Some("\\Sent".to_string()));
835 assert_eq!(mailbox.special_use(), Some("\\Sent"));
836 }
837
838 #[test]
839 fn test_message_metadata_new() {
840 let msg_id = MessageId::new();
841 let mailbox_id = MailboxId::new();
842 let flags = MessageFlags::new();
843
844 let metadata = MessageMetadata::new(msg_id, mailbox_id, 1, flags, 1024);
845
846 assert_eq!(metadata.message_id(), &msg_id);
847 assert_eq!(metadata.mailbox_id(), &mailbox_id);
848 assert_eq!(metadata.uid(), 1);
849 assert_eq!(metadata.size(), 1024);
850 }
851
852 #[test]
853 fn test_message_metadata_getters() {
854 let msg_id = MessageId::new();
855 let mailbox_id = MailboxId::new();
856 let metadata = MessageMetadata::new(msg_id, mailbox_id, 42, MessageFlags::new(), 2048);
857
858 assert_eq!(*metadata.message_id(), msg_id);
859 assert_eq!(*metadata.mailbox_id(), mailbox_id);
860 assert_eq!(metadata.uid(), 42);
861 assert_eq!(metadata.size(), 2048);
862 }
863
864 #[test]
865 fn test_search_criteria_and() {
866 let criteria = SearchCriteria::And(vec![
867 SearchCriteria::Unseen,
868 SearchCriteria::From("test@example.com".to_string()),
869 ]);
870 assert!(matches!(criteria, SearchCriteria::And(_)));
871 }
872
873 #[test]
874 fn test_search_criteria_or() {
875 let criteria = SearchCriteria::Or(vec![SearchCriteria::Flagged, SearchCriteria::Deleted]);
876 assert!(matches!(criteria, SearchCriteria::Or(_)));
877 }
878
879 #[test]
880 fn test_search_criteria_not() {
881 let criteria = SearchCriteria::Not(Box::new(SearchCriteria::Seen));
882 assert!(matches!(criteria, SearchCriteria::Not(_)));
883 }
884
885 #[test]
886 fn test_mailbox_counters_struct() {
887 let counters = MailboxCounters {
888 exists: 10,
889 recent: 3,
890 unseen: 5,
891 };
892 assert_eq!(counters.exists, 10);
893 assert_eq!(counters.recent, 3);
894 assert_eq!(counters.unseen, 5);
895 }
896}