zerobounce-sdk 1.1.1 → 2.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/documentation.md CHANGED
@@ -76,10 +76,13 @@ Zerobounce.api_usage(Date.today, Date.today)
76
76
  "sub_status_mailbox_quota_exceeded"=>0,
77
77
  "sub_status_forcible_disconnect"=>0,
78
78
  "sub_status_failed_smtp_connection"=>0,
79
+ "sub_status_accept_all"=>0,
79
80
  "sub_status_mx_forward"=>0,
80
81
  "sub_status_alternate"=>0,
81
- "sub_status_blocked"=>0,
82
82
  "sub_status_allowed"=>0,
83
+ "sub_status_blocked"=>0,
84
+ "sub_status_gold"=>0,
85
+ "sub_status_role_based_accept_all"=>0,
83
86
  "start_date"=>"4/28/2023",
84
87
  "end_date"=>"4/28/2023"}
85
88
  ```
@@ -265,10 +268,13 @@ Send file
265
268
  last_name_column: 3,
266
269
  gender_column: 4,
267
270
  has_header_row: true,
268
- return_url: nil ### results callback url
271
+ return_url: nil, ### results callback url
272
+ allow_phase_2: true ### optional; validation bulk sendfile only (omit or nil to skip)
269
273
  ### Zerobounce.validate_file_send(validate_file_path, email_address_column: 1, gender_column: 4)
270
274
  ```
271
275
 
276
+ Bulk validation uses `https://bulkapi.zerobounce.net/v2`. See [v2 send file](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-send-file), [v2 file status](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-file-status), and [v2 get file](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-get-file).
277
+
272
278
  Check file
273
279
  ```ruby
274
280
  file_id = "75d854a6-565c-49f9-b4c8-b3344480ec4c"
@@ -281,6 +287,7 @@ Zerobounce.validate_file_check(file_id)
281
287
  "upload_date"=>"2023-04-28T15:25:41Z",
282
288
  "file_status"=>"Greylisted",
283
289
  "complete_percentage"=>"0%",
290
+ "file_phase_2_status"=>"N/A", ### when present (validation bulk v2)
284
291
  "error_reason"=>nil,
285
292
  "return_url"=>nil}
286
293
  ```
@@ -293,6 +300,16 @@ Zerobounce.validate_file_get(file_id)
293
300
  => "\"email\",\"first\",\"last\",\"gender\",\"ip\",\"ZB Status\",\"ZB Sub Status\",\"ZB Account\",\"ZB Domain\",\"ZB First Name\",\"ZB Last Name\",\"ZB Gender\",\"ZB Free Email\",\"ZB MX Found\",\"ZB MX Record\",\"ZB SMTP Provider\",\"ZB Did You Mean\"\n\"disposable@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"do_not_mail\",\"disposable\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"invalid@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"invalid\",\"mailbox_not_found\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"valid@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"valid\",\"\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"toxic@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"do_not_mail\",\"toxic\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"donotmail@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"do_not_mail\",\"role_based\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"spamtrap@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"spamtrap\",\"\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"abuse@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"abuse\",\"\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"unknown@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"mail_server_temporary_error\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"catch_all@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"catch-all\",\"\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"antispam_system@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"antispam_system\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"does_not_accept_mail@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"invalid\",\"does_not_accept_mail\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"exception_occurred@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"exception_occurred\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"failed_smtp_connection@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"failed_smtp_connection\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"failed_syntax_check@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"invalid\",\"failed_syntax_check\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"forcible_disconnect@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"forcible_disconnect\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"global_suppression@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"do_not_mail\",\"global_suppression\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"greylisted@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"greylisted\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"leading_period_removed@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"valid\",\"leading_period_removed\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"mail_server_did_not_respond@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"mail_server_did_not_respond\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"mail_server_temporary_error@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"mail_server_temporary_error\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"mailbox_quota_exceeded@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"invalid\",\"mailbox_quota_exceeded\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"mailbox_not_found@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"invalid\",\"mailbox_not_found\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"no_dns_entries@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"invalid\",\"no_dns_entries\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"possible_trap@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"do_not_mail\",\"possible_trap\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"possible_typo@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"invalid\",\"possible_typo\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"role_based@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"do_not_mail\",\"role_based\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"timeout_exceeded@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"timeout_exceeded\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"unroutable_ip_address@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"invalid\",\"unroutable_ip_address\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"free_email@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"valid\",\"\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"True\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"role_based_catch_all@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"do_not_mail\",\"role_based_catch_all\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n"
