Initial release
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/vendor/
|
||||||
|
/node_modules/
|
||||||
|
.phpunit.cache/
|
||||||
|
.phpunit.result.cache
|
||||||
|
composer.lock
|
||||||
106
README.md
Normal file
106
README.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# LeadMail SDK for Laravel
|
||||||
|
|
||||||
|
Laravel package for sending emails and verifying email addresses through the [leadMail](https://mail.leadmagnet.dev) service.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- PHP 8.2+
|
||||||
|
- Laravel 11 or 12
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require leadm/leadmail
|
||||||
|
```
|
||||||
|
|
||||||
|
Publish the config file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan vendor:publish --provider="LeadM\LeadMail\LeadMailServiceProvider"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Add these to your `.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LEADMAIL_URL=https://mail.leadmagnet.dev
|
||||||
|
LEADMAIL_TOKEN=lm_your_api_token_here
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional settings:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LEADMAIL_TIMEOUT=30
|
||||||
|
LEADMAIL_VERIFY_SSL=true
|
||||||
|
LEADMAIL_AUTO_TENANT=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Send Emails via Mail Driver
|
||||||
|
|
||||||
|
Set `leadmail` as your mail driver in `.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
MAIL_MAILER=leadmail
|
||||||
|
```
|
||||||
|
|
||||||
|
Then use Laravel's `Mail` facade as usual:
|
||||||
|
|
||||||
|
```php
|
||||||
|
Mail::to('user@example.com')->send(new WelcomeMail());
|
||||||
|
```
|
||||||
|
|
||||||
|
### Send Emails via API
|
||||||
|
|
||||||
|
```php
|
||||||
|
LeadMail::sendEmail([
|
||||||
|
'from' => ['email' => 'hello@yourdomain.com', 'name' => 'Your App'],
|
||||||
|
'to' => [['email' => 'user@example.com', 'name' => 'User']],
|
||||||
|
'subject' => 'Welcome!',
|
||||||
|
'html_body' => '<h1>Welcome to our app</h1>',
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify Email Addresses
|
||||||
|
|
||||||
|
```php
|
||||||
|
$result = LeadMail::verifyEmail('user@example.com');
|
||||||
|
|
||||||
|
if ($result['data']['valid']) {
|
||||||
|
// Email is deliverable
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validation Rule
|
||||||
|
|
||||||
|
Use the `leadmail_verify` rule in your form requests:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email' => ['required', 'email', 'leadmail_verify'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Allowed Sender Domains
|
||||||
|
|
||||||
|
```php
|
||||||
|
$domains = LeadMail::getDomains();
|
||||||
|
// ['yourdomain.com', 'anotherdomain.com']
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multi-Tenancy
|
||||||
|
|
||||||
|
If your app uses [stancl/tenancy](https://tenancyforlaravel.com), the SDK automatically includes the current tenant ID in API requests via the `X-Tenant-Id` header. Disable this with:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LEADMAIL_AUTO_TENANT=false
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
49
composer.json
Normal file
49
composer.json
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"name": "leadm/leadmail",
|
||||||
|
"description": "Laravel SDK for leadMail email sending and verification service",
|
||||||
|
"type": "library",
|
||||||
|
"license": "MIT",
|
||||||
|
"homepage": "https://github.com/leadmnik/leadmail-sdk",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Lead Magnet Oy",
|
||||||
|
"homepage": "https://leadmagnet.fi"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/leadmnik/leadmail-sdk/issues",
|
||||||
|
"source": "https://github.com/leadmnik/leadmail-sdk"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^8.2",
|
||||||
|
"guzzlehttp/guzzle": "^7.0",
|
||||||
|
"illuminate/mail": "^11.0|^12.0",
|
||||||
|
"illuminate/support": "^11.0|^12.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"orchestra/testbench": "^9.0|^10.0",
|
||||||
|
"pestphp/pest": "^3.0|^4.0"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"LeadM\\LeadMail\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"LeadM\\LeadMail\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"LeadM\\LeadMail\\LeadMailServiceProvider"
|
||||||
|
],
|
||||||
|
"aliases": {
|
||||||
|
"LeadMail": "LeadM\\LeadMail\\LeadMailFacade"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimum-stability": "dev",
|
||||||
|
"prefer-stable": true
|
||||||
|
}
|
||||||
55
config/leadmail.php
Normal file
55
config/leadmail.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| LeadMail Service URL
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The base URL of your leadMail service instance.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'url' => env('LEADMAIL_URL', 'https://mail.leadmagnet.dev'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| API Token
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The bearer token for authenticating with the leadMail API.
|
||||||
|
| Generate this in the leadMail admin dashboard.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'token' => env('LEADMAIL_TOKEN'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Request Timeout
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Maximum number of seconds to wait for API responses.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'timeout' => env('LEADMAIL_TIMEOUT', 30),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| SSL Verification
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Whether to verify SSL certificates. Disable only for local development.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'verify_ssl' => env('LEADMAIL_VERIFY_SSL', true),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Auto Tenant Detection
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When enabled and stancl/tenancy is installed, automatically sends the
|
||||||
|
| current tenant's key as the X-Tenant-Id header on every request.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'auto_tenant' => env('LEADMAIL_AUTO_TENANT', true),
|
||||||
|
];
|
||||||
133
src/LeadMailClient.php
Normal file
133
src/LeadMailClient.php
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace LeadM\LeadMail;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class LeadMailClient
|
||||||
|
{
|
||||||
|
protected Client $http;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected readonly string $baseUrl,
|
||||||
|
protected readonly string $token,
|
||||||
|
protected readonly int $timeout = 30,
|
||||||
|
protected readonly bool $verifySsl = true,
|
||||||
|
protected readonly bool $autoTenant = true,
|
||||||
|
) {
|
||||||
|
$this->http = new Client([
|
||||||
|
'base_uri' => rtrim($this->baseUrl, '/').'/api/v1/',
|
||||||
|
'timeout' => $this->timeout,
|
||||||
|
'verify' => $this->verifySsl,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an email through the leadMail service.
|
||||||
|
*
|
||||||
|
* @param array{from: array, to: array, cc?: array, bcc?: array, reply_to?: array, subject: string, html_body?: string, text_body?: string, attachments?: array, metadata?: array, options?: array} $data
|
||||||
|
* @return array{success: bool, data: array{log_id: int, status: string}}
|
||||||
|
*
|
||||||
|
* @throws GuzzleException
|
||||||
|
*/
|
||||||
|
public function sendEmail(array $data): array
|
||||||
|
{
|
||||||
|
$response = $this->http->post('emails/send', [
|
||||||
|
'headers' => $this->headers(),
|
||||||
|
'json' => $data,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return json_decode($response->getBody()->getContents(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the allowed sender domains for this client app.
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
*
|
||||||
|
* @throws GuzzleException
|
||||||
|
*/
|
||||||
|
public function getDomains(): array
|
||||||
|
{
|
||||||
|
$response = $this->http->get('domains', [
|
||||||
|
'headers' => $this->headers(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = json_decode($response->getBody()->getContents(), true);
|
||||||
|
|
||||||
|
return $data['data']['domains'] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify an email address through the leadMail service.
|
||||||
|
*
|
||||||
|
* @return array{success: bool, data: array{email: string, valid: bool, status: string, reason: ?string, cached: bool}}
|
||||||
|
*/
|
||||||
|
public function verifyEmail(string $email, bool $allowDisposable = false): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$response = $this->http->post('emails/verify', [
|
||||||
|
'headers' => $this->headers(),
|
||||||
|
'json' => [
|
||||||
|
'email' => $email,
|
||||||
|
'allow_disposable' => $allowDisposable,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return json_decode($response->getBody()->getContents(), true);
|
||||||
|
} catch (GuzzleException $e) {
|
||||||
|
Log::warning('LeadMail verification failed, failing open', [
|
||||||
|
'email' => $email,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'email' => $email,
|
||||||
|
'valid' => true,
|
||||||
|
'status' => 'unknown',
|
||||||
|
'reason' => null,
|
||||||
|
'cached' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function headers(): array
|
||||||
|
{
|
||||||
|
$headers = [
|
||||||
|
'Authorization' => 'Bearer '.$this->token,
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($this->autoTenant && $this->hasTenancy()) {
|
||||||
|
$tenantId = $this->resolveTenantId();
|
||||||
|
if ($tenantId !== null) {
|
||||||
|
$headers['X-Tenant-Id'] = $tenantId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function hasTenancy(): bool
|
||||||
|
{
|
||||||
|
return class_exists(\Stancl\Tenancy\Tenancy::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function resolveTenantId(): ?string
|
||||||
|
{
|
||||||
|
if (! function_exists('tenancy') || ! tenancy()->initialized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) tenant()->getTenantKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/LeadMailFacade.php
Normal file
20
src/LeadMailFacade.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace LeadM\LeadMail;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Facade;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @method static array sendEmail(array $data)
|
||||||
|
* @method static string[] getDomains()
|
||||||
|
* @method static array verifyEmail(string $email, bool $allowDisposable = false)
|
||||||
|
*
|
||||||
|
* @see \LeadM\LeadMail\LeadMailClient
|
||||||
|
*/
|
||||||
|
class LeadMailFacade extends Facade
|
||||||
|
{
|
||||||
|
protected static function getFacadeAccessor(): string
|
||||||
|
{
|
||||||
|
return LeadMailClient::class;
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/LeadMailServiceProvider.php
Normal file
52
src/LeadMailServiceProvider.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace LeadM\LeadMail;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use LeadM\LeadMail\Rules\LeadMailVerify;
|
||||||
|
|
||||||
|
class LeadMailServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
$this->mergeConfigFrom(__DIR__.'/../config/leadmail.php', 'leadmail');
|
||||||
|
|
||||||
|
$this->app->singleton(LeadMailClient::class, function () {
|
||||||
|
return new LeadMailClient(
|
||||||
|
baseUrl: config('leadmail.url'),
|
||||||
|
token: config('leadmail.token', ''),
|
||||||
|
timeout: config('leadmail.timeout', 30),
|
||||||
|
verifySsl: config('leadmail.verify_ssl', true),
|
||||||
|
autoTenant: config('leadmail.auto_tenant', true),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
if ($this->app->runningInConsole()) {
|
||||||
|
$this->publishes([
|
||||||
|
__DIR__.'/../config/leadmail.php' => config_path('leadmail.php'),
|
||||||
|
], 'leadmail-config');
|
||||||
|
}
|
||||||
|
|
||||||
|
Mail::extend('leadmail', function () {
|
||||||
|
return new LeadMailTransport(
|
||||||
|
$this->app->make(LeadMailClient::class),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Validator::extend('leadmail_verify', function (string $attribute, mixed $value) {
|
||||||
|
$rule = $this->app->make(LeadMailVerify::class);
|
||||||
|
$passed = true;
|
||||||
|
|
||||||
|
$rule->validate($attribute, $value, function () use (&$passed) {
|
||||||
|
$passed = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return $passed;
|
||||||
|
}, 'The :attribute email address could not be verified.');
|
||||||
|
}
|
||||||
|
}
|
||||||
105
src/LeadMailTransport.php
Normal file
105
src/LeadMailTransport.php
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace LeadM\LeadMail;
|
||||||
|
|
||||||
|
use Symfony\Component\Mailer\SentMessage;
|
||||||
|
use Symfony\Component\Mailer\Transport\AbstractTransport;
|
||||||
|
use Symfony\Component\Mime\Address;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
use Symfony\Component\Mime\MessageConverter;
|
||||||
|
|
||||||
|
class LeadMailTransport extends AbstractTransport
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected readonly LeadMailClient $client,
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doSend(SentMessage $message): void
|
||||||
|
{
|
||||||
|
$email = MessageConverter::toEmail($message->getOriginalMessage());
|
||||||
|
|
||||||
|
$payload = $this->buildPayload($email);
|
||||||
|
|
||||||
|
$result = $this->client->sendEmail($payload);
|
||||||
|
|
||||||
|
if (isset($result['data']['log_id'])) {
|
||||||
|
$message->getOriginalMessage()->getHeaders()->addTextHeader(
|
||||||
|
'X-LeadMail-Log-Id',
|
||||||
|
(string) $result['data']['log_id'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
protected function buildPayload(Email $email): array
|
||||||
|
{
|
||||||
|
$payload = [
|
||||||
|
'from' => $this->formatAddress($email->getFrom()[0] ?? null),
|
||||||
|
'to' => array_map(fn (Address $a) => $this->formatAddress($a), $email->getTo()),
|
||||||
|
'subject' => $email->getSubject() ?? '',
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($email->getCc()) {
|
||||||
|
$payload['cc'] = array_map(fn (Address $a) => $this->formatAddress($a), $email->getCc());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($email->getBcc()) {
|
||||||
|
$payload['bcc'] = array_map(fn (Address $a) => $this->formatAddress($a), $email->getBcc());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($email->getReplyTo()) {
|
||||||
|
$replyTo = $email->getReplyTo()[0];
|
||||||
|
$payload['reply_to'] = $this->formatAddress($replyTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($email->getHtmlBody()) {
|
||||||
|
$payload['html_body'] = $email->getHtmlBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($email->getTextBody()) {
|
||||||
|
$payload['text_body'] = $email->getTextBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
$attachments = [];
|
||||||
|
foreach ($email->getAttachments() as $attachment) {
|
||||||
|
$attachments[] = [
|
||||||
|
'content' => base64_encode($attachment->getBody()),
|
||||||
|
'filename' => $attachment->getFilename() ?? 'attachment',
|
||||||
|
'mime_type' => $attachment->getMediaType().'/'.$attachment->getMediaSubtype(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($attachments) {
|
||||||
|
$payload['attachments'] = $attachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{email: string, name?: string}
|
||||||
|
*/
|
||||||
|
protected function formatAddress(?Address $address): array
|
||||||
|
{
|
||||||
|
if ($address === null) {
|
||||||
|
return ['email' => ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = ['email' => $address->getAddress()];
|
||||||
|
|
||||||
|
if ($address->getName()) {
|
||||||
|
$result['name'] = $address->getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return 'leadmail';
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/Rules/LeadMailVerify.php
Normal file
35
src/Rules/LeadMailVerify.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace LeadM\LeadMail\Rules;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use LeadM\LeadMail\LeadMailClient;
|
||||||
|
|
||||||
|
class LeadMailVerify implements ValidationRule
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected readonly LeadMailClient $client,
|
||||||
|
protected readonly bool $allowDisposable = false,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||||
|
{
|
||||||
|
if (! is_string($value) || ! filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$fail('The :attribute must be a valid email address.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $this->client->verifyEmail($value, $this->allowDisposable);
|
||||||
|
|
||||||
|
if (! ($result['data']['valid'] ?? true)) {
|
||||||
|
$reason = $result['data']['reason'] ?? 'verification failed';
|
||||||
|
$fail("The :attribute email address is not deliverable ({$reason}).");
|
||||||
|
}
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// Fail open — if the verification service is down, allow the email through
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user