1use std::collections::HashMap;
11use std::sync::Arc;
12
13use hickory_resolver::TokioResolver;
14use mailrs_dkim::HickoryDkimResolver;
15use sqlx::PgPool;
16
17use crate::dkim_sign::{self, DkimSignConfig};
18use crate::queue::{self, QueuedMessage};
19use crate::{DeliveryEventSender};
20
21mod delivery;
22mod smtp;
23
24pub use delivery::deliver_domain_static;
25pub use smtp::{TlsPolicy, try_deliver_via_mx, try_deliver_via_mx_with_tls};
26
27#[derive(Debug, Clone)]
29pub struct WorkerConfig {
30 pub poll_interval_secs: u64,
32 pub batch_size: u32,
34 pub max_attempts: u32,
36 pub max_concurrent_domains: usize,
38 pub max_messages_per_connection: usize,
41}
42
43impl Default for WorkerConfig {
44 fn default() -> Self {
45 Self {
46 poll_interval_secs: 30,
47 batch_size: 50,
48 max_attempts: 8,
49 max_concurrent_domains: 8,
50 max_messages_per_connection: 50,
51 }
52 }
53}
54
55pub fn group_by_domain(messages: Vec<QueuedMessage>) -> HashMap<String, Vec<QueuedMessage>> {
57 let mut groups: HashMap<String, Vec<QueuedMessage>> = HashMap::new();
58 for msg in messages {
59 groups.entry(msg.domain.clone()).or_default().push(msg);
60 }
61 groups
62}
63
64pub struct DeliveryWorker {
66 config: WorkerConfig,
67 pool: PgPool,
68 resolver: TokioResolver,
69 hostname: String,
70 dkim: Option<DkimSignConfig>,
71 dkim_resolver: Arc<HickoryDkimResolver>,
75 event_sender: Option<DeliveryEventSender>,
76 valkey_url: Option<String>,
77}
78
79impl DeliveryWorker {
80 pub fn new(
82 config: WorkerConfig,
83 pool: PgPool,
84 resolver: TokioResolver,
85 hostname: String,
86 ) -> Self {
87 let dkim_resolver = Arc::new(HickoryDkimResolver::new(resolver.clone()));
88
89 Self {
90 config,
91 pool,
92 resolver,
93 hostname,
94 dkim: None,
95 dkim_resolver,
96 event_sender: None,
97 valkey_url: None,
98 }
99 }
100
101 pub fn with_dkim(mut self, dkim: DkimSignConfig) -> Self {
103 self.dkim = Some(dkim);
104 self
105 }
106
107 pub fn with_event_sender(mut self, sender: DeliveryEventSender) -> Self {
109 self.event_sender = Some(sender);
110 self
111 }
112
113 pub fn with_valkey(mut self, url: String) -> Self {
115 self.valkey_url = Some(url);
116 self
117 }
118
119 pub async fn run(&self, mut shutdown: tokio::sync::watch::Receiver<bool>) {
121 tracing::info!(
122 "delivery worker started (poll_interval={}s)",
123 self.config.poll_interval_secs
124 );
125
126 let mut notify_rx = self.spawn_valkey_listener();
128
129 loop {
130 tokio::select! {
131 _ = tokio::time::sleep(std::time::Duration::from_secs(self.config.poll_interval_secs)) => {}
132 _ = wait_for_notify(&mut notify_rx) => {}
133 _ = shutdown.changed() => {
134 if *shutdown.borrow() {
135 tracing::info!("delivery worker shutting down");
136 return;
137 }
138 }
139 }
140
141 if let Err(e) = self.poll_and_deliver().await {
142 tracing::error!("delivery worker error: {e}");
143 }
144 }
145 }
146
147 fn spawn_valkey_listener(&self) -> Option<tokio::sync::mpsc::Receiver<()>> {
148 let url = self.valkey_url.as_ref()?;
149 let (tx, rx) = tokio::sync::mpsc::channel(16);
150 let url = url.clone();
151 tokio::spawn(async move {
152 loop {
153 match redis::Client::open(url.as_str()) {
154 Ok(client) => match client.get_async_pubsub().await {
155 Ok(mut pubsub) => {
156 if pubsub.subscribe("queue:notify").await.is_err() {
157 tokio::time::sleep(std::time::Duration::from_secs(5)).await;
158 continue;
159 }
160 tracing::info!("delivery worker subscribed to queue:notify");
161 use futures_util::StreamExt;
162 let mut stream = pubsub.on_message();
163 while let Some(_msg) = stream.next().await {
164 let _ = tx.try_send(());
165 }
166 }
167 Err(e) => {
168 tracing::warn!("valkey pubsub connect failed: {e}");
169 }
170 },
171 Err(e) => {
172 tracing::warn!("valkey client create failed: {e}");
173 }
174 }
175 tokio::time::sleep(std::time::Duration::from_secs(5)).await;
176 }
177 });
178 Some(rx)
179 }
180
181 async fn poll_and_deliver(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
182 let now = chrono::Utc::now().timestamp();
183
184 let recovered = queue::recover_stale_inflight(&self.pool, now).await?;
186 if recovered > 0 {
187 tracing::warn!("recovered {recovered} stale inflight messages");
188 }
189
190 if let Ok(pending) = queue::count_pending(&self.pool).await {
195 metrics::gauge!("mailrs_outbound_queue_depth", "status" => "pending")
196 .set(pending as f64);
197 }
198 if let Ok(inflight) = queue::count_inflight(&self.pool).await {
199 metrics::gauge!("mailrs_outbound_queue_depth", "status" => "inflight")
200 .set(inflight as f64);
201 }
202
203 let messages = queue::claim_for_delivery(&self.pool, now, self.config.batch_size).await?;
210
211 if messages.is_empty() {
212 return Ok(());
213 }
214
215 tracing::info!("claimed {} messages for delivery", messages.len());
216
217 let messages: Vec<QueuedMessage> = if let Some(ref dkim) = self.dkim {
234 use futures_util::stream::{self, StreamExt};
235 let dkim = dkim.clone();
236 let dkim_resolver = self.dkim_resolver.clone();
237 stream::iter(messages)
238 .map(|mut msg| {
239 let dkim = dkim.clone();
240 let dkim_resolver = dkim_resolver.clone();
241 async move {
242 if msg.is_forwarded {
244 match dkim_sign::arc_seal_message(
245 &dkim,
246 dkim_resolver.as_ref(),
247 &msg.message_data,
248 )
249 .await
250 {
251 Ok(sealed) => msg.message_data = sealed,
252 Err(e) => {
253 tracing::warn!("ARC sealing failed for msg {}: {e}", msg.id)
254 }
255 }
256 }
257 let data = std::mem::take(&mut msg.message_data);
261 let dkim_for_sign = dkim.clone();
262 match tokio::task::spawn_blocking(move || dkim_for_sign.sign(&data)).await {
263 Ok(Ok(signed)) => msg.message_data = signed,
264 Ok(Err(e)) => {
265 tracing::warn!("DKIM signing failed for msg {}: {e}", msg.id)
266 }
267 Err(e) => tracing::warn!(
268 "DKIM signing task join failed for msg {}: {e}",
269 msg.id
270 ),
271 }
272 msg
273 }
274 })
275 .buffer_unordered(8)
276 .collect()
277 .await
278 } else {
279 messages
280 };
281
282 let groups = group_by_domain(messages);
283 let pool = self.pool.clone();
284 let semaphore = Arc::new(tokio::sync::Semaphore::new(
285 self.config.max_concurrent_domains,
286 ));
287
288 let mut handles = Vec::new();
289 for (domain, domain_messages) in groups {
290 let sem = semaphore.clone();
291 let pool = pool.clone();
292 let resolver = self.resolver.clone();
293 let hostname = self.hostname.clone();
294 let max_per_conn = self.config.max_messages_per_connection;
295 let event_sender = self.event_sender.clone();
296
297 handles.push(tokio::spawn(async move {
298 let _permit = sem.acquire().await.unwrap();
299 deliver_domain_static(
300 &resolver,
301 &hostname,
302 &domain,
303 domain_messages,
304 &pool,
305 25,
306 max_per_conn,
307 event_sender.as_ref(),
308 )
309 .await;
310 }));
311 }
312
313 for handle in handles {
314 let _ = handle.await;
315 }
316
317 Ok(())
318 }
319}
320
321async fn wait_for_notify(rx: &mut Option<tokio::sync::mpsc::Receiver<()>>) {
323 match rx {
324 Some(r) => {
325 r.recv().await;
326 }
327 None => std::future::pending().await,
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334 use crate::queue::QueueStatus;
335
336 fn make_msg(id: i64, domain: &str) -> QueuedMessage {
337 QueuedMessage {
338 id,
339 sender: "sender@example.com".into(),
340 recipient: format!("rcpt@{domain}"),
341 domain: domain.into(),
342 message_data: vec![],
343 status: QueueStatus::Pending,
344 attempts: 0,
345 max_attempts: 8,
346 next_retry: 0,
347 last_error: None,
348 message_id: None,
349 created_at: 0,
350 updated_at: 0,
351 is_forwarded: false,
352 }
353 }
354
355 #[test]
356 fn group_by_domain_groups() {
357 let messages = vec![
358 make_msg(1, "a.com"),
359 make_msg(2, "b.com"),
360 make_msg(3, "a.com"),
361 ];
362 let groups = group_by_domain(messages);
363 assert_eq!(groups.len(), 2);
364 assert_eq!(groups["a.com"].len(), 2);
365 assert_eq!(groups["b.com"].len(), 1);
366 }
367
368 #[test]
369 fn group_by_domain_empty() {
370 let groups = group_by_domain(vec![]);
371 assert!(groups.is_empty());
372 }
373
374 #[test]
375 fn delivery_worker_config_defaults() {
376 let cfg = WorkerConfig::default();
377 assert_eq!(cfg.poll_interval_secs, 30);
378 assert_eq!(cfg.batch_size, 50);
379 assert_eq!(cfg.max_attempts, 8);
380 assert_eq!(cfg.max_concurrent_domains, 8);
381 assert_eq!(cfg.max_messages_per_connection, 50);
382 }
383
384 #[test]
385 fn group_by_domain_single_domain() {
386 let messages = vec![
387 make_msg(1, "a.com"),
388 make_msg(2, "a.com"),
389 make_msg(3, "a.com"),
390 ];
391 let groups = group_by_domain(messages);
392 assert_eq!(groups.len(), 1);
393 assert_eq!(groups["a.com"].len(), 3);
394 }
395
396 #[test]
397 fn group_by_domain_preserves_order_within_group() {
398 let messages = vec![
399 make_msg(10, "x.com"),
400 make_msg(20, "y.com"),
401 make_msg(30, "x.com"),
402 ];
403 let groups = group_by_domain(messages);
404 let x_ids: Vec<i64> = groups["x.com"].iter().map(|m| m.id).collect();
405 assert_eq!(x_ids, vec![10, 30]);
406 }
407
408 #[test]
409 fn group_by_domain_many_domains() {
410 let messages: Vec<QueuedMessage> = (0..100)
411 .map(|i| make_msg(i, &format!("domain{}.com", i % 10)))
412 .collect();
413 let groups = group_by_domain(messages);
414 assert_eq!(groups.len(), 10);
415 for v in groups.values() {
416 assert_eq!(v.len(), 10);
417 }
418 }
419
420 #[test]
421 fn worker_config_clone() {
422 let cfg = WorkerConfig::default();
423 let c2 = cfg.clone();
424 assert_eq!(c2.poll_interval_secs, cfg.poll_interval_secs);
425 assert_eq!(c2.batch_size, cfg.batch_size);
426 }
427
428 #[test]
429 fn group_by_domain_message_fields_intact() {
430 let msg = QueuedMessage {
431 id: 99,
432 sender: "orig@example.com".into(),
433 recipient: "dest@target.com".into(),
434 domain: "target.com".into(),
435 message_data: vec![0xde, 0xad],
436 status: QueueStatus::Pending,
437 attempts: 2,
438 max_attempts: 5,
439 next_retry: 12345,
440 last_error: Some("timeout".into()),
441 message_id: Some("mid99".into()),
442 created_at: 111,
443 updated_at: 222,
444 is_forwarded: true,
445 };
446 let groups = group_by_domain(vec![msg]);
447 let got = &groups["target.com"][0];
448 assert_eq!(got.id, 99);
449 assert_eq!(got.sender, "orig@example.com");
450 assert_eq!(got.attempts, 2);
451 assert_eq!(got.message_data, vec![0xde, 0xad]);
452 assert!(got.is_forwarded);
453 assert_eq!(got.last_error, Some("timeout".into()));
454 }
455
456 #[test]
457 fn tls_policy_equality() {
458 assert_eq!(TlsPolicy::Opportunistic, TlsPolicy::Opportunistic);
459 assert_eq!(TlsPolicy::Require, TlsPolicy::Require);
460 assert_ne!(TlsPolicy::Opportunistic, TlsPolicy::Require);
461 }
462
463 #[test]
464 fn tls_policy_debug() {
465 let dbg = format!("{:?}", TlsPolicy::Opportunistic);
466 assert!(dbg.contains("Opportunistic"));
467 let dbg = format!("{:?}", TlsPolicy::Require);
468 assert!(dbg.contains("Require"));
469 }
470
471 #[test]
472 fn tls_policy_clone() {
473 let p = TlsPolicy::Require;
474 let p2 = p;
475 assert_eq!(p, p2);
476 }
477
478 #[test]
479 fn tls_policy_copy_semantics() {
480 let a = TlsPolicy::Opportunistic;
482 let b = a;
483 let c = a; assert_eq!(a, b);
485 assert_eq!(b, c);
486 }
487
488 #[test]
489 fn tls_policy_all_variants_distinct() {
490 let variants = [TlsPolicy::Opportunistic, TlsPolicy::Require];
491 for (i, a) in variants.iter().enumerate() {
492 for (j, b) in variants.iter().enumerate() {
493 if i == j {
494 assert_eq!(a, b);
495 } else {
496 assert_ne!(a, b);
497 }
498 }
499 }
500 }
501
502 #[test]
503 fn worker_config_custom_values() {
504 let cfg = WorkerConfig {
505 poll_interval_secs: 10,
506 batch_size: 100,
507 max_attempts: 3,
508 max_concurrent_domains: 16,
509 max_messages_per_connection: 25,
510 };
511 assert_eq!(cfg.poll_interval_secs, 10);
512 assert_eq!(cfg.batch_size, 100);
513 assert_eq!(cfg.max_attempts, 3);
514 assert_eq!(cfg.max_concurrent_domains, 16);
515 assert_eq!(cfg.max_messages_per_connection, 25);
516 }
517
518 #[test]
519 fn worker_config_debug_format() {
520 let cfg = WorkerConfig::default();
521 let dbg = format!("{:?}", cfg);
522 assert!(dbg.contains("WorkerConfig"));
523 assert!(dbg.contains("poll_interval_secs"));
524 assert!(dbg.contains("batch_size"));
525 }
526
527 #[test]
528 fn group_by_domain_unicode_domains() {
529 let messages = vec![
530 make_msg(1, "xn--e1afmapc.xn--p1ai"), make_msg(2, "xn--e1afmapc.xn--p1ai"),
532 make_msg(3, "example.jp"),
533 ];
534 let groups = group_by_domain(messages);
535 assert_eq!(groups.len(), 2);
536 assert_eq!(groups["xn--e1afmapc.xn--p1ai"].len(), 2);
537 assert_eq!(groups["example.jp"].len(), 1);
538 }
539
540 #[test]
541 fn group_by_domain_all_unique_domains() {
542 let messages: Vec<QueuedMessage> =
543 (0..50).map(|i| make_msg(i, &format!("d{i}.com"))).collect();
544 let groups = group_by_domain(messages);
545 assert_eq!(groups.len(), 50);
546 for v in groups.values() {
547 assert_eq!(v.len(), 1);
548 }
549 }
550
551 #[test]
552 fn group_by_domain_domain_with_subdomains() {
553 let messages = vec![
555 make_msg(1, "example.com"),
556 make_msg(2, "mail.example.com"),
557 make_msg(3, "example.com"),
558 ];
559 let groups = group_by_domain(messages);
560 assert_eq!(groups.len(), 2);
561 assert_eq!(groups["example.com"].len(), 2);
562 assert_eq!(groups["mail.example.com"].len(), 1);
563 }
564
565 fn extract_sender_domain(sender: &str) -> &str {
567 sender.rsplit_once('@').map(|(_, d)| d).unwrap_or("unknown")
568 }
569
570 #[test]
571 fn sender_domain_extraction_normal() {
572 assert_eq!(extract_sender_domain("user@example.com"), "example.com");
573 }
574
575 #[test]
576 fn sender_domain_extraction_no_at() {
577 assert_eq!(extract_sender_domain("noatsign"), "unknown");
578 }
579
580 #[test]
581 fn sender_domain_extraction_multiple_at() {
582 assert_eq!(extract_sender_domain("user@sub@example.com"), "example.com");
584 }
585
586 #[test]
587 fn sender_domain_extraction_empty() {
588 assert_eq!(extract_sender_domain(""), "unknown");
589 }
590
591 #[test]
592 fn sender_domain_extraction_at_only() {
593 assert_eq!(extract_sender_domain("@"), "");
594 }
595
596 #[test]
597 fn dsn_skip_empty_sender() {
598 let msg = make_msg(1, "example.com");
600 assert!(
601 msg.sender != "<>" && !msg.sender.is_empty(),
602 "test setup: msg has a real sender"
603 );
604
605 let empty_sender = "";
607 assert!(empty_sender.is_empty() || empty_sender == "<>");
608
609 let null_sender = "<>";
611 assert!(null_sender.is_empty() || null_sender == "<>");
612 }
613
614 #[test]
615 fn dsn_skip_null_sender() {
616 let null_sender = "<>";
618 let empty_sender = "";
619 let real_sender = "user@example.com";
620
621 assert!(null_sender == "<>" || null_sender.is_empty());
623 assert!(empty_sender == "<>" || empty_sender.is_empty());
624
625 assert!(real_sender != "<>" && !real_sender.is_empty());
627 }
628
629 #[test]
630 fn retry_delay_integration_with_group_delivery() {
631 use crate::retry::retry_delay_secs;
633 for attempt in 0..10u32 {
634 let delay = retry_delay_secs(attempt);
635 assert!(
636 delay >= 60,
637 "delay at attempt {attempt} should be at least 60s"
638 );
639 assert!(
640 delay <= 28800,
641 "delay at attempt {attempt} should be capped at 28800s"
642 );
643 }
644 }
645
646 #[test]
647 fn should_bounce_integration_with_worker_defaults() {
648 use crate::retry::should_bounce;
650 let max = WorkerConfig::default().max_attempts;
651 for attempt in 0..max {
652 assert!(
653 !should_bounce(attempt, max),
654 "attempt {attempt} should not bounce"
655 );
656 }
657 assert!(should_bounce(max, max), "attempt {max} should bounce");
658 assert!(
659 should_bounce(max + 1, max),
660 "attempt {} should bounce",
661 max + 1
662 );
663 }
664
665 #[test]
666 fn make_msg_helper_defaults() {
667 let msg = make_msg(42, "test.org");
668 assert_eq!(msg.id, 42);
669 assert_eq!(msg.domain, "test.org");
670 assert_eq!(msg.recipient, "rcpt@test.org");
671 assert_eq!(msg.sender, "sender@example.com");
672 assert_eq!(msg.status, QueueStatus::Pending);
673 assert_eq!(msg.attempts, 0);
674 assert_eq!(msg.max_attempts, 8);
675 assert!(!msg.is_forwarded);
676 assert!(msg.last_error.is_none());
677 assert!(msg.message_id.is_none());
678 }
679
680 #[test]
681 fn group_by_domain_large_batch() {
682 let batch_size = WorkerConfig::default().batch_size;
684 let messages: Vec<QueuedMessage> = (0..batch_size as i64)
685 .map(|i| make_msg(i, &format!("domain{}.com", i % 5)))
686 .collect();
687 let groups = group_by_domain(messages);
688 assert_eq!(groups.len(), 5);
689 let total: usize = groups.values().map(|v| v.len()).sum();
690 assert_eq!(total, batch_size as usize);
691 }
692
693 #[test]
694 fn group_by_domain_ids_are_all_present() {
695 let messages = vec![
696 make_msg(100, "a.com"),
697 make_msg(200, "b.com"),
698 make_msg(300, "a.com"),
699 make_msg(400, "c.com"),
700 make_msg(500, "b.com"),
701 ];
702 let groups = group_by_domain(messages);
703 let mut all_ids: Vec<i64> = groups
704 .values()
705 .flat_map(|v| v.iter().map(|m| m.id))
706 .collect();
707 all_ids.sort();
708 assert_eq!(all_ids, vec![100, 200, 300, 400, 500]);
709 }
710}