From 729d3927c6a308c60c54a70244080dcff8efa13e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 10 Jul 2026 20:56:26 +0000 Subject: [PATCH 1/3] feat(batch): document and test scheduled_at, tags, and attachments Batch send already serializes these fields via CreateEmailBaseOptions. Add integration tests and docs to reflect the updated batch API schema. Co-authored-by: cpenned --- CHANGELOG.md | 1 + src/batch.rs | 164 +++++++++++++++++++++++++++++++++++++++++++++++++- src/emails.rs | 6 +- 3 files changed, 167 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e021dd2..181ccc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to ### Added +- batch send support for `scheduled_at`, `tags`, and `attachments` on [`CreateEmailBaseOptions`](https://docs.rs/resend-rs/latest/resend_rs/types/struct.CreateEmailBaseOptions.html) - add `message_id` to `Email` and webhook `EmailBody` types (https://github.com/resend/resend-rust/pull/77) diff --git a/src/batch.rs b/src/batch.rs index d6d33e7..daac066 100644 --- a/src/batch.rs +++ b/src/batch.rs @@ -20,6 +20,9 @@ impl BatchSvc { /// Instead of sending one email per HTTP request, we provide a batching endpoint /// that permits you to send up to 100 emails in a single API call. /// + /// Each [`CreateEmailBaseOptions`] in the batch supports `scheduled_at`, `tags`, + /// and `attachments`, in addition to the other send-email body parameters. + /// /// #[maybe_async::maybe_async] pub async fn send( @@ -122,8 +125,8 @@ pub mod types { mod test { use crate::test::{CLIENT, DebugResult}; use crate::types::{ - BatchValidation, CreateEmailBaseOptions, CreateTemplateOptions, EmailEvent, EmailTemplate, - Variable, VariableType, + BatchValidation, CreateAttachment, CreateEmailBaseOptions, CreateTemplateOptions, + EmailEvent, EmailTemplate, Tag, Variable, VariableType, }; #[tokio_shared_rt::test(shared = true)] @@ -323,4 +326,161 @@ mod test { Ok(()) } + + #[test] + fn serialize_schedule_tags_attachments() { + use serde_json::json; + + let email = CreateEmailBaseOptions::new( + "Acme ", + ["delivered@resend.dev"], + "hello world", + ) + .with_html("

it works!

") + .with_tag(Tag::new("category", "confirm_email")) + .with_scheduled_at("2025-09-25T11:52:01.858Z") + .with_attachment( + CreateAttachment::from_content(b"hello".to_vec()).with_filename("test.txt"), + ); + + let emails = vec![email]; + let value = serde_json::to_value(&emails).unwrap(); + + let email = &value[0]; + assert_eq!( + email["tags"], + json!([{"name": "category", "value": "confirm_email"}]) + ); + assert_eq!(email["scheduled_at"], json!("2025-09-25T11:52:01.858Z")); + assert!(email["attachments"].is_array()); + assert_eq!(email["attachments"][0]["filename"], "test.txt"); + } + + #[tokio_shared_rt::test(shared = true)] + #[cfg(not(feature = "blocking"))] + async fn tags() -> DebugResult<()> { + let resend = &*CLIENT; + std::thread::sleep(std::time::Duration::from_secs(1)); + + let emails = vec![ + CreateEmailBaseOptions::new( + "Acme ", + ["delivered@resend.dev"], + "hello world", + ) + .with_html("

it works!

") + .with_tag(Tag::new("category", "confirm_email")), + CreateEmailBaseOptions::new( + "Acme ", + ["delivered@resend.dev"], + "world hello", + ) + .with_html("

it works!

") + .with_tag(Tag::new("category", "confirm_email")), + ]; + + let emails = resend.batch.send(emails).await?; + assert_eq!(emails.len(), 2); + + Ok(()) + } + + #[tokio_shared_rt::test(shared = true)] + #[cfg(not(feature = "blocking"))] + async fn schedule() -> DebugResult<()> { + use jiff::{Span, Timestamp, Zoned}; + + let now_plus_1h = Zoned::now() + .checked_add(Span::new().hours(1)) + .expect("Valid date") + .timestamp() + .to_string(); + + let resend = &*CLIENT; + std::thread::sleep(std::time::Duration::from_secs(1)); + + let emails = vec![ + CreateEmailBaseOptions::new( + "Acme ", + ["delivered@resend.dev"], + "hello world", + ) + .with_html("

it works!

") + .with_scheduled_at(&now_plus_1h), + CreateEmailBaseOptions::new( + "Acme ", + ["delivered@resend.dev"], + "world hello", + ) + .with_html("

it works!

