Skip to main content

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}