moneo/laravel-reacher

Laravel package for check-if-email-exists (Reacher) - Check if an email address exists without sending any email

Installs: 1

Dependents: 0

Suggesters: 0

Security: 0

Stars: 1

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/moneo/laravel-reacher

dev-main 2026-02-10 13:22 UTC

This package is auto-updated.

Last update: 2026-02-10 16:09:00 UTC


README

A comprehensive Laravel package for check-if-email-exists (Reacher). Check if an email address exists without sending any email.

This package communicates with the Reacher HTTP backend and provides a clean Laravel-friendly API with Facade support, typed DTOs, validation rules, caching, retry logic, bulk verification, queue support, Artisan commands, event dispatching, and full test coverage.

Requirements

Installation

composer require moneo/laravel-reacher

The service provider and facade are auto-discovered. To publish the config file:

php artisan vendor:publish --tag=reacher-config

Configuration

Add these environment variables to your .env file:

# Required
REACHER_BASE_URL=http://localhost:8080

# API version (v1 recommended, v0 deprecated)
REACHER_API_VERSION=v1

# SMTP settings
REACHER_FROM_EMAIL=noreply@yourdomain.com
REACHER_HELLO_NAME=yourdomain.com
REACHER_SMTP_PORT=25
REACHER_CHECK_GRAVATAR=false

# Authentication (optional)
REACHER_HEADER=Authorization
REACHER_SECRET=Bearer your-api-token

# SOCKS5 Proxy (optional)
REACHER_PROXY_HOST=proxy.example.com
REACHER_PROXY_PORT=1080
REACHER_PROXY_USERNAME=user
REACHER_PROXY_PASSWORD=pass

# Timeouts
REACHER_TIMEOUT=60
REACHER_CONNECT_TIMEOUT=10

# Cache (optional)
REACHER_CACHE_ENABLED=false
REACHER_CACHE_TTL=3600
REACHER_CACHE_STORE=redis

# Retry (optional)
REACHER_RETRY_TIMES=3
REACHER_RETRY_SLEEP_MS=1000

# Logging (optional)
REACHER_LOGGING_ENABLED=false
REACHER_LOG_CHANNEL=stack

# Verification method overrides (optional)
REACHER_YAHOO_VERIF_METHOD=Headless
REACHER_HOTMAILB2C_VERIF_METHOD=Smtp

Starting the Reacher Backend

The easiest way to run the Reacher backend is via Docker:

docker run -p 8080:8080 reacherhq/backend:latest

Usage

Basic Email Check

use Moneo\Reacher\Facades\Reacher;

$result = Reacher::check('someone@gmail.com');

// Reachability status
echo $result->isReachable; // 'safe', 'risky', 'invalid', 'unknown'

// Convenience methods
$result->isSafe();
$result->isRisky();
$result->isInvalid();
$result->isUnknown();

Check with Options

$result = Reacher::check('someone@gmail.com', [
    'from_email' => 'verify@yourdomain.com',
    'hello_name' => 'yourdomain.com',
    'smtp_port' => 587,
    'check_gravatar' => true,
    'smtp_timeout' => ['secs' => 30, 'nanos' => 0],
    'verification_methods' => [
        'yahoo' => 'Headless',
        'hotmailb2c' => 'Smtp',
    ],
    'proxy' => [
        'host' => 'proxy.example.com',
        'port' => 1080,
        'username' => 'user',
        'password' => 'pass',
    ],
]);

Accessing Result Details

$result = Reacher::check('someone@gmail.com');

// Syntax
$result->syntax->isValidSyntax;  // bool
$result->syntax->domain;         // 'gmail.com'
$result->syntax->username;       // 'someone'
$result->syntax->suggestion;     // null or suggested correction

// MX Records
$result->mx->acceptsMail;  // bool
$result->mx->records;      // ['alt1.gmail-smtp-in.l.google.com.', ...]

// SMTP
$result->smtp->canConnectSmtp;  // bool
$result->smtp->isDeliverable;   // bool
$result->smtp->isCatchAll;      // bool
$result->smtp->hasFullInbox;    // bool
$result->smtp->isDisabled;      // bool

// Misc
$result->misc->isDisposable;    // bool
$result->misc->isRoleAccount;   // bool
$result->misc->isB2c;           // bool
$result->misc->gravatarUrl;     // string|null
$result->misc->haveibeenpwned;  // bool|null

// Debug info (when available)
$result->debug?->serverName;              // 'backend-dev'
$result->debug?->durationInSeconds();     // 5.5
$result->debug?->verificationMethod();    // VerificationMethod::Smtp

Convenience Helper Methods

$result = Reacher::check('someone@gmail.com');

$result->isDeliverable();     // Is the email deliverable?
$result->isDisposable();      // Is it a disposable/temporary email?
$result->isRoleAccount();     // Is it a role account (admin@, info@)?
$result->isCatchAll();        // Is it a catch-all address?
$result->hasFullInbox();      // Is the inbox full?
$result->isDisabled();        // Is the mailbox disabled?
$result->hasValidSyntax();    // Is the syntax valid?
$result->domainAcceptsMail(); // Does the domain accept mail?
$result->hasErrors();         // Did any sub-check return an error?
$result->getErrors();         // Get all errors as ['smtp' => ErrorResult, ...]

