Туториали

Clean Newsletter Imports: Native PHP 8.3 Cleans Emails, Flags Uncertains for Review

Чист увоз на билтени: Native PHP 8.3 ги чисти е-поштите, ги означува неизвесните за преглед

Managing a newsletter list is a fundamental task for many online businesses and creators. However, the quality of your contact data directly impacts your deliverability rates, sender reputation, and overall marketing effectiveness. Dirty data, such as mistyped email addresses or defunct domains, can lead to bounces, spam complaints, and wasted resources. This tutorial demonstrates how to leverage a powerful email validation API within a native PHP 8.3+ application to clean incoming newsletter imports and intelligently flag uncertain addresses for manual review, ensuring a more robust and reliable contact database.

Streamlining Newsletter Imports with Advanced Email Validation

As your audience grows, so does the volume of new subscribers. Manually vetting each email address is impractical. Automating this process with a sophisticated email validation service can save significant time and prevent common pitfalls. We'll integrate an API that goes beyond simple syntax checks, examining domain health, MX records, and even provider signals to offer a comprehensive risk assessment for each email address. This allows us to make informed decisions about which contacts to accept immediately, which to flag for a closer look, and which to reject outright.

The Mihajlo AI Email Validator Service

The service we'll be using for this integration is the Mihajlo AI Email Validator. It's designed to provide deep insights into email address validity, helping to reduce bounces and improve engagement. The API offers a free tier, making it accessible for development and small-scale projects, with scalable paid plans for higher volumes.

Getting Access to the Email Validator API

To begin using the Email Validator API, you'll need to register for an account and obtain a service token. Follow these steps:

It's crucial to store this service token securely. For production applications, use environment variables. Never commit your actual token directly into your codebase. For this tutorial, we'll assume you'll store it in a .env file.

API Endpoint and Authentication

The API endpoint for checking an email address is:

GET https://ai.mihajlo.mk/api/email-validator/v1/check-email

Authentication is handled via a query parameter:

token={serviceToken}

The primary request parameter is:

  • email: The email address to validate.

The response structure contains key fields such as status, score, recommendation, checks, and quota, which we will use to make application decisions.

Project Setup and Architecture

We will build a command-line application using native PHP 8.3+ to simulate processing a CSV import file. The architecture will involve:

  • A configuration file to store the API token.
  • A dedicated service class to encapsulate the API interaction.
  • A command-line script to orchestrate the import process.
  • A simple CSV file representing the newsletter import.

This approach keeps the integration focused and easy to understand, avoiding unnecessary framework dependencies for this specific task.

Project Structure

Let's set up a basic project directory:

newsletter-importer/
├── config/
│   └── api.php
├── src/
│   ├── Api/
│   │   └── EmailValidatorService.php
│   └── Console/
│       └── ImportNewsletterCommand.php
├── vendor/
├── .env
├── composer.json
└── import_contacts.csv

Environment Configuration

Create a .env file in the root of your project:

EMAIL_VALIDATOR_API_TOKEN=YOUR_MIHAJLO_API_TOKEN

Replace YOUR_MIHAJLO_API_TOKEN with the actual token you copied from the Mihajlo API documentation.

API Configuration

Create config/api.php to load the API token:

<?php

declare(strict_types=1);

function getenv_string(string $key, string $default = ''): string
{
    $value = getenv($key);
    if ($value === false || $value === '') {
        return $default;
    }
    return $value;
}

return [
    'mihajlo_email_validator' => [
        'api_token' => getenv_string('EMAIL_VALIDATOR_API_TOKEN'),
        'api_url' => 'https://ai.mihajlo.mk/api/email-validator/v1/check-email',
        'timeout' => 10.0, // Connection and read timeout in seconds
    ],
];

Email Validator Service Class

This class will handle all communication with the Mihajlo AI Email Validator API. We'll use PHP's built-in cURL extension for making HTTP requests, which is a standard and reliable choice for native PHP applications. We'll implement bounded timeouts and basic error handling.

Create src/Api/EmailValidatorService.php:

<?php

declare(strict_types=1);

namespace App\Api;

use Exception;
use InvalidArgumentException;

