1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::path::PathBuf;
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
6pub struct AccountId(pub String);
7
8impl AccountId {
9 pub fn as_str(&self) -> &str {
10 &self.0
11 }
12}
13
14impl fmt::Display for AccountId {
15 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16 f.write_str(&self.0)
17 }
18}
19
20impl From<&str> for AccountId {
21 fn from(s: &str) -> Self {
22 AccountId(s.to_string())
23 }
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct Account {
28 pub id: AccountId,
29 pub display_name: String,
30 pub maildir_path: PathBuf,
31 pub address: Option<String>,
32 pub mbsync_channel: Option<String>,
33 pub msmtp_account: Option<String>,
34 pub folders: Vec<Folder>,
35}
36
37impl Account {
38 pub fn tag_query(&self) -> String {
39 format!("tag:{}", self.id)
40 }
41
42 pub fn path_query(&self) -> String {
43 format!("path:\"{}/**\"", self.id)
44 }
45
46 pub fn folder(&self, name: &str) -> Option<&Folder> {
47 self.folders.iter().find(|f| f.name == name)
48 }
49
50 pub fn can_sync(&self) -> bool {
51 self.mbsync_channel.is_some()
52 }
53
54 pub fn can_send(&self) -> bool {
55 self.msmtp_account.is_some()
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(rename_all = "snake_case")]
61pub enum FolderKind {
62 Inbox,
63 Sent,
64 Drafts,
65 Trash,
66 Junk,
67 Archive,
68 Other,
69}
70
71impl FolderKind {
72 pub fn classify(name: &str) -> Self {
73 let leaf = name.rsplit('/').next().unwrap_or(name);
74 match leaf.to_ascii_lowercase().as_str() {
75 "inbox" => FolderKind::Inbox,
76 "sent" | "sent mail" | "sent items" => FolderKind::Sent,
77 "drafts" | "draft" => FolderKind::Drafts,
78 "trash" | "deleted items" | "bin" => FolderKind::Trash,
79 "junk" | "spam" => FolderKind::Junk,
80 "archive" | "all mail" => FolderKind::Archive,
81 _ => FolderKind::Other,
82 }
83 }
84
85 pub fn sort_rank(&self) -> u8 {
86 match self {
87 FolderKind::Inbox => 0,
88 FolderKind::Drafts => 1,
89 FolderKind::Sent => 2,
90 FolderKind::Archive => 3,
91 FolderKind::Junk => 4,
92 FolderKind::Trash => 5,
93 FolderKind::Other => 6,
94 }
95 }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99pub struct Folder {
100 pub name: String,
101 pub relative_path: String,
102 pub kind: FolderKind,
103}
104
105impl Folder {
106 pub fn new(account: &AccountId, relative_path: impl Into<String>) -> Self {
107 let relative_path = relative_path.into();
108 let name = relative_path.clone();
109 Self {
110 kind: FolderKind::classify(&name),
111 relative_path: format!("{account}/{relative_path}"),
112 name,
113 }
114 }
115
116 pub fn query(&self) -> String {
117 format!("path:\"{}/**\"", self.relative_path)
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn folder_query_is_recursive_and_quoted() {
127 let folder = Folder::new(&AccountId::from("main"), "Inbox");
128 assert_eq!(folder.query(), "path:\"main/Inbox/**\"");
129 }
130
131 #[test]
132 fn folder_names_with_spaces_stay_quoted() {
133 let folder = Folder::new(&AccountId::from("main"), "General Payments");
134 assert_eq!(folder.query(), "path:\"main/General Payments/**\"");
135 }
136
137 #[test]
138 fn gmail_style_nested_folders_classify_on_the_leaf() {
139 assert_eq!(FolderKind::classify("[Gmail]/Sent Mail"), FolderKind::Sent);
140 assert_eq!(
141 FolderKind::classify("[Gmail]/All Mail"),
142 FolderKind::Archive
143 );
144 assert_eq!(FolderKind::classify("Swiggy"), FolderKind::Other);
145 }
146
147 #[test]
148 fn classification_is_case_insensitive() {
149 assert_eq!(FolderKind::classify("INBOX"), FolderKind::Inbox);
150 assert_eq!(FolderKind::classify("Junk"), FolderKind::Junk);
151 }
152}