294
301
  ```
295
302
 
303
+ Optional [v2 get file](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-get-file) query parameters: `Zerobounce::GetFileOptions` and `Zerobounce::DownloadType` (`PHASE_1`, `PHASE_2`, `COMBINED`). Use `activity_data` on the options for validation `validate_file_get` only. JSON error bodies (including some HTTP 200 responses) raise `RuntimeError`; use `Zerobounce.get_file_json_indicates_error?(body)` on a raw string if needed.
304
+
305
+ ```ruby
306
+ opts = Zerobounce::GetFileOptions.new(
307
+ download_type: Zerobounce::DownloadType::COMBINED,
308
+ activity_data: true
309
+ )
310
+ csv = Zerobounce.validate_file_get(file_id, opts)
311
+ ```
312
+
296
313
  Delete file
297
314
  ```ruby
298
315
  file_id = "75d854a6-565c-49f9-b4c8-b3344480ec4c"
@@ -346,13 +363,14 @@ file_id = "89fb7262-b845-4fa1-aa25-e486347ec64e"
346
363
  => "89fb7262-b845-4fa1-aa25-e486347ec64e"
347
364
  Zerobounce.scoring_file_get(file_id)
348
365
  => "\"email\",\"ZeroBounceQualityScore\"\r\n\"disposable@example.com\",\"0\"\r\n\"invalid@example.com\",\"10\"\r\n\"valid@example.com\",\"10\"\r\n\"toxic@example.com\",\"2\"\r\n\"donotmail@example.com\",\"0\"\r\n\"spamtrap@example.com\",\"0\"\r\n\"abuse@example.com\",\"10\"\r\n\"unknown@example.com\",\"10\"\r\n\"catch_all@example.com\",\"10\"\r\n\"antispam_system@example.com\",\"0\"\r\n\"does_not_accept_mail@example.com\",\"0\"\r\n\"exception_occurred@example.com\",\"0\"\r\n\"failed_smtp_connection@example.com\",\"0\"\r\n\"failed_syntax_check@example.com\",\"0\"\r\n\"forcible_disconnect@example.com\",\"0\"\r\n\"global_suppression@example.com\",\"0\"\r\n\"greylisted@example.com\",\"0\"\r\n\"leading_period_removed@example.com\",\"0\"\r\n\"mail_server_did_not_respond@example.com\",\"0\"\r\n\"mail_server_temporary_error@example.com\",\"0\"\r\n\"mailbox_quota_exceeded@example.com\",\"0\"\r\n\"mailbox_not_found@example.com\",\"0\"\r\n\"no_dns_entries@example.com\",\"0\"\r\n\"possible_trap@example.com\",\"0\"\r\n\"possible_typo@example.com\",\"0\"\r\n\"role_based@example.com\",\"0\"\r\n\"timeout_exceeded@example.com\",\"0\"\r\n\"unroutable_ip_address@example.com\",\"0\"\r\n\"free_email@example.com\",\"0\"\r\n\"role_based_catch_all@example.com\",\"0\""
366
+ ### Optional second argument: GetFileOptions with download_type only (activity_data is not sent for scoring getfile)
349
367
  ```
350
368
 
351
369
  Delete file
352
370
  ```ruby
353
371
  file_id = "89fb7262-b845-4fa1-aa25-e486347ec64e"
354
372
  => "89fb7262-b845-4fa1-aa25-e486347ec64e"
355
- Zerobounce.validate_file_delete(file_id)
373
+ Zerobounce.scoring_file_delete(file_id)
356
374
  => {"success"=>false, "message"=>"File cannot be found."}
