Skip to main content

seer_core/
availability.rs

1//! Domain availability checking.
2//!
3//! Determines if a domain is available for registration by interpreting
4//! WHOIS/RDAP "not found" responses.
5
6use serde::{Deserialize, Serialize};
7use tracing::debug;
8
9use crate::error::Result;
10use crate::rdap::RdapClient;
11use crate::whois::WhoisClient;
12
13/// Result of a domain availability check.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct AvailabilityResult {
16    /// The domain that was checked.
17    pub domain: String,
18    /// Whether the domain appears to be available for registration.
19    pub available: bool,
20    /// Confidence level of the result ("high", "medium", "low").
21    pub confidence: String,
22    /// How availability was determined.
23    pub method: String,
24    /// Additional details about the check.
25    pub details: Option<String>,
26}
27
28/// Checks domain availability by attempting lookups and interpreting failures.
29#[derive(Debug, Clone)]
30pub struct AvailabilityChecker {
31    rdap_client: RdapClient,
32    whois_client: WhoisClient,
33}
34
35impl Default for AvailabilityChecker {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41impl AvailabilityChecker {
42    pub fn new() -> Self {
43        Self {
44            rdap_client: RdapClient::new(),
45            whois_client: WhoisClient::new(),
46        }
47    }
48
49    /// Check if a domain is available for registration.
50    pub async fn check(&self, domain: &str) -> Result<AvailabilityResult> {
51        let domain = crate::validation::normalize_domain(domain)?;
52        debug!(domain = %domain, "Checking domain availability");
53
54        // Try RDAP first - it gives structured error responses
55        match self.rdap_client.lookup_domain(&domain).await {
56            Ok(response) => {
57                // Domain exists in RDAP - check status
58                let statuses: Vec<String> = response.status.clone();
59                let is_redemption = statuses
60                    .iter()
61                    .any(|s| s.contains("redemption") || s.contains("pending delete"));
62
63                if is_redemption {
64                    return Ok(AvailabilityResult {
65                        domain,
66                        available: false,
67                        confidence: "medium".to_string(),
68                        method: "rdap".to_string(),
69                        details: Some("Domain is in redemption/pending delete period".to_string()),
70                    });
71                }
72
73                Ok(AvailabilityResult {
74                    domain,
75                    available: false,
76                    confidence: "high".to_string(),
77                    method: "rdap".to_string(),
78                    details: Some(format!(
79                        "Domain is registered (status: {})",
80                        statuses.join(", ")
81                    )),
82                })
83            }
84            Err(rdap_err) => {
85                debug!(error = %rdap_err, "RDAP lookup failed, falling back to WHOIS");
86                match self.whois_client.lookup(&domain).await {
87                    Ok(whois_response) => {
88                        if whois_response.is_available() {
89                            Ok(AvailabilityResult {
90                                domain,
91                                available: true,
92                                confidence: "high".to_string(),
93                                method: "whois".to_string(),
94                                details: Some(
95                                    "WHOIS indicates domain is not registered".to_string(),
96                                ),
97                            })
98                        } else {
99                            Ok(AvailabilityResult {
100                                domain,
101                                available: false,
102                                confidence: "high".to_string(),
103                                method: "whois".to_string(),
104                                details: whois_response
105                                    .registrar
106                                    .map(|r| format!("Registered with {}", r)),
107                            })
108                        }
109                    }
110                    Err(whois_err) => {
111                        // Both failed - domain might be available or queries blocked
112                        let whois_msg = whois_err.to_string().to_lowercase();
113                        let likely_available = whois_msg.contains("no match")
114                            || whois_msg.contains("not found")
115                            || whois_msg.contains("no data found")
116                            || whois_msg.contains("no entries found");
117
118                        if likely_available {
119                            Ok(AvailabilityResult {
120                                domain,
121                                available: true,
122                                confidence: "medium".to_string(),
123                                method: "whois_error".to_string(),
124                                details: Some(
125                                    "WHOIS server indicates no matching records".to_string(),
126                                ),
127                            })
128                        } else {
129                            Ok(AvailabilityResult {
130                                domain,
131                                available: false,
132                                confidence: "low".to_string(),
133                                method: "inconclusive".to_string(),
134                                details: Some("Could not determine availability - both RDAP and WHOIS queries failed".to_string()),
135                            })
136                        }
137                    }
138                }
139            }
140        }
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_availability_result_serialization() {
150        let result = AvailabilityResult {
151            domain: "example.com".to_string(),
152            available: false,
153            confidence: "high".to_string(),
154            method: "rdap".to_string(),
155            details: Some("Domain is registered".to_string()),
156        };
157        let json = serde_json::to_string(&result).unwrap();
158        assert!(json.contains("\"available\":false"));
159        assert!(json.contains("\"confidence\":\"high\""));
160    }
161}