1use std::collections::{BTreeMap, BTreeSet};
4use std::future::Future;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use anyhow::{Context as _, Result, bail, ensure};
9use async_channel::{self, Receiver, Sender};
10use futures::FutureExt as _;
11use futures_lite::FutureExt as _;
12use serde::{Deserialize, Serialize};
13use tokio::fs;
14use tokio::io::AsyncWriteExt;
15use tokio::task::{JoinHandle, JoinSet};
16use uuid::Uuid;
17
18#[cfg(not(target_os = "ios"))]
19use tokio::sync::oneshot;
20#[cfg(not(target_os = "ios"))]
21use tokio::time::{Duration, sleep};
22
23use crate::context::{Context, ContextBuilder};
24use crate::events::{Event, EventEmitter, EventType, Events};
25use crate::log::warn;
26use crate::push::PushSubscriber;
27use crate::stock_str::StockStrings;
28
29#[derive(Debug)]
31pub struct Accounts {
32 dir: PathBuf,
33 config: Config,
34 accounts: BTreeMap<u32, Context>,
36
37 events: Events,
39
40 pub(crate) stockstrings: StockStrings,
45
46 push_subscriber: PushSubscriber,
48
49 background_fetch_interrupt_sender: Arc<parking_lot::Mutex<Option<Sender<()>>>>,
55}
56
57impl Accounts {
58 pub async fn new(dir: PathBuf, writable: bool) -> Result<Self> {
60 if writable {
61 Self::ensure_accounts_dir(&dir).await?;
62 }
63 let events = Events::new();
64 Accounts::open(events, dir, writable).await
65 }
66
67 pub async fn new_with_events(dir: PathBuf, writable: bool, events: Events) -> Result<Self> {
70 if writable {
71 Self::ensure_accounts_dir(&dir).await?;
72 }
73 Accounts::open(events, dir, writable).await
74 }
75
76 fn get_id(&self) -> u32 {
81 0
82 }
83
84 async fn ensure_accounts_dir(dir: &Path) -> Result<()> {
88 if !dir.exists() {
89 fs::create_dir_all(dir)
90 .await
91 .context("Failed to create folder")?;
92 Config::new(dir).await?;
93 } else if !dir.join(CONFIG_NAME).exists() {
94 let mut rd = fs::read_dir(dir).await?;
95 ensure!(rd.next_entry().await?.is_none(), "{dir:?} is not empty");
96 Config::new(dir).await?;
97 }
98 Ok(())
99 }
100
101 async fn open(events: Events, dir: PathBuf, writable: bool) -> Result<Self> {
104 ensure!(dir.exists(), "directory does not exist");
105
106 let config_file = dir.join(CONFIG_NAME);
107 ensure!(config_file.exists(), "{config_file:?} does not exist");
108
109 let config = Config::from_file(config_file, writable).await?;
110
111 let stockstrings = StockStrings::new();
112 let push_subscriber = PushSubscriber::new();
113 let accounts = config
114 .load_accounts(&events, &stockstrings, push_subscriber.clone(), &dir)
115 .await
116 .context("failed to load accounts")?;
117
118 Ok(Self {
119 dir,
120 config,
121 accounts,
122 events,
123 stockstrings,
124 push_subscriber,
125 background_fetch_interrupt_sender: Default::default(),
126 })
127 }
128
129 pub fn get_account(&self, id: u32) -> Option<Context> {
131 self.accounts.get(&id).cloned()
132 }
133
134 pub fn get_selected_account(&self) -> Option<Context> {
136 let id = self.config.get_selected_account();
137 self.accounts.get(&id).cloned()
138 }
139
140 pub fn get_selected_account_id(&self) -> Option<u32> {
142 match self.config.get_selected_account() {
143 0 => None,
144 id => Some(id),
145 }
146 }
147
148 pub async fn select_account(&mut self, id: u32) -> Result<()> {
150 self.config.select_account(id).await?;
151
152 Ok(())
153 }
154
155 pub async fn add_account(&mut self) -> Result<u32> {
159 let account_config = self.config.new_account().await?;
160 let dbfile = account_config.dbfile(&self.dir);
161
162 let ctx = ContextBuilder::new(dbfile)
163 .with_id(account_config.id)
164 .with_events(self.events.clone())
165 .with_stock_strings(self.stockstrings.clone())
166 .with_push_subscriber(self.push_subscriber.clone())
167 .build()
168 .await?;
169 ctx.open("".to_string()).await?;
172
173 self.accounts.insert(account_config.id, ctx);
174 self.emit_event(EventType::AccountsChanged);
175
176 Ok(account_config.id)
177 }
178
179 pub async fn add_closed_account(&mut self) -> Result<u32> {
181 let account_config = self.config.new_account().await?;
182 let dbfile = account_config.dbfile(&self.dir);
183
184 let ctx = ContextBuilder::new(dbfile)
185 .with_id(account_config.id)
186 .with_events(self.events.clone())
187 .with_stock_strings(self.stockstrings.clone())
188 .with_push_subscriber(self.push_subscriber.clone())
189 .build()
190 .await?;
191 self.accounts.insert(account_config.id, ctx);
192 self.emit_event(EventType::AccountsChanged);
193
194 Ok(account_config.id)
195 }
196
197 pub async fn remove_account(&mut self, id: u32) -> Result<()> {
199 let ctx = self
200 .accounts
201 .remove(&id)
202 .with_context(|| format!("no account with id {id}"))?;
203 ctx.stop_io().await;
204
205 ctx.sql.close().await;
217 drop(ctx);
218
219 if let Some(cfg) = self.config.get_account(id) {
220 let account_path = self.dir.join(cfg.dir);
221
222 try_many_times(|| fs::remove_dir_all(&account_path))
223 .await
224 .context("failed to remove account data")?;
225 }
226 self.config.remove_account(id).await?;
227 self.emit_event(EventType::AccountsChanged);
228
229 Ok(())
230 }
231
232 pub async fn migrate_account(&mut self, dbfile: PathBuf) -> Result<u32> {
236 let blobdir = Context::derive_blobdir(&dbfile);
237 let walfile = Context::derive_walfile(&dbfile);
238
239 ensure!(dbfile.exists(), "no database found: {}", dbfile.display());
240 ensure!(blobdir.exists(), "no blobdir found: {}", blobdir.display());
241
242 let old_id = self.config.get_selected_account();
243
244 let account_config = self
246 .config
247 .new_account()
248 .await
249 .context("failed to create new account")?;
250
251 let new_dbfile = account_config.dbfile(&self.dir);
252 let new_blobdir = Context::derive_blobdir(&new_dbfile);
253 let new_walfile = Context::derive_walfile(&new_dbfile);
254
255 let res = {
256 fs::create_dir_all(self.dir.join(&account_config.dir))
257 .await
258 .context("failed to create dir")?;
259 try_many_times(|| fs::rename(&dbfile, &new_dbfile))
260 .await
261 .context("failed to rename dbfile")?;
262 try_many_times(|| fs::rename(&blobdir, &new_blobdir))
263 .await
264 .context("failed to rename blobdir")?;
265 if walfile.exists() {
266 fs::rename(&walfile, &new_walfile)
267 .await
268 .context("failed to rename walfile")?;
269 }
270 Ok(())
271 };
272
273 match res {
274 Ok(_) => {
275 let ctx = Context::new(
276 &new_dbfile,
277 account_config.id,
278 self.events.clone(),
279 self.stockstrings.clone(),
280 )
281 .await?;
282 self.accounts.insert(account_config.id, ctx);
283 Ok(account_config.id)
284 }
285 Err(err) => {
286 let account_path = std::path::PathBuf::from(&account_config.dir);
287 try_many_times(|| fs::remove_dir_all(&account_path))
288 .await
289 .context("failed to remove account data")?;
290 self.config.remove_account(account_config.id).await?;
291
292 self.select_account(old_id).await?;
294
295 Err(err)
296 }
297 }
298 }
299
300 pub fn get_all(&self) -> Vec<u32> {
302 let mut ordered_ids = Vec::new();
303 let mut all_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
304
305 for &id in &self.config.inner.accounts_order {
307 if all_ids.remove(&id) {
308 ordered_ids.push(id);
309 }
310 }
311
312 for id in all_ids {
314 ordered_ids.push(id);
315 }
316
317 ordered_ids
318 }
319
320 pub async fn set_accounts_order(&mut self, order: Vec<u32>) -> Result<()> {
326 let existing_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
327
328 let mut filtered_order: Vec<u32> = order
330 .into_iter()
331 .filter(|id| existing_ids.contains(id))
332 .collect();
333
334 for &id in &existing_ids {
336 if !filtered_order.contains(&id) {
337 filtered_order.push(id);
338 }
339 }
340
341 self.config.inner.accounts_order = filtered_order;
342 self.config.sync().await?;
343 self.emit_event(EventType::AccountsChanged);
344 Ok(())
345 }
346
347 pub async fn start_io(&mut self) {
349 for account in self.accounts.values_mut() {
350 account.start_io().await;
351 }
352 }
353
354 pub async fn stop_io(&self) {
356 info!(self, "Stopping IO for all accounts.");
359 for account in self.accounts.values() {
360 account.stop_io().await;
361 }
362 }
363
364 pub async fn maybe_network(&self) {
366 for account in self.accounts.values() {
367 account.scheduler.maybe_network().await;
368 }
369 }
370
371 pub async fn maybe_network_lost(&self) {
373 for account in self.accounts.values() {
374 account.scheduler.maybe_network_lost(account).await;
375 }
376 }
377
378 async fn background_fetch_no_timeout(accounts: Vec<Context>, events: Events) {
388 let n_accounts = accounts.len();
389 events.emit(Event {
390 id: 0,
391 typ: EventType::Info(format!(
392 "Starting background fetch for {n_accounts} accounts."
393 )),
394 });
395 ::tracing::event!(
396 ::tracing::Level::INFO,
397 account_id = 0,
398 "Starting background fetch for {n_accounts} accounts."
399 );
400 let mut set = JoinSet::new();
401 for account in accounts {
402 set.spawn(async move {
403 if let Err(error) = account.background_fetch().await {
404 warn!(account, "{error:#}");
405 }
406 });
407 }
408 set.join_all().await;
409 events.emit(Event {
410 id: 0,
411 typ: EventType::Info(format!(
412 "Finished background fetch for {n_accounts} accounts."
413 )),
414 });
415 ::tracing::event!(
416 ::tracing::Level::INFO,
417 account_id = 0,
418 "Finished background fetch for {n_accounts} accounts."
419 );
420 }
421
422 async fn background_fetch_with_timeout(
436 accounts: Vec<Context>,
437 events: Events,
438 timeout: std::time::Duration,
439 interrupt_sender: Arc<parking_lot::Mutex<Option<Sender<()>>>>,
440 interrupt_receiver: Option<Receiver<()>>,
441 ) {
442 let Some(interrupt_receiver) = interrupt_receiver else {
443 return;
445 };
446 if let Err(_err) = tokio::time::timeout(
447 timeout,
448 Self::background_fetch_no_timeout(accounts, events.clone())
449 .race(interrupt_receiver.recv().map(|_| ())),
450 )
451 .await
452 {
453 events.emit(Event {
454 id: 0,
455 typ: EventType::Warning("Background fetch timed out.".to_string()),
456 });
457 ::tracing::event!(
458 ::tracing::Level::WARN,
459 account_id = 0,
460 "Background fetch timed out."
461 );
462 }
463 events.emit(Event {
464 id: 0,
465 typ: EventType::AccountsBackgroundFetchDone,
466 });
467 (*interrupt_sender.lock()) = None;
468 }
469
470 pub fn background_fetch(
484 &self,
485 timeout: std::time::Duration,
486 ) -> impl Future<Output = ()> + use<> {
487 let accounts: Vec<Context> = self.accounts.values().cloned().collect();
488 let events = self.events.clone();
489 let (sender, receiver) = async_channel::bounded(1);
490 let receiver = {
491 let mut lock = self.background_fetch_interrupt_sender.lock();
492 if (*lock).is_some() {
493 None
496 } else {
497 *lock = Some(sender);
498 Some(receiver)
499 }
500 };
501 Self::background_fetch_with_timeout(
502 accounts,
503 events,
504 timeout,
505 self.background_fetch_interrupt_sender.clone(),
506 receiver,
507 )
508 }
509
510 pub fn stop_background_fetch(&self) {
518 let mut lock = self.background_fetch_interrupt_sender.lock();
519 if let Some(sender) = lock.take() {
520 sender.try_send(()).ok();
521 }
522 }
523
524 pub fn emit_event(&self, event: EventType) {
526 self.events.emit(Event { id: 0, typ: event })
527 }
528
529 pub fn get_event_emitter(&self) -> EventEmitter {
531 self.events.get_emitter()
532 }
533
534 pub async fn set_push_device_token(&self, token: &str) -> Result<()> {
536 self.push_subscriber.set_device_token(token).await;
537 Ok(())
538 }
539}
540
541const CONFIG_NAME: &str = "accounts.toml";
543
544#[cfg(not(target_os = "ios"))]
546const LOCKFILE_NAME: &str = "accounts.lock";
547
548const DB_NAME: &str = "dc.db";
550
551#[derive(Debug)]
553struct Config {
554 file: PathBuf,
555 inner: InnerConfig,
556 lock_task: Option<JoinHandle<anyhow::Result<()>>>,
559}
560
561#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
565struct InnerConfig {
566 pub selected_account: u32,
568 pub next_id: u32,
569 pub accounts: Vec<AccountConfig>,
570 #[serde(default)]
573 pub accounts_order: Vec<u32>,
574}
575
576impl Drop for Config {
577 fn drop(&mut self) {
578 if let Some(lock_task) = self.lock_task.take() {
579 lock_task.abort();
580 }
581 }
582}
583
584impl Config {
585 #[cfg(target_os = "ios")]
586 async fn create_lock_task(_dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
587 Ok(None)
591 }
592
593 #[cfg(not(target_os = "ios"))]
594 #[expect(clippy::arithmetic_side_effects)]
595 async fn create_lock_task(dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
596 let lockfile = dir.join(LOCKFILE_NAME);
597 let mut lock = fd_lock::RwLock::new(fs::File::create(lockfile).await?);
598 let (locked_tx, locked_rx) = oneshot::channel();
599 let lock_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
600 let mut timeout = Duration::from_millis(100);
601 let _guard = loop {
602 match lock.try_write() {
603 Ok(guard) => break Ok(guard),
604 Err(err) => {
605 if timeout.as_millis() > 1600 {
606 break Err(err);
607 }
608 sleep(timeout).await;
612 if err.kind() == std::io::ErrorKind::WouldBlock {
613 timeout *= 2;
614 }
615 }
616 }
617 }?;
618 locked_tx
619 .send(())
620 .ok()
621 .context("Cannot notify about lockfile locking")?;
622 let (_tx, rx) = oneshot::channel();
623 rx.await?;
624 Ok(())
625 });
626 if locked_rx.await.is_err() {
627 bail!(
628 "Delta Chat is already running. To use Delta Chat, you must first close the existing Delta Chat process, or restart your device. (accounts.lock file is already locked)"
629 );
630 };
631 Ok(Some(lock_task))
632 }
633
634 async fn new_nosync(file: PathBuf, lock: bool) -> Result<Self> {
636 let dir = file.parent().context("Cannot get config file directory")?;
637 let inner = InnerConfig {
638 accounts: Vec::new(),
639 selected_account: 0,
640 next_id: 1,
641 accounts_order: Vec::new(),
642 };
643 if !lock {
644 let cfg = Self {
645 file,
646 inner,
647 lock_task: None,
648 };
649 return Ok(cfg);
650 }
651 let lock_task = Self::create_lock_task(dir.to_path_buf()).await?;
652 let cfg = Self {
653 file,
654 inner,
655 lock_task,
656 };
657 Ok(cfg)
658 }
659
660 pub async fn new(dir: &Path) -> Result<Self> {
662 let lock = true;
663 let mut cfg = Self::new_nosync(dir.join(CONFIG_NAME), lock).await?;
664 cfg.sync().await?;
665
666 Ok(cfg)
667 }
668
669 async fn sync(&mut self) -> Result<()> {
673 #[cfg(not(target_os = "ios"))]
674 ensure!(
675 !self
676 .lock_task
677 .as_ref()
678 .context("Config is read-only")?
679 .is_finished()
680 );
681
682 let tmp_path = self.file.with_extension("toml.tmp");
683 let mut file = fs::File::create(&tmp_path)
684 .await
685 .context("failed to create a tmp config")?;
686 file.write_all(toml::to_string_pretty(&self.inner)?.as_bytes())
687 .await
688 .context("failed to write a tmp config")?;
689
690 file.sync_all()
696 .await
697 .context("failed to sync a tmp config")?;
698 drop(file);
699 fs::rename(&tmp_path, &self.file)
700 .await
701 .context("failed to rename config")?;
702 #[cfg(not(windows))]
704 {
705 let parent = self.file.parent().context("No parent directory")?;
706 let parent_file = fs::File::open(parent).await?;
707 parent_file.sync_all().await?;
708 }
709
710 Ok(())
711 }
712
713 pub async fn from_file(file: PathBuf, writable: bool) -> Result<Self> {
715 let mut config = Self::new_nosync(file, writable).await?;
716 let bytes = fs::read(&config.file)
717 .await
718 .context("Failed to read file")?;
719 let s = std::str::from_utf8(&bytes)?;
720 config.inner = toml::from_str(s).context("Failed to parse config")?;
721
722 let mut modified = false;
725 for account in &mut config.inner.accounts {
726 if account.dir.is_absolute()
727 && let Some(old_path_parent) = account.dir.parent()
728 && let Ok(new_path) = account.dir.strip_prefix(old_path_parent)
729 {
730 account.dir = new_path.to_path_buf();
731 modified = true;
732 }
733 }
734 if modified && writable {
735 config.sync().await?;
736 }
737
738 Ok(config)
739 }
740
741 pub async fn load_accounts(
746 &self,
747 events: &Events,
748 stockstrings: &StockStrings,
749 push_subscriber: PushSubscriber,
750 dir: &Path,
751 ) -> Result<BTreeMap<u32, Context>> {
752 let mut accounts = BTreeMap::new();
753
754 for account_config in &self.inner.accounts {
755 let dbfile = account_config.dbfile(dir);
756 let ctx = ContextBuilder::new(dbfile.clone())
757 .with_id(account_config.id)
758 .with_events(events.clone())
759 .with_stock_strings(stockstrings.clone())
760 .with_push_subscriber(push_subscriber.clone())
761 .build()
762 .await
763 .with_context(|| format!("failed to create context from file {:?}", &dbfile))?;
764 ctx.open("".to_string()).await?;
767
768 accounts.insert(account_config.id, ctx);
769 }
770
771 Ok(accounts)
772 }
773
774 #[expect(clippy::arithmetic_side_effects)]
776 async fn new_account(&mut self) -> Result<AccountConfig> {
777 let id = {
778 let id = self.inner.next_id;
779 let uuid = Uuid::new_v4();
780 let target_dir = PathBuf::from(uuid.to_string());
781
782 self.inner.accounts.push(AccountConfig {
783 id,
784 dir: target_dir,
785 uuid,
786 });
787 self.inner.next_id += 1;
788
789 self.inner.accounts_order.push(id);
791
792 id
793 };
794
795 self.sync().await?;
796
797 self.select_account(id)
798 .await
799 .context("failed to select just added account")?;
800 let cfg = self
801 .get_account(id)
802 .context("failed to get just added account")?;
803 Ok(cfg)
804 }
805
806 pub async fn remove_account(&mut self, id: u32) -> Result<()> {
808 {
809 if let Some(idx) = self.inner.accounts.iter().position(|e| e.id == id) {
810 self.inner.accounts.remove(idx);
812 }
813
814 self.inner.accounts_order.retain(|&x| x != id);
816
817 if self.inner.selected_account == id {
818 self.inner.selected_account = self
820 .inner
821 .accounts
822 .first()
823 .map(|e| e.id)
824 .unwrap_or_default();
825 }
826 }
827
828 self.sync().await
829 }
830
831 fn get_account(&self, id: u32) -> Option<AccountConfig> {
833 self.inner.accounts.iter().find(|e| e.id == id).cloned()
834 }
835
836 pub fn get_selected_account(&self) -> u32 {
838 self.inner.selected_account
839 }
840
841 pub async fn select_account(&mut self, id: u32) -> Result<()> {
843 {
844 ensure!(
845 self.inner.accounts.iter().any(|e| e.id == id),
846 "invalid account id: {id}"
847 );
848
849 self.inner.selected_account = id;
850 }
851
852 self.sync().await?;
853 Ok(())
854 }
855}
856
857#[expect(clippy::arithmetic_side_effects)]
866async fn try_many_times<F, Fut, T>(f: F) -> std::result::Result<(), T>
867where
868 F: Fn() -> Fut,
869 Fut: Future<Output = std::result::Result<(), T>>,
870{
871 let mut counter = 0;
872 loop {
873 counter += 1;
874
875 if let Err(err) = f().await {
876 if counter > 60 {
877 return Err(err);
878 }
879
880 tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
882 } else {
883 break;
884 }
885 }
886 Ok(())
887}
888
889#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
891struct AccountConfig {
892 pub id: u32,
894
895 pub dir: std::path::PathBuf,
899
900 pub uuid: Uuid,
902}
903
904impl AccountConfig {
905 pub fn dbfile(&self, accounts_dir: &Path) -> std::path::PathBuf {
907 accounts_dir.join(&self.dir).join(DB_NAME)
908 }
909}
910
911#[cfg(test)]
912mod tests {
913 use super::*;
914 use crate::stock_str::{self, StockMessage};
915
916 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
917 async fn test_account_new_open() {
918 let dir = tempfile::tempdir().unwrap();
919 let p: PathBuf = dir.path().join("accounts1");
920
921 {
922 let writable = true;
923 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
924 accounts.add_account().await.unwrap();
925
926 assert_eq!(accounts.accounts.len(), 1);
927 assert_eq!(accounts.config.get_selected_account(), 1);
928 }
929 for writable in [true, false] {
930 let accounts = Accounts::new(p.clone(), writable).await.unwrap();
931
932 assert_eq!(accounts.accounts.len(), 1);
933 assert_eq!(accounts.config.get_selected_account(), 1);
934 }
935 }
936
937 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
938 async fn test_account_new_empty_existing_dir() {
939 let dir = tempfile::tempdir().unwrap();
940 let p: PathBuf = dir.path().join("accounts");
941
942 fs::create_dir_all(&p).await.unwrap();
944 fs::write(p.join("stray_file.txt"), b"hello").await.unwrap();
945 assert!(Accounts::new(p.clone(), true).await.is_err());
946
947 fs::remove_file(p.join("stray_file.txt")).await.unwrap();
949
950 let mut accounts = Accounts::new(p.clone(), true).await.unwrap();
952 assert_eq!(accounts.accounts.len(), 0);
953 let id = accounts.add_account().await.unwrap();
954 assert_eq!(id, 1);
955 }
956
957 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
958 async fn test_account_new_open_conflict() {
959 let dir = tempfile::tempdir().unwrap();
960 let p: PathBuf = dir.path().join("accounts");
961 let writable = true;
962 let _accounts = Accounts::new(p.clone(), writable).await.unwrap();
963
964 let writable = true;
965 assert!(Accounts::new(p.clone(), writable).await.is_err());
966
967 let writable = false;
968 let accounts = Accounts::new(p, writable).await.unwrap();
969 assert_eq!(accounts.accounts.len(), 0);
970 assert_eq!(accounts.config.get_selected_account(), 0);
971 }
972
973 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
974 async fn test_account_new_add_remove() {
975 let dir = tempfile::tempdir().unwrap();
976 let p: PathBuf = dir.path().join("accounts");
977
978 let writable = true;
979 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
980 assert_eq!(accounts.accounts.len(), 0);
981 assert_eq!(accounts.config.get_selected_account(), 0);
982
983 let id = accounts.add_account().await.unwrap();
984 assert_eq!(id, 1);
985 assert_eq!(accounts.accounts.len(), 1);
986 assert_eq!(accounts.config.get_selected_account(), 1);
987
988 let id = accounts.add_account().await.unwrap();
989 assert_eq!(id, 2);
990 assert_eq!(accounts.config.get_selected_account(), id);
991 assert_eq!(accounts.accounts.len(), 2);
992
993 accounts.select_account(1).await.unwrap();
994 assert_eq!(accounts.config.get_selected_account(), 1);
995
996 accounts.remove_account(1).await.unwrap();
997 assert_eq!(accounts.config.get_selected_account(), 2);
998 assert_eq!(accounts.accounts.len(), 1);
999 }
1000
1001 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1002 async fn test_accounts_remove_last() -> Result<()> {
1003 let dir = tempfile::tempdir()?;
1004 let p: PathBuf = dir.path().join("accounts");
1005
1006 let writable = true;
1007 let mut accounts = Accounts::new(p.clone(), writable).await?;
1008 assert!(accounts.get_selected_account().is_none());
1009 assert_eq!(accounts.config.get_selected_account(), 0);
1010
1011 let id = accounts.add_account().await?;
1012 assert!(accounts.get_selected_account().is_some());
1013 assert_eq!(id, 1);
1014 assert_eq!(accounts.accounts.len(), 1);
1015 assert_eq!(accounts.config.get_selected_account(), id);
1016
1017 accounts.remove_account(id).await?;
1018 assert!(accounts.get_selected_account().is_none());
1019
1020 Ok(())
1021 }
1022
1023 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1024 async fn test_migrate_account() {
1025 let dir = tempfile::tempdir().unwrap();
1026 let p: PathBuf = dir.path().join("accounts");
1027
1028 let writable = true;
1029 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
1030 assert_eq!(accounts.accounts.len(), 0);
1031 assert_eq!(accounts.config.get_selected_account(), 0);
1032
1033 let extern_dbfile: PathBuf = dir.path().join("other");
1034 let ctx = Context::new(&extern_dbfile, 0, Events::new(), StockStrings::new())
1035 .await
1036 .unwrap();
1037 ctx.set_config(crate::config::Config::Addr, Some("me@mail.com"))
1038 .await
1039 .unwrap();
1040
1041 drop(ctx);
1042
1043 accounts
1044 .migrate_account(extern_dbfile.clone())
1045 .await
1046 .unwrap();
1047 assert_eq!(accounts.accounts.len(), 1);
1048 assert_eq!(accounts.config.get_selected_account(), 1);
1049
1050 let ctx = accounts.get_selected_account().unwrap();
1051 assert_eq!(
1052 "me@mail.com",
1053 ctx.get_config(crate::config::Config::Addr)
1054 .await
1055 .unwrap()
1056 .unwrap()
1057 );
1058 }
1059
1060 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1062 async fn test_accounts_sorted() {
1063 let dir = tempfile::tempdir().unwrap();
1064 let p: PathBuf = dir.path().join("accounts");
1065
1066 let writable = true;
1067 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
1068
1069 for expected_id in 1..10 {
1070 let id = accounts.add_account().await.unwrap();
1071 assert_eq!(id, expected_id);
1072 }
1073
1074 let ids = accounts.get_all();
1075 for (i, expected_id) in (1..10).enumerate() {
1076 assert_eq!(ids.get(i), Some(&expected_id));
1077 }
1078 }
1079
1080 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1081 async fn test_accounts_ids_unique_increasing_and_persisted() -> Result<()> {
1082 let dir = tempfile::tempdir()?;
1083 let p: PathBuf = dir.path().join("accounts");
1084 let dummy_accounts = 10;
1085
1086 let (id0, id1, id2) = {
1087 let writable = true;
1088 let mut accounts = Accounts::new(p.clone(), writable).await?;
1089 accounts.add_account().await?;
1090 let ids = accounts.get_all();
1091 assert_eq!(ids.len(), 1);
1092
1093 let id0 = *ids.first().unwrap();
1094 let ctx = accounts.get_account(id0).unwrap();
1095 ctx.set_config(crate::config::Config::Addr, Some("one@example.org"))
1096 .await?;
1097
1098 let id1 = accounts.add_account().await?;
1099 let ctx = accounts.get_account(id1).unwrap();
1100 ctx.set_config(crate::config::Config::Addr, Some("two@example.org"))
1101 .await?;
1102
1103 for _ in 0..dummy_accounts {
1105 let to_delete = accounts.add_account().await?;
1106 accounts.remove_account(to_delete).await?;
1107 }
1108
1109 let id2 = accounts.add_account().await?;
1110 let ctx = accounts.get_account(id2).unwrap();
1111 ctx.set_config(crate::config::Config::Addr, Some("three@example.org"))
1112 .await?;
1113
1114 accounts.select_account(id1).await?;
1115
1116 (id0, id1, id2)
1117 };
1118 assert!(id0 > 0);
1119 assert!(id1 > id0);
1120 assert!(id2 > id1 + dummy_accounts);
1121
1122 let (id0_reopened, id1_reopened, id2_reopened) = {
1123 let writable = false;
1124 let accounts = Accounts::new(p.clone(), writable).await?;
1125 let ctx = accounts.get_selected_account().unwrap();
1126 assert_eq!(
1127 ctx.get_config(crate::config::Config::Addr).await?,
1128 Some("two@example.org".to_string())
1129 );
1130
1131 let ids = accounts.get_all();
1132 assert_eq!(ids.len(), 3);
1133
1134 let id0 = *ids.first().unwrap();
1135 let ctx = accounts.get_account(id0).unwrap();
1136 assert_eq!(
1137 ctx.get_config(crate::config::Config::Addr).await?,
1138 Some("one@example.org".to_string())
1139 );
1140
1141 let id1 = *ids.get(1).unwrap();
1142 let t = accounts.get_account(id1).unwrap();
1143 assert_eq!(
1144 t.get_config(crate::config::Config::Addr).await?,
1145 Some("two@example.org".to_string())
1146 );
1147
1148 let id2 = *ids.get(2).unwrap();
1149 let ctx = accounts.get_account(id2).unwrap();
1150 assert_eq!(
1151 ctx.get_config(crate::config::Config::Addr).await?,
1152 Some("three@example.org".to_string())
1153 );
1154
1155 (id0, id1, id2)
1156 };
1157 assert_eq!(id0, id0_reopened);
1158 assert_eq!(id1, id1_reopened);
1159 assert_eq!(id2, id2_reopened);
1160
1161 Ok(())
1162 }
1163
1164 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1165 async fn test_no_accounts_event_emitter() -> Result<()> {
1166 let dir = tempfile::tempdir().unwrap();
1167 let p: PathBuf = dir.path().join("accounts");
1168
1169 let writable = true;
1170 let accounts = Accounts::new(p.clone(), writable).await?;
1171
1172 assert_eq!(accounts.accounts.len(), 0);
1174
1175 let event_emitter = accounts.get_event_emitter();
1177
1178 let duration = std::time::Duration::from_millis(1);
1180 assert!(
1181 tokio::time::timeout(duration, event_emitter.recv())
1182 .await
1183 .is_err()
1184 );
1185
1186 drop(accounts);
1188 assert_eq!(event_emitter.recv().await, None);
1189
1190 Ok(())
1191 }
1192
1193 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1194 async fn test_encrypted_account() -> Result<()> {
1195 let dir = tempfile::tempdir().context("failed to create tempdir")?;
1196 let p: PathBuf = dir.path().join("accounts");
1197
1198 let writable = true;
1199 let mut accounts = Accounts::new(p.clone(), writable)
1200 .await
1201 .context("failed to create accounts manager")?;
1202
1203 assert_eq!(accounts.accounts.len(), 0);
1204 let account_id = accounts
1205 .add_closed_account()
1206 .await
1207 .context("failed to add closed account")?;
1208 let account = accounts
1209 .get_selected_account()
1210 .context("failed to get account")?;
1211 assert_eq!(account.id, account_id);
1212 let passphrase_set_success = account
1213 .open("foobar".to_string())
1214 .await
1215 .context("failed to set passphrase")?;
1216 assert!(passphrase_set_success);
1217 drop(accounts);
1218
1219 let writable = false;
1220 let accounts = Accounts::new(p.clone(), writable)
1221 .await
1222 .context("failed to create second accounts manager")?;
1223 let account = accounts
1224 .get_selected_account()
1225 .context("failed to get account")?;
1226 assert_eq!(account.is_open().await, false);
1227
1228 assert_eq!(account.open("barfoo".to_string()).await?, false);
1230 assert_eq!(account.open("".to_string()).await?, false);
1231
1232 assert_eq!(account.open("foobar".to_string()).await?, true);
1233 assert_eq!(account.is_open().await, true);
1234
1235 Ok(())
1236 }
1237
1238 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1240 async fn test_accounts_share_translations() -> Result<()> {
1241 let dir = tempfile::tempdir().unwrap();
1242 let p: PathBuf = dir.path().join("accounts");
1243
1244 let writable = true;
1245 let mut accounts = Accounts::new(p.clone(), writable).await?;
1246 accounts.add_account().await?;
1247 accounts.add_account().await?;
1248
1249 let account1 = accounts.get_account(1).context("failed to get account 1")?;
1250 let account2 = accounts.get_account(2).context("failed to get account 2")?;
1251
1252 assert_eq!(stock_str::no_messages(&account1).await, "No messages.");
1253 assert_eq!(stock_str::no_messages(&account2).await, "No messages.");
1254 account1
1255 .set_stock_translation(StockMessage::NoMessages, "foobar".to_string())
1256 .await?;
1257 assert_eq!(stock_str::no_messages(&account1).await, "foobar");
1258 assert_eq!(stock_str::no_messages(&account2).await, "foobar");
1259
1260 Ok(())
1261 }
1262}