357
375
  ```
358
376
 
@@ -400,6 +418,81 @@ Zerobounce.guessformat("zerobounce.net")
400
418
  # Zerobounce.guessformat("zerobounce.net", first_name: "John", middle_name: 'Deere', last_name: "Doe")
401
419
  ```
402
420
 
421
+ Find Email Address
422
+ ```ruby
423
+ Zerobounce.find_email("John", domain: "zerobounce.net")
424
+ =>
425
+ {
426
+ "email": "john@zerobounce.net",
427
+ "email_confidence": "medium",
428
+ "domain": "zerobounce.net",
429
+ "company_name": "ZeroBounce",
430
+ "did_you_mean": "",
431
+ "failure_reason": ""
432
+ }
433
+
434
+ # With additional name information for better accuracy
435
+ Zerobounce.find_email("John", domain: "zerobounce.net", middle_name: "Deere", last_name: "Doe")
436
+ ```
437
+
438
+ Find Domain Information
439
+ ```ruby
440
+ Zerobounce.find_domain(domain: "zerobounce.net")
441
+ =>
442
+ {
443
+ "domain": "zerobounce.net",
444
+ "company_name": "Hertza, LLC",
445
+ "format": "first.last",
446
+ "confidence": "high",
447
+ "did_you_mean": "",
448
+ "failure_reason": "",
449
+ "other_domain_formats": [
450
+ {"format": "first", "confidence": "high"},
451
+ {"format": "last.first", "confidence": "high"},
452
+ {"format": "lastfirst", "confidence": "high"},
453
+ {"format": "firstl", "confidence": "high"},
454
+ {"format": "lfirst", "confidence": "high"},
455
+ {"format": "firstlast", "confidence": "high"},
456
+ {"format": "last_middle_f", "confidence": "high"},
457
+ {"format": "last", "confidence": "high"},
458
+ {"format": "f.last", "confidence": "medium"},
459
+ {"format": "last-f", "confidence": "medium"},
460
+ {"format": "l.first", "confidence": "medium"},
461
+ {"format": "last_f", "confidence": "medium"},
462
+ {"format": "first.middle.last", "confidence": "medium"},
463
+ {"format": "first-last", "confidence": "medium"},
464
+ {"format": "last.f", "confidence": "medium"},
465
+ {"format": "last_first", "confidence": "medium"},
466
+ {"format": "f-last", "confidence": "medium"},
467
+ {"format": "first.l", "confidence": "medium"},
468
+ {"format": "first-l", "confidence": "medium"},
469
+ {"format": "first_l", "confidence": "medium"},
470
+ {"format": "first_last", "confidence": "medium"},
471
+ {"format": "f_last", "confidence": "medium"},
472
+ {"format": "last-first", "confidence": "medium"},
473
+ {"format": "flast", "confidence": "medium"},
474
+ {"format": "lastf", "confidence": "medium"},
475
+ {"format": "l_first", "confidence": "medium"},
476
+ {"format": "l-first", "confidence": "medium"},
477
+ {"format": "first-middle-last", "confidence": "low"},
478
+ {"format": "firstmlast", "confidence": "low"},
479
+ {"format": "last.middle.first", "confidence": "low"},
480
+ {"format": "last_middle_first", "confidence": "low"},
481
+ {"format": "first_middle_last", "confidence": "low"},
482
+ {"format": "last-middle-first", "confidence": "low"},
483
+ {"format": "first-m-last", "confidence": "low"},
484
+ {"format": "firstmiddlelast", "confidence": "low"},
485
+ {"format": "last.m.first", "confidence": "low"},
486
+ {"format": "lastmfirst", "confidence": "low"},
487
+ {"format": "lastmiddlefirst", "confidence": "low"},
488
+ {"format": "last_m_first", "confidence": "low"},
489
+ {"format": "first.m.last", "confidence": "low"},
490
+ {"format": "first_m_last", "confidence": "low"},
491
+ {"format": "last-m-first", "confidence": "low"}
492
+ ]
493
+ }
494
+ ```
495
+
403
496
 
