1use std::{
8 error::Error,
9 path::{Path, PathBuf},
10};
11
12use rusqlite::{params, Connection};
13use tokio::task::spawn_blocking;
14
15#[derive(Clone, Debug)]
16pub struct EmailMetadata {
17 pub message_id: String,
18 pub from: String,
19 pub rcpt: String,
20 pub subject: String,
21 pub path: PathBuf,
22}
23
24impl EmailMetadata {
25 pub fn new(
26 message_id: String,
27 from: String,
28 rcpt: String,
29 subject: String,
30 path: PathBuf,
31 ) -> Self {
32 Self {
33 message_id,
34 from,
35 rcpt,
36 subject,
37 path,
38 }
39 }
40
41 pub async fn store_sqlite(
42 &self,
43 db: impl AsRef<Path>,
44 ) -> Result<(), Box<dyn Error + Send + Sync>> {
45 let path = db.as_ref().to_path_buf();
46 let metadata = self.clone();
47
48 spawn_blocking(move || -> Result<(), Box<dyn Error + Send + Sync>> {
51 let conn = Connection::open(path)?;
52 conn.execute(
53 "CREATE TABLE IF NOT EXISTS metadata (message_id TEXT PRIMARY KEY, _from TEXT, rcpt TEXT, subject TEXT, path TEXT)",
54 [],
55 )?;
56 conn.execute(
57 "INSERT INTO metadata (message_id, _from, rcpt, subject, path) VALUES (?1, ?2, ?3, ?4, ?5)",
58 params![metadata.message_id, metadata.from, metadata.rcpt, metadata.subject, metadata.path.to_string_lossy()],
59 )?;
60 Ok(())
61 })
62 .await??;
63 Ok(())
64 }
65
66 pub async fn retrieve_sqlite(
67 db: impl AsRef<Path>,
68 message_id: String,
69 ) -> Result<EmailMetadata, Box<dyn Error + Send + Sync>> {
70 let path = db.as_ref().to_path_buf();
71
72 spawn_blocking(
75 move || -> Result<EmailMetadata, Box<dyn Error + Send + Sync>> {
76 let conn = Connection::open(path)?;
77 let mut stmt = conn.prepare(
78 "SELECT message_id, _from, rcpt, subject, path FROM metadata WHERE message_id = ?",
79 )?;
80 let row = stmt.query_row(params![message_id], |row| {
81 Ok(EmailMetadata {
82 message_id: row.get(0)?,
83 from: row.get(1)?,
84 rcpt: row.get(2)?,
85 subject: row.get(3)?,
86 path: PathBuf::from(row.get::<_, String>(4)?),
87 })
88 })?;
89 Ok(row)
90 },
91 )
92 .await?
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use tempfile::NamedTempFile;
99
100 use super::*;
101
102 #[test]
103 fn test_email_metadata_new() {
104 let message_id = "test-message-id".to_string();
105 let from = "sender@example.com".to_string();
106 let rcpt = "recipient@example.com".to_string();
107 let subject = "Test Subject".to_string();
108 let path = PathBuf::from("/path/to/email.eml");
109
110 let metadata = EmailMetadata::new(
111 message_id.clone(),
112 from.clone(),
113 rcpt.clone(),
114 subject.clone(),
115 path.clone(),
116 );
117
118 assert_eq!(metadata.message_id, message_id);
119 assert_eq!(metadata.from, from);
120 assert_eq!(metadata.rcpt, rcpt);
121 assert_eq!(metadata.subject, subject);
122 assert_eq!(metadata.path, path);
123 }
124
125 #[test]
126 fn test_email_metadata_clone() {
127 let original = EmailMetadata::new(
128 "test-id".to_string(),
129 "sender@example.com".to_string(),
130 "recipient@example.com".to_string(),
131 "Test Subject".to_string(),
132 PathBuf::from("/path/to/email.eml"),
133 );
134
135 let cloned = original.clone();
136
137 assert_eq!(original.message_id, cloned.message_id);
138 assert_eq!(original.from, cloned.from);
139 assert_eq!(original.rcpt, cloned.rcpt);
140 assert_eq!(original.subject, cloned.subject);
141 assert_eq!(original.path, cloned.path);
142 }
143
144 #[tokio::test]
145 async fn test_store_sqlite_success() {
146 let temp_file = NamedTempFile::new().unwrap();
147 let db_path = temp_file.path();
148
149 let metadata = EmailMetadata::new(
150 "test-message-id".to_string(),
151 "sender@example.com".to_string(),
152 "recipient@example.com".to_string(),
153 "Test Subject".to_string(),
154 PathBuf::from("/path/to/email.eml"),
155 );
156
157 let result = metadata.store_sqlite(db_path).await;
158 assert!(result.is_ok(), "Failed to store metadata: {result:?}");
159
160 let conn = Connection::open(db_path).unwrap();
161 let mut stmt = conn
162 .prepare(
163 "SELECT message_id, _from, rcpt, subject, path FROM metadata WHERE message_id = ?",
164 )
165 .unwrap();
166
167 let row = stmt
168 .query_row(params!["test-message-id"], |row| {
169 Ok((
170 row.get::<_, String>(0)?,
171 row.get::<_, String>(1)?,
172 row.get::<_, String>(2)?,
173 row.get::<_, String>(3)?,
174 row.get::<_, String>(4)?,
175 ))
176 })
177 .unwrap();
178
179 assert_eq!(row.0, "test-message-id");
180 assert_eq!(row.1, "sender@example.com");
181 assert_eq!(row.2, "recipient@example.com");
182 assert_eq!(row.3, "Test Subject");
183 assert_eq!(row.4, "/path/to/email.eml");
184 }
185
186 #[tokio::test]
187 async fn test_store_sqlite_duplicate_id() {
188 let temp_file = NamedTempFile::new().unwrap();
189 let db_path = temp_file.path();
190
191 let metadata1 = EmailMetadata::new(
192 "duplicate-id".to_string(),
193 "sender1@example.com".to_string(),
194 "recipient1@example.com".to_string(),
195 "First Subject".to_string(),
196 PathBuf::from("/path/to/email1.eml"),
197 );
198
199 let metadata2 = EmailMetadata::new(
200 "duplicate-id".to_string(),
201 "sender2@example.com".to_string(),
202 "recipient2@example.com".to_string(),
203 "Second Subject".to_string(),
204 PathBuf::from("/path/to/email2.eml"),
205 );
206
207 let result1 = metadata1.store_sqlite(db_path).await;
208 assert!(result1.is_ok(), "First insert should succeed: {result1:?}");
209
210 let result2 = metadata2.store_sqlite(db_path).await;
211 assert!(
212 result2.is_err(),
213 "Second insert should fail due to duplicate ID"
214 );
215 }
216
217 #[tokio::test]
218 async fn test_store_sqlite_multiple_entries() {
219 let temp_file = NamedTempFile::new().unwrap();
220 let db_path = temp_file.path();
221
222 let metadata1 = EmailMetadata::new(
223 "id-1".to_string(),
224 "sender1@example.com".to_string(),
225 "recipient1@example.com".to_string(),
226 "Subject 1".to_string(),
227 PathBuf::from("/path/to/email1.eml"),
228 );
229
230 let metadata2 = EmailMetadata::new(
231 "id-2".to_string(),
232 "sender2@example.com".to_string(),
233 "recipient2@example.com".to_string(),
234 "Subject 2".to_string(),
235 PathBuf::from("/path/to/email2.eml"),
236 );
237
238 let result1 = metadata1.store_sqlite(db_path).await;
239 assert!(result1.is_ok(), "First insert failed: {result1:?}");
240
241 let result2 = metadata2.store_sqlite(db_path).await;
242 assert!(result2.is_ok(), "Second insert failed: {result2:?}");
243
244 let conn = Connection::open(db_path).unwrap();
245 let count: i64 = conn
246 .query_row("SELECT COUNT(*) FROM metadata", [], |row| row.get(0))
247 .unwrap();
248
249 assert_eq!(count, 2, "Should have exactly 2 entries in the database");
250 }
251
252 #[tokio::test]
253 async fn test_store_sqlite_with_special_characters() {
254 let temp_file = NamedTempFile::new().unwrap();
255 let db_path = temp_file.path();
256
257 let metadata = EmailMetadata::new(
258 "test-id-with-special-chars".to_string(),
259 "sender+tag@example.com".to_string(),
260 "recipient.name@domain.co.uk".to_string(),
261 "Subject with \"quotes\" and 'apostrophes'".to_string(),
262 PathBuf::from("/path/with spaces/email.eml"),
263 );
264
265 let result = metadata.store_sqlite(db_path).await;
266 assert!(
267 result.is_ok(),
268 "Failed to store metadata with special characters: {result:?}"
269 );
270
271 let conn = Connection::open(db_path).unwrap();
272 let mut stmt = conn
273 .prepare(
274 "SELECT message_id, _from, rcpt, subject, path FROM metadata WHERE message_id = ?",
275 )
276 .unwrap();
277 let row = stmt
278 .query_row(params!["test-id-with-special-chars"], |row| {
279 Ok((
280 row.get::<_, String>(0)?,
281 row.get::<_, String>(1)?,
282 row.get::<_, String>(2)?,
283 row.get::<_, String>(3)?,
284 row.get::<_, String>(4)?,
285 ))
286 })
287 .unwrap();
288
289 assert_eq!(row.0, "test-id-with-special-chars");
290 assert_eq!(row.1, "sender+tag@example.com");
291 assert_eq!(row.2, "recipient.name@domain.co.uk");
292 assert_eq!(row.3, "Subject with \"quotes\" and 'apostrophes'");
293 assert_eq!(row.4, "/path/with spaces/email.eml");
294 }
295
296 #[tokio::test]
297 async fn test_store_sqlite_invalid_path() {
298 let metadata = EmailMetadata::new(
299 "test-id".to_string(),
300 "sender@example.com".to_string(),
301 "recipient@example.com".to_string(),
302 "Test Subject".to_string(),
303 PathBuf::from("/path/to/email.eml"),
304 );
305 let invalid_path = PathBuf::from("/nonexistent/directory/database.db");
306 let result = metadata.store_sqlite(&invalid_path).await;
307
308 assert!(
309 result.is_err(),
310 "Should fail when trying to create database in nonexistent directory"
311 );
312 }
313
314 #[tokio::test]
315 async fn test_retrieve_sqlite_success() {
316 let temp_file = NamedTempFile::new().unwrap();
317 let db_path = temp_file.path();
318
319 let original_metadata = EmailMetadata::new(
320 "retrieve-test-id".to_string(),
321 "sender@example.com".to_string(),
322 "recipient@example.com".to_string(),
323 "Test Subject for Retrieval".to_string(),
324 PathBuf::from("/path/to/email.eml"),
325 );
326
327 let store_result = original_metadata.store_sqlite(db_path).await;
329 assert!(
330 store_result.is_ok(),
331 "Failed to store metadata: {store_result:?}"
332 );
333
334 let retrieved_metadata =
336 EmailMetadata::retrieve_sqlite(db_path, "retrieve-test-id".to_string()).await;
337 assert!(
338 retrieved_metadata.is_ok(),
339 "Failed to retrieve metadata: {retrieved_metadata:?}"
340 );
341
342 let retrieved = retrieved_metadata.unwrap();
343 assert_eq!(retrieved.message_id, original_metadata.message_id);
344 assert_eq!(retrieved.from, original_metadata.from);
345 assert_eq!(retrieved.rcpt, original_metadata.rcpt);
346 assert_eq!(retrieved.subject, original_metadata.subject);
347 assert_eq!(retrieved.path, original_metadata.path);
348 }
349
350 #[tokio::test]
351 async fn test_retrieve_sqlite_not_found() {
352 let temp_file = NamedTempFile::new().unwrap();
353 let db_path = temp_file.path();
354
355 let result = EmailMetadata::retrieve_sqlite(db_path, "non-existent-id".to_string()).await;
357 assert!(
358 result.is_err(),
359 "Should fail when trying to retrieve non-existent metadata"
360 );
361 }
362}