Skip to main content

rusmes_core/mailets/
bounce.rs

1//! Bounce message generation mailet (RFC 3464 - DSN)
2
3use crate::dsn::{smtp_diagnostic_text, smtp_to_enhanced_code};
4use crate::mailet::{Mailet, MailetAction, MailetConfig};
5use async_trait::async_trait;
6use rusmes_proto::Mail;
7use std::time::{SystemTime, UNIX_EPOCH};
8
9/// SMTP status code
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub enum SmtpStatusCode {
12    /// 4xx - Temporary failure
13    TemporaryFailure(u16),
14    /// 5xx - Permanent failure
15    PermanentFailure(u16),
16}
17
18impl SmtpStatusCode {
19    /// Parse from status code
20    pub fn from_code(code: u16) -> Option<Self> {
21        match code {
22            400..=499 => Some(SmtpStatusCode::TemporaryFailure(code)),
23            500..=599 => Some(SmtpStatusCode::PermanentFailure(code)),
24            _ => None,
25        }
26    }
27
28    /// Get numeric code
29    pub fn code(&self) -> u16 {
30        match self {
31            SmtpStatusCode::TemporaryFailure(c) | SmtpStatusCode::PermanentFailure(c) => *c,
32        }
33    }
34
35    /// Check if permanent
36    pub fn is_permanent(&self) -> bool {
37        matches!(self, SmtpStatusCode::PermanentFailure(_))
38    }
39
40    /// Get enhanced status code (RFC 3463)
41    pub fn enhanced_code(&self) -> String {
42        smtp_to_enhanced_code(self.code()).to_string()
43    }
44
45    /// Get diagnostic text
46    pub fn diagnostic_text(&self) -> &str {
47        smtp_diagnostic_text(self.code())
48    }
49}
50
51/// DSN action type (RFC 3464)
52#[derive(Debug, Clone, PartialEq)]
53pub enum DsnAction {
54    Failed,
55    Delayed,
56    Delivered,
57    Relayed,
58    Expanded,
59}
60
61impl DsnAction {
62    pub fn as_str(&self) -> &str {
63        match self {
64            DsnAction::Failed => "failed",
65            DsnAction::Delayed => "delayed",
66            DsnAction::Delivered => "delivered",
67            DsnAction::Relayed => "relayed",
68            DsnAction::Expanded => "expanded",
69        }
70    }
71}
72
73/// Delivery Status Notification (DSN)
74#[derive(Debug, Clone)]
75pub struct DeliveryStatusNotification {
76    /// Reporting MTA
77    pub reporting_mta: String,
78    /// Arrival date
79    pub arrival_date: u64,
80    /// Per-recipient fields
81    pub recipients: Vec<DsnRecipient>,
82}
83
84/// Per-recipient DSN information
85#[derive(Debug, Clone)]
86pub struct DsnRecipient {
87    /// Final recipient
88    pub final_recipient: String,
89    /// Action taken
90    pub action: DsnAction,
91    /// Status code (enhanced)
92    pub status: String,
93    /// Diagnostic code (optional)
94    pub diagnostic_code: Option<String>,
95    /// Remote MTA (optional)
96    pub remote_mta: Option<String>,
97    /// Last attempt date
98    pub last_attempt_date: Option<u64>,
99}
100
101impl DeliveryStatusNotification {
102    /// Create a new DSN
103    pub fn new(reporting_mta: String) -> Self {
104        let arrival_date = SystemTime::now()
105            .duration_since(UNIX_EPOCH)
106            .unwrap_or_default()
107            .as_secs();
108
109        Self {
110            reporting_mta,
111            arrival_date,
112            recipients: Vec::new(),
113        }
114    }
115
116    /// Add a recipient
117    pub fn add_recipient(&mut self, recipient: DsnRecipient) {
118        self.recipients.push(recipient);
119    }
120
121    /// Generate RFC 3464 message body (machine-readable delivery status)
122    pub fn generate_message_body(&self) -> String {
123        let mut body = String::new();
124
125        // Per-message DSN fields
126        body.push_str(&format!("Reporting-MTA: dns; {}\n", self.reporting_mta));
127        body.push_str(&format!(
128            "Arrival-Date: {}\n",
129            self.format_date(self.arrival_date)
130        ));
131        body.push('\n');
132
133        // Per-recipient DSN fields
134        for recipient in &self.recipients {
135            body.push_str(&format!(
136                "Final-Recipient: rfc822; {}\n",
137                recipient.final_recipient
138            ));
139            body.push_str(&format!("Action: {}\n", recipient.action.as_str()));
140            body.push_str(&format!("Status: {}\n", recipient.status));
141
142            if let Some(ref diagnostic) = recipient.diagnostic_code {
143                body.push_str(&format!("Diagnostic-Code: smtp; {}\n", diagnostic));
144            }
145
146            if let Some(ref remote_mta) = recipient.remote_mta {
147                body.push_str(&format!("Remote-MTA: dns; {}\n", remote_mta));
148            }
149
150            if let Some(date) = recipient.last_attempt_date {
151                body.push_str(&format!("Last-Attempt-Date: {}\n", self.format_date(date)));
152            }
153
154            body.push('\n');
155        }
156
157        body
158    }
159
160    /// Generate human-readable explanation text
161    pub fn generate_human_text(&self, error_code: SmtpStatusCode, error_message: &str) -> String {
162        let mut text = String::new();
163
164        text.push_str("This is an automatically generated Delivery Status Notification.\n\n");
165
166        if error_code.is_permanent() {
167            text.push_str("YOUR MESSAGE COULD NOT BE DELIVERED to the following recipients:\n\n");
168        } else {
169            text.push_str(
170                "DELIVERY OF YOUR MESSAGE HAS BEEN DELAYED to the following recipients:\n\n",
171            );
172        }
173
174        for recipient in &self.recipients {
175            text.push_str(&format!("  {}\n", recipient.final_recipient));
176        }
177
178        text.push('\n');
179        text.push_str(&format!(
180            "Reason: {} {}\n",
181            error_code.code(),
182            error_message
183        ));
184        text.push_str(&format!(
185            "Enhanced Status Code: {}\n",
186            error_code.enhanced_code()
187        ));
188        text.push_str(&format!("Diagnostic: {}\n", error_code.diagnostic_text()));
189        text.push('\n');
190
191        if error_code.is_permanent() {
192            text.push_str("No further delivery attempts will be made.\n");
193        } else {
194            text.push_str(
195                "Delivery will be retried. You will be notified if delivery continues to fail.\n",
196            );
197        }
198
199        text
200    }
201
202    fn format_date(&self, timestamp: u64) -> String {
203        use chrono::{DateTime, Utc};
204
205        if let Some(dt) = DateTime::<Utc>::from_timestamp(timestamp as i64, 0) {
206            dt.to_rfc2822()
207        } else {
208            format!("timestamp:{}", timestamp)
209        }
210    }
211}
212
213/// Bounce mailet - generates DSN messages
214pub struct BounceMailet {
215    name: String,
216    /// Reporting MTA hostname
217    reporting_mta: String,
218    /// Postmaster address
219    postmaster: String,
220    /// Include original message headers
221    include_headers: bool,
222    /// Include original message body
223    include_body: bool,
224    /// Maximum body size to include
225    max_body_size: usize,
226}
227
228impl BounceMailet {
229    /// Create a new bounce mailet
230    pub fn new() -> Self {
231        Self {
232            name: "Bounce".to_string(),
233            reporting_mta: "localhost".to_string(),
234            postmaster: "postmaster@localhost".to_string(),
235            include_headers: true,
236            include_body: false,
237            max_body_size: 1024,
238        }
239    }
240
241    /// Generate a bounce message (RFC 3464 compliant multipart/report)
242    pub fn generate_bounce(
243        &self,
244        mail: &Mail,
245        error_code: SmtpStatusCode,
246        error_message: &str,
247    ) -> String {
248        let mut dsn = DeliveryStatusNotification::new(self.reporting_mta.clone());
249
250        // Add recipient information
251        for recipient in mail.recipients() {
252            let action = if error_code.is_permanent() {
253                DsnAction::Failed
254            } else {
255                DsnAction::Delayed
256            };
257
258            let recipient_dsn = DsnRecipient {
259                final_recipient: recipient.to_string(),
260                action,
261                status: error_code.enhanced_code(),
262                diagnostic_code: Some(format!("{} {}", error_code.code(), error_message)),
263                remote_mta: mail
264                    .get_attribute("smtp.remote_mta")
265                    .and_then(|v| v.as_str())
266                    .map(|s| s.to_string()),
267                last_attempt_date: Some(
268                    SystemTime::now()
269                        .duration_since(UNIX_EPOCH)
270                        .unwrap_or_default()
271                        .as_secs(),
272                ),
273            };
274
275            dsn.add_recipient(recipient_dsn);
276        }
277
278        // Generate RFC 3464 compliant multipart/report message
279        self.generate_multipart_report(&dsn, mail, error_code, error_message)
280    }
281
282    /// Generate RFC 3464 compliant multipart/report message
283    fn generate_multipart_report(
284        &self,
285        dsn: &DeliveryStatusNotification,
286        mail: &Mail,
287        error_code: SmtpStatusCode,
288        error_message: &str,
289    ) -> String {
290        let mut message = String::new();
291        let boundary = "rusmes-dsn-boundary-7b3a9f1e";
292
293        // Main MIME headers for multipart/report
294        message.push_str("MIME-Version: 1.0\n");
295        message.push_str(&format!(
296            "Content-Type: multipart/report; report-type=delivery-status; boundary=\"{}\"\n",
297            boundary
298        ));
299        message.push('\n');
300        message.push_str("This is a MIME-encapsulated message.\n");
301        message.push('\n');
302
303        // Part 1: Human-readable explanation (text/plain)
304        message.push_str(&format!("--{}\n", boundary));
305        message.push_str("Content-Type: text/plain; charset=utf-8\n");
306        message.push_str("Content-Description: Notification\n");
307        message.push('\n');
308        message.push_str(&dsn.generate_human_text(error_code, error_message));
309        message.push('\n');
310
311        let msg_id = mail.message_id();
312        message.push_str(&format!("Original Message-ID: {}\n", msg_id));
313        message.push('\n');
314
315        // Part 2: Machine-readable delivery status (message/delivery-status)
316        message.push_str(&format!("--{}\n", boundary));
317        message.push_str("Content-Type: message/delivery-status\n");
318        message.push_str("Content-Description: Delivery Report\n");
319        message.push('\n');
320        message.push_str(&dsn.generate_message_body());
321
322        // Part 3: Original message or headers (text/rfc822-headers or message/rfc822)
323        message.push_str(&format!("--{}\n", boundary));
324
325        if self.include_body {
326            // Include entire original message
327            message.push_str("Content-Type: message/rfc822\n");
328            message.push_str("Content-Description: Undelivered Message\n");
329            message.push('\n');
330
331            // Include headers
332            self.append_original_headers(&mut message, mail);
333
334            // Include body
335            if let Some(body) = mail.get_attribute("message.body").and_then(|v| v.as_str()) {
336                message.push('\n');
337                let truncated_body = if body.len() > self.max_body_size {
338                    format!("{}... (truncated)", &body[..self.max_body_size])
339                } else {
340                    body.to_string()
341                };
342                message.push_str(&truncated_body);
343                message.push('\n');
344            }
345        } else if self.include_headers {
346            // Include headers only
347            message.push_str("Content-Type: text/rfc822-headers\n");
348            message.push_str("Content-Description: Undelivered Message Headers\n");
349            message.push('\n');
350            self.append_original_headers(&mut message, mail);
351        }
352
353        // End boundary
354        message.push_str(&format!("--{}--\n", boundary));
355
356        message
357    }
358
359    /// Append original message headers to the bounce message
360    fn append_original_headers(&self, message: &mut String, mail: &Mail) {
361        if let Some(subject) = mail
362            .get_attribute("header.Subject")
363            .and_then(|v| v.as_str())
364        {
365            message.push_str(&format!("Subject: {}\n", subject));
366        }
367        if let Some(from) = mail.get_attribute("header.From").and_then(|v| v.as_str()) {
368            message.push_str(&format!("From: {}\n", from));
369        }
370        if let Some(to) = mail.get_attribute("header.To").and_then(|v| v.as_str()) {
371            message.push_str(&format!("To: {}\n", to));
372        }
373        if let Some(date) = mail.get_attribute("header.Date").and_then(|v| v.as_str()) {
374            message.push_str(&format!("Date: {}\n", date));
375        }
376        if let Some(cc) = mail.get_attribute("header.Cc").and_then(|v| v.as_str()) {
377            message.push_str(&format!("Cc: {}\n", cc));
378        }
379        if let Some(msg_id) = mail
380            .get_attribute("header.Message-ID")
381            .and_then(|v| v.as_str())
382        {
383            message.push_str(&format!("Message-ID: {}\n", msg_id));
384        }
385    }
386}
387
388impl Default for BounceMailet {
389    fn default() -> Self {
390        Self::new()
391    }
392}
393
394#[async_trait]
395impl Mailet for BounceMailet {
396    async fn init(&mut self, config: MailetConfig) -> anyhow::Result<()> {
397        if let Some(mta) = config.get_param("reporting_mta") {
398            self.reporting_mta = mta.to_string();
399        }
400
401        if let Some(postmaster) = config.get_param("postmaster") {
402            self.postmaster = postmaster.to_string();
403        }
404
405        if let Some(include) = config.get_param("include_headers") {
406            self.include_headers = include.parse().unwrap_or(true);
407        }
408
409        if let Some(include) = config.get_param("include_body") {
410            self.include_body = include.parse().unwrap_or(false);
411        }
412
413        if let Some(max_size) = config.get_param("max_body_size") {
414            self.max_body_size = max_size.parse().unwrap_or(1024);
415        }
416
417        tracing::info!("Initialized BounceMailet");
418        Ok(())
419    }
420
421    async fn service(&self, mail: &mut Mail) -> anyhow::Result<MailetAction> {
422        // Check if this mail needs a bounce
423        let should_bounce = mail
424            .get_attribute("bounce.required")
425            .and_then(|v| v.as_bool())
426            .unwrap_or(false);
427
428        if !should_bounce {
429            return Ok(MailetAction::Continue);
430        }
431
432        // Get error information
433        let error_code_num = mail
434            .get_attribute("bounce.error_code")
435            .and_then(|v| v.as_i64())
436            .unwrap_or(550) as u16;
437
438        let error_message = mail
439            .get_attribute("bounce.error_message")
440            .and_then(|v| v.as_str())
441            .unwrap_or("Delivery failed");
442
443        let error_code = SmtpStatusCode::from_code(error_code_num)
444            .unwrap_or(SmtpStatusCode::PermanentFailure(550));
445
446        tracing::info!(
447            "Generating bounce for mail {} with code {}",
448            mail.id(),
449            error_code.code()
450        );
451
452        // Generate bounce message
453        let bounce_body = self.generate_bounce(mail, error_code, error_message);
454
455        // Store bounce in mail attributes for delivery
456        mail.set_attribute("bounce.generated", true);
457        mail.set_attribute("bounce.body", bounce_body);
458
459        Ok(MailetAction::Continue)
460    }
461
462    fn name(&self) -> &str {
463        &self.name
464    }
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470    use bytes::Bytes;
471    use rusmes_proto::{HeaderMap, MailAddress, MessageBody, MimeMessage};
472    use std::str::FromStr;
473
474    #[test]
475    fn test_smtp_status_code_parsing() {
476        assert_eq!(
477            SmtpStatusCode::from_code(450),
478            Some(SmtpStatusCode::TemporaryFailure(450))
479        );
480        assert_eq!(
481            SmtpStatusCode::from_code(550),
482            Some(SmtpStatusCode::PermanentFailure(550))
483        );
484        assert_eq!(SmtpStatusCode::from_code(250), None);
485    }
486
487    #[test]
488    fn test_smtp_status_code_permanent() {
489        assert!(SmtpStatusCode::PermanentFailure(550).is_permanent());
490        assert!(!SmtpStatusCode::TemporaryFailure(450).is_permanent());
491    }
492
493    #[test]
494    fn test_smtp_enhanced_codes() {
495        assert_eq!(
496            SmtpStatusCode::PermanentFailure(550).enhanced_code(),
497            "5.1.1"
498        );
499        assert_eq!(
500            SmtpStatusCode::TemporaryFailure(450).enhanced_code(),
501            "4.2.1"
502        );
503        assert_eq!(
504            SmtpStatusCode::PermanentFailure(552).enhanced_code(),
505            "5.2.2"
506        );
507    }
508
509    #[test]
510    fn test_smtp_diagnostic_text() {
511        assert_eq!(
512            SmtpStatusCode::PermanentFailure(550).diagnostic_text(),
513            "Requested action not taken: mailbox unavailable"
514        );
515    }
516
517    #[test]
518    fn test_dsn_action_as_str() {
519        assert_eq!(DsnAction::Failed.as_str(), "failed");
520        assert_eq!(DsnAction::Delayed.as_str(), "delayed");
521        assert_eq!(DsnAction::Delivered.as_str(), "delivered");
522    }
523
524    #[test]
525    fn test_dsn_creation() {
526        let dsn = DeliveryStatusNotification::new("mail.example.com".to_string());
527        assert_eq!(dsn.reporting_mta, "mail.example.com");
528        assert!(dsn.recipients.is_empty());
529    }
530
531    #[test]
532    fn test_dsn_add_recipient() {
533        let mut dsn = DeliveryStatusNotification::new("mail.example.com".to_string());
534        let recipient = DsnRecipient {
535            final_recipient: "user@example.com".to_string(),
536            action: DsnAction::Failed,
537            status: "5.1.1".to_string(),
538            diagnostic_code: Some("550 User unknown".to_string()),
539            remote_mta: None,
540            last_attempt_date: None,
541        };
542
543        dsn.add_recipient(recipient);
544        assert_eq!(dsn.recipients.len(), 1);
545    }
546
547    #[test]
548    fn test_dsn_message_body_generation() {
549        let mut dsn = DeliveryStatusNotification::new("mail.example.com".to_string());
550        let recipient = DsnRecipient {
551            final_recipient: "user@example.com".to_string(),
552            action: DsnAction::Failed,
553            status: "5.1.1".to_string(),
554            diagnostic_code: Some("550 User unknown".to_string()),
555            remote_mta: Some("remote.example.com".to_string()),
556            last_attempt_date: None,
557        };
558
559        dsn.add_recipient(recipient);
560        let body = dsn.generate_message_body();
561
562        assert!(body.contains("Reporting-MTA: dns; mail.example.com"));
563        assert!(body.contains("Final-Recipient: rfc822; user@example.com"));
564        assert!(body.contains("Action: failed"));
565        assert!(body.contains("Status: 5.1.1"));
566        assert!(body.contains("Diagnostic-Code: smtp; 550 User unknown"));
567        assert!(body.contains("Remote-MTA: dns; remote.example.com"));
568    }
569
570    #[tokio::test]
571    async fn test_bounce_mailet_init() {
572        let mut mailet = BounceMailet::new();
573        let config = MailetConfig::new("Bounce");
574
575        mailet.init(config).await.unwrap();
576        assert_eq!(mailet.name(), "Bounce");
577    }
578
579    #[tokio::test]
580    async fn test_bounce_mailet_no_bounce_required() {
581        let mailet = BounceMailet::new();
582        let mut mail = Mail::new(
583            Some(MailAddress::from_str("sender@test.com").unwrap()),
584            vec![MailAddress::from_str("rcpt@test.com").unwrap()],
585            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
586            None,
587            None,
588        );
589
590        let action = mailet.service(&mut mail).await.unwrap();
591        assert_eq!(action, MailetAction::Continue);
592        assert!(mail.get_attribute("bounce.generated").is_none());
593    }
594
595    #[tokio::test]
596    async fn test_bounce_mailet_generate_bounce() {
597        let mailet = BounceMailet::new();
598        let mut mail = Mail::new(
599            Some(MailAddress::from_str("sender@test.com").unwrap()),
600            vec![MailAddress::from_str("unknown@test.com").unwrap()],
601            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
602            None,
603            None,
604        );
605
606        mail.set_attribute("bounce.required", true);
607        mail.set_attribute("bounce.error_code", 550_i64);
608        mail.set_attribute("bounce.error_message", "User unknown");
609
610        let action = mailet.service(&mut mail).await.unwrap();
611        assert_eq!(action, MailetAction::Continue);
612        assert_eq!(
613            mail.get_attribute("bounce.generated")
614                .and_then(|v| v.as_bool()),
615            Some(true)
616        );
617        assert!(mail.get_attribute("bounce.body").is_some());
618    }
619
620    #[tokio::test]
621    async fn test_bounce_message_content() {
622        let mailet = BounceMailet::new();
623        let mut mail = Mail::new(
624            Some(MailAddress::from_str("sender@test.com").unwrap()),
625            vec![MailAddress::from_str("user@test.com").unwrap()],
626            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
627            None,
628            None,
629        );
630
631        mail.set_attribute("header.Subject", "Test message");
632        let error_code = SmtpStatusCode::PermanentFailure(550);
633        let bounce = mailet.generate_bounce(&mail, error_code, "User unknown");
634
635        assert!(bounce.contains("Delivery Status Notification"));
636        assert!(bounce.contains("user@test.com"));
637        assert!(bounce.contains("550"));
638        assert!(bounce.contains("User unknown"));
639        assert!(bounce.contains("5.1.1")); // Enhanced status code
640    }
641
642    #[tokio::test]
643    async fn test_bounce_config_reporting_mta() {
644        let mut mailet = BounceMailet::new();
645        let config = MailetConfig::new("Bounce").with_param("reporting_mta", "mail.example.com");
646
647        mailet.init(config).await.unwrap();
648        assert_eq!(mailet.reporting_mta, "mail.example.com");
649    }
650
651    #[tokio::test]
652    async fn test_bounce_config_postmaster() {
653        let mut mailet = BounceMailet::new();
654        let config = MailetConfig::new("Bounce").with_param("postmaster", "postmaster@example.com");
655
656        mailet.init(config).await.unwrap();
657        assert_eq!(mailet.postmaster, "postmaster@example.com");
658    }
659
660    #[tokio::test]
661    async fn test_bounce_config_include_headers() {
662        let mut mailet = BounceMailet::new();
663        let config = MailetConfig::new("Bounce").with_param("include_headers", "false");
664
665        mailet.init(config).await.unwrap();
666        assert!(!mailet.include_headers);
667    }
668
669    #[tokio::test]
670    async fn test_bounce_config_include_body() {
671        let mut mailet = BounceMailet::new();
672        let config = MailetConfig::new("Bounce").with_param("include_body", "true");
673
674        mailet.init(config).await.unwrap();
675        assert!(mailet.include_body);
676    }
677
678    #[tokio::test]
679    async fn test_bounce_config_max_body_size() {
680        let mut mailet = BounceMailet::new();
681        let config = MailetConfig::new("Bounce").with_param("max_body_size", "2048");
682
683        mailet.init(config).await.unwrap();
684        assert_eq!(mailet.max_body_size, 2048);
685    }
686
687    #[tokio::test]
688    async fn test_bounce_multipart_structure() {
689        let mailet = BounceMailet::new();
690        let mail = Mail::new(
691            Some(MailAddress::from_str("sender@test.com").unwrap()),
692            vec![MailAddress::from_str("user@test.com").unwrap()],
693            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
694            None,
695            None,
696        );
697
698        let error_code = SmtpStatusCode::PermanentFailure(550);
699        let bounce = mailet.generate_bounce(&mail, error_code, "User unknown");
700
701        // Check multipart structure (old style boundary for backwards compatibility)
702        assert!(bounce.contains("Content-Type: text/plain"));
703        assert!(bounce.contains("Content-Type: message/delivery-status"));
704    }
705
706    #[tokio::test]
707    async fn test_bounce_temporary_failure() {
708        let mailet = BounceMailet::new();
709        let mail = Mail::new(
710            Some(MailAddress::from_str("sender@test.com").unwrap()),
711            vec![MailAddress::from_str("user@test.com").unwrap()],
712            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
713            None,
714            None,
715        );
716
717        let error_code = SmtpStatusCode::TemporaryFailure(450);
718        let bounce = mailet.generate_bounce(&mail, error_code, "Mailbox unavailable");
719
720        assert!(bounce.contains("450"));
721        assert!(bounce.contains("4.2.1")); // Enhanced code for temporary
722        assert!(bounce.contains("Mailbox unavailable"));
723    }
724
725    #[tokio::test]
726    async fn test_bounce_include_original_headers() {
727        let mut mailet = BounceMailet::new();
728        mailet.include_headers = true;
729
730        let mut mail = Mail::new(
731            Some(MailAddress::from_str("sender@test.com").unwrap()),
732            vec![MailAddress::from_str("user@test.com").unwrap()],
733            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
734            None,
735            None,
736        );
737        mail.set_attribute("header.Subject", "Test Subject");
738        mail.set_attribute("header.From", "sender@test.com");
739
740        let error_code = SmtpStatusCode::PermanentFailure(550);
741        let bounce = mailet.generate_bounce(&mail, error_code, "User unknown");
742
743        assert!(bounce.contains("Subject: Test Subject"));
744        assert!(bounce.contains("From: sender@test.com"));
745    }
746
747    #[tokio::test]
748    async fn test_bounce_include_body_truncated() {
749        let mut mailet = BounceMailet::new();
750        mailet.include_body = true;
751        mailet.max_body_size = 10;
752
753        let mut mail = Mail::new(
754            Some(MailAddress::from_str("sender@test.com").unwrap()),
755            vec![MailAddress::from_str("user@test.com").unwrap()],
756            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
757            None,
758            None,
759        );
760        mail.set_attribute(
761            "message.body",
762            "This is a very long message body that should be truncated",
763        );
764
765        let error_code = SmtpStatusCode::PermanentFailure(550);
766        let bounce = mailet.generate_bounce(&mail, error_code, "User unknown");
767
768        assert!(bounce.contains("(truncated)"));
769    }
770
771    #[tokio::test]
772    async fn test_bounce_multiple_recipients() {
773        let mailet = BounceMailet::new();
774        let mail = Mail::new(
775            Some(MailAddress::from_str("sender@test.com").unwrap()),
776            vec![
777                MailAddress::from_str("user1@test.com").unwrap(),
778                MailAddress::from_str("user2@test.com").unwrap(),
779            ],
780            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
781            None,
782            None,
783        );
784
785        let error_code = SmtpStatusCode::PermanentFailure(550);
786        let bounce = mailet.generate_bounce(&mail, error_code, "Users unknown");
787
788        assert!(bounce.contains("user1@test.com"));
789        assert!(bounce.contains("user2@test.com"));
790    }
791
792    #[tokio::test]
793    async fn test_bounce_with_remote_mta() {
794        let mailet = BounceMailet::new();
795        let mut mail = Mail::new(
796            Some(MailAddress::from_str("sender@test.com").unwrap()),
797            vec![MailAddress::from_str("user@test.com").unwrap()],
798            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
799            None,
800            None,
801        );
802        mail.set_attribute("smtp.remote_mta", "remote.example.com");
803
804        let error_code = SmtpStatusCode::PermanentFailure(550);
805        let bounce = mailet.generate_bounce(&mail, error_code, "User unknown");
806
807        assert!(bounce.contains("Remote-MTA: dns; remote.example.com"));
808    }
809
810    #[test]
811    fn test_smtp_status_code_all_enhanced_codes() {
812        // Test all specific mappings
813        assert_eq!(
814            SmtpStatusCode::TemporaryFailure(421).enhanced_code(),
815            "4.4.2"
816        );
817        assert_eq!(
818            SmtpStatusCode::TemporaryFailure(450).enhanced_code(),
819            "4.2.1"
820        );
821        assert_eq!(
822            SmtpStatusCode::TemporaryFailure(451).enhanced_code(),
823            "4.3.0"
824        );
825        assert_eq!(
826            SmtpStatusCode::TemporaryFailure(452).enhanced_code(),
827            "4.2.2"
828        );
829        assert_eq!(
830            SmtpStatusCode::TemporaryFailure(454).enhanced_code(),
831            "4.7.0"
832        );
833        assert_eq!(
834            SmtpStatusCode::PermanentFailure(550).enhanced_code(),
835            "5.1.1"
836        );
837        assert_eq!(
838            SmtpStatusCode::PermanentFailure(551).enhanced_code(),
839            "5.1.6"
840        );
841        assert_eq!(
842            SmtpStatusCode::PermanentFailure(552).enhanced_code(),
843            "5.2.2"
844        );
845        assert_eq!(
846            SmtpStatusCode::PermanentFailure(553).enhanced_code(),
847            "5.1.3"
848        );
849        assert_eq!(
850            SmtpStatusCode::PermanentFailure(554).enhanced_code(),
851            "5.7.1"
852        );
853    }
854
855    #[test]
856    fn test_smtp_status_code_500_codes() {
857        assert_eq!(
858            SmtpStatusCode::PermanentFailure(500).enhanced_code(),
859            "5.5.2"
860        );
861        assert_eq!(
862            SmtpStatusCode::PermanentFailure(501).enhanced_code(),
863            "5.5.4"
864        );
865        assert_eq!(
866            SmtpStatusCode::PermanentFailure(502).enhanced_code(),
867            "5.5.1"
868        );
869        assert_eq!(
870            SmtpStatusCode::PermanentFailure(503).enhanced_code(),
871            "5.5.1"
872        );
873        assert_eq!(
874            SmtpStatusCode::PermanentFailure(504).enhanced_code(),
875            "5.5.4"
876        );
877    }
878
879    #[test]
880    fn test_dsn_human_text_permanent() {
881        let mut dsn = DeliveryStatusNotification::new("mail.example.com".to_string());
882        dsn.add_recipient(DsnRecipient {
883            final_recipient: "user@test.com".to_string(),
884            action: DsnAction::Failed,
885            status: "5.1.1".to_string(),
886            diagnostic_code: None,
887            remote_mta: None,
888            last_attempt_date: None,
889        });
890
891        let error_code = SmtpStatusCode::PermanentFailure(550);
892        let text = dsn.generate_human_text(error_code, "User unknown");
893
894        assert!(text.contains("COULD NOT BE DELIVERED"));
895        assert!(text.contains("user@test.com"));
896        assert!(text.contains("550"));
897        assert!(text.contains("User unknown"));
898        assert!(text.contains("No further delivery attempts"));
899    }
900
901    #[test]
902    fn test_dsn_human_text_temporary() {
903        let mut dsn = DeliveryStatusNotification::new("mail.example.com".to_string());
904        dsn.add_recipient(DsnRecipient {
905            final_recipient: "user@test.com".to_string(),
906            action: DsnAction::Delayed,
907            status: "4.2.1".to_string(),
908            diagnostic_code: None,
909            remote_mta: None,
910            last_attempt_date: None,
911        });
912
913        let error_code = SmtpStatusCode::TemporaryFailure(450);
914        let text = dsn.generate_human_text(error_code, "Mailbox unavailable");
915
916        assert!(text.contains("DELAYED"));
917        assert!(text.contains("user@test.com"));
918        assert!(text.contains("450"));
919        assert!(text.contains("Mailbox unavailable"));
920        assert!(text.contains("Delivery will be retried"));
921    }
922
923    #[tokio::test]
924    async fn test_multipart_report_structure() {
925        let mailet = BounceMailet::new();
926        let mail = Mail::new(
927            Some(MailAddress::from_str("sender@test.com").unwrap()),
928            vec![MailAddress::from_str("user@test.com").unwrap()],
929            MimeMessage::new(
930                HeaderMap::new(),
931                MessageBody::Small(Bytes::from("Test body")),
932            ),
933            None,
934            None,
935        );
936
937        let error_code = SmtpStatusCode::PermanentFailure(550);
938        let bounce = mailet.generate_bounce(&mail, error_code, "User unknown");
939
940        // Check RFC 3464 compliance
941        assert!(bounce.contains("MIME-Version: 1.0"));
942        assert!(bounce.contains("multipart/report"));
943        assert!(bounce.contains("report-type=delivery-status"));
944        assert!(bounce.contains("Content-Type: text/plain"));
945        assert!(bounce.contains("Content-Type: message/delivery-status"));
946    }
947
948    #[tokio::test]
949    async fn test_multipart_with_full_message() {
950        let mut mailet = BounceMailet::new();
951        mailet.include_body = true;
952
953        let mut mail = Mail::new(
954            Some(MailAddress::from_str("sender@test.com").unwrap()),
955            vec![MailAddress::from_str("user@test.com").unwrap()],
956            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
957            None,
958            None,
959        );
960        mail.set_attribute("message.body", "This is the original message body");
961
962        let error_code = SmtpStatusCode::PermanentFailure(550);
963        let bounce = mailet.generate_bounce(&mail, error_code, "User unknown");
964
965        assert!(bounce.contains("Content-Type: message/rfc822"));
966        assert!(bounce.contains("Undelivered Message"));
967        assert!(bounce.contains("original message body"));
968    }
969
970    #[tokio::test]
971    async fn test_multipart_headers_only() {
972        let mut mailet = BounceMailet::new();
973        mailet.include_headers = true;
974        mailet.include_body = false;
975
976        let mut mail = Mail::new(
977            Some(MailAddress::from_str("sender@test.com").unwrap()),
978            vec![MailAddress::from_str("user@test.com").unwrap()],
979            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
980            None,
981            None,
982        );
983        mail.set_attribute("header.Subject", "Test Subject");
984
985        let error_code = SmtpStatusCode::PermanentFailure(550);
986        let bounce = mailet.generate_bounce(&mail, error_code, "User unknown");
987
988        assert!(bounce.contains("Content-Type: text/rfc822-headers"));
989        assert!(bounce.contains("Undelivered Message Headers"));
990        assert!(bounce.contains("Subject: Test Subject"));
991    }
992
993    // Additional comprehensive tests for RFC 3464 compliance
994
995    #[test]
996    fn test_smtp_code_421_connection_timeout() {
997        let code = SmtpStatusCode::TemporaryFailure(421);
998        assert_eq!(code.enhanced_code(), "4.4.2");
999        assert!(!code.is_permanent());
1000    }
1001
1002    #[test]
1003    fn test_smtp_code_450_mailbox_unavailable() {
1004        let code = SmtpStatusCode::TemporaryFailure(450);
1005        assert_eq!(code.enhanced_code(), "4.2.1");
1006        assert!(!code.is_permanent());
1007    }
1008
1009    #[test]
1010    fn test_smtp_code_451_local_error() {
1011        let code = SmtpStatusCode::TemporaryFailure(451);
1012        assert_eq!(code.enhanced_code(), "4.3.0");
1013        assert!(!code.is_permanent());
1014    }
1015
1016    #[test]
1017    fn test_smtp_code_452_quota_exceeded_temp() {
1018        let code = SmtpStatusCode::TemporaryFailure(452);
1019        assert_eq!(code.enhanced_code(), "4.2.2");
1020        assert!(!code.is_permanent());
1021    }
1022
1023    #[test]
1024    fn test_smtp_code_554_policy_rejection() {
1025        let code = SmtpStatusCode::PermanentFailure(554);
1026        assert_eq!(code.enhanced_code(), "5.7.1");
1027        assert!(code.is_permanent());
1028    }
1029
1030    #[tokio::test]
1031    async fn test_bounce_quota_exceeded() {
1032        let mailet = BounceMailet::new();
1033        let mail = Mail::new(
1034            Some(MailAddress::from_str("sender@test.com").unwrap()),
1035            vec![MailAddress::from_str("user@test.com").unwrap()],
1036            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
1037            None,
1038            None,
1039        );
1040
1041        let error_code = SmtpStatusCode::PermanentFailure(552);
1042        let bounce = mailet.generate_bounce(&mail, error_code, "Quota exceeded");
1043
1044        assert!(bounce.contains("552"));
1045        assert!(bounce.contains("5.2.2"));
1046        assert!(bounce.contains("Quota exceeded"));
1047    }
1048
1049    #[tokio::test]
1050    async fn test_bounce_message_too_large() {
1051        let mailet = BounceMailet::new();
1052        let mail = Mail::new(
1053            Some(MailAddress::from_str("sender@test.com").unwrap()),
1054            vec![MailAddress::from_str("user@test.com").unwrap()],
1055            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
1056            None,
1057            None,
1058        );
1059
1060        let error_code = SmtpStatusCode::PermanentFailure(552);
1061        let bounce = mailet.generate_bounce(&mail, error_code, "Message too large");
1062
1063        assert!(bounce.contains("Message too large"));
1064        assert!(bounce.contains("5.2.2"));
1065    }
1066
1067    #[tokio::test]
1068    async fn test_bounce_spam_rejection() {
1069        let mailet = BounceMailet::new();
1070        let mail = Mail::new(
1071            Some(MailAddress::from_str("sender@test.com").unwrap()),
1072            vec![MailAddress::from_str("user@test.com").unwrap()],
1073            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
1074            None,
1075            None,
1076        );
1077
1078        let error_code = SmtpStatusCode::PermanentFailure(554);
1079        let bounce = mailet.generate_bounce(&mail, error_code, "Spam detected");
1080
1081        assert!(bounce.contains("Spam detected"));
1082        assert!(bounce.contains("5.7.1"));
1083    }
1084
1085    #[tokio::test]
1086    async fn test_bounce_virus_rejection() {
1087        let mailet = BounceMailet::new();
1088        let mail = Mail::new(
1089            Some(MailAddress::from_str("sender@test.com").unwrap()),
1090            vec![MailAddress::from_str("user@test.com").unwrap()],
1091            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
1092            None,
1093            None,
1094        );
1095
1096        let error_code = SmtpStatusCode::PermanentFailure(554);
1097        let bounce = mailet.generate_bounce(&mail, error_code, "Virus detected");
1098
1099        assert!(bounce.contains("Virus detected"));
1100        assert!(bounce.contains("5.7.1"));
1101    }
1102
1103    #[tokio::test]
1104    async fn test_bounce_relay_denied() {
1105        let mailet = BounceMailet::new();
1106        let mail = Mail::new(
1107            Some(MailAddress::from_str("sender@test.com").unwrap()),
1108            vec![MailAddress::from_str("user@test.com").unwrap()],
1109            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
1110            None,
1111            None,
1112        );
1113
1114        let error_code = SmtpStatusCode::PermanentFailure(554);
1115        let bounce = mailet.generate_bounce(&mail, error_code, "Relay access denied");
1116
1117        assert!(bounce.contains("Relay access denied"));
1118    }
1119
1120    #[tokio::test]
1121    async fn test_bounce_network_unreachable() {
1122        let mailet = BounceMailet::new();
1123        let mail = Mail::new(
1124            Some(MailAddress::from_str("sender@test.com").unwrap()),
1125            vec![MailAddress::from_str("user@test.com").unwrap()],
1126            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
1127            None,
1128            None,
1129        );
1130
1131        let error_code = SmtpStatusCode::TemporaryFailure(421);
1132        let bounce = mailet.generate_bounce(&mail, error_code, "Network unreachable");
1133
1134        assert!(bounce.contains("Network unreachable"));
1135        assert!(bounce.contains("4.4.2"));
1136    }
1137
1138    #[tokio::test]
1139    async fn test_bounce_invalid_address() {
1140        let mailet = BounceMailet::new();
1141        let mail = Mail::new(
1142            Some(MailAddress::from_str("sender@test.com").unwrap()),
1143            vec![MailAddress::from_str("user@test.com").unwrap()],
1144            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
1145            None,
1146            None,
1147        );
1148
1149        let error_code = SmtpStatusCode::PermanentFailure(553);
1150        let bounce = mailet.generate_bounce(&mail, error_code, "Invalid address");
1151
1152        assert!(bounce.contains("Invalid address"));
1153        assert!(bounce.contains("5.1.3"));
1154    }
1155
1156    #[tokio::test]
1157    async fn test_bounce_mailbox_moved() {
1158        let mailet = BounceMailet::new();
1159        let mail = Mail::new(
1160            Some(MailAddress::from_str("sender@test.com").unwrap()),
1161            vec![MailAddress::from_str("user@test.com").unwrap()],
1162            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
1163            None,
1164            None,
1165        );
1166
1167        let error_code = SmtpStatusCode::PermanentFailure(551);
1168        let bounce = mailet.generate_bounce(&mail, error_code, "Mailbox moved");
1169
1170        assert!(bounce.contains("Mailbox moved"));
1171        assert!(bounce.contains("5.1.6"));
1172    }
1173
1174    #[test]
1175    fn test_dsn_recipient_all_fields() {
1176        let recipient = DsnRecipient {
1177            final_recipient: "user@test.com".to_string(),
1178            action: DsnAction::Failed,
1179            status: "5.1.1".to_string(),
1180            diagnostic_code: Some("550 User unknown".to_string()),
1181            remote_mta: Some("mx.test.com".to_string()),
1182            last_attempt_date: Some(1609459200),
1183        };
1184
1185        assert_eq!(recipient.final_recipient, "user@test.com");
1186        assert_eq!(recipient.action, DsnAction::Failed);
1187        assert_eq!(recipient.status, "5.1.1");
1188        assert_eq!(
1189            recipient.diagnostic_code,
1190            Some("550 User unknown".to_string())
1191        );
1192        assert_eq!(recipient.remote_mta, Some("mx.test.com".to_string()));
1193        assert_eq!(recipient.last_attempt_date, Some(1609459200));
1194    }
1195
1196    #[test]
1197    fn test_dsn_generation_with_all_recipients() {
1198        let mut dsn = DeliveryStatusNotification::new("mail.example.com".to_string());
1199
1200        for i in 1..=3 {
1201            dsn.add_recipient(DsnRecipient {
1202                final_recipient: format!("user{}@test.com", i),
1203                action: DsnAction::Failed,
1204                status: "5.1.1".to_string(),
1205                diagnostic_code: Some("550 User unknown".to_string()),
1206                remote_mta: None,
1207                last_attempt_date: None,
1208            });
1209        }
1210
1211        let body = dsn.generate_message_body();
1212        assert!(body.contains("user1@test.com"));
1213        assert!(body.contains("user2@test.com"));
1214        assert!(body.contains("user3@test.com"));
1215    }
1216
1217    #[tokio::test]
1218    async fn test_bounce_all_headers_present() {
1219        let mailet = BounceMailet::new();
1220        let mut mail = Mail::new(
1221            Some(MailAddress::from_str("sender@test.com").unwrap()),
1222            vec![MailAddress::from_str("user@test.com").unwrap()],
1223            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
1224            None,
1225            None,
1226        );
1227
1228        mail.set_attribute("header.Subject", "Test Subject");
1229        mail.set_attribute("header.From", "sender@test.com");
1230        mail.set_attribute("header.To", "recipient@test.com");
1231        mail.set_attribute("header.Date", "Mon, 1 Jan 2024 00:00:00 +0000");
1232        mail.set_attribute("header.Cc", "cc@test.com");
1233        mail.set_attribute("header.Message-ID", "<test@example.com>");
1234
1235        let error_code = SmtpStatusCode::PermanentFailure(550);
1236        let bounce = mailet.generate_bounce(&mail, error_code, "User unknown");
1237
1238        assert!(bounce.contains("Subject: Test Subject"));
1239        assert!(bounce.contains("From: sender@test.com"));
1240        assert!(bounce.contains("To: recipient@test.com"));
1241        assert!(bounce.contains("Date: Mon, 1 Jan 2024 00:00:00 +0000"));
1242        assert!(bounce.contains("Cc: cc@test.com"));
1243        assert!(bounce.contains("Message-ID: <test@example.com>"));
1244    }
1245
1246    #[tokio::test]
1247    async fn test_bounce_rfc3464_multipart_structure() {
1248        let mailet = BounceMailet::new();
1249        let mail = Mail::new(
1250            Some(MailAddress::from_str("sender@test.com").unwrap()),
1251            vec![MailAddress::from_str("user@test.com").unwrap()],
1252            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
1253            None,
1254            None,
1255        );
1256
1257        let error_code = SmtpStatusCode::PermanentFailure(550);
1258        let bounce = mailet.generate_bounce(&mail, error_code, "User unknown");
1259
1260        // RFC 3464 requires specific structure
1261        assert!(bounce.contains("MIME-Version: 1.0"));
1262        assert!(bounce.contains("multipart/report"));
1263        assert!(bounce.contains("report-type=delivery-status"));
1264        assert!(bounce.contains("boundary="));
1265
1266        // Part 1: Human-readable
1267        assert!(bounce.contains("Content-Type: text/plain"));
1268        assert!(bounce.contains("Content-Description: Notification"));
1269
1270        // Part 2: Machine-readable
1271        assert!(bounce.contains("Content-Type: message/delivery-status"));
1272        assert!(bounce.contains("Content-Description: Delivery Report"));
1273
1274        // Part 3: Original headers
1275        assert!(bounce.contains("Content-Type: text/rfc822-headers"));
1276        assert!(bounce.contains("Content-Description: Undelivered Message Headers"));
1277    }
1278
1279    #[test]
1280    fn test_smtp_status_code_edge_cases() {
1281        // Test boundary values
1282        assert_eq!(
1283            SmtpStatusCode::from_code(400),
1284            Some(SmtpStatusCode::TemporaryFailure(400))
1285        );
1286        assert_eq!(
1287            SmtpStatusCode::from_code(499),
1288            Some(SmtpStatusCode::TemporaryFailure(499))
1289        );
1290        assert_eq!(
1291            SmtpStatusCode::from_code(500),
1292            Some(SmtpStatusCode::PermanentFailure(500))
1293        );
1294        assert_eq!(
1295            SmtpStatusCode::from_code(599),
1296            Some(SmtpStatusCode::PermanentFailure(599))
1297        );
1298
1299        // Invalid codes
1300        assert_eq!(SmtpStatusCode::from_code(200), None);
1301        assert_eq!(SmtpStatusCode::from_code(300), None);
1302        assert_eq!(SmtpStatusCode::from_code(600), None);
1303    }
1304
1305    #[tokio::test]
1306    async fn test_bounce_default_values() {
1307        let mailet = BounceMailet::default();
1308        assert_eq!(mailet.reporting_mta, "localhost");
1309        assert_eq!(mailet.postmaster, "postmaster@localhost");
1310        assert!(mailet.include_headers);
1311        assert!(!mailet.include_body);
1312        assert_eq!(mailet.max_body_size, 1024);
1313    }
1314
1315    #[tokio::test]
1316    async fn test_bounce_empty_body() {
1317        let mut mailet = BounceMailet::new();
1318        mailet.include_body = true;
1319
1320        let mail = Mail::new(
1321            Some(MailAddress::from_str("sender@test.com").unwrap()),
1322            vec![MailAddress::from_str("user@test.com").unwrap()],
1323            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from(""))),
1324            None,
1325            None,
1326        );
1327
1328        let error_code = SmtpStatusCode::PermanentFailure(550);
1329        let bounce = mailet.generate_bounce(&mail, error_code, "User unknown");
1330
1331        // Should not crash with empty body
1332        assert!(bounce.contains("User unknown"));
1333    }
1334
1335    #[test]
1336    fn test_dsn_action_debug() {
1337        let action = DsnAction::Failed;
1338        let debug_str = format!("{:?}", action);
1339        assert!(debug_str.contains("Failed"));
1340    }
1341
1342    #[test]
1343    fn test_smtp_status_code_debug() {
1344        let code = SmtpStatusCode::PermanentFailure(550);
1345        let debug_str = format!("{:?}", code);
1346        assert!(debug_str.contains("550"));
1347    }
1348
1349    #[tokio::test]
1350    async fn test_dsn_action_relayed() {
1351        assert_eq!(DsnAction::Relayed.as_str(), "relayed");
1352    }
1353
1354    #[tokio::test]
1355    async fn test_dsn_action_expanded() {
1356        assert_eq!(DsnAction::Expanded.as_str(), "expanded");
1357    }
1358
1359    #[tokio::test]
1360    async fn test_dsn_with_cc_header() {
1361        let mailet = BounceMailet::new();
1362        let mut mail = Mail::new(
1363            Some(MailAddress::from_str("sender@test.com").unwrap()),
1364            vec![MailAddress::from_str("user@test.com").unwrap()],
1365            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
1366            None,
1367            None,
1368        );
1369        mail.set_attribute("header.Cc", "cc@test.com");
1370
1371        let error_code = SmtpStatusCode::PermanentFailure(550);
1372        let bounce = mailet.generate_bounce(&mail, error_code, "User unknown");
1373
1374        assert!(bounce.contains("Cc: cc@test.com"));
1375    }
1376
1377    #[tokio::test]
1378    async fn test_dsn_with_message_id_header() {
1379        let mailet = BounceMailet::new();
1380        let mut mail = Mail::new(
1381            Some(MailAddress::from_str("sender@test.com").unwrap()),
1382            vec![MailAddress::from_str("user@test.com").unwrap()],
1383            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
1384            None,
1385            None,
1386        );
1387        mail.set_attribute("header.Message-ID", "<12345@test.com>");
1388
1389        let error_code = SmtpStatusCode::PermanentFailure(550);
1390        let bounce = mailet.generate_bounce(&mail, error_code, "User unknown");
1391
1392        assert!(bounce.contains("Message-ID: <12345@test.com>"));
1393    }
1394
1395    #[test]
1396    fn test_format_date_rfc2822() {
1397        let dsn = DeliveryStatusNotification::new("mail.example.com".to_string());
1398        let timestamp = 1609459200_u64; // 2021-01-01 00:00:00 UTC
1399        let formatted = dsn.format_date(timestamp);
1400
1401        // Should be RFC 2822 format
1402        assert!(formatted.contains("2021") || formatted.contains("timestamp:"));
1403    }
1404
1405    #[test]
1406    fn test_format_date_invalid() {
1407        let dsn = DeliveryStatusNotification::new("mail.example.com".to_string());
1408        let timestamp = i64::MAX as u64; // timestamp that may fail conversion
1409        let formatted = dsn.format_date(timestamp);
1410
1411        // Should either format correctly or fall back to simple format
1412        assert!(!formatted.is_empty());
1413    }
1414
1415    #[tokio::test]
1416    async fn test_bounce_boundary_string() {
1417        let mailet = BounceMailet::new();
1418        let mail = Mail::new(
1419            Some(MailAddress::from_str("sender@test.com").unwrap()),
1420            vec![MailAddress::from_str("user@test.com").unwrap()],
1421            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
1422            None,
1423            None,
1424        );
1425
1426        let error_code = SmtpStatusCode::PermanentFailure(550);
1427        let bounce = mailet.generate_bounce(&mail, error_code, "User unknown");
1428
1429        // Check for proper boundary markers
1430        assert!(bounce.contains("boundary=\"rusmes-dsn-boundary"));
1431        assert!(bounce.contains("--rusmes-dsn-boundary"));
1432    }
1433
1434    #[test]
1435    fn test_smtp_status_code_code_method() {
1436        let temp = SmtpStatusCode::TemporaryFailure(450);
1437        assert_eq!(temp.code(), 450);
1438
1439        let perm = SmtpStatusCode::PermanentFailure(550);
1440        assert_eq!(perm.code(), 550);
1441    }
1442
1443    #[test]
1444    fn test_dsn_recipient_clone() {
1445        let recipient = DsnRecipient {
1446            final_recipient: "user@test.com".to_string(),
1447            action: DsnAction::Failed,
1448            status: "5.1.1".to_string(),
1449            diagnostic_code: Some("550 User unknown".to_string()),
1450            remote_mta: None,
1451            last_attempt_date: None,
1452        };
1453
1454        let cloned = recipient.clone();
1455        assert_eq!(cloned.final_recipient, recipient.final_recipient);
1456        assert_eq!(cloned.action, recipient.action);
1457    }
1458
1459    #[tokio::test]
1460    async fn test_bounce_multiple_headers() {
1461        let mailet = BounceMailet::new();
1462        let mut mail = Mail::new(
1463            Some(MailAddress::from_str("sender@test.com").unwrap()),
1464            vec![MailAddress::from_str("user@test.com").unwrap()],
1465            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
1466            None,
1467            None,
1468        );
1469        mail.set_attribute("header.Subject", "Important Message");
1470        mail.set_attribute("header.From", "sender@test.com");
1471        mail.set_attribute("header.To", "user@test.com");
1472        mail.set_attribute("header.Date", "Mon, 1 Jan 2024 00:00:00 +0000");
1473
1474        let error_code = SmtpStatusCode::PermanentFailure(550);
1475        let bounce = mailet.generate_bounce(&mail, error_code, "User unknown");
1476
1477        assert!(bounce.contains("Subject: Important Message"));
1478        assert!(bounce.contains("From: sender@test.com"));
1479        assert!(bounce.contains("To: user@test.com"));
1480        assert!(bounce.contains("Date: Mon, 1 Jan 2024 00:00:00 +0000"));
1481    }
1482}