class EmailValidatorService
{
    private string $apiUrl;
    private string $apiToken;
    private float $timeout;

    public function __construct(string $apiUrl, string $apiToken, float $timeout = 10.0)
    {
        if (empty($apiToken)) {
            throw new InvalidArgumentException('Email validation API token is not configured.');
        }
        $this->apiUrl = $apiUrl;
        $this->apiToken = $apiToken;
        $this->timeout = $timeout;
    }

    /**
     * Checks a single email address using the Mihajlo AI Email Validator API.
     *
     * @param string $email The email address to validate.
     * @return array|null The validation result array, or null on critical failure.
     * @throws Exception If an unrecoverable API communication error occurs.
     */
    public function checkEmail(string $email): ?array
    {
        if (empty($email)) {
            return null; // Or throw an exception, depending on desired strictness
        }

        $url = $this->apiUrl . '?email=' . urlencode($email) . '&token=' . urlencode($this->apiToken);

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, (int) $this->timeout);
        curl_setopt($ch, CURLOPT_TIMEOUT, (int) $this->timeout);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Accept: application/json',
        ]);
        curl_setopt($ch, CURLOPT_FAILONERROR, true); // Fail on HTTP errors >= 400

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = curl_error($ch);
        curl_close($ch);

        if ($response === false) {
            // Log this error for observability
            error_log("Email validation API communication error: {$curlError} for email {$email}");
            throw new Exception("Failed to communicate with email validation service: {$curlError}");
        }

        if ($httpCode >= 400) {
            // Log this error for observability
            error_log("Email validation API returned HTTP error {$httpCode} for email {$email}. Response: {$response}");
            // Depending on the error code, you might want to retry or flag for review
            // For simplicity, we'll treat it as a critical failure to validate for now.
            throw new Exception("Email validation service returned an error (HTTP {$httpCode}) for email {$email}.");
        }

        $data = json_decode($response, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            // Log this error for observability
            error_log("Email validation API returned invalid JSON for email {$email}. Response: {$response}");
            throw new Exception("Invalid JSON response from email validation service for email {$email}.");
        }

        // Basic validation of the response structure
        if (!isset($data['status']) || !isset($data['score']) || !isset($data['recommendation'])) {
            // Log this error for observability
            error_log("Email validation API response missing required keys for email {$email}. Response: " . print_r($data, true));
            throw new Exception("Unexpected response structure from email validation service for email {$email}.");
        }

        return $data;
    }
}

Import Command-Line Script

This script will read a CSV file, process each email address using the EmailValidatorService, and categorize contacts based on the validation results. We'll simulate storing valid emails and flagging uncertain ones.

Create src/Console/ImportNewsletterCommand.php:

<?php

declare(strict_types=1);

namespace App\Console;

use App\Api\EmailValidatorService;
use Exception;
use RuntimeException;

class ImportNewsletterCommand
{
    private EmailValidatorService $emailValidatorService;
    private array $config;

    public function __construct(EmailValidatorService $emailValidatorService, array $config)
    {
        $this->emailValidatorService = $emailValidatorService;
        $this->config = $config;
    }

