Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ jobs:
uses: ruby/setup-ruby@651ff0d62c5c79bed6d53ec518387aae20e72c1a # v1
with:
ruby-version: "3.2"
bundler-cache: true
bundler-cache: false

- name: Run specs
run: bundle exec rspec
Expand All @@ -107,7 +107,7 @@ jobs:
uses: ruby/setup-ruby@651ff0d62c5c79bed6d53ec518387aae20e72c1a # v1
with:
ruby-version: "3.2"
bundler-cache: true
bundler-cache: false

- name: Configure RubyGems credentials
uses: rubygems/configure-rubygems-credentials@dc5a8d8553e6ee01fc26761a49e99e733d17954a # v2.1.0
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/sdk_ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,8 @@ jobs:
ruby-version: "3.2"
bundler-cache: true

- name: RuboCop
run: bundle exec rubocop

- name: Run specs
run: bundle exec rspec
55 changes: 55 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# AGENTS.md

Guidance for AI coding agents working in this repository.

## Project

`zero-bounce-ruby` publishes the `zerobounce-sdk` gem. Callers set `Zerobounce.config.apikey`, or construct `Zerobounce::Client.new(api_key)`. Module methods share one configuration object. A `Client` stores the key on the current thread for the duration of each call.

## Layout

- `lib/zerobounce.rb`: public methods for validate, credits, batch, files, scoring, and finder.
- `lib/zerobounce/priority3.rb`: bulk email finder, domain search, list evaluator, filters, and S3.
- `lib/zerobounce/base_request.rb` and `request.rb`: RestClient transport. `response_error.rb` turns HTML, empty bodies, and non-JSON into a short `RuntimeError`.
- `lib/zerobounce/version.rb`: `Zerobounce::VERSION`. `Gemfile.lock` pins `zerobounce-sdk (X.Y.Z)` to that same version.
- `spec/offline_api_spec.rb`: offline suite. WebMock answers without the network.
- The user agent is `ZerobounceRubyGem/#{Zerobounce::VERSION}`.

## Commands

```bash
bundle exec rspec
bundle exec rubocop
```

`rspec` is the test command. `rubocop` is the lint step; there is no separate typechecker. CI (`.github/workflows/sdk_ci.yml`) runs both on Ruby 3.2. This checkout may be a newer Ruby where RuboCop cannot load `benchmark`; use 3.2 to lint.

## Conventions

- **One key per caller.** `Zerobounce.config` is process-wide. Do not reuse it across users. Use `Zerobounce::Client` (or `with_api_key`) so two threads cannot observe each other's keys. The offline concurrency spec does this.
- **Column indexes.** `0` is a real column. Do not drop it with `if column`. Omit an optional column only when the argument is `nil`. `domain_column` of `0` is rejected. `Import`-style defaults are not used here: the send methods default name columns to 1–4, and an explicit `0` is sent.
- **Optional parameters are omitted when unset.** `timeout`, `activity_data`, `verify_plus`, and `allow_phase_2` are omitted when `nil`. Explicit `false` is sent as `false`.
- **Booleans on multipart are the strings `true` and `false`.** `has_header_row: false` on bulk email finder and domain search is sent. `allow_phase_2` is validation `sendfile` only. `activity_data` on get-file is validation only.
- **`validate` timeout is 3–60 seconds. `validate_batch` timeout is 10–120 seconds.** Out-of-range values raise `ArgumentError` before a request.
- **Failures are `RuntimeError` (or `ArgumentError` for bad arguments).** The message must be short: no raw HTML, no megabyte body, no `JSON::ParserError` dump, and no `Error: Error:` prefix. An account-level `validate_batch` error (`email_address` `all`) raises. A per-address error stays on the returned hash.
- **`validate_batch` returns the hash**, with `email_batch` and `errors`, not only the array.
- **Tests replace HTTP with WebMock.** Do not call the live API from `rspec`.
- Match the surrounding style. Do not reformat unrelated code.

## Changelog

`CHANGELOG.md` follows [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/), and the project adheres to [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html).

- Add every user-visible change to `## [Unreleased]` in the same commit as the change.
- Group entries under these headings, in this order, omitting empty ones: `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`.
- Write for SDK users: name the method, option, or behaviour, and say what changed. Leave out CI-only changes, internal refactors, and fixes to code that was never released.
- At 2.y.z, a breaking change bumps the major version, a new method or option bumps the minor version, and fixes alone bump the patch version.

## Releasing

Publishing and pushing tags need a maintainer's explicit approval. Do not bump the version, tag, or publish unless they asked.

1. Move `[Unreleased]` entries under `## [x.y.z] - YYYY-MM-DD`, leave an empty `[Unreleased]` section, and update the compare links at the bottom of `CHANGELOG.md`.
2. Set the same version in `lib/zerobounce/version.rb` and in the `zerobounce-sdk (X.Y.Z)` pin in `Gemfile.lock`. A frozen CI install fails if those two differ. The gemspec reads `Zerobounce::VERSION`; do not add a third copy.
3. `bundle exec rubocop` and `bundle exec rspec`.
4. Tag `vx.y.z` on that commit and run the Publish workflow with that tag. It publishes to RubyGems.
141 changes: 122 additions & 19 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,136 @@
# 2.1.7
# Changelog

