· Admin

Laravel'de Transactional Email: Notification, Queue ve Servis Seçimi

Kullanıcı kayıt oldu — hoşgeldin maili lazım. Şifresini sıfırladı — link lazım. Sipariş verdi — onay maili lazım. Ödeme başarısız oldu — uyarı lazım. E-posta, uygulamanızın sessiz ama en kritik parçası.

Laravel'in Mail ve Notification altyapısı bu işi çok kolaylaştırıyor. Ama "kolaylaştırıyor" demek "her şeyi doğru yapıyorsunuz" demek değil. Bu yazıda transactional email'i nasıl kurduğumu, hangi servislerle çalıştığımı ve production'da öğrendiğim dersleri paylaşacağım.

Transactional vs Marketing Email

Önce ayrımı netleştirelim çünkü ikisi farklı dünyalar:

Transactional email: Kullanıcının bir aksiyonuna veya sistem olayına bağlı otomatik gönderilen e-posta. Şifre sıfırlama, sipariş onayı, hesap doğrulama gibi.

Marketing email: Toplu gönderim. Newsletter, kampanya duyurusu, promosyon gibi.

Neden önemli bu ayrım? Çünkü e-posta servisleri bu ikisini farklı altyapılardan gönderiyor. Marketing maili aynı IP'den gönderirseniz, bir kullanıcı spam olarak işaretlediğinde o IP'nin itibarı düşer — transactional mailleriniz de spam'e düşmeye başlar. Bu yüzden ikisini ayrı tutun.

Mailable vs Notification: Hangisi Ne Zaman?

Laravel'de e-posta göndermenin iki yolu var:

Mailable — Sadece e-posta göndermek istiyorsanız:

Mail::to($user)->send(new WelcomeMail($user));

Notification — E-posta + SMS + Slack + database gibi birden fazla kanaldan bildirim göndermek istiyorsanız:

$user->notify(new OrderConfirmation($order));

Benim tercihim: transactional email için her zaman Notification kullanıyorum. Bugün sadece mail atıyorsunuz, yarın "bunu Slack'e de at" diyorsunuz — Notification'da via() metoduna bir kanal eklemek yeterli. Mailable ile bunu yapamazsınız.

Temel Bir Notification

php artisan make:notification OrderShippedNotification
namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class OrderShippedNotification extends Notification implements ShouldQueue
{
    use Queueable;

    public function __construct(
        private Order $order
    ) {}

    public function via($notifiable): array
    {
        return ['mail'];
    }

    public function toMail($notifiable): MailMessage
    {
        return (new MailMessage)
            ->subject("Siparişiniz kargoya verildi — #{$this->order->number}")
            ->greeting("Merhaba {$notifiable->name},")
            ->line("#{$this->order->number} numaralı siparişiniz kargoya verildi.")
            ->line("Kargo takip numarası: {$this->order->tracking_number}")
            ->action('Siparişi Takip Et', url("/orders/{$this->order->id}"))
            ->line('İyi alışverişler!');
    }
}

Birkaç dikkat noktası:

  • implements ShouldQueue — Bunu kesinlikle ekleyin. Birazdan neden olduğunu açıklayacağım.
  • Subject'te sipariş numarası var — Kullanıcı inbox'ında aynı "Siparişiniz kargoya verildi" başlığından 10 tane görmek istemez. Benzersiz bilgi ekleyin.
  • Constructor'da private promotion — PHP 8.1+, gereksiz property tanımı yok.

Queue Kullanın, Senkron Göndermeyin

Bu en önemli kural. E-postayı senkron göndermek şu demek: kullanıcı "Sipariş Ver" butonuna bastı, sunucu SMTP sunucusuna bağlanıyor, mail gönderiyor, response bekliyor — bu 2-5 saniye. Kullanıcı o süre boyunca beyaz ekrana bakıyor.

Queue ile:

class OrderShippedNotification extends Notification implements ShouldQueue
{
    use Queueable;

