Laravel Newsletter: Provjerite i razvrstajte kontakte uz Email Intelligence
Uvoz newslettera postaje rizičan čim se „email” tretira kao pouzdano polje. Tipfeleri, nepostojeće domene, nedostajući razmjenjivači pošte, pružatelji jednokratnih adresa i dvosmisleni signali dostave mogu bezopasan CSV pretvoriti u uzaludna slanja i narušenu reputaciju pošiljatelja.
Ovaj vodič izrađuje konzervativni Laravelov kanal za uvoz. Lokalno odbacuje očite sintaksne pogreške, poziva servis za email-inteligenciju za uvjerljive adrese, zapisuje kontakte visokog povjerenja u čisti CSV te svaku nesigurnu ili neuspjelu procjenu usmjerava u CSV za ručni pregled. Ključno je da prekid rada API-ja nikada ne postane slučajno odobrenje.
Dobijte pristup prije pisanja integracijskog koda
Započnite registracijom računa ili upotrijebite stranicu za prijavu ako ga već imate.
- Otvorite stranicu servisa Email Validator.
- Odaberite dostupni plan Free, Plus ili Pro i dovršite aktivaciju.
- Otvorite službenu dokumentaciju servisa.
- Pronađite ploču Service token i kopirajte njezin token ograničen na servis.
Ponovno generiranje ovog tokena opoziva prethodno aktivni token. Ponovno generiranje tretirajte kao rotaciju vjerodajnica: ažurirajte svako implementirano okruženje prije oslanjanja na novu vrijednost i nikada nemojte lijepiti nijedan token u kontrolu izvornog koda, zapisnike, snimke zaslona ili testne fixturee.
Točan zahtjev je HTTPS GET prema https://ai.mihajlo.mk/api/email-validator/v1/check-email. Autentikacija koristi parametar upita token={serviceToken}, dok adresa ide u parametar upita email.
Izvršite jedan minimalni zahtjev iz pouzdanog terminala:
curl --get 'https://ai.mihajlo.mk/api/email-validator/v1/check-email' \
--data-urlencode 'token=YOUR_SERVICE_TOKEN' \
--data-urlencode '[email protected]'
Budući da se autentikacija pojavljuje u nizu upita, izbjegavajte opširan izlaz naredbe i osigurajte da obrnuti proxyji, agenti za praćenje i HTTP dijagnostika prikrivaju URL-ove ili parametre upita. Sama aplikacija nikada neće zapisivati URL zahtjeva.
Sada smjestite vjerodajnicu u Laravelovu konfiguraciju okruženja:
EMAIL_VALIDATOR_TOKEN=YOUR_SERVICE_TOKEN
EMAIL_VALIDATOR_TIMEOUT=8
EMAIL_VALIDATOR_CONNECT_TIMEOUT=3
# These are application policy values, not hard-coded API assumptions.
# Copy the exact clean classifications documented for your activated service.
EMAIL_VALIDATOR_CLEAN_STATUSES=YOUR_DOCUMENTED_CLEAN_STATUS
EMAIL_VALIDATOR_CLEAN_RECOMMENDATIONS=YOUR_DOCUMENTED_CLEAN_RECOMMENDATION
EMAIL_VALIDATOR_MINIMUM_SCORE=YOUR_CHOSEN_SCORE_THRESHOLD
Ugovor odgovora pruža status, score, recommendation, checks i quota. Njihov točan rječnik vrijednosti treba doći iz službene dokumentacije, a ne iz pretpostavki ugrađenih u PHP. Ova implementacija stoga čini prihvaćene klasifikacije i prag rezultata upravljanima okruženjem. Dok stvarne vrijednosti ne zamijene rezervirana mjesta, pravilo namjerno šalje svaku adresu na pregled.
Odaberite malu arhitekturu svjesnu kvarova
Zakazana Artisan naredba dobro odgovara uobičajenim CSV uvozima: može se pokrenuti putem raspoređivača, zadatka implementacije ili sesije operatera bez uvođenja redova, radnika ili površine za prijenos putem weba. Ako uvozi kasnije stižu putem HTTP-a, iste klase servisa i pravila mogu se pozvati iz zadatka u redu.
Projekt dodaje četiri fokusirane datoteke:
app/Services/EmailValidator.phpupravlja HTTP ponašanjem i validacijom odgovora.app/Data/EmailAssessment.phppreslikava nepouzdani JSON u objekt domene.app/Domain/ContactDecision.phpprimjenjuje lokalno pravilo prihvaćanja.app/Console/Commands/TriageNewsletterImport.phpčita i zapisuje CSV datoteke.
Dodajte konfiguraciju servisa u config/services.php:
'email_validator' => [
'endpoint' => 'https://ai.mihajlo.mk/api/email-validator/v1/check-email',
'token' => env('EMAIL_VALIDATOR_TOKEN'),
'timeout' => (int) env('EMAIL_VALIDATOR_TIMEOUT', 8),
'connect_timeout' => (int) env('EMAIL_VALIDATOR_CONNECT_TIMEOUT', 3),
'clean_statuses' => env('EMAIL_VALIDATOR_CLEAN_STATUSES', ''),
'clean_recommendations' => env('EMAIL_VALIDATOR_CLEAN_RECOMMENDATIONS', ''),
'minimum_score' => env('EMAIL_VALIDATOR_MINIMUM_SCORE'),
],
Preslikajte odgovor na granici aplikacije
Udaljeni JSON je nepouzdan ulaz čak i kada je pružatelj usluge ispravan. Odbacite nedostajuća polja i neočekivane vrste spremnika prije nego što ih poslovni kod vidi. Nemojte nametati nedokumentirane granice za score; provjerite je li numerički i prepustite konfiguraciji definiranje poslovnog praga.
<?php
namespace App\Data;
use UnexpectedValueException;
final readonly class EmailAssessment
{
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
{
if (
!isset($payload['status']) || !is_string($payload['status']) ||
!array_key_exists('score', $payload) || !is_numeric($payload['score']) ||
!isset($payload['recommendation']) || !is_string($payload['recommendation']) ||
!isset($payload['checks']) || !is_array($payload['checks']) ||
!isset($payload['quota']) || !is_array($payload['quota'])
) {
throw new UnexpectedValueException('Unexpected email-validator response.');
}
return new self(
trim($payload['status']),
(float) $payload['score'],
trim($payload['recommendation']),
$payload['checks'],
$payload['quota'],
);
}
}
Izradite ograničeni HTTP klijent
Laravelov ugrađeni HTTP klijent pruža vremenska ograničenja za povezivanje i ukupni odgovor. Klijent ponovno pokušava samo kod neuspjeha povezivanja i odgovora poslužitelja 5xx, uz dva kratka odgađanja. Ne pokušava naslijepo ponovno kod neuspjele autentikacije, odbijenog unosa ili 429, koji može predstavljati privremeno ograničenje stope ili iscrpljenu kvotu.
<?php
namespace App\Services;
use App\Data\EmailAssessment;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Throwable;
final class EmailValidator
{
public function check(string $email): array
{
$token = (string) config('services.email_validator.token');
if ($token === '' || str_starts_with($token, 'YOUR_')) {
return ['assessment' => null, 'failure' => 'configuration'];
}
for ($attempt = 1; $attempt <= 3; $attempt++) {
try {
$response = Http::acceptJson()
->connectTimeout(config('services.email_validator.connect_timeout'))
->timeout(config('services.email_validator.timeout'))
->get(config('services.email_validator.endpoint'), [
'token' => $token,
'email' => $email,
]);
} catch (ConnectionException $exception) {
Log::warning('Email validation connection failure.', [
'attempt' => $attempt,
'exception' => $exception::class,
]);
if ($attempt < 3) {
usleep($attempt * 200_000);
continue;
}
return ['assessment' => null, 'failure' => 'connection'];
}
if ($response->successful()) {
try {
$payload = $response->json();
if (!is_array($payload)) {
throw new \UnexpectedValueException('JSON object expected.');
}
return [
'assessment' => EmailAssessment::fromPayload($payload),
'failure' => null,
];
} catch (Throwable $exception) {
Log::warning('Malformed email validation response.', [
'http_status' => $response->status(),
]);
return ['assessment' => null, 'failure' => 'malformed_response'];
}
}
if (in_array($response->status(), [401, 403], true)) {
return ['assessment' => null, 'failure' => 'authentication'];
}
if ($response->status() === 429) {
return ['assessment' => null, 'failure' => 'quota_or_rate_limit'];
}
if ($response->serverError() && $attempt < 3) {
usleep($attempt * 200_000);
continue;
}
return [
'assessment' => null,
'failure' => $response->clientError()
? 'request_rejected'
: 'upstream_unavailable',
];
}
return ['assessment' => null, 'failure' => 'upstream_unavailable'];
}
}
Zapisnici sadrže pokušaj i kategoriju neuspjeha, ali ne adresu, token, tijelo odgovora ili URL. To je dovoljno za operativnu dijagnostiku bez pretvaranja zapisnika u bazu kontakata.
Pretvorite inteligenciju u konzervativnu odluku
Adresa je čista samo kada su njezin status i preporuka izričito dopušteni, njezin rezultat zadovoljava odabrani prag, njezine provjere sadrže barem jedan Booleov signal bez izričitog false te su podaci o kvoti prisutni. Sve ostalo postaje materijal za pregled.
<?php
namespace App\Domain;
use App\Data\EmailAssessment;
use RecursiveArrayIterator;
use RecursiveIteratorIterator;
final class ContactDecision
{
public function decide(EmailAssessment $assessment): array
{
$statuses = $this->csvConfig('clean_statuses');
$recommendations = $this->csvConfig('clean_recommendations');
$minimum = config('services.email_validator.minimum_score');
if ($statuses === [] || $recommendations === [] || !is_numeric($minimum)) {
return [false, 'policy_not_configured'];
}
if (!in_array(strtolower($assessment->status), $statuses, true)) {
return [false, 'status_requires_review'];
}
if (!in_array(strtolower($assessment->recommendation), $recommendations, true)) {
return [false, 'recommendation_requires_review'];
}
if ($assessment->score < (float) $minimum) {
return [false, 'score_below_threshold'];
}
if ($assessment->quota === []) {
return [false, 'quota_data_missing'];
}
$booleanSignals = 0;
foreach (new RecursiveIteratorIterator(
new RecursiveArrayIterator($assessment->checks)
) as $value) {
if (is_bool($value)) {
$booleanSignals++;
if ($value === false) {
return [false, 'check_failed'];
}
}
}
return $booleanSignals > 0
? [true, 'clean']
: [false, 'checks_unusable'];
}
private function csvConfig(string $key): array
{
return array_values(array_filter(array_map(
static fn (string $value): string => strtolower(trim($value)),
explode(',', (string) config("services.email_validator.$key"))
)));
}
}
Uvezite kontakte i izradite red za pregled
Ulaz mora imati zaglavlje email. Očiti sintaksni neuspjesi i duplikati obrađuju se lokalno, čime se štedi kvota. Uspješne procjene zadržavaju svih pet polja servisa u izlazu kako bi recenzenti mogli razumjeti odluku.
<?php
namespace App\Console\Commands;
use App\Domain\ContactDecision;
use App\Services\EmailValidator;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
final class TriageNewsletterImport extends Command
{
protected $signature = 'newsletter:triage {csv}';
protected $description = 'Clean a newsletter CSV and route uncertain contacts to review.';
public function handle(EmailValidator $validator, ContactDecision $policy): int
{
$path = realpath($this->argument('csv'));
if ($path === false || !is_readable($path)) {
$this->error('The CSV is not readable.');
return self::FAILURE;
}
$input = fopen($path, 'rb');
$headers = fgetcsv($input);
$emailIndex = is_array($headers) ? array_search('email', $headers, true) : false;
if ($emailIndex === false) {
fclose($input);
$this->error('The CSV must contain an email header.');
return self::FAILURE;
}
$directory = 'import-results/'.now()->format('Ymd-His');
Storage::disk('local')->makeDirectory($directory);
$root = Storage::disk('local')->path($directory);
$clean = fopen($root.'/clean.csv', 'wb');
$review = fopen($root.'/review.csv', 'wb');
$columns = ['email', 'reason', 'status', 'score', 'recommendation', 'checks', 'quota'];
fputcsv($clean, $columns);
fputcsv($review, $columns);
$seen = [];
$exit = self::SUCCESS;
while (($row = fgetcsv($input)) !== false) {
$email = trim((string) ($row[$emailIndex] ?? ''));
$key = strtolower($email);
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
fputcsv($review, [$email, 'invalid_syntax', '', '', '', '', '']);
continue;
}
if (isset($seen[$key])) {
fputcsv($review, [$email, 'duplicate', '', '', '', '', '']);
continue;
}
$seen[$key] = true;
$result = $validator->check($email);
if ($result['assessment'] === null) {
fputcsv($review, [$email, $result['failure'], '', '', '', '', '']);
if (in_array($result['failure'], [
'configuration', 'authentication', 'quota_or_rate_limit',
], true)) {
$exit = self::FAILURE;
break;
}
continue;
}
$assessment = $result['assessment'];
[$isClean, $reason] = $policy->decide($assessment);
$record = [
$email,
$reason,
$assessment->status,
$assessment->score,
$assessment->recommendation,
json_encode($assessment->checks, JSON_THROW_ON_ERROR),
json_encode($assessment->quota, JSON_THROW_ON_ERROR),
];
fputcsv($isClean ? $clean : $review, $record);
}
fclose($input);
fclose($clean);
fclose($review);
$this->info("Results written to storage/app/$directory");
return $exit;
}
}
Pokrenite ga s php artisan newsletter:triage storage/app/imports/contacts.csv. Izlazni kod različit od nule tretirajte kao djelomični uvoz: ispravite problem s tokenom ili kvotom i ponovno pokrenite izvornu datoteku. Nikada nemojte objaviti clean.csv osim ako je naredba uspješno dovršena.
Testirajte uspjeh, neispravne podatke i ponovne pokušaje
Http::fake() održava testove determinističkima i sprječava stvarnu potrošnju kvote. Klasifikacije fixturea u nastavku namjerno su lokalne testne vrijednosti, a ne tvrdnje o dokumentiranom rječniku servisa.
<?php
namespace Tests\Feature;
use App\Services\EmailValidator;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
final class EmailValidatorTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
config([
'services.email_validator.endpoint' =>
'https://ai.mihajlo.mk/api/email-validator/v1/check-email',
'services.email_validator.token' => 'test-token',
'services.email_validator.timeout' => 8,
'services.email_validator.connect_timeout' => 3,
]);
}
public function test_it_maps_a_complete_response(): void
{
Http::fake([
'*' => Http::response([
'status' => 'test-clean',
'score' => 92,
'recommendation' => 'test-accept',
'checks' => ['syntax' => true, 'mx' => true],
'quota' => ['test' => 'present'],
]),
]);
$result = app(EmailValidator::class)->check('[email protected]');
$this->assertNull($result['failure']);
$this->assertSame(92.0, $result['assessment']->score);
Http::assertSent(function ($request): bool {
parse_str((string) parse_url($request->url(), PHP_URL_QUERY), $query);
return $request->method() === 'GET'
&& $query['token'] === 'test-token'
&& $query['email'] === '[email protected]';
});
}
public function test_it_rejects_a_malformed_response(): void
{
Http::fake(['*' => Http::response(['status' => 'incomplete'])]);
$result = app(EmailValidator::class)->check('[email protected]');
$this->assertSame('malformed_response', $result['failure']);
}
public function test_it_retries_a_server_failure_then_succeeds(): void
{
Http::fakeSequence()
->push([], 503)
->push([
'status' => 'test-clean',
'score' => 92,
'recommendation' => 'test-accept',
'checks' => ['syntax' => true],
'quota' => ['test' => 'present'],
], 200);
$result = app(EmailValidator::class)->check('[email protected]');
$this->assertNull($result['failure']);
Http::assertSentCount(2);
}
}
Pokrenite paket s php artisan test. Dodatni test naredbe trebao bi upotrijebiti privremeni CSV i potvrditi da nevaljana sintaksa, duplikati, procjene niskog povjerenja i transportni neuspjesi završavaju u review.csv.
Sigurnost, implementacija i nadzornosti
- Ubrizgajte token kroz spremište tajni platforme za implementaciju, zatim pokrenite
php artisan config:cache. Kod aplikacije čita samoconfig(), stoga predmemorirana konfiguracija ostaje pouzdana. - Ograničite uvezene datoteke i generirane rezultate jer sadrže osobne podatke. Primijenite ograničenja zadržavanja i ne postavljajte
storage/apppod javni web-korijen. - Pratite brojanja po ishodu i kategoriji neuspjeha, trajanje naredbe, broj ponovnih pokušaja i izlaze različite od nule. Izbjegavajte oznake koje sadrže email adrese jer stvaraju metrike visoke kardinalnosti i otkrivaju kontakte.
- Uskladite konkurentnost s aktiviranim planom. Paraleliziranje tisuća zahtjeva bez potvrđenih ograničenja može rutinski uvoz pretvoriti u ponovljene neuspjehe
429. - Pri rotaciji servisnog tokena imajte na umu da ponovno generiranje odmah poništava prethodno aktivni token. Ažurirajte tajne i ponovno izgradite predmemoriranu konfiguraciju prije sljedećeg uvoza.
Uobičajeni obrasci neuspjeha
401 ili 403 obično znači da servisni token nedostaje, opozvan je ili je netočan; ponovni pokušaj to ne može popraviti. 429 zahtijeva provjeru uvjeta kvote ili ograničenja stope prije nastavka. Ponovljeni 5xx ili neuspjesi povezivanja trebaju ostaviti kontakte na pregledu, a ne u čistima. Uspješan HTTP odgovor s nedostajućim poljima jednako je nesiguran i postaje malformed_response.
Ako svaki kontakt primi policy_not_configured, granica radi prema namjeri: zamijenite rezervirani status, preporuku i prag vrijednostima odabranima iz službene dokumentacije i vaše politike slanja, zatim osvježite Laravelovu predmemoriju konfiguracije.
Završni kontrolni popis za provjeru
- Aktivirani servisni token postoji samo u konfiguraciji podržanoj okruženjem.
- Zahtjev koristi
GET, točno navedenu krajnju točku te parametre upitatokeniemail. - Svih pet polja odgovora preslikano je i zadržano za odluke ili pregled.
- Samo izričito odobrene klasifikacije mogu ući u
clean.csv. - Nevaljani, duplicirani, nesigurni, neispravni i nedostupni rezultati ulaze u
review.csv. - Neuspjesi autentikacije i kvote zaustavljaju uvoz izlazom različitim od nule.
- Testovi koriste
Http::fake()i nikada ne kontaktiraju aktivni servis. - Zapisnici, metrike i konfiguracija proxyja ne otkrivaju tokene ni adrese.
Vrijedan izlaz nije najveći mogući popis čistih kontakata. To je popis čija se odobrenja mogu objasniti i čija nesigurnost ostaje vidljiva. Time što pregled čini zadanim stanjem pri neuspjehu, ovaj Laravelov kanal štiti i publiku newslettera i reputaciju potrebnu da se do nje dođe.