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.
11//!
12//!   Note that the client can be safely cloned as well as used in async/parallel contexts and the
13//!   rate limit will work as intended. The only exception to this is creating 2 clients via the
14//!   [`Resend::new`] or [`Resend::with_client`] methods which should be avoided, use `.clone()`
15//!   instead.
16//!
17//! - Secondly, a couple of helper methods as well as macros are implemented in the [`rate_limit`]
18//!   module that allow catching rate limit errors and retrying the request instead of failing.
19//!   
20//!   These were implemented to handle cases where this crate is used in a horizontally scaled
21//!   environment and thus needs to work on different machines at the same time in which case the
22//!   internal rate limits alone cannot guarantee that there will be no rate limit errors.
23//!
24//!   As long as only one program is interacting with the Resend servers on your behalf, this
25//!   module does not need to be used.
26//!
27//! ### Examples
28//!
29//! ```rust,no_run
30//! use resend_rs::types::{CreateEmailBaseOptions, Tag};
31//! use resend_rs::{Resend, Result};
32//!
33//! #[tokio::main]
34//! async fn main() -> Result<()> {
35//!     let resend = Resend::default();
36//!
37//!     let from = "Acme <onboarding@a.dev>";
38//!     let to = ["delivered@resend.dev"];
39//!     let subject = "Hello World!";
40//!
41//!     let email = CreateEmailBaseOptions::new(from, to, subject)
42//!         .with_text("Hello World!")
43//!         .with_tag(Tag::new("hello", "world"));
44//!
45//!     let id = resend.emails.send(email).await?.id;
46//!     println!("id: {id}");
47//!     Ok(())
48//! }
49//!
50//! ```
51
52pub use client::Resend;
53pub(crate) use config::Config;
54
55mod api_keys;
56mod audiences;
57mod batch;
58mod broadcasts;
59mod client;
60mod config;
61mod contacts;
62mod domains;
63mod emails;
64mod error;
65pub mod events;
66pub mod rate_limit;
67
68pub mod services {
69    //! `Resend` API services.
70
71    pub use super::api_keys::ApiKeysSvc;
72    pub use super::audiences::AudiencesSvc;
73    pub use super::batch::BatchSvc;
74    pub use super::broadcasts::BroadcastsSvc;
75    pub use super::contacts::ContactsSvc;
76    pub use super::domains::DomainsSvc;
77    pub use super::emails::EmailsSvc;
78}
79
80pub mod types {
81    //! Request and response types.
82
83    pub use super::api_keys::types::{
84        ApiKey, ApiKeyId, ApiKeyToken, CreateApiKeyOptions, Permission,
85    };
86    pub use super::audiences::types::{Audience, AudienceId, CreateAudienceResponse};
87    pub use super::batch::types::SendEmailBatchResponse;
88    pub use super::broadcasts::types::{
89        Broadcast, BroadcastId, CreateBroadcastOptions, CreateBroadcastResponse,
90        RemoveBroadcastResponse, SendBroadcastOptions, SendBroadcastResponse,
91        UpdateBroadcastOptions, UpdateBroadcastResponse,
92    };
93    pub use super::contacts::types::{Contact, ContactChanges, ContactData, ContactId};
94    pub use super::domains::types::{
95        CreateDomainOptions, DkimRecordType, Domain, DomainChanges, DomainDkimRecord, DomainId,
96        DomainRecord, DomainSpfRecord, DomainStatus, ProxyStatus, Region, SpfRecordType, Tls,
97        UpdateDomainResponse,
98    };
99    pub use super::emails::types::{
100        Attachment, CancelScheduleResponse, ContentOrPath, CreateEmailBaseOptions,
101        CreateEmailResponse, Email, EmailId, Tag, UpdateEmailOptions, UpdateEmailResponse,
102    };
103    pub use super::error::types::{ErrorKind, ErrorResponse};
104}
105
106/// Error type for operations of a [`Resend`] client.
107///
108/// <https://resend.com/docs/api-reference/errors>
109#[derive(Debug, thiserror::Error)]
110pub enum Error {
111    /// Errors that may occur during the processing an HTTP request.
112    #[error("http error: {0}")]
113    Http(#[from] reqwest::Error),
114
115    /// Errors that may occur during the processing of the API request.
116    #[error("resend error: {0}")]
117    Resend(#[from] types::ErrorResponse),
118
119    /// Errors that may occur during the parsing of an API response.
120    #[error("Failed to parse Resend API response. Received: \n{0}")]
121    Parse(String),
122
123    /// Detailed rate limit error. For the old error variant see
124    /// [`types::ErrorKind::RateLimitExceeded`].
125    #[error("Too many requests. Limit is {ratelimit_limit:?} per {ratelimit_reset:?} seconds.")]
126    RateLimit {
127        ratelimit_limit: Option<u64>,
128        ratelimit_remaining: Option<u64>,
129        ratelimit_reset: Option<u64>,
130    },
131}
132
133#[cfg(test)]
134mod test {
135    use crate::Error;
136
137    #[allow(dead_code, clippy::redundant_pub_crate)]
138    pub(crate) struct LocatedError<E: std::error::Error + 'static> {
139        inner: E,
140        location: &'static std::panic::Location<'static>,
141    }
142
143    impl From<Error> for LocatedError<Error> {
144        #[track_caller]
145        fn from(value: Error) -> Self {
146            Self {
147                inner: value,
148                location: std::panic::Location::caller(),
149            }
150        }
151    }
152
153    impl<T: std::error::Error + 'static> std::fmt::Debug for LocatedError<T> {
154        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155            write!(
156                f,
157                "{}:{}:{}\n{:?}",
158                self.location.file(),
159                self.location.line(),
160                self.location.column(),
161                self.inner
162            )
163        }
164    }
165
166    #[allow(clippy::redundant_pub_crate)]
167    pub(crate) type DebugResult<T, E = LocatedError<Error>> = Result<T, E>;
168}
169
170/// Specialized [`Result`] type for an [`Error`].
171///
172/// [`Result`]: std::result::Result
173pub type Result<T, E = Error> = std::result::Result<T, E>;
174
175#[cfg(test)]
176pub(crate) mod tests {
177    use std::sync::LazyLock;
178
179    use crate::Resend;
180
181    #[allow(clippy::redundant_pub_crate)]
182    /// Use this client in all tests to ensure rate limits are respected.
183    ///
184    /// Instantiate with:
185    /// ```
186    /// let resend = &*CLIENT;
187    /// ```
188    pub(crate) static CLIENT: LazyLock<Resend> = LazyLock::new(Resend::default);
189}