zerobounce-sdk 1.2.0 → 2.1.0

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
 
@@ -496,17 +514,17 @@ Finished in 6.81 seconds (files took 0.40587 seconds to load)
496
514
 
497
515
  ##### Test parameters
498
516
  The tests use the following environment parameters:
499
- 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)
500
- 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)
501
- 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.
502
521
 
503
- To set them
522
+ To set them:
504
523
  ```bash
505
- export ZEROBOUNCE_API_KEY=99e7ef20ceea4480a173b07b1be75371
506
- export INCORRECT_API_KEY=thiskeyisinvalidorotherwiseincorrect
524
+ export ZEROBOUNCE_API_KEY=your_api_key_here
507
525
  export TEST=unit
508
526
  ```
509
527
 
510
- A .env.sample file is provided.
528
+ A .env.example file is provided.
511
529
 
512
- 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.
@@ -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
 
@@ -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
 
@@ -41,6 +42,11 @@ 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
+
44
50
  def self.bulk_post(path, params, content_type='application/json', filepath=nil)
45
51
  response = self._post(Zerobounce.configuration.bulk_api_root_url, path, params, \
46
52
  content_type, filepath)
@@ -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
 
@@ -47,6 +48,12 @@ 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
+
50
57
  def self.bulk_post(path, params, content_type='application/json', filepath=nil)
51
58
  response = self._post(Zerobounce.configuration.bulk_api_root_url, path, params, \
52
59
  content_type, filepath)
@@ -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.2.0'
5
+ VERSION = '2.1.0'
6
6
  end
data/lib/zerobounce.rb CHANGED
@@ -2,9 +2,15 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'date'
5
+ require 'tempfile'
5
6
 
6
7
  require 'zerobounce/error'
7
8
  require 'zerobounce/version'
9
+ require 'zerobounce/download_type'
10
+ require 'zerobounce/get_file_options'
11
+ require 'zerobounce/get_file_helper'
12
+ require 'zerobounce/validate_status'
13
+ require 'zerobounce/validate_sub_status'
8
14
  require 'zerobounce/request'
9
15
  require 'zerobounce/mock_request'
10
16
  require 'zerobounce/configuration'
@@ -26,6 +32,14 @@ module Zerobounce
26
32
  end
27
33
  alias config configuration
28
34
 
35
+ # Whether a getfile response body looks like a JSON error payload (including HTTP 200).
36
+ #
37
+ # @param [String] body
38
+ # @return [Boolean]
39
+ def get_file_json_indicates_error?(body)
40
+ GetFileHelper.json_indicates_error?(body)
41
+ end
42
+
29
43
  # Configure Zerobounce inside a block.
30
44
  #
31
45
  # @example
@@ -133,10 +147,13 @@ module Zerobounce
133
147
  # "sub_status_mailbox_quota_exceeded": 0,
134
148
  # "sub_status_forcible_disconnect": 0,
135
149
  # "sub_status_failed_smtp_connection": 0,
150
+ # "sub_status_accept_all": 0,
136
151
  # "sub_status_mx_forward": 0,
137
152
  # "sub_status_alternate": 0,
138
- # "sub_status_blocked": 0,
139
153
  # "sub_status_allowed": 0,
154
+ # "sub_status_blocked": 0,
155
+ # "sub_status_gold": 0,
156
+ # "sub_status_role_based_accept_all": 0,
140
157
  # "start_date": "1/1/2018",
141
158
  # "end_date": "12/12/2023"
142
159
  # }
@@ -241,7 +258,8 @@ module Zerobounce
241
258
  last_name_column: 3,
242
259
  gender_column: 4,
243
260
  has_header_row: true,
244
- return_url: nil
261
+ return_url: nil,
262
+ allow_phase_2: nil
245
263
  )