* Update Gemfile.lock for v2.1.6 release.
All notable changes to this project will be documented in this file.

# 2.1.6
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

* Route `validatebatch` requests to `api.zerobounce.net` instead of `bulkapi.zerobounce.net`.
## [Unreleased]

# 2.1.1
### Added

* Raise development and transitive dependency floors to address security advisories for REXML, Addressable, and YARD (updated `Gemfile.lock`; see `Gemfile` and `zerobounce.gemspec`).
- `validate` options `timeout`, `activity_data`, `verify_plus`, and `use_post` (form-encoded `POST /validate`).
- `validate_batch` options `timeout`, `activity_data`, and `verify_plus`.
- `score_email` for `GET` and `POST /scoring`.
- Bulk email finder, bulk domain search, list evaluator, filters, and S3 (`validate_s3_file`, `score_s3_file`, `export_s3_file`, `list_s3_files`).
- `Zerobounce::Client` so each API key stays on its own thread. Module methods still share `Zerobounce.config`.

# 0.3.0
* Update ruby and bundler version
### Changed

# 0.2.0
- `validate_batch` returns the full response hash (`email_batch` and `errors`). A per-address error stays on that hash.
- API requests time out after 120 seconds and do not follow redirects.
- Multipart booleans are sent as `true` or `false`.

* Update faraday and faraday_middleware dependency requirements.
* Update rubocop dev dependencies and fixed failures.
* Updated minimum ruby version to 2.5.
### Fixed

# 0.1.2
- An HTML "Access denied" page is a short `RuntimeError`. An HTML page with status 200 is an error as well, including `validate_file_get`, instead of file text or a JSON parse.
- Empty and non-JSON bodies are a short error. A repeated `Error:` / `[Error]:` prefix is removed.
- `validate_batch` raises for an account-level failure (HTTP 200, error for address `all`, such as an invalid API key or no credits).
- Bulk email finder and domain search send `has_header_row` when it is false. A column index of `0` is sent when the caller passes it.
- An empty API key is rejected before a request is sent.
- `validate` timeout must be 3–60 seconds. `validate_batch` timeout must be 10–120 seconds.

* Removed the double quote from the body check since it appears a space is added after the colon.
* Added changelog file.
### Security

# 0.1.1
- Redirects are disabled, so a 3xx response cannot send the API key to another host.
- Error messages no longer include the raw HTML access-denied page.
- RestClient error text is redacted of the API key.

* Removed safe accessor usage to work with older versions of Ruby.
## [2.1.8] - 2026-08-14

# 0.1.0
### Fixed

* Added support for V2 of Zerobounce API.
- `Gemfile.lock` matches the gemspec version, so a frozen install can resolve the gem.

## [2.1.6] - 2026-06-18

### Fixed

- `validate_batch` posts to `api.zerobounce.net` instead of `bulkapi.zerobounce.net`.

## [2.1.5] - 2026-06-10

### Added

- `catchall_domain` on validation results.

## [2.1.1] - 2026-04-08

### Security

- Development and transitive dependency floors were raised for REXML, Addressable, and YARD.

## [2.1.0] - 2026-04-08

### Added

- Optional bulk get-file parameters, including download type and activity data.

## [2.0.12] - 2026-03-03

### Changed

- License the project under MIT.

## [2.0.1] - 2026-03-03

### Security

- Uploaded file paths must stay under the current directory.

## [2.0.0] - 2026-03-03

### Changed

- Release the 2.0 line of the gem.

## [1.3.0] - 2025-10-22

### Added

- `find_email` and `find_domain`.

### Deprecated

- `guessformat`. Use `find_email` or `find_domain`.

## [0.3.0] - 2023-09-18

### Changed

- Ruby and Bundler version requirements.

## [0.2.0] - 2023-09-12

### Changed

- Faraday requirements, and the minimum Ruby version is 2.5.

## [0.1.2] - 2023-04-18

### Fixed

- Error-body detection no longer depends on a quote immediately after the colon.

## [0.1.1] - 2023-04-12

### Fixed

- Removed safe-navigation calls so the client runs on older Ruby.

## [0.1.0] - 2023-04-07

### Added

- Initial client for ZeroBounce API v2.

[Unreleased]: https://github.com/zerobounce/zero-bounce-ruby/compare/v2.1.8...HEAD
[2.1.8]: https://github.com/zerobounce/zero-bounce-ruby/compare/v2.1.6...v2.1.8
[2.1.6]: https://github.com/zerobounce/zero-bounce-ruby/compare/v2.1.5...v2.1.6
[2.1.5]: https://github.com/zerobounce/zero-bounce-ruby/compare/v2.1.1...v2.1.5
[2.1.1]: https://github.com/zerobounce/zero-bounce-ruby/compare/v2.1.0...v2.1.1
[2.1.0]: https://github.com/zerobounce/zero-bounce-ruby/compare/v2.0.12...v2.1.0
[2.0.12]: https://github.com/zerobounce/zero-bounce-ruby/compare/v2.0.1...v2.0.12
[2.0.1]: https://github.com/zerobounce/zero-bounce-ruby/compare/v2.0.0...v2.0.1
[2.0.0]: https://github.com/zerobounce/zero-bounce-ruby/compare/v1.3.0...v2.0.0
[1.3.0]: https://github.com/zerobounce/zero-bounce-ruby/releases/tag/v1.3.0
70 changes: 62 additions & 8 deletions lib/zerobounce.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
require 'tempfile'

