Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
9c50535
feat: v2 validate parity (verify_plus, activity_data, batch options)
franciscbalint Oct 17, 2026
242c913
feat: add score_email GET/POST for /v2/scoring
franciscbalint Oct 17, 2026
91f857b
feat: Priority 3 API parity (bulk finder, filters, S3)
franciscbalint Oct 17, 2026
658bd1b
feat: POST /validate transport parity and listS3files
franciscbalint Oct 17, 2026
df1639e
fix: validate request URLs for CodeQL non-https-url
franciscbalint Oct 18, 2026
1b2ff47
fix: turn bad API bodies into short errors
franciscbalint Oct 19, 2026
3acd504
feat: validate WHOIS and greylist options, Inbox Placement Tester, li…
franciscbalint Oct 23, 2026
986b591
revert: restore the S3 bulk methods
franciscbalint Oct 23, 2026
d98fb36
feat: S3 email finder / domain search and SFTP/FTP bulk endpoints
franciscbalint Oct 23, 2026
24d50fd
feat: add ai_agent_mailbox sub_status (RD-66)
franciscbalint Oct 20, 2026
a5a6e66
fix: match what the live API actually accepts
franciscbalint Oct 24, 2026
618ed2f
feat: add a Warmup API client
franciscbalint Oct 24, 2026
851c5f4
test: add an opt-in live API suite
franciscbalint Oct 24, 2026
0a3eeed
docs: cover the Warmup API, the live suite and the live findings
franciscbalint Oct 24, 2026
c7b1248
fix: get_api_usage could never succeed against the live API
franciscbalint Oct 24, 2026
a15c319
test: build the offline SMTP credentials instead of hard-coding them
franciscbalint Oct 24, 2026
92a72a5
test: derive the offline SMTP credential with no string literal
franciscbalint Oct 24, 2026
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
3 changes: 3 additions & 0 deletions .github/workflows/sdk_ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,8 @@ jobs:
with:
components: rustfmt, clippy

- name: Clippy
run: cargo clippy --all-targets

- name: Run tests
run: cargo test --no-fail-fast
81 changes: 81 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# AGENTS.md

Guidance for AI coding agents working in this repository.

## Project

`zerobounce-rust-api` publishes the `zero-bounce` crate. Callers construct `ZeroBounce::new(api_key)`. Each value owns its key, HTTP client, and base URLs.

## Layout

- `src/lib.rs`: client, URL builder, and the shared GET and form POST helpers.
- `src/api/validation.rs`: single validate and `batch_validate` on the main API host.
- `src/api/bulk.rs`: bulk validation and AI scoring file methods on `bulkapi`.
- `src/api/priority3.rs`: bulk email finder, domain search, list evaluator, filters, and the S3 / SFTP bulk jobs. Finder and domain search paths are on the bulk host without `/v2`. The six file jobs go through `bulk_root_file_job`; two shapes differ from the rest and are easy to get wrong — `validate_sftp_file`'s `file_name` is optional (hence `Option`), and `export_sftp_file` takes `export_path`, not `export_bucket`.
- `src/api/inbox_placement.rs`: Inbox Placement Tester.
- `src/utility/http_error.rs`: turns HTTP failures, HTML pages, and empty or non-JSON bodies into a short `ZBError`.
- `tests/offline_api.rs`: offline suite. A local HTTP server records every request. The client is pointed at `http://127.0.0.1`, which the URL check allows.
- Version is `version` in `Cargo.toml`. `Cargo.lock` records the same package version. There is no runtime version constant.

## Commands

```bash
cargo test --no-fail-fast
cargo clippy --all-targets
cargo audit # check dependencies against the advisory database
```

The live API suite is behind the `live` feature, so `cargo test` and CI
never compile or run it:

```bash
export ZEROBOUNCE_API_KEY=... # validation API
export ZEROBOUNCE_WARMUP_API_KEY=... # Warmup API
cargo test --features live --test live_api -- --nocapture # free calls only
ZEROBOUNCE_LIVE_SPEND=1 cargo test --features live --test live_api -- --nocapture
ZEROBOUNCE_LIVE_CHANGES=1 cargo test --features live --test live_api -- --nocapture
```

