1use crate::{Dirs, LearnError, io_error, now};
28use serde::{Deserialize, Serialize};
29use sioul_core::config::{Account, Config};
30use std::collections::BTreeSet;
31
32#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
34pub struct Consent {
35 #[serde(default)]
37 pub pressed: i64,
38 #[serde(default)]
40 pub addresses: BTreeSet<String>,
41}
42
43impl Consent {
44 pub fn load(dirs: &Dirs) -> Consent {
47 std::fs::read_to_string(dirs.consent()).ok().and_then(|t| toml::from_str(&t).ok()).unwrap_or_default()
48 }
49
50 pub fn given(dirs: &Dirs, named: impl IntoIterator<Item = String>) -> Result<Consent, LearnError> {
53 let consent = Consent { pressed: now(), addresses: named.into_iter().map(|a| a.trim().to_lowercase()).filter(|a| !a.is_empty()).collect() };
54 let path = dirs.consent();
55 std::fs::create_dir_all(&dirs.data).map_err(|e| io_error(&dirs.data, e))?;
56 let text = toml::to_string(&consent).map_err(|e| io_error(&path, e))?;
57 let beside = path.with_extension("toml.new");
58 std::fs::write(&beside, text).map_err(|e| io_error(&beside, e))?;
59 std::fs::rename(&beside, &path).map_err(|e| io_error(&path, e))?;
60 Ok(consent)
61 }
62
63 pub fn allows(&self, account: &Account) -> bool {
67 if !account.is_microsoft() {
68 return true;
69 }
70 let kind = if account.is_microsoft_work() { "work" } else { "personal" };
71 self.allows_kept(Some(kind), account.address.as_deref())
72 }
73
74 pub fn allows_kept(&self, microsoft: Option<&str>, address: Option<&str>) -> bool {
79 match microsoft {
80 None => true,
81 Some("personal") => address.is_some_and(|a| self.addresses.contains(&a.trim().to_lowercase())),
82 Some(_) => false,
83 }
84 }
85}
86
87#[derive(Debug, Clone, Default, PartialEq, Eq)]
93pub struct MicrosoftIds(BTreeSet<String>);
94
95impl MicrosoftIds {
96 pub fn of(config: &Config) -> MicrosoftIds {
97 MicrosoftIds(config.every_account().filter(|a| a.is_microsoft()).map(|a| a.id.clone()).collect())
98 }
99
100 pub fn contains(&self, id: &str) -> bool {
101 self.0.contains(id)
102 }
103}
104
105pub fn named(config: &Config) -> Vec<String> {
108 config.accounts.iter().filter(|a| a.syncs() && !(a.is_microsoft() && a.is_microsoft_work())).map(label).collect()
109}
110
111pub fn work(config: &Config) -> Vec<String> {
113 config.accounts.iter().filter(|a| a.syncs() && a.is_microsoft() && a.is_microsoft_work()).map(label).collect()
114}
115
116fn label(account: &Account) -> String {
118 account.address.clone().unwrap_or_else(|| account.id.clone())
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use sioul_core::config::Security;
125
126 fn microsoft(id: &str, address: &str, kind: Option<&str>) -> Account {
127 let mut account = Account::imap(id, address, "outlook.office365.com", 993, Security::Tls, None);
128 account.auth = Some("microsoft".into());
129 account.microsoft_kind = kind.map(str::to_string);
130 account
131 }
132
133 #[test]
137 fn a_microsoft_address_is_learned_from_once_named() {
138 let root = std::env::temp_dir().join(format!("sioul-learn-consent-{}", std::process::id()));
139 let _ = std::fs::remove_dir_all(&root);
140 let dirs = Dirs::under(&root);
141 let plain = Account::imap("home", "me@example.org", "imap.example.org", 993, Security::Tls, None);
142 let personal = microsoft("live", "Me@Outlook.example", Some("personal"));
143 let work = microsoft("office", "me@contoso.example", Some("work"));
144 let config = Config { accounts: vec![plain.clone(), personal.clone(), work.clone()], ..Config::default() };
145 let none = Consent::load(&dirs);
147 assert_eq!(none, Consent::default());
148 assert!(none.allows(&plain) && !none.allows(&personal) && !none.allows(&work));
149 assert_eq!(named(&config), ["me@example.org", "Me@Outlook.example"]);
150 assert_eq!(super::work(&config), ["me@contoso.example"]);
151 let given = Consent::given(&dirs, named(&config).into_iter().chain(["me@contoso.example".to_string()])).unwrap();
153 assert_eq!(Consent::load(&dirs), given);
154 assert!(given.allows(&personal) && given.allows(&plain) && !given.allows(&work));
155 assert!(given.allows_kept(Some("personal"), Some("me@outlook.example")) && given.allows_kept(None, None));
157 assert!(!given.allows_kept(Some("work"), Some("me@contoso.example")) && !given.allows_kept(Some("personal"), Some("other@outlook.example")) && !given.allows_kept(Some("personal"), None));
158 let again = Consent::given(&dirs, ["me@example.org".to_string()]).unwrap();
160 assert!(!again.allows(&personal) && !again.allows_kept(Some("personal"), Some("me@outlook.example")));
161 let unknown = microsoft("old", "me@outlook.example", None);
163 assert!(!Consent::given(&dirs, ["me@outlook.example".to_string()]).unwrap().allows(&unknown));
164 std::fs::write(dirs.consent(), "addresses = 3").unwrap();
166 assert_eq!(Consent::load(&dirs), Consent::default());
167 std::fs::remove_dir_all(&root).unwrap();
168 }
169}