404
497
  #### Development
405
498
 
@@ -421,17 +514,17 @@ Finished in 6.81 seconds (files took 0.40587 seconds to load)
421
514
 
422
515
  ##### Test parameters
423
516
  The tests use the following environment parameters:
424
- TEST {unit|live} influences whether mocked unit tests are run or the live server is used (credits may be used if you choose to do this)
425
- ZEROBOUNCE_API_KEY {<zerobounce-api-key-value>} this key is used to make requests to the live server; it is also used in mock tests as a valid key sample (any value will work for mock tests)
426
- INCORRECT_API_KEY {any non whitespace string value that is not a valid key} used for tests where the requests are meant to fail due to the API key value.
517
+ - **TEST** {unit|live} – Influences whether mocked unit tests are run or the live server is used (credits may be used if you choose to do this).
518
+ - **ZEROBOUNCE_API_KEY** – Your API key; used to make requests to the live server and in mock tests as the valid key sample (any value will work for mock tests).
519
+
520
+ An invalid API key for error-handling tests is hardcoded in the spec; no env var is required.
427
521
 
428
- To set them
522
+ To set them:
429
523
  ```bash
430
- export ZEROBOUNCE_API_KEY=99e7ef20ceea4480a173b07b1be75371
431
- export INCORRECT_API_KEY=thiskeyisinvalidorotherwiseincorrect
524
+ export ZEROBOUNCE_API_KEY=your_api_key_here
432
525
  export TEST=unit
433
526
  ```
434
527
 
435
- A .env.sample file is provided.
528
+ A .env.example file is provided.
436
529
 
437
- Mock tests were generated using webmock and vcr. This means that actual requests were made and recorded in the spec/cassettes with an (at the time) valid API key used for testing purposes. This key has been invalidated in the meantime, however it is provided in the .env.sample file for the mock tests to work. If you do not wish to use this key for mocks, you can replace it with any value in the .yml files under spec/cassettes or delete all of them and rerun the tests so that vcr records them with a new key.
530
+ Mock tests were generated using webmock and vcr. This means that actual requests were made and recorded in the spec/cassettes with an (at the time) valid API key used for testing purposes. This key has been invalidated in the meantime, however it is provided in the .env.example file for the mock tests to work. If you do not wish to use this key for mocks, you can replace it with any value in the .yml files under spec/cassettes or delete all of them and rerun the tests so that vcr records them with a new key.
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zerobounce
4
+ # API URL constants for Zerobounce endpoints
5
+ #
6
+ # This class provides constant values for different Zerobounce API endpoints
7
+ # that can be accessed publicly by gem users.
8
+ #
9
+ class ApiUrls
10
+ # API URLs
11
+ DEFAULT_URL = 'https://api.zerobounce.net/v2/'
12
+ EU_URL = 'https://api-eu.zerobounce.net/v2/'
13
+ US_URL = 'https://api-us.zerobounce.net/v2/'
14
+
15
+ # Bulk API URLs
16
+ BULK_DEFAULT_URL = 'https://bulkapi.zerobounce.net/v2/'
17
+ end
18
+ end
@@ -13,6 +13,34 @@ module Zerobounce
13
13
 
14
14
  protected
15
15
 
16
+ # Strips trailing slashes from root URL without using a regex (avoids ReDoS).
17
+ def self.__root_without_trailing_slashes__(root)
18
+ s = root.to_s
19
+ s = s.chomp('/') while s.end_with?('/')
20
+ s
21
+ end
22
+
23
+ # Resolves and validates filepath to prevent path traversal (e.g. ../../etc/passwd).
24
+ # Returns a canonical path only if the file is under the current directory and is a regular file.
25
+ def self.__safe_file_path__(filepath)
26
+ raise ArgumentError, 'File path is required' if filepath.nil? || filepath.to_s.empty?
27
+ filepath = filepath.to_s
28
+ expanded = File.expand_path(filepath)
29
+ base = File.realpath(Dir.pwd)
30
+ base_with_sep = base + File::SEPARATOR
31
+ unless expanded == base || expanded.start_with?(base_with_sep)
32
+ raise ArgumentError, 'File path must be under the current directory'
33
+ end
34
+ canonical = File.realpath(expanded)
35
+ unless canonical == base || canonical.start_with?(base_with_sep)
36
+ raise ArgumentError, 'File path must be under the current directory'
37
+ end
38
+ unless File.file?(canonical)
39
+ raise ArgumentError, 'File path must point to a regular file'
40
+ end
41
+ canonical
42
+ end
43
+
16
44
  def self._get(root, path, params, content_type='application/json')
