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