    public function run(string $csvFilePath): void
    {
        if (!file_exists($csvFilePath) || !is_readable($csvFilePath)) {
            throw new RuntimeException("CSV file not found or not readable: {$csvFilePath}");
        }

        $handle = fopen($csvFilePath, 'r');
        if ($handle === false) {
            throw new RuntimeException("Could not open CSV file for reading: {$csvFilePath}");
        }

        $headers = fgetcsv($handle);
        if ($headers === false) {
            fclose($handle);
            throw new RuntimeException("Could not read CSV headers from: {$csvFilePath}");
        }

        // Find the column index for the email address
        $emailColumnIndex = array_search('email', array_map('strtolower', $headers));
        if ($emailColumnIndex === false) {
            fclose($handle);
            throw new RuntimeException("CSV file must contain an 'email' column.");
        }

        $validContacts = [];
        $uncertainContacts = [];
        $invalidContacts = [];
        $processedCount = 0;
        $errorCount = 0;

        echo "Starting newsletter import from {$csvFilePath}...\n";

        while (($row = fgetcsv($handle)) !== false) {
            $processedCount++;
            if (!isset($row[$emailColumnIndex])) {
                echo "Skipping row {$processedCount}: missing email value.\n";
                $invalidContacts[] = ['row' => $processedCount, 'reason' => 'Missing email value'];
                continue;
            }

            $email = trim($row[$emailColumnIndex]);

            if ($email === '') {
                echo "Skipping row {$processedCount}: empty email value.\n";
                $invalidContacts[] = ['row' => $processedCount, 'reason' => 'Empty email value'];
                continue;
            }

            try {
                $validationResult = $this->emailValidatorService->checkEmail($email);

                if ($validationResult === null) {
                    // This can happen if the email was empty after trim, or if checkEmail returned null for some reason
                    echo "Row {$processedCount}: Email '{$email}' is empty or invalid after trimming. Skipping.\n";
                    $invalidContacts[] = ['row' => $processedCount, 'email' => $email, 'reason' => 'Empty after trim'];
                    continue;
                }

                $recommendation = $validationResult['recommendation'] ?? 'unknown';

                // Decision logic based on recommendation
                switch ($recommendation) {
                    case 'deliverable':
                        $validContacts[] = ['email' => $email, 'details' => $validationResult];
                        echo "Row {$processedCount}: Email '{$email}' is deliverable.\n";
                        break;
                    case 'uncertain':
                        $uncertainContacts[] = ['email' => $email, 'details' => $validationResult];
                        echo "Row {$processedCount}: Email '{$email}' is uncertain. Flagging for review.\n";
                        break;
                    case 'undeliverable':
                    default: // Includes 'invalid', 'risky', etc.
                        $invalidContacts[] = ['email' => $email, 'details' => $validationResult];
                        echo "Row {$processedCount}: Email '{$email}' is undeliverable or invalid. Discarding.\n";
                        break;
                }

            } catch (Exception $e) {
                $errorCount++;
                // Log the exception for deeper analysis
                error_log("Error processing email '{$email}' at row {$processedCount}: {$e->getMessage()}");
                echo "Row {$processedCount}: Error validating email '{$email}'. Reason: {$e->getMessage()}. Logging for review.\n";
                // We can treat API errors as uncertain or simply log and skip based on policy
                $uncertainContacts[] = ['email' => $email, 'details' => ['error' => $e->getMessage()]];
            }
        }

        fclose($handle);

        echo "\n--- Import Summary ---\n";
        echo "Processed Rows: {$processedCount}\n";
        echo "Valid Contacts: " . count($validContacts) . "\n";
        echo "Uncertain Contacts (for review): " . count($uncertainContacts) . "\n";
        echo "Invalid/Discarded Contacts: " . count($invalidContacts) . "\n";
        if ($errorCount > 0) {
            echo "API/Processing Errors: {$errorCount}\n";
        }
        echo "----------------------\n";

        // In a real application, you would now:
        // 1. Insert $validContacts into your primary contact database.
        // 2. Store $uncertainContacts in a separate table or queue for manual review.
        // 3. Log $invalidContacts for auditing or potential cleanup.
        // 4. Handle quota limits if the API response indicates them.

        if (!empty($uncertainContacts)) {
            echo "\nUncertain contacts to review:\n";
            foreach ($uncertainContacts as $contact) {
                echo "- Email: " . ($contact['email'] ?? 'N/A') . "\n";
                if (isset($contact['details']['error'])) {
                    echo "  Reason: API error - " . $contact['details']['error'] . "\n";
                } else {
                    echo "  Details: Status=" . ($contact['details']['status'] ?? 'N/A')
                         . ", Score=" . ($contact['details']['score'] ?? 'N/A')
                         . ", Recommendation=" . ($contact['details']['recommendation'] ?? 'N/A') . "\n";
                }
            }
        }
    }
}

Composer and Autoloader Setup

We need composer.json to manage our dependencies (though for this basic example, we're relying on PHP's built-in cURL). We'll set up PSR-4 autoloading.

Create composer.json:

{
    "name": "mihajlo/newsletter-importer",
    "description": "Newsletter contact import cleaner using Email Validator API",
    "type": "project",
    "license": "MIT",
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "require": {
        "php": "^8.3"
    },
    "require-dev": {
        "phpunit/phpunit": "^10.0"
    },
    "scripts": {
        "import": "php bin/import-newsletter.php"
    }
}

Run composer install in your project root.

Bootstrap Script

Create a bootstrap script (e.g., bin/import-newsletter.php) to initialize the application, load configuration, and run the command.

<?php

declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

// Load environment variables from .env file
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/../');
$dotenv->load();

// Load configuration
$config = require __DIR__ . '/../config/api.php';

// Instantiate the Email Validator Service
try {
    $emailValidatorService = new App\Api\EmailValidatorService(
        $config['mihajlo_email_validator']['api_url'],
        $config['mihajlo_email_validator']['api_token'],
        $config['mihajlo_email_validator']['timeout']
    );
} catch (InvalidArgumentException $e) {
    echo "Configuration Error: " . $e->getMessage() . "\n";
    exit(1);
}

// Instantiate the Import Command
$importCommand = new App\Console\ImportNewsletterCommand($emailValidatorService, $config);

// --- Define the input CSV file path ---
// In a real app, this might come from CLI arguments or a web form
$csvFilePath = __DIR__ . '/../import_contacts.csv';

// --- Run the import process ---
try {
    $importCommand->run($csvFilePath);
    exit(0); // Success
} catch (RuntimeException $e) {
    echo "Import Error: " . $e->getMessage() . "\n";
    exit(1); // Failure
} catch (Exception $e) {
    echo "An unexpected error occurred: " . $e->getMessage() . "\n";
    exit(1); // Failure
}

Sample Import File

Create a sample CSV file named import_contacts.csv in your project root:

Name,Email,SubscriptionDate
Alice Smith,[email protected],2023-10-26
Bob Johnson,[email protected],2023-10-26
Charlie Brown,[email protected],2023-10-26
David Lee,[email protected],2023-10-26
Eve Adams,[email protected],2023-10-26
Frank White,[email protected],2023-10-26
Grace Hall,[email protected],2023-10-26
Henry Black,[email protected],2023-10-26
Ivy Green,[email protected],2023-10-26
Jack Blue,[email protected],2023-10-26
Bad Email,not-an-email-address,2023-10-26
Empty Email,,2023-10-26
Test User,[email protected],2023-10-26
Another Bad,[email protected],2023-10-26
Uncertain User,[email protected],2023-10-26
Syntax Error,malformed-email@,2023-10-26

Running the Import

Now you can run the import process from your project's root directory using Composer's script alias:

composer import

You should see output similar to this, indicating which emails are deliverable, uncertain, or invalid:

Loading environment variables from .env file...
Starting newsletter import from /path/to/your/project/import_contacts.csv...
Row 1: Email '[email protected]' is deliverable.
Row 2: Email '[email protected]' is undeliverable or invalid. Discarding.
Row 3: Email '[email protected]' is deliverable.
Row 4: Email '[email protected]' is deliverable.
Row 5: Email '[email protected]' is deliverable.
Row 6: Email '[email protected]' is uncertain. Flagging for review.
Row 7: Email '[email protected]' is deliverable.
Row 8: Email '[email protected]' is deliverable.
Row 9: Email '[email protected]' is deliverable.
Row 10: Email '[email protected]' is deliverable.
Row 11: Email 'not-an-email-address' is undeliverable or invalid. Discarding.
Row 12: Skipping row 12: empty email value.
Row 13: Email '[email protected]' is deliverable.
Row 14: Email '[email protected]' is undeliverable or invalid. Discarding.
Row 15: Email '[email protected]' is uncertain. Flagging for review.
Row 16: Email 'malformed-email@' is undeliverable or invalid. Discarding.

--- Import Summary ---
Processed Rows: 16
Valid Contacts: 6
Uncertain Contacts (for review): 3
Invalid/Discarded Contacts: 7
----------------------

Uncertain contacts to review:
- Email: [email protected]
  Details: Status=validated, Score=0.7, Recommendation=uncertain
- Email: [email protected]
  Details: Status=validated, Score=0.7, Recommendation=uncertain
- Email: N/A
  Reason: Missing email value

