oci_api/services/email/
models.rs

1//! Email Delivery API data models
2
3use serde::{Deserialize, Serialize};
4
5/// Email Configuration response
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct EmailConfiguration {
8    /// Compartment OCID
9    #[serde(rename = "compartmentId")]
10    pub compartment_id: String,
11
12    /// HTTP Submit endpoint
13    #[serde(rename = "httpSubmitEndpoint")]
14    pub http_submit_endpoint: String,
15
16    /// SMTP Submit endpoint
17    #[serde(rename = "smtpSubmitEndpoint")]
18    pub smtp_submit_endpoint: String,
19
20    /// Email Delivery Config ID (optional, can be null)
21    #[serde(rename = "emailDeliveryConfigId")]
22    pub email_delivery_config_id: Option<String>,
23}
24
25/// Email message
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct Email {
28    /// Message ID (optional)
29    #[serde(rename = "messageId", skip_serializing_if = "Option::is_none")]
30    pub message_id: Option<String>,
31
32    /// Sender
33    pub sender: Sender,
34
35    /// Recipients
36    pub recipients: Recipients,
37
38    /// Subject
39    pub subject: String,
40
41    /// Body (HTML)
42    #[serde(rename = "bodyHtml", skip_serializing_if = "Option::is_none")]
43    pub body_html: Option<String>,
44
45    /// Body (Plain Text)
46    #[serde(rename = "bodyText", skip_serializing_if = "Option::is_none")]
47    pub body_text: Option<String>,
48
49    /// Reply-To address (optional)
50    #[serde(rename = "replyTo", skip_serializing_if = "Option::is_none")]
51    pub reply_to: Option<Vec<EmailAddress>>,
52
53    /// Custom headers (optional)
54    #[serde(rename = "headerFields", skip_serializing_if = "Option::is_none")]
55    pub headers: Option<std::collections::HashMap<String, String>>,
56}
57
58/// Sender information
59#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
60pub struct Sender {
61    /// Sender email address
62    #[serde(rename = "senderAddress")]
63    pub sender_address: EmailAddress,
64
65    /// Compartment OCID
66    #[serde(rename = "compartmentId")]
67    pub compartment_id: String,
68}
69
70impl Sender {
71    /// Create new sender (compartment_id will be set by EmailClient)
72    pub fn new(email: impl Into<String>) -> Self {
73        Self {
74            sender_address: EmailAddress::new(email),
75            compartment_id: String::new(), // Will be set by EmailClient
76        }
77    }
78
79    /// Create sender with name (compartment_id will be set by EmailClient)
80    pub fn with_name(email: impl Into<String>, name: impl Into<String>) -> Self {
81        Self {
82            sender_address: EmailAddress::with_name(email, name),
83            compartment_id: String::new(), // Will be set by EmailClient
84        }
85    }
86
87    /// Internal method to set compartment_id (used by EmailClient)
88    pub(crate) fn set_compartment_id(&mut self, compartment_id: impl Into<String>) {
89        self.compartment_id = compartment_id.into();
90    }
91}
92
93/// Email address
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct EmailAddress {
96    /// Email address
97    pub email: String,
98
99    /// Name (optional)
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub name: Option<String>,
102}
103
104// Implement PartialEq based on email only (ignore name for equality)
105impl PartialEq for EmailAddress {
106    fn eq(&self, other: &Self) -> bool {
107        self.email == other.email
108    }
109}
110
111impl Eq for EmailAddress {}
112
113// Implement Hash based on email only (ignore name for hashing)
114impl std::hash::Hash for EmailAddress {
115    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
116        self.email.hash(state);
117    }
118}
119
120/// Recipients list
121#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
122pub struct Recipients {
123    /// To recipients
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub to: Option<Vec<EmailAddress>>,
126
127    /// CC recipients
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub cc: Option<Vec<EmailAddress>>,
130
131    /// BCC recipients
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub bcc: Option<Vec<EmailAddress>>,
134}
135
136/// Email submission response
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct SubmitEmailResponse {
139    /// Submitted email's message ID
140    #[serde(rename = "messageId")]
141    pub message_id: String,
142
143    /// Envelope ID (not envelopeMessageId as in docs)
144    #[serde(rename = "envelopeId")]
145    pub envelope_id: String,
146
147    /// Suppressed recipients (optional)
148    #[serde(
149        rename = "suppressedRecipients",
150        skip_serializing_if = "Option::is_none"
151    )]
152    pub suppressed_recipients: Option<Vec<EmailAddress>>,
153}
154
155/// Sender summary from list_senders API
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct SenderSummary {
158    /// Sender OCID
159    pub id: String,
160
161    /// Email address
162    #[serde(rename = "emailAddress")]
163    pub email_address: String,
164
165    /// Lifecycle state
166    #[serde(rename = "lifecycleState")]
167    pub lifecycle_state: SenderLifecycleState,
168
169    /// Time created
170    #[serde(rename = "timeCreated")]
171    pub time_created: String,
172
173    /// Is SPF (Sender Policy Framework) configured (optional)
174    #[serde(rename = "isSpf", skip_serializing_if = "Option::is_none")]
175    pub is_spf: Option<bool>,
176
177    /// Compartment ID (optional, not always included)
178    #[serde(rename = "compartmentId", skip_serializing_if = "Option::is_none")]
179    pub compartment_id: Option<String>,
180}
181
182/// Sender lifecycle state
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
185pub enum SenderLifecycleState {
186    /// Creating
187    Creating,
188    /// Active
189    Active,
190    /// Needs attention
191    NeedsAttention,
192    /// Inactive
193    Inactive,
194    /// Failed
195    Failed,
196    /// Deleting
197    Deleting,
198    /// Deleted
199    Deleted,
200}
201
202impl EmailAddress {
203    /// Create new email address
204    pub fn new(email: impl Into<String>) -> Self {
205        Self {
206            email: email.into(),
207            name: None,
208        }
209    }
210
211    /// Create email address with name
212    pub fn with_name(email: impl Into<String>, name: impl Into<String>) -> Self {
213        Self {
214            email: email.into(),
215            name: Some(name.into()),
216        }
217    }
218}
219
220impl Recipients {
221    /// Remove duplicates from email address list
222    fn deduplicate(addresses: Vec<EmailAddress>) -> Vec<EmailAddress> {
223        use std::collections::HashSet;
224        let mut seen = HashSet::new();
225        addresses
226            .into_iter()
227            .filter(|addr| seen.insert(addr.clone()))
228            .collect()
229    }
230
231    /// Create recipients list with To recipients (alias for `to`)
232    pub fn new(addresses: Vec<EmailAddress>) -> Self {
233        Self::to(addresses)
234    }
235
236    /// Create recipients list with only To recipients
237    pub fn to(addresses: Vec<EmailAddress>) -> Self {
238        Self {
239            to: Some(Self::deduplicate(addresses)),
240            cc: None,
241            bcc: None,
242        }
243    }
244
245    /// Create recipients list with only CC recipients
246    pub fn cc(addresses: Vec<EmailAddress>) -> Self {
247        Self {
248            to: None,
249            cc: Some(Self::deduplicate(addresses)),
250            bcc: None,
251        }
252    }
253
254    /// Create recipients list with only BCC recipients
255    pub fn bcc(addresses: Vec<EmailAddress>) -> Self {
256        Self {
257            to: None,
258            cc: None,
259            bcc: Some(Self::deduplicate(addresses)),
260        }
261    }
262
263    /// Add To recipients to existing Recipients
264    pub fn add_to(mut self, mut addresses: Vec<EmailAddress>) -> Self {
265        if let Some(ref mut to) = self.to {
266            to.append(&mut addresses);
267            *to = Self::deduplicate(to.clone());
268        } else {
269            self.to = Some(Self::deduplicate(addresses));
270        }
271        self
272    }
273
274    /// Add CC recipients to existing Recipients
275    pub fn add_cc(mut self, mut addresses: Vec<EmailAddress>) -> Self {
276        if let Some(ref mut cc) = self.cc {
277            cc.append(&mut addresses);
278            *cc = Self::deduplicate(cc.clone());
279        } else {
280            self.cc = Some(Self::deduplicate(addresses));
281        }
282        self
283    }
284
285    /// Add BCC recipients to existing Recipients
286    pub fn add_bcc(mut self, mut addresses: Vec<EmailAddress>) -> Self {
287        if let Some(ref mut bcc) = self.bcc {
288            bcc.append(&mut addresses);
289            *bcc = Self::deduplicate(bcc.clone());
290        } else {
291            self.bcc = Some(Self::deduplicate(addresses));
292        }
293        self
294    }
295
296    /// Create a new builder for Recipients
297    pub fn builder() -> RecipientsBuilder {
298        RecipientsBuilder::default()
299    }
300}
301
302/// Builder for Recipients
303#[derive(Debug, Default)]
304pub struct RecipientsBuilder {
305    to: Option<Vec<EmailAddress>>,
306    cc: Option<Vec<EmailAddress>>,
307    bcc: Option<Vec<EmailAddress>>,
308}
309
310impl RecipientsBuilder {
311    /// Set To recipients
312    pub fn to(mut self, addresses: Vec<EmailAddress>) -> Self {
313        self.to = Some(Recipients::deduplicate(addresses));
314        self
315    }
316
317    /// Set CC recipients
318    pub fn cc(mut self, addresses: Vec<EmailAddress>) -> Self {
319        self.cc = Some(Recipients::deduplicate(addresses));
320        self
321    }
322
323    /// Set BCC recipients
324    pub fn bcc(mut self, addresses: Vec<EmailAddress>) -> Self {
325        self.bcc = Some(Recipients::deduplicate(addresses));
326        self
327    }
328
329    /// Build Recipients
330    pub fn build(self) -> Recipients {
331        Recipients {
332            to: self.to,
333            cc: self.cc,
334            bcc: self.bcc,
335        }
336    }
337}
338
339impl Email {
340    /// Create a new builder for Email
341    pub fn builder() -> EmailBuilder {
342        EmailBuilder::default()
343    }
344}
345
346/// Builder for Email
347#[derive(Debug, Default)]
348pub struct EmailBuilder {
349    message_id: Option<String>,
350    sender: Option<EmailAddress>,
351    recipients: Option<Recipients>,
352    subject: Option<String>,
353    body_html: Option<String>,
354    body_text: Option<String>,
355    reply_to: Option<Vec<EmailAddress>>,
356    headers: Option<std::collections::HashMap<String, String>>,
357}
358
359impl EmailBuilder {
360    /// Set message ID
361    pub fn message_id(mut self, message_id: impl Into<String>) -> Self {
362        self.message_id = Some(message_id.into());
363        self
364    }
365
366    /// Set sender email address
367    pub fn sender(mut self, sender: EmailAddress) -> Self {
368        self.sender = Some(sender);
369        self
370    }
371
372    /// Set recipients
373    pub fn recipients(mut self, recipients: Recipients) -> Self {
374        self.recipients = Some(recipients);
375        self
376    }
377
378    /// Set subject
379    pub fn subject(mut self, subject: impl Into<String>) -> Self {
380        self.subject = Some(subject.into());
381        self
382    }
383
384    /// Set HTML body
385    pub fn body_html(mut self, body_html: impl Into<String>) -> Self {
386        self.body_html = Some(body_html.into());
387        self
388    }
389
390    /// Set plain text body
391    pub fn body_text(mut self, body_text: impl Into<String>) -> Self {
392        self.body_text = Some(body_text.into());
393        self
394    }
395
396    /// Set reply-to addresses
397    pub fn reply_to(mut self, reply_to: Vec<EmailAddress>) -> Self {
398        self.reply_to = Some(reply_to);
399        self
400    }
401
402    /// Set custom headers
403    pub fn headers(mut self, headers: std::collections::HashMap<String, String>) -> Self {
404        self.headers = Some(headers);
405        self
406    }
407
408    /// Build Email
409    ///
410    /// Returns an error if required fields are missing or invalid
411    pub fn build(self) -> crate::error::Result<Email> {
412        let sender_address = self
413            .sender
414            .ok_or_else(|| crate::error::Error::ConfigError("Sender is required".to_string()))?;
415
416        // Create Sender with empty compartment_id (will be set by send)
417        let sender = Sender {
418            sender_address,
419            compartment_id: String::new(),
420        };
421
422        let recipients = self.recipients.ok_or_else(|| {
423            crate::error::Error::ConfigError("Recipients are required".to_string())
424        })?;
425
426        let subject = self.subject.ok_or_else(|| {
427            crate::error::Error::ConfigError("Subject is required".to_string())
428        })?;
429
430        // Validate that at least one body (HTML or text) is provided
431        if self.body_html.is_none() && self.body_text.is_none() {
432            return Err(crate::error::Error::ConfigError(
433                "At least one of body_html or body_text is required".to_string(),
434            ));
435        }
436
437        Ok(Email {
438            message_id: self.message_id,
439            sender,
440            recipients,
441            subject,
442            body_html: self.body_html,
443            body_text: self.body_text,
444            reply_to: self.reply_to,
445            headers: self.headers,
446        })
447    }
448}
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453
454    #[test]
455    fn test_email_address_new() {
456        let addr = EmailAddress::new("test@example.com");
457        assert_eq!(addr.email, "test@example.com");
458        assert_eq!(addr.name, None);
459    }
460
461    #[test]
462    fn test_email_address_with_name() {
463        let addr = EmailAddress::with_name("test@example.com", "Test User");
464        assert_eq!(addr.email, "test@example.com");
465        assert_eq!(addr.name, Some("Test User".to_string()));
466    }
467
468    #[test]
469    fn test_recipients_to() {
470        let recipients = Recipients::to(vec![
471            EmailAddress::new("user1@example.com"),
472            EmailAddress::new("user2@example.com"),
473        ]);
474        assert_eq!(recipients.to.as_ref().unwrap().len(), 2);
475        assert_eq!(recipients.cc, None);
476        assert_eq!(recipients.bcc, None);
477    }
478
479    #[test]
480    fn test_recipients_builder() {
481        let recipients = Recipients::builder()
482            .to(vec![EmailAddress::new("to@example.com")])
483            .cc(vec![EmailAddress::new("cc@example.com")])
484            .bcc(vec![EmailAddress::new("bcc@example.com")])
485            .build();
486
487        assert_eq!(recipients.to.as_ref().unwrap().len(), 1);
488        assert_eq!(recipients.cc.as_ref().unwrap().len(), 1);
489        assert_eq!(recipients.bcc.as_ref().unwrap().len(), 1);
490    }
491
492    #[test]
493    fn test_submit_email_request_serialization() {
494        let mut request = Email {
495            message_id: Some("test-123".to_string()),
496            sender: Sender::with_name("sender@example.com", "Sender"),
497            recipients: Recipients::to(vec![EmailAddress::new("recipient@example.com")]),
498            subject: "Test Subject".to_string(),
499            body_html: Some("<html><body>Test</body></html>".to_string()),
500            body_text: Some("Test".to_string()),
501            reply_to: None,
502            headers: None,
503        };
504        // Set compartment_id manually for test
505        request.sender.set_compartment_id("ocid1.compartment.test");
506
507        let json = serde_json::to_string(&request).unwrap();
508        assert!(json.contains("\"sender\""));
509        assert!(json.contains("\"recipients\""));
510        assert!(json.contains("\"subject\""));
511        assert!(json.contains("\"messageId\""));
512    }
513
514    #[test]
515    fn test_submit_email_request_builder() {
516        let mut request = Email::builder()
517            .sender(EmailAddress::new("sender@example.com"))
518            .recipients(
519                Recipients::builder()
520                    .to(vec![EmailAddress::new("recipient@example.com")])
521                    .build(),
522            )
523            .subject("Test Subject")
524            .body_text("Test body")
525            .build()
526            .unwrap();
527        // Set compartment_id manually for test
528        request.sender.set_compartment_id("ocid1.compartment.test");
529
530        assert_eq!(request.subject, "Test Subject");
531        assert_eq!(request.body_text.as_ref().unwrap(), "Test body");
532        assert!(request.recipients.to.is_some());
533    }
534
535    #[test]
536    fn test_submit_email_request_builder_missing_required_fields() {
537        // Missing sender
538        let result = Email::builder()
539            .recipients(Recipients::to(vec![EmailAddress::new("to@example.com")]))
540            .subject("Test")
541            .build();
542        assert!(result.is_err());
543
544        // Missing recipients
545        let result = Email::builder()
546            .sender(EmailAddress::new("sender@example.com"))
547            .subject("Test")
548            .build();
549        assert!(result.is_err());
550
551        // Missing subject
552        let result = Email::builder()
553            .sender(EmailAddress::new("sender@example.com"))
554            .recipients(Recipients::to(vec![EmailAddress::new("to@example.com")]))
555            .build();
556        assert!(result.is_err());
557    }
558
559    #[test]
560    fn test_email_configuration_deserialization() {
561        let json = r#"{
562            "compartmentId": "ocid1.compartment.test",
563            "httpSubmitEndpoint": "https://email.ap-seoul-1.oci.oraclecloud.com",
564            "smtpSubmitEndpoint": "smtp.email.ap-seoul-1.oci.oraclecloud.com"
565        }"#;
566
567        let config: EmailConfiguration = serde_json::from_str(json).unwrap();
568        assert_eq!(config.compartment_id, "ocid1.compartment.test");
569        assert_eq!(
570            config.http_submit_endpoint,
571            "https://email.ap-seoul-1.oci.oraclecloud.com"
572        );
573        assert_eq!(
574            config.smtp_submit_endpoint,
575            "smtp.email.ap-seoul-1.oci.oraclecloud.com"
576        );
577    }
578
579    #[test]
580    fn test_submit_email_response_deserialization() {
581        let json = r#"{
582            "messageId": "msg-123",
583            "envelopeId": "env-456"
584        }"#;
585
586        let response: SubmitEmailResponse = serde_json::from_str(json).unwrap();
587        assert_eq!(response.message_id, "msg-123");
588        assert_eq!(response.envelope_id, "env-456");
589    }
590
591    #[test]
592    fn test_complete_email_request_with_all_fields() {
593        use std::collections::HashMap;
594
595        let mut headers = HashMap::new();
596        headers.insert("X-Test".to_string(), "test-value".to_string());
597
598        let mut request = Email {
599            message_id: Some("msg-001".to_string()),
600            sender: Sender::with_name("sender@example.com", "Sender Name"),
601            recipients: Recipients {
602                to: Some(vec![EmailAddress::new("to@example.com")]),
603                cc: Some(vec![EmailAddress::new("cc@example.com")]),
604                bcc: Some(vec![EmailAddress::new("bcc@example.com")]),
605            },
606            subject: "Complete Test".to_string(),
607            body_html: Some("<p>HTML body</p>".to_string()),
608            body_text: Some("Text body".to_string()),
609            reply_to: Some(vec![EmailAddress::new("replyto@example.com")]),
610            headers: Some(headers),
611        };
612        request.sender.set_compartment_id("ocid1.compartment.test");
613
614        let json = serde_json::to_string(&request).unwrap();
615        let deserialized: Email = serde_json::from_str(&json).unwrap();
616
617        assert_eq!(deserialized.message_id, Some("msg-001".to_string()));
618        assert_eq!(deserialized.subject, "Complete Test");
619        assert!(deserialized.recipients.to.is_some());
620        assert!(deserialized.recipients.cc.is_some());
621        assert!(deserialized.recipients.bcc.is_some());
622        assert!(deserialized.reply_to.is_some());
623        assert!(deserialized.headers.is_some());
624    }
625
626    #[test]
627    fn test_complete_email_request_with_builder() {
628        use std::collections::HashMap;
629
630        let mut headers = HashMap::new();
631        headers.insert("X-Test".to_string(), "test-value".to_string());
632
633        let mut request = Email::builder()
634            .message_id("msg-001")
635            .sender(EmailAddress::with_name("sender@example.com", "Sender Name"))
636            .recipients(
637                Recipients::builder()
638                    .to(vec![EmailAddress::new("to@example.com")])
639                    .cc(vec![EmailAddress::new("cc@example.com")])
640                    .bcc(vec![EmailAddress::new("bcc@example.com")])
641                    .build(),
642            )
643            .subject("Complete Test")
644            .body_html("<p>HTML body</p>")
645            .body_text("Text body")
646            .reply_to(vec![EmailAddress::new("replyto@example.com")])
647            .headers(headers)
648            .build()
649            .unwrap();
650        request.sender.set_compartment_id("ocid1.compartment.test");
651
652        assert_eq!(request.message_id, Some("msg-001".to_string()));
653        assert_eq!(request.subject, "Complete Test");
654        assert!(request.recipients.to.is_some());
655        assert!(request.recipients.cc.is_some());
656        assert!(request.recipients.bcc.is_some());
657        assert!(request.reply_to.is_some());
658        assert!(request.headers.is_some());
659    }
660
661    #[test]
662    fn test_recipients_constructors() {
663        // Test new() - should be same as to()
664        let recipients = Recipients::new(vec![EmailAddress::new("to@example.com")]);
665        assert!(recipients.to.is_some());
666        assert!(recipients.cc.is_none());
667        assert!(recipients.bcc.is_none());
668
669        // Test to()
670        let recipients = Recipients::to(vec![EmailAddress::new("to@example.com")]);
671        assert!(recipients.to.is_some());
672        assert!(recipients.cc.is_none());
673        assert!(recipients.bcc.is_none());
674
675        // Test cc()
676        let recipients = Recipients::cc(vec![EmailAddress::new("cc@example.com")]);
677        assert!(recipients.to.is_none());
678        assert!(recipients.cc.is_some());
679        assert!(recipients.bcc.is_none());
680
681        // Test bcc()
682        let recipients = Recipients::bcc(vec![EmailAddress::new("bcc@example.com")]);
683        assert!(recipients.to.is_none());
684        assert!(recipients.cc.is_none());
685        assert!(recipients.bcc.is_some());
686    }
687
688    #[test]
689    fn test_recipients_add_methods() {
690        // Start with TO recipients
691        let recipients = Recipients::to(vec![EmailAddress::new("to1@example.com")])
692            .add_to(vec![EmailAddress::new("to2@example.com")])
693            .add_cc(vec![EmailAddress::new("cc@example.com")])
694            .add_bcc(vec![EmailAddress::new("bcc@example.com")]);
695
696        assert_eq!(recipients.to.as_ref().unwrap().len(), 2);
697        assert_eq!(recipients.cc.as_ref().unwrap().len(), 1);
698        assert_eq!(recipients.bcc.as_ref().unwrap().len(), 1);
699
700        // Test adding to existing CC
701        let recipients = Recipients::cc(vec![EmailAddress::new("cc1@example.com")]).add_cc(vec![
702            EmailAddress::new("cc2@example.com"),
703            EmailAddress::new("cc3@example.com"),
704        ]);
705
706        assert_eq!(recipients.cc.as_ref().unwrap().len(), 3);
707
708        // Test adding when field is None
709        let recipients = Recipients::to(vec![EmailAddress::new("to@example.com")])
710            .add_bcc(vec![EmailAddress::new("bcc@example.com")]);
711
712        assert!(recipients.to.is_some());
713        assert!(recipients.bcc.is_some());
714    }
715
716    #[test]
717    fn test_build_missing_body() {
718        // Missing both body_html and body_text should fail
719        let result = Email::builder()
720            .sender(EmailAddress::new("sender@example.com"))
721            .recipients(Recipients::to(vec![EmailAddress::new("to@example.com")]))
722            .subject("Test")
723            .build();
724
725        assert!(result.is_err());
726        if let Err(crate::error::Error::ConfigError(msg)) = result {
727            assert!(msg.contains("body"));
728        } else {
729            panic!("Expected ConfigError about body");
730        }
731    }
732
733    #[test]
734    fn test_build_with_only_html_body() {
735        // Only body_html should be OK
736        let result = Email::builder()
737            .sender(EmailAddress::new("sender@example.com"))
738            .recipients(Recipients::to(vec![EmailAddress::new("to@example.com")]))
739            .subject("Test")
740            .body_html("<p>HTML content</p>")
741            .build();
742
743        assert!(result.is_ok());
744        let request = result.unwrap();
745        assert!(request.body_html.is_some());
746        assert!(request.body_text.is_none());
747    }
748
749    #[test]
750    fn test_build_with_only_text_body() {
751        // Only body_text should be OK
752        let result = Email::builder()
753            .sender(EmailAddress::new("sender@example.com"))
754            .recipients(Recipients::to(vec![EmailAddress::new("to@example.com")]))
755            .subject("Test")
756            .body_text("Text content")
757            .build();
758
759        assert!(result.is_ok());
760        let request = result.unwrap();
761        assert!(request.body_html.is_none());
762        assert!(request.body_text.is_some());
763    }
764
765    #[test]
766    fn test_recipients_deduplication() {
767        // Test TO deduplication
768        let recipients = Recipients::to(vec![
769            EmailAddress::new("user@example.com"),
770            EmailAddress::new("user@example.com"), // duplicate
771            EmailAddress::new("other@example.com"),
772        ]);
773        assert_eq!(recipients.to.as_ref().unwrap().len(), 2);
774
775        // Test CC deduplication
776        let recipients = Recipients::cc(vec![
777            EmailAddress::new("cc1@example.com"),
778            EmailAddress::new("cc1@example.com"), // duplicate
779        ]);
780        assert_eq!(recipients.cc.as_ref().unwrap().len(), 1);
781
782        // Test BCC deduplication
783        let recipients = Recipients::bcc(vec![
784            EmailAddress::new("bcc@example.com"),
785            EmailAddress::new("bcc@example.com"), // duplicate
786            EmailAddress::new("bcc@example.com"), // duplicate
787        ]);
788        assert_eq!(recipients.bcc.as_ref().unwrap().len(), 1);
789    }
790
791    #[test]
792    fn test_recipients_add_methods_deduplication() {
793        // Test adding duplicates
794        let recipients = Recipients::to(vec![EmailAddress::new("to@example.com")]).add_to(vec![
795            EmailAddress::new("to@example.com"), // duplicate of existing
796            EmailAddress::new("to2@example.com"),
797        ]);
798        assert_eq!(recipients.to.as_ref().unwrap().len(), 2);
799
800        // Test multiple add operations with duplicates
801        let recipients = Recipients::to(vec![EmailAddress::new("user1@example.com")])
802            .add_to(vec![EmailAddress::new("user2@example.com")])
803            .add_to(vec![
804                EmailAddress::new("user1@example.com"), // duplicate
805                EmailAddress::new("user3@example.com"),
806            ]);
807        assert_eq!(recipients.to.as_ref().unwrap().len(), 3);
808    }
809
810    #[test]
811    fn test_recipients_builder_deduplication() {
812        // Test builder with duplicates
813        let recipients = Recipients::builder()
814            .to(vec![
815                EmailAddress::new("to@example.com"),
816                EmailAddress::new("to@example.com"), // duplicate
817            ])
818            .cc(vec![
819                EmailAddress::new("cc@example.com"),
820                EmailAddress::new("cc@example.com"), // duplicate
821            ])
822            .build();
823
824        assert_eq!(recipients.to.as_ref().unwrap().len(), 1);
825        assert_eq!(recipients.cc.as_ref().unwrap().len(), 1);
826    }
827
828    #[test]
829    fn test_email_address_with_name_deduplication() {
830        // Same email with different names should be treated as duplicates
831        let recipients = Recipients::to(vec![
832            EmailAddress::new("user@example.com"),
833            EmailAddress::with_name("user@example.com", "User Name"),
834        ]);
835
836        // Should keep only one (the first one encountered)
837        assert_eq!(recipients.to.as_ref().unwrap().len(), 1);
838    }
839}