├── .gitignore
├── LICENSE
├── composer.json
├── lib
└── Verifalia
│ ├── Security
│ ├── TotpTokenProvider.php
│ ├── AuthenticationProvider.php
│ ├── UsernamePasswordAuthenticationProvider.php
│ ├── ClientCertificateAuthenticationProvider.php
│ └── BearerAuthenticationProvider.php
│ ├── Exceptions
│ └── VerifaliaException.php
│ ├── Internal
│ ├── Rest
│ │ ├── InvocationOptions.php
│ │ └── MultiplexedRestClient.php
│ └── ParserUtils.php
│ ├── Common
│ ├── ListingCursor.php
│ ├── Direction.php
│ └── ListingOptions.php
│ ├── EmailValidations
│ ├── ValidationProgress.php
│ ├── Validation.php
│ ├── ExportedEntriesFormat.php
│ ├── ValidationStatus.php
│ ├── LineEndingMode.php
│ ├── ValidationEntryClassification.php
│ ├── DeduplicationMode.php
│ ├── ValidationRequestEntry.php
│ ├── CompletionCallback.php
│ ├── QualityLevelName.php
│ ├── ValidationRequest.php
│ ├── ValidationRequestBase.php
│ ├── WaitOptions.php
│ ├── ValidationOverview.php
│ ├── FileValidationRequest.php
│ ├── ValidationEntry.php
│ ├── ValidationEntryStatus.php
│ └── EmailValidationsRestClient.php
│ ├── Credits
│ ├── Balance.php
│ └── CreditsRestClient.php
│ ├── VerifaliaRestClientOptions.php
│ └── VerifaliaRestClient.php
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | vendor
3 | composer.lock
4 | composer.phar
5 | test.php
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2005-2024 Cobisi Research - https://verifalia.com/
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "verifalia/sdk",
3 | "description": "Verifalia provides a simple HTTPS-based API for validating email addresses and checking whether they are deliverable or not. This library allows to easily integrate with Verifalia and verify email addresses in real-time.",
4 | "homepage": "https://verifalia.com/",
5 | "keywords": [
6 | "verifalia",
7 | "email",
8 | "validation",
9 | "verification",
10 | "free",
11 | "service",
12 | "saas",
13 | "list",
14 | "hygiene",
15 | "cleaning",
16 | "mx",
17 | "smtp",
18 | "mailbox",
19 | "scrub",
20 | "email checker"
21 | ],
22 | "authors": [
23 | {
24 | "name": "Rudy Chiappetta"
25 | },
26 | {
27 | "name": "John Grounz"
28 | },
29 | {
30 | "name": "Efran Cobisi"
31 | },
32 | {
33 | "name": "Stefano Tatah"
34 | },
35 | {
36 | "name": "Germano Mosconi"
37 | },
38 | {
39 | "name": "Alex Soffiateli"
40 | }
41 | ],
42 | "autoload": {
43 | "psr-4": {
44 | "Verifalia\\": "lib/Verifalia"
45 | }
46 | },
47 | "require": {
48 | "php": ">=7",
49 | "guzzlehttp/guzzle": "^6|^7",
50 | "ext-json": "*"
51 | },
52 | "support": {
53 | "issues": "https://github.com/verifalia/verifalia-php-sdk/issues"
54 | },
55 | "license": "MIT"
56 | }
--------------------------------------------------------------------------------
/lib/Verifalia/Security/TotpTokenProvider.php:
--------------------------------------------------------------------------------
1 | cursor = $cursor;
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/lib/Verifalia/Common/Direction.php:
--------------------------------------------------------------------------------
1 | progress` field.
33 | * @see Validation
34 | */
35 | class ValidationProgress
36 | {
37 | /**
38 | * @var double The percentage of completed entries, expressed as a decimal ranging from 0 to 1.
39 | */
40 | public $percentage;
41 |
42 | /**
43 | * @var ?DateInterval An estimated time span required to complete the entire job, if available.
44 | */
45 | public $estimatedTimeRemaining;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/lib/Verifalia/Common/ListingOptions.php:
--------------------------------------------------------------------------------
1 | overview = $overview;
47 | $this->entries = $entries;
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/lib/Verifalia/EmailValidations/ExportedEntriesFormat.php:
--------------------------------------------------------------------------------
1 | days > 0)
51 | {
52 | return $interval->format("%d.H:I:S");
53 | }
54 |
55 | return $interval->format("H:I:S");
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/lib/Verifalia/EmailValidations/ValidationRequestEntry.php:
--------------------------------------------------------------------------------
1 | inputData = $inputData;
55 | $this->custom = $custom;
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/lib/Verifalia/EmailValidations/CompletionCallback.php:
--------------------------------------------------------------------------------
1 | url = $url;
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/lib/Verifalia/Credits/Balance.php:
--------------------------------------------------------------------------------
1 | creditPacks = $creditPacks;
61 | $this->freeCredits = $freeCredits;
62 | $this->freeCreditsResetIn = $freeCreditsResetIn;
63 | }
64 | }
65 | }
--------------------------------------------------------------------------------
/lib/Verifalia/EmailValidations/QualityLevelName.php:
--------------------------------------------------------------------------------
1 | restClient = $restClient;
46 | }
47 |
48 | /**
49 | * Returns the current credits balance for the Verifalia account.
50 | *
51 | * @return Balance The credits balance for the Verifalia account.
52 | * @throws VerifaliaException
53 | */
54 | public function getBalance(): Balance
55 | {
56 | $response = $this->restClient->invoke(MultiplexedRestClient::HTTP_METHOD_GET, "credits/balance");
57 | $statusCode = $response->getStatusCode();
58 | $body = $response->getBody();
59 |
60 | switch ($statusCode) {
61 | case MultiplexedRestClient::HTTP_STATUS_OK: {
62 | $balance = json_decode($body);
63 |
64 | // Mapping
65 |
66 | if (!empty($balance->freeCreditsResetIn)) {
67 | $balance->freeCreditsResetIn = ParserUtils::timeSpanStringToDateInterval($balance->freeCreditsResetIn);
68 | }
69 |
70 | return new Balance($balance->creditPacks,
71 | $balance->freeCredits,
72 | $balance->freeCreditsResetIn);
73 | }
74 |
75 | default:
76 | throw new VerifaliaException("Unexpected HTTP status code $statusCode. Body: $body");
77 | }
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/lib/Verifalia/EmailValidations/ValidationRequest.php:
--------------------------------------------------------------------------------
1 | entries = array();
53 |
54 | if (is_array($entries)) {
55 | for ($x = 0; $x < count($entries); $x++) {
56 | $this->addEntry($entries[$x]);
57 | }
58 | } else {
59 | $this->addEntry($entries);
60 | }
61 | }
62 |
63 | /**
64 | * @param string|ValidationRequestEntry $entry
65 | * @return void
66 | */
67 | private function addEntry($entry)
68 | {
69 | if (is_string($entry)) {
70 | $this->entries[] = new ValidationRequestEntry($entry);
71 | } else if ($entry instanceof ValidationRequestEntry) {
72 | $this->entries[] = $entry;
73 | } else {
74 | throw new InvalidArgumentException('Invalid input entry: it must be either a string representing the email address or a ValidationRequestEntry instance.');
75 | }
76 | }
77 | }
78 | }
--------------------------------------------------------------------------------
/lib/Verifalia/Security/UsernamePasswordAuthenticationProvider.php:
--------------------------------------------------------------------------------
1 | username = $username;
54 | $this->password = $password;
55 | }
56 |
57 | public function authenticate(MultiplexedRestClient $restClient, &$requestOptions)
58 | {
59 | $requestOptions = array_merge($requestOptions, [
60 | RequestOptions::AUTH => [
61 | $this->username,
62 | $this->password
63 | ]
64 | ]);
65 | }
66 |
67 | /**
68 | * @throws VerifaliaException
69 | */
70 | public function handleUnauthorizedRequest(MultiplexedRestClient $restClient)
71 | {
72 | throw new VerifaliaException("Can't authenticate to Verifalia using the provided username and password: please check your credentials and retry.");
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/lib/Verifalia/Security/ClientCertificateAuthenticationProvider.php:
--------------------------------------------------------------------------------
1 | certificate = $certificate;
58 | }
59 |
60 | public function authenticate(MultiplexedRestClient $restClient, &$requestOptions)
61 | {
62 | $requestOptions = array_merge($requestOptions, [
63 | RequestOptions::CERT => $this->certificate
64 | ]);
65 | }
66 |
67 | /**
68 | * @throws VerifaliaException
69 | */
70 | public function handleUnauthorizedRequest(MultiplexedRestClient $restClient)
71 | {
72 | throw new VerifaliaException("Can't authenticate to Verifalia using the provided username and password: please check your credentials and retry.");
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/lib/Verifalia/VerifaliaRestClientOptions.php:
--------------------------------------------------------------------------------
1 | submissionWaitTime = 0;
72 | self::$noWait->pollWaitTime = 0;
73 | }
74 |
75 | /**
76 | * Initializes new configuration settings for waiting on the completion of an email validation job.
77 | *
78 | * @param callable|null $progress A callable which eventually receives completion progress updates for an email
79 | * validation job. It accepts a ValidationOverview, with the current email validation job overview.
80 | * @see ValidationOverview
81 | */
82 | public function __construct(callable $progress = null)
83 | {
84 | $this->progress = $progress;
85 | }
86 |
87 | function waitForNextPoll($validationOverview)
88 | {
89 | // Observe the ETA if we have one, otherwise a delay given the formula: max(5, min(30, 2^(log(noOfEntries, 10) - 1)))
90 |
91 | $delay = max(5, min(30, pow(2, log10($validationOverview->noOfEntries) - 1)));
92 |
93 | if (!empty($validationOverview->progress))
94 | {
95 | if (!empty($validationOverview->progress->estimatedTimeRemaining))
96 | {
97 | $delay = $validationOverview->progress->estimatedTimeRemaining->s;
98 | $delay += $validationOverview->progress->estimatedTimeRemaining->i * 60;
99 | $delay += $validationOverview->progress->estimatedTimeRemaining->h * 3600;
100 | $delay += $validationOverview->progress->estimatedTimeRemaining->d * 3600 * 24;
101 |
102 | // TODO: Follow the ETA more precisely
103 |
104 | $delay = max(5, min(30, $delay));
105 | }
106 | }
107 |
108 | sleep($delay);
109 | }
110 | }
111 |
112 | // Invokes the static constructor for this class (sort of)
113 |
114 | WaitOptions::__constructStatic();
115 | }
116 |
--------------------------------------------------------------------------------
/lib/Verifalia/EmailValidations/ValidationOverview.php:
--------------------------------------------------------------------------------
1 | contentType = $contentType;
96 |
97 | if (is_string($file)) {
98 | $this->file = fopen($file, 'rb');
99 |
100 | // Guess the content type, if no value has been specified
101 |
102 | if ($contentType == null) {
103 | $this->contentType = $this->tryGuessContentTypeFromFileExtension(pathinfo($file, PATHINFO_EXTENSION));
104 | }
105 | }
106 | else {
107 | $this->file = $file;
108 | }
109 |
110 | if ($this->contentType == null) {
111 | throw new VerifaliaException("Can't determine the MIME content type of the file from its extension: specify the content type manually, please.");
112 | }
113 | }
114 |
115 | private function tryGuessContentTypeFromFileExtension(string $extension)
116 | {
117 | switch ($extension)
118 | {
119 | case "txt": return "text/plain";
120 | case "csv": return "text/csv";
121 | case "tsv":
122 | case "tab": return "text/tab-separated-values";
123 | case "xls": return "application/vnd.ms-excel";
124 | case "xlsx": return "application/vnd.openxmlformats-officedocument.spreadsheetml";
125 | default: return null;
126 | }
127 | }
128 | }
129 | }
--------------------------------------------------------------------------------
/lib/Verifalia/EmailValidations/ValidationEntry.php:
--------------------------------------------------------------------------------
1 | punycode algorithm.
71 | * To get the domain part without any ASCII encoding, use `emailAddressDomainPart`.
72 | */
73 | public $asciiEmailAddressDomainPart;
74 |
75 | /**
76 | * @var ?string Gets the local part of the email address, without comments and folding white spaces.
77 | */
78 | public $emailAddressLocalPart;
79 |
80 | /**
81 | * @var ?string Gets the domain part of the email address, without comments and folding white spaces.
82 | * If the ASCII-only (punycode) version of the domain part is needed, use `asciiEmailAddressDomainPart`.
83 | */
84 | public $emailAddressDomainPart;
85 |
86 | /**
87 | * @var ?bool If true, the email address has an international domain name.
88 | */
89 | public $hasInternationalDomainName;
90 |
91 | /**
92 | * @var ?bool If true, the email address has an international mailbox name.
93 | */
94 | public $hasInternationalMailboxName;
95 |
96 | /**
97 | * @var ?bool If true, the email address comes from a disposable email address (DEA) provider.
98 | * Learn more about
99 | * disposable email addresses.
100 | */
101 | public $isDisposableEmailAddress;
102 |
103 | /**
104 | * @var ?bool If true, the email address comes from a free email address provider (e.g. gmail, yahoo, outlook / hotmail, ...).
105 | */
106 | public $isFreeEmailAddress;
107 |
108 | /**
109 | * @var ?bool If true, the local part of the email address is a well-known role account.
110 | */
111 | public $isRoleAccount;
112 |
113 | /**
114 | * @var string The validation status for this entry.
115 | * @see ValidationEntryStatus for a list of the validation statuses supported by this SDK.
116 | */
117 | public $status;
118 |
119 | /**
120 | * @var string The classification for the status of this email address. Standard values include `Deliverable`,
121 | * `Undeliverable`, `Risky` and `Unknown`.
122 | * @see ValidationEntryClassification for a list of the classifications supported by this SDK.
123 | */
124 | public $classification;
125 |
126 | /**
127 | * @var ?int The position of the character in the email address that led to the syntax validation failure.
128 | * Returns null if there is no syntax failure.
129 | */
130 | public $syntaxFailureIndex;
131 |
132 | /**
133 | * @var ?int The zero-based index of the first occurrence of this email address in the parent `Validation`.
134 | * This information is relevant when the status for this entry is `Duplicate`; duplicated items only provide
135 | * details such as this index and any possible `custom` values.
136 | * @see Validation
137 | */
138 | public $duplicateOf;
139 |
140 | /**
141 | * @var ?string[] The potential corrections for the input data, in the event Verifalia identified potential
142 | * typos during the verification process.
143 | */
144 | public $suggestions;
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/lib/Verifalia/Security/BearerAuthenticationProvider.php:
--------------------------------------------------------------------------------
1 | username = $username;
55 | $this->password = $password;
56 | // $this->totpTokenProvider = $totpTokenProvider;
57 | }
58 |
59 | /**
60 | * @throws VerifaliaException
61 | */
62 | public function authenticate(MultiplexedRestClient $restClient, &$requestOptions)
63 | {
64 | if ($this->accessToken === null) {
65 | $authData = array(
66 | 'username' => $this->username,
67 | 'password' => $this->password
68 | );
69 |
70 | $options = new InvocationOptions();
71 | $options->skipAuthentication = true;
72 |
73 | $authResponse = $restClient->invoke(MultiplexedRestClient::HTTP_METHOD_POST,
74 | 'auth/tokens',
75 | null,
76 | $authData,
77 | $options);
78 |
79 | if ($authResponse->getStatusCode() == MultiplexedRestClient::HTTP_STATUS_OK) {
80 | $this->accessToken = json_decode($authResponse->getBody())->accessToken;
81 |
82 | // TODO: Add support for MFA
83 |
84 | // // Decode the JWT token, see https://www.converticacommerce.com/support-maintenance/security/php-one-liner-decode-jwt-json-web-tokens/
85 | //
86 | // $claims = json_decode(base64_decode(str_replace('_', '/', str_replace('-','+',explode('.', $this->accessToken)[1]))));
87 | //
88 | // // Handle the multi-factor auth (MFA) request, if needed
89 | //
90 | // if (property_exists($claims, 'verifalia:mfa')) {
91 | // $totpAuthData = $this->provideAdditionalAuthFactor($restClient);
92 | // $this->accessToken = $totpAuthData->accessToken;
93 | // }
94 | }
95 | else {
96 | throw new VerifaliaException('Invalid credentials used while attempting to retrieve a bearer auth token.');
97 | }
98 | }
99 |
100 | $this->addBearerAuth($requestOptions);
101 | }
102 |
103 | public function handleUnauthorizedRequest(MultiplexedRestClient $restClient)
104 | {
105 | $this->accessToken = null;
106 | }
107 |
108 | private function addBearerAuth(&$requestOptions) {
109 | $requestOptions = array_merge($requestOptions, [
110 | RequestOptions::HEADERS => [
111 | 'Authorization' => 'Bearer '.$this->accessToken
112 | ]
113 | ]);
114 | }
115 |
116 | /**
117 | * @throws VerifaliaException
118 | */
119 | private function provideAdditionalAuthFactor(MultiplexedRestClient $restClient) {
120 | if ($this->totpTokenProvider === null) {
121 | throw new VerifaliaException('A multi-factor authentication is required but no token provider has been provided.');
122 | }
123 |
124 | for ($idxAttempt = 0; $idxAttempt < self::MAX_NO_OF_ATTEMPTS; $idxAttempt++) {
125 | // Retrieve the one-time token from the configured device
126 |
127 | $totp = $this->totpTokenProvider->provideTotpToken();
128 |
129 | // Validates the provided token against the Verifalia API
130 |
131 | try {
132 | $options = new InvocationOptions();
133 | $options->skipAuthentication = true;
134 | $options->requestOptions = [];
135 |
136 | $this->addBearerAuth($options->requestOptions);
137 |
138 | $authResponse = $restClient->invoke(MultiplexedRestClient::HTTP_METHOD_POST,
139 | 'auth/totp/verifications',
140 | null,
141 | [
142 | 'passCode' => $totp
143 | ],
144 | $options);
145 |
146 | if ($authResponse->getStatusCode() == MultiplexedRestClient::HTTP_STATUS_OK) {
147 | return json_decode($authResponse->getBody());
148 | }
149 | }
150 | catch(VerifaliaException $ex) {
151 | // Having an authorization issue is allowed here, as we are working on an TOTP token validation attempt.
152 | // We will re-throw a VerifaliaException below in the even all the configured TOTP validation attempts fail.
153 | }
154 | }
155 |
156 | throw new VerifaliaException("Invalid TOTP token provided after ".self::MAX_NO_OF_ATTEMPTS." attempt(s): aborting the authentication.");
157 | }
158 | }
159 |
160 | }
161 |
--------------------------------------------------------------------------------
/lib/Verifalia/Internal/Rest/MultiplexedRestClient.php:
--------------------------------------------------------------------------------
1 | shuffledBaseUris = $baseUris;
67 | shuffle($this->shuffledBaseUris);
68 |
69 | $this->authenticationProvider = $authenticationProvider;
70 | }
71 |
72 | /**
73 | * Invokes the specified REST resource.
74 | *
75 | * @param string $method
76 | * @param string $relativePath
77 | * @param string|array|null $query
78 | * @param ?mixed $data
79 | * @param InvocationOptions|null $options
80 | * @return ResponseInterface
81 | * @throws VerifaliaException
82 | */
83 | public function invoke(string $method, string $relativePath, $query = null, $data = null, InvocationOptions $options = null): ResponseInterface
84 | {
85 | $errors = [];
86 |
87 | // Cycle among the base URIs
88 |
89 | foreach ($this->shuffledBaseUris as $baseUri) {
90 | $underlyingClient = new Client([
91 | 'base_uri' => $baseUri . self::DEFAULT_API_VERSION . '/',
92 | ]);
93 |
94 | $requestOptions = [
95 | RequestOptions::ALLOW_REDIRECTS => false,
96 | RequestOptions::HEADERS => [
97 | 'Accept' => 'application/json',
98 | 'Accept-Encoding' => 'gzip',
99 | 'User-Agent' => 'verifalia-rest-client/php/' . self::PACKAGE_VERSION . '/' . phpversion()
100 | ],
101 | RequestOptions::HTTP_ERRORS => false
102 | ];
103 |
104 | if ($options === null || !$options->skipAuthentication) {
105 | $this->authenticationProvider->authenticate($this, $requestOptions);
106 | }
107 |
108 | if ($query !== null) {
109 | $requestOptions[RequestOptions::QUERY] = $query;
110 | }
111 |
112 | if ($method === self::HTTP_METHOD_POST || $method === self::HTTP_METHOD_PUT) {
113 | if ($data !== null) {
114 | $requestOptions[RequestOptions::JSON] = $data;
115 | }
116 | }
117 |
118 | // Additional options
119 |
120 | if ($options !== null && $options->requestOptions !== null) {
121 | $requestOptions = array_merge($requestOptions, $options->requestOptions);
122 | }
123 |
124 | // Execute the request against the current API endpoint
125 |
126 | $response = null;
127 |
128 | try {
129 | $response = $underlyingClient->request(
130 | $method,
131 | $relativePath,
132 | $requestOptions
133 | );
134 | } catch (Throwable $e) {
135 | // Records the error and continue cycling, hoping the next endpoint will handle the request
136 |
137 | $errors[] = $e;
138 | continue;
139 | }
140 |
141 | // Records an error (and continue cycling) in the event the status code is a 5xx
142 |
143 | if ($response->getStatusCode() >= 500 && $response->getStatusCode() <= 599) {
144 | $errors[] = new VerifaliaException('Status code is ' . $response->getStatusCode());
145 | continue;
146 | }
147 |
148 | // If the request is unauthorized, give the authentication provider a chance to remediate (on a subsequent attempt)
149 |
150 | if ($response->getStatusCode() == self::HTTP_STATUS_UNAUTHORIZED) {
151 | $this->authenticationProvider->handleUnauthorizedRequest($this);
152 |
153 | $errors[] = new VerifaliaException("Can't authenticate to Verifalia using the provided credentials (will retry in the next attempt).");
154 | continue;
155 | }
156 |
157 | // Fails on the first occurrence of an HTTP 403 status code
158 |
159 | if ($response->getStatusCode() === self::HTTP_STATUS_FORBIDDEN) {
160 | throw new VerifaliaException('Authentication error (HTTP ' . $response->getStatusCode() . '): ' . $response->getBody());
161 | }
162 |
163 | return $response;
164 | }
165 |
166 | // We have iterated all the base URIs at this point, so we should report the issue
167 |
168 | throw new VerifaliaException('All the endpoints are unreachable. ' . join(',', $errors));
169 | }
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/lib/Verifalia/VerifaliaRestClient.php:
--------------------------------------------------------------------------------
1 | 'your-username-here',
77 | * 'password' => 'your-password-here'
78 | * ]);
79 | *
80 | * While authenticating with your Verifalia main account credentials is possible, it is strongly advised
81 | * to create one or more users (formerly known as sub-accounts) with just the required permissions, for improved
82 | * security. To create a new user or manage existing ones, please visit https://verifalia.com/client-area#/users
83 | *
84 | * #### X.509 client-certificate authentication
85 | *
86 | * Here is an example showing how to initialize a `VerifaliaRestClient` instance using an X.590 client-certificate
87 | * for a user:
88 | *
89 | * $verifalia = new VerifaliaRestClient([
90 | * 'certificate' => '/home/gfring/Documents/lospollos.full.pem'
91 | * ]);
92 | *
93 | * #### Other options
94 | * Configuration settings include the following options:
95 | *
96 | * - username: The username of the Verifalia user to authenticate with.
97 | * - password: The password of the Verifalia user to authenticate with.
98 | * - baseUris: The base URIs of the Verifalia API.
99 | * - authentication-provider: An implementation of `AuthenticationProvider` used to authenticate against the
100 | * Verifalia API.
101 | *
102 | * @param array $options VerifaliaRestClient configuration settings.
103 | * @see VerifaliaRestClientOptions for a list of available options.
104 | */
105 | public function __construct(array $options)
106 | {
107 | // Check the provided options
108 |
109 | if ($options === null) {
110 | throw new InvalidArgumentException('options is null');
111 | }
112 |
113 | // Custom base URIs
114 |
115 | if (array_key_exists(VerifaliaRestClientOptions::BASE_URIS, $options)) {
116 | $baseUris = $options[VerifaliaRestClientOptions::BASE_URIS];
117 | }
118 |
119 | // Authentication settings
120 |
121 | if (array_key_exists(VerifaliaRestClientOptions::AUTHENTICATION_PROVIDER, $options)) {
122 | $authenticationProvider = $options[VerifaliaRestClientOptions::AUTHENTICATION_PROVIDER];
123 | }
124 | else {
125 | if (array_key_exists(VerifaliaRestClientOptions::CERTIFICATE, $options)) {
126 | $authenticationProvider = new ClientCertificateAuthenticationProvider(
127 | $options[VerifaliaRestClientOptions::CERTIFICATE]
128 | );
129 |
130 | // Default base URIs for client-certificate authentication
131 |
132 | if (empty($baseUris)) {
133 | $baseUris = self::DEFAULT_CCA_BASE_URIS;
134 | }
135 | } else {
136 | if (!array_key_exists(VerifaliaRestClientOptions::USERNAME, $options)) {
137 | throw new InvalidArgumentException("username is null or empty: please visit https://verifalia.com/client-area to set up a new user, if you don't have one.");
138 | }
139 |
140 | if (!array_key_exists(VerifaliaRestClientOptions::PASSWORD, $options)) {
141 | throw new InvalidArgumentException("password is null or empty: please visit https://verifalia.com/client-area to set up a new user, if you don't have one.");
142 | }
143 |
144 | $authenticationProvider = new UsernamePasswordAuthenticationProvider(
145 | $options[VerifaliaRestClientOptions::USERNAME],
146 | $options[VerifaliaRestClientOptions::PASSWORD]
147 | );
148 | }
149 | }
150 |
151 | if (empty($baseUris)) {
152 | $baseUris = self::DEFAULT_BASE_URIS;
153 | }
154 |
155 | // Set up the underlying REST client
156 |
157 | $restClient = new MultiplexedRestClient(
158 | $baseUris,
159 | $authenticationProvider
160 | );
161 |
162 | $this->credits = new CreditsRestClient($restClient);
163 | $this->emailValidations = new EmailValidationsRestClient($restClient);
164 | }
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/lib/Verifalia/EmailValidations/ValidationEntryStatus.php:
--------------------------------------------------------------------------------
1 | RFC 5336 and support and
201 | * announce both the 8BITMIME and the UTF8SMTP protocol extensions.
202 | */
203 | const SERVER_DOES_NOT_SUPPORT_INTERNATIONAL_MAILBOXES = 'ServerDoesNotSupportInternationalMailboxes';
204 |
205 | /**
206 | * The external mail exchanger accepts fake, nonexistent, email addresses; therefore the provided email address
207 | * MAY be nonexistent too.
208 | */
209 | const SERVER_IS_CATCH_ALL = 'ServerIsCatchAll';
210 |
211 | /**
212 | * The mail exchanger responsible for the email address under test is temporarily unavailable.
213 | */
214 | const SERVER_TEMPORARILY_UNAVAILABLE = 'ServerTemporaryUnavailable';
215 |
216 | /**
217 | * A socket connection error occurred while connecting to the mail exchanger which serves the email address
218 | * domain.
219 | */
220 | const SMTP_CONNECTION_FAILURE = 'SmtpConnectionFailure';
221 |
222 | /**
223 | * A timeout has occurred while connecting to the mail exchanger which serves the email address domain.
224 | */
225 | const SMTP_CONNECTION_TIMEOUT = 'SmtpConnectionTimeout';
226 |
227 | /**
228 | * The mail exchanger responsible for the email address under test replied one or more non-standard SMTP replies
229 | * which caused the SMTP session to be aborted.
230 | */
231 | const SMTP_DIALOG_ERROR = 'SmtpDialogError';
232 |
233 | /**
234 | * The email address has been successfully validated.
235 | */
236 | const SUCCESS = 'Success';
237 |
238 | /**
239 | * The domain literal of the email address couldn't accept messages from the Internet.
240 | */
241 | const UNACCEPTABLE_DOMAIN_LITERAL = 'UnacceptableDomainLiteral';
242 |
243 | /**
244 | * The number of parenthesis used to open comments is not equal to the one used to close them.
245 | */
246 | const UNBALANCED_COMMENT_PARENTHESIS = 'UnbalancedCommentParenthesis';
247 |
248 | /**
249 | * An unexpected quoted pair sequence has been found within a quoted word.
250 | */
251 | const UNEXPECTED_QUOTED_PAIR_SEQUENCE = 'UnexpectedQuotedPairSequence';
252 |
253 | /**
254 | * One or more unhandled exceptions have been thrown during the verification process and something went wrong on
255 | * the Verifalia side.
256 | */
257 | const UNHANDLED_EXCEPTION = 'UnhandledException';
258 |
259 | /**
260 | * A quoted pair within a quoted word is not closed properly.
261 | */
262 | const UNMATCHED_QUOTED_PAIR = 'UnmatchedQuotedPair';
263 |
264 | /**
265 | * The system assigned a user-defined classification because the input data met the criteria specified in a
266 | * custom classification override rule.
267 | */
268 | const OVERRIDE_MATCH = 'OverrideMatch';
269 | }
270 |
271 | }
--------------------------------------------------------------------------------
/lib/Verifalia/EmailValidations/EmailValidationsRestClient.php:
--------------------------------------------------------------------------------
1 | restClient = $restClient;
53 | }
54 |
55 | /**
56 | * Submits a new email validation for processing. By default, this method **waits** for the completion of the email
57 | * validation job. You can pass a `$waitOptions` parameter to request a different waiting behavior.
58 | *
59 | * @param string|string[]|ValidationRequestEntry[]|ValidationRequest|ValidationRequestEntry|FileValidationRequest $entries
60 | * One or more email addresses to validate, provided either as a string, or an array of strings or a request instance.
61 | * @param ?WaitOptions $waitOptions Defines the options that specify how to wait for the completion of the email
62 | * validation job. It can be set to `null` to wait for completion using the default options provided by the SDK,
63 | * or set to an instance of `WaitOptions` for advanced scenarios and progress tracking.
64 | * @return ?Validation An object describing the validation job.
65 | * @throws VerifaliaException
66 | */
67 | public function submit($entries, WaitOptions $waitOptions = null)
68 | {
69 | // Builds the input json structure
70 |
71 | $jsonData = array();
72 |
73 | if ($entries instanceof FileValidationRequest) {
74 | $validation = $entries;
75 |
76 | // File-specific settings
77 |
78 | if ($validation->startingRow !== null & $validation->startingRow !== 0) {
79 | $jsonData['startingRow'] = $validation->startingRow;
80 | }
81 |
82 | if ($validation->endingRow !== null) {
83 | $jsonData['endingRow'] = $validation->endingRow;
84 | }
85 |
86 | if ($validation->column !== null & $validation->column !== 0) {
87 | $jsonData['column'] = $validation->column;
88 | }
89 |
90 | if ($validation->sheet !== null & $validation->sheet !== 0) {
91 | $jsonData['sheet'] = $validation->sheet;
92 | }
93 |
94 | if ($validation->lineEnding !== null) {
95 | $jsonData['lineEnding'] = $validation->lineEnding;
96 | }
97 | }
98 | else {
99 | if ($entries instanceof ValidationRequest) {
100 | $validation = $entries;
101 | } else {
102 | $validation = new ValidationRequest($entries);
103 | }
104 |
105 | $mappedEntries = array();
106 |
107 | foreach ($validation->entries as $entry) {
108 | $mappedEntry = array('inputData' => $entry->inputData);
109 |
110 | if ($entry->custom !== null) {
111 | $mappedEntry['custom'] = $entry->custom;
112 | }
113 |
114 | $mappedEntries[] = $mappedEntry;
115 | }
116 |
117 | $jsonData['entries'] = $mappedEntries;
118 | }
119 |
120 | // Common settings
121 |
122 | if ($validation->name !== null) {
123 | $jsonData['name'] = $validation->name;
124 | }
125 |
126 | if ($validation->deduplication !== null) {
127 | $jsonData['deduplication'] = $validation->deduplication;
128 | }
129 |
130 | if ($validation->quality !== null) {
131 | $jsonData['quality'] = $validation->quality;
132 | }
133 |
134 | if ($validation->priority !== null) {
135 | $jsonData['priority'] = $validation->priority;
136 | }
137 |
138 | if ($validation->retention !== null) {
139 | $jsonData['retention'] = ParserUtils::dateIntervalToTimeSpanString($validation->retention);
140 | }
141 |
142 | if ($validation->completionCallback !== null) {
143 | $callback = array("url" => $validation->completionCallback->url);
144 |
145 | if ($validation->completionCallback->version !== null) {
146 | $callback["version"] = $validation->completionCallback->version;
147 | }
148 |
149 | if ($validation->completionCallback->skipServerCertificateValidation !== null) {
150 | $callback["skipServerCertificateValidation"] = $validation->completionCallback->skipServerCertificateValidation;
151 | }
152 |
153 | $jsonData['callback'] = $callback;
154 | }
155 |
156 | if ($waitOptions === null) {
157 | $waitOptions = WaitOptions::$default;
158 | }
159 |
160 | // Sends the request to the Verifalia servers
161 |
162 | if ($entries instanceof FileValidationRequest) {
163 | // File imports require a multi-part form submission
164 |
165 | $invocationOptions = new InvocationOptions();
166 | $invocationOptions->requestOptions = [
167 | 'multipart' => [
168 | [
169 | 'name' => 'inputFile',
170 | 'contents' => $entries->file,
171 | 'filename' => 'dummy',
172 | 'headers' => ['Content-Type' => $entries->contentType]
173 | ],
174 | [
175 | 'name' => 'settings',
176 | 'contents' => json_encode($jsonData, JSON_FORCE_OBJECT),
177 | 'headers' => ['Content-Type' => 'application/json']
178 | ],
179 | ]
180 | ];
181 |
182 | $response = $this->restClient->invoke(
183 | MultiplexedRestClient::HTTP_METHOD_POST,
184 | "email-validations",
185 | [
186 | 'waitTime' => $waitOptions->submissionWaitTime
187 | ],
188 | null,
189 | $invocationOptions
190 | );
191 | }
192 | else {
193 | $response = $this->restClient->invoke(
194 | MultiplexedRestClient::HTTP_METHOD_POST,
195 | "email-validations",
196 | [
197 | 'waitTime' => $waitOptions->submissionWaitTime
198 | ],
199 | $jsonData
200 | );
201 | }
202 |
203 | // Handles the response
204 |
205 | $statusCode = $response->getStatusCode();
206 | $body = $response->getBody();
207 |
208 | switch ($statusCode) {
209 | case MultiplexedRestClient::HTTP_STATUS_OK:
210 | case MultiplexedRestClient::HTTP_STATUS_ACCEPTED: {
211 | $partialValidation = $this->buildPartialValidation(json_decode($body));
212 |
213 | // Returns immediately if the validation has been completed or if we should not wait for it
214 |
215 | if ($waitOptions === WaitOptions::$noWait || $partialValidation->overview->status === ValidationStatus::COMPLETED) {
216 | return $this->retrieveValidationFromPartialValidation($partialValidation);
217 | }
218 |
219 | return $this->waitValidationForCompletion($partialValidation->overview, $waitOptions);
220 | }
221 |
222 | case MultiplexedRestClient::HTTP_STATUS_PAYMENT_REQUIRED:
223 | throw new VerifaliaException("Verifalia was unable to accept your request because of low account credit. Body: $body");
224 |
225 | default:
226 | throw new VerifaliaException("Unexpected HTTP status code $statusCode. Body: $body");
227 | }
228 | }
229 |
230 | /**
231 | * Returns an email validation job that was previously submitted for processing. By default, this method **waits**
232 | * for the completion of the email validation job, if applicable. You can pass a `$waitOptions` parameter to
233 | * request a different waiting behavior.
234 | *
235 | * @param string $id The unique identifier of the validation job to retrieve.
236 | * @param ?WaitOptions $waitOptions Defines the options that specify how to wait for the completion of the email
237 | * validation job. It can be set to `null` to wait for completion using the default options provided by the SDK,
238 | * or set to an instance of `WaitOptions` for advanced scenarios and progress tracking.
239 | * @return ?Validation An object describing the validation job.
240 | * @throws VerifaliaException
241 | * @throws Exception
242 | */
243 | public function get(string $id, WaitOptions $waitOptions = null)
244 | {
245 | if ($waitOptions === null) {
246 | $waitOptions = WaitOptions::$default;
247 | }
248 |
249 | $response = $this->restClient->invoke(MultiplexedRestClient::HTTP_METHOD_GET,
250 | "email-validations/$id",
251 | [
252 | 'waitTime' => $waitOptions->pollWaitTime
253 | ]);
254 |
255 | $statusCode = $response->getStatusCode();
256 | $body = $response->getBody();
257 |
258 | switch ($statusCode) {
259 | case MultiplexedRestClient::HTTP_STATUS_OK:
260 | case MultiplexedRestClient::HTTP_STATUS_ACCEPTED: {
261 | $partialValidation = $this->buildPartialValidation(json_decode($body));
262 |
263 | // Returns immediately if the validation has been completed or if we should not wait for it
264 |
265 | if ($waitOptions === WaitOptions::$noWait || $partialValidation->overview->status === ValidationStatus::COMPLETED) {
266 | return $this->retrieveValidationFromPartialValidation($partialValidation);
267 | }
268 |
269 | return $this->waitValidationForCompletion($partialValidation->overview, $waitOptions);
270 | }
271 |
272 | case MultiplexedRestClient::HTTP_STATUS_NOT_FOUND:
273 | case MultiplexedRestClient::HTTP_STATUS_GONE:
274 | return null;
275 |
276 | default:
277 | throw new VerifaliaException("Unexpected HTTP status code $statusCode. Body: $body");
278 | }
279 | }
280 |
281 | /**
282 | * @param string $id The unique identifier of the validation job to retrieve.
283 | * @param string $format The requested file format. See `ExportedEntriesFormat` for a list of the supported file formats.
284 | * @return string|null A binary string with the data exported in the requested format.
285 | * @see ExportedEntriesFormat for a list of the supported file formats.
286 | */
287 | public function exportEntries(string $id, string $format)
288 | {
289 | $invocationOptions = new InvocationOptions();
290 | $invocationOptions->requestOptions = [
291 | RequestOptions::HEADERS => [
292 | 'Accept' => $format
293 | ]
294 | ];
295 |
296 | $response = $this->restClient->invoke(MultiplexedRestClient::HTTP_METHOD_GET,
297 | "email-validations/$id/entries",
298 | null,
299 | null,
300 | $invocationOptions);
301 |
302 | $statusCode = $response->getStatusCode();
303 | $body = $response->getBody();
304 |
305 | switch ($statusCode) {
306 | case MultiplexedRestClient::HTTP_STATUS_OK: {
307 | return $body->getContents();
308 | }
309 |
310 | case MultiplexedRestClient::HTTP_STATUS_NOT_FOUND:
311 | case MultiplexedRestClient::HTTP_STATUS_GONE:
312 | return null;
313 |
314 | default:
315 | throw new VerifaliaException("Unexpected HTTP status code $statusCode. Body: $body");
316 | }
317 | }
318 |
319 | /**
320 | * @throws VerifaliaException
321 | */
322 | private function waitValidationForCompletion($validationOverview, WaitOptions $waitOptions)
323 | {
324 | $resultOverview = $validationOverview;
325 |
326 | do {
327 | // Fires a progress, since we are not yet completed
328 |
329 | if ($waitOptions->progress !== null) {
330 | call_user_func($waitOptions->progress, $resultOverview);
331 | }
332 |
333 | // Wait for the next polling schedule
334 |
335 | $waitOptions->waitForNextPoll($resultOverview);
336 |
337 | // Fetch the job from the API
338 |
339 | $result = $this->get($validationOverview->id, $waitOptions);
340 |
341 | if ($result === null) {
342 | // A null result means the validation has been deleted (or is expired) between a poll and the next one
343 |
344 | return null;
345 | }
346 |
347 | $resultOverview = $result->overview;
348 |
349 | // Returns immediately if the validation has been completed
350 |
351 | if ($resultOverview->status === ValidationStatus::COMPLETED) {
352 | return $result;
353 | }
354 | } while (true);
355 | }
356 |
357 | /**
358 | * @throws VerifaliaException
359 | * @throws Exception
360 | */
361 | private function retrieveValidationFromPartialValidation($partialValidation): Validation
362 | {
363 | $allEntries = array();
364 |
365 | if (property_exists($partialValidation, 'entries')) {
366 | $currentSegment = $partialValidation->entries;
367 |
368 | while ($currentSegment !== null && $currentSegment->data !== null) {
369 | foreach($currentSegment->data as $entry){
370 | $entry->index = (int) $entry->index;
371 |
372 | if (!empty($entry->completedOn))
373 | {
374 | $entry->completedOn = new DateTime($entry->completedOn);
375 | }
376 |
377 | if (!empty($entry->hasInternationalDomainName))
378 | {
379 | $entry->hasInternationalDomainName = (bool) $entry->hasInternationalDomainName;
380 | }
381 |
382 | if (!empty($entry->hasInternationalMailboxName))
383 | {
384 | $entry->hasInternationalMailboxName = (bool) $entry->hasInternationalMailboxName;
385 | }
386 |
387 | if (!empty($entry->isDisposableEmailAddress))
388 | {
389 | $entry->isDisposableEmailAddress = (bool) $entry->isDisposableEmailAddress;
390 | }
391 |
392 | if (!empty($entry->isFreeEmailAddress))
393 | {
394 | $entry->isFreeEmailAddress = (bool) $entry->isFreeEmailAddress;
395 | }
396 |
397 | if (!empty($entry->isRoleAccount))
398 | {
399 | $entry->isRoleAccount = (bool) $entry->isRoleAccount;
400 | }
401 |
402 | if (!empty($entry->syntaxFailureIndex))
403 | {
404 | $entry->syntaxFailureIndex = (int) $entry->syntaxFailureIndex;
405 | }
406 |
407 | if (!empty($entry->duplicateOf))
408 | {
409 | $entry->duplicateOf = (int) $entry->duplicateOf;
410 | }
411 |
412 | $allEntries[] = $entry;
413 | }
414 |
415 | if (!property_exists($currentSegment, 'meta') || !property_exists($currentSegment->meta, 'isTruncated') || $currentSegment->meta->isTruncated === false) {
416 | break;
417 | }
418 |
419 | $currentSegment = $this->listEntriesSegmented(
420 | $partialValidation->overview->id,
421 | new ListingCursor($currentSegment->meta->cursor)
422 | );
423 | }
424 | }
425 |
426 | return new Validation($partialValidation->overview, $allEntries);
427 | }
428 |
429 | /**
430 | * @throws VerifaliaException
431 | */
432 | private function listEntriesSegmented(string $id, ListingCursor $cursor)
433 | {
434 | // Generate the additional parameters, where needed
435 |
436 | $cursorParamName = $cursor->direction === Direction::FORWARD
437 | ? "cursor"
438 | : "cursor:prev";
439 |
440 | $query = [
441 | $cursorParamName => $cursor->cursor
442 | ];
443 |
444 | if ($cursor->limit > 0) {
445 | $query["limit"] = $cursor->limit;
446 | }
447 |
448 | $response = $this->restClient->invoke(
449 | MultiplexedRestClient::HTTP_METHOD_GET,
450 | "/email-validations/$id/entries",
451 | $query
452 | );
453 |
454 | $statusCode = $response->getStatusCode();
455 | $body = $response->getBody();
456 |
457 | if ($statusCode === MultiplexedRestClient::HTTP_STATUS_OK) {
458 | return json_decode($body)->data;
459 | }
460 |
461 | throw new VerifaliaException("Unexpected HTTP response: $statusCode $body");
462 | }
463 |
464 | /**
465 | * Deletes an email validation job that was previously submitted for processing.
466 | *
467 | * @param string $id The unique identifier of the email validation job to be deleted.
468 | * @return void
469 | * @throws VerifaliaException
470 | */
471 | public function delete(string $id)
472 | {
473 | // Sends the request to the Verifalia servers
474 |
475 | $response = $this->restClient->invoke(MultiplexedRestClient::HTTP_METHOD_DELETE, "email-validations/$id");
476 | $statusCode = $response->getStatusCode();
477 |
478 | if ($statusCode !== MultiplexedRestClient::HTTP_STATUS_OK) {
479 | $body = $response->getBody();
480 |
481 | throw new VerifaliaException("Unexpected HTTP status code $statusCode. Body: $body");
482 | }
483 | }
484 |
485 | /**
486 | * @throws Exception
487 | */
488 | private function buildPartialValidation($partialValidation)
489 | {
490 | $partialValidation->overview->createdOn = new DateTime($partialValidation->overview->createdOn);
491 |
492 | if (!empty($partialValidation->overview->submittedOn))
493 | {
494 | $partialValidation->overview->submittedOn = new DateTime($partialValidation->overview->submittedOn);
495 | }
496 |
497 | if (!empty($partialValidation->overview->completedOn))
498 | {
499 | $partialValidation->overview->completedOn = new DateTime($partialValidation->overview->completedOn);
500 | }
501 |
502 | if (!empty($partialValidation->overview->priority))
503 | {
504 | $partialValidation->overview->priority = (int) ($partialValidation->overview->priority);
505 | }
506 |
507 | $partialValidation->overview->noOfEntries = (int) ($partialValidation->overview->noOfEntries);
508 | $partialValidation->overview->retention = ParserUtils::timeSpanStringToDateInterval($partialValidation->overview->retention);
509 |
510 | if (!empty($partialValidation->overview->progress))
511 | {
512 | if (!empty($partialValidation->overview->progress->estimatedTimeRemaining))
513 | {
514 | $partialValidation->overview->progress->estimatedTimeRemaining = ParserUtils::timeSpanStringToDateInterval($partialValidation->overview->progress->estimatedTimeRemaining);
515 | }
516 | }
517 |
518 | return $partialValidation;
519 | }
520 | }
521 | }
522 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | [](http://packagist.org/packages/verifalia/sdk)
3 |
4 | Verifalia API - PHP SDK and helper library
5 | ==========================================
6 |
7 | This SDK library integrates with [Verifalia](https://verifalia.com) and allows to [verify email addresses](https://verifalia.com)
8 | in **PHP v7.0 and higher**.
9 |
10 | [Verifalia](https://verifalia.com/) is an online service that provides email verification and mailing list cleaning; it helps businesses reduce
11 | their bounce rate, protect their sender reputation, and ensure their email campaigns reach the intended recipients.
12 | Verifalia can [verify email addresses](https://verifalia.com/) in real-time and in bulk, using its API or client area; it also
13 | offers various features and settings to customize the verification process according to the user’s needs.
14 |
15 | Verifalia's email verification process consists of several steps, each taking fractions of a second: it checks the **formatting
16 | and syntax** (RFC 1123, RFC 2821, RFC 2822, RFC 3490, RFC 3696, RFC 4291, RFC 5321, RFC 5322, and RFC 5336) of each email address,
17 | the **domain and DNS records**, the **mail exchangers**, and the **mailbox existence**, with support for internationalized domains
18 | and mailboxes. It also detects risky email types, such as **catch-all**, **disposable**, or **spam traps** / **honeypots**.
19 |
20 | Verifalia provides detailed and **accurate reports** for each email verification: it categorizes each email address as `Deliverable`,
21 | `Undeliverable`, `Risky`, or `Unknown`, and assigns one of its exclusive set of over 40 [status codes](https://verifalia.com/developers#email-validations-status-codes).
22 | It also explains the undeliverability reason and provides **comprehensive verification details**. The service allows the user to choose the desired
23 | quality level, the waiting timeout, the deduplication preferences, the data retention settings, and the callback preferences
24 | for each verification.
25 |
26 | Of course, Verifalia never sends emails to the contacts or shares the user's data with anyone.
27 |
28 | To learn more about Verifalia please see [https://verifalia.com](https://verifalia.com/)
29 |
30 | ## Table of contents
31 |
32 | * [Getting started](#getting-started)
33 | * [Naming conventions](#naming-conventions-)
34 | * [Authentication](#authentication)
35 | * [Authenticating via Basic Auth](#authenticating-via-basic-auth)
36 | * [Authenticating via bearer token](#authenticating-via-bearer-token)
37 | * [Authenticating via X.509 client certificate (TLS mutual authentication)](#authenticating-via-x509-client-certificate-tls-mutual-authentication)
38 | * [Validating email addresses](#validating-email-addresses)
39 | * [How to validate / verify an email address](#how-to-validate--verify-an-email-address)
40 | * [How to validate / verify a list of email addresses](#how-to-validate--verify-a-list-of-email-addresses)
41 | * [How to import and submit a file for validation](#how-to-import-and-submit-a-file-for-validation)
42 | * [Processing options](#processing-options)
43 | * [Quality level](#quality-level)
44 | * [Deduplication mode](#deduplication-mode)
45 | * [Data retention](#data-retention)
46 | * [Wait options](#wait-options)
47 | * [Avoid waiting](#avoid-waiting)
48 | * [Progress tracking](#progress-tracking)
49 | * [Completion callbacks](#completion-callbacks)
50 | * [Retrieving jobs](#retrieving-jobs)
51 | * [Exporting email verification results in different output formats](#exporting-email-verification-results-in-different-output-formats)
52 | * [Don't forget to clean up, when you are done](#dont-forget-to-clean-up-when-you-are-done)
53 | * [Managing credits](#managing-credits-)
54 | * [Getting the credits balance](#getting-the-credits-balance-)
55 | * [Changelog / What's new](#changelog--whats-new)
56 | * [v3.1](#v31)
57 | * [v3.0](#v30)
58 |
59 | ## Getting started
60 |
61 | The most efficient way to add the Verifalia email verification library into your PHP project is by using [composer](https://getcomposer.org),
62 | which will automatically download and install the required files [from Packagist](http://packagist.org/packages/verifalia/sdk). With composer installed,
63 | run the following command from your project's root directory:
64 |
65 | ```bash
66 | php composer.phar require verifalia/sdk
67 | ```
68 |
69 | For Windows users, the alternative command to run is:
70 |
71 | ```batch
72 | composer require verifalia/sdk
73 | ```
74 |
75 | ### Naming conventions ###
76 |
77 | > This package follows the `PSR-4` convention names for its classes, meaning you can even load them easily with your own
78 | > autoloader.
79 |
80 | ### Authentication
81 |
82 | First things first: authentication to the Verifalia API is performed by way of either
83 | the credentials of your root Verifalia account or of one of your users (previously
84 | known as sub-accounts): if you don't have a Verifalia account, just [register for a free one](https://verifalia.com/sign-up). For security reasons,
85 | it is always advisable to [create and use a dedicated user](https://verifalia.com/client-area#/users/new) for accessing the API, as doing so will allow to assign
86 | only the specific needed permissions to it.
87 |
88 | Learn more about authenticating to the Verifalia API at [https://verifalia.com/developers#authentication](https://verifalia.com/developers#authentication)
89 |
90 | #### Authenticating via Basic Auth
91 |
92 | The most straightforward method for authenticating against the Verifalia API involves using a username and password pair.
93 | These credentials can be applied during the creation of a new instance of the `VerifaliaRestClient` class, serving as the
94 | initial step for all interactions with the Verifalia API: the provided username and password will be automatically
95 | transmitted to the API using the HTTP Basic Auth method.
96 |
97 | ```php
98 | use Verifalia\VerifaliaRestClient;
99 | use Verifalia\VerifaliaRestClientOptions;
100 |
101 | $verifalia = new VerifaliaRestClient([
102 | VerifaliaRestClientOptions::USERNAME => 'your-username-here',
103 | VerifaliaRestClientOptions::PASSWORD => 'your-password-here'
104 | ]);
105 | ```
106 |
107 | #### Authenticating via bearer token
108 |
109 | Bearer authentication offers higher security over HTTP Basic Auth, as the latter requires sending the actual credentials
110 | on each API call, while the former only requires it on a first, dedicated authentication request. On the other side, the
111 | first authentication request needed by Bearer authentication takes a non-negligible time.
112 |
113 | > ⚠️ If you need to perform only a single request, **using HTTP Basic Auth** (see above) **provides the same degree of security and is
114 | > also faster**.
115 |
116 | ```php
117 | use Verifalia\VerifaliaRestClient;
118 | use Verifalia\VerifaliaRestClientOptions;
119 | use Verifalia\Security\BearerAuthenticationProvider;
120 |
121 | $verifalia = new VerifaliaRestClient([
122 | VerifaliaRestClientOptions::AUTHENTICATION_PROVIDER =>
123 | new BearerAuthenticationProvider('your-username-here', 'your-password-here')
124 | ]);
125 | ```
126 |
127 | #### Authenticating via X.509 client certificate (TLS mutual authentication)
128 |
129 | In addition to the aforementioned authentication methods, this SDK also supports using a cryptographic X.509 client
130 | certificate to authenticate against the Verifalia API, through the TLS protocol. This method, also
131 | called mutual TLS authentication (mTLS) or two-way authentication, offers the highest degree of
132 | security, as only a cryptographically-derived key (and not the actual credentials) is sent over
133 | the wire on each request. [What is X.509 TLS client-certificate authentication?](https://verifalia.com/help/sub-accounts/what-is-x509-tls-client-certificate-authentication)
134 |
135 | ```php
136 | use Verifalia\VerifaliaRestClient;
137 | use Verifalia\VerifaliaRestClientOptions;
138 |
139 | $verifalia = new VerifaliaRestClient([
140 | VerifaliaRestClientOptions::CERTIFICATE => '/home/gfring/Documents/pollos.pem'
141 | ]);
142 | ```
143 |
144 | ## Validating email addresses
145 |
146 | Every operation related to verifying / validating email addresses is performed through the `emailValidations` field
147 | exposed by the instance of the `VerifaliaRestClient` class you created above. The property exposes some useful functions:
148 | in the next few paragraphs we are looking at the most used ones, so it is strongly advisable to explore the library and
149 | look at the embedded help for other opportunities.
150 |
151 | **The library automatically waits for the completion of email verification jobs**: if needed, it is possible to adjust
152 | the wait options and have more control over the entire underlying polling process. Please refer to the [Wait options](#wait-options)
153 | section below for additional details.
154 |
155 | ### How to validate / verify an email address
156 |
157 | To validate an email address from a PHP application you can invoke the `submit()` method: it accepts one or more email
158 | addresses and any eventual verification options you wish to pass to Verifalia, including the expected results quality,
159 | deduplication preferences, processing priority.
160 |
161 | > Note In the event you need to verify a list of email addresses, it is advisable to submit them all at once through the
162 | > `submit()` method (see the next sections), instead of iterating over the source set and submitting the addresses one
163 | > by one. Not only the all-at-once method would be faster, it would also allow to detect and mark duplicated items - a
164 | > feature which is unavailable while verifying the email addresses one by one.
165 |
166 | In the following example, we verify an email address with this library, using the default options:
167 |
168 | ```php
169 | use Verifalia\VerifaliaRestClient;
170 |
171 | $verifalia = new VerifaliaRestClient(...); // See above
172 |
173 | // Verifies an email address
174 |
175 | $job = $verifalia->emailValidations->submit('batman@gmail.com');
176 |
177 | // Print some results
178 |
179 | $entry = $job->entries[0];
180 |
181 | echo 'Classification: ' . $entry->classification;
182 | echo 'Status: ' . $entry->status;
183 |
184 | // Output:
185 | // Classification: Deliverable
186 | // Status: Success
187 | ```
188 |
189 | Once `submit()` completes successfully, the resulting verification job
190 | is guaranteed to be completed and its results' data (e.g. its `entries` field) to be available for use.
191 |
192 | As you may expect, each entry may include various additional details about the verified email address:
193 |
194 | | Attribute | Description |
195 | |-------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
196 | | `asciiEmailAddressDomainPart` | Gets the domain part of the email address, converted to ASCII if needed and with comments and folding white spaces stripped off. |
197 | | `classification` | A string with the classification for this entry; see the `ValidationEntyClassification` class for a list of the values supported at the time this SDK has been released. |
198 | | `completedOn` | The date this entry has been completed, if available. |
199 | | `custom` | A custom, optional string which is passed back upon completing the validation. To pass back and forth a custom value, use the `custom` field of `ValidationRequestEntry`. |
200 | | `duplicateOf` | The zero-based index of the first occurrence of this email address in the parent `Validation`, in the event the `status` field for this entry is `Duplicate`; duplicated items do not expose any result detail apart from this and the eventual `custom` values. |
201 | | `index` | The index of this entry within its `Validation` container; this property is mostly useful in the event the API returns a filtered view of the items. |
202 | | `inputData` | The input string being validated. |
203 | | `emailAddress` | Gets the email address, without any eventual comment or folding white space. Returns null if the input data is not a syntactically invalid e-mail address. |
204 | | `emailAddressDomainPart` | Gets the domain part of the email address, without comments and folding white spaces. |
205 | | `emailAddressLocalPart` | Gets the local part of the email address, without comments and folding white spaces. |
206 | | `hasInternationalDomainName` | If true, the email address has an international domain name. |
207 | | `hasInternationalMailboxName` | If true, the email address has an international mailbox name. |
208 | | `isDisposableEmailAddress` | If true, the email address comes from a disposable email address (DEA) provider. What is a disposable email address? |
209 | | `isFreeEmailAddress` | If true, the email address comes from a free email address provider (e.g. gmail, yahoo, outlook / hotmail, ...). |
210 | | `isRoleAccount` | If true, the local part of the email address is a well-known role account. |
211 | | `status` | The status for this entry; see the `ValidationEntryStatus` class for a list of the values supported at the time this SDK has been released. |
212 | | `suggestions` | The potential corrections for the input data, in the event Verifalia identified potential typos during the verification process. |
213 | | `syntaxFailureIndex` | The position of the character in the email address that eventually caused the syntax validation to fail. |
214 |
215 | Here is another example, showing some of the additional result details provided by Verifalia:
216 |
217 | ```php
218 | use Verifalia\VerifaliaRestClient;
219 |
220 | $verifalia = new VerifaliaRestClient(...); // See above
221 |
222 | // Verifies an email address
223 |
224 | $job = $verifalia->emailValidations->submit('bat[man@gmal.com');
225 |
226 | // Print some results
227 |
228 | $entry = $job->entries[0];
229 |
230 | echo 'Classification: ' . $entry->classification . "\n";
231 | echo 'Status: ' . $entry->status . "\n";
232 | echo 'Syntax failure index: ' . $entry->syntaxFailureIndex . "\n";
233 |
234 | if (!empty(entry->suggestions)) {
235 | echo "Suggestions\n";
236 |
237 | foreach ($entry->suggestions as $suggestion) {
238 | echo '- ' . $suggestion . "\n";
239 | }
240 | }
241 |
242 | // Output:
243 | // Classification: Undeliverable
244 | // Status: InvalidCharacterInSequence
245 | // Syntax failure index: 3
246 | // Suggestions:
247 | // - batman@gmail.com
248 | ```
249 |
250 | ### How to validate / verify a list of email addresses
251 |
252 | To verify a list of email addresses you can still call the `submit()` function, which also accepts an array of strings
253 | with the email addresses to verify:
254 |
255 | ```php
256 | use Verifalia\VerifaliaRestClient;
257 |
258 | $verifalia = new VerifaliaRestClient(...); // See above
259 |
260 | // Verifies the list of email addresses
261 |
262 | $job = $verifalia->emailValidations->submit([
263 | 'batman@gmail.com',
264 | 'steve.vai@best.music',
265 | 'samantha42@yahoo.de',
266 | ]);
267 |
268 | // Print some results
269 |
270 | foreach ($job->entries as $entry) {
271 | echo $entry->emailAddress . ' => ' . $entry->classification;
272 | }
273 |
274 | // Output:
275 | // batman@gmail.com => Deliverable
276 | // steve.vai@best.music => Undeliverable
277 | // samantha42@yahoo.de => Deliverable
278 | ```
279 |
280 | ### How to import and submit a file for validation
281 |
282 | This library includes support for submitting and validating files with email addresses, including:
283 |
284 | - **plain text files** (.txt), with one email address per line;
285 | - **comma-separated values** (.csv), **tab-separated values** (.tsv) and other delimiter-separated values files;
286 | - **Microsoft Excel spreadsheets** (.xls and .xlsx).
287 |
288 | To submit and validate files, one can still use the `submit()` function mentioned
289 | above, passing either the full name of the file to submit to Verifalia or a resource stream. Along with that, it is also possible to specify the eventual starting
290 | and ending rows to process, the column, the sheet index, the line ending and the
291 | delimiter - depending of course on the nature of the submitted file (see
292 | `FileValidationRequest` in the source to learn more).
293 |
294 | Here is how to submit and verify an Excel file, for example:
295 |
296 | ```php
297 | $job = $verifalia->emailValidations->submit(new FileValidationRequest('that-file.xlsx'));
298 | ```
299 |
300 | For more advanced options, just set the relevant properties in the `FileValidationRequest` instance:
301 |
302 | ```php
303 | $request = new FileValidationRequest('that-file.xlsx');
304 | $request->sheet 3;
305 | $request->startingRow = 1;
306 | $request->column = 5;
307 | $request->quality = QualityLevelName::HIGH;
308 |
309 | $job = $verifalia->emailValidations->submit($request);
310 | ```
311 |
312 | And here is another example, showing how to submit a resource stream instance and specifying the
313 | MIME content type of the file, which is automatically determined from the file extension in
314 | the event you pass file name instead:
315 |
316 | ```php
317 | $stream = fopen('my-list.txt', 'rb');
318 | $job = $verifalia->emailValidations->submit(new FileValidationRequest($stream, 'text/plain'));
319 | ```
320 |
321 | ### Processing options
322 |
323 | While submitting one or more email addresses for verification, it is possible to specify several
324 | options which affect the behavior of the Verifalia processing engine as well as the verification flow
325 | from the API consumer standpoint.
326 |
327 | #### Quality level
328 |
329 | Verifalia offers three distinct quality levels - namely, _Standard_, _High_ and _Extreme_ - which rule out how the email
330 | verification engine should deal with temporary undeliverability issues, with slower mail exchangers and other potentially transient
331 | problems which can affect the quality of the verification results. The `ValidationRequest` class accepts a `quality` field which allows
332 | to specify the desired quality level; here is an example showing how to verify an email address using
333 | the _High_ quality level:
334 |
335 | ```php
336 | $request = new ValidationRequest('batman@gmail.com');
337 | $request->quality = QualityLevelName::HIGH;
338 |
339 | $job = $verifalia->emailValidations->submit($request);
340 | ```
341 |
342 | #### Deduplication mode
343 |
344 | The `submit()` method can also accept and verify multiple email addresses in bulk, and allows to specify how to
345 | deal with duplicated entries pertaining to the same input set; Verifalia supports a _Safe_ deduplication
346 | mode, which strongly adheres to the old IETF standards, and a _Relaxed_ mode which is more in line with
347 | what can be found in the majority of today's mail exchangers configurations.
348 |
349 | In the next example, we show how to import and verify a list of email addresses and mark duplicated
350 | entries using the _Relaxed_ deduplication mode:
351 |
352 | ```php
353 | $request = new ValidationRequest([
354 | 'batman@gmail.com',
355 | 'steve.vai@best.music',
356 | 'samantha42@yahoo.de',
357 | ]);
358 | $request->deduplication = DeduplicationMode::RELAXED;
359 |
360 | $job = $verifalia->emailValidations->submit($request);
361 | ```
362 |
363 | #### Data retention
364 |
365 | Verifalia automatically deletes completed email verification jobs according to the data retention
366 | policy defined at the account level, which can be eventually overridden at the user level: one can
367 | use the [Verifalia clients area](https://verifalia.com/client-area) to configure these settings.
368 |
369 | It is also possible to specify a per-job data retention policy which govern the time to live of a submitted
370 | email verification job; to do that, set the `retention` field of the `ValidationRequest` instance accordingly.
371 |
372 | Here is how, for instance, one can set a data retention policy of 10 minutes while verifying
373 | an email address:
374 |
375 | ```php
376 | $request = new ValidationRequest('batman@gmail.com');
377 | $request->retention = DateInterval::createFromDateString('10 minutes');
378 |
379 | $job = $verifalia->emailValidations->submit($request);
380 | ```
381 |
382 | ### Wait options
383 |
384 | **By default, the `submit()` method submits an email verification job to Verifalia and waits
385 | for its completion**; the entire process may require some time to complete depending on the plan of the
386 | Verifalia account, the number of email addresses the submission contains, the specified quality level
387 | and other network factors including the latency of the mail exchangers under test.
388 |
389 | In waiting for the completion of a given email verification job, the library automatically polls the
390 | underlying Verifalia API until the results are ready; by default, it tries to take advantage of the long
391 | polling mode introduced with the Verifalia API v2.4, which allows to minimize the number of requests
392 | and get the verification results faster.
393 |
394 | #### Avoid waiting
395 |
396 | In certain scenarios (in a microservice architecture, for example), however, it may be preferable to avoid
397 | waiting for a job completion and ask the Verifalia API, instead, to just queue it: in that case, the library
398 | would just return the job overview (and not its verification results) and it will be necessary to retrieve
399 | the verification results using the `get()` method.
400 |
401 | To do that, it is possible to specify `WaitOptions::$noWait` as the value for the `waitOptions` parameter
402 | of the `submit()` method, as shown in the next example:
403 |
404 | ```php
405 | $verifalia = new VerifaliaRestClient(...); // See above
406 | $request = new ValidationRequest(...) // See above;
407 |
408 | $job = $verifalia->emailValidations->submit($request, WaitOptions::$noWait);
409 |
410 | echo 'Status: ' . $job->overview->status;
411 |
412 | // Status: InProgress
413 | ```
414 |
415 | #### Progress tracking
416 |
417 | For jobs with a large number of email addresses, it could be useful to track progress as they are processed
418 | by the Verifalia email verification engine; to do that, it is possible to create an instance of the
419 | `WaitOptions` class and provide a callable which eventually receives progress notifications through the
420 | `progress` field.
421 |
422 | Here is how to define a progress notification handler which displays the progress percentage of a submitted
423 | job to the console window:
424 |
425 | ```php
426 | use Verifalia\EmailValidations\ValidationOverview;
427 | use Verifalia\EmailValidations\WaitOptions;
428 |
429 | $verifalia = new VerifaliaRestClient(...); // See above
430 | $request = new ValidationRequest(...) // See above;
431 |
432 | $waitOptions = new WaitOptions(function (ValidationOverview $overview) {
433 | echo 'Job status: ' . $overview->status;
434 |
435 | if ($overview->progress !== null) {
436 | echo 'Progress: ' . $overview->progress->percentage . '%';
437 | }
438 | });
439 |
440 | $job = $verifalia->emailValidations->submit($request, $waitOptions);
441 | ```
442 |
443 | ### Completion callbacks
444 |
445 | Along with each email validation job, it is possible to specify a URL which
446 | Verifalia will invoke (POST) once the job completes: this URL must use the HTTPS or HTTP
447 | scheme and be publicly accessible over the Internet.
448 | To learn more about completion callbacks, please see https://verifalia.com/developers#email-validations-completion-callback
449 |
450 | To specify a completion callback URL, pass a `ValidationRequest` instance to the `submit()` method and set its `completionCallback`
451 | field accordingly, as shown in the example below:
452 |
453 | ```php
454 | $verifalia = new VerifaliaRestClient(...); // See above
455 | $request = new ValidationRequest(...) // See above;
456 |
457 | $request->completionCallback = new CompletionCallback('https://your-website-here/foo/bar');
458 |
459 | $job = $verifalia->emailValidations->submit($request, $waitOptions);
460 | ```
461 |
462 | Note that completion callbacks are invoked asynchronously, and it could take up to several seconds for your callback URL
463 | to get invoked.
464 |
465 | ### Retrieving jobs
466 |
467 | It is possible to retrieve a job through the `get()` method, which returns a `Validation` instance for the desired email
468 | verification job. While doing that, the library automatically waits for the completion of the job, and it is possible to
469 | adjust this behavior by passing to the aforementioned methods a `waitOptions` parameter, in the exactly same fashion as
470 | described for the `submit()` method; please see the [Wait options](#wait-options) section for additional details.
471 |
472 | Here is an example showing how to retrieve a job, given its identifier:
473 |
474 | ```php
475 | $job = $verifalia->emailValidations->get('ec415ecd-0d0b-49c4-a5f0-f35c182e40ea');
476 | ```
477 |
478 | ### Exporting email verification results in different output formats
479 |
480 | This library also allows to export the entries of a completed email validation
481 | job in different output formats through the `exportEntries()` function, with the goal of generating a human-readable representation
482 | of the verification results.
483 |
484 | > **WARNING**: While the output schema (columns / labels / data format) is fairly
485 | > complete, you should always consider it as subject to change: use the `get()`
486 | > function instead if you need to rely on a stable output schema.
487 |
488 | Here is an example showing how to export a given email verification job as a comma-separated values (CSV) file:
489 |
490 | ```php
491 | // Exports the validated entries for the job in the CSV format
492 |
493 | $export = $verifalia->emailValidations->exportEntries('722c2fd8-8837-449f-ad24-0330c597c993', ExportedEntriesFormat::CSV);
494 |
495 | // Stores the binary string into a file
496 |
497 | file_put_contents("my-list.csv", $export);
498 | ```
499 |
500 | ### Don't forget to clean up, when you are done
501 |
502 | Verifalia automatically deletes completed jobs after a configurable
503 | data-retention policy (see the related section) but it is strongly advisable that
504 | you delete your completed jobs as soon as possible, for privacy and security reasons. To do that, you can invoke the
505 | `delete()` method passing the job Id you wish to get rid of:
506 |
507 | ```php
508 | $verifalia->emailValidations->delete('ec415ecd-0d0b-49c4-a5f0-f35c182e40ea');
509 | ```
510 |
511 | Once deleted, a job is gone and there is no way to retrieve its email validation results.
512 |
513 | ## Managing credits ##
514 |
515 | To manage the Verifalia credits for your account you can use the `credits` property exposed by the `VerifaliaRestClient`
516 | instance created above.
517 |
518 | ### Getting the credits balance ###
519 |
520 | One of the most common tasks you may need to perform on your account is retrieving the available number of free daily
521 | credits and credit packs. To do that, you can use the `getBalance()` method, which returns a `Balance` object, as shown
522 | in the next example:
523 |
524 | ```php
525 | $balance = $verifalia
526 | ->credits
527 | ->getBalance();
528 |
529 | echo 'Credit packs: ' . $balance->creditPacks . "\n";
530 | echo 'Free daily credits: ' . $balance->freeCredits . "\n";
531 | echo 'Free daily credits will reset in ' . $balance->freeCreditsResetIn . "\n";
532 |
533 | // Prints out something like:
534 | // Credit packs: 956.332
535 | // Free daily credits: 128.66
536 | // Free daily credits will reset in 09:08:23
537 | ```
538 |
539 | To add credit packs to your Verifalia account visit [https://verifalia.com/client-area#/credits/add](https://verifalia.com/client-area#/credits/add).
540 |
541 | ## Changelog / What's new
542 |
543 | This section lists the changelog for the current major version of the library: for older versions,
544 | please see the [project releases](https://github.com/verifalia/verifalia-php-sdk/releases).
545 |
546 | ### v3.1
547 |
548 | Released on November 13th, 2024
549 |
550 | - Added support for importing and checking mailing list files in multiple file formats, including CSV, TSV, Excel (.xls and .xlsx) and plain text files (.txt)
551 | - Added support for exporting completed email verification entries in multiple file formats, including CSV, TSV and Excel (.xls and .xlsx)
552 | - Extended support for GuzzleHTTP to version 7.*
553 | - Fixed an issue with the parsing of the waiting time hint while waiting for job completion
554 | - Improved documentation
555 |
556 | ### v3.0
557 |
558 | Released on February 1st, 2024
559 |
560 | - Added support for Verifalia API v2.5
561 | - Added support for classification override rules
562 | - Added support for AI-powered suggestions
563 | - Added support for client-certificate authentication
564 | - Added support for bearer authentication
565 | - Added support for completion callbacks
566 | - Added support for submission and polling wait time
567 | - Added PHPDoc annotations
568 | - Improved sleeping time coercion while waiting for job completion
569 | - Breaking change: minimum PHP version requirement increased to **PHP 7.0**
570 | - Breaking change: the `submit()` method now, by default, waits for the email verification job to complete
571 | - Breaking change: renamed `IAuthenticator` interface to `AuthenticationProvider`
--------------------------------------------------------------------------------