1use crate::backend::{QtThread, Shared, config_path, load_config, say, show, start_watcher, tell, tr, want_password};
13use cxx_qt_lib::QString;
14use sioul_core::config::{self, Account, AccountKind, Security};
15use sioul_sync::discover::MicrosoftKind;
16use sioul_sync::{Control, SyncError, microsoft, secret};
17use std::sync::Arc;
18use std::sync::atomic::Ordering;
19use std::time::{Duration, Instant};
20
21const ANSWER_WITHIN: Duration = Duration::from_secs(300);
24const WATCHER_ENDS_WITHIN: Duration = Duration::from_secs(30 * 60);
27
28fn at_microsoft(account: &Account) -> bool {
31 account.kind == AccountKind::Imap && (account.is_microsoft() || account.host.as_deref().is_some_and(microsoft::is_imap_host))
32}
33
34pub(crate) fn add_microsoft_mail(qt: &QtThread, shared: &Arc<Shared>, address: String, kind: String, client_id: String, tenant: String, again: Option<String>) {
41 let (qt, shared) = (qt.clone(), Arc::clone(shared));
42 shared.oauth_stop.store(false, Ordering::Relaxed);
43 std::thread::spawn(move || {
44 let address = address.trim().to_ascii_lowercase();
45 let config = load_config();
46 let outcome = (|| -> Result<Account, String> {
47 let existing = match &again {
49 Some(id) => Some(config.every_account().find(|a| &a.id == id && at_microsoft(a)).cloned().ok_or_else(|| say("account-unknown", &[("id", id.clone())]))?),
50 None => None,
51 };
52 if existing.is_none()
53 && let Some(have) = config.every_account().find(|a| a.address.as_deref() == Some(address.as_str()) && matches!(a.kind, AccountKind::Imap | AccountKind::Jmap))
54 {
55 return Err(say("account-address-exists", &[("address", address.clone()), ("id", have.id.clone())]));
56 }
57 let account = existing.clone().unwrap_or_else(|| Account::imap(&config.free_id(&address), &address, microsoft::IMAP_HOST, 993, Security::Tls, None));
58 let login = account.login().unwrap_or(&address).to_string();
59 let kind = match &existing {
61 Some(a) if a.is_microsoft() && !a.is_microsoft_work() => MicrosoftKind::Personal,
62 Some(a) if a.is_microsoft() => MicrosoftKind::Work,
63 _ => MicrosoftKind::parse(&kind),
64 };
65 let label = address.clone();
66 let app = microsoft::app_for(&login, &client_id).map_err(|e| e.sentence(tr(), &label))?.ok_or_else(|| tr().text("microsoft-no-app", None))?;
67 let authority = microsoft::authority_for(&login, kind, &tenant).map_err(|e| e.sentence(tr(), &label))?;
68 let opener = qt.clone();
69 let show_page = move |url: &str| {
70 let url = url.to_string();
71 let _ = opener.queue(move |mut sioul| sioul.as_mut().open_url(QString::from(&url)));
72 };
73 let taken: Vec<String> = config.every_account().filter(|a| a.is_microsoft() && Some(&a.id) != again.as_ref()).filter_map(|a| a.login().map(|l| l.trim().to_lowercase())).collect();
75 let signed = microsoft::sign_in(account, &app, &authority, &taken, show_page, &shared.oauth_stop).map_err(|e| e.sentence(tr(), &label))?;
76 let yes = signed.other_login.as_deref().is_none_or(|other| asked(&qt, &shared, &address, other));
78 let (account, _) = signed.answer(yes).map_err(|e| e.sentence(tr(), &label))?;
79 match &existing {
80 None => {
81 config::add_imap_account(&config_path(), &account)?;
82 config::set_sign_in(&config_path(), &account)?;
83 }
84 Some(before) => {
85 config::set_sign_in(&config_path(), &account)?;
86 if !before.is_microsoft() {
88 let _ = secret::forget(before);
89 }
90 }
91 }
92 Ok(account)
93 })();
94 let line = outcome.as_ref().ok().map(|a| match &again {
95 Some(_) => say("account-signed-in-again-microsoft", &[("account", a.id.clone())]),
96 None => say("account-added", &[("id", a.id.clone()), ("path", a.maildir_path().display().to_string())]),
97 });
98 if let Ok(account) = outcome.as_ref() {
99 want_password(&shared, &account.id, None);
100 if let Ok(mut statuses) = shared.statuses.lock() {
101 statuses.remove(&account.id);
102 }
103 if let Some(control) = shared.watchers.lock().ok().and_then(|mut w| w.remove(&account.id)) {
105 control.stop();
106 }
107 start_watcher(&qt, &shared, account.clone());
108 }
109 let error = outcome.err();
110 let _ = qt.queue(move |mut sioul| {
111 sioul.as_mut().set_form_busy(false);
112 match (line, error) {
113 (Some(line), _) => {
114 sioul.as_mut().set_found(QString::default());
115 sioul.as_mut().set_status(QString::from(&line));
116 sioul.as_mut().account_added();
117 }
118 (None, Some(e)) => sioul.as_mut().set_form_error(QString::from(&e)),
119 (None, None) => {}
120 }
121 });
122 show(&qt, &shared);
123 });
124}
125
126fn asked(qt: &QtThread, shared: &Shared, address: &str, other: &str) -> bool {
130 let (answer, answered) = std::sync::mpsc::channel();
131 match shared.microsoft_answer.lock() {
132 Ok(mut waiting) => *waiting = Some(answer),
133 Err(_) => return false,
134 }
135 let question = say("microsoft-other-login-question", &[("address", address.to_string()), ("login", other.to_string())]);
136 let _ = qt.queue(move |mut sioul| sioul.as_mut().microsoft_question(QString::from(&question)));
137 let until = Instant::now() + ANSWER_WITHIN;
138 let yes = loop {
139 match answered.recv_timeout(Duration::from_millis(200)) {
140 Ok(yes) => break yes,
141 Err(std::sync::mpsc::RecvTimeoutError::Timeout) if !shared.oauth_stop.load(Ordering::Relaxed) && Instant::now() < until => {}
142 Err(_) => break false,
143 }
144 };
145 if let Ok(mut waiting) = shared.microsoft_answer.lock() {
146 *waiting = None;
147 }
148 yes
149}
150
151pub(crate) fn has_own_app(address: &str) -> bool {
154 microsoft::own_app(address.trim()).is_some()
155}
156
157pub(crate) fn admin_text(address: &str) -> String {
164 let address = address.trim().to_ascii_lowercase();
165 let Ok(Some(app)) = microsoft::app_for(&address, "") else { return String::new() };
166 let tenant = microsoft::grant(&address).filter(|g| !g.personal).map(|g| g.tenant).unwrap_or_else(|| address.rsplit('@').next().unwrap_or("").to_string());
167 say("microsoft-admin-steps", &[("link", microsoft::admin_consent_url(&app, &tenant))])
168}
169
170pub(crate) fn admin_text_for(address: &str, client_id: &str) -> String {
174 let client_id = client_id.trim();
175 if !microsoft::is_app_id(client_id) {
176 return String::new();
177 }
178 let domain = address.trim().to_ascii_lowercase().rsplit('@').next().unwrap_or("").to_string();
179 say("microsoft-admin-steps", &[("link", microsoft::admin_consent_url(client_id, &domain))])
180}
181
182pub(crate) fn remove(qt: &QtThread, shared: &Arc<Shared>, account: Account, delete_mail: bool, watcher: Option<Arc<Control>>) {
191 if let Ok(mut removing) = shared.removing.lock() {
192 removing.insert(account.id.clone());
193 }
194 let (qt, shared) = (qt.clone(), Arc::clone(shared));
195 std::thread::spawn(move || {
196 let line = removed(&account, delete_mail, watcher.as_deref()).unwrap_or_else(|e| e);
197 if let Ok(mut removing) = shared.removing.lock() {
198 removing.remove(&account.id);
199 }
200 tell(&qt, &shared, line);
201 show(&qt, &shared);
202 });
203}
204
205fn removed(account: &Account, delete_mail: bool, watcher: Option<&Control>) -> Result<String, String> {
207 let id = account.id.clone();
208 let page = microsoft::forget(account.login().unwrap_or(&id), !account.is_microsoft_work()).map_err(|e: SyncError| e.sentence(tr(), &id))?;
209 config::remove_account(&config_path(), &id)?;
210 if !delete_mail {
211 return Ok(say("account-removed-microsoft", &[("id", id), ("path", account.maildir_path().display().to_string()), ("page", page.to_string())]));
212 }
213 delete_after(watcher, WATCHER_ENDS_WITHIN, || {
214 sioul_core::maildir::forget_account(account)?;
215 #[cfg(not(target_os = "android"))]
216 sioul_learn::Dirs::standard().forget_account(&account.id).map_err(|e| e.to_string())?;
217 Ok(())
218 })?;
219 Ok(say("account-removed-microsoft-mail", &[("id", id), ("page", page.to_string())]))
220}
221
222fn delete_after(watcher: Option<&Control>, within: Duration, delete: impl FnOnce() -> Result<(), String>) -> Result<(), String> {
225 if let Some(watcher) = watcher {
226 watcher.wait_ended(within);
227 }
228 delete()
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use std::sync::Mutex;
235
236 #[test]
240 fn mail_is_deleted_after_its_watcher_ends() {
241 let control = Control::default();
242 control.stop();
243 let order = Mutex::new(Vec::new());
244 std::thread::scope(|s| {
245 s.spawn(|| {
247 for _ in 0..2 {
248 std::thread::sleep(Duration::from_millis(100));
249 order.lock().unwrap().push("written");
250 }
251 control.end();
252 });
253 delete_after(Some(&control), Duration::from_secs(10), || {
254 order.lock().unwrap().push("deleted");
255 Ok(())
256 })
257 .unwrap();
258 });
259 assert_eq!(*order.lock().unwrap(), ["written", "written", "deleted"]);
260 let mut done = false;
261 delete_after(None, Duration::ZERO, || {
262 done = true;
263 Ok(())
264 })
265 .unwrap();
266 assert!(done, "no watcher: at once");
267 }
268}