1use std::path::{Path, PathBuf};
7
8use serde::{Deserialize, Serialize};
9
10use crate::account::Account;
11use crate::error::CoreError;
12
13#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(default)]
15pub struct Config {
16 pub accounts: Vec<Account>,
17 pub defaults: Defaults,
18 pub trusted_senders: Vec<String>,
19}
20
21#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(default)]
23pub struct Defaults {
24 pub send: Option<String>,
25 pub calendar: Option<String>,
26}
27
28impl Config {
29 pub fn default_path() -> Result<PathBuf, CoreError> {
31 let dir = dirs::config_dir()
32 .ok_or(CoreError::NoConfigDir)?
33 .join("pidge");
34 std::fs::create_dir_all(&dir)?;
35 Ok(dir.join("config.yaml"))
36 }
37
38 pub fn load() -> Result<Self, CoreError> {
40 let path = Self::default_path()?;
41 Self::load_from(&path)
42 }
43
44 pub fn load_from(path: &Path) -> Result<Self, CoreError> {
46 if !path.exists() {
47 return Ok(Self::default());
48 }
49 let text = std::fs::read_to_string(path)?;
50 Ok(serde_yaml::from_str(&text)?)
51 }
52
53 pub fn save(&self) -> Result<(), CoreError> {
55 let path = Self::default_path()?;
56 self.save_to(&path)
57 }
58
59 pub fn save_to(&self, path: &Path) -> Result<(), CoreError> {
61 let text = serde_yaml::to_string(self)?;
62 std::fs::write(path, text)?;
63 Ok(())
64 }
65
66 pub fn add_account(&mut self, account: Account) {
69 if let Some(existing) = self.accounts.iter_mut().find(|a| a.email == account.email) {
70 *existing = account;
71 return;
72 }
73 if self.accounts.is_empty() {
74 self.defaults.send = Some(account.email.clone());
75 self.defaults.calendar = Some(account.email.clone());
76 }
77 self.accounts.push(account);
78 }
79
80 pub fn remove_account(&mut self, email: &str) -> Option<Account> {
83 let idx = self.accounts.iter().position(|a| a.email == email)?;
84 let removed = self.accounts.remove(idx);
85 if self.defaults.send.as_deref() == Some(email) {
86 self.defaults.send = None;
87 }
88 if self.defaults.calendar.as_deref() == Some(email) {
89 self.defaults.calendar = None;
90 }
91 Some(removed)
92 }
93
94 pub fn set_default_send(&mut self, email: &str) -> Result<(), CoreError> {
96 if !self.accounts.iter().any(|a| a.email == email) {
97 return Err(CoreError::UnknownAccount {
98 email: email.to_string(),
99 });
100 }
101 self.defaults.send = Some(email.to_string());
102 Ok(())
103 }
104
105 pub fn set_default_calendar(&mut self, email: &str) -> Result<(), CoreError> {
107 if !self.accounts.iter().any(|a| a.email == email) {
108 return Err(CoreError::UnknownAccount {
109 email: email.to_string(),
110 });
111 }
112 self.defaults.calendar = Some(email.to_string());
113 Ok(())
114 }
115
116 pub fn find(&self, email: &str) -> Option<&Account> {
118 self.accounts.iter().find(|a| a.email == email)
119 }
120
121 pub fn add_trusted_sender(&mut self, email: &str) {
123 let lower = email.to_lowercase();
124 if !self
125 .trusted_senders
126 .iter()
127 .any(|s| s.to_lowercase() == lower)
128 {
129 self.trusted_senders.push(email.to_string());
130 }
131 }
132
133 pub fn remove_trusted_sender(&mut self, email: &str) -> bool {
136 let lower = email.to_lowercase();
137 let before = self.trusted_senders.len();
138 self.trusted_senders.retain(|s| s.to_lowercase() != lower);
139 before != self.trusted_senders.len()
140 }
141
142 pub fn is_sender_trusted(&self, email: &str) -> bool {
144 let lower = email.to_lowercase();
145 self.trusted_senders
146 .iter()
147 .any(|s| s.to_lowercase() == lower)
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use chrono::TimeZone;
155
156 fn make_account(email: &str) -> Account {
157 Account {
158 email: email.into(),
159 tenant_id: "tid".into(),
160 home_account_id: "home".into(),
161 added_at: chrono::Utc.with_ymd_and_hms(2026, 5, 13, 22, 0, 0).unwrap(),
162 storage: crate::TokenStorage::default(),
163 }
164 }
165
166 #[test]
167 fn empty_config_serializes_and_deserializes() {
168 let c = Config::default();
169 let yaml = serde_yaml::to_string(&c).unwrap();
170 let c2: Config = serde_yaml::from_str(&yaml).unwrap();
171 assert_eq!(c, c2);
172 }
173
174 #[test]
175 fn first_added_account_becomes_both_defaults() {
176 let mut c = Config::default();
177 c.add_account(make_account("a@b.com"));
178 assert_eq!(c.defaults.send.as_deref(), Some("a@b.com"));
179 assert_eq!(c.defaults.calendar.as_deref(), Some("a@b.com"));
180 }
181
182 #[test]
183 fn second_added_account_does_not_change_defaults() {
184 let mut c = Config::default();
185 c.add_account(make_account("a@b.com"));
186 c.add_account(make_account("c@d.com"));
187 assert_eq!(c.defaults.send.as_deref(), Some("a@b.com"));
188 assert_eq!(c.defaults.calendar.as_deref(), Some("a@b.com"));
189 assert_eq!(c.accounts.len(), 2);
190 }
191
192 #[test]
193 fn removing_default_account_clears_default() {
194 let mut c = Config::default();
195 c.add_account(make_account("a@b.com"));
196 c.add_account(make_account("c@d.com"));
197 c.remove_account("a@b.com");
198 assert_eq!(c.defaults.send, None);
199 assert_eq!(c.defaults.calendar, None);
200 }
201
202 #[test]
203 fn set_default_send_for_unknown_account_errors() {
204 let mut c = Config::default();
205 c.add_account(make_account("a@b.com"));
206 assert!(matches!(
207 c.set_default_send("ghost@nowhere.com"),
208 Err(CoreError::UnknownAccount { .. })
209 ));
210 }
211
212 #[test]
213 fn config_roundtrips_through_file() {
214 let tmp = tempfile::TempDir::new().unwrap();
215 let path = tmp.path().join("config.yaml");
216
217 let mut c = Config::default();
218 c.add_account(make_account("a@b.com"));
219 c.add_account(make_account("c@d.com"));
220 c.set_default_calendar("c@d.com").unwrap();
221 c.save_to(&path).unwrap();
222
223 let c2 = Config::load_from(&path).unwrap();
224 assert_eq!(c, c2);
225 }
226
227 #[test]
228 fn add_trusted_sender_is_idempotent() {
229 let mut c = Config::default();
230 c.add_trusted_sender("a@b.com");
231 c.add_trusted_sender("a@b.com");
232 assert_eq!(c.trusted_senders.len(), 1);
233 }
234
235 #[test]
236 fn add_trusted_sender_is_case_insensitive() {
237 let mut c = Config::default();
238 c.add_trusted_sender("Maria@MKLab.se");
239 c.add_trusted_sender("maria@mklab.se");
240 assert_eq!(c.trusted_senders.len(), 1);
241 }
242
243 #[test]
244 fn remove_trusted_sender_returns_true_when_present() {
245 let mut c = Config::default();
246 c.add_trusted_sender("a@b.com");
247 assert!(c.remove_trusted_sender("a@b.com"));
248 assert!(c.trusted_senders.is_empty());
249 }
250
251 #[test]
252 fn remove_trusted_sender_returns_false_when_absent() {
253 let mut c = Config::default();
254 assert!(!c.remove_trusted_sender("ghost@nowhere.com"));
255 }
256
257 #[test]
258 fn remove_trusted_sender_is_case_insensitive() {
259 let mut c = Config::default();
260 c.add_trusted_sender("Maria@MKLab.se");
261 assert!(c.remove_trusted_sender("MARIA@mklab.SE"));
262 assert!(c.trusted_senders.is_empty());
263 }
264
265 #[test]
266 fn is_sender_trusted_case_insensitive() {
267 let mut c = Config::default();
268 c.add_trusted_sender("Maria@MKLab.se");
269 assert!(c.is_sender_trusted("maria@mklab.se"));
270 assert!(c.is_sender_trusted("MARIA@MKLAB.SE"));
271 assert!(!c.is_sender_trusted("anna@mklab.se"));
272 }
273
274 #[test]
275 fn config_with_missing_trusted_senders_loads_as_empty() {
276 let yaml = "accounts: []\ndefaults: {}\n";
277 let c: Config = serde_yaml::from_str(yaml).unwrap();
278 assert!(c.trusted_senders.is_empty());
279 }
280}