1use std::sync::Arc;
2
3use reqwest::Method;
4
5use crate::{
6 Config, Result,
7 batch::types::BatchValidation,
8 emails::types::CreateEmailBaseOptions,
9 idempotent::Idempotent,
10 types::{CreateEmailResponse, SendEmailBatchPermissiveResponse},
11};
12
13#[derive(Clone, Debug)]
15pub struct BatchSvc(pub(crate) Arc<Config>);
16
17impl BatchSvc {
18 #[maybe_async::maybe_async]
25 pub async fn send<T>(
26 &self,
27 emails: impl Into<Idempotent<T>>,
28 ) -> Result<Vec<CreateEmailResponse>>
29 where
30 T: IntoIterator<Item = CreateEmailBaseOptions> + Send,
31 {
32 Ok(self
33 .send_with_batch_validation(emails, BatchValidation::default())
34 .await?
35 .data)
36 }
37
38 #[maybe_async::maybe_async]
40 pub async fn send_with_batch_validation<T>(
41 &self,
42 emails: impl Into<Idempotent<T>>,
43 batch_validation: BatchValidation,
44 ) -> Result<SendEmailBatchPermissiveResponse>
45 where
46 T: IntoIterator<Item = CreateEmailBaseOptions> + Send,
47 {
48 let emails: Idempotent<T> = emails.into();
49
50 let emails: Vec<_> = emails.data.into_iter().collect();
51
52 let mut request = self.0.build(Method::POST, "/emails/batch");
53
54 request = request.header("x-batch-validation", batch_validation.to_string());
55
56 let response = self.0.send(request.json(&emails)).await?;
57 let content = response.json::<SendEmailBatchPermissiveResponse>().await?;
58
59 Ok(content)
60 }
61}
62
63#[allow(unreachable_pub)]
64pub mod types {
65 use crate::types::CreateEmailResponse;
66
67 #[must_use]
69 #[derive(Debug, Copy, Clone)]
70 pub enum BatchValidation {
71 Strict,
77 Permissive,
79 }
80
81 impl Default for BatchValidation {
82 fn default() -> Self {
83 Self::Strict
84 }
85 }
86
87 impl std::fmt::Display for BatchValidation {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 match self {
90 Self::Strict => write!(f, "strict"),
91 Self::Permissive => write!(f, "permissive"),
92 }
93 }
94 }
95
96 #[derive(Debug, Clone, serde::Deserialize)]
97 pub struct SendEmailBatchResponse {
98 pub data: Vec<CreateEmailResponse>,
100 }
101
102 #[derive(Debug, Clone, serde::Deserialize)]
103 pub struct SendEmailBatchPermissiveResponse {
104 pub data: Vec<CreateEmailResponse>,
106 #[serde(default)]
108 pub errors: Vec<PermissiveBatchErrors>,
109 }
110
111 #[derive(Debug, Clone, serde::Deserialize)]
112 pub struct PermissiveBatchErrors {
113 pub index: i32,
115 pub message: String,
117 }
118}
119
120#[cfg(test)]
121mod test {
122 use crate::test::{CLIENT, DebugResult};
123 use crate::types::{
124 BatchValidation, CreateEmailBaseOptions, CreateTemplateOptions, EmailEvent, EmailTemplate,
125 Variable, VariableType,
126 };
127
128 #[tokio_shared_rt::test(shared = true)]
129 #[cfg(not(feature = "blocking"))]
130 #[allow(clippy::unwrap_used, clippy::indexing_slicing)]
131 async fn strict_error() -> DebugResult<()> {
132 let resend = &*CLIENT;
133 std::thread::sleep(std::time::Duration::from_secs(1));
134
135 let emails = vec![
136 CreateEmailBaseOptions::new(
137 "Acme <onboarding@resend.dev>",
138 vec!["delivered@resend.dev"],
139 "hello world",
140 )
141 .with_html("<h1>it works!</h1>"),
142 CreateEmailBaseOptions::new(
143 "Acme <onboarding@resend.dev>",
144 vec!["NOTantosnis.barotsis@gmail.com"],
145 "world hello",
146 )
147 .with_html("<p>it works!</p>"),
148 ];
149
150 let emails = resend
151 .batch
152 .send_with_batch_validation(emails, BatchValidation::Strict)
153 .await;
154
155 assert!(emails.is_err());
157
158 Ok(())
159 }
160
161 #[tokio_shared_rt::test(shared = true)]
162 #[cfg(not(feature = "blocking"))]
163 #[allow(clippy::unwrap_used, clippy::indexing_slicing)]
164 async fn permissive_error() -> DebugResult<()> {
165 let resend = &*CLIENT;
166 std::thread::sleep(std::time::Duration::from_secs(1));
167
168 let emails = vec![
169 CreateEmailBaseOptions::new(
170 "Acme <onboarding@resend.dev>",
171 vec!["delivered@resend.dev"],
172 "hello world",
173 )
174 .with_html("<h1>it works!</h1>"),
175 CreateEmailBaseOptions::new(
176 "Acme <onboarding@resend.dev>",
177 vec!["someotheremail@gmail.com"],
178 "world hello",
179 )
180 .with_html("<p>it works!</p>"),
181 ];
182
183 let emails = resend
184 .batch
185 .send_with_batch_validation(emails, BatchValidation::Permissive)
186 .await;
187
188 assert!(emails.is_ok());
190 let emails = emails.unwrap();
191
192 std::thread::sleep(std::time::Duration::from_secs(4));
195 let failed_id = &emails.data[1].id;
196 let status = resend.emails.get(failed_id).await?;
197 assert_eq!(status.last_event, EmailEvent::Failed);
198
199 Ok(())
200 }
201
202 #[tokio_shared_rt::test(shared = true)]
203 #[cfg(not(feature = "blocking"))]
204 #[allow(clippy::unwrap_used, clippy::indexing_slicing)]
205 async fn permissive_ok() -> DebugResult<()> {
206 let resend = &*CLIENT;
207 std::thread::sleep(std::time::Duration::from_secs(1));
208
209 let emails = vec![
210 CreateEmailBaseOptions::new(
211 "Acme <onboarding@resend.dev>",
212 vec!["delivered@resend.dev"],
213 "hello world",
214 )
215 .with_html("<h1>it works!</h1>"),
216 CreateEmailBaseOptions::new(
217 "Acme <onboarding@resend.dev>",
218 vec!["delivered@resend.dev"],
219 "world hello",
220 )
221 .with_html("<p>it works!</p>"),
222 ];
223
224 let emails = resend
225 .batch
226 .send_with_batch_validation(emails, BatchValidation::Permissive)
227 .await;
228
229 assert!(emails.is_ok());
231 let emails = emails.unwrap();
232
233 assert!(emails.errors.is_empty());
235
236 Ok(())
237 }
238
239 #[tokio_shared_rt::test(shared = true)]
240 #[cfg(not(feature = "blocking"))]
241 #[allow(clippy::unwrap_used, clippy::indexing_slicing)]
242 async fn strict_ok() -> DebugResult<()> {
243 let resend = &*CLIENT;
244 std::thread::sleep(std::time::Duration::from_secs(1));
245
246 let emails = vec![
247 CreateEmailBaseOptions::new(
248 "Acme <onboarding@resend.dev>",
249 vec!["delivered@resend.dev"],
250 "hello world",
251 )
252 .with_html("<h1>it works!</h1>"),
253 CreateEmailBaseOptions::new(
254 "Acme <onboarding@resend.dev>",
255 vec!["delivered@resend.dev"],
256 "world hello",
257 )
258 .with_html("<p>it works!</p>"),
259 ];
260
261 let emails = resend.batch.send(emails).await;
262
263 assert!(emails.is_ok());
265 let _emails = emails.unwrap();
266
267 Ok(())
268 }
269
270 #[tokio_shared_rt::test(shared = true)]
271 #[cfg(not(feature = "blocking"))]
272 async fn template() -> DebugResult<()> {
273 use std::collections::HashMap;
274
275 let resend = &*CLIENT;
276 std::thread::sleep(std::time::Duration::from_secs(1));
277
278 let name = "welcome-email";
280 let html = "<strong>Hey, {{{NAME}}}, you are {{{AGE}}} years old.</strong>";
281 let variables = [
282 Variable::new("NAME", VariableType::String).with_fallback("user"),
283 Variable::new("AGE", VariableType::Number).with_fallback(25),
284 Variable::new("OPTIONAL_VARIABLE", VariableType::String).with_fallback(None::<String>),
285 ];
286 let opts = CreateTemplateOptions::new(name, html).with_variables(&variables);
287 let template = resend.templates.create(opts).await?;
288 std::thread::sleep(std::time::Duration::from_secs(2));
289 let template = resend.templates.publish(&template.id).await?;
290 std::thread::sleep(std::time::Duration::from_secs(2));
291
292 let mut variables1 = HashMap::<String, serde_json::Value>::new();
293 let _added = variables1.insert("NAME".to_string(), serde_json::json!("Tony"));
294 let _added = variables1.insert("AGE".to_string(), serde_json::json!(25));
295
296 let template1 = EmailTemplate::new(&template.id).with_variables(variables1);
297 let template_id = &template1.id.clone();
298
299 let mut variables2 = HashMap::<String, serde_json::Value>::new();
300 let _added = variables2.insert("NAME".to_string(), serde_json::json!("Not Tony"));
301 let _added = variables2.insert("AGE".to_string(), serde_json::json!(42));
302
303 let template2 = EmailTemplate::new(&template.id).with_variables(variables2);
304 let _ = &template2.id.clone();
305
306 let from = "Acme <onboarding@resend.dev>";
308 let to = ["delivered@resend.dev"];
309 let subject = "hello world";
310
311 let emails = vec![
312 CreateEmailBaseOptions::new(from, to, subject).with_template(template1),
313 CreateEmailBaseOptions::new(from, to, subject).with_template(template2),
314 ];
315
316 let _email = resend.batch.send(emails).await?;
317 std::thread::sleep(std::time::Duration::from_secs(2));
318
319 let deleted = resend.templates.delete(template_id).await?;
321 assert!(deleted.deleted);
322
323 Ok(())
324 }
325}