Laravel: Validirajte importe biltena i označite sumnjive e-adrese za ručnu provjeru
Uvoz newslettera djeluje bezazleno dok popis ne sadrži tipografske pogreške, napuštene domene, jednokratne račune i adrese koje su tehnički uvjerljive, ali operativno rizične. Trenutačno slanje na sve adrese može narušiti isporučivost; odbacivanje svake dvosmislene adrese može značiti gubitak stvarnih pretplatnika.
Ovaj vodič izrađuje Laravel uvoznik spreman za produkciju koji normalizira CSV datoteku, odbacuje očite lokalne sintaksne pogreške, provjerava uvjerljive adrese putem usluge za provjeru e-pošte, prihvaća rezultate s visokim stupnjem pouzdanosti, odbacuje rezultate s niskim stupnjem pouzdanosti i nesigurnu sredinu smješta u red za ručnu provjeru. Također čuva vraćene dokaze kako bi recenzent mogao donijeti informiranu odluku.
Dobijte pristup alatu Email Validator
Prije pisanja integracijskog koda, izradite račun na https://ai.mihajlo.mk/register ili upotrijebite https://ai.mihajlo.mk/login ako ga već imate.
- Otvorite stranicu usluge Email Validator.
- Odaberite dostupni plan Free, Plus ili Pro i dovršite njegovu aktivaciju.
- Otvorite službenu dokumentaciju alata Email Validator.
- Pronađite ploču Service token i kopirajte token ograničen na uslugu.
- Pohranite ga u Laravelovu konfiguraciju okruženja, nikada u predani izvorni kod.
Ponovno generiranje tokena usluge opoziva prethodno aktivni token. Rotaciju tretirajte kao promjenu pri implementaciji: ažurirajte svako okruženje koje koristi staru vrijednost, ponovno izgradite Laravelovu predmemoriju konfiguracije, provjerite novi token i tek tada smatrajte rotaciju dovršenom.
Potvrdite točan HTTP ugovor
API poziv je HTTP GET zahtjev na https://ai.mihajlo.mk/api/email-validator/v1/check-email. Autentikacija koristi parametar upita token={serviceToken}, dok se adresa dostavlja u parametru upita email.
Pokrenite jedan minimalni zahtjev iz sigurnog ljuskinog okruženja prije izrade uvoznika:
curl --get \
--data-urlencode "token=YOUR_SERVICE_TOKEN" \
--data-urlencode "[email protected]" \
https://ai.mihajlo.mk/api/email-validator/v1/check-email
Uspješan odgovor pruža status, score, recommendation, checks i quota. Usluga ispituje sintaksu, podatke o domeni i MX-u, signale pružatelja usluge i praktični rizik isporuke. Naša granica zahtijevat će svih pet polja, ali neće nagađati nedokumentirana značenja pojedinačnih ključeva provjere.
Izradite Laravel projekt i konfiguraciju
Potrebni su vam PHP 8.3 ili noviji, Composer, podržano izdanje Laravela i konfigurirana baza podataka. Pokrenite novu aplikaciju ili primijenite sljedeću strukturu na postojeću:
composer create-project laravel/laravel newsletter-cleaner
cd newsletter-cleaner
php artisan make:model NewsletterContact -m
php artisan make:command ImportNewsletterContacts
php artisan make:test EmailValidatorClientTest --unit
Postavite vjerodajnicu i pragove rezultata u vlasništvu aplikacije u .env. Pragovi su namjerno poslovna politika, a ne tvrdnje o isporučivosti poštanskog sandučića. Započnite konzervativno i pregledajte dobivenu raspodjelu prije omogućavanja automatskog slanja.
EMAIL_VALIDATOR_TOKEN=YOUR_SERVICE_TOKEN
EMAIL_VALIDATOR_CONNECT_TIMEOUT=3
EMAIL_VALIDATOR_TIMEOUT=8
EMAIL_VALIDATOR_ACCEPT_SCORE=85
EMAIL_VALIDATOR_REJECT_SCORE=45
Dodajte sljedeći unos u config/services.php:
'email_validator' => [
'base_url' => 'https://ai.mihajlo.mk/api/email-validator',
'token' => env('EMAIL_VALIDATOR_TOKEN'),
'connect_timeout' => (int) env('EMAIL_VALIDATOR_CONNECT_TIMEOUT', 3),
'timeout' => (int) env('EMAIL_VALIDATOR_TIMEOUT', 8),
'accept_score' => (float) env('EMAIL_VALIDATOR_ACCEPT_SCORE', 85),
'reject_score' => (float) env('EMAIL_VALIDATOR_REJECT_SCORE', 45),
],
Laravelova konfiguracija jedini je put do tajne vidljiv u izvornom kodu. Nemojte pozivati env() iz klasa aplikacije jer predmemoriranje konfiguracije mijenja ponašanje izravnog pristupa okruženju.
Modelirajte uvoz kao dokaze i odluku
Uvoznik se izvodi kao Artisan naredba umjesto HTTP kontrolera. Obrada CSV-a može trajati dulje od web zahtjeva, a naredbu je lako rasporediti, pratiti i ponovno pokrenuti. Upsertovi čine ponovna pokretanja idempotentnima na razini kontakta. Za vrlo velike datoteke kasnije se mogu dodati redovi za obradu u serijama, ali tada se konkurentnost mora uskladiti s kvotom usluge.
Izradite tablicu kontakata i model:
<?php
// database/migrations/xxxx_xx_xx_create_newsletter_contacts_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('newsletter_contacts', function (Blueprint $table) {
$table->id();
$table->string('email', 320)->unique();
$table->string('decision', 20);
$table->json('validation_evidence')->nullable();
$table->string('failure_code')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('newsletter_contacts');
}
};
// app/Models/NewsletterContact.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
final class NewsletterContact extends Model
{
protected $fillable = [
'email',
'decision',
'validation_evidence',
'failure_code',
];
protected function casts(): array
{
return ['validation_evidence' => 'array'];
}
}
Tri odluke su accepted, rejected i review. Privremeni prekid nikada ne postaje odbijanje. Ta je razlika važna: nemogućnost provjere nije dokaz da je adresa loša.
Potvrdite odgovor na granici aplikacije
Izradite rezultat domene i politiku u app/Domain/EmailValidation. Mapiranje odbacuje nedostajuća polja ili polja neispravne vrste. Politika zahtijeva potpune dokaze, koristi rezultat za pragove aplikacije i šalje nesigurni interval na pregled. Izvorni status, preporuka, provjere i kvota ostaju dostupni recenzentima i operativnom timu.
<?php
// app/Domain/EmailValidation/EmailValidationResult.php
namespace App\Domain\EmailValidation;
use InvalidArgumentException;
final readonly class EmailValidationResult
{
public function __construct(
public string $status,
public float $score,
public string $recommendation,
public array $checks,
public array $quota,
) {}
public static function fromPayload(array $payload): self
{
foreach (['status', 'score', 'recommendation', 'checks', 'quota'] as $key) {
if (!array_key_exists($key, $payload)) {
throw new InvalidArgumentException("Missing response field: {$key}");
}
}
$numericScore = is_int($payload['score']) || is_float($payload['score']);
if (!is_string($payload['status'])
|| trim($payload['status']) === ''
|| !$numericScore
|| !is_finite((float) $payload['score'])
|| !is_string($payload['recommendation'])
|| trim($payload['recommendation']) === ''
|| !is_array($payload['checks'])
|| !is_array($payload['quota'])) {
throw new InvalidArgumentException('Invalid Email Validator response');
}
return new self(
trim($payload['status']),
(float) $payload['score'],
trim($payload['recommendation']),
$payload['checks'],
$payload['quota'],
);
}
public function evidence(): array
{
return [
'status' => $this->status,
'score' => $this->score,
'recommendation' => $this->recommendation,
'checks' => $this->checks,
'quota' => $this->quota,
];
}
}
// app/Domain/EmailValidation/ImportDecisionPolicy.php
namespace App\Domain\EmailValidation;
final class ImportDecisionPolicy
{
public function decide(EmailValidationResult $result): string
{
$completeEvidence = $result->status !== ''
&& $result->recommendation !== ''
&& $result->checks !== []
&& $result->quota !== [];
if (!$completeEvidence) {
return 'review';
}
if ($result->score >= config('services.email_validator.accept_score')) {
return 'accepted';
}
if ($result->score <= config('services.email_validator.reject_score')) {
return 'rejected';
}
return 'review';
}
}
Ovo konzervativno mapiranje koristi svako vraćeno polje bez dodjeljivanja izmišljenih značenja vrijednostima statusa, preporuke, provjere ili kvote specifičnima za pružatelja usluge. Ako službena dokumentacija definira vrijednosti koje želite provoditi, dodajte izričiti popis dopuštenih vrijednosti i pokrijte ga ugovornim testovima.
Izgradite ograničen API klijent svjestan ponovnih pokušaja
Klijent u nastavku dvaput ponovno pokušava kod neuspjelih veza i pogrešaka poslužitelja, uz kratko ograničeno čekanje. Ne pokušava ponovno kod neuspjeha autentikacije, neuspjeha provjere ni HTTP 429 odgovora. Odgovor o ograničenju brzine ili kvoti zahtijeva upravljanje kapacitetom, a ne tijesnu petlju ponovnih pokušaja.
<?php
// app/Services/EmailValidatorClient.php
namespace App\Services;
use App\Domain\EmailValidation\EmailValidationResult;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use InvalidArgumentException;
use RuntimeException;
use Throwable;
final class ValidationUnavailable extends RuntimeException
{
public function __construct(
public readonly string $reason,
?Throwable $previous = null,
) {
parent::__construct($reason, 0, $previous);
}
}
final class EmailValidatorClient
{
public function check(string $email): EmailValidationResult
{
$delays = [0, 200_000, 600_000];
for ($attempt = 1; $attempt <= count($delays); $attempt++) {
if ($delays[$attempt - 1] > 0) {
usleep($delays[$attempt - 1]);
}
try {
$response = Http::baseUrl(config('services.email_validator.base_url'))
->acceptJson()
->connectTimeout(config('services.email_validator.connect_timeout'))
->timeout(config('services.email_validator.timeout'))
->get('/v1/check-email', [
'token' => config('services.email_validator.token'),
'email' => $email,
]);
} catch (ConnectionException $exception) {
Log::warning('email_validation_connection_failure', [
'attempt' => $attempt,
'email_hash' => hash('sha256', $email),
]);
if ($attempt === count($delays)) {
throw new ValidationUnavailable('connection_failure', $exception);
}
continue;
}
if ($response->successful()) {
$payload = $response->json();
if (!is_array($payload)) {
throw new ValidationUnavailable('malformed_response');
}
try {
return EmailValidationResult::fromPayload($payload);
} catch (InvalidArgumentException $exception) {
throw new ValidationUnavailable('malformed_response', $exception);
}
}
if ($response->status() === 429) {
throw new ValidationUnavailable('rate_or_quota_limited');
}
if (in_array($response->status(), [401, 403], true)) {
throw new ValidationUnavailable('authentication_failure');
}
if ($response->serverError() && $attempt < count($delays)) {
Log::warning('email_validation_server_failure', [
'attempt' => $attempt,
'status' => $response->status(),
]);
continue;
}
throw new ValidationUnavailable(
$response->serverError() ? 'upstream_failure' : 'request_rejected'
);
}
throw new ValidationUnavailable('upstream_failure');
}
}
Dnevnici sadrže jednosmjerni hash e-pošte, broj pokušaja i status — ne adresu, token, puni URL ni tijelo odgovora. To je posebno važno jer se autentikacija nizom upita inače može otkriti u zapisima proxyja, praćenja ili iznimki.
Uvezite CSV i izradite red za pregled
Ulazna datoteka treba zaglavlje email. Naredba uklanja razmake, malim slovima pretvara samo dio domene, lokalno odbacuje jasne sintaksne neuspjehe i poziva uslugu za uvjerljive adrese.
<?php
// app/Console/Commands/ImportNewsletterContacts.php
namespace App\Console\Commands;
use App\Domain\EmailValidation\ImportDecisionPolicy;
use App\Models\NewsletterContact;
use App\Services\EmailValidatorClient;
use App\Services\ValidationUnavailable;
use Illuminate\Console\Command;
use SplFileObject;
final class ImportNewsletterContacts extends Command
{
protected $signature = 'newsletter:import {file}';
protected $description = 'Validate and import newsletter contacts';
public function handle(
EmailValidatorClient $client,
ImportDecisionPolicy $policy,
): int {
$path = $this->argument('file');
if (!is_readable($path)) {
$this->error('The CSV file is not readable.');
return self::FAILURE;
}
$csv = new SplFileObject($path);
$csv->setFlags(
SplFileObject::READ_CSV
| SplFileObject::SKIP_EMPTY
| SplFileObject::DROP_NEW_LINE
);
$headers = $csv->fgetcsv();
$headers = is_array($headers)
? array_map(fn ($value) => strtolower(trim((string) $value)), $headers)
: [];
$emailColumn = array_search('email', $headers, true);
if ($emailColumn === false) {
$this->error('The CSV must contain an email header.');
return self::FAILURE;
}
$counts = ['accepted' => 0, 'rejected' => 0, 'review' => 0];
foreach ($csv as $row) {
if (!is_array($row) || !isset($row[$emailColumn])) {
continue;
}
$email = $this->normalize((string) $row[$emailColumn]);
if ($email === '') {
continue;
}
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
$this->save($email, 'rejected', null, 'local_syntax');
$counts['rejected']++;
continue;
}
try {
$result = $client->check($email);
$decision = $policy->decide($result);
$this->save($email, $decision, $result->evidence(), null);
} catch (ValidationUnavailable $exception) {
$decision = 'review';
$this->save($email, $decision, null, $exception->reason);
}
$counts[$decision]++;
}
$this->line(json_encode($counts, JSON_THROW_ON_ERROR));
return self::SUCCESS;
}
private function normalize(string $email): string
{
$email = trim($email);
$separator = strrpos($email, '@');
if ($separator === false) {
return $email;
}
return substr($email, 0, $separator + 1)
. strtolower(substr($email, $separator + 1));
}
private function save(
string $email,
string $decision,
?array $evidence,
?string $failure,
): void {
NewsletterContact::updateOrCreate(
['email' => $email],
[
'decision' => $decision,
'validation_evidence' => $evidence,
'failure_code' => $failure,
],
);
}
}
Testirajte uspjeh, nesigurnost, ponovne pokušaje i neuspjehe kvote
Laravelov HTTP lažni odgovor održava testove determinističkima i jamči da nijedan test ne doseže stvarnu uslugu. Dodajte reprezentativne ugovorne testove:
<?php
// tests/Unit/EmailValidatorClientTest.php
namespace Tests\Unit;
use App\Domain\EmailValidation\ImportDecisionPolicy;
use App\Services\EmailValidatorClient;
use App\Services\ValidationUnavailable;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
final class EmailValidatorClientTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
config([
'services.email_validator.base_url' =>
'https://ai.mihajlo.mk/api/email-validator',
'services.email_validator.token' => 'TEST_SERVICE_TOKEN',
'services.email_validator.connect_timeout' => 1,
'services.email_validator.timeout' => 2,
'services.email_validator.accept_score' => 85,
'services.email_validator.reject_score' => 45,
]);
Http::preventStrayRequests();
}
public function test_high_score_with_complete_evidence_is_accepted(): void
{
Http::fake(['*' => Http::response($this->payload(92), 200)]);
$result = app(EmailValidatorClient::class)->check('[email protected]');
$this->assertSame(
'accepted',
app(ImportDecisionPolicy::class)->decide($result)
);
}
public function test_uncertain_score_is_sent_to_review(): void
{
Http::fake(['*' => Http::response($this->payload(65), 200)]);
$result = app(EmailValidatorClient::class)->check('[email protected]');
$this->assertSame(
'review',
app(ImportDecisionPolicy::class)->decide($result)
);
}
public function test_server_error_is_retried(): void
{
Http::fake([
'*' => Http::sequence()
->push([], 500)
->push($this->payload(90), 200),
]);
app(EmailValidatorClient::class)->check('[email protected]');
Http::assertSentCount(2);
}
public function test_rate_limit_becomes_structured_failure(): void
{
Http::fake(['*' => Http::response([], 429)]);
try {
app(EmailValidatorClient::class)->check('[email protected]');
$this->fail('Expected ValidationUnavailable');
} catch (ValidationUnavailable $exception) {
$this->assertSame('rate_or_quota_limited', $exception->reason);
}
Http::assertSentCount(1);
}
private function payload(int $score): array
{
return [
'status' => 'test-status',
'score' => $score,
'recommendation' => 'test-recommendation',
'checks' => ['test-check' => true],
'quota' => ['test-quota' => 1],
];
}
}
Sigurno implementirajte i provjerite
Pokrenite migracije, predmemorirajte konfiguraciju nakon umetanja produkcijskog tokena, izvršite testove, a zatim uvezite malu reprezentativnu datoteku:
php artisan test
php artisan migrate --force
php artisan config:cache
php artisan newsletter:import storage/app/imports/subscribers.csv
Ograničite dozvole za CSV i datoteke okruženja, zahtijevajte TLS te konfigurirajte proxyje i alate za performanse aplikacije da skrivaju nizove upita. Pratite brojke po odluci, kodove neuspjeha, HTTP 429 odgovore, neuspjehe autentikacije, uzvodne neuspjehe i promjene omjera prihvaćenih prema onima za pregled. Upozoravajte na nagle promjene umjesto zapisivanja osobnih podataka.
Uobičajeni neuspjesi imaju različita rješenja. Neuspjeh autentikacije obično znači da konfigurirani token nedostaje, zastario je ili je opozvan. Odgovor 429 zahtijeva provjeru kapaciteta plana i tempa uvoza. Ponovljeni neuspjesi poslužitelja ili veze trebali bi ostaviti kontakte na pregledu do kontroliranog ponovnog pokretanja. Neispravan odgovor trebao bi potaknuti istragu prije promjena politike. Nedostajuće CSV zaglavlje problem je ugovora ulaza i ispravno zaustavlja cijelu naredbu.
Završni kontrolni popis za provjeru
- Token postoji samo u konfiguraciji podržanoj okruženjem i produkcijskoj pohrani tajni.
- Minimalni API zahtjev uspijeva s točnim GET krajnjim pristupom i parametrima upita.
- Konfiguracija je predmemorirana nakon promjena tokena.
- Automatizirani testovi ne mogu upućivati nepredviđene mrežne zahtjeve.
- Uzorci s visokom pouzdanošću, niskom pouzdanošću i nesigurni uzorci dosežu predviđena stanja.
- Putanje za HTTP 429, autentikaciju, istek vremena, pogrešku poslužitelja i neispravan odgovor mogu se pratiti.
- Recenzenti mogu pregledati dokaze o statusu, rezultatu, preporuci, provjerama i kvoti.
- Samo se kontakti s odlukom
acceptedizvoze na platformu za slanje.
Pouzdan uvoz newslettera nije binarni provjeravatelj e-pošte omotan petljom. To je cjevovod odluka koji razlikuje dokaze od politike, privremeni neuspjeh od odbijanja te automatizaciju od opravdane nesigurnosti. Očuvajte te razlike i red za ručnu provjeru postat će sigurnosna značajka, a ne mjesto za odlaganje.