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 && !dir.exists() {
61 Accounts::create(&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 && !dir.exists() {
71 Accounts::create(&dir).await?;
72 }
73
74 Accounts::open(events, dir, writable).await
75 }
76
77 fn get_id(&self) -> u32 {
82 0
83 }
84
85 async fn create(dir: &Path) -> Result<()> {
87 fs::create_dir_all(dir)
88 .await
89 .context("failed to create folder")?;
90
91 Config::new(dir).await?;
92
93 Ok(())
94 }
95
96 async fn open(events: Events, dir: PathBuf, writable: bool) -> Result<Self> {
99 ensure!(dir.exists(), "directory does not exist");
100
101 let config_file = dir.join(CONFIG_NAME);
102 ensure!(config_file.exists(), "{config_file:?} does not exist");
103
104 let config = Config::from_file(config_file, writable).await?;
105
106 let stockstrings = StockStrings::new();
107 let push_subscriber = PushSubscriber::new();
108 let accounts = config
109 .load_accounts(&events, &stockstrings, push_subscriber.clone(), &dir)
110 .await
111 .context("failed to load accounts")?;
112
113 Ok(Self {
114 dir,
115 config,
116 accounts,
117 events,
118 stockstrings,
119 push_subscriber,
120 background_fetch_interrupt_sender: Default::default(),
121 })
122 }
123
124 pub fn get_account(&self, id: u32) -> Option<Context> {
126 self.accounts.get(&id).cloned()
127 }
128
129 pub fn get_selected_account(&self) -> Option<Context> {
131 let id = self.config.get_selected_account();
132 self.accounts.get(&id).cloned()
133 }
134
135 pub fn get_selected_account_id(&self) -> Option<u32> {
137 match self.config.get_selected_account() {
138 0 => None,
139 id => Some(id),
140 }
141 }
142
143 pub async fn select_account(&mut self, id: u32) -> Result<()> {
145 self.config.select_account(id).await?;
146
147 Ok(())
148 }
149
150 pub async fn add_account(&mut self) -> Result<u32> {
154 let account_config = self.config.new_account().await?;
155 let dbfile = account_config.dbfile(&self.dir);
156
157 let ctx = ContextBuilder::new(dbfile)
158 .with_id(account_config.id)
159 .with_events(self.events.clone())
160 .with_stock_strings(self.stockstrings.clone())
161 .with_push_subscriber(self.push_subscriber.clone())
162 .build()
163 .await?;
164 ctx.open("".to_string()).await?;
167
168 self.accounts.insert(account_config.id, ctx);
169 self.emit_event(EventType::AccountsChanged);
170
171 Ok(account_config.id)
172 }
173
174 pub async fn add_closed_account(&mut self) -> Result<u32> {
176 let account_config = self.config.new_account().await?;
177 let dbfile = account_config.dbfile(&self.dir);
178
179 let ctx = ContextBuilder::new(dbfile)
180 .with_id(account_config.id)
181 .with_events(self.events.clone())
182 .with_stock_strings(self.stockstrings.clone())
183 .with_push_subscriber(self.push_subscriber.clone())
184 .build()
185 .await?;
186 self.accounts.insert(account_config.id, ctx);
187 self.emit_event(EventType::AccountsChanged);
188
189 Ok(account_config.id)
190 }
191
192 pub async fn remove_account(&mut self, id: u32) -> Result<()> {
194 let ctx = self
195 .accounts
196 .remove(&id)
197 .with_context(|| format!("no account with id {id}"))?;
198 ctx.stop_io().await;
199
200 ctx.sql.close().await;
212 drop(ctx);
213
214 if let Some(cfg) = self.config.get_account(id) {
215 let account_path = self.dir.join(cfg.dir);
216
217 try_many_times(|| fs::remove_dir_all(&account_path))
218 .await
219 .context("failed to remove account data")?;
220 }
221 self.config.remove_account(id).await?;
222 self.emit_event(EventType::AccountsChanged);
223
224 Ok(())
225 }
226
227 pub async fn migrate_account(&mut self, dbfile: PathBuf) -> Result<u32> {
231 let blobdir = Context::derive_blobdir(&dbfile);
232 let walfile = Context::derive_walfile(&dbfile);
233
234 ensure!(dbfile.exists(), "no database found: {}", dbfile.display());
235 ensure!(blobdir.exists(), "no blobdir found: {}", blobdir.display());
236
237 let old_id = self.config.get_selected_account();
238
239 let account_config = self
241 .config
242 .new_account()
243 .await
244 .context("failed to create new account")?;
245
246 let new_dbfile = account_config.dbfile(&self.dir);
247 let new_blobdir = Context::derive_blobdir(&new_dbfile);
248 let new_walfile = Context::derive_walfile(&new_dbfile);
249
250 let res = {
251 fs::create_dir_all(self.dir.join(&account_config.dir))
252 .await
253 .context("failed to create dir")?;
254 try_many_times(|| fs::rename(&dbfile, &new_dbfile))
255 .await
256 .context("failed to rename dbfile")?;
257 try_many_times(|| fs::rename(&blobdir, &new_blobdir))
258 .await
259 .context("failed to rename blobdir")?;
260 if walfile.exists() {
261 fs::rename(&walfile, &new_walfile)
262 .await
263 .context("failed to rename walfile")?;
264 }
265 Ok(())
266 };
267
268 match res {
269 Ok(_) => {
270 let ctx = Context::new(
271 &new_dbfile,
272 account_config.id,
273 self.events.clone(),
274 self.stockstrings.clone(),
275 )
276 .await?;
277 self.accounts.insert(account_config.id, ctx);
278 Ok(account_config.id)
279 }
280 Err(err) => {
281 let account_path = std::path::PathBuf::from(&account_config.dir);
282 try_many_times(|| fs::remove_dir_all(&account_path))
283 .await
284 .context("failed to remove account data")?;
285 self.config.remove_account(account_config.id).await?;
286
287 self.select_account(old_id).await?;
289
290 Err(err)
291 }
292 }
293 }
294
295 pub fn get_all(&self) -> Vec<u32> {
297 let mut ordered_ids = Vec::new();
298 let mut all_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
299
300 for &id in &self.config.inner.accounts_order {
302 if all_ids.remove(&id) {
303 ordered_ids.push(id);
304 }
305 }
306
307 for id in all_ids {
309 ordered_ids.push(id);
310 }
311
312 ordered_ids
313 }
314
315 pub async fn set_accounts_order(&mut self, order: Vec<u32>) -> Result<()> {
321 let existing_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
322
323 let mut filtered_order: Vec<u32> = order
325 .into_iter()
326 .filter(|id| existing_ids.contains(id))
327 .collect();
328
329 for &id in &existing_ids {
331 if !filtered_order.contains(&id) {
332 filtered_order.push(id);
333 }
334 }
335
336 self.config.inner.accounts_order = filtered_order;
337 self.config.sync().await?;
338 self.emit_event(EventType::AccountsChanged);
339 Ok(())
340 }
341
342 pub async fn start_io(&mut self) {
344 for account in self.accounts.values_mut() {
345 account.start_io().await;
346 }
347 }
348
349 pub async fn stop_io(&self) {
351 info!(self, "Stopping IO for all accounts.");
354 for account in self.accounts.values() {
355 account.stop_io().await;
356 }
357 }
358
359 pub async fn maybe_network(&self) {
361 for account in self.accounts.values() {
362 account.scheduler.maybe_network().await;
363 }
364 }
365
366 pub async fn maybe_network_lost(&self) {
368 for account in self.accounts.values() {
369 account.scheduler.maybe_network_lost(account).await;
370 }
371 }
372
373 async fn background_fetch_no_timeout(accounts: Vec<Context>, events: Events) {
383 let n_accounts = accounts.len();
384 events.emit(Event {
385 id: 0,
386 typ: EventType::Info(format!(
387 "Starting background fetch for {n_accounts} accounts."
388 )),
389 });
390 ::tracing::event!(
391 ::tracing::Level::INFO,
392 account_id = 0,
393 "Starting background fetch for {n_accounts} accounts."
394 );
395 let mut set = JoinSet::new();
396 for account in accounts {
397 set.spawn(async move {
398 if let Err(error) = account.background_fetch().await {
399 warn!(account, "{error:#}");
400 }
401 });
402 }
403 set.join_all().await;
404 events.emit(Event {
405 id: 0,
406 typ: EventType::Info(format!(
407 "Finished background fetch for {n_accounts} accounts."
408 )),
409 });
410 ::tracing::event!(
411 ::tracing::Level::INFO,
412 account_id = 0,
413 "Finished background fetch for {n_accounts} accounts."
414 );
415 }
416
417 async fn background_fetch_with_timeout(
431 accounts: Vec<Context>,
432 events: Events,
433 timeout: std::time::Duration,
434 interrupt_sender: Arc<parking_lot::Mutex<Option<Sender<()>>>>,
435 interrupt_receiver: Option<Receiver<()>>,
436 ) {
437 let Some(interrupt_receiver) = interrupt_receiver else {
438 return;
440 };
441 if let Err(_err) = tokio::time::timeout(
442 timeout,
443 Self::background_fetch_no_timeout(accounts, events.clone())
444 .race(interrupt_receiver.recv().map(|_| ())),
445 )
446 .await
447 {
448 events.emit(Event {
449 id: 0,
450 typ: EventType::Warning("Background fetch timed out.".to_string()),
451 });
452 ::tracing::event!(
453 ::tracing::Level::WARN,
454 account_id = 0,
455 "Background fetch timed out."
456 );
457 }
458 events.emit(Event {
459 id: 0,
460 typ: EventType::AccountsBackgroundFetchDone,
461 });
462 (*interrupt_sender.lock()) = None;
463 }
464
465 pub fn background_fetch(
479 &self,
480 timeout: std::time::Duration,
481 ) -> impl Future<Output = ()> + use<> {
482 let accounts: Vec<Context> = self.accounts.values().cloned().collect();
483 let events = self.events.clone();
484 let (sender, receiver) = async_channel::bounded(1);
485 let receiver = {
486 let mut lock = self.background_fetch_interrupt_sender.lock();
487 if (*lock).is_some() {
488 None
491 } else {
492 *lock = Some(sender);
493 Some(receiver)
494 }
495 };
496 Self::background_fetch_with_timeout(
497 accounts,
498 events,
499 timeout,
500 self.background_fetch_interrupt_sender.clone(),
501 receiver,
502 )
503 }
504
505 pub fn stop_background_fetch(&self) {
513 let mut lock = self.background_fetch_interrupt_sender.lock();
514 if let Some(sender) = lock.take() {
515 sender.try_send(()).ok();
516 }
517 }
518
519 pub fn emit_event(&self, event: EventType) {
521 self.events.emit(Event { id: 0, typ: event })
522 }
523
524 pub fn get_event_emitter(&self) -> EventEmitter {
526 self.events.get_emitter()
527 }
528
529 pub async fn set_push_device_token(&self, token: &str) -> Result<()> {
531 self.push_subscriber.set_device_token(token).await;
532 Ok(())
533 }
534}
535
536const CONFIG_NAME: &str = "accounts.toml";
538
539#[cfg(not(target_os = "ios"))]
541const LOCKFILE_NAME: &str = "accounts.lock";
542
543const DB_NAME: &str = "dc.db";
545
546#[derive(Debug)]
548struct Config {
549 file: PathBuf,
550 inner: InnerConfig,
551 lock_task: Option<JoinHandle<anyhow::Result<()>>>,
554}
555
556#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
560struct InnerConfig {
561 pub selected_account: u32,
563 pub next_id: u32,
564 pub accounts: Vec<AccountConfig>,
565 #[serde(default)]
568 pub accounts_order: Vec<u32>,
569}
570
571impl Drop for Config {
572 fn drop(&mut self) {
573 if let Some(lock_task) = self.lock_task.take() {
574 lock_task.abort();
575 }
576 }
577}
578
579impl Config {
580 #[cfg(target_os = "ios")]
581 async fn create_lock_task(_dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
582 Ok(None)
586 }
587
588 #[cfg(not(target_os = "ios"))]
589 #[expect(clippy::arithmetic_side_effects)]
590 async fn create_lock_task(dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
591 let lockfile = dir.join(LOCKFILE_NAME);
592 let mut lock = fd_lock::RwLock::new(fs::File::create(lockfile).await?);
593 let (locked_tx, locked_rx) = oneshot::channel();
594 let lock_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
595 let mut timeout = Duration::from_millis(100);
596 let _guard = loop {
597 match lock.try_write() {
598 Ok(guard) => break Ok(guard),
599 Err(err) => {
600 if timeout.as_millis() > 1600 {
601 break Err(err);
602 }
603 sleep(timeout).await;
607 if err.kind() == std::io::ErrorKind::WouldBlock {
608 timeout *= 2;
609 }
610 }
611 }
612 }?;
613 locked_tx
614 .send(())
615 .ok()
616 .context("Cannot notify about lockfile locking")?;
617 let (_tx, rx) = oneshot::channel();
618 rx.await?;
619 Ok(())
620 });
621 if locked_rx.await.is_err() {
622 bail!(
623 "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)"
624 );
625 };
626 Ok(Some(lock_task))
627 }
628
629 async fn new_nosync(file: PathBuf, lock: bool) -> Result<Self> {
631 let dir = file.parent().context("Cannot get config file directory")?;
632 let inner = InnerConfig {
633 accounts: Vec::new(),
634 selected_account: 0,
635 next_id: 1,
636 accounts_order: Vec::new(),
637 };
638 if !lock {
639 let cfg = Self {
640 file,
641 inner,
642 lock_task: None,
643 };
644 return Ok(cfg);
645 }
646 let lock_task = Self::create_lock_task(dir.to_path_buf()).await?;
647 let cfg = Self {
648 file,
649 inner,
650 lock_task,
651 };
652 Ok(cfg)
653 }
654
655 pub async fn new(dir: &Path) -> Result<Self> {
657 let lock = true;
658 let mut cfg = Self::new_nosync(dir.join(CONFIG_NAME), lock).await?;
659 cfg.sync().await?;
660
661 Ok(cfg)
662 }
663
664 async fn sync(&mut self) -> Result<()> {
668 #[cfg(not(target_os = "ios"))]
669 ensure!(
670 !self
671 .lock_task
672 .as_ref()
673 .context("Config is read-only")?
674 .is_finished()
675 );
676
677 let tmp_path = self.file.with_extension("toml.tmp");
678 let mut file = fs::File::create(&tmp_path)
679 .await
680 .context("failed to create a tmp config")?;
681 file.write_all(toml::to_string_pretty(&self.inner)?.as_bytes())
682 .await
683 .context("failed to write a tmp config")?;
684 file.sync_data()
685 .await
686 .context("failed to sync a tmp config")?;
687 drop(file);
688 fs::rename(&tmp_path, &self.file)
689 .await
690 .context("failed to rename config")?;
691 Ok(())
692 }
693
694 pub async fn from_file(file: PathBuf, writable: bool) -> Result<Self> {
696 let mut config = Self::new_nosync(file, writable).await?;
697 let bytes = fs::read(&config.file)
698 .await
699 .context("Failed to read file")?;
700 let s = std::str::from_utf8(&bytes)?;
701 config.inner = toml::from_str(s).context("Failed to parse config")?;
702
703 let mut modified = false;
706 for account in &mut config.inner.accounts {
707 if account.dir.is_absolute()
708 && let Some(old_path_parent) = account.dir.parent()
709 && let Ok(new_path) = account.dir.strip_prefix(old_path_parent)
710 {
711 account.dir = new_path.to_path_buf();
712 modified = true;
713 }
714 }
715 if modified && writable {
716 config.sync().await?;
717 }
718
719 Ok(config)
720 }
721
722 pub async fn load_accounts(
727 &self,
728 events: &Events,
729 stockstrings: &StockStrings,
730 push_subscriber: PushSubscriber,
731 dir: &Path,
732 ) -> Result<BTreeMap<u32, Context>> {
733 let mut accounts = BTreeMap::new();
734
735 for account_config in &self.inner.accounts {
736 let dbfile = account_config.dbfile(dir);
737 let ctx = ContextBuilder::new(dbfile.clone())
738 .with_id(account_config.id)
739 .with_events(events.clone())
740 .with_stock_strings(stockstrings.clone())
741 .with_push_subscriber(push_subscriber.clone())
742 .build()
743 .await
744 .with_context(|| format!("failed to create context from file {:?}", &dbfile))?;
745 ctx.open("".to_string()).await?;
748
749 accounts.insert(account_config.id, ctx);
750 }
751
752 Ok(accounts)
753 }
754
755 #[expect(clippy::arithmetic_side_effects)]
757 async fn new_account(&mut self) -> Result<AccountConfig> {
758 let id = {
759 let id = self.inner.next_id;
760 let uuid = Uuid::new_v4();
761 let target_dir = PathBuf::from(uuid.to_string());
762
763 self.inner.accounts.push(AccountConfig {
764 id,
765 dir: target_dir,
766 uuid,
767 });
768 self.inner.next_id += 1;
769
770 self.inner.accounts_order.push(id);
772
773 id
774 };
775
776 self.sync().await?;
777
778 self.select_account(id)
779 .await
780 .context("failed to select just added account")?;
781 let cfg = self
782 .get_account(id)
783 .context("failed to get just added account")?;
784 Ok(cfg)
785 }
786
787 pub async fn remove_account(&mut self, id: u32) -> Result<()> {
789 {
790 if let Some(idx) = self.inner.accounts.iter().position(|e| e.id == id) {
791 self.inner.accounts.remove(idx);
793 }
794
795 self.inner.accounts_order.retain(|&x| x != id);
797
798 if self.inner.selected_account == id {
799 self.inner.selected_account = self
801 .inner
802 .accounts
803 .first()
804 .map(|e| e.id)
805 .unwrap_or_default();
806 }
807 }
808
809 self.sync().await
810 }
811
812 fn get_account(&self, id: u32) -> Option<AccountConfig> {
814 self.inner.accounts.iter().find(|e| e.id == id).cloned()
815 }
816
817 pub fn get_selected_account(&self) -> u32 {
819 self.inner.selected_account
820 }
821
822 pub async fn select_account(&mut self, id: u32) -> Result<()> {
824 {
825 ensure!(
826 self.inner.accounts.iter().any(|e| e.id == id),
827 "invalid account id: {id}"
828 );
829
830 self.inner.selected_account = id;
831 }
832
833 self.sync().await?;
834 Ok(())
835 }
836}
837
838#[expect(clippy::arithmetic_side_effects)]
847async fn try_many_times<F, Fut, T>(f: F) -> std::result::Result<(), T>
848where
849 F: Fn() -> Fut,
850 Fut: Future<Output = std::result::Result<(), T>>,
851{
852 let mut counter = 0;
853 loop {
854 counter += 1;
855
856 if let Err(err) = f().await {
857 if counter > 60 {
858 return Err(err);
859 }
860
861 tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
863 } else {
864 break;
865 }
866 }
867 Ok(())
868}
869
870#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
872struct AccountConfig {
873 pub id: u32,
875
876 pub dir: std::path::PathBuf,
880
881 pub uuid: Uuid,
883}
884
885impl AccountConfig {
886 pub fn dbfile(&self, accounts_dir: &Path) -> std::path::PathBuf {
888 accounts_dir.join(&self.dir).join(DB_NAME)
889 }
890}
891
892#[cfg(test)]
893mod tests {
894 use super::*;
895 use crate::stock_str::{self, StockMessage};
896
897 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
898 async fn test_account_new_open() {
899 let dir = tempfile::tempdir().unwrap();
900 let p: PathBuf = dir.path().join("accounts1");
901
902 {
903 let writable = true;
904 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
905 accounts.add_account().await.unwrap();
906
907 assert_eq!(accounts.accounts.len(), 1);
908 assert_eq!(accounts.config.get_selected_account(), 1);
909 }
910 for writable in [true, false] {
911 let accounts = Accounts::new(p.clone(), writable).await.unwrap();
912
913 assert_eq!(accounts.accounts.len(), 1);
914 assert_eq!(accounts.config.get_selected_account(), 1);
915 }
916 }
917
918 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
919 async fn test_account_new_open_conflict() {
920 let dir = tempfile::tempdir().unwrap();
921 let p: PathBuf = dir.path().join("accounts");
922 let writable = true;
923 let _accounts = Accounts::new(p.clone(), writable).await.unwrap();
924
925 let writable = true;
926 assert!(Accounts::new(p.clone(), writable).await.is_err());
927
928 let writable = false;
929 let accounts = Accounts::new(p, writable).await.unwrap();
930 assert_eq!(accounts.accounts.len(), 0);
931 assert_eq!(accounts.config.get_selected_account(), 0);
932 }
933
934 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
935 async fn test_account_new_add_remove() {
936 let dir = tempfile::tempdir().unwrap();
937 let p: PathBuf = dir.path().join("accounts");
938
939 let writable = true;
940 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
941 assert_eq!(accounts.accounts.len(), 0);
942 assert_eq!(accounts.config.get_selected_account(), 0);
943
944 let id = accounts.add_account().await.unwrap();
945 assert_eq!(id, 1);
946 assert_eq!(accounts.accounts.len(), 1);
947 assert_eq!(accounts.config.get_selected_account(), 1);
948
949 let id = accounts.add_account().await.unwrap();
950 assert_eq!(id, 2);
951 assert_eq!(accounts.config.get_selected_account(), id);
952 assert_eq!(accounts.accounts.len(), 2);
953
954 accounts.select_account(1).await.unwrap();
955 assert_eq!(accounts.config.get_selected_account(), 1);
956
957 accounts.remove_account(1).await.unwrap();
958 assert_eq!(accounts.config.get_selected_account(), 2);
959 assert_eq!(accounts.accounts.len(), 1);
960 }
961
962 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
963 async fn test_accounts_remove_last() -> Result<()> {
964 let dir = tempfile::tempdir()?;
965 let p: PathBuf = dir.path().join("accounts");
966
967 let writable = true;
968 let mut accounts = Accounts::new(p.clone(), writable).await?;
969 assert!(accounts.get_selected_account().is_none());
970 assert_eq!(accounts.config.get_selected_account(), 0);
971
972 let id = accounts.add_account().await?;
973 assert!(accounts.get_selected_account().is_some());
974 assert_eq!(id, 1);
975 assert_eq!(accounts.accounts.len(), 1);
976 assert_eq!(accounts.config.get_selected_account(), id);
977
978 accounts.remove_account(id).await?;
979 assert!(accounts.get_selected_account().is_none());
980
981 Ok(())
982 }
983
984 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
985 async fn test_migrate_account() {
986 let dir = tempfile::tempdir().unwrap();
987 let p: PathBuf = dir.path().join("accounts");
988
989 let writable = true;
990 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
991 assert_eq!(accounts.accounts.len(), 0);
992 assert_eq!(accounts.config.get_selected_account(), 0);
993
994 let extern_dbfile: PathBuf = dir.path().join("other");
995 let ctx = Context::new(&extern_dbfile, 0, Events::new(), StockStrings::new())
996 .await
997 .unwrap();
998 ctx.set_config(crate::config::Config::Addr, Some("me@mail.com"))
999 .await
1000 .unwrap();
1001
1002 drop(ctx);
1003
1004 accounts
1005 .migrate_account(extern_dbfile.clone())
1006 .await
1007 .unwrap();
1008 assert_eq!(accounts.accounts.len(), 1);
1009 assert_eq!(accounts.config.get_selected_account(), 1);
1010
1011 let ctx = accounts.get_selected_account().unwrap();
1012 assert_eq!(
1013 "me@mail.com",
1014 ctx.get_config(crate::config::Config::Addr)
1015 .await
1016 .unwrap()
1017 .unwrap()
1018 );
1019 }
1020
1021 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1023 async fn test_accounts_sorted() {
1024 let dir = tempfile::tempdir().unwrap();
1025 let p: PathBuf = dir.path().join("accounts");
1026
1027 let writable = true;
1028 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
1029
1030 for expected_id in 1..10 {
1031 let id = accounts.add_account().await.unwrap();
1032 assert_eq!(id, expected_id);
1033 }
1034
1035 let ids = accounts.get_all();
1036 for (i, expected_id) in (1..10).enumerate() {
1037 assert_eq!(ids.get(i), Some(&expected_id));
1038 }
1039 }
1040
1041 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1042 async fn test_accounts_ids_unique_increasing_and_persisted() -> Result<()> {
1043 let dir = tempfile::tempdir()?;
1044 let p: PathBuf = dir.path().join("accounts");
1045 let dummy_accounts = 10;
1046
1047 let (id0, id1, id2) = {
1048 let writable = true;
1049 let mut accounts = Accounts::new(p.clone(), writable).await?;
1050 accounts.add_account().await?;
1051 let ids = accounts.get_all();
1052 assert_eq!(ids.len(), 1);
1053
1054 let id0 = *ids.first().unwrap();
1055 let ctx = accounts.get_account(id0).unwrap();
1056 ctx.set_config(crate::config::Config::Addr, Some("one@example.org"))
1057 .await?;
1058
1059 let id1 = accounts.add_account().await?;
1060 let ctx = accounts.get_account(id1).unwrap();
1061 ctx.set_config(crate::config::Config::Addr, Some("two@example.org"))
1062 .await?;
1063
1064 for _ in 0..dummy_accounts {
1066 let to_delete = accounts.add_account().await?;
1067 accounts.remove_account(to_delete).await?;
1068 }
1069
1070 let id2 = accounts.add_account().await?;
1071 let ctx = accounts.get_account(id2).unwrap();
1072 ctx.set_config(crate::config::Config::Addr, Some("three@example.org"))
1073 .await?;
1074
1075 accounts.select_account(id1).await?;
1076
1077 (id0, id1, id2)
1078 };
1079 assert!(id0 > 0);
1080 assert!(id1 > id0);
1081 assert!(id2 > id1 + dummy_accounts);
1082
1083 let (id0_reopened, id1_reopened, id2_reopened) = {
1084 let writable = false;
1085 let accounts = Accounts::new(p.clone(), writable).await?;
1086 let ctx = accounts.get_selected_account().unwrap();
1087 assert_eq!(
1088 ctx.get_config(crate::config::Config::Addr).await?,
1089 Some("two@example.org".to_string())
1090 );
1091
1092 let ids = accounts.get_all();
1093 assert_eq!(ids.len(), 3);
1094
1095 let id0 = *ids.first().unwrap();
1096 let ctx = accounts.get_account(id0).unwrap();
1097 assert_eq!(
1098 ctx.get_config(crate::config::Config::Addr).await?,
1099 Some("one@example.org".to_string())
1100 );
1101
1102 let id1 = *ids.get(1).unwrap();
1103 let t = accounts.get_account(id1).unwrap();
1104 assert_eq!(
1105 t.get_config(crate::config::Config::Addr).await?,
1106 Some("two@example.org".to_string())
1107 );
1108
1109 let id2 = *ids.get(2).unwrap();
1110 let ctx = accounts.get_account(id2).unwrap();
1111 assert_eq!(
1112 ctx.get_config(crate::config::Config::Addr).await?,
1113 Some("three@example.org".to_string())
1114 );
1115
1116 (id0, id1, id2)
1117 };
1118 assert_eq!(id0, id0_reopened);
1119 assert_eq!(id1, id1_reopened);
1120 assert_eq!(id2, id2_reopened);
1121
1122 Ok(())
1123 }
1124
1125 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1126 async fn test_no_accounts_event_emitter() -> Result<()> {
1127 let dir = tempfile::tempdir().unwrap();
1128 let p: PathBuf = dir.path().join("accounts");
1129
1130 let writable = true;
1131 let accounts = Accounts::new(p.clone(), writable).await?;
1132
1133 assert_eq!(accounts.accounts.len(), 0);
1135
1136 let event_emitter = accounts.get_event_emitter();
1138
1139 let duration = std::time::Duration::from_millis(1);
1141 assert!(
1142 tokio::time::timeout(duration, event_emitter.recv())
1143 .await
1144 .is_err()
1145 );
1146
1147 drop(accounts);
1149 assert_eq!(event_emitter.recv().await, None);
1150
1151 Ok(())
1152 }
1153
1154 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1155 async fn test_encrypted_account() -> Result<()> {
1156 let dir = tempfile::tempdir().context("failed to create tempdir")?;
1157 let p: PathBuf = dir.path().join("accounts");
1158
1159 let writable = true;
1160 let mut accounts = Accounts::new(p.clone(), writable)
1161 .await
1162 .context("failed to create accounts manager")?;
1163
1164 assert_eq!(accounts.accounts.len(), 0);
1165 let account_id = accounts
1166 .add_closed_account()
1167 .await
1168 .context("failed to add closed account")?;
1169 let account = accounts
1170 .get_selected_account()
1171 .context("failed to get account")?;
1172 assert_eq!(account.id, account_id);
1173 let passphrase_set_success = account
1174 .open("foobar".to_string())
1175 .await
1176 .context("failed to set passphrase")?;
1177 assert!(passphrase_set_success);
1178 drop(accounts);
1179
1180 let writable = false;
1181 let accounts = Accounts::new(p.clone(), writable)
1182 .await
1183 .context("failed to create second accounts manager")?;
1184 let account = accounts
1185 .get_selected_account()
1186 .context("failed to get account")?;
1187 assert_eq!(account.is_open().await, false);
1188
1189 assert_eq!(account.open("barfoo".to_string()).await?, false);
1191 assert_eq!(account.open("".to_string()).await?, false);
1192
1193 assert_eq!(account.open("foobar".to_string()).await?, true);
1194 assert_eq!(account.is_open().await, true);
1195
1196 Ok(())
1197 }
1198
1199 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1201 async fn test_accounts_share_translations() -> Result<()> {
1202 let dir = tempfile::tempdir().unwrap();
1203 let p: PathBuf = dir.path().join("accounts");
1204
1205 let writable = true;
1206 let mut accounts = Accounts::new(p.clone(), writable).await?;
1207 accounts.add_account().await?;
1208 accounts.add_account().await?;
1209
1210 let account1 = accounts.get_account(1).context("failed to get account 1")?;
1211 let account2 = accounts.get_account(2).context("failed to get account 2")?;
1212
1213 assert_eq!(stock_str::no_messages(&account1).await, "No messages.");
1214 assert_eq!(stock_str::no_messages(&account2).await, "No messages.");
1215 account1
1216 .set_stock_translation(StockMessage::NoMessages, "foobar".to_string())
1217 .await?;
1218 assert_eq!(stock_str::no_messages(&account1).await, "foobar");
1219 assert_eq!(stock_str::no_messages(&account2).await, "foobar");
1220
1221 Ok(())
1222 }
1223}