Skip to main content

ecr_core/
account.rs

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}