1use std::sync::Mutex;
16
17use crate::queue::{QueueStatus, QueuedMessage};
18
19#[derive(Debug, thiserror::Error)]
22pub enum StoreError {
23 #[error("store backend: {0}")]
25 Backend(String),
26}
27
28#[cfg(feature = "pg")]
29impl From<sqlx::Error> for StoreError {
30 fn from(err: sqlx::Error) -> Self {
31 Self::Backend(err.to_string())
32 }
33}
34
35#[async_trait::async_trait]
45pub trait QueueStore: Send + Sync {
46 #[allow(clippy::too_many_arguments)]
48 async fn enqueue(
49 &self,
50 sender: &str,
51 recipient: &str,
52 domain: &str,
53 message_data: &[u8],
54 message_id: Option<&str>,
55 now: i64,
56 is_forwarded: bool,
57 ) -> Result<i64, StoreError>;
58
59 #[allow(clippy::too_many_arguments)]
61 async fn enqueue_scheduled(
62 &self,
63 sender: &str,
64 recipient: &str,
65 domain: &str,
66 message_data: &[u8],
67 message_id: Option<&str>,
68 created_at: i64,
69 scheduled_at: i64,
70 ) -> Result<i64, StoreError>;
71
72 async fn dequeue(&self, now: i64, limit: u32) -> Result<Vec<QueuedMessage>, StoreError>;
74
75 async fn recover_stale_inflight(&self, now: i64) -> Result<u64, StoreError>;
78
79 async fn mark_inflight(&self, id: i64, now: i64) -> Result<(), StoreError>;
81
82 async fn mark_delivered(&self, id: i64, now: i64) -> Result<(), StoreError>;
84
85 async fn mark_failed(
88 &self,
89 id: i64,
90 error: &str,
91 next_retry: i64,
92 now: i64,
93 ) -> Result<(), StoreError>;
94
95 async fn mark_bounced(&self, id: i64, error: &str, now: i64) -> Result<(), StoreError>;
97
98 async fn get_message(&self, id: i64) -> Result<Option<QueuedMessage>, StoreError>;
100
101 async fn queue_stats(&self) -> Result<Vec<(String, i64)>, StoreError>;
103
104 async fn list_recent(&self, limit: i32) -> Result<Vec<QueuedMessage>, StoreError>;
106
107 async fn cancel_pending(&self, id: i64) -> Result<bool, StoreError>;
109
110 async fn cancel_pending_by_message_id(
113 &self,
114 message_id: &str,
115 sender: &str,
116 ) -> Result<bool, StoreError>;
117
118 async fn retry_message(&self, id: i64, now: i64) -> Result<bool, StoreError>;
120
121 async fn is_suppressed(&self, email: &str) -> bool;
123
124 async fn add_suppression(
126 &self,
127 email: &str,
128 reason: &str,
129 smtp_code: Option<i32>,
130 ) -> Result<(), StoreError>;
131
132 async fn remove_suppression(&self, email: &str) -> Result<bool, StoreError>;
135
136 async fn list_suppressions(
138 &self,
139 limit: i64,
140 ) -> Result<Vec<(String, String, Option<i32>, i64)>, StoreError>;
141}
142
143#[async_trait::async_trait]
152pub trait Notifier: Send + Sync {
153 async fn notify(&self);
155
156 async fn wait(&self);
158}
159
160#[derive(Default)]
166pub struct InMemoryNotifier {
167 inner: tokio::sync::Notify,
168}
169
170impl InMemoryNotifier {
171 pub fn new() -> Self {
173 Self::default()
174 }
175}
176
177#[async_trait::async_trait]
178impl Notifier for InMemoryNotifier {
179 async fn notify(&self) {
180 self.inner.notify_one();
181 }
182 async fn wait(&self) {
183 self.inner.notified().await;
184 }
185}
186
187pub struct NoopNotifier;
191
192#[async_trait::async_trait]
193impl Notifier for NoopNotifier {
194 async fn notify(&self) {}
195 async fn wait(&self) {
196 std::future::pending::<()>().await;
197 }
198}
199
200pub struct InMemoryQueueStore {
203 state: Mutex<MemState>,
204}
205
206struct MemState {
207 next_id: i64,
208 messages: Vec<QueuedMessage>,
209 suppressions: Vec<(String, String, Option<i32>, i64)>, }
211
212impl Default for InMemoryQueueStore {
213 fn default() -> Self {
214 Self::new()
215 }
216}
217
218impl InMemoryQueueStore {
219 pub fn new() -> Self {
221 Self {
222 state: Mutex::new(MemState {
223 next_id: 1,
224 messages: Vec::new(),
225 suppressions: Vec::new(),
226 }),
227 }
228 }
229
230 #[allow(clippy::too_many_arguments)]
231 fn insert_msg(
232 s: &mut MemState,
233 sender: &str,
234 recipient: &str,
235 domain: &str,
236 message_data: &[u8],
237 message_id: Option<&str>,
238 next_retry: i64,
239 created_at: i64,
240 is_forwarded: bool,
241 ) -> i64 {
242 let id = s.next_id;
243 s.next_id += 1;
244 s.messages.push(QueuedMessage {
245 id,
246 sender: sender.into(),
247 recipient: recipient.into(),
248 domain: domain.into(),
249 message_data: message_data.to_vec(),
250 status: QueueStatus::Pending,
251 attempts: 0,
252 max_attempts: 8,
253 next_retry,
254 last_error: None,
255 message_id: message_id.map(str::to_owned),
256 created_at,
257 updated_at: created_at,
258 is_forwarded,
259 });
260 id
261 }
262}
263
264#[async_trait::async_trait]
265impl QueueStore for InMemoryQueueStore {
266 async fn enqueue(
267 &self,
268 sender: &str,
269 recipient: &str,
270 domain: &str,
271 message_data: &[u8],
272 message_id: Option<&str>,
273 now: i64,
274 is_forwarded: bool,
275 ) -> Result<i64, StoreError> {
276 let mut s = self.state.lock().unwrap();
277 Ok(Self::insert_msg(
278 &mut s,
279 sender,
280 recipient,
281 domain,
282 message_data,
283 message_id,
284 now,
285 now,
286 is_forwarded,
287 ))
288 }
289
290 async fn enqueue_scheduled(
291 &self,
292 sender: &str,
293 recipient: &str,
294 domain: &str,
295 message_data: &[u8],
296 message_id: Option<&str>,
297 created_at: i64,
298 scheduled_at: i64,
299 ) -> Result<i64, StoreError> {
300 let mut s = self.state.lock().unwrap();
301 Ok(Self::insert_msg(
302 &mut s,
303 sender,
304 recipient,
305 domain,
306 message_data,
307 message_id,
308 scheduled_at,
309 created_at,
310 false,
311 ))
312 }
313
314 async fn dequeue(&self, now: i64, limit: u32) -> Result<Vec<QueuedMessage>, StoreError> {
315 let s = self.state.lock().unwrap();
316 let mut out: Vec<QueuedMessage> = s
317 .messages
318 .iter()
319 .filter(|m| m.status == QueueStatus::Pending && m.next_retry <= now)
320 .cloned()
321 .collect();
322 out.sort_by_key(|m| m.next_retry);
323 out.truncate(limit as usize);
324 Ok(out)
325 }
326
327 async fn recover_stale_inflight(&self, now: i64) -> Result<u64, StoreError> {
328 let threshold = now - 600;
329 let mut s = self.state.lock().unwrap();
330 let mut affected = 0u64;
331 for m in s.messages.iter_mut() {
332 if m.status == QueueStatus::InFlight && m.updated_at < threshold {
333 m.status = QueueStatus::Pending;
334 m.updated_at = now;
335 affected += 1;
336 }
337 }
338 Ok(affected)
339 }
340
341 async fn mark_inflight(&self, id: i64, now: i64) -> Result<(), StoreError> {
342 let mut s = self.state.lock().unwrap();
343 if let Some(m) = s.messages.iter_mut().find(|m| m.id == id) {
344 m.status = QueueStatus::InFlight;
345 m.updated_at = now;
346 }
347 Ok(())
348 }
349
350 async fn mark_delivered(&self, id: i64, now: i64) -> Result<(), StoreError> {
351 let mut s = self.state.lock().unwrap();
352 if let Some(m) = s.messages.iter_mut().find(|m| m.id == id) {
353 m.status = QueueStatus::Delivered;
354 m.updated_at = now;
355 }
356 Ok(())
357 }
358
359 async fn mark_failed(
360 &self,
361 id: i64,
362 error: &str,
363 next_retry: i64,
364 now: i64,
365 ) -> Result<(), StoreError> {
366 let mut s = self.state.lock().unwrap();
367 if let Some(m) = s.messages.iter_mut().find(|m| m.id == id) {
368 m.status = QueueStatus::Pending;
369 m.attempts += 1;
370 m.last_error = Some(error.into());
371 m.next_retry = next_retry;
372 m.updated_at = now;
373 }
374 Ok(())
375 }
376
377 async fn mark_bounced(&self, id: i64, error: &str, now: i64) -> Result<(), StoreError> {
378 let mut s = self.state.lock().unwrap();
379 if let Some(m) = s.messages.iter_mut().find(|m| m.id == id) {
380 m.status = QueueStatus::Bounced;
381 m.last_error = Some(error.into());
382 m.updated_at = now;
383 }
384 Ok(())
385 }
386
387 async fn get_message(&self, id: i64) -> Result<Option<QueuedMessage>, StoreError> {
388 let s = self.state.lock().unwrap();
389 Ok(s.messages.iter().find(|m| m.id == id).cloned())
390 }
391
392 async fn queue_stats(&self) -> Result<Vec<(String, i64)>, StoreError> {
393 use std::collections::HashMap;
394 let s = self.state.lock().unwrap();
395 let mut counts: HashMap<&'static str, i64> = HashMap::new();
396 for m in &s.messages {
397 *counts.entry(m.status.as_str()).or_insert(0) += 1;
398 }
399 Ok(counts
400 .into_iter()
401 .map(|(k, v)| (k.to_string(), v))
402 .collect())
403 }
404
405 async fn list_recent(&self, limit: i32) -> Result<Vec<QueuedMessage>, StoreError> {
406 let s = self.state.lock().unwrap();
407 let mut out: Vec<QueuedMessage> = s.messages.clone();
408 out.sort_by_key(|m| std::cmp::Reverse(m.created_at));
409 out.truncate(limit.max(0) as usize);
410 Ok(out)
411 }
412
413 async fn cancel_pending(&self, id: i64) -> Result<bool, StoreError> {
414 let mut s = self.state.lock().unwrap();
415 let before = s.messages.len();
416 s.messages
417 .retain(|m| !(m.id == id && m.status == QueueStatus::Pending));
418 Ok(s.messages.len() < before)
419 }
420
421 async fn cancel_pending_by_message_id(
422 &self,
423 message_id: &str,
424 sender: &str,
425 ) -> Result<bool, StoreError> {
426 let mut s = self.state.lock().unwrap();
427 let before = s.messages.len();
428 s.messages.retain(|m| {
429 !(m.status == QueueStatus::Pending
430 && m.sender == sender
431 && m.message_id.as_deref() == Some(message_id))
432 });
433 Ok(s.messages.len() < before)
434 }
435
436 async fn retry_message(&self, id: i64, now: i64) -> Result<bool, StoreError> {
437 let mut s = self.state.lock().unwrap();
438 if let Some(m) = s.messages.iter_mut().find(|m| {
439 m.id == id && (m.status == QueueStatus::Bounced || m.status == QueueStatus::Failed)
440 }) {
441 m.status = QueueStatus::Pending;
442 m.next_retry = now;
443 m.updated_at = now;
444 return Ok(true);
445 }
446 Ok(false)
447 }
448
449 async fn is_suppressed(&self, email: &str) -> bool {
450 let s = self.state.lock().unwrap();
451 s.suppressions.iter().any(|(e, _, _, _)| e == email)
452 }
453
454 async fn add_suppression(
455 &self,
456 email: &str,
457 reason: &str,
458 smtp_code: Option<i32>,
459 ) -> Result<(), StoreError> {
460 let mut s = self.state.lock().unwrap();
461 if let Some(entry) = s.suppressions.iter_mut().find(|(e, _, _, _)| e == email) {
462 entry.1 = reason.into();
463 entry.2 = smtp_code;
464 } else {
465 let now = chrono::Utc::now().timestamp();
466 s.suppressions
467 .push((email.into(), reason.into(), smtp_code, now));
468 }
469 Ok(())
470 }
471
472 async fn remove_suppression(&self, email: &str) -> Result<bool, StoreError> {
473 let mut s = self.state.lock().unwrap();
474 let before = s.suppressions.len();
475 s.suppressions.retain(|(e, _, _, _)| e != email);
476 Ok(s.suppressions.len() < before)
477 }
478
479 async fn list_suppressions(
480 &self,
481 limit: i64,
482 ) -> Result<Vec<(String, String, Option<i32>, i64)>, StoreError> {
483 let s = self.state.lock().unwrap();
484 let mut out = s.suppressions.clone();
485 out.sort_by_key(|(_, _, _, t)| std::cmp::Reverse(*t));
486 out.truncate(limit.max(0) as usize);
487 Ok(out)
488 }
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494 use std::sync::Arc;
495 use std::time::Duration;
496
497 fn store() -> Arc<dyn QueueStore> {
498 Arc::new(InMemoryQueueStore::new())
499 }
500
501 #[tokio::test]
502 async fn enqueue_dequeue_roundtrip() {
503 let s = store();
504 let id = s
505 .enqueue("a@x", "b@y", "y", b"raw", None, 0, false)
506 .await
507 .unwrap();
508 let msgs = s.dequeue(0, 10).await.unwrap();
509 assert_eq!(msgs.len(), 1);
510 assert_eq!(msgs[0].id, id);
511 assert_eq!(msgs[0].sender, "a@x");
512 }
513
514 #[tokio::test]
515 async fn lifecycle_transitions() {
516 let s = store();
517 let id = s
518 .enqueue("a@x", "b@y", "y", b"", None, 0, false)
519 .await
520 .unwrap();
521 s.mark_inflight(id, 10).await.unwrap();
522 s.mark_delivered(id, 20).await.unwrap();
523 assert_eq!(
524 s.get_message(id).await.unwrap().unwrap().status,
525 QueueStatus::Delivered
526 );
527 }
528
529 #[tokio::test]
530 async fn stale_inflight_recovers() {
531 let s = store();
532 let id = s
533 .enqueue("a@x", "b@y", "y", b"", None, 0, false)
534 .await
535 .unwrap();
536 s.mark_inflight(id, 0).await.unwrap();
537 let n = s.recover_stale_inflight(601).await.unwrap();
539 assert_eq!(n, 1);
540 assert_eq!(
541 s.get_message(id).await.unwrap().unwrap().status,
542 QueueStatus::Pending
543 );
544 }
545
546 #[tokio::test]
547 async fn suppression_list() {
548 let s = store();
549 assert!(!s.is_suppressed("user@x").await);
550 s.add_suppression("user@x", "bounced", Some(550))
551 .await
552 .unwrap();
553 assert!(s.is_suppressed("user@x").await);
554 assert!(s.remove_suppression("user@x").await.unwrap());
555 assert!(!s.is_suppressed("user@x").await);
556 }
557
558 #[tokio::test]
559 async fn in_memory_notifier_wakes_waiter() {
560 let n = Arc::new(InMemoryNotifier::new());
561 let n2 = n.clone();
562 let h = tokio::spawn(async move {
563 tokio::time::timeout(Duration::from_secs(2), n2.wait())
564 .await
565 .expect("waiter timed out")
566 });
567 n.notify().await;
568 h.await.unwrap();
569 }
570}