Keys come from the environment only. Never write one into a file, a commit, a
log, a fixture or a message.

`cargo test` compiles the crate and the integration tests. That is the typecheck. CI (`.github/workflows/sdk_ci.yml`) runs `cargo clippy --all-targets` and `cargo test --no-fail-fast`. Clippy is not denied: existing tests use `assert_eq!` on booleans, and `cargo fmt --check` does not pass on the examples. Do not reformat files you did not change.

## Conventions

- **Only endpoints documented at https://www.zerobounce.net/docs/.** Do not add a method for an endpoint that the public docs do not describe, even if it exists on the server. Check before concluding something is undocumented: the S3 bulk endpoints live under https://www.zerobounce.net/docs/api-integrations/amazon-s3-bulk-api, not the main API reference.
- **The list evaluator and the Inbox Placement Tester have their own base URLs** on `ZBUrlProvider` (`list_evaluator_url`, `inbox_placement_url`). They are not built from `bulk_url`, and `send_evaluated_file` stays on the bulk base. Offline tests must point both at the fake server or they reach the live API.
- **Where the live API and the docs disagree, follow the API and say so in a comment at the call site.** The ones found so far: `validatebatch` takes `timeout` as a JSON *string*; the Inbox Placement Tester authenticates with `Authorization: Bearer`, not `api_key`; its listing is `GET /tests`, not `/test`; `send-evaluated-file` takes `file_id` as a form field, not a file upload; the S3 and SFTP jobs require `return_url` and `file_name`, `validateSFTPfile` included; the Warmup `check-domain-dmarc` takes an email address, not a domain; and a failed SMTP login in `test-smtp-configuration` comes back as an HTTP 500.
- **The Inbox Placement Tester takes the API key as an `Authorization: Bearer` header and JSON bodies with camelCase fields.** Only fields the caller set are sent. Its error bodies use `detail`.
- **`{"success": false}` on an HTTP 200 is a failure.** The API answers an unknown bulk file id that way. `ensure_json_api_body` turns it into a `ZBError::ApiError` carrying the status.
- **`src/warmup.rs` is a separate client for the Warmup API**, a different product with its own host and its own team-scoped key. It is not part of `ZeroBounce`.

- **One `ZeroBounce` per API key.** `api_key` is a field on the value. Do not share one value across users. The offline concurrency test builds a separate client per key.
- **Column indexes.** `email_address_column` is a `u32` and is sent as given, including `0`. Name, gender, and IP columns are `Option<u32>`: `None` is omitted, and `Some(0)` is sent. Bulk finder `domain_column` of `0` is rejected.
- **Optional parameters are omitted when unset.** `timeout`, `activity_data`, `verify_plus`, and `allow_phase_2` are `Option` and are not sent until the caller sets them. Explicit `false` is sent as `false`.
- **Booleans are the strings `true` and `false`.** `bool::to_string` does that. Do not send `1` or `0`. `allow_phase_2` is validation `sendfile` only. `activity_data` on getfile is validation only. AI scoring `sendfile` does not send name, gender, or IP columns.
- **Empty `ip_address` is omitted** on single validate. Batch items always include `ip_address`, and an empty string is sent.
- **`validate` timeout is 3–60 seconds. `batch_validate` timeout is 10–120 seconds.** Out-of-range values are rejected before a request.
- **Failures are `ZBError`, which implements `std::error::Error`.** The message must be short: no raw HTML, no megabyte body, no serde dump, and no `Error: Error:` prefix. An account-level batch error (`email_address` `all`) is `Err`. A per-address error stays on the `Ok` result. A failed API response is `ZBError::ApiError`, whose status is reachable through `ZBError::status_code()`; the key is never in the message.
- **Tests must not call the live API.** Point `url_provider` at `http://127.0.0.1` or use `mockito`.
- 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.