Quick Check Methods

// One-liner checks (each makes an API call)
Reacher::isDeliverable('someone@gmail.com');  // bool
Reacher::isSafe('someone@gmail.com');         // bool
Reacher::isDisposable('someone@gmail.com');   // bool
Reacher::isRoleAccount('someone@gmail.com');  // bool
Reacher::isCatchAll('someone@gmail.com');     // bool

Bulk Email Check (Sequential)

$results = Reacher::checkMany([
    'user1@gmail.com',
    'user2@yahoo.com',
    'user3@outlook.com',
]);

foreach ($results as $result) {
    echo "{$result->input}: {$result->isReachable}\n";
}

Bulk Verification via API (v1)

For large-scale verification using Reacher's bulk API with RabbitMQ worker queue:

// Create a bulk job
$jobId = Reacher::createBulk([
    'user1@gmail.com',
    'user2@yahoo.com',
    // ... thousands of emails
]);

// With webhook
$jobId = Reacher::createBulk($emails, [
    'webhook' => [
        'on_each_email' => [
            'url' => 'https://yoursite.com/api/reacher-webhook',
            'extra' => ['batch_id' => 'abc123'],
        ],
    ],
]);

// Check job status
$job = Reacher::getBulkStatus($jobId);
echo $job->jobStatus;           // 'Running' or 'Completed'
echo $job->progressPercentage(); // 75.0
echo $job->totalProcessed;       // 750
echo $job->summary->totalSafe;   // 500

// Get results (paginated)
$results = Reacher::getBulkResults($jobId, limit: 50, offset: 0);

foreach ($results->results as $result) {
    echo "{$result->input}: {$result->isReachable}\n";
}

// Filter results
$safeEmails = $results->safe();
$invalidEmails = $results->invalid();

Backend Health Check

// Get backend version
$version = Reacher::version(); // '0.11.0'

// Check if backend is healthy
if (Reacher::isHealthy()) {
    echo 'Reacher is running';
}

Using the Reachability Enum

use Moneo\Reacher\Enums\Reachability;

$result = Reacher::check('someone@gmail.com');

match ($result->reachability()) {
    Reachability::Safe    => 'Email is safe to send to',
    Reachability::Risky   => 'Email might bounce',
    Reachability::Invalid => 'Email does not exist',
    Reachability::Unknown => 'Could not determine',
};

API Version Control

// Default uses v1 (recommended)
$result = Reacher::check('test@example.com');

// Switch to v0 (deprecated, bypasses throttle)
Reacher::setApiVersion('v0');
$result = Reacher::check('test@example.com');

Caching

Enable caching to avoid redundant API calls for the same email:

REACHER_CACHE_ENABLED=true
REACHER_CACHE_TTL=3600
REACHER_CACHE_STORE=redis
// First call hits API, stores in cache
$result = Reacher::check('someone@gmail.com');

// Second call returns from cache
$result = Reacher::check('someone@gmail.com');

// Bypass cache for a single call
$result = Reacher::withoutCache()->check('someone@gmail.com');

// Remove a specific email from cache
Reacher::forget('someone@gmail.com');

Note: Cache is only used for calls without custom options to ensure consistency.

Retry & Backoff

Automatic retry with exponential backoff for transient errors:

REACHER_RETRY_TIMES=3
REACHER_RETRY_SLEEP_MS=1000
// config/reacher.php
'retry' => [
    'times' => 3,              // Max retry attempts
    'sleep_ms' => 1000,        // Initial wait (1 second)
    'backoff_multiplier' => 2, // Exponential: 1s, 2s, 4s
    'retry_on' => [429, 500, 502, 503, 504],
],

Rate limit (429) responses are automatically retried. The package parses the retry-after duration from Reacher's error message.

Queue / Jobs

Dispatch email checks to the background queue:

// Single email check (dispatched to queue)
Reacher::dispatchCheck('someone@gmail.com');

// With options and custom queue
Reacher::dispatchCheck('someone@gmail.com', ['smtp_port' => 587], queue: 'email-verification');

// Bulk check via queue
Reacher::dispatchBulkCheck([
    'user1@gmail.com',
    'user2@yahoo.com',
], queue: 'email-verification');

When a queued check completes, an EmailChecked event is automatically dispatched.

Events

The package dispatches Laravel events:

Event When
EmailChecked After each email verification completes
BulkJobStarted After a bulk job is created via API
BulkJobCompleted After a bulk job finishes
use Moneo\Reacher\Events\EmailChecked;
use Moneo\Reacher\Events\BulkJobStarted;

// In EventServiceProvider or listener
class LogVerification
{
    public function handle(EmailChecked $event): void
    {
        logger()->info('Email checked', [
            'email' => $event->email,
            'reachable' => $event->result->isReachable,
        ]);
    }
}

Validation Rule

The package includes a Laravel validation rule:

use Moneo\Reacher\Rules\ReachableEmail;
use Moneo\Reacher\Enums\Reachability;

