Skip to main content

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 automations;
59mod batch;
60mod broadcasts;
61mod client;
62mod config;
63mod contacts;
64mod domains;
65mod emails;
66mod error;
67pub mod events;
68pub mod idempotent;
69pub mod list_opts;
70mod logs;
71mod oauth;
72pub mod rate_limit;
73mod receiving;
74mod segments;
75mod suppressions;
76mod templates;
77mod topics;
78mod webhooks;
79
80pub mod services {
81    //! `Resend` API services.
82
83    pub use super::api_keys::ApiKeysSvc;
84    pub use super::automations::AutomationsSvc;
85    pub use super::batch::BatchSvc;
86    pub use super::broadcasts::BroadcastsSvc;
87    pub use super::contacts::ContactsSvc;
88    pub use super::domains::DomainsSvc;
89    pub use super::emails::EmailsSvc;
90    pub use super::logs::LogsSvc;
91    pub use super::oauth::OAuthSvc;
92    pub use super::receiving::ReceivingSvc;
93    pub use super::segments::SegmentsSvc;
94    pub use super::suppressions::SuppressionsSvc;
95    pub use super::templates::TemplateSvc;
96    pub use super::topics::TopicsSvc;
97}
98
99pub mod types {
100    //! Request and response types.
101
102    pub use super::api_keys::types::{
103        ApiKey, ApiKeyId, ApiKeyToken, CreateApiKeyOptions, Permission,
104    };
105    pub use super::automations::types::{
106        AddToSegmentStepConfig, Automation, AutomationId, AutomationMinimal, AutomationRun,
107        AutomationRunId, AutomationStatus, AutomationTemplate, Connection, ConnectionType,
108        CreateAutomationOptions, CreateAutomationResponse, DelayStepConfig,
109        DeleteAutomationResponse, SendEmailStepConfig, Step, StopAutomationResponse,
110        TriggerStepConfig, UpdateAutomationOptions, UpdateAutomationResponse,
111        WaitForEventStepConfig,
112    };
113    pub use super::batch::types::{
114        BatchValidation, PermissiveBatchErrors, SendEmailBatchPermissiveResponse,
115        SendEmailBatchResponse,
116    };
117    pub use super::broadcasts::types::{
118        Broadcast, BroadcastId, CreateBroadcastOptions, CreateBroadcastResponse,
119        RemoveBroadcastResponse, SendBroadcastOptions, SendBroadcastResponse,
120        UpdateBroadcastOptions, UpdateBroadcastResponse,
121    };
122    pub use super::contacts::types::{
123        AddContactSegmentResponse, Contact, ContactChanges, ContactId, ContactImport,
124        ContactImportColumnMap, ContactImportCounts, ContactImportId, ContactImportOnConflict,
125        ContactImportPropertyMapping, ContactImportPropertyType, ContactImportStatus,
126        ContactImportTopic, ContactImportTopicSubscription, ContactProperty,
127        ContactPropertyChanges, ContactPropertyId, ContactTopic, CreateContactImportOptions,
128        CreateContactImportResponse, CreateContactOptions, CreateContactPropertyOptions,
129        CreateContactPropertyResponse, DeleteContactPropertyResponse, PropertyType,
130        RemoveContactSegmentResponse, SegmentObject, UpdateContactPropertyResponse,
131        UpdateContactTopicOptions,
132    };
133    pub use super::domains::types::{
134        CreateDomainClaimOptions, CreateDomainOptions, DkimRecordType, Domain, DomainCapabilities,
135        DomainCapabilityStatus, DomainChanges, DomainClaim, DomainClaimBlockedReason,
136        DomainClaimId, DomainClaimRecord, DomainClaimRecordType, DomainClaimStatus,
137        DomainDkimRecord, DomainId, DomainRecord, DomainRecordStatus, DomainSpfRecord,
138        DomainStatus, ProxyStatus, ReceivingRecord, ReceivingRecordType, Region, SpfRecordType,
139        Tls, UpdateDomainResponse, VerifyDomainResponse,
140    };
141    pub use super::emails::types::{
142        Attachment, CancelScheduleResponse, ContentDisposition, ContentOrPath, CreateAttachment,
143        CreateEmailBaseOptions, CreateEmailResponse, Email, EmailEvent, EmailId, EmailTemplate,
144        Tag, UpdateEmailOptions, UpdateEmailResponse,
145    };
146    pub use super::error::types::{ErrorKind, ErrorResponse};
147    pub use super::events::types::{
148        ContactIdOrEmail, CreateEventOptions, CreateEventResponse, DeleteEventResponse,
149        GetEventResponse, SendEventOptions, SendEventResponse, UpdateEventOptions,
150        UpdateEventResponse,
151    };
152    pub use super::logs::types::{Log, LogId};
153    pub use super::oauth::types::{
154        ClientId, OAuthGrant, OAuthGrantClient, OAuthGrantId, RevokeOAuthGrantResponse,
155    };
156    pub use super::receiving::types::{
157        ForwardInboundEmailResponse, ForwardReceivingEmail, GetInboundEmailOptions,
158        GetInboundEmailRaw, InboundAttachment, InboundAttachmentId, InboundEmail,
159        InboundEmailHtmlFormat, InboundEmailId,
160    };
161    pub use super::segments::types::{CreateSegmentResponse, Segment, SegmentId};
162    pub use super::suppressions::types::{
163        AddSuppressionOptions, AddSuppressionResponse, BatchAddSuppressionOptions,
164        BatchAddSuppressionResponse, BatchRemoveSuppressionOptions,
165        BatchRemoveSuppressionsResponse, EmailsSpecified, IdsSpecified, NotSpecified,
166        RemoveSuppressionResponse, Suppression, SuppressionId, SuppressionOrigin,
167    };
168    pub use super::templates::types::{
169        CreateTemplateOptions, CreateTemplateResponse, DeleteTemplateResponse,
170        DuplicateTemplateResponse, PublishTemplateResponse, Template, TemplateEvent, TemplateId,
171        UpdateTemplateOptions, UpdateTemplateResponse, Variable, VariableType,
172    };
173    pub use super::topics::types::{
174        CreateTopicOptions, CreateTopicResponse, DeleteTopicResponse, SubscriptionType, Topic,
175        TopicId, TopicVisibility, UpdateTopicOptions, UpdateTopicResponse,
176    };
177    pub use super::webhooks::types::{
178        CreateWebhookOptions, CreateWebhookResponse, DeleteWebhookResponse, UpdateWebhookOptions,
179        UpdateWebhookResponse, Webhook, WebhookId, WebhookStatus,
180    };
181}
182
183/// Error type for operations of a [`Resend`] client.
184///
185/// <https://resend.com/docs/api-reference/errors>
186#[derive(Debug, thiserror::Error)]
187pub enum Error {
188    /// Errors that may occur during the processing an HTTP request.
189    #[error("http error: {0}")]
190    Http(#[from] reqwest::Error),
191
192    /// Errors that may occur during the processing of the API request.
193    #[error("resend error: {0}")]
194    Resend(#[from] types::ErrorResponse),
195
196    /// Errors that may occur during the parsing of an API response.
197    #[error("Failed to parse Resend API response. Received: \n{message}")]
198    Parse {
199        message: String,
200        source: Option<Box<dyn std::error::Error + Send + Sync>>,
201    },
202
203    /// Other more generic errors
204    #[error("{0}")]
205    Other(String),
206
207    /// Detailed rate limit error. For the old error variant see
208    /// [`types::ErrorKind::RateLimitExceeded`].
209    #[error("Too many requests. Limit is {ratelimit_limit:?} per {ratelimit_reset:?} seconds.")]
210    RateLimit {
211        ratelimit_limit: Option<u64>,
212        ratelimit_remaining: Option<u64>,
213        ratelimit_reset: Option<u64>,
214    },
215}
216
217macro_rules! define_id_type {
218    ($name:ident) => {
219        /// Unique identifier.
220        #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
221        pub struct $name(ecow::EcoString);
222
223        impl $name {
224            /// Creates a new [`$name`].
225            #[inline]
226            #[must_use]
227            pub fn new(id: &str) -> Self {
228                Self(ecow::EcoString::from(id))
229            }
230        }
231
232        impl std::ops::Deref for $name {
233            type Target = str;
234
235            #[inline]
236            fn deref(&self) -> &Self::Target {
237                self.as_ref()
238            }
239        }
240
241        impl AsRef<str> for $name {
242            #[inline]
243            fn as_ref(&self) -> &str {
244                self.0.as_str()
245            }
246        }
247
248        impl std::fmt::Display for $name {
249            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250                std::fmt::Display::fmt(&self.0, f)
251            }
252        }
253    };
254}
255
256pub(crate) use define_id_type;
257
258/// Specialized [`Result`] type for an [`Error`].
259///
260/// [`Result`]: std::result::Result
261pub type Result<T, E = Error> = std::result::Result<T, E>;
262
263#[cfg(test)]
264mod test {
265    use std::sync::LazyLock;
266
267    use crate::{Error, Resend};
268
269    #[allow(dead_code, clippy::redundant_pub_crate)]
270    pub(crate) struct LocatedError<E: std::error::Error + 'static> {
271        inner: E,
272        location: &'static std::panic::Location<'static>,
273    }
274
275    impl From<Error> for LocatedError<Error> {
276        #[track_caller]
277        fn from(value: Error) -> Self {
278            Self {
279                inner: value,
280                location: std::panic::Location::caller(),
281            }
282        }
283    }
284
285    impl<T: std::error::Error + 'static> std::fmt::Debug for LocatedError<T> {
286        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287            write!(
288                f,
289                "{}:{}:{}\n{:?}",
290                self.location.file(),
291                self.location.line(),
292                self.location.column(),
293                self.inner
294            )
295        }
296    }
297
298    #[allow(clippy::redundant_pub_crate)]
299    pub(crate) type DebugResult<T, E = LocatedError<Error>> = Result<T, E>;
300
301    #[allow(clippy::redundant_pub_crate)]
302    /// Use this client in all tests to ensure rate limits are respected.
303    ///
304    /// Instantiate with:
305    /// ```
306    /// let resend = &*CLIENT;
307    /// ```
308    pub(crate) static CLIENT: LazyLock<Resend> = LazyLock::new(Resend::default);
309
310    // <https://stackoverflow.com/a/77859502/12756474>
311    #[allow(clippy::redundant_pub_crate)]
312    pub(crate) async fn retry<O, E, F>(
313        mut f: F,
314        retries: i32,
315        interval: std::time::Duration,
316    ) -> Result<O, E>
317    where
318        F: AsyncFnMut() -> Result<O, E>,
319    {
320        let mut count = 0;
321        loop {
322            match f().await {
323                Ok(output) => break Ok(output),
324                Err(e) => {
325                    println!("try {count} failed");
326                    count += 1;
327                    if count == retries {
328                        return Err(e);
329                    }
330                    tokio::time::sleep(interval).await;
331                }
332            }
333        }
334    }
335}