1use std::time::Duration;
2
3use serde::de::DeserializeOwned;
4use serde_json::json;
5
6use crate::error::{classify, extract_message, Error};
7use crate::models::*;
8use crate::{check_batch, encode_segment, DEFAULT_BASE_URL, USER_AGENT};
9
10#[derive(Debug, Clone)]
21pub struct Client {
22 api_key: Option<String>,
23 base_url: String,
24 http: reqwest::Client,
25}
26
27#[derive(Debug, Default)]
29pub struct ClientBuilder {
30 api_key: Option<String>,
31 base_url: Option<String>,
32 timeout: Option<Duration>,
33}
34
35impl ClientBuilder {
36 pub fn api_key(mut self, key: impl Into<String>) -> Self {
39 self.api_key = Some(key.into());
40 self
41 }
42
43 pub fn base_url(mut self, base: impl Into<String>) -> Self {
45 self.base_url = Some(base.into());
46 self
47 }
48
49 pub fn timeout(mut self, timeout: Duration) -> Self {
51 self.timeout = Some(timeout);
52 self
53 }
54
55 pub fn build(self) -> Client {
56 let http = reqwest::Client::builder()
57 .timeout(self.timeout.unwrap_or(Duration::from_secs(10)))
58 .build()
59 .expect("failed to build reqwest client");
60 Client {
61 api_key: self.api_key,
62 base_url: self
63 .base_url
64 .unwrap_or_else(|| DEFAULT_BASE_URL.to_string())
65 .trim_end_matches('/')
66 .to_string(),
67 http,
68 }
69 }
70}
71
72impl Default for Client {
73 fn default() -> Self {
74 Self::new()
75 }
76}
77
78impl Client {
79 pub fn new() -> Self {
82 Self::builder().build()
83 }
84
85 pub fn with_api_key(key: impl Into<String>) -> Self {
87 Self::builder().api_key(key).build()
88 }
89
90 pub fn builder() -> ClientBuilder {
91 ClientBuilder::default()
92 }
93
94 pub async fn lookup(&self) -> Result<IpInfo, Error> {
98 self.get("/api/v1/ip".into()).await
99 }
100
101 pub async fn lookup_ip(&self, ip: &str) -> Result<IpInfo, Error> {
103 self.get(format!("/api/v1/ip/{}", encode_segment(ip))).await
104 }
105
106 pub async fn lookup_batch(&self, ips: &[&str]) -> Result<BatchIpLookupResponse, Error> {
108 check_batch(ips, "ips")?;
109 self.post("/api/v1/ip/batch".into(), json!({ "ips": ips })).await
110 }
111
112 pub async fn ip_reputation(&self, ip: &str) -> Result<serde_json::Value, Error> {
114 self.get(format!("/api/v1/ip-reputation/{}", encode_segment(ip))).await
115 }
116
117 pub async fn tor_check(&self, ip: &str) -> Result<TorDetection, Error> {
119 self.get(format!("/api/v1/tor/{}", encode_segment(ip))).await
120 }
121
122 pub async fn asn(&self, ip: &str) -> Result<AsnLookup, Error> {
124 self.get(format!("/api/v1/asn/{}", encode_segment(ip))).await
125 }
126
127 pub async fn email_info(&self, email: &str) -> Result<EmailInfo, Error> {
131 self.get(format!("/api/v1/email/{}", encode_segment(email))).await
132 }
133
134 pub async fn validate_email(&self, email: &str) -> Result<AdvancedEmailValidation, Error> {
136 self.get(format!("/api/v1/email/advanced/{}", encode_segment(email))).await
137 }
138
139 pub async fn validate_email_batch(
141 &self,
142 emails: &[&str],
143 ) -> Result<BatchEmailValidationResponse, Error> {
144 check_batch(emails, "emails")?;
145 self.post("/api/v1/email/advanced/batch".into(), json!({ "emails": emails }))
146 .await
147 }
148
149 pub async fn risk_score(&self) -> Result<RiskScore, Error> {
153 self.get("/api/v1/risk-score".into()).await
154 }
155
156 pub async fn risk_score_ip(&self, ip: &str) -> Result<RiskScore, Error> {
158 self.get(format!("/api/v1/risk-score/{}", encode_segment(ip))).await
159 }
160
161 pub async fn email_risk_score(&self, email: &str) -> Result<RiskScore, Error> {
163 self.get(format!("/api/v1/risk-score/email/{}", encode_segment(email)))
164 .await
165 }
166
167 pub async fn whois(&self, domain: &str) -> Result<Whois, Error> {
170 self.get(format!("/api/v1/dns/whois/{}", encode_segment(domain))).await
171 }
172
173 pub async fn reverse_dns(&self, ip: &str) -> Result<ReverseDns, Error> {
174 self.get(format!("/api/v1/dns/reverse/{}", encode_segment(ip))).await
175 }
176
177 pub async fn forward_dns(&self, hostname: &str) -> Result<ForwardDns, Error> {
178 self.get(format!("/api/v1/dns/forward/{}", encode_segment(hostname)))
179 .await
180 }
181
182 pub async fn mx_records(&self, domain: &str) -> Result<MxLookup, Error> {
183 self.get(format!("/api/v1/dns/mx/{}", encode_segment(domain))).await
184 }
185
186 pub async fn domain_age(&self, domain: &str) -> Result<DomainAge, Error> {
187 self.get(format!("/api/v1/domain/age/{}", encode_segment(domain))).await
188 }
189
190 pub async fn domain_age_batch(
191 &self,
192 domains: &[&str],
193 ) -> Result<BatchDomainAgeResponse, Error> {
194 if domains.is_empty() {
195 return Err(Error::InvalidArgument("domains must not be empty".into()));
196 }
197 self.post("/api/v1/domain/age/batch".into(), json!({ "domains": domains }))
198 .await
199 }
200
201 pub async fn rate_limit(&self) -> Result<RateLimitInfo, Error> {
204 self.get("/api/v1/ratelimit".into()).await
205 }
206
207 pub async fn usage_summary(&self) -> Result<UsageSummary, Error> {
208 self.get("/api/v1/usage/summary".into()).await
209 }
210
211 async fn get<T: DeserializeOwned>(&self, path: String) -> Result<T, Error> {
214 self.request(reqwest::Method::GET, path, None).await
215 }
216
217 async fn post<T: DeserializeOwned>(
218 &self,
219 path: String,
220 body: serde_json::Value,
221 ) -> Result<T, Error> {
222 self.request(reqwest::Method::POST, path, Some(body)).await
223 }
224
225 async fn request<T: DeserializeOwned>(
226 &self,
227 method: reqwest::Method,
228 path: String,
229 body: Option<serde_json::Value>,
230 ) -> Result<T, Error> {
231 let mut request = self
232 .http
233 .request(method, format!("{}{}", self.base_url, path))
234 .header(reqwest::header::USER_AGENT, USER_AGENT)
235 .header(reqwest::header::ACCEPT, "application/json");
236 if let Some(key) = &self.api_key {
237 request = request.query(&[("api_key", key)]);
238 }
239 if let Some(body) = body {
240 request = request.json(&body);
241 }
242
243 let response = request.send().await?;
244 let status = response.status().as_u16();
245 if !response.status().is_success() {
246 let limit = header_i64(&response, "x-ratelimit-limit");
247 let remaining = header_i64(&response, "x-ratelimit-remaining");
248 let reset = header_i64(&response, "x-ratelimit-reset");
249 let body = response.text().await.unwrap_or_default();
250 let message = extract_message(status, &body);
251 if status == 429 {
252 return Err(Error::RateLimit {
253 status,
254 message,
255 body,
256 limit,
257 remaining,
258 reset,
259 });
260 }
261 return Err(classify(status, message, body));
262 }
263 Ok(response.json::<T>().await?)
264 }
265}
266
267fn header_i64(response: &reqwest::Response, name: &str) -> Option<i64> {
268 response
269 .headers()
270 .get(name)
271 .and_then(|value| value.to_str().ok())
272 .and_then(|value| value.parse().ok())
273}