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#[derive(Clone, Debug)]
22pub struct ReceivingSvc(pub(crate) Arc<Config>);
23
24impl ReceivingSvc {
25 #[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 #[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 #[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 #[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 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}