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.
- checksums.yaml +4 -4
- data/.dockerignore +17 -0
- data/.env.example +3 -0
- data/.github/auto_assign.yml +5 -0
- data/.github/dependabot.yml +35 -0
- data/.github/workflows/auto_assign_ci.yaml +21 -0
- data/.github/workflows/codeql.yml +105 -0
- data/.github/workflows/sdk_ci.yml +29 -0
- data/.gitignore +6 -0
- data/CODE_OF_CONDUCT.md +1 -6
- data/CONTRIBUTING.md +112 -1
- data/Dockerfile +17 -0
- data/Gemfile.lock +6 -2
- data/LICENSE +1 -1
- data/LICENSE.txt +6 -6
- data/README.md +52 -20
- data/SECURITY.md +21 -0
- data/documentation.md +29 -11
- data/lib/zerobounce/base_request.rb +31 -3
- data/lib/zerobounce/download_type.rb +10 -0
- data/lib/zerobounce/get_file_helper.rb +90 -0
- data/lib/zerobounce/get_file_options.rb +14 -0
- data/lib/zerobounce/mock_request.rb +6 -0
- data/lib/zerobounce/request.rb +7 -0
- data/lib/zerobounce/validate_status.rb +17 -0
- data/lib/zerobounce/validate_sub_status.rb +40 -0
- data/lib/zerobounce/version.rb +1 -1
- data/lib/zerobounce.rb +90 -16
- data/zerobounce.gemspec +2 -0
- metadata +45 -8
- data/.env.sample +0 -5
- data/documentation_es.md +0 -524
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
|
|
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.
|
|
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}
|
|
500
|
-
ZEROBOUNCE_API_KEY
|
|
501
|
-
|
|
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=
|
|
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.
|
|
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.
|
|
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,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)
|
data/lib/zerobounce/request.rb
CHANGED
|
@@ -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
|
data/lib/zerobounce/version.rb
CHANGED
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]
|
|
281
|
-
#
|
|
282
|
-
#
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
#
|
|
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]
|
|
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
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
params = {file_id: file_id}
|
|
342
|
-
|
|
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'
|