jmap_client/core/
session.rs

1/*
2 * Copyright Stalwart Labs LLC See the COPYING
3 * file at the top-level directory of this distribution.
4 *
5 * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6 * https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7 * <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
8 * option. This file may not be copied, modified, or distributed
9 * except according to those terms.
10 */
11
12use crate::{
13    email::{MailCapabilities, SubmissionCapabilities},
14    URI,
15};
16use ahash::AHashMap;
17use serde::{Deserialize, Serialize};
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct Session {
21    #[serde(rename = "capabilities")]
22    capabilities: AHashMap<String, Capabilities>,
23
24    #[serde(rename = "accounts")]
25    accounts: AHashMap<String, Account>,
26
27    #[serde(rename = "primaryAccounts")]
28    primary_accounts: AHashMap<String, String>,
29
30    #[serde(rename = "username")]
31    username: String,
32
33    #[serde(rename = "apiUrl")]
34    api_url: String,
35
36    #[serde(rename = "downloadUrl")]
37    download_url: String,
38
39    #[serde(rename = "uploadUrl")]
40    upload_url: String,
41
42    #[serde(rename = "eventSourceUrl")]
43    event_source_url: String,
44
45    #[serde(rename = "state")]
46    state: String,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct Account {
51    #[serde(rename = "name")]
52    name: String,
53
54    #[serde(rename = "isPersonal")]
55    is_personal: bool,
56
57    #[serde(rename = "isReadOnly")]
58    is_read_only: bool,
59
60    #[serde(rename = "accountCapabilities")]
61    account_capabilities: AHashMap<String, Capabilities>,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
65#[serde(untagged)]
66pub enum Capabilities {
67    Core(CoreCapabilities),
68    Mail(MailCapabilities),
69    Submission(SubmissionCapabilities),
70    WebSocket(WebSocketCapabilities),
71    Sieve(SieveCapabilities),
72    Empty(EmptyCapabilities),
73    Other(serde_json::Value),
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct CoreCapabilities {
78    #[serde(rename = "maxSizeUpload")]
79    max_size_upload: usize,
80
81    #[serde(rename = "maxConcurrentUpload")]
82    max_concurrent_upload: usize,
83
84    #[serde(rename = "maxSizeRequest")]
85    max_size_request: usize,
86
87    #[serde(rename = "maxConcurrentRequests")]
88    max_concurrent_requests: usize,
89
90    #[serde(rename = "maxCallsInRequest")]
91    max_calls_in_request: usize,
92
93    #[serde(rename = "maxObjectsInGet")]
94    max_objects_in_get: usize,
95
96    #[serde(rename = "maxObjectsInSet")]
97    max_objects_in_set: usize,
98
99    #[serde(rename = "collationAlgorithms")]
100    collation_algorithms: Vec<String>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct WebSocketCapabilities {
105    #[serde(rename = "url")]
106    url: String,
107    #[serde(rename = "supportsPush")]
108    supports_push: bool,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct SieveCapabilities {
113    #[serde(rename = "implementation")]
114    implementation: Option<String>,
115    #[serde(rename = "maxSizeScriptName")]
116    max_script_name: Option<usize>,
117    #[serde(rename = "maxSizeScript")]
118    max_script_size: Option<usize>,
119    #[serde(rename = "maxNumberScripts")]
120    max_scripts: Option<usize>,
121    #[serde(rename = "maxNumberRedirects")]
122    max_redirects: Option<usize>,
123    #[serde(rename = "sieveExtensions")]
124    extensions: Vec<String>,
125    #[serde(rename = "notificationMethods")]
126    notification_methods: Option<Vec<String>>,
127    #[serde(rename = "externalLists")]
128    ext_lists: Option<Vec<String>>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct EmptyCapabilities {}
133
134impl Session {
135    pub fn capabilities(&self) -> impl Iterator<Item = &String> {
136        self.capabilities.keys()
137    }
138
139    pub fn capability(&self, capability: impl AsRef<str>) -> Option<&Capabilities> {
140        self.capabilities.get(capability.as_ref())
141    }
142
143    pub fn has_capability(&self, capability: impl AsRef<str>) -> bool {
144        self.capabilities.contains_key(capability.as_ref())
145    }
146
147    pub fn websocket_capabilities(&self) -> Option<&WebSocketCapabilities> {
148        self.capabilities
149            .get(URI::WebSocket.as_ref())
150            .and_then(|v| match v {
151                Capabilities::WebSocket(capabilities) => Some(capabilities),
152                _ => None,
153            })
154    }
155
156    pub fn core_capabilities(&self) -> Option<&CoreCapabilities> {
157        self.capabilities
158            .get(URI::Core.as_ref())
159            .and_then(|v| match v {
160                Capabilities::Core(capabilities) => Some(capabilities),
161                _ => None,
162            })
163    }
164
165    pub fn mail_capabilities(&self) -> Option<&MailCapabilities> {
166        self.capabilities
167            .get(URI::Mail.as_ref())
168            .and_then(|v| match v {
169                Capabilities::Mail(capabilities) => Some(capabilities),
170                _ => None,
171            })
172    }
173
174    pub fn submission_capabilities(&self) -> Option<&SubmissionCapabilities> {
175        self.capabilities
176            .get(URI::Submission.as_ref())
177            .and_then(|v| match v {
178                Capabilities::Submission(capabilities) => Some(capabilities),
179                _ => None,
180            })
181    }
182
183    pub fn sieve_capabilities(&self) -> Option<&SieveCapabilities> {
184        self.capabilities
185            .get(URI::Sieve.as_ref())
186            .and_then(|v| match v {
187                Capabilities::Sieve(capabilities) => Some(capabilities),
188                _ => None,
189            })
190    }
191
192    pub fn accounts(&self) -> impl Iterator<Item = &String> {
193        self.accounts.keys()
194    }
195
196    pub fn account(&self, account: &str) -> Option<&Account> {
197        self.accounts.get(account)
198    }
199
200    pub fn primary_accounts(&self) -> impl Iterator<Item = (&String, &String)> {
201        self.primary_accounts.iter()
202    }
203
204    pub fn username(&self) -> &str {
205        &self.username
206    }
207
208    pub fn api_url(&self) -> &str {
209        &self.api_url
210    }
211
212    pub fn download_url(&self) -> &str {
213        &self.download_url
214    }
215
216    pub fn upload_url(&self) -> &str {
217        &self.upload_url
218    }
219
220    pub fn event_source_url(&self) -> &str {
221        &self.event_source_url
222    }
223
224    pub fn state(&self) -> &str {
225        &self.state
226    }
227}
228
229impl Account {
230    pub fn name(&self) -> &str {
231        &self.name
232    }
233
234    pub fn is_personal(&self) -> bool {
235        self.is_personal
236    }
237
238    pub fn is_read_only(&self) -> bool {
239        self.is_read_only
240    }
241
242    pub fn capabilities(&self) -> impl Iterator<Item = &String> {
243        self.account_capabilities.keys()
244    }
245
246    pub fn capability(&self, capability: &str) -> Option<&Capabilities> {
247        self.account_capabilities.get(capability)
248    }
249}
250
251impl CoreCapabilities {
252    pub fn max_size_upload(&self) -> usize {
253        self.max_size_upload
254    }
255
256    pub fn max_concurrent_upload(&self) -> usize {
257        self.max_concurrent_upload
258    }
259
260    pub fn max_size_request(&self) -> usize {
261        self.max_size_request
262    }
263
264    pub fn max_concurrent_requests(&self) -> usize {
265        self.max_concurrent_requests
266    }
267
268    pub fn max_calls_in_request(&self) -> usize {
269        self.max_calls_in_request
270    }
271
272    pub fn max_objects_in_get(&self) -> usize {
273        self.max_objects_in_get
274    }
275
276    pub fn max_objects_in_set(&self) -> usize {
277        self.max_objects_in_set
278    }
279
280    pub fn collation_algorithms(&self) -> &[String] {
281        &self.collation_algorithms
282    }
283}
284
285impl WebSocketCapabilities {
286    pub fn url(&self) -> &str {
287        &self.url
288    }
289
290    pub fn supports_push(&self) -> bool {
291        self.supports_push
292    }
293}
294
295impl SieveCapabilities {
296    pub fn max_script_name_size(&self) -> usize {
297        self.max_script_name.unwrap_or(512)
298    }
299
300    pub fn max_script_size(&self) -> Option<usize> {
301        self.max_script_size
302    }
303
304    pub fn max_number_scripts(&self) -> Option<usize> {
305        self.max_scripts
306    }
307
308    pub fn max_number_redirects(&self) -> Option<usize> {
309        self.max_redirects
310    }
311
312    pub fn sieve_extensions(&self) -> &[String] {
313        &self.extensions
314    }
315
316    pub fn notification_methods(&self) -> Option<&[String]> {
317        self.notification_methods.as_deref()
318    }
319
320    pub fn external_lists(&self) -> Option<&[String]> {
321        self.ext_lists.as_deref()
322    }
323}
324
325pub trait URLParser: Sized {
326    fn parse(value: &str) -> Option<Self>;
327}
328
329pub enum URLPart<T: URLParser> {
330    Value(String),
331    Parameter(T),
332}
333
334impl<T: URLParser> URLPart<T> {
335    pub fn parse(url: &str) -> crate::Result<Vec<URLPart<T>>> {
336        let mut parts = Vec::new();
337        let mut buf = String::with_capacity(url.len());
338        let mut in_parameter = false;
339
340        for ch in url.chars() {
341            match ch {
342                '{' => {
343                    if !buf.is_empty() {
344                        parts.push(URLPart::Value(buf.clone()));
345                        buf.clear();
346                    }
347                    in_parameter = true;
348                }
349                '}' => {
350                    if in_parameter && !buf.is_empty() {
351                        parts.push(URLPart::Parameter(T::parse(&buf).ok_or_else(|| {
352                            crate::Error::Internal(format!(
353                                "Invalid parameter '{}' in URL: {}",
354                                buf, url
355                            ))
356                        })?));
357                        buf.clear();
358                    } else {
359                        return Err(crate::Error::Internal(format!("Invalid URL: {}", url)));
360                    }
361                    in_parameter = false;
362                }
363                _ => {
364                    buf.push(ch);
365                }
366            }
367        }
368
369        if !buf.is_empty() {
370            if !in_parameter {
371                parts.push(URLPart::Value(buf.clone()));
372            } else {
373                return Err(crate::Error::Internal(format!("Invalid URL: {}", url)));
374            }
375        }
376
377        Ok(parts)
378    }
379}