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 serde::{Deserialize, Serialize};
66
67 use crate::types::CreateEmailResponse;
68
69 #[must_use]
71 #[derive(Default, Debug, Copy, Clone)]
72 pub enum BatchValidation {
73 #[default]
79 Strict,
80 Permissive,
82 }
83
84 impl std::fmt::Display for BatchValidation {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 match self {
87 Self::Strict => write!(f, "strict"),
88 Self::Permissive => write!(f, "permissive"),
89 }
90 }
91 }
92
93 #[derive(Debug, Clone, Serialize, Deserialize)]
94 pub struct SendEmailBatchResponse {
95 pub data: Vec<CreateEmailResponse>,
97 }
98
99 #[derive(Debug, Clone, Serialize, Deserialize)]
100 pub struct SendEmailBatchPermissiveResponse {
101 pub data: Vec<CreateEmailResponse>,
103 #[serde(default)]
105 pub errors: Vec<PermissiveBatchErrors>,
106 }
107
108 #[derive(Debug, Clone, Serialize, Deserialize)]
109 pub struct PermissiveBatchErrors {
110 pub index: i32,
112 pub message: String,
114 }
115}
116
117#[cfg(test)]
118mod test {
119 use crate::test::{CLIENT, DebugResult};
120 use crate::types::{
121 BatchValidation, CreateEmailBaseOptions, CreateTemplateOptions, EmailEvent, EmailTemplate,
122 Variable, VariableType,
123 };
124
125 #[tokio_shared_rt::test(shared = true)]
126 #[cfg(not(feature = "blocking"))]
127 #[allow(clippy::unwrap_used, clippy::indexing_slicing)]
128 async fn strict_error() -> DebugResult<()> {
129 let resend = &*CLIENT;
130 std::thread::sleep(std::time::Duration::from_secs(1));
131
132 let emails = vec![
133 CreateEmailBaseOptions::new(
134 "Acme <onboarding@resend.dev>",
135 vec!["delivered@resend.dev"],
136 "hello world",
137 )
138 .with_html("<h1>it works!</h1>"),
139 CreateEmailBaseOptions::new(
140 "Acme <onboarding@resend.dev>",
141 vec!["NOTantosnis.barotsis@gmail.com"],
142 "world hello",
143 )
144 .with_html("<p>it works!</p>"),
145 ];
146
147 let emails = resend
148 .batch
149 .send_with_batch_validation(emails, BatchValidation::Strict)
150 .await;
151
152 assert!(emails.is_err());
154
155 Ok(())
156 }
157
158 #[tokio_shared_rt::test(shared = true)]
159 #[cfg(not(feature = "blocking"))]
160 #[allow(clippy::unwrap_used, clippy::indexing_slicing)]
161 async fn permissive_error() -> DebugResult<()> {
162 let resend = &*CLIENT;
163 std::thread::sleep(std::time::Duration::from_secs(1));
164
165 let emails = vec![
166 CreateEmailBaseOptions::new(
167 "Acme <onboarding@resend.dev>",
168 vec!["delivered@resend.dev"],
169 "hello world",
170 )
171 .with_html("<h1>it works!</h1>"),
172 CreateEmailBaseOptions::new(
173 "Acme <onboarding@resend.dev>",
174 vec!["someotheremail@gmail.com"],
175 "world hello",
176 )
177 .with_html("<p>it works!</p>"),
178 ];
179
180 let emails = resend
181 .batch
182 .send_with_batch_validation(emails, BatchValidation::Permissive)
183 .await;
184
185 assert!(emails.is_ok());
187 let emails = emails.unwrap();
188
189 std::thread::sleep(std::time::Duration::from_secs(4));
192 let failed_id = &emails.data[1].id;
193 let status = resend.emails.get(failed_id).await?;
194 assert_eq!(status.last_event, EmailEvent::Failed);
195
196 Ok(())
197 }
198
199 #[tokio_shared_rt::test(shared = true)]
200 #[cfg(not(feature = "blocking"))]
201 #[allow(clippy::unwrap_used, clippy::indexing_slicing)]
202 async fn permissive_ok() -> DebugResult<()> {
203 let resend = &*CLIENT;
204 std::thread::sleep(std::time::Duration::from_secs(1));
205
206 let emails = vec![
207 CreateEmailBaseOptions::new(
208 "Acme <onboarding@resend.dev>",
209 vec!["delivered@resend.dev"],
210 "hello world",
211 )
212 .with_html("<h1>it works!</h1>"),
213 CreateEmailBaseOptions::new(
214 "Acme <onboarding@resend.dev>",
215 vec!["delivered@resend.dev"],
216 "world hello",
217 )
218 .with_html("<p>it works!</p>"),
219 ];
220
221 let emails = resend
222 .batch
223 .send_with_batch_validation(emails, BatchValidation::Permissive)
224 .await;
225
226 assert!(emails.is_ok());
228 let emails = emails.unwrap();
229
230 assert!(emails.errors.is_empty());
232
233 Ok(())
234 }
235
236 #[tokio_shared_rt::test(shared = true)]
237 #[cfg(not(feature = "blocking"))]
238 #[allow(clippy::unwrap_used, clippy::indexing_slicing)]
239 async fn strict_ok() -> DebugResult<()> {
240 let resend = &*CLIENT;
241 std::thread::sleep(std::time::Duration::from_secs(1));
242
243 let emails = vec![
244 CreateEmailBaseOptions::new(
245 "Acme <onboarding@resend.dev>",
246 vec!["delivered@resend.dev"],
247 "hello world",
248 )
249 .with_html("<h1>it works!</h1>"),
250 CreateEmailBaseOptions::new(
251 "Acme <onboarding@resend.dev>",
252 vec!["delivered@resend.dev"],
253 "world hello",
254 )
255 .with_html("<p>it works!</p>"),
256 ];
257
258 let emails = resend.batch.send(emails).await;
259
260 assert!(emails.is_ok());
262 let _emails = emails.unwrap();
263
264 Ok(())
265 }
266
267 #[tokio_shared_rt::test(shared = true)]
268 #[cfg(not(feature = "blocking"))]
269 async fn template() -> DebugResult<()> {
270 use std::collections::HashMap;
271
272 let resend = &*CLIENT;
273 std::thread::sleep(std::time::Duration::from_secs(1));
274
275 let name = "welcome-email";
277 let html = "<strong>Hey, {{{NAME}}}, you are {{{AGE}}} years old.</strong>";
278 let variables = [
279 Variable::new("NAME", VariableType::String).with_fallback("user"),
280 Variable::new("AGE", VariableType::Number).with_fallback(25),
281 Variable::new("OPTIONAL_VARIABLE", VariableType::String).with_fallback(None::<String>),
282 ];
283 let opts = CreateTemplateOptions::new(name, html).with_variables(&variables);
284 let template = resend.templates.create(opts).await?;
285 std::thread::sleep(std::time::Duration::from_secs(2));
286 let template = resend.templates.publish(&template.id).await?;
287 std::thread::sleep(std::time::Duration::from_secs(2));
288
289 let mut variables1 = HashMap::<String, serde_json::Value>::new();
290 let _added = variables1.insert("NAME".to_string(), serde_json::json!("Tony"));
291 let _added = variables1.insert("AGE".to_string(), serde_json::json!(25));
292
293 let template1 = EmailTemplate::new(&template.id).with_variables(variables1);
294 let template_id = &template1.id.clone();
295
296 let mut variables2 = HashMap::<String, serde_json::Value>::new();
297 let _added = variables2.insert("NAME".to_string(), serde_json::json!("Not Tony"));
298 let _added = variables2.insert("AGE".to_string(), serde_json::json!(42));
299
300 let template2 = EmailTemplate::new(&template.id).with_variables(variables2);
301 let _ = &template2.id.clone();
302
303 let from = "Acme <onboarding@resend.dev>";
305 let to = ["delivered@resend.dev"];
306 let subject = "hello world";
307
308 let emails = vec![
309 CreateEmailBaseOptions::new(from, to, subject).with_template(template1),
310 CreateEmailBaseOptions::new(from, to, subject).with_template(template2),
311 ];
312
313 let _email = resend.batch.send(emails).await?;
314 std::thread::sleep(std::time::Duration::from_secs(2));
315
316 let deleted = resend.templates.delete(template_id).await?;
318 assert!(deleted.deleted);
319
320 Ok(())
321 }
322}