Note that the "Empty Email" row is handled by the CSV parsing and the check for empty values after trimming. The "Syntax Error" and "Bad Email" are likely caught by the API's syntax checks.

Automated Testing

Robust testing is essential for production integrations. We'll use PHPUnit to test our EmailValidatorService. To avoid actual API calls during tests, we'll use a deterministic fake transport for cURL.

First, install PHPUnit if you haven't already:

composer require --dev phpunit/phpunit

Create a test file tests/EmailValidatorServiceTest.php:

<?php

declare(strict_types=1);

namespace Tests;

use App\Api\EmailValidatorService;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use Exception;

// A simple mock for cURL responses
class CurlMock
{
    private array $responses;
    private int $callCount = 0;

    public function __construct(array $responses)
    {
        $this->responses = $responses;
    }

    public function __call(string $name, array $arguments)
    {
        if ($name === 'exec') {
            if (!isset($this->responses[$this->callCount])) {
                throw new Exception("Mock cURL: No response defined for call count {$this->callCount}");
            }
            return $this->responses[$this->callCount++];
        }
        // Simulate setting options - we don't need to assert them for this simple mock
        return true;
    }

    public function getCallCount(): int
    {
        return $this->callCount;
    }
}

class EmailValidatorServiceTest extends TestCase
{
    private const TEST_API_URL = 'https://ai.mihajlo.mk/api/email-validator/v1/check-email';
    private const TEST_API_TOKEN = 'test_token';
    private const TEST_TIMEOUT = 5.0;

    /**
     * @runInSeparateProcess // Important if mocking global functions like curl_init
     */
    public function testCheckEmailSuccessDeliverable(): void
    {
        $mockResponse = json_encode([
            'email' => '[email protected]',
            'status' => 'validated',
            'score' => 0.9,
            'recommendation' => 'deliverable',
            'checks' => ['syntax' => true, 'domain' => true, 'mx' => true, 'provider' => true, 'disposable' => false],
            'quota' => ['used' => 10, 'total' => 1000]
        ]);

        $curlMock = new CurlMock([$mockResponse]);
        $this->mockCurlExec($curlMock);

        $service = new EmailValidatorService(self::TEST_API_URL, self::TEST_API_TOKEN, self::TEST_TIMEOUT);
        $result = $service->checkEmail('[email protected]');

        $this->assertNotNull($result);
        $this->assertEquals('validated', $result['status']);
        $this->assertEquals('deliverable', $result['recommendation']);
        $this->assertEquals(1, $curlMock->getCallCount());
    }

    /**
     * @runInSeparateProcess
     */
    public function testCheckEmailSuccessUncertain(): void
    {
        $mockResponse = json_encode([
            'email' => '[email protected]',
            'status' => 'validated',
            'score' => 0.7,
            'recommendation' => 'uncertain',
            'checks' => ['syntax' => true, 'domain' => true, 'mx' => true, 'provider' => false, 'disposable' => false],
            'quota' => ['used' => 11, 'total' => 1000]
        ]);

        $curlMock = new CurlMock([$mockResponse]);
        $this->mockCurlExec($curlMock);

        $service = new EmailValidatorService(self::TEST_API_URL, self::TEST_API_TOKEN, self::TEST_TIMEOUT);
        $result = $service->checkEmail('[email protected]');

        $this->assertNotNull($result);
        $this->assertEquals('uncertain', $result['recommendation']);
        $this->assertEquals(1, $curlMock->getCallCount());
    }

    /**
     * @runInSeparateProcess
     */
    public function testCheckEmailSuccessUndeliverable(): void
    {
        $mockResponse = json_encode([
            'email' => '[email protected]',
            'status' => 'invalid',
            'score' => 0.1,
            'recommendation' => 'undeliverable',
            'checks' => ['syntax' => true, 'domain' => false, 'mx' => false, 'provider' => false, 'disposable' => false],
            'quota' => ['used' => 12, 'total' => 1000]
        ]);

        $curlMock = new CurlMock([$mockResponse]);
        $this->mockCurlExec($curlMock);

        $service = new EmailValidatorService(self::TEST_API_URL, self::TEST_API_TOKEN, self::TEST_TIMEOUT);
        $result = $service->checkEmail('[email protected]');

        $this->assertNotNull($result);
        $this->assertEquals('undeliverable', $result['recommendation']);
        $this->assertEquals(1, $curlMock->getCallCount());
    }

