CeníkAPI & SMTPŘešeníSrovnáníNástrojeBlogFAQKontakt
PřihlásitRegistrovat
Tmavý režim
← Zpět na blog
27. října 2026·4 min čtení·Tým TopEmail

Laravel 11: Mail přes SMTP relay TopEmail, Mailable ve frontě a HTTP klient pro značky a webhooky

Nastavení .env pro MAIL_MAILER=smtp na smtp.topemail.cz, Mailable s Reply-To a značkami v hlavičkách, odesílání přes frontu, varianta přes REST API s Laravel HTTP klientem a Idempotency-Key, a route pro webhook email.status s ověřením podpisu a výjimkou z CSRF.

Laravel používá pod kapotou Symfony Mailer, takže přepnutí na SMTP relay je změna pěti řádků v .env. Článek ale jde dál: ukazuje Mailable, který posílá značky pro párování, odesílání přes frontu, aby HTTP požadavek zákazníka nečekal na SMTP, variantu přes REST API s Laravel HTTP klientem, a route pro webhook se stavem doručení. Všechno pro Laravel 11 a PHP 8.3.

Krok 1: .env

MAIL_MAILER=smtp
MAIL_HOST=smtp.topemail.cz
MAIL_PORT=587
MAIL_USERNAME=CLIENT_ID
MAIL_PASSWORD=SECRET
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="objednavky@muj-eshop.cz"
MAIL_FROM_NAME="${APP_NAME}"

Novější skeleton Laravelu 11 má v config/mail.php místo encryption klíč scheme; pak použijte MAIL_SCHEME=smtp (pro port 465 MAIL_SCHEME=smtps). Na portu 587 Symfony Mailer vyjedná STARTTLS automaticky, takže proměnná pro šifrování není pro funkčnost rozhodující. Alternativně celé připojení jedním DSN:

MAIL_URL=smtp://CLIENT_ID:SECRET@smtp.topemail.cz:587

Pokud SECRET obsahuje @, : nebo /, v DSN ho URL-zakódujte. MAIL_FROM_ADDRESS musí být na doméně ověřené v TopEmail (TXT topemail._domainkey, SPF include:spf.topemail.cz, _dmarc), jinak relay odpoví 550 5.7.1 a Laravel vyhodí TransportException. Po změně .env spusťte php artisan config:clear.

Rychlý test v Tinkeru:

php artisan tinker
>>> Mail::raw('Test z Laravelu', fn ($m) => $m->to('vas@gmail.com')->subject('Test SMTP'));

Pak v Gmailu „Zobrazit originál" a zkontrolovat SPF, DKIM a DMARC PASS.

Krok 2: Mailable se značkami

php artisan make:mail OrderConfirmed --markdown=emails.orders.confirmed
<?php
// app/Mail/OrderConfirmed.php
namespace App\Mail;

use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Mail\Mailables\Headers;
use Illuminate\Queue\SerializesModels;

class OrderConfirmed extends Mailable implements ShouldQueue
{
    use Queueable, SerializesModels;

    public function __construct(public Order $order) {}

    public function envelope(): Envelope
    {
        return new Envelope(
            subject: "Potvrzení objednávky {$this->order->code}",
            replyTo: [new Address('podpora@muj-eshop.cz', 'Podpora')],
        );
    }

    public function content(): Content
    {
        return new Content(markdown: 'emails.orders.confirmed');
    }

    public function headers(): Headers
    {
        return new Headers(text: [
            'X-TopEmail-Tag-order' => $this->order->code,
            'X-TopEmail-Tag-type'  => 'confirm',
        ]);
    }
}

Hlavičky X-TopEmail-Tag-<klíč> relay převede na značky, které se vrátí ve webhooku a v historii; metoda headers() je od Laravelu 10 součást Mailable. Markdown šablona vygeneruje HTML i textovou verzi, což je pro doručitelnost správně.

Odeslání:

Mail::to($order->email)->send(new OrderConfirmed($order));

Protože Mailable implementuje ShouldQueue, send() ho ve skutečnosti zařadí do fronty. Spusťte worker (php artisan queue:work) a v .env nastavte QUEUE_CONNECTION na database nebo redis; s výchozím sync by se e-mail posílal v požadavku a zákazník by čekal na SMTP.

Idempotence u SMTP: pokud worker spadne po odeslání a před označením úlohy, Laravel ji zopakuje a e-mail odejde dvakrát. Relay dedupuje podle Message-ID, takže nastavte stabilní:

public function headers(): Headers
{
    return new Headers(
        messageId: "order-{$this->order->code}-confirm@muj-eshop.cz",
        text: ['X-TopEmail-Tag-order' => $this->order->code],
    );
}

