Skip to main content

oci_api/services/email/
client.rs

1//! Email client
2
3use async_trait::async_trait;
4use reqwest::Method;
5
6use crate::client::Oci;
7use crate::client::request_executor::{RequestPayload, RequestTarget};
8use crate::error::Result;
9use crate::services::email::models::*;
10use crate::services::email::sender_trait::EmailSender;
11
12/// Email client
13#[derive(Clone)]
14pub struct EmailDelivery {
15    /// OCI HTTP client
16    oci_client: Oci,
17
18    /// Submit endpoint (loaded from email configuration)
19    submit_endpoint: String,
20}
21
22impl EmailDelivery {
23    fn control_host_for(region: &str, realm_domain: &str) -> String {
24        format!("ctrl.email.{region}.oci.{realm_domain}")
25    }
26
27    /// Create new Email client
28    ///
29    /// Loads email configuration and caches the submit endpoint.
30    ///
31    /// # Arguments
32    /// * `oci_client` - OCI HTTP client
33    pub async fn new(oci_client: Oci) -> Result<Self> {
34        // Email configuration is a tenancy-level resource, so use tenancy_id
35        let tenancy_id = oci_client.tenancy_id().to_string();
36        let region = oci_client.region().to_string();
37
38        // Get email configuration
39        let config =
40            Self::get_email_configuration_internal(&oci_client, &tenancy_id, &region).await?;
41
42        Ok(Self {
43            oci_client,
44            submit_endpoint: config.http_submit_endpoint,
45        })
46    }
47
48    /// Get Email Configuration (internal helper)
49    async fn get_email_configuration_internal(
50        oci_client: &Oci,
51        compartment_id: &str,
52        region: &str,
53    ) -> Result<EmailConfiguration> {
54        let path = format!("/20170907/configuration?compartmentId={compartment_id}");
55        let host = Self::control_host_for(region, oci_client.realm_domain());
56        let response = oci_client
57            .executor()
58            .execute(
59                Method::GET,
60                RequestTarget {
61                    scheme: "https",
62                    host: &host,
63                    path: &path,
64                },
65                RequestPayload {
66                    body: None,
67                    content_type: None,
68                    extra_headers: Vec::new(),
69                },
70            )
71            .await?;
72        response.json().await.map_err(Into::into)
73    }
74
75    /// Get Email Configuration (public API)
76    ///
77    /// # Arguments
78    /// * `compartment_id` - Compartment OCID (typically tenancy OCID)
79    pub async fn get_email_configuration(
80        &self,
81        compartment_id: impl Into<String>,
82    ) -> Result<EmailConfiguration> {
83        let compartment_id = compartment_id.into();
84        let region = self.oci_client.region().to_string();
85        Self::get_email_configuration_internal(&self.oci_client, &compartment_id, &region).await
86    }
87
88    /// Send email
89    ///
90    /// # Arguments
91    /// * `email` - Email message
92    ///
93    /// # Note
94    /// The compartment_id from Oci will be automatically set in the sender.
95    pub async fn send(&self, email: Email) -> Result<SubmitEmailResponse> {
96        self.send_impl(email).await
97    }
98
99    /// 실제 전송 로직 (inherent method + trait impl 공용)
100    async fn send_impl(&self, mut email: Email) -> Result<SubmitEmailResponse> {
101        // Get compartment_id from Oci
102        let compartment_id = self.oci_client.compartment_id().to_string();
103
104        // Set compartment_id in sender if not already set
105        if email.sender.compartment_id.is_empty() {
106            email.sender.set_compartment_id(&compartment_id);
107        }
108
109        let path = "/20220926/actions/submitEmail";
110        let body_json = serde_json::to_string(&email)?;
111        let response = self
112            .oci_client
113            .executor()
114            .execute(
115                Method::POST,
116                RequestTarget {
117                    scheme: "https",
118                    host: &self.submit_endpoint,
119                    path,
120                },
121                RequestPayload {
122                    body: Some(body_json),
123                    content_type: Some("application/json"),
124                    extra_headers: Vec::new(),
125                },
126            )
127            .await?;
128        response.json().await.map_err(Into::into)
129    }
130
131    /// List approved senders
132    ///
133    /// # Arguments
134    /// * `compartment_id` - Compartment OCID (required)
135    /// * `lifecycle_state` - Optional filter by lifecycle state
136    /// * `email_address` - Optional filter by email address
137    pub async fn list_senders(
138        &self,
139        compartment_id: impl Into<String>,
140        lifecycle_state: Option<&str>,
141        email_address: Option<&str>,
142    ) -> Result<Vec<SenderSummary>> {
143        let compartment_id = compartment_id.into();
144
145        // Build query string
146        let mut query_params = vec![format!("compartmentId={}", compartment_id)];
147
148        if let Some(state) = lifecycle_state {
149            query_params.push(format!("lifecycleState={state}"));
150        }
151
152        if let Some(email) = email_address {
153            query_params.push(format!("emailAddress={email}"));
154        }
155
156        let query_string = query_params.join("&");
157        let path = format!("/20170907/senders?{query_string}");
158        let host = Self::control_host_for(self.oci_client.region(), self.oci_client.realm_domain());
159        let response = self
160            .oci_client
161            .executor()
162            .execute(
163                Method::GET,
164                RequestTarget {
165                    scheme: "https",
166                    host: &host,
167                    path: &path,
168                },
169                RequestPayload {
170                    body: None,
171                    content_type: None,
172                    extra_headers: Vec::new(),
173                },
174            )
175            .await?;
176        response.json().await.map_err(Into::into)
177    }
178}
179
180#[async_trait]
181impl EmailSender for EmailDelivery {
182    async fn send(&self, email: Email) -> Result<SubmitEmailResponse> {
183        self.send_impl(email).await
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::EmailDelivery;
190    use crate::client::{AuthMode, Oci};
191
192    fn instance_principal_client(region: &str, realm_domain: &str) -> Oci {
193        Oci::builder()
194            .auth_mode(AuthMode::InstancePrincipal)
195            .tenancy_id("ocid1.tenancy.oc1..example")
196            .region(region)
197            .realm_domain_component(realm_domain)
198            .build()
199            .unwrap()
200    }
201
202    #[test]
203    fn email_control_host_uses_commercial_realm_for_instance_principal() {
204        let oci = instance_principal_client("ap-chuncheon-1", "oraclecloud.com");
205        assert_eq!(
206            EmailDelivery::control_host_for(oci.region(), oci.realm_domain()),
207            "ctrl.email.ap-chuncheon-1.oci.oraclecloud.com"
208        );
209    }
210
211    #[test]
212    fn email_control_host_uses_gov_realm_for_instance_principal() {
213        let oci = instance_principal_client("us-langley-1", "oraclegovcloud.com");
214        assert_eq!(
215            EmailDelivery::control_host_for(oci.region(), oci.realm_domain()),
216            "ctrl.email.us-langley-1.oci.oraclegovcloud.com"
217        );
218    }
219}