17
45
 
18
46
  # puts path
@@ -21,7 +49,7 @@ module Zerobounce
21
49
  raise ("API key must be assigned") if not Zerobounce.config.apikey
22
50
 
23
51
  params[:api_key] = Zerobounce.config.apikey
24
- url = "#{root}/#{path}"
52
+ url = "#{Zerobounce::BaseRequest.__root_without_trailing_slashes__(root)}/#{path}"
25
53
 
26
54
  response = RestClient.get(url, {params: params})
27
55
  return response
@@ -32,11 +60,11 @@ module Zerobounce
32
60
  raise ("API key must be assigned") if not Zerobounce.config.apikey
33
61
 
34
62
  params[:api_key] = Zerobounce.config.apikey
35
- url = "#{root}/#{path}"
63
+ url = "#{Zerobounce::BaseRequest.__root_without_trailing_slashes__(root)}/#{path}"
36
64
  response = nil
37
65
 
38
66
  if filepath or content_type == 'multipart/form-data'
39
- params[:file] = File.new(filepath, 'rb')
67
+ params[:file] = File.new(Zerobounce::BaseRequest.__safe_file_path__(filepath), 'rb')
40
68
  params[:multipart] = true
41
69
  response = RestClient.post(url, params)
42
70
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'rest-client'
4
4
  require 'dotenv'
5
+ require_relative 'api_urls'
5
6
 
6
7
  module Zerobounce
7
8
  # Configuration object for Zerobounce.
@@ -17,20 +18,23 @@ module Zerobounce
17
18
  # @attr [String] apikey
18
19
  # A Zerobounce API key.
19
20
  #
21
+ # @attr [String] api_root_url
22
+ # The Zerobounce API root URL. Defaults to ApiUrls::DEFAULT_URL.
23
+ #
24
+ # @attr [String] bulk_api_root_url
25
+ # The Zerobounce bulk API root URL.
26
+ #
20
27
  # @attr [Array<Symbol>] valid_statues
21
28
  # The statuses that are considered valid by {Response#valid?}.
22
29
  class Configuration
23
- attr_accessor :host
24
- attr_accessor :headers
25
- attr_accessor :apikey
26
- attr_accessor :valid_statuses
27
- attr_accessor :mock
30
+ attr_accessor :headers, :apikey, :api_root_url, :bulk_api_root_url, :valid_statuses, :mock
28
31
 
29
- def initialize(mock=false)
30
- if File.file?(".env") then Dotenv.load(".env") else Dotenv.load end
31
- self.host = 'https://api.zerobounce.net'
32
+ def initialize(mock = false)
33
+ File.file?('.env') ? Dotenv.load('.env') : Dotenv.load
32
34
  self.apikey = ENV['ZEROBOUNCE_API_KEY']
33
- self.valid_statuses = %i[valid catch_all]
35
+ self.api_root_url = ENV['ZEROBOUNCE_API_URL'] || ApiUrls::DEFAULT_URL
36
+ self.bulk_api_root_url = ENV['ZEROBOUNCE_BULK_API_URL'] || ApiUrls::BULK_DEFAULT_URL
37
+ self.valid_statuses = %i[valid catch_all accept_all]
34
38
  self.headers = { user_agent: "ZerobounceRubyGem/#{Zerobounce::VERSION}" }
35
39
  self.mock = mock