    /**
     * @runInSeparateProcess
     */
    public function testCheckEmailEmptyEmailReturnsNull(): void
    {
        $service = new EmailValidatorService(self::TEST_API_URL, self::TEST_API_TOKEN, self::TEST_TIMEOUT);
        $result = $service->checkEmail('');
        $this->assertNull($result);
    }

    /**
     * @runInSeparateProcess
     */
    public function testCheckEmailApiCommunicationErrorThrowsException(): void
    {
        $curlMock = new CurlMock([false]); // Simulate curl_exec returning false
        $this->mockCurlExec($curlMock);

        $service = new EmailValidatorService(self::TEST_API_URL, self::TEST_API_TOKEN, self::TEST_TIMEOUT);

        $this->expectException(Exception::class);
        $this->expectExceptionMessage('Failed to communicate with email validation service: '); // Partial message check
        $service->checkEmail('[email protected]');
    }

    /**
     * @runInSeparateProcess
     */
    public function testCheckEmailInvalidJsonResponseThrowsException(): void
    {
        $curlMock = new CurlMock(['This is not JSON']);
        $this->mockCurlExec($curlMock);

        $service = new EmailValidatorService(self::TEST_API_URL, self::TEST_API_TOKEN, self::TEST_TIMEOUT);

        $this->expectException(Exception::class);
        $this->expectExceptionMessage('Invalid JSON response from email validation service');
        $service->checkEmail('[email protected]');
    }

    /**
     * @runInSeparateProcess
     */
    public function testCheckEmailMissingRequiredKeysThrowsException(): void
    {
        $mockResponse = json_encode(['partial' => 'data']); // Missing status, score, recommendation
        $curlMock = new CurlMock([$mockResponse]);
        $this->mockCurlExec($curlMock);

        $service = new EmailValidatorService(self::TEST_API_URL, self::TEST_API_TOKEN, self::TEST_TIMEOUT);

        $this->expectException(Exception::class);
        $this->expectExceptionMessage('Unexpected response structure');
        $service->checkEmail('[email protected]');
    }

    /**
     * @runInSeparateProcess
     */
    public function testConstructorThrowsExceptionIfTokenMissing(): void
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage('Email validation API token is not configured.');
        new EmailValidatorService(self::TEST_API_URL, '', self::TEST_TIMEOUT);
    }

    /**
     * Helper to mock global curl functions.
     * This is a simplified approach for demonstration. Real-world mocking might use a dedicated library.
     */
    private function mockCurlExec(CurlMock $curlMock): void
    {
        // We need to mock curl_init, curl_setopt, curl_exec, curl_getinfo, curl_error, curl_close
        // For simplicity, we'll only mock the essential ones for exec and error reporting.
        // In a more complex scenario, you'd mock all of them.

        // Mock curl_init to return a handle that our CurlMock can intercept
        $mockHandle = new \stdClass(); // A dummy object to represent a cURL handle
        $GLOBALS['mock_curl_handle'] = $mockHandle;
        $GLOBALS['mock_curl_responses'] = $curlMock;

        // Mock specific curl functions
        if (!function_exists('curl_init')) {
            eval('function curl_init() { return $GLOBALS["mock_curl_handle"]; }');
        }
        if (!function_exists('curl_setopt')) {
            eval('function curl_setopt($ch, $option, $value) { return true; }'); // Assume options are set
        }
        if (!function_exists('curl_exec')) {
            eval('function curl_exec($ch) {
                if ($ch === $GLOBALS["mock_curl_handle"]) {
                    return $GLOBALS["mock_curl_responses"]->exec(null); // Pass null as arg for exec
                }
                return false; // Fallback for unexpected handles
            }');
        }
        if (!function_exists('curl_getinfo')) {
            eval('function curl_getinfo($ch, $option) {
                if ($ch === $GLOBALS["mock_curl_handle"] && $option === CURLINFO_HTTP_CODE) {
                    // Assuming a successful response for this mock scenario
                    // In a real mock, you'd need to parse response or define HTTP codes
                    return 200;
                }
                return null;
            }');
        }
         if (!function_exists('curl_error')) {
            eval('function curl_error($ch) {
                if ($ch === $GLOBALS["mock_curl_handle"]) {
                    return "Mocked cURL error";
                }
                return "";
            }');
        }
        if (!function_exists('curl_close')) {
            eval('function curl_close($ch) { /* no-op */ }');
        }
    }
}

