Skip to main content

resend_rs/
domains.rs

1use std::fmt;
2use std::sync::Arc;
3
4use reqwest::Method;
5use types::DeleteDomainResponse;
6
7use crate::{Config, Result, domains::types::VerifyDomainResponse};
8use crate::{
9    list_opts::{ListOptions, ListResponse},
10    types::{CreateDomainClaimOptions, CreateDomainOptions, Domain, DomainChanges, DomainClaim},
11};
12
13use self::types::UpdateDomainResponse;
14
15/// `Resend` APIs for `/domains` endpoints.
16#[derive(Clone)]
17pub struct DomainsSvc(pub(crate) Arc<Config>);
18
19impl DomainsSvc {
20    /// Creates a domain through the Resend Email API.
21    ///
22    /// <https://resend.com/docs/api-reference/domains/create-domain>
23    #[maybe_async::maybe_async]
24    pub async fn create(&self, domain: CreateDomainOptions) -> Result<Domain> {
25        let request = self.0.build(Method::POST, "/domains");
26        let response = self.0.send(request.json(&domain)).await?;
27        let content = response.json::<Domain>().await?;
28
29        Ok(content)
30    }
31
32    /// Retrieves a single domain for the authenticated user.
33    ///
34    /// <https://resend.com/docs/api-reference/domains/get-domain>
35    #[maybe_async::maybe_async]
36    pub async fn get(&self, domain_id: &str) -> Result<Domain> {
37        let path = format!("/domains/{domain_id}");
38
39        let request = self.0.build(Method::GET, &path);
40        let response = self.0.send(request).await?;
41        let content = response.json::<Domain>().await?;
42
43        Ok(content)
44    }
45
46    /// Verifies an existing domain.
47    ///
48    /// <https://resend.com/docs/api-reference/domains/verify-domain>
49    #[maybe_async::maybe_async]
50    pub async fn verify(&self, domain_id: &str) -> Result<VerifyDomainResponse> {
51        let path = format!("/domains/{domain_id}/verify");
52
53        let request = self.0.build(Method::POST, &path);
54        let response = self.0.send(request).await?;
55        let content = response.json::<VerifyDomainResponse>().await?;
56
57        Ok(content)
58    }
59
60    /// Updates an existing domain.
61    ///
62    /// <https://resend.com/docs/api-reference/domains/update-domain>
63    #[maybe_async::maybe_async]
64    pub async fn update(
65        &self,
66        domain_id: &str,
67        update: DomainChanges,
68    ) -> Result<UpdateDomainResponse> {
69        let path = format!("/domains/{domain_id}");
70
71        let request = self.0.build(Method::PATCH, &path);
72        let response = self.0.send(request.json(&update)).await?;
73        let content = response.json::<UpdateDomainResponse>().await?;
74
75        Ok(content)
76    }
77
78    /// Retrieves a list of domains for the authenticated user.
79    ///
80    /// - Default limit: no limit (return everything)
81    ///
82    /// <https://resend.com/docs/api-reference/domains/list-domains>
83    #[maybe_async::maybe_async]
84    pub async fn list<T>(&self, list_opts: ListOptions<T>) -> Result<ListResponse<Domain>> {
85        let request = self.0.build(Method::GET, "/domains").query(&list_opts);
86        let response = self.0.send(request).await?;
87        let content = response.json::<ListResponse<Domain>>().await?;
88
89        Ok(content)
90    }
91
92    /// Removes an existing domain.
93    ///
94    /// Returns whether the domain was deleted successfully.
95    ///
96    /// <https://resend.com/docs/api-reference/domains/delete-domain>
97    #[maybe_async::maybe_async]
98    pub async fn delete(&self, domain_id: &str) -> Result<DeleteDomainResponse> {
99        let path = format!("/domains/{domain_id}");
100
101        let request = self.0.build(Method::DELETE, &path);
102        let response = self.0.send(request).await?;
103        let content = response.json::<DeleteDomainResponse>().await?;
104
105        Ok(content)
106    }
107
108    /// Claim a domain that is already verified by another team.
109    ///
110    /// <https://resend.com/docs/api-reference/domains/claim-domain>
111    #[maybe_async::maybe_async]
112    pub async fn claim(&self, domain_claim: CreateDomainClaimOptions) -> Result<DomainClaim> {
113        let request = self.0.build(Method::POST, "/domains/claim");
114        let response = self.0.send(request.json(&domain_claim)).await?;
115        let content = response.json::<DomainClaim>().await?;
116
117        Ok(content)
118    }
119
120    /// Retrieve the latest claim for a domain.
121    ///
122    /// <https://resend.com/docs/api-reference/domains/get-domain-claim>
123    #[maybe_async::maybe_async]
124    pub async fn get_claim(&self, domain_id: &str) -> Result<DomainClaim> {
125        let path = format!("/domains/{domain_id}/claim");
126
127        let request = self.0.build(Method::GET, &path);
128        let response = self.0.send(request).await?;
129        let content = response.json::<DomainClaim>().await?;
130
131        Ok(content)
132    }
133
134    /// Trigger DNS verification for a domain claim.
135    ///
136    /// <https://resend.com/docs/api-reference/domains/verify-domain-claim>
137    #[maybe_async::maybe_async]
138    pub async fn verify_claim(&self, domain_id: &str) -> Result<DomainClaim> {
139        let path = format!("/domains/{domain_id}/claim/verify");
140
141        let request = self.0.build(Method::POST, &path);
142        let response = self.0.send(request).await?;
143        let content = response.json::<DomainClaim>().await?;
144
145        Ok(content)
146    }
147}
148
149impl fmt::Debug for DomainsSvc {
150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151        fmt::Debug::fmt(&self.0, f)
152    }
153}
154
155#[allow(unreachable_pub)]
156pub mod types {
157    use serde::{Deserialize, Serialize};
158    use serde_json::Value;
159
160    #[derive(Debug, Copy, Clone, Serialize)]
161    #[serde(rename_all = "lowercase")]
162    pub enum Tls {
163        /// Enforced TLS on the other hand, requires that the email communication must use TLS no
164        /// matter what. If the receiving server does not support TLS, the email will not be sent.
165        Enforced,
166        /// Opportunistic TLS means that it always attempts to make a secure connection to the
167        /// receiving mail server. If it can’t establish a secure connection, it sends the message
168        /// unencrypted.
169        Opportunistic,
170    }
171
172    crate::define_id_type!(DomainId);
173    crate::define_id_type!(DomainClaimId);
174
175    /// Details of a new [`Domain`].
176    #[must_use]
177    #[derive(Debug, Clone, Serialize)]
178    pub struct CreateDomainOptions {
179        /// The name of the domain you want to create.
180        #[serde(rename = "name")]
181        name: String,
182        /// The region where the email will be sent from.
183        ///
184        /// Possible values are `'us-east-1' | 'eu-west-1' | 'sa-east-1' | 'ap-northeast-1'`.
185        #[serde(rename = "region", skip_serializing_if = "Option::is_none")]
186        region: Option<Region>,
187        /// For advanced use cases, choose a subdomain for the Return-Path address.
188        /// The custom return path is used for SPF authentication, DMARC alignment, and handling
189        /// bounced emails. Defaults to `send` (i.e., `send.yourdomain.tld`). Avoid setting values
190        /// that could undermine credibility (e.g. `testing`), as they may be exposed to recipients.
191        #[serde(skip_serializing_if = "Option::is_none")]
192        custom_return_path: Option<String>,
193
194        #[serde(skip_serializing_if = "Option::is_none")]
195        open_tracking: Option<bool>,
196        #[serde(skip_serializing_if = "Option::is_none")]
197        click_tracking: Option<bool>,
198        #[serde(skip_serializing_if = "Option::is_none")]
199        tracking_subdomain: Option<String>,
200        #[serde(skip_serializing_if = "Option::is_none")]
201        tls: Option<Tls>,
202        #[serde(skip_serializing_if = "Option::is_none")]
203        capabilities: Option<Value>,
204    }
205
206    impl CreateDomainOptions {
207        /// Creates a new [`CreateDomainOptions`].
208        ///
209        /// - `name`: The name of the domain you want to create.
210        #[inline]
211        pub fn new(name: &str) -> Self {
212            Self {
213                name: name.to_owned(),
214                region: None,
215                custom_return_path: None,
216
217                open_tracking: None,
218                click_tracking: None,
219                tracking_subdomain: None,
220                tls: None,
221                capabilities: None,
222            }
223        }
224
225        /// The region where the email will be sent from.
226        #[inline]
227        pub fn with_region(mut self, region: impl Into<Region>) -> Self {
228            self.region = Some(region.into());
229            self
230        }
231
232        /// For advanced use cases, choose a subdomain for the Return-Path address.
233        /// The custom return path is used for SPF authentication, DMARC alignment, and handling
234        /// bounced emails. Defaults to `send` (i.e., `send.yourdomain.tld`). Avoid setting values
235        /// that could undermine credibility (e.g. `testing`), as they may be exposed to recipients.
236        #[inline]
237        pub fn with_custom_return_path(mut self, custom_return_path: impl Into<String>) -> Self {
238            self.custom_return_path = Some(custom_return_path.into());
239            self
240        }
241
242        #[inline]
243        pub fn with_open_tracking(mut self, open_tracking: bool) -> Self {
244            self.open_tracking = Some(open_tracking);
245            self
246        }
247
248        #[inline]
249        pub fn with_click_tracking(mut self, click_tracking: bool) -> Self {
250            self.click_tracking = Some(click_tracking);
251            self
252        }
253
254        #[inline]
255        pub fn with_tracking_subdomain(mut self, tracking_subdomain: impl Into<String>) -> Self {
256            self.tracking_subdomain = Some(tracking_subdomain.into());
257            self
258        }
259
260        #[inline]
261        pub fn with_tls(mut self, tls: Tls) -> Self {
262            self.tls = Some(tls);
263            self
264        }
265
266        #[inline]
267        pub fn with_capabilities(mut self, capabilities: Value) -> Self {
268            self.capabilities = Some(capabilities);
269            self
270        }
271    }
272
273    /// Region where [`CreateEmailBaseOptions`]s will be sent from.
274    ///
275    /// Possible values are 'us-east-1' | 'eu-west-1' | 'sa-east-1' | 'ap-northeast-1'.
276    ///
277    /// [`CreateEmailBaseOptions`]: crate::types::CreateEmailBaseOptions
278    #[non_exhaustive]
279    #[derive(Debug, Clone, Serialize, Deserialize)]
280    pub enum Region {
281        /// 'us-east-1'
282        #[serde(rename = "us-east-1")]
283        UsEast1,
284        /// 'eu-west-1'
285        #[serde(rename = "eu-west-1")]
286        EuWest1,
287        /// 'sa-east-1'
288        #[serde(rename = "sa-east-1")]
289        SaEast1,
290        /// 'ap-northeast-1'
291        #[serde(rename = "ap-northeast-1")]
292        ApNorthEast1,
293    }
294
295    #[derive(Debug, Clone, Serialize, Deserialize)]
296    pub struct DomainSpfRecord {
297        /// The name of the record.
298        pub name: String,
299        /// The value of the record.
300        pub value: String,
301        /// The type of record.
302        #[serde(rename = "type")]
303        pub r#type: SpfRecordType,
304        /// The time to live for the record.
305        pub ttl: String,
306        /// The status of the record.
307        pub status: DomainRecordStatus,
308
309        pub routing_policy: Option<String>,
310        pub priority: Option<i32>,
311        pub proxy_status: Option<ProxyStatus>,
312    }
313
314    #[derive(Debug, Clone, Serialize, Deserialize)]
315    pub struct DomainDkimRecord {
316        /// The name of the record.
317        pub name: String,
318        /// The value of the record.
319        pub value: String,
320        /// The type of record.
321        #[serde(rename = "type")]
322        pub r#type: DkimRecordType,
323        /// The time to live for the record.
324        pub ttl: String,
325        /// The status of the record.
326        pub status: DomainRecordStatus,
327
328        pub routing_policy: Option<String>,
329        pub priority: Option<i32>,
330        pub proxy_status: Option<ProxyStatus>,
331    }
332
333    #[derive(Debug, Clone, Serialize, Deserialize)]
334    pub struct ReceivingRecord {
335        /// The name of the record.
336        pub name: String,
337        /// The value of the record.
338        pub value: String,
339        /// The type of record.
340        #[serde(rename = "type")]
341        pub r#type: ReceivingRecordType,
342        /// The time to live for the record.
343        pub ttl: String,
344        /// The status of the record.
345        pub status: DomainRecordStatus,
346
347        pub priority: i32,
348    }
349
350    #[derive(Debug, Clone, Serialize, Deserialize)]
351    pub struct TrackingRecord {
352        /// The name of the record.
353        pub name: String,
354        /// The value of the record.
355        pub value: String,
356        /// The type of record.
357        #[serde(rename = "type")]
358        pub r#type: TrackingRecordType,
359        /// The time to live for the record.
360        pub ttl: String,
361        /// The status of the record.
362        pub status: DomainRecordStatus,
363    }
364
365    #[derive(Debug, Clone, Serialize, Deserialize)]
366    pub struct TrackingCaaRecord {
367        /// The name of the record.
368        pub name: String,
369        /// The value of the record (e.g. `0 issue "amazon.com"`).
370        pub value: String,
371        /// The type of record.
372        #[serde(rename = "type")]
373        pub r#type: TrackingCaaRecordType,
374        /// The time to live for the record.
375        pub ttl: String,
376        /// The status of the record.
377        pub status: DomainRecordStatus,
378    }
379
380    #[derive(Debug, Copy, Clone, Serialize, Deserialize)]
381    pub enum ReceivingRecordType {
382        #[allow(clippy::upper_case_acronyms)]
383        MX,
384    }
385
386    #[derive(Debug, Copy, Clone, Serialize, Deserialize)]
387    pub enum TrackingRecordType {
388        #[allow(clippy::upper_case_acronyms)]
389        CNAME,
390    }
391
392    #[derive(Debug, Copy, Clone, Serialize, Deserialize)]
393    pub enum TrackingCaaRecordType {
394        #[allow(clippy::upper_case_acronyms)]
395        CAA,
396    }
397
398    #[derive(Debug, Copy, Clone, Serialize, Deserialize)]
399    pub enum ProxyStatus {
400        Enable,
401        Disable,
402    }
403
404    #[derive(Debug, Copy, Clone, Serialize, Deserialize)]
405    #[serde(rename_all = "snake_case")]
406    pub enum DomainStatus {
407        Pending,
408        Verified,
409        Failed,
410        NotStarted,
411        PartiallyVerified,
412        PartiallyFailed,
413    }
414
415    #[derive(Debug, Copy, Clone, Serialize, Deserialize)]
416    #[serde(rename_all = "snake_case")]
417    pub enum DomainRecordStatus {
418        Pending,
419        Verified,
420        Failed,
421        TemporaryFailure,
422        NotStarted,
423    }
424
425    #[derive(Debug, Copy, Clone, Serialize, Deserialize)]
426    pub enum SpfRecordType {
427        MX,
428        #[allow(clippy::upper_case_acronyms)]
429        TXT,
430    }
431
432    #[derive(Debug, Copy, Clone, Serialize, Deserialize)]
433    pub enum DkimRecordType {
434        #[allow(clippy::upper_case_acronyms)]
435        CNAME,
436        #[allow(clippy::upper_case_acronyms)]
437        TXT,
438    }
439
440    /// Individual [`Domain`] record.
441    #[derive(Debug, Clone, Serialize, Deserialize)]
442    #[serde(tag = "record")]
443    pub enum DomainRecord {
444        #[serde(rename = "SPF")]
445        DomainSpfRecord(DomainSpfRecord),
446        #[serde(rename = "DKIM")]
447        DomainDkimRecord(DomainDkimRecord),
448        #[serde(rename = "Receiving MX")]
449        ReceivingRecord(ReceivingRecord),
450        #[serde(rename = "Tracking")]
451        TrackingRecord(TrackingRecord),
452        #[serde(rename = "TrackingCAA")]
453        TrackingCaaRecord(TrackingCaaRecord),
454    }
455
456    /// Details of an existing domain.
457    #[must_use]
458    #[derive(Debug, Clone, Serialize, Deserialize)]
459    pub struct Domain {
460        /// The ID of the domain.
461        pub id: DomainId,
462        /// The name of the domain.
463        pub name: String,
464        /// The status of the domain.
465        pub status: DomainStatus,
466
467        /// The date and time the domain was created.
468        pub created_at: String,
469        /// The region where the domain is hosted.
470        pub region: Region,
471        /// The records of the domain.
472        pub records: Option<Vec<DomainRecord>>,
473
474        pub capabilities: DomainCapabilities,
475
476        /// Whether open tracking is enabled for this domain.
477        #[serde(skip_serializing_if = "Option::is_none")]
478        pub open_tracking: Option<bool>,
479        /// Whether click tracking is enabled for this domain.
480        #[serde(skip_serializing_if = "Option::is_none")]
481        pub click_tracking: Option<bool>,
482    }
483
484    #[must_use]
485    #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
486    pub struct DomainCapabilities {
487        pub sending: DomainCapabilityStatus,
488        pub receiving: DomainCapabilityStatus,
489    }
490
491    #[non_exhaustive]
492    #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
493    #[serde(rename_all = "snake_case")]
494    pub enum DomainCapabilityStatus {
495        Enabled,
496        Disabled,
497    }
498
499    #[derive(Debug, Clone, Serialize, Deserialize)]
500    pub struct VerifyDomainResponse {
501        /// The ID of the domain.
502        #[allow(dead_code)]
503        pub id: DomainId,
504    }
505
506    /// List of changes to apply to a [`Domain`].
507    #[must_use]
508    #[derive(Debug, Default, Clone, Serialize)]
509    pub struct DomainChanges {
510        /// Enable or disable click tracking for the domain.
511        #[serde(skip_serializing_if = "Option::is_none")]
512        click_tracking: Option<bool>,
513        /// Enable or disable open tracking for the domain.
514        #[serde(skip_serializing_if = "Option::is_none")]
515        open_tracking: Option<bool>,
516        #[serde(skip_serializing_if = "Option::is_none")]
517        tls: Option<Tls>,
518        #[serde(skip_serializing_if = "Option::is_none")]
519        capabilities: Option<DomainCapabilities>,
520        #[serde(skip_serializing_if = "Option::is_none")]
521        tracking_subdomain: Option<String>,
522    }
523
524    impl DomainChanges {
525        /// Creates a new [`DomainChanges`].
526        #[inline]
527        pub fn new() -> Self {
528            Self::default()
529        }
530
531        /// Toggles the click tracking to `enable`.
532        #[inline]
533        pub const fn with_click_tracking(mut self, enable: bool) -> Self {
534            self.click_tracking = Some(enable);
535            self
536        }
537
538        /// Toggles the open tracing to `enable`.
539        #[inline]
540        pub const fn with_open_tracking(mut self, enable: bool) -> Self {
541            self.open_tracking = Some(enable);
542            self
543        }
544
545        /// Changes the TLS configuration.
546        #[inline]
547        pub const fn with_tls(mut self, tls: Tls) -> Self {
548            self.tls = Some(tls);
549            self
550        }
551
552        #[inline]
553        pub fn with_tracking_subdomain(mut self, tracking_subdomain: impl Into<String>) -> Self {
554            self.tracking_subdomain = Some(tracking_subdomain.into());
555            self
556        }
557    }
558
559    #[derive(Debug, Clone, Serialize, Deserialize)]
560    pub struct UpdateDomainResponse {
561        /// The ID of the updated domain.
562        pub id: DomainId,
563    }
564
565    #[derive(Debug, Clone, Serialize, Deserialize)]
566    pub struct DeleteDomainResponse {
567        /// The ID of the domain.
568        pub id: DomainId,
569        /// Indicates whether the domain was deleted successfully.
570        pub deleted: bool,
571    }
572
573    /// Details of a new [`DomainClaim`].
574    #[must_use]
575    #[derive(Debug, Clone, Serialize)]
576    pub struct CreateDomainClaimOptions {
577        #[serde(rename = "name")]
578        name: String,
579        #[serde(rename = "region", skip_serializing_if = "Option::is_none")]
580        region: Option<Region>,
581        #[serde(skip_serializing_if = "Option::is_none")]
582        custom_return_path: Option<String>,
583        #[serde(skip_serializing_if = "Option::is_none")]
584        open_tracking: Option<bool>,
585        #[serde(skip_serializing_if = "Option::is_none")]
586        click_tracking: Option<bool>,
587        #[serde(skip_serializing_if = "Option::is_none")]
588        tracking_subdomain: Option<String>,
589    }
590
591    impl CreateDomainClaimOptions {
592        pub fn new(name: &str) -> Self {
593            Self {
594                name: name.to_owned(),
595                region: None,
596                custom_return_path: None,
597                open_tracking: None,
598                click_tracking: None,
599                tracking_subdomain: None,
600            }
601        }
602
603        /// The region where the email will be sent from.
604        #[inline]
605        pub fn with_region(mut self, region: impl Into<Region>) -> Self {
606            self.region = Some(region.into());
607            self
608        }
609
610        /// For advanced use cases, choose a subdomain for the Return-Path address.
611        /// The custom return path is used for SPF authentication, DMARC alignment, and handling
612        /// bounced emails. Defaults to `send` (i.e., `send.yourdomain.tld`). Avoid setting values
613        /// that could undermine credibility (e.g. `testing`), as they may be exposed to recipients.
614        #[inline]
615        pub fn with_custom_return_path(mut self, custom_return_path: impl Into<String>) -> Self {
616            self.custom_return_path = Some(custom_return_path.into());
617            self
618        }
619
620        #[inline]
621        pub fn with_open_tracking(mut self, open_tracking: bool) -> Self {
622            self.open_tracking = Some(open_tracking);
623            self
624        }
625
626        #[inline]
627        pub fn with_click_tracking(mut self, click_tracking: bool) -> Self {
628            self.click_tracking = Some(click_tracking);
629            self
630        }
631
632        #[inline]
633        pub fn with_tracking_subdomain(mut self, tracking_subdomain: impl Into<String>) -> Self {
634            self.tracking_subdomain = Some(tracking_subdomain.into());
635            self
636        }
637    }
638
639    #[derive(Debug, Copy, Clone, Serialize, Deserialize)]
640    #[serde(rename_all = "snake_case")]
641    pub enum DomainClaimStatus {
642        Pending,
643        Verified,
644        Completed,
645        Blocked,
646        Expired,
647        Superseded,
648        Canceled,
649        Failed,
650    }
651
652    #[derive(Debug, Copy, Clone, Serialize, Deserialize)]
653    #[serde(rename_all = "snake_case")]
654    pub enum DomainClaimBlockedReason {
655        GracePeriod,
656        RecentOwnerActivity,
657        PendingScheduledEmails,
658    }
659
660    #[derive(Debug, Copy, Clone, Serialize, Deserialize)]
661    #[serde(rename_all = "snake_case")]
662    pub enum DomainClaimRecordType {
663        #[allow(clippy::upper_case_acronyms)]
664        TXT,
665    }
666
667    #[must_use]
668    #[derive(Debug, Clone, Serialize, Deserialize)]
669    pub struct DomainClaimRecord {
670        #[serde(rename = "type")]
671        pub r#type: DomainClaimRecordType,
672        pub name: String,
673        pub value: String,
674        pub ttl: String,
675    }
676
677    #[must_use]
678    #[derive(Debug, Clone, Serialize, Deserialize)]
679    pub struct DomainClaim {
680        pub id: DomainClaimId,
681        pub name: String,
682        pub status: DomainClaimStatus,
683        pub domain_id: Option<String>,
684        pub region: Option<Region>,
685        pub record: DomainClaimRecord,
686        pub blocked_reason: Option<DomainClaimBlockedReason>,
687        pub failure_reason: Option<String>,
688        pub created_at: String,
689        pub expires_at: String,
690    }
691}
692
693#[cfg(test)]
694#[allow(clippy::needless_return)]
695mod test {
696    #[cfg(not(feature = "blocking"))]
697    use crate::{
698        domains::types::DeleteDomainResponse,
699        domains::types::{CreateDomainOptions, DomainChanges, Tls},
700        list_opts::ListOptions,
701        test::{CLIENT, DebugResult, retry},
702    };
703
704    #[tokio_shared_rt::test(shared = true)]
705    #[serial_test::serial]
706    #[cfg(not(feature = "blocking"))]
707    async fn all() -> DebugResult<()> {
708        let resend = &*CLIENT;
709
710        // Create
711        let domain = resend
712            .domains
713            .create(CreateDomainOptions::new("resend-rust.com"))
714            .await?;
715
716        std::thread::sleep(std::time::Duration::from_secs(4));
717
718        // List.
719        let list = resend.domains.list(ListOptions::default()).await?;
720        assert_eq!(list.len(), 1);
721
722        // Get
723        let domain = resend.domains.get(&domain.id).await?;
724
725        // Update
726        let updates = DomainChanges::new()
727            .with_open_tracking(false)
728            .with_click_tracking(true)
729            .with_tls(Tls::Enforced);
730
731        std::thread::sleep(std::time::Duration::from_secs(4));
732        let f = async || resend.domains.update(&domain.id, updates.clone()).await;
733        let domain = retry(f, 5, std::time::Duration::from_secs(2)).await?;
734        std::thread::sleep(std::time::Duration::from_secs(4));
735
736        // Delete
737        let f = async || resend.domains.delete(&domain.id).await;
738        let resp: DeleteDomainResponse = retry(f, 5, std::time::Duration::from_secs(2)).await?;
739
740        assert!(resp.deleted);
741
742        // List.
743        let list = resend.domains.list(ListOptions::default()).await?;
744        assert!(list.is_empty());
745
746        Ok(())
747    }
748
749    #[test]
750    #[allow(clippy::indexing_slicing)]
751    fn deserialize_domain_with_tracking_caa_record() {
752        use crate::domains::types::{Domain, DomainRecord};
753
754        let json = r#"{
755            "object": "domain",
756            "id": "7c2a439f-d5fc-4dc1-8bab-ced17f14c972",
757            "name": "namingishard.dev",
758            "created_at": "2026-04-14 11:16:24.808219+00",
759            "status": "verified",
760            "capabilities": { "sending": "enabled", "receiving": "disabled" },
761            "records": [
762                {
763                    "record": "Tracking",
764                    "type": "CNAME",
765                    "name": "links",
766                    "value": "links1.resend-dns-staging.com",
767                    "ttl": "Auto",
768                    "status": "verified"
769                },
770                {
771                    "record": "TrackingCAA",
772                    "name": "",
773                    "type": "CAA",
774                    "ttl": "Auto",
775                    "value": "0 issue \"amazon.com\"",
776                    "status": "verified"
777                }
778            ],
779            "region": "eu-west-1"
780        }"#;
781
782        let domain: Domain = serde_json::from_str(json).expect("domain deserializes");
783        let records = domain.records.expect("records present");
784        assert_eq!(records.len(), 2);
785        assert!(matches!(records[0], DomainRecord::TrackingRecord(_)));
786        assert!(matches!(records[1], DomainRecord::TrackingCaaRecord(_)));
787    }
788
789    #[test]
790    fn deserialize_domain_with_tracking_fields() {
791        use crate::domains::types::Domain;
792
793        let json = r#"{
794            "object": "domain",
795            "id": "fd61172c-cafc-40f5-b049-b45947779a29",
796            "name": "resend.com",
797            "status": "verified",
798            "created_at": "2023-06-21 06:10:36.144+00",
799            "region": "us-east-1",
800            "capabilities": { "sending": "enabled", "receiving": "disabled" },
801            "open_tracking": true,
802            "click_tracking": false
803        }"#;
804
805        let domain: Domain = serde_json::from_str(json).expect("domain deserializes");
806        assert_eq!(domain.open_tracking, Some(true));
807        assert_eq!(domain.click_tracking, Some(false));
808    }
809
810    #[test]
811    fn deserialize_partially_verified_domain() {
812        use crate::domains::types::{Domain, DomainStatus};
813
814        let json = r#"{
815            "object": "domain",
816            "id": "fd61172c-cafc-40f5-b049-b45947779a29",
817            "name": "resend.com",
818            "status": "partially_verified",
819            "created_at": "2023-06-21 06:10:36.144+00",
820            "region": "us-east-1",
821            "capabilities": { "sending": "enabled", "receiving": "disabled" }
822        }"#;
823
824        let domain: Domain = serde_json::from_str(json).expect("domain deserializes");
825        assert!(matches!(domain.status, DomainStatus::PartiallyVerified));
826    }
827
828    #[test]
829    fn deserialize_partially_failed_domain() {
830        use crate::domains::types::{Domain, DomainStatus};
831
832        let json = r#"{
833            "object": "domain",
834            "id": "fd61172c-cafc-40f5-b049-b45947779a29",
835            "name": "resend.com",
836            "status": "partially_failed",
837            "created_at": "2023-06-21 06:10:36.144+00",
838            "region": "us-east-1",
839            "capabilities": { "sending": "enabled", "receiving": "enabled" }
840        }"#;
841
842        let domain: Domain = serde_json::from_str(json).expect("domain deserializes");
843        assert!(matches!(domain.status, DomainStatus::PartiallyFailed));
844    }
845}