    // Bu kadar. implements ShouldQueue + use Queueable yeterli.
}

.env'de queue driver'ı ayarlayın:

QUEUE_CONNECTION=redis

Worker'ı başlatın:

php artisan queue:work redis --queue=default --tries=3

--tries=3 önemli — SMTP sunucusu geçici olarak erişilemezse 3 kez dener. Senkron gönderimde bu şansınız yok, 500 hatası alırsınız.

Production'da Supervisor ile worker'ı ayakta tutun:

[program:queue-worker]
command=php /var/www/html/artisan queue:work redis --queue=default --tries=3 --sleep=3
numprocs=2
autostart=true
autorestart=true

Özel Blade Şablonları

Laravel'in default mail template'i idare eder ama profesyonel görünmez. Kendi tasarımınızı yapın:

public function toMail($notifiable): MailMessage
{
    return (new MailMessage)
        ->view('emails.order-shipped', [
            'order' => $this->order,
            'user' => $notifiable,
        ]);
}

resources/views/emails/order-shipped.blade.php:

<!DOCTYPE html>
<html>
<body style="font-family: -apple-system, sans-serif; max-width: 600px; margin: 0 auto;">
    <div style="background: #f8f9fa; padding: 20px; text-align: center;">
        <h1 style="color: #333;">{{ config('app.name') }}</h1>
    </div>

    <div style="padding: 30px;">
        <p>Merhaba {{ $user->name }},</p>

        <p><strong>#{{ $order->number }}</strong> numaralı siparişiniz kargoya verildi.</p>

        <table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
            @foreach($order->items as $item)
            <tr style="border-bottom: 1px solid #eee;">
                <td style="padding: 8px;">{{ $item->product->name }}</td>
                <td style="padding: 8px; text-align: right;">{{ $item->quantity }} adet</td>
            </tr>
            @endforeach
        </table>

        <a href="{{ url("/orders/{$order->id}") }}"
           style="display: inline-block; background: #4f46e5; color: #fff; padding: 12px 24px; border-radius: 6px; text-decoration: none;">
            Siparişi Takip Et
        </a>
    </div>
</body>
</html>

E-posta HTML'i web HTML'i gibi değil — CSS framework'leri çalışmıyor, inline style zorunlu, <table> layout hâlâ en güvenilir yol. Gmail, Outlook ve Apple Mail hepsi farklı render ediyor. Test edin.

Event + Listener ile Tetikleme

E-postayı controller'dan doğrudan tetiklemek yerine Event/Listener kullanmak daha temiz:

// Controller
public function ship(Order $order)
{
    $order->update(['status' => 'shipped']);

    event(new OrderShipped($order));

    return back()->with('success', 'Kargoya verildi.');
}
// Listener
class SendOrderShippedEmail
{
    public function handle(OrderShipped $event): void
    {
        $event->order->user->notify(
            new OrderShippedNotification($event->order)
        );
    }
}

Neden bu kadar uğraşıyoruz? Çünkü yarın aynı event'e başka listener'lar ekleyeceksiniz: stok güncelleme, kargo API'sine bildirim, admin'e Slack mesajı. Controller'da bunları üst üste yazmak yerine her biri ayrı listener'da yaşar.

Servis Seçimi

Postmark — Transactional'a Odaklı

Postmark sadece transactional email gönderiyor, marketing email kabul etmiyor. Bu sayede IP itibarı çok yüksek, deliverability mükemmel.

MAIL_MAILER=postmark
POSTMARK_TOKEN=your-server-token

Laravel Postmark'ı native destekliyor, ek paket gerekmiyor. Ücretsiz planı ayda 100 mail — development için yeterli. Production'da aylık $15'tan başlıyor.

Mailgun — Hacim Önemliyse

Günde onbinlerce mail gönderiyorsanız Mailgun daha uygun:

MAIL_MAILER=mailgun
MAILGUN_DOMAIN=mg.siteniz.com
MAILGUN_SECRET=key-xxx

Hem transactional hem bulk destekliyor ama bu aynı zamanda dezavantajı — IP itibarını kendiniz yönetmeniz gerekiyor.

Amazon SES — En Ucuz Seçenek

10.000 mail = $1. Ciddi hacimli projeler için en maliyet-etkin çözüm. Ama kurulumu ve bounce/complaint yönetimi diğerlerine göre daha zahmetli.

Benim Tercihim

Küçük-orta proje: Postmark. Deliverability'si rakipsiz, kurulumu 5 dakika. Büyük hacim: SES + kendi bounce yönetimi. Ucuz ama emek istiyor.

DNS Ayarları: SPF, DKIM, DMARC

Bu üçlüyü ayarlamadan production'a çıkmayın. Aksi halde mailleriniz spam klasörüne düşer.

SPF — Hangi sunucuların sizin adınıza mail gönderebileceğini belirler:

v=spf1 include:spf.postmarkapp.com ~all

DKIM — Mail içeriğinin değiştirilmediğini kriptografik olarak doğrular. Servis sağlayıcınız size DKIM kaydını verecek.

DMARC — SPF ve DKIM kontrolünden geçemeyen maillere ne yapılacağını belirler:

v=DMARC1; p=quarantine; rua=mailto:[email protected]

Bu üç kaydı DNS'e ekledikten sonra mail-tester.com ile puanınızı kontrol edin. 10/10 almanız lazım.

Davranışa Dayalı Otomatik Mailler

Scheduler + Notification kombinasyonuyla kullanıcı davranışına göre otomatik mail gönderebilirsiniz:

// app/Console/Kernel.php veya routes/console.php
Schedule::call(function () {
    // 7 gündür giriş yapmayan aktif kullanıcılar
    User::where('last_login_at', '<', now()->subDays(7))
        ->where('is_active', true)
        ->each(fn ($user) => $user->notify(new InactiveUserReminder()));
})->daily();

Dikkat: Bu tarz mailleri çok sık göndermeyin. Haftada birden fazla "seni özledik" maili spam algısı yaratır.

Test Etmek

Development'ta gerçek mail göndermek istemezsiniz. Seçenekler:

Mailpit (eski MailHog) — Lokal SMTP sunucusu, gelen mailleri web arayüzünde gösterir:

MAIL_MAILER=smtp
MAIL_HOST=127.0.0.1
MAIL_PORT=1025

log driver — Mailleri storage/logs/laravel.log'a yazar:

MAIL_MAILER=log

Test assertion — Feature test'lerde mail gönderildi mi kontrolü:

public function test_order_shipped_sends_email(): void
{
    Notification::fake();

    $order = Order::factory()->create();

    event(new OrderShipped($order));

    Notification::assertSentTo(
        $order->user,
        OrderShippedNotification::class
    );
}

Notification::fake() gerçek mail göndermiyor, sadece gönderilip gönderilmediğini kontrol ediyor. Test'leriniz hızlı kalıyor ve SMTP'ye bağımlılık olmuyor.

Pratikte Kullandığım Yapı

Her projemde şu yapıyı kuruyorum:

  1. Tüm transactional mailler Notification — Mailable kullanmıyorum
  2. Hepsi ShouldQueue — İstisnasız
  3. Event/Listener pattern — Controller'da $user->notify() yazmıyorum
  4. Özel Blade template — Default Laravel template yerine marka uyumlu tasarım
  5. Postmark — Küçük projeler için. SES — büyük hacim için
  6. SPF + DKIM + DMARC — Production'da olmazsa olmaz
  7. Mailpit — Development'ta

E-posta basit görünüyor ama production'da sürprizler çıkıyor: bounce handling, rate limiting, template rendering farkları, spam filtreleri. Ne kadar erken doğru altyapıyı kurarsanız o kadar az sorunla karşılaşırsınız.