36
40
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zerobounce
4
+ # Values for bulk getfile query parameter +download_type+ (validation and scoring).
5
+ module DownloadType
6
+ PHASE_1 = 'phase_1'
7
+ PHASE_2 = 'phase_2'
8
+ COMBINED = 'combined'
9
+ end
10
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'stringio'
5
+
6
+ module Zerobounce
7
+ # Bulk getfile response handling (v2): HTTP errors, JSON error bodies (including HTTP 200).
8
+ module GetFileHelper
9
+ module_function
10
+
11
+ # @param [String] body
12
+ # @return [Boolean]
13
+ def json_indicates_error?(body)
14
+ t = body.to_s.lstrip
15
+ return false if t.empty? || t[0] != '{'
16
+
17
+ o = JSON.parse(t)
18
+ return false unless o.is_a?(Hash)
19
+
20
+ if o.key?('success')
21
+ s = o['success']
22
+ return true if s == false || s == 'False' || s == 'false'
23
+ end
24
+
25
+ %w[message error error_message].each do |k|
26
+ v = o[k]
27
+ next if v.nil?
28
+
29
+ return true if v.is_a?(String) && !v.strip.empty?
30
+ return true if v.is_a?(Array) && !v.empty?
31
+ end
32
+
33
+ o.key?('success')
34
+ rescue JSON::ParserError
35
+ false
36
+ end
37
+
38
+ # @param [String] body
39
+ # @return [String]
40
+ def format_error_message(body)
41
+ o = JSON.parse(body.to_s)
42
+ return body.to_s unless o.is_a?(Hash)
43
+
44
+ %w[message error error_message].each do |k|
45
+ v = o[k]
46
+ next if v.nil?
47
+ return v.strip if v.is_a?(String) && !v.strip.empty?
48
+ return v[0].strip if v.is_a?(Array) && v[0].is_a?(String)
49
+ end
50
+ body.to_s
51
+ rescue JSON::ParserError
52
+ body.to_s.empty? ? 'Invalid getfile response' : body.to_s
53
+ end
54
+
55
+ # @param [String] body
56
+ # @param [String] content_type
57
+ # @return [Boolean]
58
+ def should_treat_as_error?(body, content_type)
59
+ ct = content_type.to_s.downcase
60
+ return true if ct.include?('application/json')
61
+
62
+ json_indicates_error?(body)
63
+ end
64
+
65
+ # @param [RestClient::Response] response
66
+ # @return [String] CSV/text body on success
67
+ def process_getfile_response(response)
68
+ code = response.code.to_i
69
+ body_str = response.body.to_s
70
+ ct = (response.headers[:content_type] || response.headers['Content-Type'] || '').to_s
71
+
72
+ if code > 299
73
+ msg = if body_str.lstrip.start_with?('{')
74
+ format_error_message(body_str)
75
+ else
76
+ body_str.empty? ? "HTTP #{code}" : body_str
77
+ end
78
+ raise msg
79
+ end
80
+
81
+ if should_treat_as_error?(body_str, ct)
82
+ raise format_error_message(body_str)
83
+ end
84
+
85
+ io = StringIO.new(response.body)
86
+ io.set_encoding_by_bom
87
+ io.string
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zerobounce
4
+ # Optional query parameters for bulk getfile.
5
+ # +activity_data+ applies to validation +validate_file_get+ only; scoring getfile does not send it.
6
+ class GetFileOptions
7
+ attr_accessor :download_type, :activity_data
8
+
9
+ def initialize(download_type: nil, activity_data: nil)
10
+ @download_type = download_type
11
+ @activity_data = activity_data
12
+ end
13
+ end
14
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'zerobounce/base_request'
4
+ require 'zerobounce/get_file_helper'
4
5
 
5
6
  module Zerobounce
6
7
 
@@ -8,7 +9,7 @@ module Zerobounce
8
9
  class MockRequest < BaseRequest
9
10
 
10
11
  def self.get(path, params, content_type='application/json')
11
- response = self._get(Zerobounce::API_ROOT_URL, path, params, content_type)
12
+ response = self._get(Zerobounce.configuration.api_root_url, path, params, content_type)
12
13
  if response.headers[:content_type] == 'application/json'
