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 file.sync_data()
690 .await
691 .context("failed to sync a tmp config")?;
692 drop(file);
693 fs::rename(&tmp_path, &self.file)
694 .await
695 .context("failed to rename config")?;
696 Ok(())
697 }
698
699 pub async fn from_file(file: PathBuf, writable: bool) -> Result<Self> {
701 let mut config = Self::new_nosync(file, writable).await?;
702 let bytes = fs::read(&config.file)
703 .await
704 .context("Failed to read file")?;
705 let s = std::str::from_utf8(&bytes)?;
706 config.inner = toml::from_str(s).context("Failed to parse config")?;
707
708 let mut modified = false;
711 for account in &mut config.inner.accounts {
712 if account.dir.is_absolute()
713 && let Some(old_path_parent) = account.dir.parent()
714 && let Ok(new_path) = account.dir.strip_prefix(old_path_parent)
715 {
716 account.dir = new_path.to_path_buf();
717 modified = true;
718 }
719 }
720 if modified && writable {
721 config.sync().await?;
722 }
723
724 Ok(config)
725 }
726
727 pub async fn load_accounts(
732 &self,
733 events: &Events,
734 stockstrings: &StockStrings,
735 push_subscriber: PushSubscriber,
736 dir: &Path,
737 ) -> Result<BTreeMap<u32, Context>> {
738 let mut accounts = BTreeMap::new();
739
740 for account_config in &self.inner.accounts {
741 let dbfile = account_config.dbfile(dir);
742 let ctx = ContextBuilder::new(dbfile.clone())
743 .with_id(account_config.id)
744 .with_events(events.clone())
745 .with_stock_strings(stockstrings.clone())
746 .with_push_subscriber(push_subscriber.clone())
747 .build()
748 .await
749 .with_context(|| format!("failed to create context from file {:?}", &dbfile))?;
750 ctx.open("".to_string()).await?;
753
754 accounts.insert(account_config.id, ctx);
755 }
756
757 Ok(accounts)
758 }
759
760 #[expect(clippy::arithmetic_side_effects)]
762 async fn new_account(&mut self) -> Result<AccountConfig> {
763 let id = {
764 let id = self.inner.next_id;
765 let uuid = Uuid::new_v4();
766 let target_dir = PathBuf::from(uuid.to_string());
767
768 self.inner.accounts.push(AccountConfig {
769 id,
770 dir: target_dir,
771 uuid,
772 });
773 self.inner.next_id += 1;
774
775 self.inner.accounts_order.push(id);
777
778 id
779 };
780
781 self.sync().await?;
782
783 self.select_account(id)
784 .await
785 .context("failed to select just added account")?;
786 let cfg = self
787 .get_account(id)
788 .context("failed to get just added account")?;
789 Ok(cfg)
790 }
791
792 pub async fn remove_account(&mut self, id: u32) -> Result<()> {
794 {
795 if let Some(idx) = self.inner.accounts.iter().position(|e| e.id == id) {
796 self.inner.accounts.remove(idx);
798 }
799
800 self.inner.accounts_order.retain(|&x| x != id);
802
803 if self.inner.selected_account == id {
804 self.inner.selected_account = self
806 .inner
807 .accounts
808 .first()
809 .map(|e| e.id)
810 .unwrap_or_default();
811 }
812 }
813
814 self.sync().await
815 }
816
817 fn get_account(&self, id: u32) -> Option<AccountConfig> {
819 self.inner.accounts.iter().find(|e| e.id == id).cloned()
820 }
821
822 pub fn get_selected_account(&self) -> u32 {
824 self.inner.selected_account
825 }
826
827 pub async fn select_account(&mut self, id: u32) -> Result<()> {
829 {
830 ensure!(
831 self.inner.accounts.iter().any(|e| e.id == id),
832 "invalid account id: {id}"
833 );
834
835 self.inner.selected_account = id;
836 }
837
838 self.sync().await?;
839 Ok(())
840 }
841}
842
843#[expect(clippy::arithmetic_side_effects)]
852async fn try_many_times<F, Fut, T>(f: F) -> std::result::Result<(), T>
853where
854 F: Fn() -> Fut,
855 Fut: Future<Output = std::result::Result<(), T>>,
856{
857 let mut counter = 0;
858 loop {
859 counter += 1;
860
861 if let Err(err) = f().await {
862 if counter > 60 {
863 return Err(err);
864 }
865
866 tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
868 } else {
869 break;
870 }
871 }
872 Ok(())
873}
874
875#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
877struct AccountConfig {
878 pub id: u32,
880
881 pub dir: std::path::PathBuf,
885
886 pub uuid: Uuid,
888}
889
890impl AccountConfig {
891 pub fn dbfile(&self, accounts_dir: &Path) -> std::path::PathBuf {
893 accounts_dir.join(&self.dir).join(DB_NAME)
894 }
895}
896
897#[cfg(test)]
898mod tests {
899 use super::*;
900 use crate::stock_str::{self, StockMessage};
901
902 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
903 async fn test_account_new_open() {
904 let dir = tempfile::tempdir().unwrap();
905 let p: PathBuf = dir.path().join("accounts1");
906
907 {
908 let writable = true;
909 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
910 accounts.add_account().await.unwrap();
911
912 assert_eq!(accounts.accounts.len(), 1);
913 assert_eq!(accounts.config.get_selected_account(), 1);
914 }
915 for writable in [true, false] {
916 let accounts = Accounts::new(p.clone(), writable).await.unwrap();
917
918 assert_eq!(accounts.accounts.len(), 1);
919 assert_eq!(accounts.config.get_selected_account(), 1);
920 }
921 }
922
923 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
924 async fn test_account_new_empty_existing_dir() {
925 let dir = tempfile::tempdir().unwrap();
926 let p: PathBuf = dir.path().join("accounts");
927
928 fs::create_dir_all(&p).await.unwrap();
930 fs::write(p.join("stray_file.txt"), b"hello").await.unwrap();
931 assert!(Accounts::new(p.clone(), true).await.is_err());
932
933 fs::remove_file(p.join("stray_file.txt")).await.unwrap();
935
936 let mut accounts = Accounts::new(p.clone(), true).await.unwrap();
938 assert_eq!(accounts.accounts.len(), 0);
939 let id = accounts.add_account().await.unwrap();
940 assert_eq!(id, 1);
941 }
942
943 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
944 async fn test_account_new_open_conflict() {
945 let dir = tempfile::tempdir().unwrap();
946 let p: PathBuf = dir.path().join("accounts");
947 let writable = true;
948 let _accounts = Accounts::new(p.clone(), writable).await.unwrap();
949
950 let writable = true;
951 assert!(Accounts::new(p.clone(), writable).await.is_err());
952
953 let writable = false;
954 let accounts = Accounts::new(p, writable).await.unwrap();
955 assert_eq!(accounts.accounts.len(), 0);
956 assert_eq!(accounts.config.get_selected_account(), 0);
957 }
958
959 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
960 async fn test_account_new_add_remove() {
961 let dir = tempfile::tempdir().unwrap();
962 let p: PathBuf = dir.path().join("accounts");
963
964 let writable = true;
965 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
966 assert_eq!(accounts.accounts.len(), 0);
967 assert_eq!(accounts.config.get_selected_account(), 0);
968
969 let id = accounts.add_account().await.unwrap();
970 assert_eq!(id, 1);
971 assert_eq!(accounts.accounts.len(), 1);
972 assert_eq!(accounts.config.get_selected_account(), 1);
973
974 let id = accounts.add_account().await.unwrap();
975 assert_eq!(id, 2);
976 assert_eq!(accounts.config.get_selected_account(), id);
977 assert_eq!(accounts.accounts.len(), 2);
978
979 accounts.select_account(1).await.unwrap();
980 assert_eq!(accounts.config.get_selected_account(), 1);
981
982 accounts.remove_account(1).await.unwrap();
983 assert_eq!(accounts.config.get_selected_account(), 2);
984 assert_eq!(accounts.accounts.len(), 1);
985 }
986
987 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
988 async fn test_accounts_remove_last() -> Result<()> {
989 let dir = tempfile::tempdir()?;
990 let p: PathBuf = dir.path().join("accounts");
991
992 let writable = true;
993 let mut accounts = Accounts::new(p.clone(), writable).await?;
994 assert!(accounts.get_selected_account().is_none());
995 assert_eq!(accounts.config.get_selected_account(), 0);
996
997 let id = accounts.add_account().await?;
998 assert!(accounts.get_selected_account().is_some());
999 assert_eq!(id, 1);
1000 assert_eq!(accounts.accounts.len(), 1);
1001 assert_eq!(accounts.config.get_selected_account(), id);
1002
1003 accounts.remove_account(id).await?;
1004 assert!(accounts.get_selected_account().is_none());
1005
1006 Ok(())
1007 }
1008
1009 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1010 async fn test_migrate_account() {
1011 let dir = tempfile::tempdir().unwrap();
1012 let p: PathBuf = dir.path().join("accounts");
1013
1014 let writable = true;
1015 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
1016 assert_eq!(accounts.accounts.len(), 0);
1017 assert_eq!(accounts.config.get_selected_account(), 0);
1018
1019 let extern_dbfile: PathBuf = dir.path().join("other");
1020 let ctx = Context::new(&extern_dbfile, 0, Events::new(), StockStrings::new())
1021 .await
1022 .unwrap();
1023 ctx.set_config(crate::config::Config::Addr, Some("me@mail.com"))
1024 .await
1025 .unwrap();
1026
1027 drop(ctx);
1028
1029 accounts
1030 .migrate_account(extern_dbfile.clone())
1031 .await
1032 .unwrap();
1033 assert_eq!(accounts.accounts.len(), 1);
1034 assert_eq!(accounts.config.get_selected_account(), 1);
1035
1036 let ctx = accounts.get_selected_account().unwrap();
1037 assert_eq!(
1038 "me@mail.com",
1039 ctx.get_config(crate::config::Config::Addr)
1040 .await
1041 .unwrap()
1042 .unwrap()
1043 );
1044 }
1045
1046 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1048 async fn test_accounts_sorted() {
1049 let dir = tempfile::tempdir().unwrap();
1050 let p: PathBuf = dir.path().join("accounts");
1051
1052 let writable = true;
1053 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
1054
1055 for expected_id in 1..10 {
1056 let id = accounts.add_account().await.unwrap();
1057 assert_eq!(id, expected_id);
1058 }
1059
1060 let ids = accounts.get_all();
1061 for (i, expected_id) in (1..10).enumerate() {
1062 assert_eq!(ids.get(i), Some(&expected_id));
1063 }
1064 }
1065
1066 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1067 async fn test_accounts_ids_unique_increasing_and_persisted() -> Result<()> {
1068 let dir = tempfile::tempdir()?;
1069 let p: PathBuf = dir.path().join("accounts");
1070 let dummy_accounts = 10;
1071
1072 let (id0, id1, id2) = {
1073 let writable = true;
1074 let mut accounts = Accounts::new(p.clone(), writable).await?;
1075 accounts.add_account().await?;
1076 let ids = accounts.get_all();
1077 assert_eq!(ids.len(), 1);
1078
1079 let id0 = *ids.first().unwrap();
1080 let ctx = accounts.get_account(id0).unwrap();
1081 ctx.set_config(crate::config::Config::Addr, Some("one@example.org"))
1082 .await?;
1083
1084 let id1 = accounts.add_account().await?;
1085 let ctx = accounts.get_account(id1).unwrap();
1086 ctx.set_config(crate::config::Config::Addr, Some("two@example.org"))
1087 .await?;
1088
1089 for _ in 0..dummy_accounts {
1091 let to_delete = accounts.add_account().await?;
1092 accounts.remove_account(to_delete).await?;
1093 }
1094
1095 let id2 = accounts.add_account().await?;
1096 let ctx = accounts.get_account(id2).unwrap();
1097 ctx.set_config(crate::config::Config::Addr, Some("three@example.org"))
1098 .await?;
1099
1100 accounts.select_account(id1).await?;
1101
1102 (id0, id1, id2)
1103 };
1104 assert!(id0 > 0);
1105 assert!(id1 > id0);
1106 assert!(id2 > id1 + dummy_accounts);
1107
1108 let (id0_reopened, id1_reopened, id2_reopened) = {
1109 let writable = false;
1110 let accounts = Accounts::new(p.clone(), writable).await?;
1111 let ctx = accounts.get_selected_account().unwrap();
1112 assert_eq!(
1113 ctx.get_config(crate::config::Config::Addr).await?,
1114 Some("two@example.org".to_string())
1115 );
1116
1117 let ids = accounts.get_all();
1118 assert_eq!(ids.len(), 3);
1119
1120 let id0 = *ids.first().unwrap();
1121 let ctx = accounts.get_account(id0).unwrap();
1122 assert_eq!(
1123 ctx.get_config(crate::config::Config::Addr).await?,
1124 Some("one@example.org".to_string())
1125 );
1126
1127 let id1 = *ids.get(1).unwrap();
1128 let t = accounts.get_account(id1).unwrap();
1129 assert_eq!(
1130 t.get_config(crate::config::Config::Addr).await?,
1131 Some("two@example.org".to_string())
1132 );
1133
1134 let id2 = *ids.get(2).unwrap();
1135 let ctx = accounts.get_account(id2).unwrap();
1136 assert_eq!(
1137 ctx.get_config(crate::config::Config::Addr).await?,
1138 Some("three@example.org".to_string())
1139 );
1140
1141 (id0, id1, id2)
1142 };
1143 assert_eq!(id0, id0_reopened);
1144 assert_eq!(id1, id1_reopened);
1145 assert_eq!(id2, id2_reopened);
1146
1147 Ok(())
1148 }
1149
1150 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1151 async fn test_no_accounts_event_emitter() -> Result<()> {
1152 let dir = tempfile::tempdir().unwrap();
1153 let p: PathBuf = dir.path().join("accounts");
1154
1155 let writable = true;
1156 let accounts = Accounts::new(p.clone(), writable).await?;
1157
1158 assert_eq!(accounts.accounts.len(), 0);
1160
1161 let event_emitter = accounts.get_event_emitter();
1163
1164 let duration = std::time::Duration::from_millis(1);
1166 assert!(
1167 tokio::time::timeout(duration, event_emitter.recv())
1168 .await
1169 .is_err()
1170 );
1171
1172 drop(accounts);
1174 assert_eq!(event_emitter.recv().await, None);
1175
1176 Ok(())
1177 }
1178
1179 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1180 async fn test_encrypted_account() -> Result<()> {
1181 let dir = tempfile::tempdir().context("failed to create tempdir")?;
1182 let p: PathBuf = dir.path().join("accounts");
1183
1184 let writable = true;
1185 let mut accounts = Accounts::new(p.clone(), writable)
1186 .await
1187 .context("failed to create accounts manager")?;
1188
1189 assert_eq!(accounts.accounts.len(), 0);
1190 let account_id = accounts
1191 .add_closed_account()
1192 .await
1193 .context("failed to add closed account")?;
1194 let account = accounts
1195 .get_selected_account()
1196 .context("failed to get account")?;
1197 assert_eq!(account.id, account_id);
1198 let passphrase_set_success = account
1199 .open("foobar".to_string())
1200 .await
1201 .context("failed to set passphrase")?;
1202 assert!(passphrase_set_success);
1203 drop(accounts);
1204
1205 let writable = false;
1206 let accounts = Accounts::new(p.clone(), writable)
1207 .await
1208 .context("failed to create second accounts manager")?;
1209 let account = accounts
1210 .get_selected_account()
1211 .context("failed to get account")?;
1212 assert_eq!(account.is_open().await, false);
1213
1214 assert_eq!(account.open("barfoo".to_string()).await?, false);
1216 assert_eq!(account.open("".to_string()).await?, false);
1217
1218 assert_eq!(account.open("foobar".to_string()).await?, true);
1219 assert_eq!(account.is_open().await, true);
1220
1221 Ok(())
1222 }
1223
1224 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1226 async fn test_accounts_share_translations() -> Result<()> {
1227 let dir = tempfile::tempdir().unwrap();
1228 let p: PathBuf = dir.path().join("accounts");
1229
1230 let writable = true;
1231 let mut accounts = Accounts::new(p.clone(), writable).await?;
1232 accounts.add_account().await?;
1233 accounts.add_account().await?;
1234
1235 let account1 = accounts.get_account(1).context("failed to get account 1")?;
1236 let account2 = accounts.get_account(2).context("failed to get account 2")?;
1237
1238 assert_eq!(stock_str::no_messages(&account1).await, "No messages.");
1239 assert_eq!(stock_str::no_messages(&account2).await, "No messages.");
1240 account1
1241 .set_stock_translation(StockMessage::NoMessages, "foobar".to_string())
1242 .await?;
1243 assert_eq!(stock_str::no_messages(&account1).await, "foobar");
1244 assert_eq!(stock_str::no_messages(&account2).await, "foobar");
1245
1246 Ok(())
1247 }
1248}