Skip to main content

rusmes_jmap/methods/
mod.rs

1//! JMAP method handlers
2
3pub mod email;
4pub mod email_advanced;
5pub(crate) mod email_query_helpers;
6pub mod identity;
7pub mod mailbox;
8pub mod push_subscription;
9pub mod search_snippet;
10pub mod submission;
11pub mod thread;
12pub mod vacation;
13
14use crate::blob::BlobStorage;
15use crate::types::{JmapError, JmapErrorType, JmapMethodCall, JmapMethodResponse, Principal};
16use rusmes_core::transport::NullMailTransport;
17use rusmes_storage::backends::filesystem::FilesystemBackend;
18use rusmes_storage::StorageBackend;
19use std::path::PathBuf;
20use std::sync::Arc;
21
22/// Dispatch JMAP method call.
23///
24/// Method handlers receive `&Principal` so they can enforce that the
25/// `accountId` named in the JMAP request belongs to the authenticated
26/// caller; mismatches are rejected with `urn:ietf:params:jmap:error:forbidden`.
27///
28/// Outgoing mail delivery uses a [`NullMailTransport`] by default.  Callers
29/// that need real SMTP delivery should construct a dedicated dispatch path
30/// with a concrete transport.
31pub async fn dispatch_method(
32    call: JmapMethodCall,
33    capabilities: &[String],
34    principal: &Principal,
35) -> anyhow::Result<JmapMethodResponse> {
36    let method_name = &call.0;
37    let call_id = &call.2;
38
39    // PushSubscription methods are handled without per-account state.
40    if method_name == "PushSubscription/get" {
41        let request = serde_json::from_value(call.1)?;
42        let response = push_subscription::push_subscription_get(request, principal).await?;
43        return Ok(JmapMethodResponse(
44            "PushSubscription/get".to_string(),
45            serde_json::to_value(response)?,
46            call_id.clone(),
47        ));
48    }
49    if method_name == "PushSubscription/set" {
50        let request = serde_json::from_value(call.1)?;
51        let response = push_subscription::push_subscription_set(request, principal).await?;
52        return Ok(JmapMethodResponse(
53            "PushSubscription/set".to_string(),
54            serde_json::to_value(response)?,
55            call_id.clone(),
56        ));
57    }
58
59    // Validate method requires proper capability
60    if let Err(error) = validate_method_capability(method_name, capabilities) {
61        return Ok(JmapMethodResponse(
62            "error".to_string(),
63            serde_json::to_value(error)?,
64            call_id.clone(),
65        ));
66    }
67
68    // Get storage backend from configured path
69    let backend = Arc::new(FilesystemBackend::new(PathBuf::from("/tmp/rusmes/mail")));
70    let message_store = backend.message_store();
71    let blob_storage = BlobStorage::new();
72    let identity_store = identity::FileIdentityStore::new(PathBuf::from("/tmp/rusmes/jmap"));
73    let vacation_store = vacation::FileVacationStore::new(PathBuf::from("/tmp/rusmes/data"));
74    let submission_store = submission::FileSubmissionStore::new(PathBuf::from("/tmp/rusmes/jmap"));
75    let mail_transport = NullMailTransport;
76
77    // Dispatch to the appropriate handler
78    match method_name.as_str() {
79        // Email methods
80        "Email/get" => {
81            let request = serde_json::from_value(call.1)?;
82            let response = email::email_get(request, message_store.as_ref(), principal).await?;
83            Ok(JmapMethodResponse(
84                "Email/get".to_string(),
85                serde_json::to_value(response)?,
86                call_id.clone(),
87            ))
88        }
89        "Email/set" => {
90            let request = serde_json::from_value(call.1)?;
91            let response = email::email_set(request, message_store.as_ref(), principal).await?;
92            Ok(JmapMethodResponse(
93                "Email/set".to_string(),
94                serde_json::to_value(response)?,
95                call_id.clone(),
96            ))
97        }
98        "Email/query" => {
99            let request = serde_json::from_value(call.1)?;
100            let response = email::email_query(request, message_store.as_ref(), principal).await?;
101            Ok(JmapMethodResponse(
102                "Email/query".to_string(),
103                serde_json::to_value(response)?,
104                call_id.clone(),
105            ))
106        }
107        "Email/changes" => {
108            let request = serde_json::from_value(call.1)?;
109            let response =
110                email_advanced::email_changes(request, message_store.as_ref(), principal).await?;
111            Ok(JmapMethodResponse(
112                "Email/changes".to_string(),
113                serde_json::to_value(response)?,
114                call_id.clone(),
115            ))
116        }
117        "Email/queryChanges" => {
118            let request = serde_json::from_value(call.1)?;
119            let response =
120                email_advanced::email_query_changes(request, message_store.as_ref(), principal)
121                    .await?;
122            Ok(JmapMethodResponse(
123                "Email/queryChanges".to_string(),
124                serde_json::to_value(response)?,
125                call_id.clone(),
126            ))
127        }
128        "Email/copy" => {
129            let request = serde_json::from_value(call.1)?;
130            let response =
131                email_advanced::email_copy(request, message_store.as_ref(), principal).await?;
132            Ok(JmapMethodResponse(
133                "Email/copy".to_string(),
134                serde_json::to_value(response)?,
135                call_id.clone(),
136            ))
137        }
138        "Email/import" => {
139            let request = serde_json::from_value(call.1)?;
140            let response = email_advanced::email_import(
141                request,
142                message_store.as_ref(),
143                &blob_storage,
144                principal,
145            )
146            .await?;
147            Ok(JmapMethodResponse(
148                "Email/import".to_string(),
149                serde_json::to_value(response)?,
150                call_id.clone(),
151            ))
152        }
153        "Email/parse" => {
154            let request = serde_json::from_value(call.1)?;
155            let response = email_advanced::email_parse(
156                request,
157                message_store.as_ref(),
158                &blob_storage,
159                principal,
160            )
161            .await?;
162            Ok(JmapMethodResponse(
163                "Email/parse".to_string(),
164                serde_json::to_value(response)?,
165                call_id.clone(),
166            ))
167        }
168
169        // EmailSubmission methods
170        "EmailSubmission/get" => {
171            let request = serde_json::from_value(call.1)?;
172            let response =
173                submission::email_submission_get(request, message_store.as_ref(), principal)
174                    .await?;
175            Ok(JmapMethodResponse(
176                "EmailSubmission/get".to_string(),
177                serde_json::to_value(response)?,
178                call_id.clone(),
179            ))
180        }
181        "EmailSubmission/set" => {
182            let request = serde_json::from_value(call.1)?;
183            let ctx = submission::SubmissionContext {
184                message_store: message_store.as_ref(),
185                submission_store: &submission_store,
186                identity_store: &identity_store,
187                mail_transport: &mail_transport,
188            };
189            let response = submission::email_submission_set(request, principal, &ctx).await?;
190            Ok(JmapMethodResponse(
191                "EmailSubmission/set".to_string(),
192                serde_json::to_value(response)?,
193                call_id.clone(),
194            ))
195        }
196        "EmailSubmission/query" => {
197            let request = serde_json::from_value(call.1)?;
198            let response =
199                submission::email_submission_query(request, message_store.as_ref(), principal)
200                    .await?;
201            Ok(JmapMethodResponse(
202                "EmailSubmission/query".to_string(),
203                serde_json::to_value(response)?,
204                call_id.clone(),
205            ))
206        }
207        "EmailSubmission/changes" => {
208            let request = serde_json::from_value(call.1)?;
209            let response =
210                submission::email_submission_changes(request, message_store.as_ref(), principal)
211                    .await?;
212            Ok(JmapMethodResponse(
213                "EmailSubmission/changes".to_string(),
214                serde_json::to_value(response)?,
215                call_id.clone(),
216            ))
217        }
218
219        // Mailbox methods
220        "Mailbox/get" => {
221            let request = serde_json::from_value(call.1)?;
222            let response = mailbox::mailbox_get(request, message_store.as_ref(), principal).await?;
223            Ok(JmapMethodResponse(
224                "Mailbox/get".to_string(),
225                serde_json::to_value(response)?,
226                call_id.clone(),
227            ))
228        }
229        "Mailbox/set" => {
230            let request = serde_json::from_value(call.1)?;
231            let response = mailbox::mailbox_set(request, message_store.as_ref(), principal).await?;
232            Ok(JmapMethodResponse(
233                "Mailbox/set".to_string(),
234                serde_json::to_value(response)?,
235                call_id.clone(),
236            ))
237        }
238        "Mailbox/query" => {
239            let request = serde_json::from_value(call.1)?;
240            let response =
241                mailbox::mailbox_query(request, message_store.as_ref(), principal).await?;
242            Ok(JmapMethodResponse(
243                "Mailbox/query".to_string(),
244                serde_json::to_value(response)?,
245                call_id.clone(),
246            ))
247        }
248        "Mailbox/changes" => {
249            let request = serde_json::from_value(call.1)?;
250            let response =
251                mailbox::mailbox_changes(request, message_store.as_ref(), principal).await?;
252            Ok(JmapMethodResponse(
253                "Mailbox/changes".to_string(),
254                serde_json::to_value(response)?,
255                call_id.clone(),
256            ))
257        }
258        "Mailbox/queryChanges" => {
259            let request = serde_json::from_value(call.1)?;
260            let response =
261                mailbox::mailbox_query_changes(request, message_store.as_ref(), principal).await?;
262            Ok(JmapMethodResponse(
263                "Mailbox/queryChanges".to_string(),
264                serde_json::to_value(response)?,
265                call_id.clone(),
266            ))
267        }
268
269        // Thread methods
270        "Thread/get" => {
271            let request = serde_json::from_value(call.1)?;
272            let response = thread::thread_get(request, message_store.as_ref(), principal).await?;
273            Ok(JmapMethodResponse(
274                "Thread/get".to_string(),
275                serde_json::to_value(response)?,
276                call_id.clone(),
277            ))
278        }
279        "Thread/changes" => {
280            let request = serde_json::from_value(call.1)?;
281            let response =
282                thread::thread_changes(request, message_store.as_ref(), principal).await?;
283            Ok(JmapMethodResponse(
284                "Thread/changes".to_string(),
285                serde_json::to_value(response)?,
286                call_id.clone(),
287            ))
288        }
289
290        // SearchSnippet methods
291        "SearchSnippet/get" => {
292            let request = serde_json::from_value(call.1)?;
293            let response =
294                search_snippet::search_snippet_get(request, message_store.as_ref(), principal)
295                    .await?;
296            Ok(JmapMethodResponse(
297                "SearchSnippet/get".to_string(),
298                serde_json::to_value(response)?,
299                call_id.clone(),
300            ))
301        }
302
303        // Identity methods
304        "Identity/get" => {
305            let request = serde_json::from_value(call.1)?;
306            let response =
307                identity::identity_get(request, message_store.as_ref(), &identity_store, principal)
308                    .await?;
309            Ok(JmapMethodResponse(
310                "Identity/get".to_string(),
311                serde_json::to_value(response)?,
312                call_id.clone(),
313            ))
314        }
315        "Identity/set" => {
316            let request = serde_json::from_value(call.1)?;
317            let response =
318                identity::identity_set(request, message_store.as_ref(), &identity_store, principal)
319                    .await?;
320            Ok(JmapMethodResponse(
321                "Identity/set".to_string(),
322                serde_json::to_value(response)?,
323                call_id.clone(),
324            ))
325        }
326        "Identity/changes" => {
327            let request = serde_json::from_value(call.1)?;
328            let response = identity::identity_changes(
329                request,
330                message_store.as_ref(),
331                &identity_store,
332                principal,
333            )
334            .await?;
335            Ok(JmapMethodResponse(
336                "Identity/changes".to_string(),
337                serde_json::to_value(response)?,
338                call_id.clone(),
339            ))
340        }
341
342        // VacationResponse methods
343        "VacationResponse/get" => {
344            let request = serde_json::from_value(call.1)?;
345            let response = vacation::vacation_response_get(
346                request,
347                message_store.as_ref(),
348                principal,
349                &vacation_store,
350            )
351            .await?;
352            Ok(JmapMethodResponse(
353                "VacationResponse/get".to_string(),
354                serde_json::to_value(response)?,
355                call_id.clone(),
356            ))
357        }
358        "VacationResponse/set" => {
359            let request = serde_json::from_value(call.1)?;
360            let response = vacation::vacation_response_set(
361                request,
362                message_store.as_ref(),
363                principal,
364                &vacation_store,
365            )
366            .await?;
367            Ok(JmapMethodResponse(
368                "VacationResponse/set".to_string(),
369                serde_json::to_value(response)?,
370                call_id.clone(),
371            ))
372        }
373
374        _ => {
375            // Return unknownMethod error
376            Ok(JmapMethodResponse(
377                "error".to_string(),
378                serde_json::to_value(
379                    JmapError::new(JmapErrorType::UnknownMethod)
380                        .with_detail(format!("Unknown method: {}", method_name)),
381                )?,
382                call_id.clone(),
383            ))
384        }
385    }
386}
387
388/// Validate that the method is supported by the declared capabilities
389fn validate_method_capability(method_name: &str, capabilities: &[String]) -> Result<(), JmapError> {
390    let required_capability = match method_name {
391        m if m.starts_with("Email/") => "urn:ietf:params:jmap:mail",
392        m if m.starts_with("Mailbox/") => "urn:ietf:params:jmap:mail",
393        m if m.starts_with("Thread/") => "urn:ietf:params:jmap:mail",
394        m if m.starts_with("SearchSnippet/") => "urn:ietf:params:jmap:mail",
395        m if m.starts_with("EmailSubmission/") => "urn:ietf:params:jmap:submission",
396        m if m.starts_with("Identity/") => "urn:ietf:params:jmap:submission",
397        m if m.starts_with("VacationResponse/") => "urn:ietf:params:jmap:vacationresponse",
398        // PushSubscription is a core RFC 8620 method — only core capability required.
399        m if m.starts_with("PushSubscription/") => {
400            return Ok(());
401        }
402        _ => {
403            // Core methods don't require additional capabilities beyond core
404            return Ok(());
405        }
406    };
407
408    if !capabilities.iter().any(|cap| cap == required_capability) {
409        return Err(
410            JmapError::new(JmapErrorType::UnknownMethod).with_detail(format!(
411                "Method '{}' requires capability '{}' which was not declared in 'using'",
412                method_name, required_capability
413            )),
414        );
415    }
416
417    Ok(())
418}
419
420/// Helper used by every method handler: assert that `requested_account_id`
421/// matches the principal's owned account and return a [`ForbiddenError`]
422/// otherwise. The error converts cleanly into `anyhow::Error` via the
423/// `?` operator.
424pub(crate) fn ensure_account_ownership(
425    requested_account_id: &str,
426    principal: &Principal,
427) -> Result<(), ForbiddenError> {
428    if principal.owns_account(requested_account_id) {
429        Ok(())
430    } else {
431        tracing::warn!(
432            "JMAP account ownership mismatch: principal {} attempted to access account {}",
433            principal.username,
434            requested_account_id
435        );
436        Err(ForbiddenError {
437            requested_account_id: requested_account_id.to_string(),
438            principal_account_id: principal.account_id.clone(),
439        })
440    }
441}
442
443/// Strongly-typed ownership-mismatch error returned by individual method
444/// handlers. Implements `From` into `anyhow::Error` via [`std::error::Error`]
445/// so handlers can use `?` directly.
446#[derive(Debug, Clone)]
447pub struct ForbiddenError {
448    /// `accountId` named in the JMAP request.
449    pub requested_account_id: String,
450    /// `accountId` actually owned by the authenticated [`Principal`].
451    pub principal_account_id: String,
452}
453
454impl std::fmt::Display for ForbiddenError {
455    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
456        write!(
457            f,
458            "{}: requested account '{}' is not owned by principal (owns '{}')",
459            JmapErrorType::Forbidden.as_str(),
460            self.requested_account_id,
461            self.principal_account_id
462        )
463    }
464}
465
466impl std::error::Error for ForbiddenError {}
467
468#[cfg(test)]
469mod tests {
470    use super::*;
471    use crate::types::Principal;
472
473    fn alice() -> Principal {
474        Principal {
475            username: "alice".to_string(),
476            account_id: "account-alice".to_string(),
477            scopes: vec![],
478        }
479    }
480
481    #[test]
482    fn ensure_ownership_ok() {
483        let p = alice();
484        assert!(ensure_account_ownership("account-alice", &p).is_ok());
485    }
486
487    #[test]
488    fn ensure_ownership_rejected() {
489        let p = alice();
490        let err = ensure_account_ownership("account-bob", &p).expect_err("should reject");
491        assert_eq!(err.requested_account_id, "account-bob");
492        assert_eq!(err.principal_account_id, "account-alice");
493    }
494}