1pub 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
22pub 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 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 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 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 match method_name.as_str() {
79 "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/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/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/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/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/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/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 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
388fn 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 m if m.starts_with("PushSubscription/") => {
400 return Ok(());
401 }
402 _ => {
403 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
420pub(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#[derive(Debug, Clone)]
447pub struct ForbiddenError {
448 pub requested_account_id: String,
450 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}