Skip to main content

nylas_types/
tracking.rs

1//! Message tracking types.
2//!
3//! Support for tracking message opens, link clicks, and thread activity.
4
5use serde::{Deserialize, Serialize};
6
7/// Message tracking settings.
8///
9/// Controls whether to track opens and link clicks for sent messages.
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
11pub struct TrackingOptions {
12    /// Enable open tracking for this message.
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub opens: Option<bool>,
15
16    /// Enable link click tracking for this message.
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub links: Option<bool>,
19
20    /// Enable thread reply tracking for this message.
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub thread_replies: Option<bool>,
23
24    /// Custom label for tracking this message.
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub label: Option<String>,
27}
28
29impl TrackingOptions {
30    /// Create a new builder for tracking options.
31    pub fn builder() -> TrackingOptionsBuilder {
32        TrackingOptionsBuilder::default()
33    }
34
35    /// Enable all tracking options.
36    pub fn all_enabled() -> Self {
37        Self {
38            opens: Some(true),
39            links: Some(true),
40            thread_replies: Some(true),
41            label: None,
42        }
43    }
44
45    /// Disable all tracking options.
46    pub fn all_disabled() -> Self {
47        Self {
48            opens: Some(false),
49            links: Some(false),
50            thread_replies: Some(false),
51            label: None,
52        }
53    }
54}
55
56/// Builder for TrackingOptions.
57#[derive(Debug, Default)]
58pub struct TrackingOptionsBuilder {
59    opens: Option<bool>,
60    links: Option<bool>,
61    thread_replies: Option<bool>,
62    label: Option<String>,
63}
64
65impl TrackingOptionsBuilder {
66    /// Enable or disable open tracking.
67    pub fn opens(mut self, enabled: bool) -> Self {
68        self.opens = Some(enabled);
69        self
70    }
71
72    /// Enable or disable link click tracking.
73    pub fn links(mut self, enabled: bool) -> Self {
74        self.links = Some(enabled);
75        self
76    }
77
78    /// Enable or disable thread reply tracking.
79    pub fn thread_replies(mut self, enabled: bool) -> Self {
80        self.thread_replies = Some(enabled);
81        self
82    }
83
84    /// Set a custom label for this tracking.
85    pub fn label(mut self, label: impl Into<String>) -> Self {
86        self.label = Some(label.into());
87        self
88    }
89
90    /// Build the TrackingOptions.
91    pub fn build(self) -> TrackingOptions {
92        TrackingOptions {
93            opens: self.opens,
94            links: self.links,
95            thread_replies: self.thread_replies,
96            label: self.label,
97        }
98    }
99}
100
101/// Open tracking event data.
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103pub struct MessageOpened {
104    /// The message ID that was opened.
105    pub message_id: String,
106
107    /// Timestamp when the message was opened (Unix timestamp).
108    pub timestamp: i64,
109
110    /// IP address of the recipient who opened the message.
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub ip_address: Option<String>,
113
114    /// User agent of the recipient's email client.
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub user_agent: Option<String>,
117
118    /// Count of how many times this message has been opened.
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub count: Option<u32>,
121}
122
123/// Link click tracking event data.
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125pub struct LinkClicked {
126    /// The message ID containing the clicked link.
127    pub message_id: String,
128
129    /// The URL that was clicked.
130    pub link: String,
131
132    /// Timestamp when the link was clicked (Unix timestamp).
133    pub timestamp: i64,
134
135    /// IP address of the recipient who clicked the link.
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub ip_address: Option<String>,
138
139    /// User agent of the recipient's browser.
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub user_agent: Option<String>,
142
143    /// Count of how many times this link has been clicked.
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub count: Option<u32>,
146}
147
148/// Thread reply tracking event data.
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150pub struct ThreadReplied {
151    /// The original message ID.
152    pub message_id: String,
153
154    /// The thread ID.
155    pub thread_id: String,
156
157    /// The reply message ID.
158    pub reply_message_id: String,
159
160    /// Timestamp when the reply was received (Unix timestamp).
161    pub timestamp: i64,
162
163    /// Email address of the person who replied.
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub from: Option<String>,
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_tracking_options_builder() {
174        let options = TrackingOptions::builder()
175            .opens(true)
176            .links(true)
177            .thread_replies(false)
178            .label("campaign-2024")
179            .build();
180
181        assert_eq!(options.opens, Some(true));
182        assert_eq!(options.links, Some(true));
183        assert_eq!(options.thread_replies, Some(false));
184        assert_eq!(options.label, Some("campaign-2024".to_string()));
185    }
186
187    #[test]
188    fn test_tracking_options_all_enabled() {
189        let options = TrackingOptions::all_enabled();
190
191        assert_eq!(options.opens, Some(true));
192        assert_eq!(options.links, Some(true));
193        assert_eq!(options.thread_replies, Some(true));
194        assert_eq!(options.label, None);
195    }
196
197    #[test]
198    fn test_tracking_options_all_disabled() {
199        let options = TrackingOptions::all_disabled();
200
201        assert_eq!(options.opens, Some(false));
202        assert_eq!(options.links, Some(false));
203        assert_eq!(options.thread_replies, Some(false));
204    }
205
206    #[test]
207    fn test_tracking_options_serialization() {
208        let options = TrackingOptions::builder().opens(true).links(false).build();
209
210        let json = serde_json::to_string(&options).unwrap();
211        let deserialized: TrackingOptions = serde_json::from_str(&json).unwrap();
212
213        assert_eq!(options, deserialized);
214    }
215
216    #[test]
217    fn test_message_opened_serialization() {
218        let opened = MessageOpened {
219            message_id: "msg_123".to_string(),
220            timestamp: 1234567890,
221            ip_address: Some("192.168.1.1".to_string()),
222            user_agent: Some("Mozilla/5.0".to_string()),
223            count: Some(1),
224        };
225
226        let json = serde_json::to_string(&opened).unwrap();
227        let deserialized: MessageOpened = serde_json::from_str(&json).unwrap();
228
229        assert_eq!(opened, deserialized);
230    }
231
232    #[test]
233    fn test_link_clicked_serialization() {
234        let clicked = LinkClicked {
235            message_id: "msg_123".to_string(),
236            link: "https://example.com".to_string(),
237            timestamp: 1234567890,
238            ip_address: Some("192.168.1.1".to_string()),
239            user_agent: Some("Mozilla/5.0".to_string()),
240            count: Some(2),
241        };
242
243        let json = serde_json::to_string(&clicked).unwrap();
244        let deserialized: LinkClicked = serde_json::from_str(&json).unwrap();
245
246        assert_eq!(clicked, deserialized);
247    }
248
249    #[test]
250    fn test_thread_replied_serialization() {
251        let replied = ThreadReplied {
252            message_id: "msg_123".to_string(),
253            thread_id: "thread_456".to_string(),
254            reply_message_id: "msg_789".to_string(),
255            timestamp: 1234567890,
256            from: Some("user@example.com".to_string()),
257        };
258
259        let json = serde_json::to_string(&replied).unwrap();
260        let deserialized: ThreadReplied = serde_json::from_str(&json).unwrap();
261
262        assert_eq!(replied, deserialized);
263    }
264}