1use super::defaults::{default_imap_port, default_true};
2use crate::error::{AppError, Result};
3use serde::{Deserialize, Serialize};
4
5#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
6#[serde(deny_unknown_fields)]
7pub struct ImapSection {
8 pub host: Option<String>,
9 #[serde(default = "default_imap_port")]
10 pub port: u16,
11 #[serde(default = "default_true")]
12 pub tls: bool,
13 pub username: Option<String>,
14 pub password_secret: Option<String>,
15 pub password_secret_env: Option<String>,
16}
17
18impl Default for ImapSection {
19 fn default() -> Self {
20 Self {
21 host: None,
22 port: default_imap_port(),
23 tls: true,
24 username: None,
25 password_secret: None,
26 password_secret_env: None,
27 }
28 }
29}
30
31#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
32#[serde(deny_unknown_fields)]
33pub struct ImapMailboxConfig {
34 #[serde(default)]
35 pub mailbox_name: Option<String>,
36 #[serde(default)]
37 pub special_use: Option<String>,
38}
39
40impl ImapMailboxConfig {
41 pub(super) fn empty() -> Self {
42 Self {
43 mailbox_name: None,
44 special_use: None,
45 }
46 }
47
48 pub(super) fn validate(&self, id: &str) -> Result<()> {
49 match (&self.mailbox_name, &self.special_use) {
50 (Some(mailbox), None) if !mailbox.trim().is_empty() => {}
51 (None, Some(special_use)) if !special_use.trim().is_empty() => {}
52 (Some(_), Some(_)) => {
53 return Err(AppError::new(
54 "config_invalid",
55 format!("mailboxes.{id}.mailbox_name and special_use are mutually exclusive"),
56 ));
57 }
58 _ => {
59 return Err(AppError::new(
60 "config_invalid",
61 format!("mailboxes.{id} must set exactly one of mailbox_name or special_use"),
62 ));
63 }
64 }
65 Ok(())
66 }
67
68 pub fn offline_mailbox_name(&self) -> Option<String> {
69 if let Some(mailbox) = &self.mailbox_name {
70 return Some(mailbox.clone());
71 }
72 self.special_use
73 .as_deref()
74 .and_then(SpecialUseKind::from_attribute)
75 .map(|kind| kind.fallback_names()[0].to_string())
76 }
77
78 pub fn matches_mailbox_offline(&self, mailbox_name: &str) -> bool {
79 if self.mailbox_name.as_deref() == Some(mailbox_name) {
80 return true;
81 }
82 self.special_use
83 .as_deref()
84 .and_then(SpecialUseKind::from_attribute)
85 .is_some_and(|kind| {
86 kind.fallback_names()
87 .iter()
88 .any(|name| mailbox_name.eq_ignore_ascii_case(name))
89 })
90 }
91}
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94pub enum SpecialUseKind {
95 Archive,
96 Junk,
97 Trash,
98 Sent,
99 Drafts,
100 All,
101 Flagged,
102}
103
104impl SpecialUseKind {
105 pub fn as_str(self) -> &'static str {
106 match self {
107 SpecialUseKind::Archive => "archive",
108 SpecialUseKind::Junk => "junk",
109 SpecialUseKind::Trash => "trash",
110 SpecialUseKind::Sent => "sent",
111 SpecialUseKind::Drafts => "drafts",
112 SpecialUseKind::All => "all",
113 SpecialUseKind::Flagged => "flagged",
114 }
115 }
116
117 #[allow(clippy::should_implement_trait)]
118 pub fn from_str(value: &str) -> Option<Self> {
119 match value.to_ascii_lowercase().as_str() {
120 "archive" => Some(SpecialUseKind::Archive),
121 "junk" | "spam" => Some(SpecialUseKind::Junk),
122 "trash" => Some(SpecialUseKind::Trash),
123 "sent" => Some(SpecialUseKind::Sent),
124 "drafts" => Some(SpecialUseKind::Drafts),
125 "all" => Some(SpecialUseKind::All),
126 "flagged" | "flag" => Some(SpecialUseKind::Flagged),
127 _ => None,
128 }
129 }
130
131 pub fn from_attribute(value: &str) -> Option<Self> {
132 special_use_kinds()
133 .iter()
134 .copied()
135 .find(|kind| value.eq_ignore_ascii_case(kind.attribute()))
136 }
137
138 pub fn attribute(self) -> &'static str {
139 match self {
140 SpecialUseKind::Archive => "\\Archive",
141 SpecialUseKind::Junk => "\\Junk",
142 SpecialUseKind::Trash => "\\Trash",
143 SpecialUseKind::Sent => "\\Sent",
144 SpecialUseKind::Drafts => "\\Drafts",
145 SpecialUseKind::All => "\\All",
146 SpecialUseKind::Flagged => "\\Flagged",
147 }
148 }
149
150 pub fn fallback_names(self) -> &'static [&'static str] {
151 match self {
152 SpecialUseKind::Archive => &["Archive", "Archives"],
153 SpecialUseKind::Junk => &["Junk", "Spam", "Junk Email", "Junk E-mail"],
154 SpecialUseKind::Trash => &["Trash", "Deleted Items", "Deleted Messages", "Bin"],
155 SpecialUseKind::Sent => &["Sent", "Sent Mail", "Sent Messages"],
156 SpecialUseKind::Drafts => &["Drafts", "Draft"],
157 SpecialUseKind::All => &["All Mail", "All"],
158 SpecialUseKind::Flagged => &["Flagged", "Starred"],
159 }
160 }
161
162 pub fn can_move_to(self) -> bool {
163 !matches!(self, SpecialUseKind::All | SpecialUseKind::Flagged)
164 }
165}
166
167pub fn special_use_kinds() -> &'static [SpecialUseKind] {
168 &[
169 SpecialUseKind::All,
170 SpecialUseKind::Archive,
171 SpecialUseKind::Drafts,
172 SpecialUseKind::Flagged,
173 SpecialUseKind::Junk,
174 SpecialUseKind::Sent,
175 SpecialUseKind::Trash,
176 ]
177}
178
179#[derive(Clone, Debug, PartialEq, Eq)]
180pub struct SpecialUseTarget {
181 pub kind: SpecialUseKind,
182 pub mailbox_name: String,
183 pub source: SpecialUseSource,
184 pub attribute: &'static str,
185 pub flag: Option<String>,
186 pub can_move_to: bool,
187}
188
189#[derive(Clone, Copy, Debug, PartialEq, Eq)]
190pub enum SpecialUseSource {
191 Mailboxes,
192 Rfc6154Attribute,
193 FallbackName,
194}
195
196impl SpecialUseSource {
197 pub fn as_str(self) -> &'static str {
198 match self {
199 SpecialUseSource::Mailboxes => "mailboxes",
200 SpecialUseSource::Rfc6154Attribute => "rfc6154_attribute",
201 SpecialUseSource::FallbackName => "fallback_name",
202 }
203 }
204}
205
206#[derive(Clone, Debug, PartialEq, Eq)]
207pub struct ImapConfig {
208 pub host: String,
209 pub port: u16,
210 pub tls: bool,
211 pub username: String,
212 pub password_secret: String,
213 pub mailboxes: Vec<String>,
214}