nylas_types/
thread.rs

1//! Thread types for Nylas API v3
2//!
3//! Threads represent email conversations grouped by subject and participants.
4
5use serde::{Deserialize, Serialize};
6
7use crate::common::{EmailAddress, GrantId, ThreadId};
8
9/// An email thread (conversation).
10///
11/// Threads group related messages together based on subject and participants.
12///
13/// # Example
14///
15/// ```
16/// # use nylas_types::{Thread, ThreadId, GrantId};
17/// let thread = Thread {
18///     id: ThreadId::new("thread_123"),
19///     grant_id: GrantId::new("grant_456"),
20///     subject: Some("Project Discussion".to_string()),
21///     participants: vec![],
22///     message_ids: vec![],
23///     draft_ids: vec![],
24///     folders: vec![],
25///     snippet: Some("Let's discuss the project timeline...".to_string()),
26///     starred: Some(false),
27///     unread: Some(false),
28///     latest_draft_or_message: None,
29///     has_attachments: Some(false),
30/// };
31/// ```
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33pub struct Thread {
34    /// Unique identifier for the thread.
35    pub id: ThreadId,
36
37    /// Grant ID this thread belongs to.
38    pub grant_id: GrantId,
39
40    /// Subject of the thread.
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub subject: Option<String>,
43
44    /// Participants in the thread.
45    #[serde(default)]
46    pub participants: Vec<EmailAddress>,
47
48    /// IDs of messages in this thread.
49    #[serde(default)]
50    pub message_ids: Vec<String>,
51
52    /// IDs of drafts in this thread.
53    #[serde(default)]
54    pub draft_ids: Vec<String>,
55
56    /// Folder IDs this thread belongs to.
57    #[serde(default)]
58    pub folders: Vec<String>,
59
60    /// Text snippet from the thread.
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub snippet: Option<String>,
63
64    /// Whether the thread is starred.
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub starred: Option<bool>,
67
68    /// Whether the thread has unread messages.
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub unread: Option<bool>,
71
72    /// Unix timestamp of the latest message or draft.
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub latest_draft_or_message: Option<i64>,
75
76    /// Whether the thread has attachments.
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub has_attachments: Option<bool>,
79}
80
81/// Request to update a thread.
82///
83/// Only certain fields can be updated (starred, unread, folders).
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
85pub struct UpdateThreadRequest {
86    /// Update starred status.
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub starred: Option<bool>,
89
90    /// Update unread status.
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub unread: Option<bool>,
93
94    /// Update folder IDs.
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub folders: Option<Vec<String>>,
97}
98
99impl UpdateThreadRequest {
100    /// Create a builder for UpdateThreadRequest.
101    pub fn builder() -> UpdateThreadRequestBuilder {
102        UpdateThreadRequestBuilder::default()
103    }
104}
105
106/// Builder for UpdateThreadRequest.
107#[derive(Debug, Clone, Default)]
108pub struct UpdateThreadRequestBuilder {
109    starred: Option<bool>,
110    unread: Option<bool>,
111    folders: Option<Vec<String>>,
112}
113
114impl UpdateThreadRequestBuilder {
115    /// Set starred status.
116    pub fn starred(mut self, starred: bool) -> Self {
117        self.starred = Some(starred);
118        self
119    }
120
121    /// Set unread status.
122    pub fn unread(mut self, unread: bool) -> Self {
123        self.unread = Some(unread);
124        self
125    }
126
127    /// Set folder IDs.
128    pub fn folders(mut self, folders: Vec<String>) -> Self {
129        self.folders = Some(folders);
130        self
131    }
132
133    /// Build the UpdateThreadRequest.
134    pub fn build(self) -> UpdateThreadRequest {
135        UpdateThreadRequest {
136            starred: self.starred,
137            unread: self.unread,
138            folders: self.folders,
139        }
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_thread_creation() {
149        let thread = Thread {
150            id: ThreadId::new("thread_123"),
151            grant_id: GrantId::new("grant_456"),
152            subject: Some("Test Subject".to_string()),
153            participants: vec![],
154            message_ids: vec!["msg_1".to_string(), "msg_2".to_string()],
155            draft_ids: vec![],
156            folders: vec!["folder_1".to_string()],
157            snippet: Some("Test snippet".to_string()),
158            starred: Some(false),
159            unread: Some(true),
160            latest_draft_or_message: Some(1234567890),
161            has_attachments: Some(false),
162        };
163
164        assert_eq!(thread.id.as_str(), "thread_123");
165        assert_eq!(thread.subject, Some("Test Subject".to_string()));
166        assert_eq!(thread.message_ids.len(), 2);
167        assert_eq!(thread.unread, Some(true));
168    }
169
170    #[test]
171    fn test_thread_serialization() {
172        let thread = Thread {
173            id: ThreadId::new("thread_123"),
174            grant_id: GrantId::new("grant_456"),
175            subject: Some("Test".to_string()),
176            participants: vec![],
177            message_ids: vec!["msg_1".to_string()],
178            draft_ids: vec![],
179            folders: vec![],
180            snippet: None,
181            starred: Some(true),
182            unread: Some(false),
183            latest_draft_or_message: None,
184            has_attachments: Some(false),
185        };
186
187        let json = serde_json::to_string(&thread).unwrap();
188        assert!(json.contains("thread_123"));
189        assert!(json.contains("Test"));
190    }
191
192    #[test]
193    fn test_thread_deserialization() {
194        let json = r#"{
195            "id": "thread_123",
196            "grant_id": "grant_456",
197            "subject": "Test Subject",
198            "participants": [],
199            "message_ids": ["msg_1", "msg_2"],
200            "draft_ids": [],
201            "folders": ["folder_1"],
202            "starred": false,
203            "unread": true,
204            "latest_draft_or_message": 1234567890
205        }"#;
206
207        let thread: Thread = serde_json::from_str(json).unwrap();
208        assert_eq!(thread.id.as_str(), "thread_123");
209        assert_eq!(thread.subject, Some("Test Subject".to_string()));
210        assert_eq!(thread.message_ids.len(), 2);
211        assert_eq!(thread.unread, Some(true));
212    }
213
214    #[test]
215    fn test_update_thread_request_builder() {
216        let update = UpdateThreadRequest::builder()
217            .starred(true)
218            .unread(false)
219            .folders(vec!["folder_1".to_string()])
220            .build();
221
222        assert_eq!(update.starred, Some(true));
223        assert_eq!(update.unread, Some(false));
224        assert_eq!(update.folders, Some(vec!["folder_1".to_string()]));
225    }
226
227    #[test]
228    fn test_update_thread_request_partial() {
229        let update = UpdateThreadRequest::builder().starred(true).build();
230
231        assert_eq!(update.starred, Some(true));
232        assert_eq!(update.unread, None);
233        assert_eq!(update.folders, None);
234    }
235
236    #[test]
237    fn test_update_thread_request_serialization() {
238        let update = UpdateThreadRequest::builder()
239            .starred(true)
240            .unread(false)
241            .build();
242
243        let json = serde_json::to_string(&update).unwrap();
244        assert!(json.contains("starred"));
245        assert!(json.contains("unread"));
246
247        let deserialized: UpdateThreadRequest = serde_json::from_str(&json).unwrap();
248        assert_eq!(deserialized, update);
249    }
250}