1use std::fmt;
2use std::sync::Arc;
3
4use reqwest::Method;
5use types::DeleteDomainResponse;
6
7use crate::{Config, Result};
8use crate::{
9 list_opts::{ListOptions, ListResponse},
10 types::{CreateDomainOptions, Domain, DomainChanges},
11};
12
13use self::types::UpdateDomainResponse;
14
15#[derive(Clone)]
17pub struct DomainsSvc(pub(crate) Arc<Config>);
18
19impl DomainsSvc {
20 #[maybe_async::maybe_async]
24 #[allow(clippy::needless_pass_by_value)]
26 pub async fn add(&self, domain: CreateDomainOptions) -> Result<Domain> {
27 let request = self.0.build(Method::POST, "/domains");
28 let response = self.0.send(request.json(&domain)).await?;
29 let content = response.json::<Domain>().await?;
30
31 Ok(content)
32 }
33
34 #[maybe_async::maybe_async]
38 pub async fn get(&self, domain_id: &str) -> Result<Domain> {
39 let path = format!("/domains/{domain_id}");
40
41 let request = self.0.build(Method::GET, &path);
42 let response = self.0.send(request).await?;
43 let content = response.json::<Domain>().await?;
44
45 Ok(content)
46 }
47
48 #[maybe_async::maybe_async]
52 pub async fn verify(&self, domain_id: &str) -> Result<()> {
53 let path = format!("/domains/{domain_id}/verify");
54
55 let request = self.0.build(Method::POST, &path);
56 let response = self.0.send(request).await?;
57 let _content = response.json::<types::VerifyDomainResponse>().await?;
58
59 Ok(())
60 }
61
62 #[maybe_async::maybe_async]
66 pub async fn update(
67 &self,
68 domain_id: &str,
69 update: DomainChanges,
70 ) -> Result<UpdateDomainResponse> {
71 let path = format!("/domains/{domain_id}");
72
73 let request = self.0.build(Method::PATCH, &path);
74 let response = self.0.send(request.json(&update)).await?;
75 let content = response.json::<UpdateDomainResponse>().await?;
76
77 Ok(content)
78 }
79
80 #[maybe_async::maybe_async]
86 #[allow(clippy::needless_pass_by_value)]
87 pub async fn list<T>(&self, list_opts: ListOptions<T>) -> Result<ListResponse<Domain>> {
88 let request = self.0.build(Method::GET, "/domains").query(&list_opts);
89 let response = self.0.send(request).await?;
90 let content = response.json::<ListResponse<Domain>>().await?;
91
92 Ok(content)
93 }
94
95 #[maybe_async::maybe_async]
101 #[allow(clippy::needless_pass_by_value)]
102 pub async fn delete(&self, domain_id: &str) -> Result<DeleteDomainResponse> {
103 let path = format!("/domains/{domain_id}");
104
105 let request = self.0.build(Method::DELETE, &path);
106 let response = self.0.send(request).await?;
107 let content = response.json::<DeleteDomainResponse>().await?;
108
109 Ok(content)
110 }
111}
112
113impl fmt::Debug for DomainsSvc {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 fmt::Debug::fmt(&self.0, f)
116 }
117}
118
119#[allow(unreachable_pub)]
120pub mod types {
121 use serde::{Deserialize, Serialize};
122
123 #[derive(Debug, Copy, Clone, Serialize)]
124 #[serde(rename_all = "lowercase")]
125 pub enum Tls {
126 Enforced,
129 Opportunistic,
133 }
134
135 crate::define_id_type!(DomainId);
136
137 #[must_use]
139 #[derive(Debug, Clone, Serialize)]
140 pub struct CreateDomainOptions {
141 #[serde(rename = "name")]
143 name: String,
144 #[serde(rename = "region", skip_serializing_if = "Option::is_none")]
148 region: Option<Region>,
149 #[serde(skip_serializing_if = "Option::is_none")]
154 custom_return_path: Option<String>,
155 }
156
157 impl CreateDomainOptions {
158 #[inline]
162 pub fn new(name: &str) -> Self {
163 Self {
164 name: name.to_owned(),
165 region: None,
166 custom_return_path: None,
167 }
168 }
169
170 #[inline]
172 pub fn with_region(mut self, region: impl Into<Region>) -> Self {
173 self.region = Some(region.into());
174 self
175 }
176
177 #[inline]
182 pub fn with_custom_return_path(mut self, custom_return_path: impl Into<String>) -> Self {
183 self.custom_return_path = Some(custom_return_path.into());
184 self
185 }
186 }
187
188 #[non_exhaustive]
194 #[derive(Debug, Clone, Serialize, Deserialize)]
195 pub enum Region {
196 #[serde(rename = "us-east-1")]
198 UsEast1,
199 #[serde(rename = "eu-west-1")]
201 EuWest1,
202 #[serde(rename = "sa-east-1")]
204 SaEast1,
205 #[serde(rename = "ap-northeast-1")]
207 ApNorthEast1,
208 }
209
210 #[derive(Debug, Clone, Deserialize)]
211 pub struct DomainSpfRecord {
212 pub name: String,
214 pub value: String,
216 #[serde(rename = "type")]
218 pub d_type: SpfRecordType,
219 pub ttl: String,
221 pub status: DomainStatus,
223
224 pub routing_policy: Option<String>,
225 pub priority: Option<i32>,
226 pub proxy_status: Option<ProxyStatus>,
227 }
228
229 #[derive(Debug, Clone, Deserialize)]
230 pub struct DomainDkimRecord {
231 pub name: String,
233 pub value: String,
235 #[serde(rename = "type")]
237 pub d_type: DkimRecordType,
238 pub ttl: String,
240 pub status: DomainStatus,
242
243 pub routing_policy: Option<String>,
244 pub priority: Option<i32>,
245 pub proxy_status: Option<ProxyStatus>,
246 }
247
248 #[derive(Debug, Clone, Deserialize)]
249 pub struct ReceivingRecord {
250 pub name: String,
252 pub value: String,
254 #[serde(rename = "type")]
256 pub d_type: ReceivingRecordType,
257 pub ttl: String,
259 pub status: DomainStatus,
261
262 pub priority: i32,
263 }
264
265 #[derive(Debug, Copy, Clone, Deserialize)]
266 pub enum ReceivingRecordType {
267 #[allow(clippy::upper_case_acronyms)]
268 MX,
269 }
270
271 #[derive(Debug, Copy, Clone, Deserialize)]
272 pub enum ProxyStatus {
273 Enable,
274 Disable,
275 }
276
277 #[derive(Debug, Copy, Clone, Deserialize)]
278 pub enum DomainStatus {
279 Pending,
280 Verified,
281 Failed,
282 #[serde(rename = "temporary_failure")]
283 TemporaryFailure,
284 #[serde(rename = "not_started")]
285 NotStarted,
286 }
287
288 #[derive(Debug, Copy, Clone, Deserialize)]
289 pub enum SpfRecordType {
290 MX,
291 #[allow(clippy::upper_case_acronyms)]
292 TXT,
293 }
294
295 #[derive(Debug, Copy, Clone, Deserialize)]
296 pub enum DkimRecordType {
297 #[allow(clippy::upper_case_acronyms)]
298 CNAME,
299 #[allow(clippy::upper_case_acronyms)]
300 TXT,
301 }
302
303 #[derive(Debug, Clone, Deserialize)]
305 #[serde(tag = "record")]
306 pub enum DomainRecord {
307 #[serde(rename = "SPF")]
308 DomainSpfRecord(DomainSpfRecord),
309 #[serde(rename = "DKIM")]
310 DomainDkimRecord(DomainDkimRecord),
311 #[serde(rename = "Receiving")]
312 ReceivingRecord(ReceivingRecord),
313 }
314
315 #[must_use]
317 #[derive(Debug, Clone, Deserialize)]
318 pub struct Domain {
319 pub id: DomainId,
321 pub name: String,
323 pub status: String,
326
327 pub created_at: String,
329 pub region: Region,
331 pub records: Option<Vec<DomainRecord>>,
333 }
334
335 #[derive(Debug, Clone, Deserialize)]
336 pub struct VerifyDomainResponse {
337 #[allow(dead_code)]
339 pub id: DomainId,
340 }
341
342 #[must_use]
344 #[derive(Debug, Default, Copy, Clone, Serialize)]
345 pub struct DomainChanges {
346 #[serde(skip_serializing_if = "Option::is_none")]
348 click_tracking: Option<bool>,
349 #[serde(skip_serializing_if = "Option::is_none")]
351 open_tracking: Option<bool>,
352 #[serde(skip_serializing_if = "Option::is_none")]
353 tls: Option<Tls>,
354 }
355
356 impl DomainChanges {
357 #[inline]
359 pub fn new() -> Self {
360 Self::default()
361 }
362
363 #[inline]
365 pub const fn with_click_tracking(mut self, enable: bool) -> Self {
366 self.click_tracking = Some(enable);
367 self
368 }
369
370 #[inline]
372 pub const fn with_open_tracking(mut self, enable: bool) -> Self {
373 self.open_tracking = Some(enable);
374 self
375 }
376
377 #[inline]
379 pub const fn with_tls(mut self, tls: Tls) -> Self {
380 self.tls = Some(tls);
381 self
382 }
383 }
384
385 #[derive(Debug, Clone, Deserialize)]
386 pub struct UpdateDomainResponse {
387 pub id: DomainId,
389 }
390
391 #[derive(Debug, Clone, Deserialize)]
392 pub struct DeleteDomainResponse {
393 pub id: DomainId,
395 pub deleted: bool,
397 }
398}
399
400#[cfg(test)]
401#[allow(clippy::needless_return)]
402mod test {
403 use crate::domains::types::DeleteDomainResponse;
404 use crate::list_opts::ListOptions;
405 use crate::{
406 domains::types::{CreateDomainOptions, DomainChanges, Tls},
407 test::{CLIENT, DebugResult, retry},
408 };
409
410 #[tokio_shared_rt::test(shared = true)]
411 #[cfg(not(feature = "blocking"))]
412 #[ignore = "Flaky backend"]
413 async fn all() -> DebugResult<()> {
414 let resend = &*CLIENT;
415
416 let domain = resend
418 .domains
419 .add(CreateDomainOptions::new("example.com"))
420 .await?;
421
422 std::thread::sleep(std::time::Duration::from_secs(4));
423
424 let list = resend.domains.list(ListOptions::default()).await?;
426 assert!(list.len() == 1);
427
428 let domain = resend.domains.get(&domain.id).await?;
430
431 let updates = DomainChanges::new()
433 .with_open_tracking(false)
434 .with_click_tracking(true)
435 .with_tls(Tls::Enforced);
436
437 std::thread::sleep(std::time::Duration::from_secs(4));
438 let f = async || resend.domains.update(&domain.id, updates).await;
439 let domain = retry(f, 5, std::time::Duration::from_secs(2)).await?;
440 std::thread::sleep(std::time::Duration::from_secs(4));
441
442 let f = async || resend.domains.delete(&domain.id).await;
444 let resp: DeleteDomainResponse = retry(f, 5, std::time::Duration::from_secs(2)).await?;
445
446 assert!(resp.deleted);
447
448 let list = resend.domains.list(ListOptions::default()).await?;
450 assert!(list.is_empty());
451
452 Ok(())
453 }
454}