resend_rs/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3//! ### Rate Limits
4//!
5//! Resend implements rate limiting on their API which can sometimes get in the way of whatever
6//! you are trying to do. This crate handles that in 2 ways:
7//!
8//! - Firstly *all* requests made by the [`Resend`] client are automatically rate limited to
9//!   9 req/1.1s to avoid collisions with the 10 req/s limit that Resend imposes at the time of
10//!   writing this. Note that this can be changed by changing the `RESEND_RATE_LIMIT` environment
11//!   variable (by default it is set to `9`).
12//!
13//!   Note that the client can be safely cloned as well as used in async/parallel contexts and the
14//!   rate limit will work as intended. The only exception to this is creating 2 clients via the
15//!   [`Resend::new`] or [`Resend::with_client`] methods which should be avoided, use `.clone()`
16//!   instead.
17//!
18//! - Secondly, a couple of helper methods as well as macros are implemented in the [`rate_limit`]
19//!   module that allow catching rate limit errors and retrying the request instead of failing.
20//!   
21//!   These were implemented to handle cases where this crate is used in a horizontally scaled
22//!   environment and thus needs to work on different machines at the same time in which case the
23//!   internal rate limits alone cannot guarantee that there will be no rate limit errors.
24//!
25//!   As long as only one program is interacting with the Resend servers on your behalf, this
26//!   module does not need to be used.
27//!
28//! ### Examples
29//!
30//! ```rust,no_run
31//! use resend_rs::types::{CreateEmailBaseOptions, Tag};
32//! use resend_rs::{Resend, Result};
33//!
34//! #[tokio::main]
35//! async fn main() -> Result<()> {
36//!     let resend = Resend::default();
37//!
38//!     let from = "Acme <onboarding@a.dev>";
39//!     let to = ["delivered@resend.dev"];
40//!     let subject = "Hello World!";
41//!
42//!     let email = CreateEmailBaseOptions::new(from, to, subject)
43//!         .with_text("Hello World!")
44//!         .with_tag(Tag::new("hello", "world"));
45//!
46//!     let id = resend.emails.send(email).await?.id;
47//!     println!("id: {id}");
48//!     Ok(())
49//! }
50//!
51//! ```
52
53pub use client::Resend;
54pub use config::{Config, ConfigBuilder};
55pub use serde_json::{Value, json};
56
57mod api_keys;
58mod batch;
59mod broadcasts;
60mod client;
61mod config;
62mod contacts;
63mod domains;
64mod emails;
65mod error;
66pub mod events;
67pub mod idempotent;
68pub mod list_opts;
69pub mod rate_limit;
70mod receiving;
71mod segments;
72mod templates;
73mod topics;
74mod webhooks;
75
76pub mod services {
77    //! `Resend` API services.
78
79    pub use super::api_keys::ApiKeysSvc;
80    pub use super::batch::BatchSvc;
81    pub use super::broadcasts::BroadcastsSvc;
82    pub use super::contacts::ContactsSvc;
83    pub use super::domains::DomainsSvc;
84    pub use super::emails::EmailsSvc;
85    pub use super::receiving::ReceivingSvc;
86    pub use super::segments::SegmentsSvc;
87    pub use super::templates::TemplateSvc;
88    pub use super::topics::TopicsSvc;
89}
90
91pub mod types {
92    //! Request and response types.
93
94    pub use super::api_keys::types::{
95        ApiKey, ApiKeyId, ApiKeyToken, CreateApiKeyOptions, Permission,
96    };
97    pub use super::batch::types::{
98        BatchValidation, PermissiveBatchErrors, SendEmailBatchPermissiveResponse,
99        SendEmailBatchResponse,
100    };
101    pub use super::broadcasts::types::{
102        Broadcast, BroadcastId, CreateBroadcastOptions, CreateBroadcastResponse,
103        RemoveBroadcastResponse, SendBroadcastOptions, SendBroadcastResponse,
104        UpdateBroadcastOptions, UpdateBroadcastResponse,
105    };
106    pub use super::contacts::types::{
107        AddContactSegmentResponse, Contact, ContactChanges, ContactId, ContactProperty,
108        ContactPropertyChanges, ContactPropertyId, ContactTopic, CreateContactOptions,
109        CreateContactPropertyOptions, CreateContactPropertyResponse, DeleteContactPropertyResponse,
110        PropertyType, RemoveContactSegmentResponse, UpdateContactPropertyResponse,
111        UpdateContactTopicOptions,
112    };
113    pub use super::domains::types::{
114        CreateDomainOptions, DkimRecordType, Domain, DomainChanges, DomainDkimRecord, DomainId,
115        DomainRecord, DomainSpfRecord, DomainStatus, ProxyStatus, ReceivingRecord,
116        ReceivingRecordType, Region, SpfRecordType, Tls, UpdateDomainResponse,
117    };
118    pub use super::emails::types::{
119        Attachment, CancelScheduleResponse, ContentOrPath, CreateAttachment,
120        CreateEmailBaseOptions, CreateEmailResponse, Email, EmailEvent, EmailId, EmailTemplate,
121        Tag, UpdateEmailOptions, UpdateEmailResponse,
122    };
123    pub use super::error::types::{ErrorKind, ErrorResponse};
124    pub use super::receiving::types::{
125        InboundAttachment, InboundAttatchmentId, InboundEmail, InboundEmailId,
126    };
127    pub use super::segments::types::{CreateSegmentResponse, Segment, SegmentId};
128    pub use super::templates::types::{
129        CreateTemplateOptions, CreateTemplateResponse, DeleteTemplateResponse,
130        DuplicateTemplateResponse, PublishTemplateResponse, Template, TemplateEvent, TemplateId,
131        UpdateTemplateOptions, UpdateTemplateResponse, Variable, VariableType,
132    };
133    pub use super::topics::types::{
134        CreateTopicOptions, CreateTopicResponse, DeleteTopicResponse, SubscriptionType, Topic,
135        TopicId, TopicVisibility, UpdateTopicOptions, UpdateTopicResponse,
136    };
137    pub use super::webhooks::types::{
138        CreateWebhookOptions, CreateWebhookResponse, DeleteWebhookResponse, UpdateWebhookOptions,
139        UpdateWebhookResponse, Webhook, WebhookId, WebhookStatus,
140    };
141}
142
143/// Error type for operations of a [`Resend`] client.
144///
145/// <https://resend.com/docs/api-reference/errors>
146#[derive(Debug, thiserror::Error)]
147pub enum Error {
148    /// Errors that may occur during the processing an HTTP request.
149    #[error("http error: {0}")]
150    Http(#[from] reqwest::Error),
151
152    /// Errors that may occur during the processing of the API request.
153    #[error("resend error: {0}")]
154    Resend(#[from] types::ErrorResponse),
155
156    /// Errors that may occur during the parsing of an API response.
157    #[error("Failed to parse Resend API response. Received: \n{0}")]
158    Parse(String),
159
160    /// Detailed rate limit error. For the old error variant see
161    /// [`types::ErrorKind::RateLimitExceeded`].
162    #[error("Too many requests. Limit is {ratelimit_limit:?} per {ratelimit_reset:?} seconds.")]
163    RateLimit {
164        ratelimit_limit: Option<u64>,
165        ratelimit_remaining: Option<u64>,
166        ratelimit_reset: Option<u64>,
167    },
168}
169
170macro_rules! define_id_type {
171    ($name:ident) => {
172        /// Unique identifier.
173        #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
174        pub struct $name(ecow::EcoString);
175
176        impl $name {
177            /// Creates a new [`$name`].
178            #[inline]
179            #[must_use]
180            pub fn new(id: &str) -> Self {
181                Self(ecow::EcoString::from(id))
182            }
183        }
184
185        impl std::ops::Deref for $name {
186            type Target = str;
187
188            #[inline]
189            fn deref(&self) -> &Self::Target {
190                self.as_ref()
191            }
192        }
193
194        impl AsRef<str> for $name {
195            #[inline]
196            fn as_ref(&self) -> &str {
197                self.0.as_str()
198            }
199        }
200
201        impl std::fmt::Display for $name {
202            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203                std::fmt::Display::fmt(&self.0, f)
204            }
205        }
206    };
207}
208
209pub(crate) use define_id_type;
210
211/// Specialized [`Result`] type for an [`Error`].
212///
213/// [`Result`]: std::result::Result
214pub type Result<T, E = Error> = std::result::Result<T, E>;
215
216#[cfg(test)]
217mod test {
218    use std::sync::LazyLock;
219
220    use crate::{Error, Resend};
221
222    #[allow(dead_code, clippy::redundant_pub_crate)]
223    pub(crate) struct LocatedError<E: std::error::Error + 'static> {
224        inner: E,
225        location: &'static std::panic::Location<'static>,
226    }
227
228    impl From<Error> for LocatedError<Error> {
229        #[track_caller]
230        fn from(value: Error) -> Self {
231            Self {
232                inner: value,
233                location: std::panic::Location::caller(),
234            }
235        }
236    }
237
238    impl<T: std::error::Error + 'static> std::fmt::Debug for LocatedError<T> {
239        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240            write!(
241                f,
242                "{}:{}:{}\n{:?}",
243                self.location.file(),
244                self.location.line(),
245                self.location.column(),
246                self.inner
247            )
248        }
249    }
250
251    #[allow(clippy::redundant_pub_crate)]
252    pub(crate) type DebugResult<T, E = LocatedError<Error>> = Result<T, E>;
253
254    #[allow(clippy::redundant_pub_crate)]
255    /// Use this client in all tests to ensure rate limits are respected.
256    ///
257    /// Instantiate with:
258    /// ```
259    /// let resend = &*CLIENT;
260    /// ```
261    pub(crate) static CLIENT: LazyLock<Resend> = LazyLock::new(Resend::default);
262
263    // <https://stackoverflow.com/a/77859502/12756474>
264    #[allow(clippy::redundant_pub_crate)]
265    pub(crate) async fn retry<O, E, F>(
266        mut f: F,
267        retries: i32,
268        interval: std::time::Duration,
269    ) -> Result<O, E>
270    where
271        F: AsyncFnMut() -> Result<O, E>,
272    {
273        let mut count = 0;
274        loop {
275            match f().await {
276                Ok(output) => break Ok(output),
277                Err(e) => {
278                    println!("try {count} failed");
279                    count += 1;
280                    if count == retries {
281                        return Err(e);
282                    }
283                    tokio::time::sleep(interval).await;
284                }
285            }
286        }
287    }
288}