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    #[expect(clippy::arithmetic_side_effects)]
590    async fn create_lock_task(dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
591        let lockfile = dir.join(LOCKFILE_NAME);
592        let mut lock = fd_lock::RwLock::new(fs::File::create(lockfile).await?);
593        let (locked_tx, locked_rx) = oneshot::channel();
594        let lock_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
595            let mut timeout = Duration::from_millis(100);
596            let _guard = loop {
597                match lock.try_write() {
598                    Ok(guard) => break Ok(guard),
599                    Err(err) => {
600                        if timeout.as_millis() > 1600 {
601                            break Err(err);
602                        }
603                        // We need to wait for the previous lock_task to be aborted thus unlocking
604                        // the lockfile. We don't open configs for writing often outside of the
605                        // tests, so this adds delays to the tests, but otherwise ok.
606                        sleep(timeout).await;
607                        if err.kind() == std::io::ErrorKind::WouldBlock {
608                            timeout *= 2;
609                        }
610                    }
611                }
612            }?;
613            locked_tx
614                .send(())
615                .ok()
616                .context("Cannot notify about lockfile locking")?;
617            let (_tx, rx) = oneshot::channel();
618            rx.await?;
619            Ok(())
620        });
621        if locked_rx.await.is_err() {
622            bail!(
623                "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)"
624            );
625        };
626        Ok(Some(lock_task))
627    }
628
629    /// Creates a new Config for `file`, but doesn't open/sync it.
630    async fn new_nosync(file: PathBuf, lock: bool) -> Result<Self> {
631        let dir = file.parent().context("Cannot get config file directory")?;
632        let inner = InnerConfig {
633            accounts: Vec::new(),
634            selected_account: 0,
635            next_id: 1,
636            accounts_order: Vec::new(),
637        };
638        if !lock {
639            let cfg = Self {
640                file,
641                inner,
642                lock_task: None,
643            };
644            return Ok(cfg);
645        }
646        let lock_task = Self::create_lock_task(dir.to_path_buf()).await?;
647        let cfg = Self {
648            file,
649            inner,
650            lock_task,
651        };
652        Ok(cfg)
653    }
654
655    /// Creates a new configuration file in the given account manager directory.
656    pub async fn new(dir: &Path) -> Result<Self> {
657        let lock = true;
658        let mut cfg = Self::new_nosync(dir.join(CONFIG_NAME), lock).await?;
659        cfg.sync().await?;
660
661        Ok(cfg)
662    }
663
664    /// Sync the inmemory representation to disk.
665    /// Takes a mutable reference because the saved file is a part of the `Config` state. This
666    /// protects from parallel calls resulting to a wrong file contents.
667    async fn sync(&mut self) -> Result<()> {
668        #[cfg(not(target_os = "ios"))]
669        ensure!(
670            !self
671                .lock_task
672                .as_ref()
673                .context("Config is read-only")?
674                .is_finished()
675        );
676
677        let tmp_path = self.file.with_extension("toml.tmp");
678        let mut file = fs::File::create(&tmp_path)
679            .await
680            .context("failed to create a tmp config")?;
681        file.write_all(toml::to_string_pretty(&self.inner)?.as_bytes())
682            .await
683            .context("failed to write a tmp config")?;
684        file.sync_data()
685            .await
686            .context("failed to sync a tmp config")?;
687        drop(file);
688        fs::rename(&tmp_path, &self.file)
689            .await
690            .context("failed to rename config")?;
691        Ok(())
692    }
693
694    /// Read a configuration from the given file into memory.
695    pub async fn from_file(file: PathBuf, writable: bool) -> Result<Self> {
696        let mut config = Self::new_nosync(file, writable).await?;
697        let bytes = fs::read(&config.file)
698            .await
699            .context("Failed to read file")?;
700        let s = std::str::from_utf8(&bytes)?;
701        config.inner = toml::from_str(s).context("Failed to parse config")?;
702
703        // Previous versions of the core stored absolute paths in account config.
704        // Convert them to relative paths.
705        let mut modified = false;
706        for account in &mut config.inner.accounts {
707            if account.dir.is_absolute()
708                && let Some(old_path_parent) = account.dir.parent()
709                && let Ok(new_path) = account.dir.strip_prefix(old_path_parent)
710            {
711                account.dir = new_path.to_path_buf();
712                modified = true;
713            }
714        }
715        if modified && writable {
716            config.sync().await?;
717        }
718
719        Ok(config)
720    }
721
722    /// Loads all accounts defined in the configuration file.
723    ///
724    /// Created contexts share the same event channel and stock string
725    /// translations.
726    pub async fn load_accounts(
727        &self,
728        events: &Events,
729        stockstrings: &StockStrings,
730        push_subscriber: PushSubscriber,
731        dir: &Path,
732    ) -> Result<BTreeMap<u32, Context>> {
733        let mut accounts = BTreeMap::new();
734
735        for account_config in &self.inner.accounts {
736            let dbfile = account_config.dbfile(dir);
737            let ctx = ContextBuilder::new(dbfile.clone())
738                .with_id(account_config.id)
739                .with_events(events.clone())
740                .with_stock_strings(stockstrings.clone())
741                .with_push_subscriber(push_subscriber.clone())
742                .build()
743                .await
744                .with_context(|| format!("failed to create context from file {:?}", &dbfile))?;
745            // Try to open without a passphrase,
746            // but do not return an error if account is passphare-protected.
747            ctx.open("".to_string()).await?;
748
749            accounts.insert(account_config.id, ctx);
750        }
751
752        Ok(accounts)
753    }
754
755    /// Creates a new account in the account manager directory.
756    #[expect(clippy::arithmetic_side_effects)]
757    async fn new_account(&mut self) -> Result<AccountConfig> {
758        let id = {
759            let id = self.inner.next_id;
760            let uuid = Uuid::new_v4();
761            let target_dir = PathBuf::from(uuid.to_string());
762
763            self.inner.accounts.push(AccountConfig {
764                id,
765                dir: target_dir,
766                uuid,
767            });
768            self.inner.next_id += 1;
769
770            // Add new account to the end of the order list
771            self.inner.accounts_order.push(id);
772
773            id
774        };
775
776        self.sync().await?;
777
778        self.select_account(id)
779            .await
780            .context("failed to select just added account")?;
781        let cfg = self
782            .get_account(id)
783            .context("failed to get just added account")?;
784        Ok(cfg)
785    }
786
787    /// Removes an existing account entirely.
788    pub async fn remove_account(&mut self, id: u32) -> Result<()> {
789        {
790            if let Some(idx) = self.inner.accounts.iter().position(|e| e.id == id) {
791                // remove account from the configs
792                self.inner.accounts.remove(idx);
793            }
794
795            // Remove from order list as well
796            self.inner.accounts_order.retain(|&x| x != id);
797
798            if self.inner.selected_account == id {
799                // reset selected account
800                self.inner.selected_account = self
801                    .inner
802                    .accounts
803                    .first()
804                    .map(|e| e.id)
805                    .unwrap_or_default();
806            }
807        }
808
809        self.sync().await
810    }
811
812    /// Returns configuration file section for the given account ID.
813    fn get_account(&self, id: u32) -> Option<AccountConfig> {
814        self.inner.accounts.iter().find(|e| e.id == id).cloned()
815    }
816
817    /// Returns the ID of selected account.
818    pub fn get_selected_account(&self) -> u32 {
819        self.inner.selected_account
820    }
821
822    /// Changes selected account ID.
823    pub async fn select_account(&mut self, id: u32) -> Result<()> {
824        {
825            ensure!(
826                self.inner.accounts.iter().any(|e| e.id == id),
827                "invalid account id: {id}"
828            );
829
830            self.inner.selected_account = id;
831        }
832
833        self.sync().await?;
834        Ok(())
835    }
836}
837
838/// Spend up to 1 minute trying to do the operation.
839///
840/// Even if Delta Chat itself does not hold the file lock,
841/// there may be other processes such as antivirus,
842/// or the filesystem may be network-mounted.
843///
844/// Without this workaround removing account may fail on Windows with an error
845/// "The process cannot access the file because it is being used by another process. (os error 32)".
846#[expect(clippy::arithmetic_side_effects)]
847async fn try_many_times<F, Fut, T>(f: F) -> std::result::Result<(), T>
848where
849    F: Fn() -> Fut,
850    Fut: Future<Output = std::result::Result<(), T>>,
851{
852    let mut counter = 0;
853    loop {
854        counter += 1;
855
856        if let Err(err) = f().await {
857            if counter > 60 {
858                return Err(err);
859            }
860
861            // Wait 1 second and try again.
862            tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
863        } else {
864            break;
865        }
866    }
867    Ok(())
868}
869
870/// Configuration of a single account.
871#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
872struct AccountConfig {
873    /// Unique id.
874    pub id: u32,
875
876    /// Root directory for all data for this account.
877    ///
878    /// The path is relative to the account manager directory.
879    pub dir: std::path::PathBuf,
880
881    /// Universally unique account identifier.
882    pub uuid: Uuid,
883}
884
885impl AccountConfig {
886    /// Get the canonical dbfile name for this configuration.
887    pub fn dbfile(&self, accounts_dir: &Path) -> std::path::PathBuf {
888        accounts_dir.join(&self.dir).join(DB_NAME)
889    }
890}
891
892#[cfg(test)]
893mod tests {
894    use super::*;
895    use crate::stock_str::{self, StockMessage};
896
897    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
898    async fn test_account_new_open() {
899        let dir = tempfile::tempdir().unwrap();
900        let p: PathBuf = dir.path().join("accounts1");
901
902        {
903            let writable = true;
904            let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
905            accounts.add_account().await.unwrap();
906
907            assert_eq!(accounts.accounts.len(), 1);
908            assert_eq!(accounts.config.get_selected_account(), 1);
909        }
910        for writable in [true, false] {
911            let accounts = Accounts::new(p.clone(), writable).await.unwrap();
912
913            assert_eq!(accounts.accounts.len(), 1);
914            assert_eq!(accounts.config.get_selected_account(), 1);
915        }
916    }
917
918    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
919    async fn test_account_new_open_conflict() {
920        let dir = tempfile::tempdir().unwrap();
921        let p: PathBuf = dir.path().join("accounts");
922        let writable = true;
923        let _accounts = Accounts::new(p.clone(), writable).await.unwrap();
924
925        let writable = true;
926        assert!(Accounts::new(p.clone(), writable).await.is_err());
927
928        let writable = false;
929        let accounts = Accounts::new(p, writable).await.unwrap();
930        assert_eq!(accounts.accounts.len(), 0);
931        assert_eq!(accounts.config.get_selected_account(), 0);
932    }
933
934    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
935    async fn test_account_new_add_remove() {
936        let dir = tempfile::tempdir().unwrap();
937        let p: PathBuf = dir.path().join("accounts");
938
939        let writable = true;
940        let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
941        assert_eq!(accounts.accounts.len(), 0);
942        assert_eq!(accounts.config.get_selected_account(), 0);
943
944        let id = accounts.add_account().await.unwrap();
945        assert_eq!(id, 1);
946        assert_eq!(accounts.accounts.len(), 1);
947        assert_eq!(accounts.config.get_selected_account(), 1);
948
949        let id = accounts.add_account().await.unwrap();
950        assert_eq!(id, 2);
951        assert_eq!(accounts.config.get_selected_account(), id);
952        assert_eq!(accounts.accounts.len(), 2);
953
954        accounts.select_account(1).await.unwrap();
955        assert_eq!(accounts.config.get_selected_account(), 1);
956
957        accounts.remove_account(1).await.unwrap();
958        assert_eq!(accounts.config.get_selected_account(), 2);
959        assert_eq!(accounts.accounts.len(), 1);
960    }
961
962    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
963    async fn test_accounts_remove_last() -> Result<()> {
964        let dir = tempfile::tempdir()?;
965        let p: PathBuf = dir.path().join("accounts");
966
967        let writable = true;
968        let mut accounts = Accounts::new(p.clone(), writable).await?;
969        assert!(accounts.get_selected_account().is_none());
970        assert_eq!(accounts.config.get_selected_account(), 0);
971
972        let id = accounts.add_account().await?;
973        assert!(accounts.get_selected_account().is_some());
974        assert_eq!(id, 1);
975        assert_eq!(accounts.accounts.len(), 1);
976        assert_eq!(accounts.config.get_selected_account(), id);
977
978        accounts.remove_account(id).await?;
979        assert!(accounts.get_selected_account().is_none());
980
981        Ok(())
982    }
983
984    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
985    async fn test_migrate_account() {
986        let dir = tempfile::tempdir().unwrap();
987        let p: PathBuf = dir.path().join("accounts");
988
989        let writable = true;
990        let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
991        assert_eq!(accounts.accounts.len(), 0);
992        assert_eq!(accounts.config.get_selected_account(), 0);
993
994        let extern_dbfile: PathBuf = dir.path().join("other");
995        let ctx = Context::new(&extern_dbfile, 0, Events::new(), StockStrings::new())
996            .await
997            .unwrap();
998        ctx.set_config(crate::config::Config::Addr, Some("me@mail.com"))
999            .await
1000            .unwrap();
1001
1002        drop(ctx);
1003
1004        accounts
1005            .migrate_account(extern_dbfile.clone())
1006            .await
1007            .unwrap();
1008        assert_eq!(accounts.accounts.len(), 1);
1009        assert_eq!(accounts.config.get_selected_account(), 1);
1010
1011        let ctx = accounts.get_selected_account().unwrap();
1012        assert_eq!(
1013            "me@mail.com",
1014            ctx.get_config(crate::config::Config::Addr)
1015                .await
1016                .unwrap()
1017                .unwrap()
1018        );
1019    }
1020
1021    /// Tests that accounts are sorted by ID.
1022    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1023    async fn test_accounts_sorted() {
1024        let dir = tempfile::tempdir().unwrap();
1025        let p: PathBuf = dir.path().join("accounts");
1026
1027        let writable = true;
1028        let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
1029
1030        for expected_id in 1..10 {
1031            let id = accounts.add_account().await.unwrap();
1032            assert_eq!(id, expected_id);
1033        }
1034
1035        let ids = accounts.get_all();
1036        for (i, expected_id) in (1..10).enumerate() {
1037            assert_eq!(ids.get(i), Some(&expected_id));
1038        }
1039    }
1040
1041    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1042    async fn test_accounts_ids_unique_increasing_and_persisted() -> Result<()> {
1043        let dir = tempfile::tempdir()?;
1044        let p: PathBuf = dir.path().join("accounts");
1045        let dummy_accounts = 10;
1046
1047        let (id0, id1, id2) = {
1048            let writable = true;
1049            let mut accounts = Accounts::new(p.clone(), writable).await?;
1050            accounts.add_account().await?;
1051            let ids = accounts.get_all();
1052            assert_eq!(ids.len(), 1);
1053
1054            let id0 = *ids.first().unwrap();
1055            let ctx = accounts.get_account(id0).unwrap();
1056            ctx.set_config(crate::config::Config::Addr, Some("one@example.org"))
1057                .await?;
1058
1059            let id1 = accounts.add_account().await?;
1060            let ctx = accounts.get_account(id1).unwrap();
1061            ctx.set_config(crate::config::Config::Addr, Some("two@example.org"))
1062                .await?;
1063
1064            // add and remove some accounts and force a gap (ids must not be reused)
1065            for _ in 0..dummy_accounts {
1066                let to_delete = accounts.add_account().await?;
1067                accounts.remove_account(to_delete).await?;
1068            }
1069
1070            let id2 = accounts.add_account().await?;
1071            let ctx = accounts.get_account(id2).unwrap();
1072            ctx.set_config(crate::config::Config::Addr, Some("three@example.org"))
1073                .await?;
1074
1075            accounts.select_account(id1).await?;
1076
1077            (id0, id1, id2)
1078        };
1079        assert!(id0 > 0);
1080        assert!(id1 > id0);
1081        assert!(id2 > id1 + dummy_accounts);
1082
1083        let (id0_reopened, id1_reopened, id2_reopened) = {
1084            let writable = false;
1085            let accounts = Accounts::new(p.clone(), writable).await?;
1086            let ctx = accounts.get_selected_account().unwrap();
1087            assert_eq!(
1088                ctx.get_config(crate::config::Config::Addr).await?,
1089                Some("two@example.org".to_string())
1090            );
1091
1092            let ids = accounts.get_all();
1093            assert_eq!(ids.len(), 3);
1094
1095            let id0 = *ids.first().unwrap();
1096            let ctx = accounts.get_account(id0).unwrap();
1097            assert_eq!(
1098                ctx.get_config(crate::config::Config::Addr).await?,
1099                Some("one@example.org".to_string())
1100            );
1101
1102            let id1 = *ids.get(1).unwrap();
1103            let t = accounts.get_account(id1).unwrap();
1104            assert_eq!(
1105                t.get_config(crate::config::Config::Addr).await?,
1106                Some("two@example.org".to_string())
1107            );
1108
1109            let id2 = *ids.get(2).unwrap();
1110            let ctx = accounts.get_account(id2).unwrap();
1111            assert_eq!(
1112                ctx.get_config(crate::config::Config::Addr).await?,
1113                Some("three@example.org".to_string())
1114            );
1115
1116            (id0, id1, id2)
1117        };
1118        assert_eq!(id0, id0_reopened);
1119        assert_eq!(id1, id1_reopened);
1120        assert_eq!(id2, id2_reopened);
1121
1122        Ok(())
1123    }
1124
1125    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1126    async fn test_no_accounts_event_emitter() -> Result<()> {
1127        let dir = tempfile::tempdir().unwrap();
1128        let p: PathBuf = dir.path().join("accounts");
1129
1130        let writable = true;
1131        let accounts = Accounts::new(p.clone(), writable).await?;
1132
1133        // Make sure there are no accounts.
1134        assert_eq!(accounts.accounts.len(), 0);
1135
1136        // Create event emitter.
1137        let event_emitter = accounts.get_event_emitter();
1138
1139        // Test that event emitter does not return `None` immediately.
1140        let duration = std::time::Duration::from_millis(1);
1141        assert!(
1142            tokio::time::timeout(duration, event_emitter.recv())
1143                .await
1144                .is_err()
1145        );
1146
1147        // When account manager is dropped, event emitter is exhausted.
1148        drop(accounts);
1149        assert_eq!(event_emitter.recv().await, None);
1150
1151        Ok(())
1152    }
1153
1154    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1155    async fn test_encrypted_account() -> Result<()> {
1156        let dir = tempfile::tempdir().context("failed to create tempdir")?;
1157        let p: PathBuf = dir.path().join("accounts");
1158
1159        let writable = true;
1160        let mut accounts = Accounts::new(p.clone(), writable)
1161            .await
1162            .context("failed to create accounts manager")?;
1163
1164        assert_eq!(accounts.accounts.len(), 0);
1165        let account_id = accounts
1166            .add_closed_account()
1167            .await
1168            .context("failed to add closed account")?;
1169        let account = accounts
1170            .get_selected_account()
1171            .context("failed to get account")?;
1172        assert_eq!(account.id, account_id);
1173        let passphrase_set_success = account
1174            .open("foobar".to_string())
1175            .await
1176            .context("failed to set passphrase")?;
1177        assert!(passphrase_set_success);
1178        drop(accounts);
1179
1180        let writable = false;
1181        let accounts = Accounts::new(p.clone(), writable)
1182            .await
1183            .context("failed to create second accounts manager")?;
1184        let account = accounts
1185            .get_selected_account()
1186            .context("failed to get account")?;
1187        assert_eq!(account.is_open().await, false);
1188
1189        // Try wrong passphrase.
1190        assert_eq!(account.open("barfoo".to_string()).await?, false);
1191        assert_eq!(account.open("".to_string()).await?, false);
1192
1193        assert_eq!(account.open("foobar".to_string()).await?, true);
1194        assert_eq!(account.is_open().await, true);
1195
1196        Ok(())
1197    }
1198
1199    /// Tests that accounts share stock string translations.
1200    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1201    async fn test_accounts_share_translations() -> Result<()> {
1202        let dir = tempfile::tempdir().unwrap();
1203        let p: PathBuf = dir.path().join("accounts");
1204
1205        let writable = true;
1206        let mut accounts = Accounts::new(p.clone(), writable).await?;
1207        accounts.add_account().await?;
1208        accounts.add_account().await?;
1209
1210        let account1 = accounts.get_account(1).context("failed to get account 1")?;
1211        let account2 = accounts.get_account(2).context("failed to get account 2")?;
1212
1213        assert_eq!(stock_str::no_messages(&account1).await, "No messages.");
1214        assert_eq!(stock_str::no_messages(&account2).await, "No messages.");
1215        account1
1216            .set_stock_translation(StockMessage::NoMessages, "foobar".to_string())
1217            .await?;
1218        assert_eq!(stock_str::no_messages(&account1).await, "foobar");
1219        assert_eq!(stock_str::no_messages(&account2).await, "foobar");
1220
1221        Ok(())
1222    }
1223}