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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ __recovery/

# Executables, debugging and run-time builds
tests/unit_tests
/unit_tests_console
debugging
debugging.*

Expand Down
61 changes: 61 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# AGENTS.md

Guidance for AI coding agents working in this repository.

## Project

`zero-bounce-pascal` is the ZeroBounce Pascal/Delphi SDK. Callers use `ZBInitialize(apiKey)` and the `Zb*` functions in `src/`. HTTP uses Free Pascal `fphttpclient` or Delphi `THTTPClient` in `src/zbutility.pas`. There is no package registry: releases are GitHub tags.

## Layout

- `src/zbvalidation.pas`: single and batch validation.
- `src/zbbulk.pas`: bulk file send, status, get, and delete, including AI scoring.
- `src/zbgeneric.pas`: credits, usage, activity, scoring, email finder, and domain search.
- `src/zbpriority3.pas`: bulk email finder, domain search, list evaluator, filters, and S3.
- `src/zbstructures.pas`: response records and JSON parsing.
- `src/zbutility.pas`: HTTP, URL encoding, and `ZbException`.
- `packages/zerobounce.lpk`: package version as attributes `Major`, `Minor`, and `Build`. There is no single version string.
- `tests/unit_tests_console.lpr`: FPCUnit runner. `tests/testoffline.pas` is the offline suite. It speaks HTTP to a stub bound to `127.0.0.1`.
- `examples/delphi-api-v2/`: a separate Indy example. It is not the SDK HTTP stack.

## Commands

Free Pascal was not on the machine used to add the offline suite. CI installs it. The default test command is:

```bash
fpc -Fu./src -Fu./tests -Mobjfpc -O2 -gl -viwn \
tests/unit_tests_console.lpr -FE.
./unit_tests_console --all
```

That compile is the typecheck. There is no separate lint step. CI (`.github/workflows/sdk_ci.yml`) runs the same build and `./unit_tests_console --all`. Do not commit `unit_tests_console` or `debugging`.

## Conventions

- **The API key is per thread.** `ZbApiKey` and `ZbApiBaseURL` are `threadvar`. Call `ZBInitialize` on every thread before requests. Do not share one thread across two keys.
- **Column indexes are 1-based** for optional name, gender, and IP columns. `0` means the field is left out. `ZbValidateS3File` sends `EmailAddressColumn` when `EmailAddressColumnSpecified` is true, including `0`.
- **Tri-state ints are `-1` omit, `0` false, `1` true** on `TZbValidateOptions`. Booleans that have a `Specified` flag are omitted until that flag is true. Explicit false is sent as `false`.
- **Booleans are the strings `true` and `false`.**
- **`AllowPhase2` is validation bulk send only.** `ActivityData` on get-file is validation only.
- **Batch validation uses `ZbApiBaseURL`**, which defaults to `https://api.zerobounce.net/v2`, not the bulk host. An empty IP address is JSON `null`.
- **Failures raise `ZbException`.** `Message` is short: no raw HTML, no megabyte body, and no `Error: Error:` prefix. `Payload` still holds the response body. An account-level batch error (`EmailAddress` `all`) raises. A per-address error stays on `TZBBatchValidation.Errors`.
- **Offline tests retarget `ZbApiBaseURL`, `BULK_URI`, and `ZbBulkRootURL` at `http://127.0.0.1`.** Do not call the live API from the default test run.
- 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. Leave out CI-only changes and fixes to code that was never released.
- At 2.y.z, a breaking change bumps the major version, a new function 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.
2. Set the same version on `<Version Major="x" Minor="y" Build="z"/>` in `packages/zerobounce.lpk`. Those three attributes are the version. There is no runtime version constant.
3. Build and run `./unit_tests_console --all` with the command above.
4. Tag `vx.y.z` on that commit and run the Publish workflow. It creates the GitHub release. There is no package registry.
95 changes: 67 additions & 28 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,89 @@

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