") + .with_scheduled_at(&now_plus_1h), + ]; + + let emails = resend.batch.send(emails).await?; + assert_eq!(emails.len(), 2); + std::thread::sleep(std::time::Duration::from_secs(4)); + + for email in emails { + let email = resend.emails.get(&email.id).await?; + assert_eq!(email.last_event, EmailEvent::Scheduled); + assert!(email.scheduled_at.is_some()); + let time = email + .scheduled_at + .unwrap() + .parse::() + .expect("Valid timestamp"); + let time_delta = (time - Timestamp::now()).round(jiff::Unit::Hour).unwrap(); + assert_eq!( + time_delta.compare(Span::new().hours(1)).unwrap(), + std::cmp::Ordering::Equal + ); + + let _cancelled = resend.emails.cancel(&email.id).await?; + } + + Ok(()) + } + + #[tokio_shared_rt::test(shared = true)] + #[cfg(not(feature = "blocking"))] + async fn attachments() -> DebugResult<()> { + use crate::list_opts::ListOptions; + + let resend = &*CLIENT; + std::thread::sleep(std::time::Duration::from_secs(1)); + + let attachment = CreateAttachment::from_content(include_bytes!("../README.md").to_vec()) + .with_filename("README.md"); + + let emails = vec![ + CreateEmailBaseOptions::new( + "Acme ", + ["delivered@resend.dev"], + "hello world", + ) + .with_html("

it works!

") + .with_attachment(attachment.clone()), + CreateEmailBaseOptions::new( + "Acme ", + ["delivered@resend.dev"], + "world hello", + ) + .with_html("

it works!

") + .with_attachment(attachment), + ]; + + let emails = resend.batch.send(emails).await?; + assert_eq!(emails.len(), 2); + std::thread::sleep(std::time::Duration::from_secs(1)); + + for email in emails { + let attachments = resend + .emails + .list_attachments(&email.id, ListOptions::default()) + .await?; + assert_eq!(attachments.data.len(), 1); + } + + Ok(()) + } } diff --git a/src/emails.rs b/src/emails.rs index 68432b3..425681c 100644 --- a/src/emails.rs +++ b/src/emails.rs @@ -160,9 +160,11 @@ pub mod types { /// All requisite components and associated data to send an email. /// - /// See [`docs`]. + /// Used by both the single-send and batch-send endpoints. See [`send_email_docs`] + /// and [`batch_email_docs`]. /// - /// [`docs`]: https://resend.com/docs/api-reference/emails/send-email#body-parameters + /// [`send_email_docs`]: https://resend.com/docs/api-reference/emails/send-email#body-parameters + /// [`batch_email_docs`]: https://resend.com/docs/api-reference/emails/send-batch-emails#body-parameters #[must_use] #[derive(Debug, Clone, Serialize)] pub struct CreateEmailBaseOptions { From 25bbaf6024149620e3766ae708e76edea31e57d6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 13 Jul 2026 16:17:28 +0000 Subject: [PATCH 2/3] fix(batch): scope batch support to tags only Attachments and scheduled_at are not supported on the batch endpoint. Remove related tests and docs added in the previous revision. Co-authored-by: cpenned --- CHANGELOG.md | 2 +- src/batch.rs | 121 +++------------------------------------------------ 2 files changed, 8 insertions(+), 115 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 181ccc6..1cc88a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to ### Added -- batch send support for `scheduled_at`, `tags`, and `attachments` on [`CreateEmailBaseOptions`](https://docs.rs/resend-rs/latest/resend_rs/types/struct.CreateEmailBaseOptions.html) +- batch send support for `tags` on [`CreateEmailBaseOptions`](https://docs.rs/resend-rs/latest/resend_rs/types/struct.CreateEmailBaseOptions.html) - add `message_id` to `Email` and webhook `EmailBody` types (https://github.com/resend/resend-rust/pull/77) diff --git a/src/batch.rs b/src/batch.rs index daac066..541a43c 100644 --- a/src/batch.rs +++ b/src/batch.rs @@ -20,8 +20,8 @@ impl BatchSvc { /// Instead of sending one email per HTTP request, we provide a batching endpoint /// that permits you to send up to 100 emails in a single API call. /// - /// Each [`CreateEmailBaseOptions`] in the batch supports `scheduled_at`, `tags`, - /// and `attachments`, in addition to the other send-email body parameters. + /// Each [`CreateEmailBaseOptions`] in the batch supports `tags`, in addition to + /// the other send-email body parameters. /// /// #[maybe_async::maybe_async] @@ -125,8 +125,8 @@ pub mod types { mod test { use crate::test::{CLIENT, DebugResult}; use crate::types::{ - BatchValidation, CreateAttachment, CreateEmailBaseOptions, CreateTemplateOptions, - EmailEvent, EmailTemplate, Tag, Variable, VariableType, + BatchValidation, CreateEmailBaseOptions, CreateTemplateOptions, EmailEvent, EmailTemplate, + Tag, Variable, VariableType, }; #[tokio_shared_rt::test(shared = true)] @@ -328,7 +328,7 @@ mod test { } #[test] - fn serialize_schedule_tags_attachments() { + fn serialize_tags() { use serde_json::json; let email = CreateEmailBaseOptions::new( @@ -337,23 +337,15 @@ mod test { "hello world", ) .with_html("