// Basic: only accept 'safe' emails, reject disposable
$request->validate([
    'email' => ['required', 'email', new ReachableEmail],
]);

// Accept both 'safe' and 'risky' emails
$request->validate([
    'email' => ['required', 'email', new ReachableEmail(
        acceptedLevels: [Reachability::Safe, Reachability::Risky],
    )],
]);

// Allow disposable, reject role accounts
$request->validate([
    'email' => ['required', 'email', new ReachableEmail(
        rejectDisposable: false,
        rejectRoleAccounts: true,
    )],
]);

Artisan Commands

# Check a single email
php artisan reacher:check someone@gmail.com
php artisan reacher:check someone@gmail.com --json
php artisan reacher:check someone@gmail.com --smtp-port=587 --gravatar

# Bulk check from file (one email per line)
php artisan reacher:bulk emails.txt
php artisan reacher:bulk emails.txt --json
php artisan reacher:bulk emails.txt --api-bulk --webhook=https://yoursite.com/hook

# Health check
php artisan reacher:health

Error Handling

use Moneo\Reacher\Exceptions\ReacherApiException;
use Moneo\Reacher\Exceptions\ReacherException;
use Moneo\Reacher\Exceptions\RateLimitException;
use Moneo\Reacher\Exceptions\AuthenticationException;
use Moneo\Reacher\Exceptions\InvalidEmailException;

try {
    $result = Reacher::check('someone@gmail.com');
} catch (InvalidEmailException $e) {
    // Email format is invalid (before API call)
} catch (AuthenticationException $e) {
    // 401/403 - Check your API credentials
} catch (RateLimitException $e) {
    // 429 - Rate limited
    $e->getRetryAfterSeconds(); // Seconds to wait
} catch (ReacherApiException $e) {
    // Other API errors (5xx, connection issues)
    $e->getCode();          // HTTP status code
    $e->getResponseBody();  // Response body array
} catch (ReacherException $e) {
    // General error (JSON decode failure, etc.)
}

Handling Sub-Result Errors

The Reacher API can return errors for individual sections (mx, smtp, misc):

$result = Reacher::check('someone@example.com');

if ($result->hasErrors()) {
    foreach ($result->getErrors() as $section => $error) {
        echo "[{$section}] {$error->type}: {$error->message}\n";
        // e.g. [smtp] SmtpError: Connection timed out
    }
}

// Check specific sections
if ($result->smtp->hasError()) {
    echo $result->smtp->error->message;
}

Logging

Enable logging to track API calls:

REACHER_LOGGING_ENABLED=true
REACHER_LOG_CHANNEL=stack

Logs include: request details, response reachability status, duration, retries, and errors.

Dependency Injection

You can use dependency injection instead of the Facade:

use Moneo\Reacher\Reacher;

class EmailVerificationController extends Controller
{
    public function __construct(
        private Reacher $reacher,
    ) {}

    public function verify(Request $request)
    {
        $result = $this->reacher->check($request->email);

        return response()->json([
            'reachable' => $result->isReachable,
            'deliverable' => $result->isDeliverable(),
            'disposable' => $result->isDisposable(),
        ]);
    }
}

Full API Reference

Facade Methods

Method Return Description
check($email, $options) EmailCheckResult Verify a single email
checkMany($emails, $options) EmailCheckResult[] Verify multiple emails sequentially
isDeliverable($email) bool Quick deliverability check
isSafe($email) bool Quick safety check
isDisposable($email) bool Quick disposable check
isRoleAccount($email) bool Quick role account check
isCatchAll($email) bool Quick catch-all check
createBulk($emails, $options) int Create bulk verification job
getBulkStatus($jobId) BulkJob Get bulk job status
getBulkResults($jobId, $limit, $offset) BulkResults Get bulk job results
version() string Get backend version
isHealthy() bool Check backend health
dispatchCheck($email, $options, $queue) PendingDispatch Queue a single check
dispatchBulkCheck($emails, $options, $queue) PendingDispatch Queue a bulk check
withoutCache() Reacher Bypass cache for next call
forget($email) bool Remove email from cache
getApiVersion() string Get current API version
setApiVersion($version) Reacher Set API version (v0/v1)

Data Classes

Class Properties
EmailCheckResult input, isReachable, syntax, mx, smtp, misc, debug
SyntaxResult domain, isValidSyntax, username, suggestion
MxResult acceptsMail, records, error
SmtpResult canConnectSmtp, hasFullInbox, isCatchAll, isDeliverable, isDisabled, error
MiscResult isDisposable, isRoleAccount, isB2c, gravatarUrl, haveibeenpwned, error
DebugResult startTime, endTime, duration, serverName, verifMethodType
BulkJob jobId, createdAt, finishedAt, totalRecords, totalProcessed, summary, jobStatus
BulkSummary totalSafe, totalRisky, totalInvalid, totalUnknown
BulkResults results
ErrorResult type, message

Enums

Enum Values
Reachability Safe, Risky, Invalid, Unknown
VerificationMethod Smtp, Headless, Api, Skipped
JobStatus Running, Completed

Testing

composer test

License

MIT License. See LICENSE for more information.