Skip to main content

resend_rs/
suppressions.rs

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/// `Resend` APIs for `/suppressions` endpoints.
17#[derive(Clone, Debug)]
18pub struct SuppressionsSvc(pub(crate) Arc<Config>);
19
20impl SuppressionsSvc {
21    /// Add an email address to the suppression list.
22    ///
23    /// <https://resend.com/docs/api-reference/suppressions/add-suppression>
24    #[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    /// Retrieve a single suppression by ID or email.
34    ///
35    /// <https://resend.com/docs/api-reference/suppressions/get-suppression>
36    #[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    /// Show all suppressions.
49    ///
50    /// - Default limit: 20
51    ///
52    /// <https://resend.com/docs/api-reference/suppressions/list-suppressions>
53    #[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    /// Remove a single suppression by ID or email.
63    ///
64    /// <https://resend.com/docs/api-reference/suppressions/remove-suppression>
65    #[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    /// Add up to 100 email addresses to the suppression list at once.
78    ///
79    /// <https://resend.com/docs/api-reference/suppressions/add-suppressions>
80    #[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    /// Remove up to 100 suppressions from the suppression list at once.
93    ///
94    /// This endpoint requires that you have specified either ids or emails but not both,
95    /// hence you need [`BatchRemoveSuppressionOptions<EmailsSpecified>`] or
96    /// [`BatchRemoveSuppressionOptions<IdsSpecified>`].
97    ///
98    /// You can create those by doing:
99    ///
100    /// ```rust
101    /// # use resend_rs::types::BatchRemoveSuppressionOptions;
102    /// let _tmp = BatchRemoveSuppressionOptions::new().add_emails(vec!["emails"]);
103    /// let _tmp = BatchRemoveSuppressionOptions::new().add_ids(vec!["ids"]);
104    /// ```
105    ///
106    /// <https://resend.com/docs/api-reference/suppressions/remove-suppressions>
107    #[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        // Add
399        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        // Get
404        let suppression = resend.suppressions.get(&suppression.id).await?;
405
406        // List
407        let suppressions = resend.suppressions.list(ListOptions::default()).await?;
408        assert_eq!(suppressions.len(), 1);
409
410        // Remove
411        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        // Batch add
420        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        // Batch remove
433        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}