Skip to main content

sendry/resources/
emails.rs

1//! Send and inspect emails.
2
3use reqwest::Method;
4use serde::{Deserialize, Serialize};
5
6use crate::{client::Sendry, error::Error, Page};
7
8/// Emails resource handle.
9#[derive(Debug, Clone)]
10pub struct Emails {
11    client: Sendry,
12}
13
14impl Emails {
15    pub(crate) fn new(client: Sendry) -> Self {
16        Self { client }
17    }
18
19    /// Send a single email.
20    pub async fn send(&self, params: SendEmail) -> Result<EmailResponse, Error> {
21        self.client
22            .request(
23                self.client
24                    .build(Method::POST, "/v1/emails", &[], Some(&params)),
25            )
26            .await
27    }
28
29    /// Send a batch of up to 100 emails in one request.
30    pub async fn send_batch(&self, params: BatchEmail) -> Result<BatchResponse, Error> {
31        self.client
32            .request(
33                self.client
34                    .build(Method::POST, "/v1/emails/batch", &[], Some(&params)),
35            )
36            .await
37    }
38
39    /// Retrieve a single email by id.
40    pub async fn get(&self, id: &str) -> Result<Email, Error> {
41        self.client
42            .request(
43                self.client
44                    .build::<()>(Method::GET, &format!("/v1/emails/{id}"), &[], None),
45            )
46            .await
47    }
48
49    /// List emails (cursor-paginated).
50    pub async fn list(&self, query: ListEmails) -> Result<Page<Email>, Error> {
51        let q = query.to_query();
52        self.client
53            .request(self.client.build::<()>(Method::GET, "/v1/emails", &q, None))
54            .await
55    }
56
57    /// Send a marketing email with mandatory unsubscribe support.
58    pub async fn send_marketing(
59        &self,
60        params: SendMarketingEmail,
61    ) -> Result<EmailResponse, Error> {
62        self.client
63            .request(self.client.build(
64                Method::POST,
65                "/v1/emails/marketing",
66                &[],
67                Some(&params),
68            ))
69            .await
70    }
71
72    /// Cancel a queued email (only works if not yet sent).
73    pub async fn cancel(&self, id: &str) -> Result<CancelEmailResponse, Error> {
74        self.client
75            .request(self.client.build::<()>(
76                Method::POST,
77                &format!("/v1/emails/{id}/cancel"),
78                &[],
79                None,
80            ))
81            .await
82    }
83}
84
85/// Parameters for [`Emails::send_marketing`].
86#[derive(Debug, Clone, Default, Serialize)]
87pub struct SendMarketingEmail {
88    /// Verified sender.
89    pub from: String,
90    /// Recipients (single or many).
91    pub to: Vec<String>,
92    /// Subject.
93    pub subject: String,
94    /// HTML body.
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub html: Option<String>,
97    /// Plain-text body.
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub text: Option<String>,
100    /// Reply-to.
101    #[serde(skip_serializing_if = "Option::is_none", rename = "reply_to")]
102    pub reply_to: Option<String>,
103    /// Custom headers.
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub headers: Option<serde_json::Value>,
106    /// SES tags.
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub tags: Option<Vec<Tag>>,
109    /// Required unsubscribe URL.
110    pub unsubscribe_url: String,
111    /// Optional list id.
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub list_id: Option<String>,
114    /// ISO datetime to delay sending.
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub scheduled_at: Option<String>,
117    /// Template id.
118    #[serde(skip_serializing_if = "Option::is_none", rename = "template_id")]
119    pub template_id: Option<String>,
120}
121
122/// Response from [`Emails::cancel`].
123#[derive(Debug, Clone, Deserialize)]
124pub struct CancelEmailResponse {
125    /// Email id.
126    pub id: String,
127    /// Always `cancelled`.
128    pub status: String,
129}
130
131/// Parameters for [`Emails::send`].
132#[derive(Debug, Clone, Default, Serialize)]
133pub struct SendEmail {
134    /// The verified sender address. Required.
135    pub from: String,
136    /// Recipients.
137    pub to: Vec<String>,
138    /// Subject line.
139    pub subject: String,
140    /// HTML body.
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub html: Option<String>,
143    /// Plain-text body.
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub text: Option<String>,
146    /// Carbon copy recipients.
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub cc: Option<Vec<String>>,
149    /// Blind carbon copy recipients.
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub bcc: Option<Vec<String>>,
152    /// Reply-To address.
153    #[serde(skip_serializing_if = "Option::is_none", rename = "reply_to")]
154    pub reply_to: Option<String>,
155    /// Custom SMTP headers.
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub headers: Option<serde_json::Value>,
158    /// SES message tags.
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub tags: Option<Vec<Tag>>,
161    /// Inline attachments.
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub attachments: Option<Vec<Attachment>>,
164    /// Template id to render (mutually exclusive with `html`/`text`).
165    #[serde(skip_serializing_if = "Option::is_none", rename = "template_id")]
166    pub template_id: Option<String>,
167    /// Template variable substitutions.
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub variables: Option<serde_json::Value>,
170}
171
172/// Single SES message tag.
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct Tag {
175    /// Tag name.
176    pub name: String,
177    /// Tag value.
178    pub value: String,
179}
180
181/// File attachment.
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct Attachment {
184    /// File name shown to the recipient.
185    pub filename: String,
186    /// Base64-encoded content.
187    pub content: String,
188    /// MIME type.
189    #[serde(rename = "contentType")]
190    pub content_type: String,
191}
192
193/// Response from [`Emails::send`].
194#[derive(Debug, Clone, Deserialize)]
195pub struct EmailResponse {
196    /// Sendry-assigned id, e.g. `em_abc123`.
197    pub id: String,
198    /// `queued` initially; later moves to `sent`/`bounced`/etc.
199    pub status: String,
200}
201
202/// Single email as returned by `get` / `list`.
203#[derive(Debug, Clone, Deserialize)]
204pub struct Email {
205    /// Email id.
206    pub id: String,
207    /// From address.
208    pub from: String,
209    /// To addresses.
210    pub to: Vec<String>,
211    /// Subject line.
212    pub subject: String,
213    /// Current status.
214    pub status: String,
215    /// Send timestamp (ISO-8601).
216    pub created_at: String,
217}
218
219/// Parameters for [`Emails::send_batch`].
220#[derive(Debug, Clone, Serialize)]
221pub struct BatchEmail {
222    /// Shared sender address.
223    pub from: String,
224    /// Up to 100 emails.
225    pub emails: Vec<BatchEmailItem>,
226}
227
228/// One item in a batch send.
229#[derive(Debug, Clone, Default, Serialize)]
230pub struct BatchEmailItem {
231    /// Recipient.
232    pub to: String,
233    /// Subject.
234    pub subject: String,
235    /// HTML body.
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub html: Option<String>,
238    /// Plain-text body.
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub text: Option<String>,
241}
242
243/// Response from [`Emails::send_batch`].
244#[derive(Debug, Clone, Deserialize)]
245pub struct BatchResponse {
246    /// Count of emails queued.
247    pub sent: u32,
248    /// Per-item failures (validation errors only).
249    pub failed: Vec<BatchFailure>,
250}
251
252/// One failed item in a batch response.
253#[derive(Debug, Clone, Deserialize)]
254pub struct BatchFailure {
255    /// Recipient that failed.
256    pub to: String,
257    /// Server-side error message.
258    pub error: String,
259}
260
261/// Filters for [`Emails::list`].
262#[derive(Debug, Clone, Default)]
263pub struct ListEmails {
264    /// Page size (max 100).
265    pub limit: Option<u32>,
266    /// Cursor from a previous page.
267    pub cursor: Option<String>,
268    /// Filter by status.
269    pub status: Option<String>,
270}
271
272impl ListEmails {
273    fn to_query(&self) -> Vec<(&'static str, String)> {
274        let mut q = Vec::new();
275        if let Some(l) = self.limit {
276            q.push(("limit", l.to_string()));
277        }
278        if let Some(c) = &self.cursor {
279            q.push(("cursor", c.clone()));
280        }
281        if let Some(s) = &self.status {
282            q.push(("status", s.clone()));
283        }
284        q
285    }
286}