1#[cfg(not(feature = "blocking"))]
2use governor::{
3 Quota, RateLimiter,
4 clock::MonotonicClock,
5 middleware::NoOpMiddleware,
6 state::{InMemoryState, NotKeyed},
7};
8#[cfg(feature = "blocking")]
9use reqwest::blocking::{Client, RequestBuilder, Response};
10#[cfg(not(feature = "blocking"))]
11use reqwest::{Client, RequestBuilder, Response};
12use reqwest::{Method, Url};
13use reqwest::{StatusCode, header::USER_AGENT};
14use std::{env, fmt};
15#[cfg(not(feature = "blocking"))]
16use std::{num::NonZeroU32, sync::Arc, time::Duration};
17
18use crate::{Error, Result, error::types::ErrorResponse};
19
20#[cfg(doc)]
21use crate::Resend;
22
23#[derive(Debug, Clone)]
47#[non_exhaustive]
48pub struct ConfigBuilder {
49 api_key: String,
50 base_url: Option<Url>,
51 client: Option<Client>,
52}
53
54impl ConfigBuilder {
55 pub fn new<S>(api_key: S) -> Self
57 where
58 S: Into<String>,
59 {
60 Self {
61 api_key: api_key.into(),
62 base_url: None,
63 client: None,
64 }
65 }
66
67 #[must_use]
77 pub fn base_url(mut self, url: Url) -> Self {
78 self.base_url = Some(url);
79 self
80 }
81
82 #[must_use]
84 pub fn client(mut self, client: Client) -> Self {
85 self.client = Some(client);
86 self
87 }
88
89 pub fn build(self) -> Config {
91 Config::new(self.api_key, self.client.unwrap_or_default(), self.base_url)
92 }
93}
94
95#[non_exhaustive]
99#[derive(Clone)]
100pub struct Config {
101 pub(crate) user_agent: String,
102 pub(crate) api_key: String,
103 pub(crate) base_url: Url,
104 pub(crate) client: Client,
105 #[cfg(not(feature = "blocking"))]
106 limiter: Arc<
107 RateLimiter<
108 NotKeyed,
109 InMemoryState,
110 MonotonicClock,
111 NoOpMiddleware<<MonotonicClock as governor::clock::Clock>::Instant>,
112 >,
113 >,
114}
115
116impl Config {
117 pub fn builder<S>(api_key: S) -> ConfigBuilder
121 where
122 S: Into<String>,
123 {
124 ConfigBuilder::new(api_key.into())
125 }
126
127 #[must_use]
132 pub(crate) fn new(api_key: String, client: Client, base_url: Option<Url>) -> Self {
133 let env_base_url = base_url.unwrap_or_else(|| {
134 env::var("RESEND_BASE_URL")
135 .map_or_else(
136 |_| Url::parse("https://api.resend.com"),
137 |env_var| Url::parse(env_var.as_str()),
138 )
139 .expect("env variable `RESEND_BASE_URL` should be a valid URL")
140 });
141
142 let env_user_agent = format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
143
144 #[cfg(not(feature = "blocking"))]
146 let rate_limit_per_sec = env::var("RESEND_RATE_LIMIT")
147 .unwrap_or_else(|_| "9".to_owned())
148 .parse::<u32>()
149 .expect("env variable `RESEND_RATE_LIMIT` should be a valid u32");
150
151 #[cfg(not(feature = "blocking"))]
152 let quota = Quota::with_period(Duration::from_millis(1100))
153 .expect("Valid quota")
154 .allow_burst(
155 NonZeroU32::new(rate_limit_per_sec).expect("Rate limit is a valid non zero u32"),
156 );
157
158 #[cfg(not(feature = "blocking"))]
159 let limiter = Arc::new(RateLimiter::direct_with_clock(quota, MonotonicClock));
160 Self {
163 user_agent: env_user_agent,
164 api_key,
165 base_url: env_base_url,
166 client,
167 #[cfg(not(feature = "blocking"))]
168 limiter,
169 }
170 }
171
172 pub(crate) fn build(&self, method: Method, path: &str) -> RequestBuilder {
174 let path = self
175 .base_url
176 .join(path)
177 .expect("should be a valid API endpoint");
178
179 self.client
180 .request(method, path)
181 .bearer_auth(self.api_key.as_str())
182 .header(USER_AGENT, self.user_agent.as_str())
183 }
184
185 #[allow(unreachable_pub)]
186 #[maybe_async::maybe_async]
187 pub async fn send(&self, request: RequestBuilder) -> Result<Response> {
188 #[cfg(not(feature = "blocking"))]
189 {
190 let jitter =
191 governor::Jitter::new(Duration::from_millis(10), Duration::from_millis(50));
192 self.limiter.until_ready_with_jitter(jitter).await;
193 }
194
195 let request = request.build()?;
196
197 let response = self.client.execute(request).await?;
198
199 match response.status() {
200 StatusCode::TOO_MANY_REQUESTS => {
201 let headers = response.headers();
202
203 let ratelimit_limit = headers
204 .get("ratelimit-limit")
205 .and_then(|v| v.to_str().ok())
206 .and_then(|v| v.parse::<u64>().ok());
207 let ratelimit_remaining = headers
208 .get("ratelimit-remaining")
209 .and_then(|v| v.to_str().ok())
210 .and_then(|v| v.parse::<u64>().ok());
211 let ratelimit_reset = headers
212 .get("ratelimit-reset")
213 .and_then(|v| v.to_str().ok())
214 .and_then(|v| v.parse::<u64>().ok());
215
216 Err(Error::RateLimit {
217 ratelimit_limit,
218 ratelimit_remaining,
219 ratelimit_reset,
220 })
221 }
222 x if x.is_client_error() || x.is_server_error() => {
223 let content_type_is_html = response
225 .headers()
226 .get("content-type")
227 .and_then(|el| el.to_str().ok())
228 .is_some_and(|content_type| content_type.contains("html"));
229
230 if content_type_is_html {
231 return Err(Error::Parse(response.text().await?));
232 }
233
234 let error = response.json::<ErrorResponse>().await?;
235 Err(Error::Resend(error))
236 }
237 _ => Ok(response),
238 }
239 }
240}
241
242impl fmt::Debug for Config {
243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 f.debug_struct("Client")
246 .field("api_key", &"re_*********")
247 .field("user_agent", &self.user_agent.as_str())
248 .field("base_url", &self.base_url.as_str())
249 .finish_non_exhaustive()
250 }
251}