From 1aabb657a2e11d24336843f2cdf0ee96a211deec Mon Sep 17 00:00:00 2001 From: Daniel Nguyen Date: Thu, 15 Jan 2026 12:01:17 +1100 Subject: [PATCH] feat: add timeout option for email validation --- documentation.md | 10 ++++- package-lock.json | 18 +++++++-- package.json | 2 +- src/utils.js | 6 +++ src/zero-bounce.js | 29 ++++++++++++--- tests/zero-bounce.test.js | 78 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 132 insertions(+), 11 deletions(-) diff --git a/documentation.md b/documentation.md index 7e0eef9..380bd09 100644 --- a/documentation.md +++ b/documentation.md @@ -57,13 +57,19 @@ try { ```javascript const email = ""; // The email address you want to validate -const ip_address = "127.0.0.1"; // The IP Address the email signed up from (Optional) +// Using options object (recommended) try { - const response = await zeroBounce.validateEmail(email, ip_address); + const response = await zeroBounce.validateEmail(email, { + ip_address: "127.0.0.1", // The IP Address the email signed up from (Optional) + timeout: 10, // Validation timeout in seconds, 3-60 (Optional) + }); } catch (error) { console.error(error); } + +// Legacy syntax (still supported for backwards compatibility) +// const response = await zeroBounce.validateEmail(email, "127.0.0.1"); ``` - ####### Get api usage from a start date to an end date diff --git a/package-lock.json b/package-lock.json index 23814d0..89bb7eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@zerobounce/zero-bounce-sdk", - "version": "1.2.0", + "version": "1.2.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@zerobounce/zero-bounce-sdk", - "version": "1.2.0", + "version": "1.2.1", "license": "ISC", "devDependencies": { "@babel/core": "^7.21.4", @@ -58,6 +58,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.4.tgz", "integrity": "sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.21.4", @@ -2536,6 +2537,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2588,6 +2590,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2848,6 +2851,7 @@ "url": "https://tidelift.com/funding/github/npm/browserslist" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001449", "electron-to-chromium": "^1.4.284", @@ -5925,6 +5929,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.78.0.tgz", "integrity": "sha512-gT5DP72KInmE/3azEaQrISjTvLYlSM0j1Ezhht/KLVkrqtv10JoP/RXhwmX/frrutOPuSq3o5Vq0ehR/4Vmd1g==", "dev": true, + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^0.0.51", @@ -5972,6 +5977,7 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.0.1.tgz", "integrity": "sha512-S3KVAyfwUqr0Mo/ur3NzIp6jnerNpo7GUO6so51mxLi1spqsA17YcMXy0WOIJtBSnj748lthxC6XLbNKh/ZC+A==", "dev": true, + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.0.1", @@ -6262,6 +6268,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.4.tgz", "integrity": "sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA==", "dev": true, + "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.21.4", @@ -8129,7 +8136,8 @@ "version": "8.8.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "dev": true + "dev": true, + "peer": true }, "acorn-globals": { "version": "7.0.1", @@ -8168,6 +8176,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -8361,6 +8370,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", "dev": true, + "peer": true, "requires": { "caniuse-lite": "^1.0.30001449", "electron-to-chromium": "^1.4.284", @@ -10647,6 +10657,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.78.0.tgz", "integrity": "sha512-gT5DP72KInmE/3azEaQrISjTvLYlSM0j1Ezhht/KLVkrqtv10JoP/RXhwmX/frrutOPuSq3o5Vq0ehR/4Vmd1g==", "dev": true, + "peer": true, "requires": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^0.0.51", @@ -10679,6 +10690,7 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.0.1.tgz", "integrity": "sha512-S3KVAyfwUqr0Mo/ur3NzIp6jnerNpo7GUO6so51mxLi1spqsA17YcMXy0WOIJtBSnj748lthxC6XLbNKh/ZC+A==", "dev": true, + "peer": true, "requires": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.0.1", diff --git a/package.json b/package.json index abab9e6..9b35403 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zerobounce/zero-bounce-sdk", - "version": "1.2.0", + "version": "1.2.1", "description": "This SDK contains methods for interacting easily with ZeroBounce API. More information about ZeroBounce you can find in the official documentation.", "main": "dist/zeroBounceSDK.js", "scripts": { diff --git a/src/utils.js b/src/utils.js index 60793d5..b5c02c8 100644 --- a/src/utils.js +++ b/src/utils.js @@ -74,3 +74,9 @@ export function parameterIsMissing(parameter, aditionalInfo = "") { `ZeroBounce: ${parameter} parameter is missing. ${aditionalInfo}` ); } + +export function parameterIsInvalid(parameter, aditionalInfo = "") { + console.error( + `ZeroBounce: ${parameter} parameter is invalid. ${aditionalInfo}` + ); +} diff --git a/src/zero-bounce.js b/src/zero-bounce.js index 3ba9c28..a3c011f 100644 --- a/src/zero-bounce.js +++ b/src/zero-bounce.js @@ -1,4 +1,4 @@ -import { createRequest, notInitialized, parameterIsMissing } from "./utils.js"; +import { createRequest, notInitialized, parameterIsMissing, parameterIsInvalid } from "./utils.js"; export class ZeroBounceSDK { static ApiURL = Object.freeze({ @@ -40,9 +40,11 @@ export class ZeroBounceSDK { /** * @param email - email to be validated - * @param ip_address + * @param options - options object or ip_address string for backwards compatibility + * @param options.ip_address - IP address (optional) + * @param options.timeout - validation timeout in seconds, 3-60 (optional). If met, the API will return unknown/greylisted. * */ - validateEmail(email, ip_address = null) { + validateEmail(email, options = null) { if (!this._initialized) { notInitialized(); return; @@ -50,11 +52,28 @@ export class ZeroBounceSDK { parameterIsMissing("Email"); return; } + + let ip_address; + let timeout; + if (typeof options === "string") { + ip_address = options; + } else if (options && typeof options === "object") { + ip_address = options.ip_address; + timeout = options.timeout; + } + + if (timeout != null && (timeout < 3 || timeout > 60)) { + parameterIsInvalid("timeout", "Must be between 3 and 60 seconds."); + return; + } + const params = { api_key: this._api_key, - email: email, - ip_address, + email, }; + if (ip_address != null) params.ip_address = ip_address; + if (timeout != null) params.timeout = timeout; + return createRequest({ requestType: "GET", params, path: "/validate", apiBaseURL: this._api_base_url }); } diff --git a/tests/zero-bounce.test.js b/tests/zero-bounce.test.js index cf082ea..e6f56d8 100644 --- a/tests/zero-bounce.test.js +++ b/tests/zero-bounce.test.js @@ -199,6 +199,84 @@ describe("ZeroBounceSDK", () => { const response = await zeroBounceSDK.validateEmail(email, ip_address); expect(response).toEqual(expectedResponse); }); + + it("should pass options object to the API", async () => { + const expectedResponse = { + "address": "valid@example.com", + "status": "valid", + "sub_status": "", + "free_email": false, + "did_you_mean": null, + "account": null, + "domain": null, + "domain_age_days": "9692", + "smtp_provider": "example", + "mx_found": "true", + "mx_record": "mx.example.com", + "firstname": "zero", + "lastname": "bounce", + "gender": "male", + "country": null, + "region": null, + "city": null, + "zipcode": null, + "processed_at": "2023-04-27 13:47:23.980" + } + + const fetchSpy = jest.spyOn(global, "fetch").mockImplementationOnce(() => Promise.resolve({ + json: () => Promise.resolve(expectedResponse), + text: () => Promise.resolve(JSON.stringify(expectedResponse)), + })); + + zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL); + await zeroBounceSDK.validateEmail(email, { ip_address: ip_address, timeout: 10 }); + + expect(fetchSpy).toHaveBeenCalledWith( + expect.stringContaining("ip_address=127.0.0.1"), + expect.any(Object) + ); + expect(fetchSpy).toHaveBeenCalledWith( + expect.stringContaining("timeout=10"), + expect.any(Object) + ); + }); + + it("should pass timeout only via options object", async () => { + const expectedResponse = { + "address": "valid@example.com", + "status": "valid", + "sub_status": "", + } + + const fetchSpy = jest.spyOn(global, "fetch").mockImplementationOnce(() => Promise.resolve({ + json: () => Promise.resolve(expectedResponse), + text: () => Promise.resolve(JSON.stringify(expectedResponse)), + })); + + zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL); + await zeroBounceSDK.validateEmail(email, { timeout: 30 }); + + expect(fetchSpy).toHaveBeenCalledWith( + expect.stringContaining("timeout=30"), + expect.any(Object) + ); + }); + + it("should throw an error if timeout is less than 3", async () => { + zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL); + await zeroBounceSDK.validateEmail(email, { timeout: 2 }); + expect(console.error).toHaveBeenCalledWith( + expect.stringContaining("timeout parameter is invalid") + ); + }); + + it("should throw an error if timeout is greater than 60", async () => { + zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL); + await zeroBounceSDK.validateEmail(email, { timeout: 61 }); + expect(console.error).toHaveBeenCalledWith( + expect.stringContaining("timeout parameter is invalid") + ); + }); });