Skip to main content

oci_api/services/email/
client.rs

1//! Email client
2
3use async_trait::async_trait;
4
5use crate::client::Oci;
6use crate::error::{Error, Result};
7use crate::services::email::models::*;
8use crate::services::email::sender_trait::EmailSender;
9
10/// Email client
11#[derive(Clone)]
12pub struct EmailDelivery {
13    /// OCI HTTP client
14    oci_client: Oci,
15
16    /// Submit endpoint (loaded from email configuration)
17    submit_endpoint: String,
18}
19
20impl EmailDelivery {
21    /// Create new Email client
22    ///
23    /// Loads email configuration and caches the submit endpoint.
24    ///
25    /// # Arguments
26    /// * `oci_client` - OCI HTTP client
27    pub async fn new(oci_client: Oci) -> Result<Self> {
28        // Email configuration is a tenancy-level resource, so use tenancy_id
29        let tenancy_id = oci_client.tenancy_id().to_string();
30        let region = oci_client.region().to_string();
31
32        // Get email configuration
33        let config =
34            Self::get_email_configuration_internal(&oci_client, &tenancy_id, &region).await?;
35
36        Ok(Self {
37            oci_client,
38            submit_endpoint: config.http_submit_endpoint,
39        })
40    }
41
42    /// Get Email Configuration (internal helper)
43    async fn get_email_configuration_internal(
44        oci_client: &Oci,
45        compartment_id: &str,
46        region: &str,
47    ) -> Result<EmailConfiguration> {
48        // Build path with query string
49        let path = format!("/20170907/configuration?compartmentId={compartment_id}");
50        let host = format!("ctrl.email.{region}.oci.oraclecloud.com");
51        let url = format!("https://{host}{path}");
52
53        // Sign request
54        let (date_header, auth_header) = oci_client
55            .signer()
56            .sign_request("GET", &path, &host, None)?;
57
58        // Build and execute request
59        let response = oci_client
60            .client()
61            .get(&url)
62            .header("host", &host)
63            .header("date", &date_header)
64            .header("authorization", &auth_header)
65            .send()
66            .await?;
67
68        if !response.status().is_success() {
69            let status = response.status();
70            let body = response.text().await?;
71            return Err(Error::ApiError {
72                code: status.to_string(),
73                message: body,
74            });
75        }
76
77        response.json().await.map_err(Into::into)
78    }
79
80    /// Get Email Configuration (public API)
81    ///
82    /// # Arguments
83    /// * `compartment_id` - Compartment OCID (typically tenancy OCID)
84    pub async fn get_email_configuration(
85        &self,
86        compartment_id: impl Into<String>,
87    ) -> Result<EmailConfiguration> {
88        let compartment_id = compartment_id.into();
89        let region = self.oci_client.region().to_string();
90        Self::get_email_configuration_internal(&self.oci_client, &compartment_id, &region).await
91    }
92
93    /// Send email
94    ///
95    /// # Arguments
96    /// * `email` - Email message
97    ///
98    /// # Note
99    /// The compartment_id from Oci will be automatically set in the sender.
100    pub async fn send(&self, email: Email) -> Result<SubmitEmailResponse> {
101        self.send_impl(email).await
102    }
103
104    /// 실제 전송 로직 (inherent method + trait impl 공용)
105    async fn send_impl(&self, mut email: Email) -> Result<SubmitEmailResponse> {
106        // Get compartment_id from Oci
107        let compartment_id = self.oci_client.compartment_id().to_string();
108
109        // Set compartment_id in sender if not already set
110        if email.sender.compartment_id.is_empty() {
111            email.sender.set_compartment_id(&compartment_id);
112        }
113
114        // Build path and URL
115        let path = "/20220926/actions/submitEmail";
116        let url = format!("https://{}{}", &self.submit_endpoint, path);
117
118        // Serialize JSON body
119        let body_json = serde_json::to_string(&email)?;
120
121        // Calculate body SHA256 for x-content-sha256 header
122        let body_sha256 = {
123            use base64::{Engine, engine::general_purpose};
124            use sha2::{Digest, Sha256};
125            let mut hasher = Sha256::new();
126            hasher.update(body_json.as_bytes());
127            let result = hasher.finalize();
128            general_purpose::STANDARD.encode(result)
129        };
130
131        // Sign request (with body)
132        let (date_header, auth_header) = self.oci_client.signer().sign_request(
133            "POST",
134            path,
135            &self.submit_endpoint,
136            Some(&body_json),
137        )?;
138
139        // Build and execute request
140        let response = self
141            .oci_client
142            .client()
143            .post(&url)
144            .header("host", &self.submit_endpoint)
145            .header("date", &date_header)
146            .header("authorization", &auth_header)
147            .header("content-type", "application/json")
148            .header("content-length", body_json.len().to_string())
149            .header("x-content-sha256", &body_sha256)
150            .body(body_json)
151            .send()
152            .await?;
153
154        if !response.status().is_success() {
155            let status = response.status();
156            let body = response.text().await?;
157            return Err(Error::ApiError {
158                code: status.to_string(),
159                message: body,
160            });
161        }
162
163        let submit_response: SubmitEmailResponse = response.json().await?;
164        Ok(submit_response)
165    }
166
167    /// List approved senders
168    ///
169    /// # Arguments
170    /// * `compartment_id` - Compartment OCID (required)
171    /// * `lifecycle_state` - Optional filter by lifecycle state
172    /// * `email_address` - Optional filter by email address
173    pub async fn list_senders(
174        &self,
175        compartment_id: impl Into<String>,
176        lifecycle_state: Option<&str>,
177        email_address: Option<&str>,
178    ) -> Result<Vec<SenderSummary>> {
179        let compartment_id = compartment_id.into();
180
181        // Build query string
182        let mut query_params = vec![format!("compartmentId={}", compartment_id)];
183
184        if let Some(state) = lifecycle_state {
185            query_params.push(format!("lifecycleState={state}"));
186        }
187
188        if let Some(email) = email_address {
189            query_params.push(format!("emailAddress={email}"));
190        }
191
192        let query_string = query_params.join("&");
193        let path = format!("/20170907/senders?{query_string}");
194        let host = format!(
195            "ctrl.email.{}.oci.oraclecloud.com",
196            self.oci_client.region()
197        );
198        let url = format!("https://{host}{path}");
199
200        // Sign request
201        let (date_header, auth_header) = self
202            .oci_client
203            .signer()
204            .sign_request("GET", &path, &host, None)?;
205
206        // Build and execute request
207        let response = self
208            .oci_client
209            .client()
210            .get(&url)
211            .header("host", &host)
212            .header("date", &date_header)
213            .header("authorization", &auth_header)
214            .send()
215            .await?;
216
217        if !response.status().is_success() {
218            let status = response.status();
219            let body = response.text().await?;
220            return Err(Error::ApiError {
221                code: status.to_string(),
222                message: body,
223            });
224        }
225
226        let senders: Vec<SenderSummary> = response.json().await?;
227        Ok(senders)
228    }
229}
230
231#[async_trait]
232impl EmailSender for EmailDelivery {
233    async fn send(&self, email: Email) -> Result<SubmitEmailResponse> {
234        self.send_impl(email).await
235    }
236}