deltachat/
accounts.rs

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