Opakovaný pokus se stejným Message-ID server nepošle znovu.

Krok 3: varianta přes REST API

Když chcete ID e-mailu hned v odpovědi a chyby jako JSON, použijte Laravel HTTP klient. Hodí se pro ověřovací kódy a vše, kde potřebujete vědět, že odešlo:

use Illuminate\Support\Facades\Http;

$response = Http::withToken(config('services.topemail.key'))   // "CLIENT_ID:SECRET"
    ->withHeaders(['Idempotency-Key' => "order-{$order->code}-confirm"])
    ->timeout(15)
    ->post('https://topemail.cz/api/email/send', [
        'from'    => 'Můj e-shop <objednavky@muj-eshop.cz>',
        'to'      => [$order->email],
        'subject' => "Potvrzení objednávky {$order->code}",
        'html'    => view('emails.orders.confirmed-html', compact('order'))->render(),
        'text'    => view('emails.orders.confirmed-text', compact('order'))->render(),
        'replyTo' => 'podpora@muj-eshop.cz',
        'tags'    => ['order' => $order->code, 'type' => 'confirm'],
    ]);

if ($response->failed()) {
    $code = $response->json('code');            // invalid_recipient, domain_not_verified, rate_limited…
    if ($code === 'invalid_recipient') {
        $order->update(['email_invalid' => true]);
    } elseif ($response->status() === 429 || $response->serverError()) {
        throw new \RuntimeException('retry');  // nechat frontu opakovat se stejným klíčem
    } else {
        report(new \RuntimeException("TopEmail {$code}: {$response->json('error')}"));
    }
} else {
    $order->update(['confirm_email_id' => $response->json('emails.0.id')]);
}

Klíč dejte do config/services.php jako 'topemail' => ['key' => env('TOPEMAIL_KEY')], ne přímo env() v kódu, aby fungovalo config:cache. Idempotency-Key je odvozený z objednávky; náhodný Str::uuid() by idempotenci zrušil. Přílohy posíláte jako attachments => [['filename' => …, 'content' => base64_encode($pdf), 'contentType' => 'application/pdf']], do 10 MB.

Stav e-mailu: Http::withToken(...)->get("https://topemail.cz/api/email/{$id}")->json('status').

Krok 4: webhook email.status

Při změně stavu pošleme POST s JSON (event, id, to, status, detail, tags, occurredAt) a hlavičkou X-TopEmail-Signature: sha256=<HMAC-SHA256 těla s webhook secretem>.

// routes/web.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Jobs\ProcessEmailStatus;

Route::post('/webhooks/topemail', function (Request $request) {
    $expected = 'sha256=' . hash_hmac('sha256', $request->getContent(), config('services.topemail.webhook_secret'));

    abort_unless(hash_equals($expected, $request->header('X-TopEmail-Signature', '')), 401);

    ProcessEmailStatus::dispatch($request->json()->all());
    return response()->noContent(200);
});

Laravel 11 vyjímá trasu z CSRF ochrany v bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
    $middleware->validateCsrfTokens(except: ['webhooks/topemail']);
})

Bez toho dostane webhook 419 a my ho budeme opakovat. $request->getContent() je syrové tělo, ze kterého se podpis počítá; hash_equals() porovnává v konstantním čase. Job ProcessEmailStatus musí být idempotentní (událost může přijít dvakrát): ukládejte události s unikátním klíčem a duplicitu ignorujte, jak popisuje návod k webhookům. V jobu pak reagujte na bounced (neplatná adresa) a complained.

Lokální vývoj

V .env.local nechte MAIL_MAILER=log nebo Mailpit, aby testovací objednávky nechodily reálným lidem; na stagingu použijte oddělený API klíč. Webhook lokálně otestujete přes tunel (ngrok, Cloudflare Tunnel) nebo tak, že si v testu podpis vyrobíte sami hash_hmac('sha256', $body, 'test-secret').

Shrnutí

Pět řádků v .env, Mailable s headers() pro značky a stabilním Message-ID, fronta místo sync. Pro kritické e-maily HTTP klient s Idempotency-Key a chybami podle code. Webhook jako route s hash_equals, vyjmutá z CSRF. Oba kanály běží na jednom účtu za 0,06 Kč za e-mail, prvních 1 000 měsíčně zdarma; úplný přehled polí a stavů je v dokumentaci API.

LaravelPHPSMTPapiintegracenávod
Transakční e-maily z vaší domény

Transakční e-maily z vaší domény přes API nebo SMTP, od 0,025 do 0,06 Kč za e-mail podle objemu, prvních 1 000 e-mailů měsíčně zdarma. Registrace zdarma, bez platební karty.

Registrovat zdarma →