- **examples/delphi-api-v2/** — Merged content from the former **zerobounce-delphi-api-v2** repository (now decommissioned):
- **ZeroBounceValidate.pas** — Shared unit with `UrlEncode` and `IsValidStatus` for reuse and testing (Delphi/Indy).
- **ValidateEmailExample.dpr** — Minimal console example for single-email validation using Indy (IdHTTP, IdSSLOpenSSL) and `System.JSON`.
- **TestValidateEmail.dpr** — Console test runner (no Indy/API key) for `UrlEncode` and `IsValidStatus`.
- **run-tests.bat** / **run-tests.sh** — Scripts to run the test executable after building.
- Unit tests for URL encoding (empty, alphanumeric, `@`, email, spaces, dot, hyphen).
- Unit tests for status interpretation (valid/invalid/unknown case-insensitive, catch-all, spamtrap, sub_status).
- `ZbValidateEmail` and `ZbBatchValidateEmails` take `TZbValidateOptions`: `Timeout`, `ActivityData`, `VerifyPlus`, and `UsePost` (form-encoded `POST /validate`).
- `TZbValidationResult` reads `ActiveInDays` and `ActiveFirstSeen`.
- `ZbScoreEmail` (`GET /v2/scoring`) and `ZbScoreEmailPost` (form-encoded `POST /v2/scoring`).
- Bulk email finder (`ZbSendEmailFinderFile`, `ZbEmailFinderFileStatus`, `ZbEmailFinderResultFetch`, `ZbEmailFinderResultDelete`).
- Bulk domain search (`ZbSendDomainSearchFile`, `ZbDomainSearchFileStatus`, `ZbDomainSearchResultFetch`, `ZbDomainSearchResultDelete`).
- List evaluator (`ZbSendListEvaluatorFile`, `ZbGetListEvaluatorResult`, `ZbSendEvaluatedFile`).
- Filters (`ZbAddFilter`, `ZbListFilters`, `ZbDeleteFilter`).
- S3 bulk (`ZbValidateS3File`, `ZbScoreS3File`, `ZbExportS3File`, `ZbListS3Files`).

---
### Changed

## [2.0.0] - 2025-02-26 (zerobounce-delphi-api-v2, merged)
- API requests time out after 120 seconds.
- Query values, including the API key and file id, are percent-encoded.
- `ZbApiKey` and `ZbApiBaseURL` are per thread. Call `ZBInitialize` on each thread that makes requests.

### Added (from zerobounce-delphi-api-v2)
### Fixed

- **ZeroBounceValidate.pas** — Shared unit with `UrlEncode` and `IsValidStatus` for reuse and testing.
- **TestValidateEmail.dpr** — Console test runner (no Indy/API key) for `UrlEncode` and `IsValidStatus`.
- **run-tests.bat** / **run-tests.sh** — Scripts to run the test executable after building.
- Unit tests for URL encoding (empty, alphanumeric, `@`, email, spaces, dot, hyphen).
- Unit tests for status interpretation (valid/invalid/unknown case-insensitive, catch-all, spamtrap, sub_status).
- A 403 HTML "Access denied" page is raised as a short error. An HTML page with status 200 is an error as well, including bulk `getfile`, instead of file content.
- Empty and non-JSON bodies are a short error. A repeated `Error:` / `[Error]:` prefix is removed.
- `ZbBatchValidateEmails` raises when the batch has an account-level error (HTTP 200, error for address `all`, such as an invalid API key or no credits). An error for one address stays on the returned batch.
- Bulk email finder and domain search send `has_header_row` when it is set to false.
- `ZbBatchValidateEmails` sends an empty IP address as JSON `null`.
- Requests are not sent when `ZBInitialize` was given an empty API key.

### Changed (from zerobounce-delphi-api-v2)
### Security

- **ValidateEmailExample.dpr** now uses `ZeroBounceValidate` for encoding and status logic.
- Status check is **case-insensitive** so API responses like `"invalid"` or `"Unknown"` are handled correctly.
- Query parameters are percent-encoded, so a value cannot rewrite the request URL.
- Exception messages no longer include the raw HTML access-denied page. The response body remains on `ZbException.Payload`.

### Fixed (from zerobounce-delphi-api-v2)
## [2.1.2] - 2026-06-10

- **Resource leak** when the API response was not valid JSON: `HTTP` and `SSLHandler` are now always freed via an outer `try/finally`.
### Added

- `TZbValidationResult.CatchallDomain`, read from `catchall_domain`.

## [2.1.0] - 2026-04-05

### Added

- Optional bulk get-file parameters: `DownloadType` (`phase_1`, `phase_2`, `combined`) and `ActivityData` (validation get-file only).
- `AllowPhase2` on validation bulk file submit.
- File status field for phase 2.

### Fixed

- Bulk get-file treats a JSON error body, including HTTP 200 with `"success": false`, as an error instead of file content.

## [2.0.11] - 2026-03-03

---
### Changed

## [1.x] — zerobounce-delphi-api-v2 (initial)
- License the project under MIT.

- Single-email validation console example using Indy and Zero Bounce API v2.
## [2.0.0] - 2026-02-27

---
### Added

- `examples/delphi-api-v2`: single-email validation example and offline tests for URL encoding and status interpretation, merged from the former zerobounce-delphi-api-v2 repository.

### Changed

- The example uses shared encoding and case-insensitive status checks.

### Fixed

- The example frees the HTTP client when the response is not valid JSON.

## [1.1.0] - 2023-09-04

### Added

## [1.1.0] — zero-bounce-pascal
- Full Pascal/Delphi SDK: validation, batch, bulk, AI scoring, find email, domain search, credits, API usage, and activity data.

- Full Pascal/Delphi SDK: validation, batch, bulk, AI scoring, find email, domain search, credits, API usage, activity data.
- See repository history and tags for earlier changes.
[Unreleased]: https://github.com/zerobounce/zero-bounce-pascal/compare/v2.1.3...HEAD
[2.1.2]: https://github.com/zerobounce/zero-bounce-pascal/compare/v2.1.0...v2.1.2
[2.1.0]: https://github.com/zerobounce/zero-bounce-pascal/compare/v2.0.11...v2.1.0
[2.0.11]: https://github.com/zerobounce/zero-bounce-pascal/compare/v2.0.0...v2.0.11
[2.0.0]: https://github.com/zerobounce/zero-bounce-pascal/compare/v1.1.0...v2.0.0
[1.1.0]: https://github.com/zerobounce/zero-bounce-pascal/releases/tag/v1.1.0
5 changes: 5 additions & 0 deletions packages/zerobounce.lpk
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@
<HasRegisterProc Value="True"/>
<UnitName Value="ZbValidation"/>
</Item>
<Item>
<Filename Value="../src/zbpriority3.pas"/>
<HasRegisterProc Value="True"/>
<UnitName Value="ZbPriority3"/>
</Item>
<Item>
<Filename Value="../src/zboptions.inc"/>
<Type Value="Include"/>
Expand Down
3 changes: 2 additions & 1 deletion packages/zerobounce.pas
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
interface

uses
ZbBulk, ZbGeneric, ZbStructures, ZbUtility, ZbValidation,
ZbBulk, ZbGeneric, ZbPriority3, ZbStructures, ZbUtility, ZbValidation,
LazarusPackageIntf;

implementation
Expand All @@ -20,6 +20,7 @@ procedure Register;
RegisterUnit('ZbStructures', @ZbStructures.Register);
RegisterUnit('ZbUtility', @ZbUtility.Register);
RegisterUnit('ZbValidation', @ZbValidation.Register);
RegisterUnit('ZbPriority3', @ZbPriority3.Register);
end;

initialization
Expand Down
3 changes: 3 additions & 0 deletions src/ZeroBounce.lpr
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
ZbGetCredits,
ZbGetApiUsage,
ZbGetApiUsageWithin,
ZbScoreEmail,
ZbScoreEmailPost,
ZbValidateEmail,
ZbValidateEmailAndIp,
ZbBatchValidateEmails,
Expand All @@ -31,6 +33,7 @@
// auxiliary
ZBInitialize,
TApiUsage,
TZbScoreEmailResult,
TZbValidationResult,
TZbBatchError,
TZbBatchValidation,
Expand Down
10 changes: 6 additions & 4 deletions src/zbbulk.pas
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ function GenericFileStatusCheck(endpoint, FileId: String): TZBFileStatus;
response: TZbRequestResponse;
error: ZbException;
begin
UrlToAccess := Concat(BULK_URI, endpoint, '?api_key=', ZbApiKey);
UrlToAccess := Concat(UrlToAccess, '&file_id=', FileId);
UrlToAccess := Concat(BULK_URI, endpoint, '?api_key=', EncodeParam(ZbApiKey));
UrlToAccess := Concat(UrlToAccess, '&file_id=', EncodeParam(FileId));
response := ZBGetRequest(UrlToAccess);

// attempt json parsing
Expand Down Expand Up @@ -171,6 +171,8 @@ function GenericResultFetch(const endpoint, FileId: String; Scoring: Boolean;
raise error;
end;

if ZbBodyLooksLikeHtml(response.Payload) then
raise ZbException.FromResponse('', response);
IsJsonCT := response.ContentType.Contains(JSON_CONTENT_TYPE);
if IsJsonCT or ZbGetFileJsonIndicatesError(response.Payload) then
begin
Expand All @@ -190,8 +192,8 @@ function GenericResultDelete(endpoint, FileId: String): TZBFileFeedback;
response: TZbRequestResponse;
error: ZbException;
begin
UrlToAccess := Concat(BULK_URI, endpoint, '?api_key=', ZbApiKey);
UrlToAccess := Concat(UrlToAccess, '&file_id=', FileId);
UrlToAccess := Concat(BULK_URI, endpoint, '?api_key=', EncodeParam(ZbApiKey));
UrlToAccess := Concat(UrlToAccess, '&file_id=', EncodeParam(FileId));
response := ZBGetRequest(UrlToAccess);

// attempt json parsing
Expand Down
59 changes: 51 additions & 8 deletions src/zbgeneric.pas
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ function ZbGetCredits : Integer;
function ZbGetApiUsage : TApiUsage; overload;
function ZbGetApiUsage(StartDate, EndDate: TDate) : TApiUsage; overload;
function ZbActivityData(Email: String): Integer;
function ZbScoreEmail(Email: String): TZbScoreEmailResult;
function ZbScoreEmailPost(Email: String): TZbScoreEmailResult;
function ZbFindEmailByDomain(Domain, FirstName, MiddleName, LastName: String) : TZbFindEmailResponse; overload;
function ZbFindEmailByDomain(Domain, FirstName, LastName: String) : TZbFindEmailResponse; overload;
function ZbFindEmailByDomain(Domain, FirstName: String) : TZbFindEmailResponse; overload;
Expand All @@ -35,7 +37,7 @@ implementation
response: TZbRequestResponse;
error: ZbException;
begin
UrlToAccess := Concat(ZbApiBaseURL, ENDPOINT_CREDITS, '?api_key=', ZbApiKey);
UrlToAccess := Concat(ZbApiBaseURL, ENDPOINT_CREDITS, '?api_key=', EncodeParam(ZbApiKey));
response := ZBGetRequest(UrlToAccess);

// attempt json parsing
Expand All @@ -58,7 +60,7 @@ implementation
response: TZbRequestResponse;
error: ZbException;
begin
UrlToAccess := Concat(ZbApiBaseURL, ENDPOINT_API_USAGE, '?api_key=', ZbApiKey);
UrlToAccess := Concat(ZbApiBaseURL, ENDPOINT_API_USAGE, '?api_key=', EncodeParam(ZbApiKey));
DateAuxString := Format(
'%d-%d-%d',
[
Expand Down Expand Up @@ -99,6 +101,47 @@ implementation
end;

// Returns -1 if no activity data was found for the email
function ZbScoreEmailFromResponse(const response: TZbRequestResponse): TZbScoreEmailResult;
var
error: ZbException;
begin
try
Result := TZbScoreEmailResultFromJson(response.Payload);
except on e: Exception do
begin
error := ZbException.FromResponse(e.Message, response);
error.MarkJsonError;
raise error;
end;
end;
end;

function ZbScoreEmail(Email: String): TZbScoreEmailResult;
var
UrlToAccess: String;
response: TZbRequestResponse;
begin
UrlToAccess := Concat(ZbApiBaseURL, ENDPOINT_SCORE_EMAIL);
UrlToAccess := Concat(UrlToAccess, '?api_key=', EncodeParam(ZbApiKey));
UrlToAccess := Concat(UrlToAccess, '&email=', EncodeParam(Email));
response := ZBGetRequest(UrlToAccess);
Result := ZbScoreEmailFromResponse(response);
end;

function ZbScoreEmailPost(Email: String): TZbScoreEmailResult;
var
UrlToAccess, FormBody: String;
response: TZbRequestResponse;
begin
UrlToAccess := Concat(ZbApiBaseURL, ENDPOINT_SCORE_EMAIL);
FormBody := Concat(
'api_key=', EncodeParam(ZbApiKey),
'&email=', EncodeParam(Email)
);
response := ZBFormPostRequest(UrlToAccess, FormBody);
Result := ZbScoreEmailFromResponse(response);
end;

function ZbActivityData(Email: String): Integer;
var
UrlToAccess: String;
Expand All @@ -107,7 +150,7 @@ implementation
error: ZbException;
begin
UrlToAccess := Concat(ZbApiBaseURL, ENDPOINT_ACTIVITY_DATA);
UrlToAccess := Concat(UrlToAccess, '?api_key=', ZbApiKey);
UrlToAccess := Concat(UrlToAccess, '?api_key=', EncodeParam(ZbApiKey));
UrlToAccess := Concat(UrlToAccess, '&email=', EncodeParam(Email));
response := ZBGetRequest(UrlToAccess);

Expand Down Expand Up @@ -135,7 +178,7 @@ implementation
error: ZbException;
begin
UrlToAccess := Concat(ZbApiBaseURL, ENDPOINT_EMAIL_FINDER);
UrlToAccess := Concat(UrlToAccess, '?api_key=', ZbApiKey);
UrlToAccess := Concat(UrlToAccess, '?api_key=', EncodeParam(ZbApiKey));
UrlToAccess := Concat(UrlToAccess, '&domain=', EncodeParam(Domain));
UrlToAccess := Concat(UrlToAccess, '&first_name=', EncodeParam(FirstName));

Expand Down Expand Up @@ -173,7 +216,7 @@ implementation
error: ZbException;
begin
UrlToAccess := Concat(ZbApiBaseURL, ENDPOINT_EMAIL_FINDER);
UrlToAccess := Concat(UrlToAccess, '?api_key=', ZbApiKey);
UrlToAccess := Concat(UrlToAccess, '?api_key=', EncodeParam(ZbApiKey));
UrlToAccess := Concat(UrlToAccess, '&company_name=', EncodeParam(CompanyName));
UrlToAccess := Concat(UrlToAccess, '&first_name=', EncodeParam(FirstName));

Expand Down Expand Up @@ -211,7 +254,7 @@ implementation
error: ZbException;
begin
UrlToAccess := Concat(ZbApiBaseURL, ENDPOINT_EMAIL_FINDER);
UrlToAccess := Concat(UrlToAccess, '?api_key=', ZbApiKey);
UrlToAccess := Concat(UrlToAccess, '?api_key=', EncodeParam(ZbApiKey));
UrlToAccess := Concat(UrlToAccess, '&domain=', EncodeParam(Domain));

response := ZBGetRequest(UrlToAccess);
Expand All @@ -233,7 +276,7 @@ implementation
error: ZbException;
begin
UrlToAccess := Concat(ZbApiBaseURL, ENDPOINT_EMAIL_FINDER);
UrlToAccess := Concat(UrlToAccess, '?api_key=', ZbApiKey);
UrlToAccess := Concat(UrlToAccess, '?api_key=', EncodeParam(ZbApiKey));
UrlToAccess := Concat(UrlToAccess, '&company_name=', EncodeParam(CompanyName));

response := ZBGetRequest(UrlToAccess);
Expand All @@ -255,7 +298,7 @@ implementation
error: ZbException;
begin
UrlToAccess := Concat(ZbApiBaseURL, ENDPOINT_EMAIL_FINDER);
UrlToAccess := Concat(UrlToAccess, '?api_key=', ZbApiKey);
UrlToAccess := Concat(UrlToAccess, '?api_key=', EncodeParam(ZbApiKey));
UrlToAccess := Concat(UrlToAccess, '&domain=', EncodeParam(Domain));

if FirstName <> '' then
Expand Down
Loading
Loading