use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailConfiguration {
#[serde(rename = "compartmentId")]
pub compartment_id: String,
#[serde(rename = "httpSubmitEndpoint")]
pub http_submit_endpoint: String,
#[serde(rename = "smtpSubmitEndpoint")]
pub smtp_submit_endpoint: String,
#[serde(rename = "emailDeliveryConfigId")]
pub email_delivery_config_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Email {
#[serde(rename = "messageId", skip_serializing_if = "Option::is_none")]
pub message_id: Option<String>,
pub sender: Sender,
pub recipients: Recipients,
pub subject: String,
#[serde(rename = "bodyHtml", skip_serializing_if = "Option::is_none")]
pub body_html: Option<String>,
#[serde(rename = "bodyText", skip_serializing_if = "Option::is_none")]
pub body_text: Option<String>,
#[serde(rename = "replyTo", skip_serializing_if = "Option::is_none")]
pub reply_to: Option<Vec<EmailAddress>>,
#[serde(rename = "headerFields", skip_serializing_if = "Option::is_none")]
pub headers: Option<std::collections::HashMap<String, String>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Sender {
#[serde(rename = "senderAddress")]
pub sender_address: EmailAddress,
#[serde(rename = "compartmentId")]
pub compartment_id: String,
}
impl Sender {
pub fn new(email: impl Into<String>) -> Self {
Self {
sender_address: EmailAddress::new(email),
compartment_id: String::new(), }
}
pub fn with_name(email: impl Into<String>, name: impl Into<String>) -> Self {
Self {
sender_address: EmailAddress::with_name(email, name),
compartment_id: String::new(), }
}
pub(crate) fn set_compartment_id(&mut self, compartment_id: impl Into<String>) {
self.compartment_id = compartment_id.into();
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailAddress {
pub email: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl PartialEq for EmailAddress {
fn eq(&self, other: &Self) -> bool {
self.email == other.email
}
}
impl Eq for EmailAddress {}
impl std::hash::Hash for EmailAddress {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.email.hash(state);
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Recipients {
#[serde(skip_serializing_if = "Option::is_none")]
pub to: Option<Vec<EmailAddress>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cc: Option<Vec<EmailAddress>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bcc: Option<Vec<EmailAddress>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubmitEmailResponse {
#[serde(rename = "messageId")]
pub message_id: String,
#[serde(rename = "envelopeId")]
pub envelope_id: String,
#[serde(
rename = "suppressedRecipients",
skip_serializing_if = "Option::is_none"
)]
pub suppressed_recipients: Option<Vec<EmailAddress>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SenderSummary {
pub id: String,
#[serde(rename = "emailAddress")]
pub email_address: String,
#[serde(rename = "lifecycleState")]
pub lifecycle_state: SenderLifecycleState,
#[serde(rename = "timeCreated")]
pub time_created: String,
#[serde(rename = "isSpf", skip_serializing_if = "Option::is_none")]
pub is_spf: Option<bool>,
#[serde(rename = "compartmentId", skip_serializing_if = "Option::is_none")]
pub compartment_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum SenderLifecycleState {
Creating,
Active,
NeedsAttention,
Inactive,
Failed,
Deleting,
Deleted,
}
impl EmailAddress {
pub fn new(email: impl Into<String>) -> Self {
Self {
email: email.into(),
name: None,
}
}
pub fn with_name(email: impl Into<String>, name: impl Into<String>) -> Self {
Self {
email: email.into(),
name: Some(name.into()),
}
}
}
impl Recipients {
fn deduplicate(addresses: Vec<EmailAddress>) -> Vec<EmailAddress> {
use std::collections::HashSet;
let mut seen = HashSet::new();
addresses
.into_iter()
.filter(|addr| seen.insert(addr.clone()))
.collect()
}
pub fn new(addresses: Vec<EmailAddress>) -> Self {
Self::to(addresses)
}
pub fn to(addresses: Vec<EmailAddress>) -> Self {
Self {
to: Some(Self::deduplicate(addresses)),
cc: None,
bcc: None,
}
}
pub fn cc(addresses: Vec<EmailAddress>) -> Self {
Self {
to: None,
cc: Some(Self::deduplicate(addresses)),
bcc: None,
}
}
pub fn bcc(addresses: Vec<EmailAddress>) -> Self {
Self {
to: None,
cc: None,
bcc: Some(Self::deduplicate(addresses)),
}
}
pub fn add_to(mut self, mut addresses: Vec<EmailAddress>) -> Self {
if let Some(ref mut to) = self.to {
to.append(&mut addresses);
*to = Self::deduplicate(to.clone());
} else {
self.to = Some(Self::deduplicate(addresses));
}
self
}
pub fn add_cc(mut self, mut addresses: Vec<EmailAddress>) -> Self {
if let Some(ref mut cc) = self.cc {
cc.append(&mut addresses);
*cc = Self::deduplicate(cc.clone());
} else {
self.cc = Some(Self::deduplicate(addresses));
}
self
}
pub fn add_bcc(mut self, mut addresses: Vec<EmailAddress>) -> Self {
if let Some(ref mut bcc) = self.bcc {
bcc.append(&mut addresses);
*bcc = Self::deduplicate(bcc.clone());
} else {
self.bcc = Some(Self::deduplicate(addresses));
}
self
}
pub fn builder() -> RecipientsBuilder {
RecipientsBuilder::default()
}
}
#[derive(Debug, Default)]
pub struct RecipientsBuilder {
to: Option<Vec<EmailAddress>>,
cc: Option<Vec<EmailAddress>>,
bcc: Option<Vec<EmailAddress>>,
}
impl RecipientsBuilder {
pub fn to(mut self, addresses: Vec<EmailAddress>) -> Self {
self.to = Some(Recipients::deduplicate(addresses));
self
}
pub fn cc(mut self, addresses: Vec<EmailAddress>) -> Self {
self.cc = Some(Recipients::deduplicate(addresses));
self
}
pub fn bcc(mut self, addresses: Vec<EmailAddress>) -> Self {
self.bcc = Some(Recipients::deduplicate(addresses));
self
}
pub fn build(self) -> Recipients {
Recipients {
to: self.to,
cc: self.cc,
bcc: self.bcc,
}
}
}
impl Email {
pub fn builder() -> EmailBuilder {
EmailBuilder::default()
}
}
#[derive(Debug, Default)]
pub struct EmailBuilder {
message_id: Option<String>,
sender: Option<EmailAddress>,
recipients: Option<Recipients>,
subject: Option<String>,
body_html: Option<String>,
body_text: Option<String>,
reply_to: Option<Vec<EmailAddress>>,
headers: Option<std::collections::HashMap<String, String>>,
}
impl EmailBuilder {
pub fn message_id(mut self, message_id: impl Into<String>) -> Self {
self.message_id = Some(message_id.into());
self
}
pub fn sender(mut self, sender: EmailAddress) -> Self {
self.sender = Some(sender);
self
}
pub fn recipients(mut self, recipients: Recipients) -> Self {
self.recipients = Some(recipients);
self
}
pub fn subject(mut self, subject: impl Into<String>) -> Self {
self.subject = Some(subject.into());
self
}
pub fn body_html(mut self, body_html: impl Into<String>) -> Self {
self.body_html = Some(body_html.into());
self
}
pub fn body_text(mut self, body_text: impl Into<String>) -> Self {
self.body_text = Some(body_text.into());
self
}
pub fn reply_to(mut self, reply_to: Vec<EmailAddress>) -> Self {
self.reply_to = Some(reply_to);
self
}
pub fn headers(mut self, headers: std::collections::HashMap<String, String>) -> Self {
self.headers = Some(headers);
self
}
pub fn build(self) -> crate::error::Result<Email> {
let sender_address = self
.sender
.ok_or_else(|| crate::error::Error::ConfigError("Sender is required".to_string()))?;
let sender = Sender {
sender_address,
compartment_id: String::new(),
};
let recipients = self.recipients.ok_or_else(|| {
crate::error::Error::ConfigError("Recipients are required".to_string())
})?;
let subject = self.subject.ok_or_else(|| {
crate::error::Error::ConfigError("Subject is required".to_string())
})?;
if self.body_html.is_none() && self.body_text.is_none() {
return Err(crate::error::Error::ConfigError(
"At least one of body_html or body_text is required".to_string(),
));
}
Ok(Email {
message_id: self.message_id,
sender,
recipients,
subject,
body_html: self.body_html,
body_text: self.body_text,
reply_to: self.reply_to,
headers: self.headers,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_email_address_new() {
let addr = EmailAddress::new("test@example.com");
assert_eq!(addr.email, "test@example.com");
assert_eq!(addr.name, None);
}
#[test]
fn test_email_address_with_name() {
let addr = EmailAddress::with_name("test@example.com", "Test User");
assert_eq!(addr.email, "test@example.com");
assert_eq!(addr.name, Some("Test User".to_string()));
}
#[test]
fn test_recipients_to() {
let recipients = Recipients::to(vec![
EmailAddress::new("user1@example.com"),
EmailAddress::new("user2@example.com"),
]);
assert_eq!(recipients.to.as_ref().unwrap().len(), 2);
assert_eq!(recipients.cc, None);
assert_eq!(recipients.bcc, None);
}
#[test]
fn test_recipients_builder() {
let recipients = Recipients::builder()
.to(vec![EmailAddress::new("to@example.com")])
.cc(vec![EmailAddress::new("cc@example.com")])
.bcc(vec![EmailAddress::new("bcc@example.com")])
.build();
assert_eq!(recipients.to.as_ref().unwrap().len(), 1);
assert_eq!(recipients.cc.as_ref().unwrap().len(), 1);
assert_eq!(recipients.bcc.as_ref().unwrap().len(), 1);
}
#[test]
fn test_submit_email_request_serialization() {
let mut request = Email {
message_id: Some("test-123".to_string()),
sender: Sender::with_name("sender@example.com", "Sender"),
recipients: Recipients::to(vec![EmailAddress::new("recipient@example.com")]),
subject: "Test Subject".to_string(),
body_html: Some("<html><body>Test</body></html>".to_string()),
body_text: Some("Test".to_string()),
reply_to: None,
headers: None,
};
request.sender.set_compartment_id("ocid1.compartment.test");
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"sender\""));
assert!(json.contains("\"recipients\""));
assert!(json.contains("\"subject\""));
assert!(json.contains("\"messageId\""));
}
#[test]
fn test_submit_email_request_builder() {
let mut request = Email::builder()
.sender(EmailAddress::new("sender@example.com"))
.recipients(
Recipients::builder()
.to(vec![EmailAddress::new("recipient@example.com")])
.build(),
)
.subject("Test Subject")
.body_text("Test body")
.build()
.unwrap();
request.sender.set_compartment_id("ocid1.compartment.test");
assert_eq!(request.subject, "Test Subject");
assert_eq!(request.body_text.as_ref().unwrap(), "Test body");
assert!(request.recipients.to.is_some());
}
#[test]
fn test_submit_email_request_builder_missing_required_fields() {
let result = Email::builder()
.recipients(Recipients::to(vec![EmailAddress::new("to@example.com")]))
.subject("Test")
.build();
assert!(result.is_err());
let result = Email::builder()
.sender(EmailAddress::new("sender@example.com"))
.subject("Test")
.build();
assert!(result.is_err());
let result = Email::builder()
.sender(EmailAddress::new("sender@example.com"))
.recipients(Recipients::to(vec![EmailAddress::new("to@example.com")]))
.build();
assert!(result.is_err());
}
#[test]
fn test_email_configuration_deserialization() {
let json = r#"{
"compartmentId": "ocid1.compartment.test",
"httpSubmitEndpoint": "https://email.ap-seoul-1.oci.oraclecloud.com",
"smtpSubmitEndpoint": "smtp.email.ap-seoul-1.oci.oraclecloud.com"
}"#;
let config: EmailConfiguration = serde_json::from_str(json).unwrap();
assert_eq!(config.compartment_id, "ocid1.compartment.test");
assert_eq!(
config.http_submit_endpoint,
"https://email.ap-seoul-1.oci.oraclecloud.com"
);
assert_eq!(
config.smtp_submit_endpoint,
"smtp.email.ap-seoul-1.oci.oraclecloud.com"
);
}
#[test]
fn test_submit_email_response_deserialization() {
let json = r#"{
"messageId": "msg-123",
"envelopeId": "env-456"
}"#;
let response: SubmitEmailResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.message_id, "msg-123");
assert_eq!(response.envelope_id, "env-456");
}
#[test]
fn test_complete_email_request_with_all_fields() {
use std::collections::HashMap;
let mut headers = HashMap::new();
headers.insert("X-Test".to_string(), "test-value".to_string());
let mut request = Email {
message_id: Some("msg-001".to_string()),
sender: Sender::with_name("sender@example.com", "Sender Name"),
recipients: Recipients {
to: Some(vec![EmailAddress::new("to@example.com")]),
cc: Some(vec![EmailAddress::new("cc@example.com")]),
bcc: Some(vec![EmailAddress::new("bcc@example.com")]),
},
subject: "Complete Test".to_string(),
body_html: Some("<p>HTML body</p>".to_string()),
body_text: Some("Text body".to_string()),
reply_to: Some(vec![EmailAddress::new("replyto@example.com")]),
headers: Some(headers),
};
request.sender.set_compartment_id("ocid1.compartment.test");
let json = serde_json::to_string(&request).unwrap();
let deserialized: Email = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.message_id, Some("msg-001".to_string()));
assert_eq!(deserialized.subject, "Complete Test");
assert!(deserialized.recipients.to.is_some());
assert!(deserialized.recipients.cc.is_some());
assert!(deserialized.recipients.bcc.is_some());
assert!(deserialized.reply_to.is_some());
assert!(deserialized.headers.is_some());
}
#[test]
fn test_complete_email_request_with_builder() {
use std::collections::HashMap;
let mut headers = HashMap::new();
headers.insert("X-Test".to_string(), "test-value".to_string());
let mut request = Email::builder()
.message_id("msg-001")
.sender(EmailAddress::with_name("sender@example.com", "Sender Name"))
.recipients(
Recipients::builder()
.to(vec![EmailAddress::new("to@example.com")])
.cc(vec![EmailAddress::new("cc@example.com")])
.bcc(vec![EmailAddress::new("bcc@example.com")])
.build(),
)
.subject("Complete Test")
.body_html("<p>HTML body</p>")
.body_text("Text body")
.reply_to(vec![EmailAddress::new("replyto@example.com")])
.headers(headers)
.build()
.unwrap();
request.sender.set_compartment_id("ocid1.compartment.test");
assert_eq!(request.message_id, Some("msg-001".to_string()));
assert_eq!(request.subject, "Complete Test");
assert!(request.recipients.to.is_some());
assert!(request.recipients.cc.is_some());
assert!(request.recipients.bcc.is_some());
assert!(request.reply_to.is_some());
assert!(request.headers.is_some());
}
#[test]
fn test_recipients_constructors() {
let recipients = Recipients::new(vec![EmailAddress::new("to@example.com")]);
assert!(recipients.to.is_some());
assert!(recipients.cc.is_none());
assert!(recipients.bcc.is_none());
let recipients = Recipients::to(vec![EmailAddress::new("to@example.com")]);
assert!(recipients.to.is_some());
assert!(recipients.cc.is_none());
assert!(recipients.bcc.is_none());
let recipients = Recipients::cc(vec![EmailAddress::new("cc@example.com")]);
assert!(recipients.to.is_none());
assert!(recipients.cc.is_some());
assert!(recipients.bcc.is_none());
let recipients = Recipients::bcc(vec![EmailAddress::new("bcc@example.com")]);
assert!(recipients.to.is_none());
assert!(recipients.cc.is_none());
assert!(recipients.bcc.is_some());
}
#[test]
fn test_recipients_add_methods() {
let recipients = Recipients::to(vec![EmailAddress::new("to1@example.com")])
.add_to(vec![EmailAddress::new("to2@example.com")])
.add_cc(vec![EmailAddress::new("cc@example.com")])
.add_bcc(vec![EmailAddress::new("bcc@example.com")]);
assert_eq!(recipients.to.as_ref().unwrap().len(), 2);
assert_eq!(recipients.cc.as_ref().unwrap().len(), 1);
assert_eq!(recipients.bcc.as_ref().unwrap().len(), 1);
let recipients = Recipients::cc(vec![EmailAddress::new("cc1@example.com")]).add_cc(vec![
EmailAddress::new("cc2@example.com"),
EmailAddress::new("cc3@example.com"),
]);
assert_eq!(recipients.cc.as_ref().unwrap().len(), 3);
let recipients = Recipients::to(vec![EmailAddress::new("to@example.com")])
.add_bcc(vec![EmailAddress::new("bcc@example.com")]);
assert!(recipients.to.is_some());
assert!(recipients.bcc.is_some());
}
#[test]
fn test_build_missing_body() {
let result = Email::builder()
.sender(EmailAddress::new("sender@example.com"))
.recipients(Recipients::to(vec![EmailAddress::new("to@example.com")]))
.subject("Test")
.build();
assert!(result.is_err());
if let Err(crate::error::Error::ConfigError(msg)) = result {
assert!(msg.contains("body"));
} else {
panic!("Expected ConfigError about body");
}
}
#[test]
fn test_build_with_only_html_body() {
let result = Email::builder()
.sender(EmailAddress::new("sender@example.com"))
.recipients(Recipients::to(vec![EmailAddress::new("to@example.com")]))
.subject("Test")
.body_html("<p>HTML content</p>")
.build();
assert!(result.is_ok());
let request = result.unwrap();
assert!(request.body_html.is_some());
assert!(request.body_text.is_none());
}
#[test]
fn test_build_with_only_text_body() {
let result = Email::builder()
.sender(EmailAddress::new("sender@example.com"))
.recipients(Recipients::to(vec![EmailAddress::new("to@example.com")]))
.subject("Test")
.body_text("Text content")
.build();
assert!(result.is_ok());
let request = result.unwrap();
assert!(request.body_html.is_none());
assert!(request.body_text.is_some());
}
#[test]
fn test_recipients_deduplication() {
let recipients = Recipients::to(vec![
EmailAddress::new("user@example.com"),
EmailAddress::new("user@example.com"), EmailAddress::new("other@example.com"),
]);
assert_eq!(recipients.to.as_ref().unwrap().len(), 2);
let recipients = Recipients::cc(vec![
EmailAddress::new("cc1@example.com"),
EmailAddress::new("cc1@example.com"), ]);
assert_eq!(recipients.cc.as_ref().unwrap().len(), 1);
let recipients = Recipients::bcc(vec![
EmailAddress::new("bcc@example.com"),
EmailAddress::new("bcc@example.com"), EmailAddress::new("bcc@example.com"), ]);
assert_eq!(recipients.bcc.as_ref().unwrap().len(), 1);
}
#[test]
fn test_recipients_add_methods_deduplication() {
let recipients = Recipients::to(vec![EmailAddress::new("to@example.com")]).add_to(vec![
EmailAddress::new("to@example.com"), EmailAddress::new("to2@example.com"),
]);
assert_eq!(recipients.to.as_ref().unwrap().len(), 2);
let recipients = Recipients::to(vec![EmailAddress::new("user1@example.com")])
.add_to(vec![EmailAddress::new("user2@example.com")])
.add_to(vec![
EmailAddress::new("user1@example.com"), EmailAddress::new("user3@example.com"),
]);
assert_eq!(recipients.to.as_ref().unwrap().len(), 3);
}
#[test]
fn test_recipients_builder_deduplication() {
let recipients = Recipients::builder()
.to(vec![
EmailAddress::new("to@example.com"),
EmailAddress::new("to@example.com"), ])
.cc(vec![
EmailAddress::new("cc@example.com"),
EmailAddress::new("cc@example.com"), ])
.build();
assert_eq!(recipients.to.as_ref().unwrap().len(), 1);
assert_eq!(recipients.cc.as_ref().unwrap().len(), 1);
}
#[test]
fn test_email_address_with_name_deduplication() {
let recipients = Recipients::to(vec![
EmailAddress::new("user@example.com"),
EmailAddress::with_name("user@example.com", "User Name"),
]);
assert_eq!(recipients.to.as_ref().unwrap().len(), 1);
}
}