Laravel obrasci za kontakt: Nepogrešiva provjera valjanosti e-pošte uz predmemoriranje i rezervnu opciju
Obrazac za kontakt trebao bi odbaciti očite slijepe ulice, a da pritom ne postane ovisan o drugoj usluzi za prihvaćanje legitimnih poruka. Ta je napetost srž provjere e-pošte spremne za produkciju. Sama provjera sintakse propušta nepostojeće domene i rizične signale isporuke, dok stroga udaljena ovisnost može pretvoriti prekid rada pružatelja usluge u nefunkcionalnu stranicu za kontakt.
Ovaj vodič izrađuje Laravel obrazac za kontakt koji poziva uslugu za provjeru e-pošte, prevodi njezin odgovor u odluku na razini aplikacije, predmemorira uspješne procjene i dopušta nastavak rada kada je provjera privremeno nedostupna. Obrazac i dalje koristi Laravelovu lokalnu provjeru kao prvu liniju obrane te nikada ne izlaže token usluge pregledniku.
Dobijte pristup i kopirajte token usluge
Registrirajte se na https://ai.mihajlo.mk/register ili upotrijebite https://ai.mihajlo.mk/login ako već imate račun.
- Otvorite stranicu usluge Email Validator.
- Odaberite dostupni paket Free, Plus ili Pro i dovršite njegovu aktivaciju.
- Otvorite službenu dokumentaciju usluge.
- Pronađite ploču Service token i kopirajte token ograničen na uslugu.
- Pohranite ga u Laravelovu konfiguraciju okruženja, nikada u predani PHP ili JavaScript.
Ova usluga zahtijeva token. Autentikacija koristi parametar upita token={serviceToken}. Ponovno generiranje tokena opoziva prethodno aktivni token, stoga rotacija tokena mora uključivati ažuriranje svakog implementiranog okruženja koje ga koristi.
Potvrdite krajnju točku prije pisanja Laravel koda
Točan API poziv je GET https://ai.mihajlo.mk/api/email-validator/v1/check-email. Prihvaća parametre upita email i token.
curl --get 'https://ai.mihajlo.mk/api/email-validator/v1/check-email' \
--data-urlencode '[email protected]' \
--data-urlencode 'token=YOUR_SERVICE_TOKEN'
Pregledajte ovaj odgovor zajedno sa službenom dokumentacijom prije odabira poslovnih pragova. Integracija u nastavku koristi status, score, recommendation, checks i quota, ali provjerava njihove tipove umjesto da pretpostavi kako svaki uspješan HTTP odgovor ima upotrebljivo tijelo.
Dodajte vrijednosti specifične za implementaciju u .env:
EMAIL_VALIDATOR_TOKEN=YOUR_SERVICE_TOKEN
EMAIL_VALIDATOR_MIN_SCORE=60
EMAIL_VALIDATOR_DENY_RECOMMENDATIONS=reject,invalid,undeliverable
EMAIL_VALIDATOR_CACHE_HOURS=12
[email protected]
Prag rezultata i popis za odbijanje pravilo su aplikacije, a ne tvrdnja da su to jedine moguće vrijednosti preporuka usluge. Uskladite ih s trenutačnom dokumentacijom, svojim testnim odgovorom i tolerancijom na lažna odbijanja.
Arhitektura: stroga na granici, otporna u obrascu
Zahtjev slijedi namjerno kratku putanju:
- Laravel lokalno provjerava ime, sintaksu e-pošte i poruku.
- Namjenski klijent normalizira e-poštu i provjerava ključ predmemorije raspršenog sadržaja.
- Pri promašaju predmemorije poslužitelj poziva validator s ograničenim vremenskim ograničenjima.
- DTO provjerava i mapira vanjski odgovor.
- Pravilo vraća
allow,denyiliunavailable. denyvraća pogrešku polja;unavailableprihvaća poruku prema pravilu gracioznog povratnog postupka.
Dopuštanje nastavka rada prikladno je za običan obrazac za kontakt jer je gubitak stvarnog upita obično gori od primanja jedne upitne adrese. Ponovno postavljanje lozinke, provjere vlasništva računa i financijski tijekovi rada trebali bi koristiti drukčije pravilo.
Relevantne datoteke projekta su config/services.php, app/Services/EmailValidator.php, app/Data/EmailAssessment.php, app/Http/Requests/ContactRequest.php, app/Http/Controllers/ContactController.php, app/Mail/ContactMessage.php, Blade prikazi, routes/web.php i test značajke.
Konfigurirajte granicu usluge
Dodajte sljedeći unos unutar polja koje vraća config/services.php:
'email_validator' => [
'url' => 'https://ai.mihajlo.mk/api/email-validator/v1/check-email',
'token' => env('EMAIL_VALIDATOR_TOKEN'),
'minimum_score' => (float) env('EMAIL_VALIDATOR_MIN_SCORE', 60),
'deny_recommendations' => array_values(array_filter(array_map(
'trim',
explode(',', env(
'EMAIL_VALIDATOR_DENY_RECOMMENDATIONS',
'reject,invalid,undeliverable'
))
))),
'cache_hours' => (int) env('EMAIL_VALIDATOR_CACHE_HOURS', 12),
],
Indirekcija konfiguracije čini Laravelovu predmemoriju konfiguracije sigurnom za upotrebu i čuva tajne izvan kontrole izvornog koda. Ne pozivajte env() iz klasa aplikacije.
Mapirajte odgovor u rezultat domene
Izradite app/Data/EmailAssessment.php. Mapper prihvaća polja najviše razine ili polja unutar objekta data, a zatim odbacuje neispravne odgovore na granici aplikacije.
<?php
namespace App\Data;
final readonly class EmailAssessment
{
public function __construct(
public string $decision,
public ?string $status = null,
public int|float|null $score = null,
public ?string $recommendation = null,
public array $checks = [],
public array $quota = [],
public ?string $failure = null,
) {}
public static function unavailable(string $failure): self
{
return new self('unavailable', failure: $failure);
}
public static function fromPayload(array $json): self
{
$data = is_array($json['data'] ?? null) ? $json['data'] : [];
$body = array_merge($json, $data);
$status = $body['status'] ?? null;
$score = $body['score'] ?? null;
$recommendation = $body['recommendation'] ?? null;
$checks = $body['checks'] ?? null;
$quota = $body['quota'] ?? null;
if (! is_scalar($status)
|| ! is_numeric($score)
|| ! is_string($recommendation)
|| $recommendation === ''
|| ! is_array($checks)
|| $checks === []
|| ! is_array($quota)) {
return self::unavailable('malformed_response');
}
$normalizedStatus = strtolower((string) $status);
$normalizedRecommendation = strtolower(trim($recommendation));
$deny = array_map(
fn (string $value) => strtolower($value),
config('services.email_validator.deny_recommendations', [])
);
$remaining = data_get($quota, 'remaining');
if (in_array($normalizedStatus, ['error', 'failed', 'failure'], true)) {
return self::unavailable('service_status');
}
if (is_numeric($remaining) && (float) $remaining <= 0) {
return self::unavailable('quota_exhausted');
}
$decision = (float) $score
< (float) config('services.email_validator.minimum_score')
|| in_array($normalizedRecommendation, $deny, true)
? 'deny'
: 'allow';
return new self(
decision: $decision,
status: (string) $status,
score: (float) $score,
recommendation: $recommendation,
checks: $checks,
quota: $quota,
);
}
}
Cjelokupna struktura checks ostaje dostupna za buduće usavršavanje pravila bez povezivanja obrasca s nedokumentiranim ugniježđenim poljima. Neispravan rezultat postaje unavailable, nikada slučajno odbijanje.
Pozovite API s predmemoriranjem, vremenskim ograničenjima i selektivnim ponovnim pokušajima
Izradite app/Services/EmailValidator.php:
<?php
namespace App\Services;
use App\Data\EmailAssessment;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
final class EmailValidator
{
public function check(string $email): EmailAssessment
{
$normalized = strtolower(trim($email));
$cacheKey = 'email-validator:'.hash('sha256', $normalized);
if ($cached = Cache::get($cacheKey)) {
return $cached;
}
$token = config('services.email_validator.token');
if (! is_string($token) || $token === '') {
Log::error('Email validator token is not configured');
return EmailAssessment::unavailable('missing_token');
}
for ($attempt = 1; $attempt <= 3; $attempt++) {
try {
$response = Http::acceptJson()
->connectTimeout(2)
->timeout(5)
->get(config('services.email_validator.url'), [
'email' => $normalized,
'token' => $token,
]);
} catch (ConnectionException) {
if ($attempt < 3) {
usleep($attempt === 1 ? 100_000 : 300_000);
continue;
}
Log::warning('Email validator connection failed', [
'email_hash' => substr(hash('sha256', $normalized), 0, 12),
]);
return EmailAssessment::unavailable('connection_failed');
}
if (in_array($response->status(), [500, 502, 503, 504], true)
&& $attempt < 3) {
usleep($attempt === 1 ? 100_000 : 300_000);
continue;
}
if ($response->status() === 429) {
Log::warning('Email validator rate or quota limit reached');
return EmailAssessment::unavailable('rate_limited');
}
if (! $response->successful()) {
Log::warning('Email validator rejected the request', [
'http_status' => $response->status(),
]);
return EmailAssessment::unavailable('http_'.$response->status());
}
$json = $response->json();
$assessment = is_array($json)
? EmailAssessment::fromPayload($json)
: EmailAssessment::unavailable('invalid_json');
if ($assessment->decision !== 'unavailable') {
Cache::put(
$cacheKey,
$assessment,
now()->addHours(
config('services.email_validator.cache_hours', 12)
)
);
}
Log::info('Email validation completed', [
'decision' => $assessment->decision,
'status' => $assessment->status,
'score' => $assessment->score,
'recommendation' => $assessment->recommendation,
]);
return $assessment;
}
return EmailAssessment::unavailable('unexpected_failure');
}
}
Ponovno se pokušavaju samo neuspjele veze i vjerojatno prolazni kvarovi poslužitelja. Pogreške autentikacije, neispravni zahtjevi i odgovori o kvoti ne ponavljaju se naslijepo. Neuspješne se procjene ne predmemoriraju, što omogućuje oporavak čim usluga postane dostupna.
Povežite validator s obrascem za kontakt
Izradite zahtjev naredbom php artisan make:request ContactRequest, a zatim definirajte njegova pravila:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
final class ContactRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:100'],
'email' => ['required', 'email:rfc', 'max:254'],
'message' => ['required', 'string', 'min:10', 'max:5000'],
];
}
}
Izradite standardni Laravel mailable pod nazivom ContactMessage čiji konstruktor izlaže $senderName, $senderEmail i $body, a čiji je prikaz sadržaja mail.contact. U tom Blade prikazu ispisujte vrijednosti s escapeanim Blade izrazima kao što je {{ $senderEmail }}; ne prikazujte poruku neescapeanom sintaksom.
Kontroler izvršava udaljenu provjeru prije slanja:
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ContactRequest;
use App\Mail\ContactMessage;
use App\Services\EmailValidator;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
final class ContactController extends Controller
{
public function store(
ContactRequest $request,
EmailValidator $validator
): RedirectResponse {
$data = $request->validated();
$assessment = $validator->check($data['email']);
if ($assessment->decision === 'deny') {
return back()->withInput()->withErrors([
'email' => 'Navedite drugu isporučivu adresu e-pošte.',
]);
}
if ($assessment->decision === 'unavailable') {
Log::notice('Contact accepted with validation fallback', [
'reason' => $assessment->failure,
]);
}
Mail::to(config('contact.to'))->send(new ContactMessage(
senderName: $data['name'],
senderEmail: $data['email'],
body: $data['message'],
));
return back()->with('status', 'Hvala. Vaša je poruka poslana.');
}
}
Izradite config/contact.php koji vraća ['to' => env('CONTACT_TO')]. Registrirajte rute sa zaštitom od zloupotrebe:
use App\Http\Controllers\ContactController;
use Illuminate\Support\Facades\Route;
Route::view('/contact', 'contact')->name('contact');
Route::post('/contact', [ContactController::class, 'store'])
->middleware('throttle:10,1')
->name('contact.store');
Obrazac resources/views/contact.blade.php trebao bi slati zahtjev na route('contact.store'), uključivati Laravelov @csrf, prikazivati pogreške provjere i koristiti vrijednosti old(). CSRF zaštita, ograničavanje zahtjeva, ograničenja duljine i escapeani izlaz pokrivaju osnovnu napadnu površinu obrasca za kontakt.
Testirajte odluke, predmemoriranje i povratni postupak
Laravelov HTTP fake održava testove determinističkima i osigurava da token usluge ne napusti testni proces.
<?php
namespace Tests\Feature;
use App\Mail\ContactMessage;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
final class ContactTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
Cache::flush();
config([
'services.email_validator.token' => 'test-token',
'services.email_validator.minimum_score' => 60,
'services.email_validator.deny_recommendations' => ['invalid'],
'contact.to' => '[email protected]',
]);
}
public function test_safe_email_is_checked_cached_and_sent(): void
{
Mail::fake();
Http::fake([
'https://ai.mihajlo.mk/api/email-validator/v1/check-email*' =>
Http::response([
'status' => 'success',
'score' => 92,
'recommendation' => 'safe',
'checks' => ['assessment_available' => true],
'quota' => ['remaining' => 20],
]),
]);
$payload = [
'name' => 'Ada',
'email' => '[email protected]',
'message' => 'Please send the project details.',
];
$this->post('/contact', $payload)->assertSessionHas('status');
$this->post('/contact', $payload)->assertSessionHas('status');
Http::assertSentCount(1);
Http::assertSent(fn ($request) =>
$request['email'] === '[email protected]'
&& $request['token'] === 'test-token'
);
Mail::assertSent(ContactMessage::class, 2);
}
public function test_low_score_is_rejected(): void
{
Mail::fake();
Http::fake([
'*' => Http::response([
'status' => 'success',
'score' => 25,
'recommendation' => 'invalid',
'checks' => ['assessment_available' => true],
'quota' => ['remaining' => 19],
]),
]);
$this->post('/contact', [
'name' => 'Ada',
'email' => '[email protected]',
'message' => 'This message is long enough.',
])->assertSessionHasErrors('email');
Mail::assertNothingSent();
}
public function test_service_failure_falls_back_to_accepting_message(): void
{
Mail::fake();
Http::fake(['*' => Http::response([], 503)]);
$this->post('/contact', [
'name' => 'Ada',
'email' => '[email protected]',
'message' => 'Please send the project details.',
])->assertSessionHas('status');
Mail::assertSent(ContactMessage::class);
}
}
Pokrenite paket testova naredbom php artisan test. Primjeri namjerno sadrže samo lažne vjerodajnice i sintetičke podatke odgovora.
Implementacija, nadzor i česti kvarovi
Upotrijebite zajedničku predmemoriju kao što je Redis kada se aplikacija izvodi na više instanci; u suprotnom će svaki čvor izvršavati vlastite pozive za provjeru. Nakon postavljanja vrijednosti produkcijskog okruženja pokrenite php artisan config:cache. Prije omogućavanja javne rute provjerite radi li konfigurirani prijenos pošte.
Pratite odluke provjere, broj povratnih postupaka, HTTP statusne kodove, latenciju na infrastrukturnom sloju i preostalu kvotu kada je ta vrijednost prisutna. Nikada ne zapisujte token, puni URL zahtjeva, sirovu e-poštu, tijelo poruke ni cjelokupan API odgovor. Budući da se autentikacija prenosi u nizu upita, pregledajte zapisivanje proxyja i HTTP klijenta kako biste osigurali da se parametri upita redigiraju.
Česti odgovori 401 ili 403 obično ukazuju na token koji nedostaje, opozvan je ili je neispravno implementiran. 429 bi trebao pokrenuti istragu kvote i prometa, a ne agresivne ponovne pokušaje. Ponavljani povratni postupci zbog neispravnog odgovora ukazuju na promjenu ugovora ili neočekivanu omotnicu pogreške. Ako svaki zahtjev promašuje predmemoriju, potvrdite produkcijski upravljački program predmemorije, dozvole, TTL i dijele li implementacije istu predmemoriju.
Završni popis za provjeru
- Paket usluge je aktivan, a trenutačni token ograničen na uslugu prisutan je samo u konfiguraciji okruženja.
- GET zahtjev šalje oba parametra upita
emailitokenna točnu krajnju točku. - Lokalna provjera izvršava se prije udaljenog zahtjeva.
- Status, rezultat, preporuka, provjere i kvota mapiraju se obrambeno.
- Samo dovršene procjene predmemoriraju se pod ključevima e-pošte raspršenog sadržaja.
- Autentikacija, provjera i neuspjesi kvote ne ponavljaju se naslijepo.
- Prekid rada validatora dopušta poruku za kontakt uz emitiranje strukturiranog zapisa povratnog postupka.
- CSRF zaštita, ograničavanje zahtjeva, sigurno escapeanje izlaza, konfigurirana dostava pošte i automatizirani testovi su postavljeni.
Robustan obrazac za kontakt nije onaj s najagresivnijim filtrom. To je onaj koji donosi pažljivu odluku kada su dokazi dostupni, čuva korisnikov put kada nisu te ostavlja dovoljno operativnih dokaza da se razlikuje jedno od drugoga.