zero_bounce/api/
mod.rs

1pub mod bulk;
2pub mod validation;
3
4use std::collections::HashMap;
5
6use chrono::{NaiveDate, Utc};
7use serde_json::from_str;
8
9pub use crate::ZeroBounce;
10use crate::utility::structures::generic::{FindEmailResponse, FindEmailResponseV2, DomainSearchResponseV2};
11use crate::utility::{ZBError, ZBResult, ENDPOINT_EMAIL_FINDER};
12use crate::utility::structures::{ActivityData, ApiUsage};
13use crate::utility::{ENDPOINT_ACTIVITY_DATA, ENDPOINT_API_USAGE, ENDPOINT_CREDITS};
14
15/// Builder for the `find_email_v2` API call.
16/// 
17/// # Example
18/// ```no_run
19/// use zero_bounce::ZeroBounce;
20/// use zero_bounce::utility::ZBResult;
21/// 
22/// # fn main() -> ZBResult<()> {
23/// let zb = ZeroBounce::new("your_api_key");
24/// let result = zb.find_email_v2()
25///     .first_name("John")
26///     .domain("example.com")
27///     .last_name("Doe")
28///     .call()?;
29/// # Ok(())
30/// # }
31/// ```
32pub struct FindEmailV2Builder<'a> {
33    client: &'a ZeroBounce,
34    first_name: Option<&'a str>,
35    domain: Option<&'a str>,
36    company_name: Option<&'a str>,
37    middle_name: Option<&'a str>,
38    last_name: Option<&'a str>,
39}
40
41/// Builder for the `domain_search_v2` API call.
42/// 
43/// # Example
44/// ```no_run
45/// use zero_bounce::ZeroBounce;
46/// use zero_bounce::utility::ZBResult;
47/// 
48/// # fn main() -> ZBResult<()> {
49/// let zb = ZeroBounce::new("your_api_key");
50/// let result = zb.domain_search_v2()
51///     .domain("example.com")
52///     .call()?;
53/// # Ok(())
54/// # }
55/// ```
56pub struct DomainSearchV2Builder<'a> {
57    client: &'a ZeroBounce,
58    domain: Option<&'a str>,
59    company_name: Option<&'a str>,
60}
61
62impl ZeroBounce {
63
64    fn get_credits_from_string(string_value: String) -> ZBResult<i64> {
65        from_str::<serde_json::Value>(string_value.as_ref())?
66            .get("Credits")
67            .and_then(serde_json::Value::as_str)
68            .map(str::parse::<i64>)
69            .ok_or(ZBError::explicit("credits value not in json"))?
70            .map_err(ZBError::IntParseError)
71    }
72
73    pub fn get_credits(&self) -> ZBResult<i64> {
74        let query_args = HashMap::new();
75
76        let response_content = self.generic_get_request(
77            self.url_provider.url_of(ENDPOINT_CREDITS), query_args
78        )?;
79
80        Self::get_credits_from_string(response_content)
81    }
82
83    pub fn get_api_usage(&self, start_date: NaiveDate, end_date: NaiveDate) -> ZBResult<ApiUsage> {
84        let start_date_str = start_date.format("%F").to_string();
85        let end_date_str = end_date.format("%F").to_string();
86        let query_args = HashMap::from([
87            ("start_date", start_date_str.as_str()),
88            ("end_date", end_date_str.as_str()),
89        ]);
90
91        let response_content = self.generic_get_request(
92            self.url_provider.url_of(ENDPOINT_API_USAGE), query_args
93        )?;
94
95        let api_usage = from_str::<ApiUsage>(&response_content)?;
96        Ok(api_usage)
97    }
98
99    pub fn get_api_usage_overall(&self) -> ZBResult<ApiUsage> {
100        let start_date = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap();
101        let end_date = Utc::now().naive_local().date();
102        self.get_api_usage(start_date, end_date)
103    }
104
105    pub fn get_activity_data(&self, email: &str) -> ZBResult<ActivityData> {
106        let query_args = HashMap::from([
107            ("email", email),
108        ]);
109
110        let response_content = self.generic_get_request(
111            self.url_provider.url_of(ENDPOINT_ACTIVITY_DATA), query_args
112        )?;
113
114        let activity_data = from_str::<ActivityData>(&response_content)?;
115        Ok(activity_data)
116    }
117
118    /// Deprecated: Use `find_email_v2` instead.
119    /// 
120    /// This function is kept for backward compatibility but will be removed in a future version.
121    #[deprecated(
122        since = "1.2.0",
123        note = "Use `find_email_v2` instead. The new version supports both domain and company_name parameters."
124    )]
125    pub fn find_email(&self, domain: &str, first_name: &str, middle_name: &str, last_name: &str) -> ZBResult<FindEmailResponse> {
126        let mut query_args = HashMap::from([
127            ("domain", domain),
128        ]);
129        if !first_name.is_empty() {
130            query_args.insert("first_name", first_name);
131        }
132        if !middle_name.is_empty() {
133            query_args.insert("middle_name", middle_name);
134        }
135        if !last_name.is_empty() {
136            query_args.insert("last_name", last_name);
137        }
138
139        let response_content = self.generic_get_request(
140            self.url_provider.url_of(ENDPOINT_EMAIL_FINDER), query_args
141        )?;
142
143        let activity_data = from_str::<FindEmailResponse>(&response_content)?;
144        Ok(activity_data)
145    }
146
147    /// Find an email address using either a domain or company name.
148    /// 
149    /// Returns a builder that allows you to set parameters using method chaining.
150    /// 
151    /// # Requirements
152    /// - `first_name` is mandatory
153    /// - Exactly one of `domain` or `company_name` must be provided (XOR requirement)
154    /// 
155    /// # Example
156    /// ```no_run
157    /// use zero_bounce::ZeroBounce;
158    /// use zero_bounce::utility::ZBResult;
159    /// 
160    /// # fn main() -> ZBResult<()> {
161    /// let zb = ZeroBounce::new("your_api_key");
162    /// // Using domain
163    /// let result = zb.find_email_v2()
164    ///     .first_name("John")
165    ///     .domain("example.com")
166    ///     .last_name("Doe")
167    ///     .call()?;
168    /// // Or using company name
169    /// let result = zb.find_email_v2()
170    ///     .first_name("John")
171    ///     .company_name("Example Inc")
172    ///     .last_name("Doe")
173    ///     .call()?;
174    /// # Ok(())
175    /// # }
176    /// ```
177    pub fn find_email_v2(&self) -> FindEmailV2Builder<'_> {
178        FindEmailV2Builder {
179            client: self,
180            first_name: None,
181            domain: None,
182            company_name: None,
183            middle_name: None,
184            last_name: None,
185        }
186    }
187
188    /// Deprecated: Use `domain_search_v2` instead.
189    /// 
190    /// This function is kept for backward compatibility but will be removed in a future version.
191    #[deprecated(
192        since = "1.2.0",
193        note = "Use `domain_search_v2` instead. The new version supports both domain and company_name parameters."
194    )]
195    #[allow(deprecated)] // This method uses deprecated find_email, which is expected
196    pub fn domain_search(&self, domain: &str) -> ZBResult<FindEmailResponse> {
197        self.find_email(domain, "", "", "")
198    }
199
200    /// Search for email formats using either a domain or company name.
201    /// 
202    /// Returns a builder that allows you to set parameters using method chaining.
203    /// 
204    /// # Requirements
205    /// Exactly one of `domain` or `company_name` must be provided (XOR requirement).
206    /// 
207    /// # Example
208    /// ```no_run
209    /// use zero_bounce::ZeroBounce;
210    /// use zero_bounce::utility::ZBResult;
211    /// 
212    /// # fn main() -> ZBResult<()> {
213    /// let zb = ZeroBounce::new("your_api_key");
214    /// // Using domain
215    /// let result = zb.domain_search_v2()
216    ///     .domain("example.com")
217    ///     .call()?;
218    /// // Or using company name
219    /// let result = zb.domain_search_v2()
220    ///     .company_name("Example Inc")
221    ///     .call()?;
222    /// # Ok(())
223    /// # }
224    /// ```
225    pub fn domain_search_v2(&self) -> DomainSearchV2Builder<'_> {
226        DomainSearchV2Builder {
227            client: self,
228            domain: None,
229            company_name: None,
230        }
231    }
232
233}
234
235impl<'a> FindEmailV2Builder<'a> {
236    /// Set the first name (mandatory).
237    pub fn first_name(mut self, name: &'a str) -> Self {
238        self.first_name = Some(name);
239        self
240    }
241
242    /// Set the domain name (exactly one of domain or company_name must be provided).
243    pub fn domain(mut self, domain: &'a str) -> Self {
244        self.domain = Some(domain);
245        self
246    }
247
248    /// Set the company name (exactly one of domain or company_name must be provided).
249    pub fn company_name(mut self, company: &'a str) -> Self {
250        self.company_name = Some(company);
251        self
252    }
253
254    /// Set the middle name (optional).
255    pub fn middle_name(mut self, name: &'a str) -> Self {
256        self.middle_name = Some(name);
257        self
258    }
259
260    /// Set the last name (optional).
261    pub fn last_name(mut self, name: &'a str) -> Self {
262        self.last_name = Some(name);
263        self
264    }
265
266    /// Execute the API call and return the result.
267    pub fn call(self) -> ZBResult<FindEmailResponseV2> {
268        let first_name = self.first_name.ok_or_else(|| ZBError::explicit("first_name is mandatory and must be set"))?;
269        
270        if first_name.is_empty() {
271            return Err(ZBError::explicit("first_name cannot be empty"));
272        }
273
274        // Validate XOR requirement: exactly one of domain or company_name must be provided
275        match (self.domain, self.company_name) {
276            (Some(d), None) => {
277                if d.is_empty() {
278                    return Err(ZBError::explicit("domain cannot be empty"));
279                }
280            }
281            (None, Some(c)) => {
282                if c.is_empty() {
283                    return Err(ZBError::explicit("company_name cannot be empty"));
284                }
285            }
286            (Some(_), Some(_)) => {
287                return Err(ZBError::explicit("exactly one of domain or company_name must be provided, not both"));
288            }
289            (None, None) => {
290                return Err(ZBError::explicit("either domain or company_name must be provided"));
291            }
292        }
293
294        let mut query_args = HashMap::from([
295            ("first_name", first_name),
296        ]);
297
298        if let Some(d) = self.domain {
299            query_args.insert("domain", d);
300        }
301
302        if let Some(c) = self.company_name {
303            query_args.insert("company_name", c);
304        }
305
306        if let Some(middle) = self.middle_name {
307            if !middle.is_empty() {
308                query_args.insert("middle_name", middle);
309            }
310        }
311
312        if let Some(last) = self.last_name {
313            if !last.is_empty() {
314                query_args.insert("last_name", last);
315            }
316        }
317
318        let response_content = self.client.generic_get_request(
319            self.client.url_provider.url_of(ENDPOINT_EMAIL_FINDER), query_args
320        )?;
321
322        let find_email_response = from_str::<FindEmailResponseV2>(&response_content)?;
323        Ok(find_email_response)
324    }
325}
326
327impl<'a> DomainSearchV2Builder<'a> {
328    /// Set the domain name (exactly one of domain or company_name must be provided).
329    pub fn domain(mut self, domain: &'a str) -> Self {
330        self.domain = Some(domain);
331        self
332    }
333
334    /// Set the company name (exactly one of domain or company_name must be provided).
335    pub fn company_name(mut self, company: &'a str) -> Self {
336        self.company_name = Some(company);
337        self
338    }
339
340    /// Execute the API call and return the result.
341    pub fn call(self) -> ZBResult<DomainSearchResponseV2> {
342        // Validate XOR requirement: exactly one of domain or company_name must be provided
343        match (self.domain, self.company_name) {
344            (Some(d), None) => {
345                if d.is_empty() {
346                    return Err(ZBError::explicit("domain cannot be empty"));
347                }
348            }
349            (None, Some(c)) => {
350                if c.is_empty() {
351                    return Err(ZBError::explicit("company_name cannot be empty"));
352                }
353            }
354            (Some(_), Some(_)) => {
355                return Err(ZBError::explicit("exactly one of domain or company_name must be provided, not both"));
356            }
357            (None, None) => {
358                return Err(ZBError::explicit("either domain or company_name must be provided"));
359            }
360        }
361
362        let mut query_args = HashMap::new();
363
364        if let Some(d) = self.domain {
365            query_args.insert("domain", d);
366        }
367
368        if let Some(c) = self.company_name {
369            query_args.insert("company_name", c);
370        }
371
372        let response_content = self.client.generic_get_request(
373            self.client.url_provider.url_of(ENDPOINT_EMAIL_FINDER), query_args
374        )?;
375
376        let domain_search_response = from_str::<DomainSearchResponseV2>(&response_content)?;
377        Ok(domain_search_response)
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use crate::utility::mock_constants::CREDITS_RESPONSE_OK;
384    use crate::utility::mock_constants::CREDITS_RESPONSE_NEGATIVE;
385
386    use super::*;
387
388    #[test]
389    fn test_credits_negative() {
390        let credits = ZeroBounce::get_credits_from_string(CREDITS_RESPONSE_NEGATIVE.to_string());
391        assert!(credits.is_ok());
392
393        let amount = credits.unwrap();
394        assert_eq!(amount, -1);
395    }
396
397    #[test]
398    fn test_credits_ok() {
399        let credits = ZeroBounce::get_credits_from_string(CREDITS_RESPONSE_OK.to_string());
400        assert!(credits.is_ok());
401
402        let amount = credits.unwrap();
403        assert_eq!(amount, 123456);
404    }
405
406}