1use std::sync::Arc;
2
3use reqwest::Method;
4
5use crate::{
6 Config, Result,
7 list_opts::{ListOptions, ListResponse},
8 suppressions::types::SpecifiedMarker,
9 types::{
10 AddSuppressionOptions, AddSuppressionResponse, BatchAddSuppressionOptions,
11 BatchAddSuppressionResponse, BatchRemoveSuppressionOptions,
12 BatchRemoveSuppressionsResponse, RemoveSuppressionResponse, Suppression,
13 },
14};
15
16#[derive(Clone, Debug)]
18pub struct SuppressionsSvc(pub(crate) Arc<Config>);
19
20impl SuppressionsSvc {
21 #[maybe_async::maybe_async]
25 pub async fn add(&self, opts: AddSuppressionOptions) -> Result<AddSuppressionResponse> {
26 let request = self.0.build(Method::POST, "/suppressions");
27 let response = self.0.send(request.json(&opts)).await?;
28 let content = response.json::<AddSuppressionResponse>().await?;
29
30 Ok(content)
31 }
32
33 #[maybe_async::maybe_async]
37 pub async fn get(&self, id_or_email: &str) -> Result<Suppression> {
38 let id_or_email = urlencoding::encode(id_or_email);
39 let path = format!("/suppressions/{id_or_email}");
40
41 let request = self.0.build(Method::GET, &path);
42 let response = self.0.send(request).await?;
43 let content = response.json::<Suppression>().await?;
44
45 Ok(content)
46 }
47
48 #[maybe_async::maybe_async]
54 pub async fn list<T>(&self, list_opts: ListOptions<T>) -> Result<ListResponse<Suppression>> {
55 let request = self.0.build(Method::GET, "/suppressions").query(&list_opts);
56 let response = self.0.send(request).await?;
57 let content = response.json::<ListResponse<Suppression>>().await?;
58
59 Ok(content)
60 }
61
62 #[maybe_async::maybe_async]
66 pub async fn remove(&self, id_or_email: &str) -> Result<RemoveSuppressionResponse> {
67 let id_or_email = urlencoding::encode(id_or_email);
68 let path = format!("/suppressions/{id_or_email}");
69
70 let request = self.0.build(Method::DELETE, &path);
71 let response = self.0.send(request).await?;
72 let content = response.json::<RemoveSuppressionResponse>().await?;
73
74 Ok(content)
75 }
76
77 #[maybe_async::maybe_async]
81 pub async fn batch_add(
82 &self,
83 opts: BatchAddSuppressionOptions,
84 ) -> Result<BatchAddSuppressionResponse> {
85 let request = self.0.build(Method::POST, "/suppressions/batch/add");
86 let response = self.0.send(request.json(&opts)).await?;
87 let content = response.json::<BatchAddSuppressionResponse>().await?;
88
89 Ok(content)
90 }
91
92 #[maybe_async::maybe_async]
108 #[allow(private_bounds)]
109 pub async fn batch_remove<M: SpecifiedMarker>(
110 &self,
111 ids_or_emails: BatchRemoveSuppressionOptions<M>,
112 ) -> Result<BatchRemoveSuppressionsResponse> {
113 let request = self.0.build(Method::POST, "/suppressions/batch/remove");
114 let response = self.0.send(request.json(&ids_or_emails)).await?;
115 let content = response.json::<BatchRemoveSuppressionsResponse>().await?;
116
117 Ok(content)
118 }
119}
120
121#[allow(unreachable_pub)]
122pub mod types {
123 use std::borrow::ToOwned;
124
125 use serde::{Deserialize, Serialize};
126
127 use crate::types::EmailId;
128
129 crate::define_id_type!(SuppressionId);
130
131 #[must_use]
132 #[derive(Debug, Default, Clone, Serialize)]
133 pub struct AddSuppressionOptions {
134 email: String,
135 }
136
137 impl AddSuppressionOptions {
138 #[inline]
139 pub fn new() -> Self {
140 Self::default()
141 }
142
143 #[inline]
144 pub fn with_email(mut self, email: &str) -> Self {
145 email.clone_into(&mut self.email);
146 self
147 }
148 }
149
150 #[must_use]
151 #[derive(Debug, Clone, Deserialize)]
152 pub struct AddSuppressionResponse {
153 pub id: SuppressionId,
154 }
155
156 #[must_use]
157 #[derive(Debug, Clone, Deserialize)]
158 pub struct Suppression {
159 pub id: SuppressionId,
160 pub email: String,
161 pub created_at: String,
162 pub origin: SuppressionOrigin,
163 pub source_id: Option<EmailId>,
164 }
165
166 #[must_use]
167 #[derive(Debug, Clone, Copy, Deserialize)]
168 #[serde(rename_all = "snake_case")]
169 pub enum SuppressionOrigin {
170 Bounce,
171 Complaint,
172 Manual,
173 }
174
175 #[must_use]
176 #[derive(Debug, Clone, Deserialize)]
177 pub struct RemoveSuppressionResponse {
178 pub id: SuppressionId,
179 pub deleted: bool,
180 }
181
182 #[must_use]
183 #[derive(Debug, Default, Clone, Serialize)]
184 pub struct BatchAddSuppressionOptions {
185 emails: Vec<String>,
186 }
187
188 impl BatchAddSuppressionOptions {
189 #[inline]
190 pub fn new() -> Self {
191 Self::default()
192 }
193
194 #[inline]
195 pub fn add_email(mut self, email: &str) -> Self {
196 self.emails.push(email.to_owned());
197 self
198 }
199
200 #[inline]
201 pub fn add_emails(mut self, emails: impl IntoIterator<Item = impl Into<String>>) -> Self {
202 self.emails.extend(emails.into_iter().map(Into::into));
203 self
204 }
205 }
206
207 impl From<Vec<String>> for BatchAddSuppressionOptions {
208 fn from(value: Vec<String>) -> Self {
209 Self::new().add_emails(value)
210 }
211 }
212 impl From<Vec<&str>> for BatchAddSuppressionOptions {
213 fn from(value: Vec<&str>) -> Self {
214 Self::new().add_emails(value.into_iter().map(ToOwned::to_owned))
215 }
216 }
217
218 #[must_use]
219 #[derive(Debug, Clone, Deserialize)]
220 pub struct BatchAddSuppressionResponse {
221 pub data: Vec<AddSuppressionResponse>,
222 }
223
224 #[allow(clippy::redundant_pub_crate)]
225 pub(crate) trait SpecifiedMarker {}
226 impl SpecifiedMarker for EmailsSpecified {}
227 impl SpecifiedMarker for IdsSpecified {}
228
229 #[derive(Debug, Clone, Copy, Default)]
230 pub struct NotSpecified {}
231
232 #[derive(Debug, Clone, Copy)]
233 pub struct EmailsSpecified {}
234
235 #[derive(Debug, Clone, Copy)]
236 pub struct IdsSpecified {}
237
238 #[must_use]
239 #[derive(Debug, Default, Clone, Serialize)]
240 pub struct BatchRemoveSuppressionOptions<Marker = NotSpecified> {
241 #[serde(skip)]
242 marker: std::marker::PhantomData<Marker>,
243
244 #[serde(skip_serializing_if = "Vec::is_empty")]
245 emails: Vec<String>,
246 #[serde(skip_serializing_if = "Vec::is_empty")]
247 ids: Vec<String>,
248 }
249
250 impl BatchRemoveSuppressionOptions {
251 #[inline]
252 pub fn new() -> Self {
253 Self {
254 marker: std::marker::PhantomData::<NotSpecified>,
255 emails: vec![],
256 ids: vec![],
257 }
258 }
259 }
260
261 impl BatchRemoveSuppressionOptions<NotSpecified> {
262 #[inline]
263 pub fn add_emails(
264 self,
265 emails: impl IntoIterator<Item = impl Into<String>>,
266 ) -> BatchRemoveSuppressionOptions<EmailsSpecified> {
267 BatchRemoveSuppressionOptions::<EmailsSpecified> {
268 marker: std::marker::PhantomData,
269 emails: emails.into_iter().map(Into::into).collect(),
270 ids: self.ids,
271 }
272 }
273
274 #[inline]
275 pub fn add_ids(
276 self,
277 ids: impl IntoIterator<Item = impl Into<String>>,
278 ) -> BatchRemoveSuppressionOptions<IdsSpecified> {
279 BatchRemoveSuppressionOptions::<IdsSpecified> {
280 marker: std::marker::PhantomData,
281 emails: self.emails,
282 ids: ids.into_iter().map(Into::into).collect(),
283 }
284 }
285 }
286
287 #[must_use]
288 #[derive(Debug, Clone, Deserialize)]
289 pub struct BatchRemoveSuppressionsResponse {
290 pub data: Vec<RemoveSuppressionResponse>,
291 }
292}
293
294#[cfg(test)]
295#[allow(clippy::unwrap_used)]
296mod test {
297 #[cfg(not(feature = "blocking"))]
298 use crate::test::{CLIENT, DebugResult};
299 use crate::{
300 list_opts::ListResponse,
301 types::{
302 AddSuppressionResponse, BatchAddSuppressionResponse, BatchRemoveSuppressionsResponse,
303 RemoveSuppressionResponse, Suppression,
304 },
305 };
306
307 #[test]
308 fn deserialize() {
309 let add_suppression = r#"{
310 "object": "suppression",
311 "id": "e169aa45-1ecf-4183-9955-b1499d5701d3"
312 }"#;
313 let get_suppression = r#"{
314 "object": "suppression",
315 "id": "e169aa45-1ecf-4183-9955-b1499d5701d3",
316 "email": "steve.wozniak@example.com",
317 "origin": "bounce",
318 "source_id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
319 "created_at": "2026-10-06 23:47:56.678+00"
320 }"#;
321 let list_suppressions = r#"{
322 "object": "list",
323 "has_more": false,
324 "data": [
325 {
326 "object": "suppression",
327 "id": "e169aa45-1ecf-4183-9955-b1499d5701d3",
328 "email": "steve.wozniak@example.com",
329 "origin": "manual",
330 "source_id": null,
331 "created_at": "2026-10-06 23:47:56.678+00"
332 },
333 {
334 "object": "suppression",
335 "id": "520784e2-887d-4c25-b53c-4ad46ad38100",
336 "email": "susan.kare@example.com",
337 "origin": "bounce",
338 "source_id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
339 "created_at": "2026-10-07 08:12:03.412+00"
340 }
341 ]
342 }"#;
343 let remove_suppression = r#"{
344 "object": "suppression",
345 "id": "e169aa45-1ecf-4183-9955-b1499d5701d3",
346 "deleted": true
347 }"#;
348 let add_suppressions = r#"{
349 "data": [
350 {
351 "object": "suppression",
352 "id": "e169aa45-1ecf-4183-9955-b1499d5701d3"
353 },
354 {
355 "object": "suppression",
356 "id": "520784e2-887d-4c25-b53c-4ad46ad38100"
357 }
358 ]
359 }"#;
360 let remove_suppressions = r#"{
361 "data": [
362 {
363 "object": "suppression",
364 "id": "e169aa45-1ecf-4183-9955-b1499d5701d3",
365 "deleted": true
366 }
367 ]
368 }"#;
369
370 let res = serde_json::from_str::<AddSuppressionResponse>(add_suppression);
371 assert!(res.is_ok());
372 let res = serde_json::from_str::<Suppression>(get_suppression);
373 assert!(res.is_ok());
374 let res = serde_json::from_str::<ListResponse<Suppression>>(list_suppressions);
375 assert!(res.is_ok());
376 let res = serde_json::from_str::<RemoveSuppressionResponse>(remove_suppression);
377 assert!(res.is_ok());
378 let res = serde_json::from_str::<BatchAddSuppressionResponse>(add_suppressions);
379 assert!(res.is_ok());
380 let res = serde_json::from_str::<BatchRemoveSuppressionsResponse>(remove_suppressions);
381 assert!(res.is_ok());
382 }
383
384 #[tokio_shared_rt::test(shared = true)]
385 #[serial_test::serial]
386 #[cfg(not(feature = "blocking"))]
387 async fn all() -> DebugResult<()> {
388 use crate::{
389 list_opts::ListOptions,
390 types::{
391 AddSuppressionOptions, BatchAddSuppressionOptions, BatchRemoveSuppressionOptions,
392 },
393 };
394
395 let resend = &*CLIENT;
396 std::thread::sleep(std::time::Duration::from_secs(1));
397
398 let opts = AddSuppressionOptions::new().with_email("steve.wozniak@example.com");
400 let suppression = resend.suppressions.add(opts).await?;
401 std::thread::sleep(std::time::Duration::from_secs(2));
402
403 let suppression = resend.suppressions.get(&suppression.id).await?;
405
406 let suppressions = resend.suppressions.list(ListOptions::default()).await?;
408 assert_eq!(suppressions.len(), 1);
409
410 let removed = resend.suppressions.remove(&suppression.id).await?;
412 assert!(removed.deleted);
413
414 std::thread::sleep(std::time::Duration::from_secs(2));
415
416 let suppressions = resend.suppressions.list(ListOptions::default()).await?;
417 assert_eq!(suppressions.len(), 0);
418
419 let opts = vec!["steve.wozniak@example.com", "susan.kare@example.com"];
421 let batch_add = resend
422 .suppressions
423 .batch_add(BatchAddSuppressionOptions::from(opts))
424 .await?;
425 assert_eq!(batch_add.data.len(), 2);
426
427 std::thread::sleep(std::time::Duration::from_secs(2));
428
429 let suppressions = resend.suppressions.list(ListOptions::default()).await?;
430 assert_eq!(suppressions.len(), 2);
431
432 let opts = batch_add
434 .data
435 .into_iter()
436 .map(|el| el.id.to_string())
437 .collect::<Vec<_>>();
438 let batch_remove = resend
439 .suppressions
440 .batch_remove(BatchRemoveSuppressionOptions::new().add_ids(opts))
441 .await?;
442 assert_eq!(batch_remove.data.len(), 2);
443
444 std::thread::sleep(std::time::Duration::from_secs(2));
445
446 let suppressions = resend.suppressions.list(ListOptions::default()).await?;
447 assert_eq!(suppressions.len(), 0);
448
449 Ok(())
450 }
451}