**Run the live suite by hand before every release.** The offline suite proves the SDK sends what we think it sends; only the live suite proves the API accepts it. Run it with real keys, including `ZEROBOUNCE_LIVE_SPEND=1` and `ZEROBOUNCE_LIVE_CHANGES=1`, and do not release on a failure.

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 `Cargo.toml` `version` and in the `zero-bounce` package entry in `Cargo.lock`. The publish workflow fails if the tag and `Cargo.toml` disagree.
3. `cargo test --no-fail-fast`.
4. Tag `vx.y.z` on that commit and run the Publish workflow with that tag. It publishes to crates.io.
107 changes: 107 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Changelog

All notable changes to this project will be documented in this file.

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).

## [Unreleased]

### Added

- `ZBValidateOptions` for single validation: `timeout`, `activity_data`, `verify_plus`, and `use_post` (form-encoded `POST /v2/validate`). The same options apply to `batch_validate_with_options`.
- `score_email` (`GET /v2/scoring`) and `score_email_post` (form-encoded `POST /v2/scoring`).
- Bulk email finder, bulk domain search, list evaluator, filters, and S3 bulk (`validate_s3_file`, `score_s3_file`, `export_s3_file`, `list_s3_files`, `email_finder_s3_file`, `domain_search_s3_file`).
- SFTP/FTP bulk: `validate_sftp_file`, `score_sftp_file`, `email_finder_sftp_file`, `domain_search_sftp_file`, and `export_sftp_file`. The server, path and protocol come from your ZeroBounce settings. `validate_sftp_file` takes `file_name` as `Option` because the docs list it as optional there alone; `export_sftp_file` takes `export_path` rather than the `export_bucket` `export_s3_file` uses.
- `ZBValidateOptions` accepts `domain_info`, `greylist_processing`, and `callback_url`. `callback_url` needs `greylist_processing: Some(true)` and is rejected without it, and when blank, before a request is sent.
- `ZBValidation` carries `domain_website_exists`, `domain_registrant_company_name`, and `message`. `message` is read from the API's `Message` key, returned while greylist processing is still running.
- Inbox Placement Tester on `https://bulkapi.zerobounce.net/api/ipt`: `get_inbox_placement_providers`, `get_inbox_placement_test_types`, `create_inbox_placement_test`, `get_inbox_placement_test`, `list_inbox_placement_tests`, `delete_inbox_placement_test`, `get_inbox_placement_webhook`, `set_inbox_placement_webhook`.
- `ZBUrlProvider` gains `list_evaluator_url` and `inbox_placement_url`, both overridable.
- `ZeroBounceWarmup`, a client for the Warmup API. It is a separate product with its own host (`https://warmup-api.zerobounce.net`), its own team-scoped key sent in an `API-Key` header, and all 26 documented endpoints, including the seed-list and statistics CSV downloads.
- `ZBError::ApiError { status, message }` and `ZBError::status_code()`, so callers can tell a Cloudflare 403 from an API 400/404 and a Warmup 429.
- An opt-in live API suite behind the `live` cargo feature. `cargo test` and CI never compile it; running it takes `cargo test --features live --test live_api` with keys in the environment, and it only spends credits or changes the account behind `ZEROBOUNCE_LIVE_SPEND=1` and `ZEROBOUNCE_LIVE_CHANGES=1`.

### Changed

- List evaluator requests use `ZBUrlProvider::list_evaluator_url` instead of building the path from the bulk base, and the file id is URL-encoded as one path segment. `send_evaluated_file` stays on the bulk base.

- API requests time out after 120 seconds and do not follow redirects.
- `batch_validate` sends `timeout` as a JSON string. Sent as a number, the live API rejects the whole batch with an account-level error.
- The Inbox Placement Tester authenticates with an `Authorization: Bearer` header instead of an `api_key` parameter, so the key no longer reaches a query string that gets logged.
- `send_evaluated_file(file_id)` posts the list evaluator's `file_id` as a form field. The endpoint rejects a file upload with "file_id: This field is required", so it no longer takes a file.
- The S3 and SFTP/FTP jobs require `file_name` and `return_url`, `validate_sftp_file` included, so both are now `&str` rather than `Option`. The API answers "This field is required" otherwise.
- A base URL must be `https://`, except `http://127.0.0.1`, `http://localhost`, and `http://[::1]` for tests.

### Fixed