13
14
  response_body = response.body
14
15
  response_body_json = JSON.parse(response_body)
@@ -25,7 +26,7 @@ module Zerobounce
25
26
  end
26
27
 
27
28
  def self.bulk_get(path, params, content_type='application/json')
28
- response = self._get(Zerobounce::BULK_API_ROOT_URL, path, params, content_type)
29
+ response = self._get(Zerobounce.configuration.bulk_api_root_url, path, params, content_type)
29
30
  if response.headers[:content_type] == 'application/json'
30
31
  response_body = response.body
31
32
  response_body_json = JSON.parse(response_body)
@@ -41,8 +42,30 @@ module Zerobounce
41
42
  end
42
43
  end
43
44
 
45
+ def self.bulk_getfile(path, params)
46
+ response = self._get(Zerobounce.configuration.bulk_api_root_url, path, params, 'application/json')
47
+ GetFileHelper.process_getfile_response(response)
48
+ end
49
+
50
+ def self.post(path, params, content_type='application/json', filepath=nil)
51
+ response = self._post(Zerobounce.configuration.api_root_url, path, params, \
52
+ content_type, filepath)
53
+ if response.headers[:content_type] == 'application/json'
54
+ response_body = response.body
55
+ response_body_json = JSON.parse(response_body)
56
+
57
+ raise (response_body_json['error']) if response_body_json.key?('error')
58
+ raise (response_body_json['errors'][0]['error']) \
59
+ if response_body_json.key?('errors') and \
60
+ response_body_json['errors'].length > 0
61
+
62
+ return response_body_json
63
+ end
64
+ return response.body
65
+ end
66
+
44
67
  def self.bulk_post(path, params, content_type='application/json', filepath=nil)
