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