Skip to main content

hey_sdk/
account_scope.rs

1use std::sync::Arc;
2
3use crate::client::{Client, ScopeState};
4use crate::error::Error;
5use crate::generated::types::{Account, Identity};
6
7impl Client {
8    /// Derives a client that presents mail from one linked account and acts as that
9    /// account's user and default sender. The account is checked against the identity
10    /// first, so a stale or foreign id fails here rather than on the first read. That
11    /// check goes out through an unscoped client derived from this one: the identity
12    /// belongs to the whole login, and reading it filtered to an account would leave
13    /// nothing to check the account against.
14    ///
15    /// Calendar, journal, habits and time tracking belong to the identity, so they read
16    /// the same through a scoped client.
17    pub async fn for_account(&self, account_id: i64) -> Result<Client, Error> {
18        if account_id <= 0 {
19            return Err(Error::usage("account id must be positive"));
20        }
21        let root = Client {
22            shared: self.shared.clone(),
23            account_id: None,
24            scope: Arc::default(),
25        };
26        let identity = root.identity().get().await?;
27        let accessible = identity
28            .accounts
29            .iter()
30            .flatten()
31            .any(|account| account.id == account_id && account_is_accessible(account));
32        if !accessible {
33            return Err(Error::not_found("accessible account", account_id));
34        }
35        let scope = ScopeState::default();
36        *scope.default_sender_id.lock().await = default_sender_for(&identity, Some(account_id));
37        *scope.account_user_id.lock().await = identity
38            .all_users
39            .iter()
40            .flatten()
41            .find(|user| user.account_id == Some(account_id))
42            .map(|user| user.id);
43        Ok(Client {
44            shared: self.shared.clone(),
45            account_id: Some(account_id),
46            scope: Arc::new(scope),
47        })
48    }
49
50    /// The sender a message goes out as when the caller names none: the scoped
51    /// account's default sender, or the identity's default sender for All Accounts.
52    pub async fn default_sender_id(&self) -> Result<i64, Error> {
53        // The lock is held across the identity read on purpose: it makes concurrent callers
54        // share one read rather than each starting their own, and whoever arrives second
55        // finds the answer already in hand.
56        let mut cached = self.scope.default_sender_id.lock().await;
57        if let Some(id) = *cached {
58            return Ok(id);
59        }
60        let identity = self.identity().get().await?;
61        let id = match (
62            default_sender_for(&identity, self.account_id),
63            self.account_id,
64        ) {
65            (Some(id), _) => id,
66            (None, Some(account_id)) => {
67                return Err(Error::not_found("sender for account", account_id));
68            }
69            (None, None) => match identity.primary_contact.as_ref().map(|contact| contact.id) {
70                Some(id) => id,
71                None => return Err(Error::api(0, "no sender found in identity")),
72            },
73        };
74        *cached = Some(id);
75        Ok(id)
76    }
77
78    /// The identity's user in the scoped account, which is what a record is filed under.
79    pub async fn account_user_id(&self) -> Result<i64, Error> {
80        let account_id = self
81            .account_id
82            .ok_or_else(|| Error::usage("account user id needs an account-scoped client"))?;
83        // Held across the read, as in `default_sender_id`, so concurrent callers make one
84        // request between them.
85        let mut cached = self.scope.account_user_id.lock().await;
86        if let Some(id) = *cached {
87            return Ok(id);
88        }
89        let identity = self.identity().get().await?;
90        match identity
91            .all_users
92            .iter()
93            .flatten()
94            .find(|user| user.account_id == Some(account_id))
95        {
96            Some(user) => {
97                *cached = Some(user.id);
98                Ok(user.id)
99            }
100            None => Err(Error::not_found("user for account", account_id)),
101        }
102    }
103}
104
105fn account_is_accessible(account: &Account) -> bool {
106    let status = account.status.as_deref().unwrap_or_default();
107    let purpose = account.purpose.as_deref().unwrap_or_default();
108    status == "active" || (status == "inactive" && (purpose == "work" || purpose == "domains"))
109}
110
111fn default_sender_for(identity: &Identity, account_id: Option<i64>) -> Option<i64> {
112    let senders: Vec<_> = identity
113        .senders
114        .iter()
115        .flatten()
116        .filter(|sender| account_id.is_none_or(|id| sender.account_id == Some(id)))
117        .collect();
118    senders
119        .iter()
120        .find(|sender| sender.default == Some(true))
121        .or_else(|| senders.first())
122        .map(|sender| sender.id)
123}