jmap_mail_types/thread.rs
1//! RFC 8621 §3 Thread object.
2//!
3//! Provides [`Thread`] — groups related [`crate::Email`] objects by
4//! conversation thread.
5
6use jmap_types::Id;
7use serde::{Deserialize, Serialize};
8
9/// A Thread object as defined in RFC 8621 §3.
10///
11/// Groups related Email objects by conversation thread. The `emailIds` field
12/// lists member Email ids sorted oldest-first by `receivedAt`.
13#[non_exhaustive]
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct Thread {
17 /// The id of the Thread (immutable; server-set).
18 pub id: Id,
19 /// The ids of the Emails in the Thread, sorted oldest-first by `receivedAt` (server-set).
20 pub email_ids: Vec<Id>,
21 /// Catch-all for vendor / site / private extension fields not covered
22 /// by the typed fields above. Preserves unknown fields across
23 /// deserialize/serialize round-trip per workspace extras-preservation
24 /// policy (see workspace AGENTS.md).
25 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
26 pub extra: serde_json::Map<String, serde_json::Value>,
27}
28
29impl Thread {
30 /// Construct a [`Thread`] from its two required fields.
31 pub fn new(id: Id, email_ids: Vec<Id>) -> Self {
32 Self {
33 id,
34 email_ids,
35 extra: serde_json::Map::new(),
36 }
37 }
38}
39
40#[cfg(test)]
41mod tests {
42 use super::*;
43 use serde_json::json;
44
45 // ── Extras-preservation policy tests (JMAP-lbdy.2) ───────────────────
46
47 /// `Thread.extra` captures vendor fields and preserves them across
48 /// deserialize/serialize round-trip.
49 #[test]
50 fn thread_preserves_vendor_extras() {
51 let raw = json!({
52 "id": "t1",
53 "emailIds": ["e1", "e2"],
54 "acmeCorpConversationTag": "support"
55 });
56 let thr: Thread = serde_json::from_value(raw).unwrap();
57 assert_eq!(
58 thr.extra
59 .get("acmeCorpConversationTag")
60 .and_then(|v| v.as_str()),
61 Some("support")
62 );
63 let back = serde_json::to_value(&thr).unwrap();
64 assert_eq!(back["acmeCorpConversationTag"], "support");
65 }
66}