require 'zerobounce/error'
require 'zerobounce/response_error'
require 'zerobounce/client'
require 'zerobounce/version'
require 'zerobounce/download_type'
require 'zerobounce/get_file_options'
Expand All @@ -15,11 +17,13 @@
require 'zerobounce/mock_request'
require 'zerobounce/configuration'
require 'zerobounce/api_urls'
require 'zerobounce/priority3'

# Validate an email address with Zerobounce.net
module Zerobounce

class << self
include Priority3
attr_writer :configuration

@@request = ENV['TEST']=='unit' ? MockRequest : Request
Expand Down Expand Up @@ -52,6 +56,15 @@ def configure
yield configuration
end

# Run a block with an API key that is visible only on this thread.
def with_api_key(api_key)
previous = Thread.current[:zerobounce_api_key]
Thread.current[:zerobounce_api_key] = api_key
yield
ensure
Thread.current[:zerobounce_api_key] = previous
end

# Validates the email address and gets geoip information for an IP if provided.
#
# @param [String] :email The email address to validate.
Expand Down Expand Up @@ -80,9 +93,26 @@ def configure
# "zipcode": null,
# "processed_at": "2023-04-18 12:09:39.074"
# }
def validate(email, ip_address=nil)
def validate(email, ip_address=nil, timeout: nil, activity_data: nil, verify_plus: nil, use_post: false)
unless timeout.nil?
timeout_i = Integer(timeout)
if timeout_i < 3 || timeout_i > 60
raise ArgumentError, 'Invalid parameter: timeout (must be between 3 and 60 seconds)'
end
end
params = {email: email, ip_address: ip_address}
@@request.get('validate', params)
params[:timeout] = timeout unless timeout.nil?
unless activity_data.nil?
params[:activity_data] = activity_data ? 'true' : 'false'
end
unless verify_plus.nil?
params[:verify_plus] = verify_plus ? 'true' : 'false'
end
if use_post
@@request.post('validate', params, 'application/x-www-form-urlencoded')
else
@@request.get('validate', params)
end
end

# Get the number of remaining credits on the account.
Expand Down Expand Up @@ -183,6 +213,21 @@ def activity(email)
@@request.get('activity', params)
end

# Real-time AI scoring for a single email (GET /scoring by default).
#
# @param [String] :email Email to score
# @param [Boolean] :use_post When true, uses POST /scoring with form body
#
# @return [Hash] e.g. {"email"=>"mark@gmail.com", "score"=>10}
def score_email(email, use_post: false)
params = { email: email }
if use_post
@@request.post('scoring', params, 'application/x-www-form-urlencoded')
else
@@request.get('scoring', params)
end
end

# Validate email batch
#
# @param [Array] :emails List of email addresses to validate.
Expand Down Expand Up @@ -217,7 +262,13 @@ def activity(email)
# },
# ...
# ]
def validate_batch(emails, ip_addresses=[])
def validate_batch(emails, ip_addresses=[], timeout: nil, activity_data: nil, verify_plus: nil)
unless timeout.nil?
timeout_i = Integer(timeout)
if timeout_i < 10 || timeout_i > 120
raise ArgumentError, 'Invalid parameter: timeout (must be between 10 and 120 seconds)'
end
end

raise ArgumentError.new if emails.class != Array
emails.each do |email|
Expand All @@ -232,8 +283,11 @@ def validate_batch(emails, ip_addresses=[])
})
end
params = {email_batch: email_batch}
params[:timeout] = timeout unless timeout.nil?
params[:activity_data] = activity_data unless activity_data.nil?
params[:verify_plus] = verify_plus unless verify_plus.nil?
results = @@request.post('validatebatch', params)
return results['email_batch']
results
end

# Validate CSV file
Expand Down Expand Up @@ -267,10 +321,10 @@ def validate_file_send(
email_address_column: email_address_column,
has_header_row: has_header_row,
}
params[:first_name_column] = first_name_column if first_name_column
params[:last_name_column] = last_name_column if last_name_column
params[:gender_column] = gender_column if gender_column
params[:return_url] = return_url if return_url
params[:first_name_column] = first_name_column unless first_name_column.nil?
params[:last_name_column] = last_name_column unless last_name_column.nil?
params[:gender_column] = gender_column unless gender_column.nil?
params[:return_url] = return_url unless return_url.nil? || return_url.to_s.empty?
unless allow_phase_2.nil?
params[:allow_phase_2] = allow_phase_2 ? 'true' : 'false'
end
Expand Down
Loading
Loading