1use std::sync::atomic::{AtomicU64, Ordering};
8
9use dashmap::DashMap;
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug)]
18pub struct EmailStore {
19 emails: DashMap<String, SentEmail>,
21 total_sent: AtomicU64,
23}
24
25impl Default for EmailStore {
26 fn default() -> Self {
27 Self::new()
28 }
29}
30
31impl EmailStore {
32 #[must_use]
34 pub fn new() -> Self {
35 Self {
36 emails: DashMap::new(),
37 total_sent: AtomicU64::new(0),
38 }
39 }
40
41 pub fn capture(&self, email: SentEmail) -> String {
43 let id = email.id.clone();
44 self.emails.insert(id.clone(), email);
45 self.total_sent.fetch_add(1, Ordering::Relaxed);
46 id
47 }
48
49 #[must_use]
51 pub fn query(&self, filter_id: Option<&str>, filter_source: Option<&str>) -> Vec<SentEmail> {
52 self.emails
53 .iter()
54 .filter(|entry| {
55 let email = entry.value();
56 let id_match = filter_id.is_none_or(|id| id.is_empty() || email.id == id);
57 let source_match =
58 filter_source.is_none_or(|src| src.is_empty() || email.source == src);
59 id_match && source_match
60 })
61 .map(|entry| entry.value().clone())
62 .collect()
63 }
64
65 pub fn remove(&self, id: &str) {
67 self.emails.remove(id);
68 }
69
70 pub fn clear(&self) {
72 self.emails.clear();
73 }
74
75 #[must_use]
77 pub fn total_sent(&self) -> u64 {
78 self.total_sent.load(Ordering::Relaxed)
79 }
80
81 #[must_use]
83 pub fn count(&self) -> usize {
84 self.emails.len()
85 }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90#[serde(rename_all = "camelCase")]
91pub struct SentEmail {
92 pub id: String,
94 pub region: String,
96 pub timestamp: String,
98 pub source: String,
100 pub destination: SentEmailDestination,
102 pub subject: Option<String>,
104 pub body: Option<SentEmailBody>,
106 pub raw_data: Option<String>,
108 pub template: Option<String>,
110 pub template_data: Option<String>,
112 pub tags: Vec<SentEmailTag>,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct SentEmailDestination {
120 pub to_addresses: Vec<String>,
122 pub cc_addresses: Vec<String>,
124 pub bcc_addresses: Vec<String>,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130#[serde(rename_all = "camelCase")]
131pub struct SentEmailBody {
132 pub text_part: Option<String>,
134 pub html_part: Option<String>,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
140#[serde(rename_all = "camelCase")]
141pub struct SentEmailTag {
142 pub name: String,
144 pub value: String,
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 fn make_test_email(id: &str, source: &str) -> SentEmail {
153 SentEmail {
154 id: id.to_owned(),
155 region: "us-east-1".to_owned(),
156 timestamp: "2026-03-19T10:00:00Z".to_owned(),
157 source: source.to_owned(),
158 destination: SentEmailDestination {
159 to_addresses: vec!["recipient@example.com".to_owned()],
160 cc_addresses: Vec::new(),
161 bcc_addresses: Vec::new(),
162 },
163 subject: Some("Test Subject".to_owned()),
164 body: Some(SentEmailBody {
165 text_part: Some("Hello".to_owned()),
166 html_part: None,
167 }),
168 raw_data: None,
169 template: None,
170 template_data: None,
171 tags: Vec::new(),
172 }
173 }
174
175 #[test]
176 fn test_should_capture_and_query_email() {
177 let store = EmailStore::new();
178 let email = make_test_email("msg-1", "sender@example.com");
179 let id = store.capture(email);
180 assert_eq!(id, "msg-1");
181 assert_eq!(store.count(), 1);
182 assert_eq!(store.total_sent(), 1);
183
184 let results = store.query(None, None);
185 assert_eq!(results.len(), 1);
186 assert_eq!(results[0].source, "sender@example.com");
187 }
188
189 #[test]
190 fn test_should_query_by_id() {
191 let store = EmailStore::new();
192 store.capture(make_test_email("msg-1", "a@b.com"));
193 store.capture(make_test_email("msg-2", "c@d.com"));
194
195 let results = store.query(Some("msg-1"), None);
196 assert_eq!(results.len(), 1);
197 assert_eq!(results[0].id, "msg-1");
198 }
199
200 #[test]
201 fn test_should_query_by_source() {
202 let store = EmailStore::new();
203 store.capture(make_test_email("msg-1", "a@b.com"));
204 store.capture(make_test_email("msg-2", "c@d.com"));
205
206 let results = store.query(None, Some("a@b.com"));
207 assert_eq!(results.len(), 1);
208 assert_eq!(results[0].source, "a@b.com");
209 }
210
211 #[test]
212 fn test_should_query_with_both_filters() {
213 let store = EmailStore::new();
214 store.capture(make_test_email("msg-1", "a@b.com"));
215 store.capture(make_test_email("msg-2", "a@b.com"));
216
217 let results = store.query(Some("msg-1"), Some("a@b.com"));
218 assert_eq!(results.len(), 1);
219 assert_eq!(results[0].id, "msg-1");
220 }
221
222 #[test]
223 fn test_should_remove_single_email() {
224 let store = EmailStore::new();
225 store.capture(make_test_email("msg-1", "a@b.com"));
226 store.capture(make_test_email("msg-2", "c@d.com"));
227 store.remove("msg-1");
228 assert_eq!(store.count(), 1);
229 assert!(store.query(Some("msg-1"), None).is_empty());
230 }
231
232 #[test]
233 fn test_should_clear_all_emails() {
234 let store = EmailStore::new();
235 store.capture(make_test_email("msg-1", "a@b.com"));
236 store.capture(make_test_email("msg-2", "c@d.com"));
237 store.clear();
238 assert_eq!(store.count(), 0);
239 assert_eq!(store.total_sent(), 2);
241 }
242
243 #[test]
244 fn test_should_serialize_to_json() {
245 let email = make_test_email("msg-1", "sender@example.com");
246 let json = serde_json::to_string(&email);
247 assert!(json.is_ok());
248 let json = json.unwrap_or_default();
249 assert!(json.contains("\"id\":\"msg-1\""));
250 assert!(json.contains("\"source\":\"sender@example.com\""));
251 assert!(json.contains("\"toAddresses\""));
252 }
253}