Skip to main content

resend_rs/
receiving.rs

1use std::sync::Arc;
2
3use ::reqwest::Method;
4use mailparse::{DispositionType, MailHeaderMap};
5
6#[cfg(feature = "blocking")]
7use reqwest::blocking as reqwest;
8
9use crate::{
10    Config, Error, Result,
11    emails::EmailsSvc,
12    list_opts::{ListOptions, ListResponse},
13    receiving::types::ForwardReceivingEmail,
14    types::{
15        Attachment, CreateAttachment, CreateEmailBaseOptions, ForwardInboundEmailResponse,
16        GetInboundEmailOptions, InboundEmail, InboundEmailId,
17    },
18};
19
20/// `Resend` APIs for `/emails/receiving` endpoints.
21#[derive(Clone, Debug)]
22pub struct ReceivingSvc(pub(crate) Arc<Config>);
23
24impl ReceivingSvc {
25    /// Retrieve a single received email.
26    ///
27    /// <https://resend.com/docs/api-reference/emails/retrieve-received-email>
28    #[maybe_async::maybe_async]
29    pub async fn get(&self, email_id: &str, opts: GetInboundEmailOptions) -> Result<InboundEmail> {
30        let path = format!("/emails/receiving/{email_id}");
31
32        let request = self.0.build(Method::GET, &path).query(&opts);
33        let response = self.0.send(request).await?;
34        let content = response.json::<InboundEmail>().await?;
35
36        Ok(content)
37    }
38
39    /// Retrieve a list of received emails for the authenticated user.
40    ///
41    /// <https://resend.com/docs/api-reference/emails/list-received-emails>
42    #[maybe_async::maybe_async]
43    pub async fn list<T>(&self, list_opts: ListOptions<T>) -> Result<ListResponse<InboundEmail>> {
44        let request = self
45            .0
46            .build(Method::GET, "/emails/receiving")
47            .query(&list_opts);
48        let response = self.0.send(request).await?;
49        let content = response.json::<ListResponse<InboundEmail>>().await?;
50
51        Ok(content)
52    }
53
54    /// Retrieve a single attachment from a received email.
55    ///
56    /// <https://resend.com/docs/api-reference/emails/retrieve-received-email>
57    #[maybe_async::maybe_async]
58    pub async fn get_attachment(&self, attachment_id: &str, email_id: &str) -> Result<Attachment> {
59        let path = format!("/emails/receiving/{email_id}/attachments/{attachment_id}");
60
61        let request = self.0.build(Method::GET, &path);
62        let response = self.0.send(request).await?;
63        let content = response.json::<Attachment>().await?;
64
65        Ok(content)
66    }
67
68    /// Retrieve a list of email attachments and their contents.
69    ///
70    /// <https://resend.com/docs/api-reference/attachments/list-received-email-attachments>
71    #[maybe_async::maybe_async]
72    pub async fn list_attachments<T>(
73        &self,
74        email_id: &str,
75        list_opts: ListOptions<T>,
76    ) -> Result<ListResponse<Attachment>> {
77        let path = format!("/emails/receiving/{email_id}/attachments");
78
79        let request = self.0.build(Method::GET, &path).query(&list_opts);
80        let response = self.0.send(request).await?;
81        let content = response.json::<ListResponse<Attachment>>().await?;
82
83        Ok(content)
84    }
85
86    #[maybe_async::maybe_async]
87    pub async fn forward(
88        &self,
89        opts: ForwardReceivingEmail,
90    ) -> Result<ForwardInboundEmailResponse> {
91        let email_response = self
92            .get(&opts.email_id, GetInboundEmailOptions::default())
93            .await?;
94
95        let raw = email_response.raw.ok_or_else(|| {
96            Error::Resend(crate::types::ErrorResponse {
97                status_code: 400,
98                message: "Raw email content is not available for this email".to_owned(),
99                name: "validation_error".to_owned(),
100            })
101        })?;
102
103        let raw_response_content = reqwest::get(raw.download_url).await?.bytes().await?;
104
105        let email_svc = EmailsSvc(Arc::<Config>::clone(&self.0));
106
107        if opts.passthrough {
108            let parsed =
109                mailparse::parse_mail(&raw_response_content).map_err(|e| Error::Parse {
110                    message: "Failed to parse raw email".to_owned(),
111                    source: Some(Box::new(e)),
112                })?;
113
114            let attachments = parsed
115                .subparts
116                .iter()
117                .filter(|el| {
118                    el.get_content_disposition().disposition == DispositionType::Attachment
119                })
120                .map(|attachment| {
121                    let disposition = attachment.get_content_disposition();
122
123                    let filename = disposition
124                        .params
125                        .get("filename")
126                        .ok_or_else(|| Error::Other("Could not get filename".to_string()))?
127                        .to_owned();
128                    let content = attachment
129                        .get_body_raw()
130                        .map_err(|_e| Error::Other("Could not get attachment body".to_string()))?;
131                    let content_type = attachment.ctype.mimetype.clone();
132
133                    if let Some(content_id) = attachment.headers.get_first_header("Content-ID") {
134                        let mut content_id = content_id.get_key();
135                        if content_id.starts_with('<') {
136                            content_id = content_id[1..content_id.len() - 1].to_string();
137                        }
138
139                        let attachment = CreateAttachment::from_content(content)
140                            .with_content_id(&content_id)
141                            .with_filename(&filename)
142                            .with_content_type(&content_type);
143                        Ok(attachment)
144                    } else {
145                        let attachment = CreateAttachment::from_content(content)
146                            .with_filename(&filename)
147                            .with_content_type(&content_type);
148                        Ok(attachment)
149                    }
150                })
151                .collect::<Result<Vec<_>>>()?;
152
153            let mut email = CreateEmailBaseOptions::new(opts.from, opts.to, email_response.subject)
154                .with_attachments(attachments);
155
156            if let Some(text) = &opts.text {
157                email = email.with_text(text);
158            } else if let Some(html) = &opts.html {
159                email = email.with_html(html);
160            }
161
162            let res = email_svc.send(email).await?;
163
164            Ok(ForwardInboundEmailResponse {
165                id: InboundEmailId::new(&res.id),
166            })
167        } else {
168            let subject = if email_response.subject.starts_with("Fwd:") {
169                email_response.subject
170            } else {
171                format!("Fwd: {}", email_response.subject)
172            };
173
174            let attachment = CreateAttachment::from_content(raw_response_content.to_vec())
175                .with_filename("forwarded_message.eml")
176                .with_content_type("message/rfc822");
177
178            let mut email = CreateEmailBaseOptions::new(opts.from, opts.to, subject)
179                .with_attachments(vec![attachment]);
180
181            if let Some(text) = &opts.text {
182                email = email.with_text(text);
183            } else if let Some(html) = &opts.html {
184                email = email.with_html(html);
185            }
186
187            let res = email_svc.send(email).await?;
188
189            Ok(ForwardInboundEmailResponse {
190                id: InboundEmailId::new(&res.id),
191            })
192        }
193    }
194}
195
196#[allow(unreachable_pub)]
197pub mod types {
198    use std::collections::HashMap;
199
200    use serde::{Deserialize, Serialize};
201
202    crate::define_id_type!(InboundEmailId);
203    crate::define_id_type!(InboundAttachmentId);
204
205    #[must_use]
206    #[derive(Debug, Clone, Serialize, Deserialize)]
207    pub struct Raw {
208        pub download_url: String,
209        pub expires_at: String,
210    }
211
212    #[must_use]
213    #[derive(Debug, Clone, Serialize, Deserialize, Default)]
214    pub struct GetInboundEmailOptions {
215        html_format: InboundEmailHtmlFormat,
216        #[serde(skip_serializing_if = "Option::is_none")]
217        raw: Option<GetInboundEmailRaw>,
218    }
219
220    impl GetInboundEmailOptions {
221        #[inline]
222        pub fn with_html_format(mut self, html_format: InboundEmailHtmlFormat) -> Self {
223            self.html_format = html_format;
224            self
225        }
226
227        #[inline]
228        pub fn with_raw(mut self, raw: GetInboundEmailRaw) -> Self {
229            self.raw = Some(raw);
230            self
231        }
232    }
233
234    #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Default)]
235    #[must_use]
236    #[serde(rename_all = "snake_case")]
237    pub enum InboundEmailHtmlFormat {
238        #[default]
239        DataUri,
240        Cid,
241    }
242
243    #[must_use]
244    #[derive(Debug, Clone, Serialize, Deserialize)]
245    pub struct GetInboundEmailRaw {
246        pub download_url: String,
247        pub expires_at: String,
248    }
249
250    #[must_use]
251    #[derive(Debug, Clone, Serialize, Deserialize)]
252    pub struct InboundEmail {
253        pub id: InboundEmailId,
254        pub to: Vec<String>,
255        pub from: String,
256        pub created_at: String,
257        pub subject: String,
258        #[serde(default)]
259        pub bcc: Vec<String>,
260        #[serde(default)]
261        pub cc: Vec<String>,
262        #[serde(default)]
263        pub reply_to: Vec<String>,
264        #[serde(default)]
265        pub received_for: Vec<String>,
266        pub html: Option<String>,
267        pub text: Option<String>,
268        #[serde(default)]
269        pub headers: HashMap<String, String>,
270        pub message_id: String,
271        pub raw: Option<Raw>,
272        #[serde(default)]
273        pub attachments: Vec<InboundAttachment>,
274    }
275
276    #[must_use]
277    #[derive(Debug, Clone, Serialize, Deserialize)]
278    pub struct InboundAttachment {
279        pub id: InboundAttachmentId,
280        pub filename: Option<String>,
281        pub size: Option<u32>,
282        pub content_type: String,
283        pub content_id: Option<String>,
284        pub content_disposition: Option<String>,
285    }
286
287    #[must_use]
288    #[derive(Debug, Clone, Serialize, Deserialize)]
289    pub struct ForwardReceivingEmail {
290        pub(crate) passthrough: bool,
291
292        #[serde(skip_serializing_if = "Option::is_none")]
293        pub(crate) text: Option<String>,
294        #[serde(skip_serializing_if = "Option::is_none")]
295        pub(crate) html: Option<String>,
296
297        pub(crate) email_id: InboundEmailId,
298        pub(crate) to: Vec<String>,
299        pub(crate) from: String,
300    }
301
302    impl ForwardReceivingEmail {
303        pub fn new(
304            email_id: InboundEmailId,
305            from: impl Into<String>,
306            to: impl IntoIterator<Item = impl Into<String>>,
307        ) -> Self {
308            Self {
309                passthrough: true,
310                text: None,
311                html: None,
312                email_id,
313                to: to.into_iter().map(Into::into).collect(),
314                from: from.into(),
315            }
316        }
317    }
318
319    impl ForwardReceivingEmail {
320        #[inline]
321        pub fn with_passthrough(mut self, passthrough: bool) -> Self {
322            self.passthrough = passthrough;
323            self
324        }
325
326        #[inline]
327        pub fn with_text(mut self, text: &str) -> Self {
328            self.text = Some(text.to_owned());
329            self
330        }
331
332        #[inline]
333        pub fn with_html(mut self, html: &str) -> Self {
334            self.html = Some(html.to_owned());
335            self
336        }
337    }
338
339    #[derive(Debug, Clone, Serialize, Deserialize)]
340    pub struct ForwardInboundEmailResponse {
341        pub id: InboundEmailId,
342    }
343}
344
345#[cfg(test)]
346#[allow(clippy::unwrap_used)]
347#[allow(clippy::needless_return)]
348mod test {
349    #[cfg(not(feature = "blocking"))]
350    use crate::{
351        list_opts::ListOptions,
352        test::{CLIENT, DebugResult},
353        types::ForwardReceivingEmail,
354    };
355    use crate::{list_opts::ListResponse, types::InboundEmail};
356
357    #[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."]
358    #[tokio_shared_rt::test(shared = true)]
359    #[serial_test::serial]
360    #[cfg(not(feature = "blocking"))]
361    async fn all() -> DebugResult<()> {
362        use crate::types::GetInboundEmailOptions;
363
364        let resend = &*CLIENT;
365
366        // std::thread::sleep(std::time::Duration::from_secs(1));
367
368        let emails = resend.receiving.list(ListOptions::default()).await?;
369
370        let email_id = &emails.data.first().unwrap().id;
371
372        let _email = resend
373            .receiving
374            .get(email_id, GetInboundEmailOptions::default())
375            .await?;
376
377        let fwd_opts = ForwardReceivingEmail::new(
378            email_id.clone(),
379            "test@resend.dev",
380            vec!["delivered@resend.dev"],
381        )
382        .with_text("text")
383        .with_passthrough(true);
384        let _fwd_res = resend.receiving.forward(fwd_opts).await?;
385
386        let attachments = resend
387            .receiving
388            .list_attachments(email_id, ListOptions::default())
389            .await?;
390
391        let attachment_id = &attachments.data.first().unwrap().id;
392
393        let _attachment = resend
394            .receiving
395            .get_attachment(attachment_id, email_id)
396            .await?;
397
398        Ok(())
399    }
400
401    #[test]
402    fn deserialize_test() {
403        let emails = r#"{
404  "object": "list",
405  "has_more": true,
406  "data": [
407    {
408      "id": "a39999a6-88e3-48b1-888b-beaabcde1b33",
409      "to": ["recipient@example.com"],
410      "from": "sender@example.com",
411      "created_at": "2025-10-09 14:37:40.951732+00",
412      "subject": "Hello World",
413      "bcc": [],
414      "cc": [],
415      "reply_to": [],
416      "message_id": "<111-222-333@email.provider.example.com>",
417      "attachments": [
418        {
419          "filename": "example.txt",
420          "content_type": "text/plain",
421          "content_id": null,
422          "content_disposition": "attachment",
423          "id": "47e999c7-c89c-4999-bf32-aaaaa1c3ff21",
424          "size": 13
425        }
426      ]
427    }
428  ]
429}"#;
430
431        let res = serde_json::from_str::<ListResponse<InboundEmail>>(emails);
432        assert!(res.is_ok());
433    }
434
435    #[test]
436    fn deserialize_test2() {
437        let emails = r#"{
438  "object": "list",
439  "has_more": true,
440  "data": [
441    {
442      "id": "a39999a6-88e3-48b1-888b-beaabcde1b33",
443      "to": ["recipient@example.com"],
444      "from": "sender@example.com",
445      "created_at": "2025-10-09 14:37:40.951732+00",
446      "subject": "Hello World",
447      "bcc": [],
448      "cc": [],
449      "reply_to": [],
450      "received_for": ["forwarded@example.com"],
451      "message_id": "<111-222-333@email.provider.example.com>",
452      "raw": {
453        "download_url": "https://example.com/emails/raw/abc123?signature=xyz789",
454        "expires_at": "2023-04-08 00:13:52.669661+00"
455      },
456      "attachments": [
457        {
458          "filename": "example.txt",
459          "content_type": "text/plain",
460          "content_id": null,
461          "content_disposition": "attachment",
462          "id": "47e999c7-c89c-4999-bf32-aaaaa1c3ff21",
463          "size": 13
464        }
465      ]
466    }
467  ]
468}"#;
469
470        let res = serde_json::from_str::<ListResponse<InboundEmail>>(emails);
471        assert!(res.is_ok());
472        let data = res.unwrap();
473        let email = data.data.first().unwrap();
474        assert!(email.raw.is_some());
475        assert_eq!(email.received_for, vec!["forwarded@example.com"]);
476    }
477}