45
- response = self._post(Zerobounce::BULK_API_ROOT_URL, path, params, \
68
+ response = self._post(Zerobounce.configuration.bulk_api_root_url, path, params, \
46
69
  content_type, filepath)
47
70
  if response.headers[:content_type] == 'application/json'
48
71
  response_body = response.body
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'stringio'
4
4
  require 'zerobounce/base_request'
5
+ require 'zerobounce/get_file_helper'
5
6
 
6
7
  module Zerobounce
7
8
 
@@ -9,7 +10,7 @@ module Zerobounce
9
10
  class Request < BaseRequest
10
11
 
11
12
  def self.get(path, params, content_type='application/json')
12
- response = self._get(Zerobounce::API_ROOT_URL, path, params, content_type)
13
+ response = self._get(Zerobounce.configuration.api_root_url, path, params, content_type)
13
14
  if response.headers[:content_type] == 'application/json'
14
15
  response_body = response.body
15
16
  response_body_json = JSON.parse(response_body)
@@ -29,7 +30,7 @@ module Zerobounce
29
30
  end
30
31
 
31
32
  def self.bulk_get(path, params, content_type='application/json')
32
- response = self._get(Zerobounce::BULK_API_ROOT_URL, path, params, content_type)
33
+ response = self._get(Zerobounce.configuration.bulk_api_root_url, path, params, content_type)
33
34
  if response.headers[:content_type] == 'application/json'
34
35
  response_body = response.body
35
36
  response_body_json = JSON.parse(response_body)
@@ -47,8 +48,31 @@ module Zerobounce
47
48
  end
48
49
  end
49
50
 
51
+ # Bulk getfile only: treats non-2xx and JSON error payloads (including HTTP 200) as failures.
52
+ def self.bulk_getfile(path, params)
53
+ response = self._get(Zerobounce.configuration.bulk_api_root_url, path, params, 'application/json')
54
+ GetFileHelper.process_getfile_response(response)
55
+ end
56
+
57
+ def self.post(path, params, content_type='application/json', filepath=nil)
58
+ response = self._post(Zerobounce.configuration.api_root_url, path, params, \
59
+ content_type, filepath)
60
+ if response.headers[:content_type] == 'application/json'
61
+ response_body = response.body
62
+ response_body_json = JSON.parse(response_body)
63
+
64
+ raise (response_body_json['error']) if response_body_json.key?('error')
65
+ raise (response_body_json['errors'][0]['error']) \
66
+ if response_body_json.key?('errors') and \
67
+ response_body_json['errors'].length > 0
68
+
69
+ return response_body_json
70
+ end
71
+ return response.body
72
+ end
73
+
50
74
  def self.bulk_post(path, params, content_type='application/json', filepath=nil)
51
- response = self._post(Zerobounce::BULK_API_ROOT_URL, path, params, \
75
+ response = self._post(Zerobounce.configuration.bulk_api_root_url, path, params, \
52
76
  content_type, filepath)
53
77
  if response.headers[:content_type] == 'application/json'
54
78
  response_body = response.body
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zerobounce
4
+ # Validation status values returned by the API (validate, validate_batch).
5
+ # Use for comparison: response['status'] == Zerobounce::ValidateStatus::VALID
6
+ # Unknown/future API values are not listed; compare against response['status'] as string.
7
+ module ValidateStatus
8
+ NONE = ''
9
+ VALID = 'valid'
10
+ INVALID = 'invalid'
11
+ CATCH_ALL = 'catch-all'
12
+ UNKNOWN = 'unknown'
13
+ SPAMTRAP = 'spamtrap'
14
+ ABUSE = 'abuse'
15
+ DO_NOT_MAIL = 'do_not_mail'
16
+ end
17
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zerobounce
4
+ # Validation sub-status values returned by the API (validate, validate_batch).
5
+ # Use for comparison: response['sub_status'] == Zerobounce::ValidateSubStatus::ACCEPT_ALL
6
+ # Unknown/future API values are not listed; compare against response['sub_status'] as string.
7
+ module ValidateSubStatus
8
+ NONE = ''
9
+ ANTISPAM_SYSTEM = 'antispam_system'
10
+ GREYLISTED = 'greylisted'
11
+ MAIL_SERVER_TEMPORARY_ERROR = 'mail_server_temporary_error'
12
+ FORCIBLE_DISCONNECT = 'forcible_disconnect'
13
+ MAIL_SERVER_DID_NOT_RESPOND = 'mail_server_did_not_respond'
14
+ TIMEOUT_EXCEEDED = 'timeout_exceeded'
15
+ FAILED_SMTP_CONNECTION = 'failed_smtp_connection'
16
+ MAILBOX_QUOTA_EXCEEDED = 'mailbox_quota_exceeded'
17
+ EXCEPTION_OCCURRED = 'exception_occurred'
18
+ POSSIBLE_TRAP = 'possible_trap'
19
+ ROLE_BASED = 'role_based'
20
+ GLOBAL_SUPPRESSION = 'global_suppression'
21
+ MAILBOX_NOT_FOUND = 'mailbox_not_found'
22
+ NO_DNS_ENTRIES = 'no_dns_entries'
23
+ FAILED_SYNTAX_CHECK = 'failed_syntax_check'
24
+ POSSIBLE_TYPO = 'possible_typo'
25
+ UNROUTABLE_IP_ADDRESS = 'unroutable_ip_address'
26
+ LEADING_PERIOD_REMOVED = 'leading_period_removed'
27
+ DOES_NOT_ACCEPT_MAIL = 'does_not_accept_mail'
28
+ ALIAS_ADDRESS = 'alias_address'
29
+ ROLE_BASED_CATCH_ALL = 'role_based_catch_all'
30
+ DISPOSABLE = 'disposable'
31
+ TOXIC = 'toxic'
32
+ ALTERNATE = 'alternate'
33
+ MX_FORWARD = 'mx_forward'
34
+ BLOCKED = 'blocked'
35
+ ALLOWED = 'allowed'
36
+ ACCEPT_ALL = 'accept_all'
37
+ ROLE_BASED_ACCEPT_ALL = 'role_based_accept_all'
38
+ GOLD = 'gold'
39
+ end
40
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Zerobounce
4
4
  # The version of the gem.
5
- VERSION = '1.1.1'
5
+ VERSION = '2.1.8'
6
6
  end