Run the tests:

vendor/bin/phpunit tests/EmailValidatorServiceTest.php

Security Considerations

  • API Token Security: As emphasized, never hardcode your API token. Use environment variables and ensure these files are not committed to version control.
  • Input Sanitization: While the API validates the email format, always ensure that data coming into your application (e.g., from CSV files) is handled safely. The trim() in the command script is a basic step.
  • Rate Limiting and Quotas: The API response includes quota information. Your application should monitor this and handle cases where you might exceed limits. Implement appropriate retry strategies with exponential backoff for temporary rate limits, but be aware that persistent limits might require upgrading your plan.

Observability and Error Handling

  • Logging: Critical errors (API communication failures, invalid JSON, unexpected responses) are logged using PHP's error_log(). In a production environment, you'd integrate this with a more robust logging system (e.g., Monolog, ELK stack, cloud logging services).
  • Structured Failures: The command script categorizes emails into validContacts, uncertainContacts, and invalidContacts. This structured output allows for different follow-up actions. API errors during processing are treated as uncertain, prompting a review rather than immediate discarding.
  • Timeouts: The timeout parameter in the service class (configured via config/api.php) prevents the application from hanging indefinitely if the API is unresponsive.

Deployment Notes

  • Ensure the .env file is correctly configured on your production server.
  • The PHP version must be 8.3 or higher.
  • The cURL extension must be enabled for PHP.
  • The script is designed to be run from the command line. For web applications, you would typically trigger this process via a background job (e.g., using a queue system like Laravel Queues or Symfony Messenger) or a scheduled task.

Common Failures and Troubleshooting

  • API Token Issues: Ensure the token is correctly copied and that the EMAIL_VALIDATOR_API_TOKEN environment variable is set. Check the Mihajlo API dashboard to confirm your plan is active.
  • Network Connectivity: Verify that your server can reach ai.mihajlo.mk. Firewall rules or DNS issues can block access.
  • CSV Parsing Errors: Malformed CSV files can cause issues. Ensure consistent delimiters and quoting. The script includes basic checks for missing email columns and values.
  • Rate Limiting: If you consistently hit rate limits, you'll need to implement smarter retry logic or upgrade your API plan. Log these events to monitor usage.
  • Unexpected API Responses: While we validate for essential keys, the API might change its response format in the future. Monitor logs for unexpected data structures.

Final Verification Checklist

  • [ ] API token is securely stored in environment variables and not in code.
  • [ ] PHP version is 8.3+.
  • [ ] cURL extension is enabled.
  • [ ] Composer dependencies are installed.
  • [ ] Sample .env and config/api.php are correctly set up.
  • [ ] import_contacts.csv is present and has an "email" column.
  • [ ] Running composer import executes without fatal errors.
  • [ ] Output correctly categorizes emails as deliverable, uncertain, or invalid.
  • [ ] Unit tests for EmailValidatorService pass.
  • [ ] Error logging is configured for production.

By integrating the Mihajlo AI Email Validator service, you've taken a significant step towards maintaining a clean and effective newsletter contact list. The ability to distinguish between definitively deliverable emails, those requiring a second look, and those that are clearly invalid empowers you to manage your subscriber data with greater precision, ultimately leading to better campaign performance and a healthier sender reputation.

Портрет на автор на блогот

Mihajlo

Јас сум Михајло - развивач поттикнат од љубопитност, дисциплина и постојаната желба да создадам нешто значајно. Споделувам увиди, упатства и бесплатни услуги за да им помогнам на другите да ја поедностават својата работа и да растат во постојано развивачкиот свет на софтверот и вештачката интелигенција.