it works!

") - .with_tag(Tag::new("category", "confirm_email")) - .with_scheduled_at("2025-09-25T11:52:01.858Z") - .with_attachment( - CreateAttachment::from_content(b"hello".to_vec()).with_filename("test.txt"), - ); + .with_tag(Tag::new("category", "confirm_email")); let emails = vec![email]; let value = serde_json::to_value(&emails).unwrap(); - let email = &value[0]; assert_eq!( - email["tags"], + value[0]["tags"], json!([{"name": "category", "value": "confirm_email"}]) ); - assert_eq!(email["scheduled_at"], json!("2025-09-25T11:52:01.858Z")); - assert!(email["attachments"].is_array()); - assert_eq!(email["attachments"][0]["filename"], "test.txt"); } #[tokio_shared_rt::test(shared = true)] @@ -384,103 +376,4 @@ mod test { Ok(()) } - - #[tokio_shared_rt::test(shared = true)] - #[cfg(not(feature = "blocking"))] - async fn schedule() -> DebugResult<()> { - use jiff::{Span, Timestamp, Zoned}; - - let now_plus_1h = Zoned::now() - .checked_add(Span::new().hours(1)) - .expect("Valid date") - .timestamp() - .to_string(); - - let resend = &*CLIENT; - std::thread::sleep(std::time::Duration::from_secs(1)); - - let emails = vec![ - CreateEmailBaseOptions::new( - "Acme ", - ["delivered@resend.dev"], - "hello world", - ) - .with_html("

it works!

") - .with_scheduled_at(&now_plus_1h), - CreateEmailBaseOptions::new( - "Acme ", - ["delivered@resend.dev"], - "world hello", - ) - .with_html("

it works!

") - .with_scheduled_at(&now_plus_1h), - ]; - - let emails = resend.batch.send(emails).await?; - assert_eq!(emails.len(), 2); - std::thread::sleep(std::time::Duration::from_secs(4)); - - for email in emails { - let email = resend.emails.get(&email.id).await?; - assert_eq!(email.last_event, EmailEvent::Scheduled); - assert!(email.scheduled_at.is_some()); - let time = email - .scheduled_at - .unwrap() - .parse::() - .expect("Valid timestamp"); - let time_delta = (time - Timestamp::now()).round(jiff::Unit::Hour).unwrap(); - assert_eq!( - time_delta.compare(Span::new().hours(1)).unwrap(), - std::cmp::Ordering::Equal - ); - - let _cancelled = resend.emails.cancel(&email.id).await?; - } - - Ok(()) - } - - #[tokio_shared_rt::test(shared = true)] - #[cfg(not(feature = "blocking"))] - async fn attachments() -> DebugResult<()> { - use crate::list_opts::ListOptions; - - let resend = &*CLIENT; - std::thread::sleep(std::time::Duration::from_secs(1)); - - let attachment = CreateAttachment::from_content(include_bytes!("../README.md").to_vec()) - .with_filename("README.md"); - - let emails = vec![ - CreateEmailBaseOptions::new( - "Acme ", - ["delivered@resend.dev"], - "hello world", - ) - .with_html("

it works!

") - .with_attachment(attachment.clone()), - CreateEmailBaseOptions::new( - "Acme ", - ["delivered@resend.dev"], - "world hello", - ) - .with_html("

it works!

") - .with_attachment(attachment), - ]; - - let emails = resend.batch.send(emails).await?; - assert_eq!(emails.len(), 2); - std::thread::sleep(std::time::Duration::from_secs(1)); - - for email in emails { - let attachments = resend - .emails - .list_attachments(&email.id, ListOptions::default()) - .await?; - assert_eq!(attachments.data.len(), 1); - } - - Ok(()) - } } From bfdb2675a09d0d593bbc7ca3cc4226144d83854c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 15 Jul 2026 03:32:16 +0000 Subject: [PATCH 3/3] docs: revert CreateEmailBaseOptions batch doc link Avoid implying all send-email fields, such as attachments, apply to batch. Co-authored-by: cpenned --- src/emails.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/emails.rs b/src/emails.rs index 425681c..68432b3 100644 --- a/src/emails.rs +++ b/src/emails.rs @@ -160,11 +160,9 @@ pub mod types { /// All requisite components and associated data to send an email. /// - /// Used by both the single-send and batch-send endpoints. See [`send_email_docs`] - /// and [`batch_email_docs`]. + /// See [`docs`]. /// - /// [`send_email_docs`]: https://resend.com/docs/api-reference/emails/send-email#body-parameters - /// [`batch_email_docs`]: https://resend.com/docs/api-reference/emails/send-batch-emails#body-parameters + /// [`docs`]: https://resend.com/docs/api-reference/emails/send-email#body-parameters #[must_use] #[derive(Debug, Clone, Serialize)] pub struct CreateEmailBaseOptions {