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