oci_api/services/email/
client.rs

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