deltachat/
accounts.rs

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