deltachat/
accounts.rs

1//! # Account manager module.
2
3use std::collections::BTreeMap;
4use std::future::Future;
5use std::path::{Path, PathBuf};
6
7use anyhow::{Context as _, Result, bail, ensure};
8use serde::{Deserialize, Serialize};
9use tokio::fs;
10use tokio::io::AsyncWriteExt;
11use tokio::task::{JoinHandle, JoinSet};
12use uuid::Uuid;
13
14#[cfg(not(target_os = "ios"))]
15use tokio::sync::oneshot;
16#[cfg(not(target_os = "ios"))]
17use tokio::time::{Duration, sleep};
18
19use crate::context::{Context, ContextBuilder};
20use crate::events::{Event, EventEmitter, EventType, Events};
21use crate::log::{info, warn};
22use crate::push::PushSubscriber;
23use crate::stock_str::StockStrings;
24
25/// Account manager, that can handle multiple accounts in a single place.
26#[derive(Debug)]
27pub struct Accounts {
28    dir: PathBuf,
29    config: Config,
30    /// Map from account ID to the account.
31    accounts: BTreeMap<u32, Context>,
32
33    /// Event channel to emit account manager errors.
34    events: Events,
35
36    /// Stock string translations shared by all created contexts.
37    ///
38    /// This way changing a translation for one context automatically
39    /// changes it for all other contexts.
40    pub(crate) stockstrings: StockStrings,
41
42    /// Push notification subscriber shared between accounts.
43    push_subscriber: PushSubscriber,
44}
45
46impl Accounts {
47    /// Loads or creates an accounts folder at the given `dir`.
48    pub async fn new(dir: PathBuf, writable: bool) -> Result<Self> {
49        if writable && !dir.exists() {
50            Accounts::create(&dir).await?;
51        }
52
53        Accounts::open(dir, writable).await
54    }
55
56    /// Creates a new default structure.
57    async fn create(dir: &Path) -> Result<()> {
58        fs::create_dir_all(dir)
59            .await
60            .context("failed to create folder")?;
61
62        Config::new(dir).await?;
63
64        Ok(())
65    }
66
67    /// Opens an existing accounts structure. Will error if the folder doesn't exist,
68    /// no account exists and no config exists.
69    async fn open(dir: PathBuf, writable: bool) -> Result<Self> {
70        ensure!(dir.exists(), "directory does not exist");
71
72        let config_file = dir.join(CONFIG_NAME);
73        ensure!(config_file.exists(), "{:?} does not exist", config_file);
74
75        let config = Config::from_file(config_file, writable).await?;
76        let events = Events::new();
77        let stockstrings = StockStrings::new();
78        let push_subscriber = PushSubscriber::new();
79        let accounts = config
80            .load_accounts(&events, &stockstrings, push_subscriber.clone(), &dir)
81            .await
82            .context("failed to load accounts")?;
83
84        Ok(Self {
85            dir,
86            config,
87            accounts,
88            events,
89            stockstrings,
90            push_subscriber,
91        })
92    }
93
94    /// Returns an account by its `id`:
95    pub fn get_account(&self, id: u32) -> Option<Context> {
96        self.accounts.get(&id).cloned()
97    }
98
99    /// Returns the currently selected account.
100    pub fn get_selected_account(&self) -> Option<Context> {
101        let id = self.config.get_selected_account();
102        self.accounts.get(&id).cloned()
103    }
104
105    /// Returns the currently selected account's id or None if no account is selected.
106    pub fn get_selected_account_id(&self) -> Option<u32> {
107        match self.config.get_selected_account() {
108            0 => None,
109            id => Some(id),
110        }
111    }
112
113    /// Selects the given account.
114    pub async fn select_account(&mut self, id: u32) -> Result<()> {
115        self.config.select_account(id).await?;
116
117        Ok(())
118    }
119
120    /// Adds a new account and opens it.
121    ///
122    /// Returns account ID.
123    pub async fn add_account(&mut self) -> Result<u32> {
124        let account_config = self.config.new_account().await?;
125        let dbfile = account_config.dbfile(&self.dir);
126
127        let ctx = ContextBuilder::new(dbfile)
128            .with_id(account_config.id)
129            .with_events(self.events.clone())
130            .with_stock_strings(self.stockstrings.clone())
131            .with_push_subscriber(self.push_subscriber.clone())
132            .build()
133            .await?;
134        // Try to open without a passphrase,
135        // but do not return an error if account is passphare-protected.
136        ctx.open("".to_string()).await?;
137
138        self.accounts.insert(account_config.id, ctx);
139        self.emit_event(EventType::AccountsChanged);
140
141        Ok(account_config.id)
142    }
143
144    /// Adds a new closed account.
145    pub async fn add_closed_account(&mut self) -> Result<u32> {
146        let account_config = self.config.new_account().await?;
147        let dbfile = account_config.dbfile(&self.dir);
148
149        let ctx = ContextBuilder::new(dbfile)
150            .with_id(account_config.id)
151            .with_events(self.events.clone())
152            .with_stock_strings(self.stockstrings.clone())
153            .with_push_subscriber(self.push_subscriber.clone())
154            .build()
155            .await?;
156        self.accounts.insert(account_config.id, ctx);
157        self.emit_event(EventType::AccountsChanged);
158
159        Ok(account_config.id)
160    }
161
162    /// Removes an account.
163    pub async fn remove_account(&mut self, id: u32) -> Result<()> {
164        let ctx = self
165            .accounts
166            .remove(&id)
167            .with_context(|| format!("no account with id {id}"))?;
168        ctx.stop_io().await;
169
170        // Explicitly close the database
171        // to make sure the database file is closed
172        // and can be removed on Windows.
173        // If some spawned task tries to use the database afterwards,
174        // it will fail.
175        //
176        // Previously `stop_io()` aborted the tasks without awaiting them
177        // and this resulted in keeping `Context` clones inside
178        // `Future`s that were not dropped. This bug is fixed now,
179        // but explicitly closing the database ensures that file is freed
180        // even if not all `Context` references are dropped.
181        ctx.sql.close().await;
182        drop(ctx);
183
184        if let Some(cfg) = self.config.get_account(id) {
185            let account_path = self.dir.join(cfg.dir);
186
187            try_many_times(|| fs::remove_dir_all(&account_path))
188                .await
189                .context("failed to remove account data")?;
190        }
191        self.config.remove_account(id).await?;
192        self.emit_event(EventType::AccountsChanged);
193
194        Ok(())
195    }
196
197    /// Migrates an existing account into this structure.
198    ///
199    /// Returns the ID of new account.
200    pub async fn migrate_account(&mut self, dbfile: PathBuf) -> Result<u32> {
201        let blobdir = Context::derive_blobdir(&dbfile);
202        let walfile = Context::derive_walfile(&dbfile);
203
204        ensure!(dbfile.exists(), "no database found: {}", dbfile.display());
205        ensure!(blobdir.exists(), "no blobdir found: {}", blobdir.display());
206
207        let old_id = self.config.get_selected_account();
208
209        // create new account
210        let account_config = self
211            .config
212            .new_account()
213            .await
214            .context("failed to create new account")?;
215
216        let new_dbfile = account_config.dbfile(&self.dir);
217        let new_blobdir = Context::derive_blobdir(&new_dbfile);
218        let new_walfile = Context::derive_walfile(&new_dbfile);
219
220        let res = {
221            fs::create_dir_all(self.dir.join(&account_config.dir))
222                .await
223                .context("failed to create dir")?;
224            try_many_times(|| fs::rename(&dbfile, &new_dbfile))
225                .await
226                .context("failed to rename dbfile")?;
227            try_many_times(|| fs::rename(&blobdir, &new_blobdir))
228                .await
229                .context("failed to rename blobdir")?;
230            if walfile.exists() {
231                fs::rename(&walfile, &new_walfile)
232                    .await
233                    .context("failed to rename walfile")?;
234            }
235            Ok(())
236        };
237
238        match res {
239            Ok(_) => {
240                let ctx = Context::new(
241                    &new_dbfile,
242                    account_config.id,
243                    self.events.clone(),
244                    self.stockstrings.clone(),
245                )
246                .await?;
247                self.accounts.insert(account_config.id, ctx);
248                Ok(account_config.id)
249            }
250            Err(err) => {
251                let account_path = std::path::PathBuf::from(&account_config.dir);
252                try_many_times(|| fs::remove_dir_all(&account_path))
253                    .await
254                    .context("failed to remove account data")?;
255                self.config.remove_account(account_config.id).await?;
256
257                // set selection back
258                self.select_account(old_id).await?;
259
260                Err(err)
261            }
262        }
263    }
264
265    /// Get a list of all account ids.
266    pub fn get_all(&self) -> Vec<u32> {
267        self.accounts.keys().copied().collect()
268    }
269
270    /// Starts background tasks such as IMAP and SMTP loops for all accounts.
271    pub async fn start_io(&mut self) {
272        for account in self.accounts.values_mut() {
273            account.start_io().await;
274        }
275    }
276
277    /// Stops background tasks for all accounts.
278    pub async fn stop_io(&self) {
279        // Sending an event here wakes up event loop even
280        // if there are no accounts.
281        info!(self, "Stopping IO for all accounts.");
282        for account in self.accounts.values() {
283            account.stop_io().await;
284        }
285    }
286
287    /// Notifies all accounts that the network may have become available.
288    pub async fn maybe_network(&self) {
289        for account in self.accounts.values() {
290            account.scheduler.maybe_network().await;
291        }
292    }
293
294    /// Notifies all accounts that the network connection may have been lost.
295    pub async fn maybe_network_lost(&self) {
296        for account in self.accounts.values() {
297            account.scheduler.maybe_network_lost(account).await;
298        }
299    }
300
301    /// Performs a background fetch for all accounts in parallel.
302    ///
303    /// This is an auxiliary function and not part of public API.
304    /// Use [Accounts::background_fetch] instead.
305    async fn background_fetch_no_timeout(accounts: Vec<Context>, events: Events) {
306        events.emit(Event {
307            id: 0,
308            typ: EventType::Info(format!(
309                "Starting background fetch for {} accounts.",
310                accounts.len()
311            )),
312        });
313        let mut set = JoinSet::new();
314        for account in accounts {
315            set.spawn(async move {
316                if let Err(error) = account.background_fetch().await {
317                    warn!(account, "{error:#}");
318                }
319            });
320        }
321        set.join_all().await;
322    }
323
324    /// Auxiliary function for [Accounts::background_fetch].
325    async fn background_fetch_with_timeout(
326        accounts: Vec<Context>,
327        events: Events,
328        timeout: std::time::Duration,
329    ) {
330        if let Err(_err) = tokio::time::timeout(
331            timeout,
332            Self::background_fetch_no_timeout(accounts, events.clone()),
333        )
334        .await
335        {
336            events.emit(Event {
337                id: 0,
338                typ: EventType::Warning("Background fetch timed out.".to_string()),
339            });
340        }
341        events.emit(Event {
342            id: 0,
343            typ: EventType::AccountsBackgroundFetchDone,
344        });
345    }
346
347    /// Performs a background fetch for all accounts in parallel with a timeout.
348    ///
349    /// The `AccountsBackgroundFetchDone` event is emitted at the end,
350    /// process all events until you get this one and you can safely return to the background
351    /// without forgetting to create notifications caused by timing race conditions.
352    ///
353    /// Returns a future that resolves when background fetch is done,
354    /// but does not capture `&self`.
355    pub fn background_fetch(
356        &self,
357        timeout: std::time::Duration,
358    ) -> impl Future<Output = ()> + use<> {
359        let accounts: Vec<Context> = self.accounts.values().cloned().collect();
360        let events = self.events.clone();
361        Self::background_fetch_with_timeout(accounts, events, timeout)
362    }
363
364    /// Emits a single event.
365    pub fn emit_event(&self, event: EventType) {
366        self.events.emit(Event { id: 0, typ: event })
367    }
368
369    /// Returns event emitter.
370    pub fn get_event_emitter(&self) -> EventEmitter {
371        self.events.get_emitter()
372    }
373
374    /// Sets notification token for Apple Push Notification service.
375    pub async fn set_push_device_token(&self, token: &str) -> Result<()> {
376        self.push_subscriber.set_device_token(token).await;
377        Ok(())
378    }
379}
380
381/// Configuration file name.
382const CONFIG_NAME: &str = "accounts.toml";
383
384/// Lockfile name.
385#[cfg(not(target_os = "ios"))]
386const LOCKFILE_NAME: &str = "accounts.lock";
387
388/// Database file name.
389const DB_NAME: &str = "dc.db";
390
391/// Account manager configuration file.
392#[derive(Debug)]
393struct Config {
394    file: PathBuf,
395    inner: InnerConfig,
396    // We lock the lockfile in the Config constructors to protect also from having multiple Config
397    // objects for the same config file.
398    lock_task: Option<JoinHandle<anyhow::Result<()>>>,
399}
400
401/// Account manager configuration file contents.
402///
403/// This is serialized into TOML.
404#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
405struct InnerConfig {
406    /// The currently selected account.
407    pub selected_account: u32,
408    pub next_id: u32,
409    pub accounts: Vec<AccountConfig>,
410}
411
412impl Drop for Config {
413    fn drop(&mut self) {
414        if let Some(lock_task) = self.lock_task.take() {
415            lock_task.abort();
416        }
417    }
418}
419
420impl Config {
421    #[cfg(target_os = "ios")]
422    async fn create_lock_task(_dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
423        // Do not lock accounts.toml on iOS.
424        // This results in 0xdead10cc crashes on suspend.
425        // iOS itself ensures that multiple instances of Delta Chat are not running.
426        Ok(None)
427    }
428
429    #[cfg(not(target_os = "ios"))]
430    async fn create_lock_task(dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
431        let lockfile = dir.join(LOCKFILE_NAME);
432        let mut lock = fd_lock::RwLock::new(fs::File::create(lockfile).await?);
433        let (locked_tx, locked_rx) = oneshot::channel();
434        let lock_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
435            let mut timeout = Duration::from_millis(100);
436            let _guard = loop {
437                match lock.try_write() {
438                    Ok(guard) => break Ok(guard),
439                    Err(err) => {
440                        if timeout.as_millis() > 1600 {
441                            break Err(err);
442                        }
443                        // We need to wait for the previous lock_task to be aborted thus unlocking
444                        // the lockfile. We don't open configs for writing often outside of the
445                        // tests, so this adds delays to the tests, but otherwise ok.
446                        sleep(timeout).await;
447                        if err.kind() == std::io::ErrorKind::WouldBlock {
448                            timeout *= 2;
449                        }
450                    }
451                }
452            }?;
453            locked_tx
454                .send(())
455                .ok()
456                .context("Cannot notify about lockfile locking")?;
457            let (_tx, rx) = oneshot::channel();
458            rx.await?;
459            Ok(())
460        });
461        if locked_rx.await.is_err() {
462            bail!(
463                "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)"
464            );
465        };
466        Ok(Some(lock_task))
467    }
468
469    /// Creates a new Config for `file`, but doesn't open/sync it.
470    async fn new_nosync(file: PathBuf, lock: bool) -> Result<Self> {
471        let dir = file.parent().context("Cannot get config file directory")?;
472        let inner = InnerConfig {
473            accounts: Vec::new(),
474            selected_account: 0,
475            next_id: 1,
476        };
477        if !lock {
478            let cfg = Self {
479                file,
480                inner,
481                lock_task: None,
482            };
483            return Ok(cfg);
484        }
485        let lock_task = Self::create_lock_task(dir.to_path_buf()).await?;
486        let cfg = Self {
487            file,
488            inner,
489            lock_task,
490        };
491        Ok(cfg)
492    }
493
494    /// Creates a new configuration file in the given account manager directory.
495    pub async fn new(dir: &Path) -> Result<Self> {
496        let lock = true;
497        let mut cfg = Self::new_nosync(dir.join(CONFIG_NAME), lock).await?;
498        cfg.sync().await?;
499
500        Ok(cfg)
501    }
502
503    /// Sync the inmemory representation to disk.
504    /// Takes a mutable reference because the saved file is a part of the `Config` state. This
505    /// protects from parallel calls resulting to a wrong file contents.
506    async fn sync(&mut self) -> Result<()> {
507        #[cfg(not(target_os = "ios"))]
508        ensure!(
509            !self
510                .lock_task
511                .as_ref()
512                .context("Config is read-only")?
513                .is_finished()
514        );
515
516        let tmp_path = self.file.with_extension("toml.tmp");
517        let mut file = fs::File::create(&tmp_path)
518            .await
519            .context("failed to create a tmp config")?;
520        file.write_all(toml::to_string_pretty(&self.inner)?.as_bytes())
521            .await
522            .context("failed to write a tmp config")?;
523        file.sync_data()
524            .await
525            .context("failed to sync a tmp config")?;
526        drop(file);
527        fs::rename(&tmp_path, &self.file)
528            .await
529            .context("failed to rename config")?;
530        Ok(())
531    }
532
533    /// Read a configuration from the given file into memory.
534    pub async fn from_file(file: PathBuf, writable: bool) -> Result<Self> {
535        let mut config = Self::new_nosync(file, writable).await?;
536        let bytes = fs::read(&config.file)
537            .await
538            .context("Failed to read file")?;
539        let s = std::str::from_utf8(&bytes)?;
540        config.inner = toml::from_str(s).context("Failed to parse config")?;
541
542        // Previous versions of the core stored absolute paths in account config.
543        // Convert them to relative paths.
544        let mut modified = false;
545        for account in &mut config.inner.accounts {
546            if account.dir.is_absolute() {
547                if let Some(old_path_parent) = account.dir.parent() {
548                    if let Ok(new_path) = account.dir.strip_prefix(old_path_parent) {
549                        account.dir = new_path.to_path_buf();
550                        modified = true;
551                    }
552                }
553            }
554        }
555        if modified && writable {
556            config.sync().await?;
557        }
558
559        Ok(config)
560    }
561
562    /// Loads all accounts defined in the configuration file.
563    ///
564    /// Created contexts share the same event channel and stock string
565    /// translations.
566    pub async fn load_accounts(
567        &self,
568        events: &Events,
569        stockstrings: &StockStrings,
570        push_subscriber: PushSubscriber,
571        dir: &Path,
572    ) -> Result<BTreeMap<u32, Context>> {
573        let mut accounts = BTreeMap::new();
574
575        for account_config in &self.inner.accounts {
576            let dbfile = account_config.dbfile(dir);
577            let ctx = ContextBuilder::new(dbfile.clone())
578                .with_id(account_config.id)
579                .with_events(events.clone())
580                .with_stock_strings(stockstrings.clone())
581                .with_push_subscriber(push_subscriber.clone())
582                .build()
583                .await
584                .with_context(|| format!("failed to create context from file {:?}", &dbfile))?;
585            // Try to open without a passphrase,
586            // but do not return an error if account is passphare-protected.
587            ctx.open("".to_string()).await?;
588
589            accounts.insert(account_config.id, ctx);
590        }
591
592        Ok(accounts)
593    }
594
595    /// Creates a new account in the account manager directory.
596    async fn new_account(&mut self) -> Result<AccountConfig> {
597        let id = {
598            let id = self.inner.next_id;
599            let uuid = Uuid::new_v4();
600            let target_dir = PathBuf::from(uuid.to_string());
601
602            self.inner.accounts.push(AccountConfig {
603                id,
604                dir: target_dir,
605                uuid,
606            });
607            self.inner.next_id += 1;
608            id
609        };
610
611        self.sync().await?;
612
613        self.select_account(id)
614            .await
615            .context("failed to select just added account")?;
616        let cfg = self
617            .get_account(id)
618            .context("failed to get just added account")?;
619        Ok(cfg)
620    }
621
622    /// Removes an existing account entirely.
623    pub async fn remove_account(&mut self, id: u32) -> Result<()> {
624        {
625            if let Some(idx) = self.inner.accounts.iter().position(|e| e.id == id) {
626                // remove account from the configs
627                self.inner.accounts.remove(idx);
628            }
629            if self.inner.selected_account == id {
630                // reset selected account
631                self.inner.selected_account = self
632                    .inner
633                    .accounts
634                    .first()
635                    .map(|e| e.id)
636                    .unwrap_or_default();
637            }
638        }
639
640        self.sync().await
641    }
642
643    /// Returns configuration file section for the given account ID.
644    fn get_account(&self, id: u32) -> Option<AccountConfig> {
645        self.inner.accounts.iter().find(|e| e.id == id).cloned()
646    }
647
648    /// Returns the ID of selected account.
649    pub fn get_selected_account(&self) -> u32 {
650        self.inner.selected_account
651    }
652
653    /// Changes selected account ID.
654    pub async fn select_account(&mut self, id: u32) -> Result<()> {
655        {
656            ensure!(
657                self.inner.accounts.iter().any(|e| e.id == id),
658                "invalid account id: {}",
659                id
660            );
661
662            self.inner.selected_account = id;
663        }
664
665        self.sync().await?;
666        Ok(())
667    }
668}
669
670/// Spend up to 1 minute trying to do the operation.
671///
672/// Even if Delta Chat itself does not hold the file lock,
673/// there may be other processes such as antivirus,
674/// or the filesystem may be network-mounted.
675///
676/// Without this workaround removing account may fail on Windows with an error
677/// "The process cannot access the file because it is being used by another process. (os error 32)".
678async fn try_many_times<F, Fut, T>(f: F) -> std::result::Result<(), T>
679where
680    F: Fn() -> Fut,
681    Fut: Future<Output = std::result::Result<(), T>>,
682{
683    let mut counter = 0;
684    loop {
685        counter += 1;
686
687        if let Err(err) = f().await {
688            if counter > 60 {
689                return Err(err);
690            }
691
692            // Wait 1 second and try again.
693            tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
694        } else {
695            break;
696        }
697    }
698    Ok(())
699}
700
701/// Configuration of a single account.
702#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
703struct AccountConfig {
704    /// Unique id.
705    pub id: u32,
706
707    /// Root directory for all data for this account.
708    ///
709    /// The path is relative to the account manager directory.
710    pub dir: std::path::PathBuf,
711
712    /// Universally unique account identifier.
713    pub uuid: Uuid,
714}
715
716impl AccountConfig {
717    /// Get the canonical dbfile name for this configuration.
718    pub fn dbfile(&self, accounts_dir: &Path) -> std::path::PathBuf {
719        accounts_dir.join(&self.dir).join(DB_NAME)
720    }
721}
722
723#[cfg(test)]
724mod tests {
725    use super::*;
726    use crate::stock_str::{self, StockMessage};
727
728    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
729    async fn test_account_new_open() {
730        let dir = tempfile::tempdir().unwrap();
731        let p: PathBuf = dir.path().join("accounts1");
732
733        {
734            let writable = true;
735            let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
736            accounts.add_account().await.unwrap();
737
738            assert_eq!(accounts.accounts.len(), 1);
739            assert_eq!(accounts.config.get_selected_account(), 1);
740        }
741        for writable in [true, false] {
742            let accounts = Accounts::new(p.clone(), writable).await.unwrap();
743
744            assert_eq!(accounts.accounts.len(), 1);
745            assert_eq!(accounts.config.get_selected_account(), 1);
746        }
747    }
748
749    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
750    async fn test_account_new_open_conflict() {
751        let dir = tempfile::tempdir().unwrap();
752        let p: PathBuf = dir.path().join("accounts");
753        let writable = true;
754        let _accounts = Accounts::new(p.clone(), writable).await.unwrap();
755
756        let writable = true;
757        assert!(Accounts::new(p.clone(), writable).await.is_err());
758
759        let writable = false;
760        let accounts = Accounts::new(p, writable).await.unwrap();
761        assert_eq!(accounts.accounts.len(), 0);
762        assert_eq!(accounts.config.get_selected_account(), 0);
763    }
764
765    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
766    async fn test_account_new_add_remove() {
767        let dir = tempfile::tempdir().unwrap();
768        let p: PathBuf = dir.path().join("accounts");
769
770        let writable = true;
771        let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
772        assert_eq!(accounts.accounts.len(), 0);
773        assert_eq!(accounts.config.get_selected_account(), 0);
774
775        let id = accounts.add_account().await.unwrap();
776        assert_eq!(id, 1);
777        assert_eq!(accounts.accounts.len(), 1);
778        assert_eq!(accounts.config.get_selected_account(), 1);
779
780        let id = accounts.add_account().await.unwrap();
781        assert_eq!(id, 2);
782        assert_eq!(accounts.config.get_selected_account(), id);
783        assert_eq!(accounts.accounts.len(), 2);
784
785        accounts.select_account(1).await.unwrap();
786        assert_eq!(accounts.config.get_selected_account(), 1);
787
788        accounts.remove_account(1).await.unwrap();
789        assert_eq!(accounts.config.get_selected_account(), 2);
790        assert_eq!(accounts.accounts.len(), 1);
791    }
792
793    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
794    async fn test_accounts_remove_last() -> Result<()> {
795        let dir = tempfile::tempdir()?;
796        let p: PathBuf = dir.path().join("accounts");
797
798        let writable = true;
799        let mut accounts = Accounts::new(p.clone(), writable).await?;
800        assert!(accounts.get_selected_account().is_none());
801        assert_eq!(accounts.config.get_selected_account(), 0);
802
803        let id = accounts.add_account().await?;
804        assert!(accounts.get_selected_account().is_some());
805        assert_eq!(id, 1);
806        assert_eq!(accounts.accounts.len(), 1);
807        assert_eq!(accounts.config.get_selected_account(), id);
808
809        accounts.remove_account(id).await?;
810        assert!(accounts.get_selected_account().is_none());
811
812        Ok(())
813    }
814
815    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
816    async fn test_migrate_account() {
817        let dir = tempfile::tempdir().unwrap();
818        let p: PathBuf = dir.path().join("accounts");
819
820        let writable = true;
821        let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
822        assert_eq!(accounts.accounts.len(), 0);
823        assert_eq!(accounts.config.get_selected_account(), 0);
824
825        let extern_dbfile: PathBuf = dir.path().join("other");
826        let ctx = Context::new(&extern_dbfile, 0, Events::new(), StockStrings::new())
827            .await
828            .unwrap();
829        ctx.set_config(crate::config::Config::Addr, Some("me@mail.com"))
830            .await
831            .unwrap();
832
833        drop(ctx);
834
835        accounts
836            .migrate_account(extern_dbfile.clone())
837            .await
838            .unwrap();
839        assert_eq!(accounts.accounts.len(), 1);
840        assert_eq!(accounts.config.get_selected_account(), 1);
841
842        let ctx = accounts.get_selected_account().unwrap();
843        assert_eq!(
844            "me@mail.com",
845            ctx.get_config(crate::config::Config::Addr)
846                .await
847                .unwrap()
848                .unwrap()
849        );
850    }
851
852    /// Tests that accounts are sorted by ID.
853    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
854    async fn test_accounts_sorted() {
855        let dir = tempfile::tempdir().unwrap();
856        let p: PathBuf = dir.path().join("accounts");
857
858        let writable = true;
859        let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
860
861        for expected_id in 1..10 {
862            let id = accounts.add_account().await.unwrap();
863            assert_eq!(id, expected_id);
864        }
865
866        let ids = accounts.get_all();
867        for (i, expected_id) in (1..10).enumerate() {
868            assert_eq!(ids.get(i), Some(&expected_id));
869        }
870    }
871
872    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
873    async fn test_accounts_ids_unique_increasing_and_persisted() -> Result<()> {
874        let dir = tempfile::tempdir()?;
875        let p: PathBuf = dir.path().join("accounts");
876        let dummy_accounts = 10;
877
878        let (id0, id1, id2) = {
879            let writable = true;
880            let mut accounts = Accounts::new(p.clone(), writable).await?;
881            accounts.add_account().await?;
882            let ids = accounts.get_all();
883            assert_eq!(ids.len(), 1);
884
885            let id0 = *ids.first().unwrap();
886            let ctx = accounts.get_account(id0).unwrap();
887            ctx.set_config(crate::config::Config::Addr, Some("one@example.org"))
888                .await?;
889
890            let id1 = accounts.add_account().await?;
891            let ctx = accounts.get_account(id1).unwrap();
892            ctx.set_config(crate::config::Config::Addr, Some("two@example.org"))
893                .await?;
894
895            // add and remove some accounts and force a gap (ids must not be reused)
896            for _ in 0..dummy_accounts {
897                let to_delete = accounts.add_account().await?;
898                accounts.remove_account(to_delete).await?;
899            }
900
901            let id2 = accounts.add_account().await?;
902            let ctx = accounts.get_account(id2).unwrap();
903            ctx.set_config(crate::config::Config::Addr, Some("three@example.org"))
904                .await?;
905
906            accounts.select_account(id1).await?;
907
908            (id0, id1, id2)
909        };
910        assert!(id0 > 0);
911        assert!(id1 > id0);
912        assert!(id2 > id1 + dummy_accounts);
913
914        let (id0_reopened, id1_reopened, id2_reopened) = {
915            let writable = false;
916            let accounts = Accounts::new(p.clone(), writable).await?;
917            let ctx = accounts.get_selected_account().unwrap();
918            assert_eq!(
919                ctx.get_config(crate::config::Config::Addr).await?,
920                Some("two@example.org".to_string())
921            );
922
923            let ids = accounts.get_all();
924            assert_eq!(ids.len(), 3);
925
926            let id0 = *ids.first().unwrap();
927            let ctx = accounts.get_account(id0).unwrap();
928            assert_eq!(
929                ctx.get_config(crate::config::Config::Addr).await?,
930                Some("one@example.org".to_string())
931            );
932
933            let id1 = *ids.get(1).unwrap();
934            let t = accounts.get_account(id1).unwrap();
935            assert_eq!(
936                t.get_config(crate::config::Config::Addr).await?,
937                Some("two@example.org".to_string())
938            );
939
940            let id2 = *ids.get(2).unwrap();
941            let ctx = accounts.get_account(id2).unwrap();
942            assert_eq!(
943                ctx.get_config(crate::config::Config::Addr).await?,
944                Some("three@example.org".to_string())
945            );
946
947            (id0, id1, id2)
948        };
949        assert_eq!(id0, id0_reopened);
950        assert_eq!(id1, id1_reopened);
951        assert_eq!(id2, id2_reopened);
952
953        Ok(())
954    }
955
956    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
957    async fn test_no_accounts_event_emitter() -> Result<()> {
958        let dir = tempfile::tempdir().unwrap();
959        let p: PathBuf = dir.path().join("accounts");
960
961        let writable = true;
962        let accounts = Accounts::new(p.clone(), writable).await?;
963
964        // Make sure there are no accounts.
965        assert_eq!(accounts.accounts.len(), 0);
966
967        // Create event emitter.
968        let event_emitter = accounts.get_event_emitter();
969
970        // Test that event emitter does not return `None` immediately.
971        let duration = std::time::Duration::from_millis(1);
972        assert!(
973            tokio::time::timeout(duration, event_emitter.recv())
974                .await
975                .is_err()
976        );
977
978        // When account manager is dropped, event emitter is exhausted.
979        drop(accounts);
980        assert_eq!(event_emitter.recv().await, None);
981
982        Ok(())
983    }
984
985    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
986    async fn test_encrypted_account() -> Result<()> {
987        let dir = tempfile::tempdir().context("failed to create tempdir")?;
988        let p: PathBuf = dir.path().join("accounts");
989
990        let writable = true;
991        let mut accounts = Accounts::new(p.clone(), writable)
992            .await
993            .context("failed to create accounts manager")?;
994
995        assert_eq!(accounts.accounts.len(), 0);
996        let account_id = accounts
997            .add_closed_account()
998            .await
999            .context("failed to add closed account")?;
1000        let account = accounts
1001            .get_selected_account()
1002            .context("failed to get account")?;
1003        assert_eq!(account.id, account_id);
1004        let passphrase_set_success = account
1005            .open("foobar".to_string())
1006            .await
1007            .context("failed to set passphrase")?;
1008        assert!(passphrase_set_success);
1009        drop(accounts);
1010
1011        let writable = false;
1012        let accounts = Accounts::new(p.clone(), writable)
1013            .await
1014            .context("failed to create second accounts manager")?;
1015        let account = accounts
1016            .get_selected_account()
1017            .context("failed to get account")?;
1018        assert_eq!(account.is_open().await, false);
1019
1020        // Try wrong passphrase.
1021        assert_eq!(account.open("barfoo".to_string()).await?, false);
1022        assert_eq!(account.open("".to_string()).await?, false);
1023
1024        assert_eq!(account.open("foobar".to_string()).await?, true);
1025        assert_eq!(account.is_open().await, true);
1026
1027        Ok(())
1028    }
1029
1030    /// Tests that accounts share stock string translations.
1031    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1032    async fn test_accounts_share_translations() -> Result<()> {
1033        let dir = tempfile::tempdir().unwrap();
1034        let p: PathBuf = dir.path().join("accounts");
1035
1036        let writable = true;
1037        let mut accounts = Accounts::new(p.clone(), writable).await?;
1038        accounts.add_account().await?;
1039        accounts.add_account().await?;
1040
1041        let account1 = accounts.get_account(1).context("failed to get account 1")?;
1042        let account2 = accounts.get_account(2).context("failed to get account 2")?;
1043
1044        assert_eq!(stock_str::no_messages(&account1).await, "No messages.");
1045        assert_eq!(stock_str::no_messages(&account2).await, "No messages.");
1046        account1
1047            .set_stock_translation(StockMessage::NoMessages, "foobar".to_string())
1048            .await?;
1049        assert_eq!(stock_str::no_messages(&account1).await, "foobar");
1050        assert_eq!(stock_str::no_messages(&account2).await, "foobar");
1051
1052        Ok(())
1053    }
1054}