1pub mod email;
9pub mod identity;
10pub mod mailbox;
11pub mod search_snippet;
12pub mod submission;
13pub mod thread;
14pub mod vacation;
15
16use std::collections::HashMap;
17
18use jmap_types::{Id, State};
19
20pub use jmap_types::{
29 AddedItem, ChangesResponse, GetResponse, QueryChangesResponse, QueryResponse, SetError,
30 SetResponse,
31};
32
33#[derive(Debug, Default, serde::Serialize)]
41#[serde(rename_all = "camelCase")]
42pub struct EmailGetParams {
43 #[serde(skip_serializing_if = "Option::is_none")]
45 pub body_properties: Option<Vec<String>>,
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub fetch_text_body_values: Option<bool>,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub fetch_html_body_values: Option<bool>,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub fetch_all_body_values: Option<bool>,
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub max_body_value_bytes: Option<u64>,
58 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
63 pub extra: serde_json::Map<String, serde_json::Value>,
64}
65
66#[derive(Debug, serde::Serialize)]
68#[serde(rename_all = "camelCase")]
69pub struct EmailCopyParams {
70 pub from_account_id: Id,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub on_success_destroy_original: Option<bool>,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub destroy_from_if_in_state: Option<jmap_types::State>,
78 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
83 pub extra: serde_json::Map<String, serde_json::Value>,
84}
85
86#[derive(Debug, Default, serde::Serialize)]
88#[serde(rename_all = "camelCase")]
89pub struct MailboxSetParams {
90 #[serde(skip_serializing_if = "Option::is_none")]
93 pub on_destroy_remove_emails: Option<bool>,
94 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
99 pub extra: serde_json::Map<String, serde_json::Value>,
100}
101
102#[derive(Debug, Default, serde::Serialize)]
112#[serde(rename_all = "camelCase")]
113pub struct EmailSubmissionSetParams {
114 #[serde(skip_serializing_if = "Option::is_none")]
122 pub on_success_update_email: Option<HashMap<String, jmap_types::PatchObject>>,
123
124 #[serde(skip_serializing_if = "Option::is_none")]
129 pub on_success_destroy_email: Option<Vec<String>>,
130
131 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
136 pub extra: serde_json::Map<String, serde_json::Value>,
137}
138
139#[derive(Debug, serde::Serialize)]
148#[serde(rename_all = "camelCase")]
149pub struct EmailImportInput<'a> {
150 pub blob_id: &'a Id,
152 #[serde(serialize_with = "ser_mailbox_id_set")]
155 pub mailbox_ids: &'a [Id],
156 #[serde(
159 skip_serializing_if = "Option::is_none",
160 serialize_with = "ser_opt_keyword_set"
161 )]
162 pub keywords: Option<&'a [&'a str]>,
163 #[serde(skip_serializing_if = "Option::is_none")]
166 pub received_at: Option<&'a jmap_types::UTCDate>,
167 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
172 pub extra: serde_json::Map<String, serde_json::Value>,
173}
174
175fn ser_mailbox_id_set<S: serde::Serializer>(ids: &&[Id], s: S) -> Result<S::Ok, S::Error> {
176 use serde::ser::SerializeMap;
177 let mut m = s.serialize_map(Some(ids.len()))?;
178 for id in *ids {
179 m.serialize_entry(id.as_ref(), &true)?;
180 }
181 m.end()
182}
183
184fn ser_opt_keyword_set<S: serde::Serializer>(
185 kws: &Option<&[&str]>,
186 s: S,
187) -> Result<S::Ok, S::Error> {
188 use serde::ser::SerializeMap;
189 let kws = kws.expect("skip_serializing_if guarantees Some");
190 let mut m = s.serialize_map(Some(kws.len()))?;
191 for k in kws {
192 m.serialize_entry(k, &true)?;
193 }
194 m.end()
195}
196
197#[non_exhaustive]
203#[derive(Debug, Clone, serde::Deserialize)]
204#[serde(rename_all = "camelCase")]
205pub struct EmailImportCreated {
206 pub id: Id,
208 pub blob_id: Id,
210 pub thread_id: Id,
212 pub size: u64,
214 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
219 pub extra: serde_json::Map<String, serde_json::Value>,
220}
221
222#[non_exhaustive]
224#[derive(Debug, Clone, serde::Deserialize)]
225#[serde(rename_all = "camelCase")]
226pub struct EmailImportResponse {
227 pub account_id: Id,
229 #[serde(default)]
231 pub old_state: Option<State>,
232 pub new_state: State,
234 #[serde(default)]
236 pub created: Option<HashMap<String, EmailImportCreated>>,
237 #[serde(default)]
240 pub not_created: Option<HashMap<String, SetError>>,
241 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
246 pub extra: serde_json::Map<String, serde_json::Value>,
247}
248
249#[derive(Debug, Default, serde::Serialize)]
254#[serde(rename_all = "camelCase")]
255pub struct EmailParseParams {
256 #[serde(skip_serializing_if = "Option::is_none")]
260 pub properties: Option<Vec<String>>,
261 #[serde(skip_serializing_if = "Option::is_none")]
263 pub body_properties: Option<Vec<String>>,
264 #[serde(skip_serializing_if = "Option::is_none")]
266 pub fetch_text_body_values: Option<bool>,
267 #[serde(skip_serializing_if = "Option::is_none")]
269 pub fetch_html_body_values: Option<bool>,
270 #[serde(skip_serializing_if = "Option::is_none")]
272 pub fetch_all_body_values: Option<bool>,
273 #[serde(skip_serializing_if = "Option::is_none")]
275 pub max_body_value_bytes: Option<u64>,
276 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
281 pub extra: serde_json::Map<String, serde_json::Value>,
282}
283
284#[non_exhaustive]
290#[derive(Debug, Clone, serde::Deserialize)]
291#[serde(rename_all = "camelCase")]
292pub struct EmailParseResponse {
293 pub account_id: Id,
295 #[serde(default)]
297 pub parsed: Option<HashMap<Id, jmap_mail_types::Email>>,
298 #[serde(default)]
300 pub not_parsable: Option<Vec<Id>>,
301 #[serde(default)]
303 pub not_found: Option<Vec<Id>>,
304 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
309 pub extra: serde_json::Map<String, serde_json::Value>,
310}
311
312pub(crate) const CALL_ID: &str = "r1";
319
320pub(crate) const USING_MAIL: &[&str] = &["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"];
322
323pub(crate) fn build_request(
335 method: &str,
336 args: serde_json::Value,
337 using: &[&str],
338) -> jmap_types::JmapRequest {
339 let using_vec: Vec<String> = using.iter().map(|&s| s.to_owned()).collect();
340 let invocation: jmap_types::Invocation = (method.to_owned(), args, CALL_ID.to_owned());
341 jmap_types::JmapRequest::new(using_vec, vec![invocation], None)
342}
343
344#[non_exhaustive]
372#[derive(Clone)]
373pub struct SessionClient {
374 pub(crate) client: jmap_base_client::JmapClient,
375 pub(crate) session: jmap_base_client::Session,
376}
377
378impl std::fmt::Debug for SessionClient {
379 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
380 f.debug_struct("SessionClient")
381 .field("client", &"<JmapClient>")
385 .field("session", &self.session)
386 .finish()
387 }
388}
389
390impl SessionClient {
391 pub(crate) fn session_parts(&self) -> Result<(&str, &str), jmap_base_client::ClientError> {
396 let api_url = self.session.api_url.as_str();
397 let account_id = self
398 .session
399 .primary_account_id("urn:ietf:params:jmap:mail")
400 .ok_or_else(|| {
401 jmap_base_client::ClientError::InvalidSession(
402 "no primary account for urn:ietf:params:jmap:mail".into(),
403 )
404 })?;
405 Ok((api_url, account_id))
406 }
407
408 pub(crate) async fn call_internal(
410 &self,
411 api_url: &str,
412 req: &jmap_types::JmapRequest,
413 ) -> Result<jmap_types::JmapResponse, jmap_base_client::ClientError> {
414 self.client.call(api_url, req).await
415 }
416}
417
418#[cfg(test)]
423mod tests {
424 use super::*;
425 use serde_json::json;
426
427 #[test]
432 fn build_request_method_name_and_call_id() {
433 let req = build_request(
434 "Email/get",
435 json!({"accountId": "acc1", "ids": null}),
436 USING_MAIL,
437 );
438 let v = serde_json::to_value(&req).expect("serialize JmapRequest");
439
440 let calls = v["methodCalls"]
441 .as_array()
442 .expect("methodCalls must be array");
443 assert_eq!(calls.len(), 1, "must have exactly 1 method call");
444 assert_eq!(calls[0][0], json!("Email/get"), "method name must match");
445 assert_eq!(calls[0][2], json!("r1"), "call_id must be CALL_ID constant");
446 }
447
448 #[test]
451 fn using_mail_contains_correct_uris() {
452 let req = build_request("Email/get", json!({}), USING_MAIL);
453 let v = serde_json::to_value(&req).expect("serialize");
454 let using = v["using"].as_array().expect("using must be array");
455 assert_eq!(using.len(), 2);
456 assert!(
457 using.contains(&json!("urn:ietf:params:jmap:core")),
458 "must include jmap:core"
459 );
460 assert!(
461 using.contains(&json!("urn:ietf:params:jmap:mail")),
462 "must include jmap:mail"
463 );
464 }
465
466 #[test]
469 fn session_parts_err_no_primary_account() {
470 let session_json = json!({
471 "capabilities": {},
472 "accounts": {},
473 "primaryAccounts": {},
474 "username": "user@example.com",
475 "apiUrl": "https://jmap.example.com/api/",
476 "downloadUrl": "https://jmap.example.com/dl/{accountId}/{blobId}/{name}?accept={type}",
477 "uploadUrl": "https://jmap.example.com/ul/{accountId}/",
478 "eventSourceUrl": "https://jmap.example.com/sse/?types={types}&closeafter={closeafter}&ping={ping}",
479 "state": "s1"
480 });
481 let session: jmap_base_client::Session =
482 serde_json::from_value(session_json).expect("session must deserialize");
483
484 let result = session.primary_account_id("urn:ietf:params:jmap:mail");
485 assert!(
486 result.is_none(),
487 "must return None when mail capability is not in primaryAccounts"
488 );
489 }
490
491 #[test]
494 fn get_response_deserializes() {
495 let json = json!({
496 "accountId": "acc1",
497 "state": "s42",
498 "list": [],
499 "notFound": ["missing1"]
500 });
501 let resp: GetResponse<serde_json::Value> =
502 serde_json::from_value(json).expect("GetResponse must deserialize");
503 assert_eq!(resp.account_id, "acc1");
504 assert_eq!(resp.state, "s42");
505 assert!(resp.list.is_empty());
506 assert_eq!(
507 resp.not_found.as_deref(),
508 Some(["missing1".into()].as_slice())
509 );
510 }
511
512 #[test]
514 fn changes_response_deserializes() {
515 let json = json!({
516 "accountId": "acc1",
517 "oldState": "s10",
518 "newState": "s11",
519 "hasMoreChanges": false,
520 "created": ["id1"],
521 "updated": ["id2"],
522 "destroyed": []
523 });
524 let resp: ChangesResponse =
525 serde_json::from_value(json).expect("ChangesResponse must deserialize");
526 assert_eq!(resp.old_state, "s10");
527 assert_eq!(resp.new_state, "s11");
528 assert!(!resp.has_more_changes);
529 }
530
531 #[test]
533 fn set_response_deserializes() {
534 let json = json!({
535 "accountId": "acc1",
536 "oldState": "s10",
537 "newState": "s11",
538 "created": null,
539 "updated": null,
540 "destroyed": ["id1"],
541 "notCreated": null,
542 "notUpdated": null,
543 "notDestroyed": null
544 });
545 let resp: SetResponse = serde_json::from_value(json).expect("SetResponse must deserialize");
546 assert_eq!(resp.new_state, "s11");
547 assert_eq!(resp.destroyed.as_deref(), Some(["id1".into()].as_slice()));
548 }
549
550 #[test]
561 fn set_response_updated_accepts_null_values() {
562 let json = json!({
563 "accountId": "acc1",
564 "oldState": "s1",
565 "newState": "s2",
566 "updated": {
567 "M1": null,
568 "M2": null
569 }
570 });
571 let resp: SetResponse<jmap_mail_types::Email> = serde_json::from_value(json)
572 .expect("SetResponse must accept Id[Foo|null] per RFC 8620 §5.3");
573 let updated = resp.updated.expect("updated must be Some");
574 assert_eq!(updated.len(), 2, "two ids in updated map");
575 assert!(
576 updated
577 .get(&Id::from("M1"))
578 .expect("M1 key present")
579 .is_none(),
580 "M1 value must be None (null)"
581 );
582 assert!(
583 updated
584 .get(&Id::from("M2"))
585 .expect("M2 key present")
586 .is_none(),
587 "M2 value must be None (null)"
588 );
589 }
590
591 #[test]
596 fn set_response_updated_accepts_object_values() {
597 let json = json!({
598 "accountId": "acc1",
599 "oldState": "s1",
600 "newState": "s2",
601 "updated": {
602 "M1": { "id": "M1", "subject": "Hello" }
603 }
604 });
605 let resp: SetResponse<serde_json::Value> = serde_json::from_value(json)
606 .expect("SetResponse must accept Id[Foo] per RFC 8620 §5.3");
607 let updated = resp.updated.expect("updated must be Some");
608 let m1 = updated
609 .get(&Id::from("M1"))
610 .expect("M1 key present")
611 .as_ref()
612 .expect("M1 value must be Some when server reports deltas");
613 assert_eq!(m1["subject"], json!("Hello"));
614 }
615
616 #[test]
618 fn query_changes_response_deserializes() {
619 let json = json!({
620 "accountId": "acc1",
621 "oldQueryState": "qs1",
622 "newQueryState": "qs2",
623 "total": 5,
624 "removed": ["id3"],
625 "added": [{"id": "id4", "index": 0}]
626 });
627 let resp: QueryChangesResponse =
628 serde_json::from_value(json).expect("QueryChangesResponse must deserialize");
629 assert_eq!(resp.old_query_state, "qs1");
630 assert_eq!(resp.new_query_state, "qs2");
631 assert_eq!(resp.total, Some(5));
632 assert_eq!(resp.removed.len(), 1);
633 assert_eq!(resp.added.len(), 1);
634 assert_eq!(resp.added[0].index, 0);
635 }
636
637 #[test]
640 fn email_get_params_default_serializes_to_empty_object() {
641 let params = EmailGetParams::default();
642 let v = serde_json::to_value(¶ms).expect("serialize EmailGetParams");
643 assert_eq!(v, serde_json::json!({}), "default must serialize to {{}}");
644 }
645
646 #[test]
649 fn email_get_params_all_fields_serializes_correctly() {
650 let params = EmailGetParams {
651 body_properties: Some(vec!["partId".into(), "type".into()]),
652 fetch_text_body_values: Some(true),
653 fetch_html_body_values: Some(false),
654 fetch_all_body_values: Some(true),
655 max_body_value_bytes: Some(1024),
656 extra: serde_json::Map::new(),
657 };
658 let v = serde_json::to_value(¶ms).expect("serialize");
659 assert_eq!(
660 v["bodyProperties"],
661 json!(["partId", "type"]),
662 "bodyProperties"
663 );
664 assert_eq!(v["fetchTextBodyValues"], json!(true));
665 assert_eq!(v["fetchHtmlBodyValues"], json!(false));
666 assert_eq!(v["fetchAllBodyValues"], json!(true));
667 assert_eq!(v["maxBodyValueBytes"], json!(1024_u64));
668 }
669
670 #[test]
673 fn email_copy_params_serializes_correctly() {
674 let params = EmailCopyParams {
675 from_account_id: "acct-src".into(),
676 on_success_destroy_original: Some(true),
677 destroy_from_if_in_state: Some("s99".into()),
678 extra: serde_json::Map::new(),
679 };
680 let v = serde_json::to_value(¶ms).expect("serialize");
681 assert_eq!(v["fromAccountId"], json!("acct-src"));
682 assert_eq!(v["onSuccessDestroyOriginal"], json!(true));
683 assert_eq!(v["destroyFromIfInState"], json!("s99"));
684 }
685
686 #[test]
688 fn email_copy_params_omits_none_fields() {
689 let params = EmailCopyParams {
690 from_account_id: "acct-src".into(),
691 on_success_destroy_original: None,
692 destroy_from_if_in_state: None,
693 extra: serde_json::Map::new(),
694 };
695 let v = serde_json::to_value(¶ms).expect("serialize");
696 assert_eq!(v["fromAccountId"], json!("acct-src"));
697 assert!(
698 v.get("onSuccessDestroyOriginal").is_none() || v["onSuccessDestroyOriginal"].is_null(),
699 "onSuccessDestroyOriginal must be absent"
700 );
701 }
702
703 #[test]
716 fn email_get_params_propagates_vendor_extras() {
717 let mut params = EmailGetParams::default();
718 params
719 .extra
720 .insert("acmeCorpInline".into(), json!("aggressive"));
721 let v = serde_json::to_value(¶ms).expect("serialize EmailGetParams");
722 assert_eq!(v["acmeCorpInline"], json!("aggressive"));
723 }
724
725 #[test]
727 fn email_copy_params_propagates_vendor_extras() {
728 let mut extra = serde_json::Map::new();
729 extra.insert("acmeCorpAudit".into(), json!(true));
730 let params = EmailCopyParams {
731 from_account_id: "acct-src".into(),
732 on_success_destroy_original: None,
733 destroy_from_if_in_state: None,
734 extra,
735 };
736 let v = serde_json::to_value(¶ms).expect("serialize EmailCopyParams");
737 assert_eq!(v["acmeCorpAudit"], json!(true));
738 }
739
740 #[test]
742 fn mailbox_set_params_propagates_vendor_extras() {
743 let mut params = MailboxSetParams::default();
744 params
745 .extra
746 .insert("acmeCorpCascade".into(), json!("strict"));
747 let v = serde_json::to_value(¶ms).expect("serialize MailboxSetParams");
748 assert_eq!(v["acmeCorpCascade"], json!("strict"));
749 }
750
751 #[test]
753 fn email_submission_set_params_propagates_vendor_extras() {
754 let mut params = EmailSubmissionSetParams::default();
755 params
756 .extra
757 .insert("acmeCorpQueue".into(), json!("priority"));
758 let v = serde_json::to_value(¶ms).expect("serialize EmailSubmissionSetParams");
759 assert_eq!(v["acmeCorpQueue"], json!("priority"));
760 }
761
762 #[test]
764 fn email_import_input_propagates_vendor_extras() {
765 let blob = Id::from("blob1");
766 let mailboxes = [Id::from("mb1")];
767 let mut extra = serde_json::Map::new();
768 extra.insert("acmeCorpSource".into(), json!("mta-relay"));
769 let input = EmailImportInput {
770 blob_id: &blob,
771 mailbox_ids: &mailboxes,
772 keywords: None,
773 received_at: None,
774 extra,
775 };
776 let v = serde_json::to_value(&input).expect("serialize EmailImportInput");
777 assert_eq!(v["acmeCorpSource"], json!("mta-relay"));
778 }
779
780 #[test]
782 fn email_parse_params_propagates_vendor_extras() {
783 let mut params = EmailParseParams::default();
784 params.extra.insert("acmeCorpStrict".into(), json!(true));
785 let v = serde_json::to_value(¶ms).expect("serialize EmailParseParams");
786 assert_eq!(v["acmeCorpStrict"], json!(true));
787 }
788
789 #[test]
791 fn email_import_created_preserves_vendor_extras() {
792 let raw = json!({
793 "id": "M1",
794 "blobId": "B1",
795 "threadId": "T1",
796 "size": 1024,
797 "acmeCorpAntivirus": "clean"
798 });
799 let created: EmailImportCreated =
800 serde_json::from_value(raw).expect("EmailImportCreated must deserialize");
801 assert_eq!(
802 created
803 .extra
804 .get("acmeCorpAntivirus")
805 .and_then(|v| v.as_str()),
806 Some("clean")
807 );
808 }
809
810 #[test]
812 fn email_import_response_preserves_vendor_extras() {
813 let raw = json!({
814 "accountId": "acc1",
815 "newState": "s2",
816 "acmeCorpJobId": "job-42"
817 });
818 let resp: EmailImportResponse =
819 serde_json::from_value(raw).expect("EmailImportResponse must deserialize");
820 assert_eq!(
821 resp.extra.get("acmeCorpJobId").and_then(|v| v.as_str()),
822 Some("job-42")
823 );
824 }
825
826 #[test]
828 fn email_parse_response_preserves_vendor_extras() {
829 let raw = json!({
830 "accountId": "acc1",
831 "acmeCorpParser": "v3"
832 });
833 let resp: EmailParseResponse =
834 serde_json::from_value(raw).expect("EmailParseResponse must deserialize");
835 assert_eq!(
836 resp.extra.get("acmeCorpParser").and_then(|v| v.as_str()),
837 Some("v3")
838 );
839 }
840}