1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
11pub struct TrackingOptions {
12 #[serde(skip_serializing_if = "Option::is_none")]
14 pub opens: Option<bool>,
15
16 #[serde(skip_serializing_if = "Option::is_none")]
18 pub links: Option<bool>,
19
20 #[serde(skip_serializing_if = "Option::is_none")]
22 pub thread_replies: Option<bool>,
23
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub label: Option<String>,
27}
28
29impl TrackingOptions {
30 pub fn builder() -> TrackingOptionsBuilder {
32 TrackingOptionsBuilder::default()
33 }
34
35 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 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#[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 pub fn opens(mut self, enabled: bool) -> Self {
68 self.opens = Some(enabled);
69 self
70 }
71
72 pub fn links(mut self, enabled: bool) -> Self {
74 self.links = Some(enabled);
75 self
76 }
77
78 pub fn thread_replies(mut self, enabled: bool) -> Self {
80 self.thread_replies = Some(enabled);
81 self
82 }
83
84 pub fn label(mut self, label: impl Into<String>) -> Self {
86 self.label = Some(label.into());
87 self
88 }
89
90 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103pub struct MessageOpened {
104 pub message_id: String,
106
107 pub timestamp: i64,
109
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub ip_address: Option<String>,
113
114 #[serde(skip_serializing_if = "Option::is_none")]
116 pub user_agent: Option<String>,
117
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub count: Option<u32>,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125pub struct LinkClicked {
126 pub message_id: String,
128
129 pub link: String,
131
132 pub timestamp: i64,
134
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub ip_address: Option<String>,
138
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub user_agent: Option<String>,
142
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub count: Option<u32>,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150pub struct ThreadReplied {
151 pub message_id: String,
153
154 pub thread_id: String,
156
157 pub reply_message_id: String,
159
160 pub timestamp: i64,
162
163 #[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}