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