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