resend_rs/
receiving.rs

1use std::sync::Arc;
2
3use reqwest::Method;
4
5use crate::{
6    Config, Result,
7    list_opts::{ListOptions, ListResponse},
8    types::{InboundAttachment, InboundEmail},
9};
10
11/// `Resend` APIs for `/emails/receiving` endpoints.
12#[derive(Clone, Debug)]
13pub struct ReceivingSvc(pub(crate) Arc<Config>);
14
15impl ReceivingSvc {
16    /// Retrieve a single received email.
17    ///
18    /// <https://resend.com/docs/api-reference/emails/retrieve-received-email>
19    #[maybe_async::maybe_async]
20    pub async fn get(&self, email_id: &str) -> Result<InboundEmail> {
21        let path = format!("/emails/receiving/{email_id}");
22
23        let request = self.0.build(Method::GET, &path);
24        let response = self.0.send(request).await?;
25        let content = response.json::<InboundEmail>().await?;
26
27        Ok(content)
28    }
29
30    /// Retrieve a list of received emails for the authenticated user.
31    ///
32    /// <https://resend.com/docs/api-reference/emails/list-received-emails>
33    #[maybe_async::maybe_async]
34    #[allow(clippy::needless_pass_by_value)]
35    pub async fn list<T>(&self, list_opts: ListOptions<T>) -> Result<ListResponse<InboundEmail>> {
36        let request = self
37            .0
38            .build(Method::GET, "/emails/receiving")
39            .query(&list_opts);
40        let response = self.0.send(request).await?;
41        let content = response.json::<ListResponse<InboundEmail>>().await?;
42
43        Ok(content)
44    }
45
46    /// Retrieve a single attachment from a received email.
47    ///
48    /// <https://resend.com/docs/api-reference/emails/retrieve-received-email>
49    #[maybe_async::maybe_async]
50    pub async fn get_attachment(
51        &self,
52        attachment_id: &str,
53        email_id: &str,
54    ) -> Result<InboundAttachment> {
55        let path = format!("/emails/receiving/{email_id}/attachments/{attachment_id}");
56
57        let request = self.0.build(Method::GET, &path);
58        let response = self.0.send(request).await?;
59        let content = response.json::<InboundAttachment>().await?;
60
61        Ok(content)
62    }
63
64    /// Retrieve a list of email attachments and their contents.
65    ///
66    /// <https://resend.com/docs/api-reference/attachments/list-received-email-attachments>
67    #[maybe_async::maybe_async]
68    #[allow(clippy::needless_pass_by_value)]
69    pub async fn list_attachments<T>(
70        &self,
71        email_id: &str,
72        list_opts: ListOptions<T>,
73    ) -> Result<ListResponse<InboundAttachment>> {
74        let path = format!("/emails/receiving/{email_id}/attachments");
75
76        let request = self.0.build(Method::GET, &path).query(&list_opts);
77        let response = self.0.send(request).await?;
78        let content = response.json::<ListResponse<InboundAttachment>>().await?;
79
80        Ok(content)
81    }
82}
83
84#[allow(unreachable_pub)]
85pub mod types {
86    use std::collections::HashMap;
87
88    use serde::Deserialize;
89
90    crate::define_id_type!(InboundEmailId);
91    crate::define_id_type!(InboundAttatchmentId);
92
93    #[must_use]
94    #[derive(Debug, Clone, Deserialize)]
95    pub struct InboundEmail {
96        pub id: InboundEmailId,
97        pub to: Vec<String>,
98        pub from: String,
99        pub created_at: String,
100        pub subject: String,
101        #[serde(default)]
102        pub bcc: Vec<String>,
103        #[serde(default)]
104        pub cc: Vec<String>,
105        #[serde(default)]
106        pub reply_to: Vec<String>,
107        pub html: Option<String>,
108        pub text: Option<String>,
109        #[serde(default)]
110        pub headers: HashMap<String, String>,
111        pub message_id: String,
112        #[serde(default)]
113        pub attachments: Vec<InboundAttachment>,
114    }
115
116    #[must_use]
117    #[derive(Debug, Clone, Deserialize)]
118    pub struct InboundAttachment {
119        pub id: InboundAttatchmentId,
120        pub filename: String,
121        pub content_type: String,
122        pub content_id: Option<String>,
123        pub content_disposition: String,
124        pub size: u32,
125    }
126}
127
128#[cfg(test)]
129#[allow(clippy::unwrap_used)]
130#[allow(clippy::needless_return)]
131mod test {
132    use crate::{list_opts::ListOptions, types::InboundEmail};
133    use crate::{
134        list_opts::ListResponse,
135        test::{CLIENT, DebugResult},
136    };
137
138    #[ignore = "At the moment, we can't programmatically send inbound emails and since said inbound emails are only retained for 2 weeks, this cannot be automatically tested."]
139    #[tokio_shared_rt::test(shared = true)]
140    #[cfg(not(feature = "blocking"))]
141    async fn all() -> DebugResult<()> {
142        let resend = &*CLIENT;
143
144        // std::thread::sleep(std::time::Duration::from_secs(1));
145
146        let emails = resend.receiving.list(ListOptions::default()).await?;
147
148        let email_id = &emails.data.first().unwrap().id;
149
150        let _email = resend.receiving.get(email_id).await?;
151
152        let attachments = resend
153            .receiving
154            .list_attachments(email_id, ListOptions::default())
155            .await?;
156
157        let attachment_id = &attachments.data.first().unwrap().id;
158
159        let _attachment = resend
160            .receiving
161            .get_attachment(attachment_id, email_id)
162            .await?;
163
164        Ok(())
165    }
166
167    #[test]
168    fn deserialize_test() {
169        let emails = r#"{
170  "object": "list",
171  "has_more": true,
172  "data": [
173    {
174      "id": "a39999a6-88e3-48b1-888b-beaabcde1b33",
175      "to": ["recipient@example.com"],
176      "from": "sender@example.com",
177      "created_at": "2025-10-09 14:37:40.951732+00",
178      "subject": "Hello World",
179      "bcc": [],
180      "cc": [],
181      "reply_to": [],
182      "message_id": "<111-222-333@email.provider.example.com>",
183      "attachments": [
184        {
185          "filename": "example.txt",
186          "content_type": "text/plain",
187          "content_id": null,
188          "content_disposition": "attachment",
189          "id": "47e999c7-c89c-4999-bf32-aaaaa1c3ff21",
190          "size": 13
191        }
192      ]
193    }
194  ]
195}"#;
196
197        let res = serde_json::from_str::<ListResponse<InboundEmail>>(emails);
198        assert!(res.is_ok());
199    }
200}