246
264
  params = {
247
265
  email_address_column: email_address_column,
@@ -251,9 +269,34 @@ module Zerobounce
251
269
  params[:last_name_column] = last_name_column if last_name_column
252
270
  params[:gender_column] = gender_column if gender_column
253
271
  params[:return_url] = return_url if return_url
272
+ unless allow_phase_2.nil?
273
+ params[:allow_phase_2] = allow_phase_2 ? 'true' : 'false'
274
+ end
254
275
  @@request.bulk_post('sendfile', params, 'multipart/form-data', filepath)
255
276
  end
256
277
 
278
+ # Validate CSV from a stream (IO or String).
279
+ #
280
+ # @param [IO, String] io Stream or string content to upload
281
+ # @param [String] file_name Filename for the upload (e.g. "emails.csv")
282
+ # @option [Int] :email_address_column (same as validate_file_send)
283
+ # @option [Int] :first_name_column
284
+ # @option [Int] :last_name_column
285
+ # @option [Int] :gender_column
286
+ # @option [Int] :has_header_row
287
+ # @option [Int] :return_url
288
+ # @option [Boolean] :allow_phase_2 When set, sends allow_phase_2 (validation bulk only).
289
+ # @return [Hash] same as validate_file_send
290
+ def validate_file_send_stream(io, file_name, **opts)
291
+ content = io.respond_to?(:read) ? io.read : io.to_s
292
+ Tempfile.create(['zb', File.extname(file_name)]) do |tmp|
293
+ tmp.binmode
294
+ tmp.write(content)
295
+ tmp.rewind
296
+ validate_file_send(tmp.path, **opts)
297
+ end
298
+ end
299
+
257
300
  # Get validate file status
258
301
  #
259
302
  # @param [String] :file_id Id of the file.
@@ -277,13 +320,20 @@ module Zerobounce
277
320
 
278
321
  # Get validate results file
279
322
  #
280
- # @param [String] :file_id Id of the file.
281
- #
282
- # @return [String/File?]
283
- def validate_file_get(file_id)
284
- # todo:
285
- params = {file_id: file_id}
286
- @@request.bulk_get('getfile', params)
323
+ # @param [String] file_id Id of the file.
324
+ # @param [Zerobounce::GetFileOptions, nil] options Optional +download_type+ and +activity_data+ (validation bulk only).
325
+ #
326
+ # @return [String] file body on success
327
+ # @raise [RuntimeError] on API/JSON error responses
328
+ def validate_file_get(file_id, options = nil)
329
+ params = { file_id: file_id }
330
+ if options
331
+ params[:download_type] = options.download_type if options.download_type
332
+ unless options.activity_data.nil?
333
+ params[:activity_data] = options.activity_data ? 'true' : 'false'
334
+ end
335
+ end
336
+ @@request.bulk_getfile('getfile', params)
287
337
  end
288
338
 
289
339
  # Delete validate file
@@ -331,15 +381,37 @@ module Zerobounce
331
381
  @@request.bulk_post('scoring/sendfile', params, 'multipart/form-data', filepath)
332
382
  end
333
383
 
334
- # Get validate results file
384
+ # Score CSV from a stream (IO or String).
385
+ #
386
+ # @param [IO, String] io Stream or string content to upload
387
+ # @param [String] file_name Filename for the upload (e.g. "emails.csv")
388
+ # @option [Int] :email_address_column (same as scoring_file_send)
389
+ # @option [Int] :has_header_row
390
+ # @option [Int] :return_url
391
+ # @return [Hash] same as scoring_file_send
392
+ def scoring_file_send_stream(io, file_name, **opts)
393
+ content = io.respond_to?(:read) ? io.read : io.to_s
394
+ Tempfile.create(['zb', File.extname(file_name)]) do |tmp|
395
+ tmp.binmode
396
+ tmp.write(content)
397
+ tmp.rewind
398
+ scoring_file_send(tmp.path, **opts)
399
+ end
400
+ end
401
+
402
+ # Get scoring bulk results file
335
403
  #
336
- # @param [String] :file_id Id of the file.
404
+ # @param [String] file_id Id of the file.
405
+ # @param [Zerobounce::GetFileOptions, nil] options Optional +download_type+; +activity_data+ is not sent.
337
406
  #
338
- # @return [String/File?]
339
- def scoring_file_get(file_id)
340
- # todo:
341
- params = {file_id: file_id}
342
- @@request.bulk_get('scoring/getfile', params)
407
+ # @return [String] file body on success
408
+ # @raise [RuntimeError] on API/JSON error responses
409
+ def scoring_file_get(file_id, options = nil)
410
+ params = { file_id: file_id }
411
+ if options && options.download_type
412
+ params[:download_type] = options.download_type
413
+ end
414
+ @@request.bulk_getfile('scoring/getfile', params)
343
415
  end
344
416
 
345
417
  # Get validate file status
@@ -425,6 +497,8 @@ module Zerobounce
425
497
  # ]
426
498
  # }
427
499
  def guessformat(domain, first_name: '', middle_name: '', last_name: '')
500
+ warn "[DEPRECATION] `guessformat` is deprecated and will be removed in a future version.\n" \
501
+ "Please use `find_email` or `find_domain` instead."
428
502
  params = {domain: domain}
429
503
  params[:first_name] = first_name unless first_name.nil? || first_name.empty?
430
504
  params[:middle_name] = middle_name unless middle_name.nil? || middle_name.empty?
data/zerobounce.gemspec CHANGED
@@ -29,6 +29,8 @@ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
29
29
  spec.add_dependency 'rest-client', '~>2.1'
30
30
  spec.add_dependency 'dotenv'
31
31
 
32
+ spec.add_development_dependency 'base64' # stdlib gem on Ruby 3.4+
33
+ spec.add_development_dependency 'bigdecimal' # required by crack (webmock) on Ruby 3.4+
32
34
  spec.add_development_dependency 'bundler', '~> 2.4.6'
33
35
  spec.add_development_dependency 'pry', '~> 0.14.1'
34
36
  spec.add_development_dependency 'rake', '~> 13.0'