- `get_api_usage` no longer fails outright. The API returns only the counters that apply to the account and the date range asked for, and an ordinary response carries neither `sub_status_gold` nor `sub_status_role_based_accept_all`; deserializing them as required fields failed the whole call, reported as "ZeroBounce returned a response that is not valid JSON". Every counter on `ApiUsage` now defaults to 0 when absent, so a counter the API adds or drops later is no longer fatal. `sub_status_leading_period_removed` and `sub_status_alias_address`, which the API does return, are now read.

- `{"success": false}` on an HTTP 200 is an error instead of a result. The API answers an unknown bulk file id that way.
- An error body whose JSON carries a `detail` string (the Inbox Placement Tester, Django REST framework) returns that text.

- An HTML "Access denied" page is returned as a short `ZBError`. An HTML page with status 200 is an error as well, including bulk `getfile`, instead of file bytes or a JSON parse.
- Empty and non-JSON bodies are a short error. The message is no longer a serde parse dump, and a repeated `Error:` / `[Error]:` prefix is removed.
- `batch_validate` returns an error for an account-level failure (HTTP 200, error for address `all`, such as an invalid API key or no credits). An error for one address stays on the successful result.
- AI scoring `sendfile` does not send name, gender, or IP columns, or `allow_phase_2`.
- An empty API key is rejected before a request is sent.
- `validate` timeout must be 3–60 seconds. `batch_validate` timeout must be 10–120 seconds.

### Security

- Redirects are disabled, so a 3xx response cannot send the API key to another host.
- Error values no longer include the raw HTML access-denied page.
- A Warmup API key with surrounding whitespace or control characters is rejected, because the transport would otherwise quote it unredacted in its own error.
- The Warmup SMTP test masks the username and password out of every error it raises.

## [2.1.2] - 2026-06-10

### Added

- `catchall_domain` on the validation response.

## [2.1.0] - 2026-04-05

### Added

- `ZBGetFileOptions` with `download_type` and `activity_data` (validation getfile only).

### Fixed

- Bulk getfile treats a JSON error body, including HTTP 200, as a failure instead of file content.

## [2.0.11] - 2026-03-03

### Changed

- License the project under MIT.

## [2.0.0] - 2026-03-03

### Changed

- Release the 2.0 line of the client.

## [1.3.0] - 2025-11-10

### Added

- `accept_all` status.
- Base URL selection (`ApiBaseUrl::Default`, `USA`, and `EU`).

### Deprecated

- `domain_search`. Use `domain_search_v2`.

## [1.1.0] - 2023-09-04

### Added

- Initial client for validation, credits, activity, and bulk file calls.

[Unreleased]: https://github.com/zerobounce/zerobounce-rust-api/compare/v2.1.3...HEAD
[2.1.2]: https://github.com/zerobounce/zerobounce-rust-api/compare/v2.1.0...v2.1.2
[2.1.0]: https://github.com/zerobounce/zerobounce-rust-api/compare/v2.0.11...v2.1.0
[2.0.11]: https://github.com/zerobounce/zerobounce-rust-api/compare/v2.0.0...v2.0.11
[2.0.0]: https://github.com/zerobounce/zerobounce-rust-api/compare/v1.3.0...v2.0.0
[1.3.0]: https://github.com/zerobounce/zerobounce-rust-api/compare/v1.1.0...v1.3.0
[1.1.0]: https://github.com/zerobounce/zerobounce-rust-api/releases/tag/v1.1.0
11 changes: 10 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,19 @@ documentation = "https://docs.rs/zero-bounce"
[dependencies]
bytes = "1.4.0"
chrono = "0.4.24"
reqwest = { version = "0.13.2", features = ["blocking", "multipart", "query"] }
reqwest = { version = "0.13.2", features = ["blocking", "multipart", "query", "form"] }
serde = { version ="1.0.159", features = ["derive"] }
serde_json = "1.0.95"

[features]
# The live API suite. Off by default, so `cargo test` and CI never touch the
# network: run it with `cargo test --features live --test live_api`.
live = []

[[test]]
name = "live_api"
required-features = ["live"]

[dev-dependencies]
mockito = "1.0.2"
dotenv = "0.15"
Loading
Loading