Correu electrònic

Com enviar correus electrònics a Laravel: Mailables, Markdown mail, adjunts, cues i configuració de drivers.

Configuració del correu#

Enviar correus electrònics és una de les funcionalitats més bàsiques i necessàries de qualsevol aplicació web: confirmacions de registre, restabliment de contrasenyes, notificacions de comandes, informes periòdics i molt més. Laravel proporciona un sistema de correu complet basat en Symfony Mailer que suporta múltiples proveïdors (SMTP, Mailgun, Postmark, Amazon SES, Resend) i permet canviar entre ells simplement modificant variables d'entorn.

Tota la configuració del correu es gestiona al fitxer config/mail.php, però els valors sensibles (credencials, host, port) es llegeixen del fitxer .env. Això significa que pots tenir configuracions diferents per a desenvolupament (on probablement vols capturar tots els correus amb Mailpit o Mailtrap) i producció (on envies correus reals a través d'un servei com Mailgun o SES), sense canviar ni una línia de codi.

Les variables d'entorn que necessites configurar són les següents:

MAIL_MAILER=smtp
MAIL_HOST=smtp.mailgun.org
MAIL_PORT=587
MAIL_USERNAME=postmaster@laravelandorra.com
MAIL_PASSWORD=secret
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=hola@laravelandorra.com
MAIL_FROM_NAME="Laravel Andorra"

La variable MAIL_MAILER determina quin driver s'utilitza per enviar els correus. Els drivers disponibles inclouen smtp, ses, mailgun, postmark, resend, sendmail, log i array. El driver log és especialment útil durant el desenvolupament perquè escriu el contingut dels correus al fitxer de log en lloc d'enviar-los, cosa que et permet verificar el contingut sense necessitat d'un servidor de correu real. El driver array emmagatzema els correus en memòria i és ideal per als tests automatitzats.

Per utilitzar proveïdors com Mailgun o Postmark, cal instal·lar el paquet corresponent i configurar les credencials addicionals:

# Per a Mailgun
composer require symfony/mailgun-mailer symfony/http-client
 
# Per a Postmark
composer require symfony/postmark-mailer symfony/http-client
 
# Per a Amazon SES
composer require aws/aws-sdk-php
# Mailgun
MAIL_MAILER=mailgun
MAILGUN_DOMAIN=mg.laravelandorra.com
MAILGUN_SECRET=key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
 
# Amazon SES
MAIL_MAILER=ses
AWS_ACCESS_KEY_ID=your-key
AWS_SECRET_ACCESS_KEY=your-secret
AWS_DEFAULT_REGION=eu-west-1

Crear un Mailable#

A Laravel, cada tipus de correu es representa amb una classe anomenada Mailable. Aquesta classe encapsula tot el que defineix el correu: el destinatari, l'assumpte, la plantilla, les dades que s'hi passen, els adjunts i les capçaleres. Aquesta aproximació orientada a objectes fa que els correus siguin fàcils de testejar, reutilitzar i mantenir, perquè tota la lògica d'un correu específic viu en un sol lloc.

Per crear un Mailable, utilitza la comanda Artisan:

php artisan make:mail WelcomeEmail

Això genera una classe a app/Mail/WelcomeEmail.php amb tres mètodes principals: envelope() per definir l'assumpte i les capçaleres, content() per especificar la vista i les dades, i attachments() per afegir fitxers adjunts. Aquesta estructura és molt més clara que passar arrays de configuració a una funció genèrica d'enviament:

namespace App\Mail;
 
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
 
class WelcomeEmail extends Mailable
{
    use Queueable, SerializesModels;
 
    public function __construct(
        public User $user
    ) {}
 
    public function envelope(): Envelope
    {
        return new Envelope(
            subject: 'Benvingut a Laravel Andorra!',
        );
    }
 
    public function content(): Content
    {
        return new Content(
            view: 'emails.welcome',
            with: [
                'userName' => $this->user->name,
                'dashboardUrl' => route('dashboard'),
            ],
        );
    }
 
    public function attachments(): array
    {
        return [];
    }
}

Fixa't que les propietats públiques del Mailable (en aquest cas $user) estan disponibles automàticament a la vista del correu. Això vol dir que a la plantilla Blade pots accedir a $user->name, $user->email i qualsevol altra propietat del model directament, sense necessitat de passar-les explícitament amb with. L'array with dins de content() serveix per passar dades addicionals que no són propietats del Mailable, com URLs generades o valors calculats.

L'Envelope#

L'objecte Envelope controla les metadades del correu: l'assumpte, el remitent, els destinataris CC i BCC, les capçaleres i les etiquetes. Pots configurar tot això de manera expressiva:

public function envelope(): Envelope
{
    return new Envelope(
        from: new Address('info@laravelandorra.com', 'Laravel Andorra'),
        replyTo: [
            new Address('suport@laravelandorra.com', 'Suport'),
        ],
        subject: 'Benvingut a Laravel Andorra!',
        tags: ['welcome', 'onboarding'],
        metadata: [
            'user_id' => $this->user->id,
        ],
    );
}

Les etiquetes (tags) i les metadades (metadata) són especialment útils si utilitzes proveïdors com Mailgun o Postmark, que permeten filtrar i buscar correus per aquestes etiquetes als seus dashboards. Això facilita el seguiment de campanyes de correu i la depuració de problemes d'entrega.

Enviar correus#

Per enviar un Mailable, utilitza la facade Mail. El mètode to() accepta una adreça de correu, un objecte amb propietat email, o una col·lecció d'ambdós. La flexibilitat de Laravel aquí és notable: pots passar un model d'usuari directament i Laravel extraurà automàticament l'adreça de correu de la propietat email del model.

use App\Mail\WelcomeEmail;
use Illuminate\Support\Facades\Mail;
 
// Enviar a un usuari (utilitza $user->email automàticament)
Mail::to($user)->send(new WelcomeEmail($user));
 
// Enviar a una adreça específica
Mail::to('joan@example.com')->send(new WelcomeEmail($user));
 
// Amb CC i BCC
Mail::to($user)
    ->cc('admin@laravelandorra.com')
    ->cc(['equip@laravelandorra.com', 'marketing@laravelandorra.com'])
    ->bcc('registre@laravelandorra.com')
    ->send(new WelcomeEmail($user));
 
// Enviar a múltiples destinataris
Mail::to($subscribers)->send(new NewsletterEmail($newsletter));

El mètode send() envia el correu immediatament, cosa que significa que l'execució del codi es bloqueja fins que el correu s'ha enviat (o ha fallat). En una petició HTTP, això pot afegir diversos segons de latència, cosa que l'usuari nota com una pàgina lenta. Per això, en la majoria de casos, és molt millor enviar els correus a través de la cua.

Enviar correus a la cua#

Les cues permeten diferir l'enviament del correu a un procés en segon pla. L'usuari rep la resposta immediatament i el correu s'envia uns segons després. Aquesta pràctica és tan important per al rendiment que hauria de ser el mètode d'enviament per defecte a qualsevol aplicació en producció:

// Enviar a la cua (recomanat per a producció)
Mail::to($user)->queue(new WelcomeEmail($user));
 
// Enviar a la cua amb un retard
Mail::to($user)->later(now()->addMinutes(10), new WelcomeEmail($user));
 
// Especificar la cua i la connexió
Mail::to($user)->queue(
    (new WelcomeEmail($user))
        ->onQueue('emails')
        ->onConnection('redis')
);

Per utilitzar queue(), el teu Mailable ha d'incloure el trait Queueable (que ja ve inclòs per defecte). Si vols que tots els correus d'un Mailable concret s'enviïn sempre per la cua sense haver de recordar cridar queue(), pots implementar la interfície ShouldQueue:

use Illuminate\Contracts\Queue\ShouldQueue;
 
class WelcomeEmail extends Mailable implements ShouldQueue
{
    use Queueable, SerializesModels;
 
    // Amb ShouldQueue, Mail::to($user)->send() envia automàticament a la cua
}

Amb ShouldQueue implementat, fins i tot quan crides send() el correu s'enviarà per la cua automàticament. Això és molt pràctic perquè no cal canviar el codi d'enviament a tot arreu de l'aplicació.

Markdown Mail#

Laravel ofereix plantilles de correu basades en Markdown que generen automàticament versions HTML boniques i versions de text pla. Aquestes plantilles utilitzen components Blade especials que produeixen un disseny responsive i professional sense necessitat d'escriure CSS inline (que és la norma als correus HTML, on el suport de CSS és molt limitat i inconsistent entre clients de correu).

Per crear un Mailable amb Markdown, pots especificar-ho a la comanda Artisan:

php artisan make:mail OrderConfirmation --markdown=emails.orders.confirmation

Això crea tant la classe Mailable com la plantilla Markdown. A la classe, el mètode content() referencia la vista Markdown en lloc d'una vista Blade normal:

public function content(): Content
{
    return new Content(
        markdown: 'emails.orders.confirmation',
        with: [
            'order' => $this->order,
            'url' => route('orders.show', $this->order),
        ],
    );
}

La plantilla Markdown utilitza components especials prefixats amb x-mail:::

{{-- resources/views/emails/orders/confirmation.blade.php --}}
<x-mail::message>
# Confirmació de comanda
 
Hola {{ $order->user->name }},
 
La teva comanda **#{{ $order->number }}** s'ha processat correctament. A continuació trobaràs els detalls:
 
<x-mail::table>
| Producte | Quantitat | Preu |
|:---------|:---------:|-----:|
@foreach ($order->items as $item)
| {{ $item->product->name }} | {{ $item->quantity }} | {{ number_format($item->total, 2) }} € |
@endforeach
| **Total** | | **{{ number_format($order->total, 2) }} €** |
</x-mail::table>
 
<x-mail::button :url="$url" color="primary">
Veure la comanda
</x-mail::button>
 
Si tens qualsevol pregunta, no dubtis a contactar-nos.
 
Gràcies,<br>
{{ config('app.name') }}
</x-mail::message>

Els components disponibles inclouen x-mail::message (el layout principal), x-mail::button (botons amb enllaç), x-mail::table (taules amb format Markdown) i x-mail::panel (panells destacats amb fons gris). El component x-mail::button accepta els colors primary, success i error per indicar visualment el tipus d'acció.

Personalitzar les plantilles Markdown#

Si vols modificar l'aspecte visual de les plantilles Markdown, pots publicar-les amb Artisan. Això copia les plantilles per defecte de Laravel al teu directori de vistes, on pots editar-les lliurement:

php artisan vendor:publish --tag=laravel-mail

Les plantilles es copiaran a resources/views/vendor/mail/. Dins trobaràs dos directoris: html/ amb les plantilles HTML i text/ amb les plantilles de text pla. Pots modificar els colors, la tipografia, el logo i qualsevol altre aspecte visual editant els fitxers themes/default.css i les plantilles Blade.

Adjunts#

Els correus sovint necessiten incloure fitxers adjunts: factures en PDF, imatges, documents o qualsevol altre fitxer. Laravel ofereix diverses maneres d'adjuntar fitxers a un Mailable, cadascuna adequada per a situacions diferents.

El mètode més directe és retornar els adjunts des del mètode attachments() del Mailable:

use Illuminate\Mail\Mailables\Attachment;
 
public function attachments(): array
{
    return [
        // Des d'un camí al disc
        Attachment::fromPath('/path/to/invoice.pdf')
            ->as('factura.pdf')
            ->withMime('application/pdf'),
 
        // Des del sistema de Storage
        Attachment::fromStorage('invoices/factura-123.pdf')
            ->as('factura.pdf'),
 
        // Des d'un disc específic de Storage
        Attachment::fromStorageDisk('s3', 'invoices/factura-123.pdf'),
 
        // Des de dades en memòria
        Attachment::fromData(
            fn () => $this->generatePdf(),
            'informe.pdf'
        )->withMime('application/pdf'),
    ];
}

El mètode fromStorage() és el més recomanat perquè treballa amb el sistema d'abstracció de fitxers de Laravel. Si els teus fitxers estan a S3, no cal baixar-los al servidor local primer: Laravel els llegeix directament del disc configurat. El mètode fromData() és ideal per a fitxers generats dinàmicament, com PDFs o CSVs que es creen al moment.

Adjunts inline#

Els adjunts inline s'utilitzen per incrustar imatges directament dins del cos HTML del correu. En lloc de mostrar-se com un fitxer adjunt que l'usuari ha de descarregar, la imatge apareix dins del text del correu. Això és útil per a logos, banners o qualsevol imatge que formi part del disseny del correu:

<x-mail::message>
# El teu resum mensual
 
Aquí tens el gràfic de rendiment d'aquest mes:
 
<img src="{{ $message->embed(storage_path('app/charts/monthly.png')) }}" alt="Gràfic mensual">
 
També pots incrustar dades directament:
 
<img src="{{ $message->embedData($chartData, 'chart.png') }}" alt="Gràfic">
 
</x-mail::message>

La variable $message està disponible automàticament a totes les vistes de correu i representa la instància del missatge Symfony que es construeix internament.

Previsualitzar correus al navegador#

Durant el desenvolupament, vols poder veure com queda un correu abans d'enviar-lo. Obrir la bústia de correu cada vegada que fas un canvi a la plantilla és lent i ineficient. Laravel et permet renderitzar un Mailable directament al navegador, com si fos una pàgina web normal. Això accelera enormement el procés de disseny de correus.

Simplement defineix una ruta que retorni una instància del Mailable:

// routes/web.php (NOMÉS en desenvolupament!)
Route::get('/mail-preview', function () {
    $user = App\Models\User::factory()->make();
    $order = App\Models\Order::factory()->make(['user_id' => $user->id]);
 
    return new App\Mail\OrderConfirmation($order);
});

Quan accedeixes a /mail-preview al navegador, Laravel renderitza el correu complet amb tot el HTML i els estils, exactament com ho veurien els destinataris. Pots utilitzar factory()->make() per crear models sense guardar-los a la base de dades, cosa que és perfecta per a previsualitzacions. Recorda eliminar o protegir aquesta ruta en producció.

Events de correu#

Laravel dispara events durant el procés d'enviament de correus, cosa que et permet executar accions addicionals com registrar enviaments, actualitzar comptadors o detectar errors. Aquests events són útils per monitoritzar el sistema de correu i mantenir un registre de tota la comunicació enviada.

Els dos events principals són MessageSending (abans d'enviar) i MessageSent (després d'enviar). Pots registrar listeners per a aquests events al teu EventServiceProvider o directament amb l'atribut de listener:

use Illuminate\Mail\Events\MessageSending;
use Illuminate\Mail\Events\MessageSent;
 
class LogSentEmail
{
    public function handle(MessageSent $event): void
    {
        $headers = $event->message->getHeaders();
        $to = $event->message->getTo();
 
        logger()->info('Correu enviat', [
            'to' => collect($to)->map(fn ($addr) => $addr->getAddress())->all(),
            'subject' => $event->message->getSubject(),
            'mailer' => $event->data['mailer'] ?? 'default',
        ]);
    }
}

L'event MessageSending és especialment útil perquè et permet cancel·lar l'enviament retornant false des del listener. Per exemple, podries impedir que s'enviïn correus a adreces de test en producció:

class PreventTestEmails
{
    public function handle(MessageSending $event): bool
    {
        $to = collect($event->message->getTo())
            ->map(fn ($addr) => $addr->getAddress());
 
        // Cancel·lar si és una adreça de test
        if ($to->contains(fn ($email) => str_ends_with($email, '@test.com'))) {
            return false;
        }
 
        return true;
    }
}

Múltiples mailers#

Algunes aplicacions necessiten enviar correus a través de diferents proveïdors segons el tipus de correu. Per exemple, podries voler enviar correus transaccionals (confirmacions, restabliment de contrasenyes) a través de Postmark per la seva alta velocitat d'entrega, i correus de màrqueting (newsletters, promocions) a través de Mailgun per el seu millor preu per volum. Laravel suporta múltiples mailers configurats simultàniament.

La configuració es fa al fitxer config/mail.php, on pots definir tants mailers com necessitis:

// config/mail.php
'mailers' => [
    'postmark' => [
        'transport' => 'postmark',
    ],
 
    'mailgun' => [
        'transport' => 'mailgun',
    ],
 
    'smtp' => [
        'transport' => 'smtp',
        'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
        'port' => env('MAIL_PORT', 587),
        'encryption' => env('MAIL_ENCRYPTION', 'tls'),
        'username' => env('MAIL_USERNAME'),
        'password' => env('MAIL_PASSWORD'),
    ],
],
 
'default' => env('MAIL_MAILER', 'smtp'),

Per enviar un correu a través d'un mailer específic, utilitza el mètode mailer():

// Utilitzar el mailer per defecte
Mail::to($user)->send(new WelcomeEmail($user));
 
// Utilitzar un mailer específic
Mail::mailer('postmark')
    ->to($user)
    ->send(new WelcomeEmail($user));
 
Mail::mailer('mailgun')
    ->to($subscribers)
    ->send(new NewsletterEmail($newsletter));

També pots especificar el mailer directament dins del Mailable, cosa que és útil quan un tipus de correu sempre s'ha d'enviar per un proveïdor concret:

class NewsletterEmail extends Mailable
{
    public $mailer = 'mailgun';
 
    // ...
}

Correus amb text pla i cadenes HTML#

No tots els correus necessiten plantilles Blade. Per a correus simples o generats programàticament, Laravel ofereix alternatives més directes. Pots enviar un correu amb HTML pur sense crear una vista:

public function content(): Content
{
    return new Content(
        htmlString: '<h1>Benvingut!</h1><p>Gràcies per registrar-te.</p>',
    );
}

O enviar un correu amb text pla sense HTML:

public function content(): Content
{
    return new Content(
        text: 'emails.welcome-text',
    );
}

Si vols proporcionar tant la versió HTML com la versió de text pla, pots combinar-les. Alguns clients de correu (especialment corporatius) prefereixen mostrar la versió de text pla, així que és bona pràctica oferir ambdues:

public function content(): Content
{
    return new Content(
        view: 'emails.welcome',
        text: 'emails.welcome-text',
    );
}

Eines de test per a correu#

Durant el desenvolupament, no vols enviar correus reals als teus usuaris de prova. Hi ha diverses eines que capturen els correus sortints i et permeten inspeccionar-los en una interfície web. Les dues opcions més populars són Mailpit (local) i Mailtrap (al núvol).

Mailpit és un servidor de correu local que captura tots els correus enviats per l'aplicació i els mostra en una interfície web. Ve inclòs amb Laravel Herd i Laravel Sail, cosa que fa que la configuració sigui immediata:

# Mailpit (inclòs a Laravel Sail i Herd)
MAIL_MAILER=smtp
MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null

Mailtrap és un servei al núvol que funciona de manera similar però no requereix cap instal·lació local. Simplement configura les credencials que et proporciona Mailtrap:

# Mailtrap
MAIL_MAILER=smtp
MAIL_HOST=sandbox.smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=your-mailtrap-username
MAIL_PASSWORD=your-mailtrap-password

Una tercera opció per al desenvolupament és el driver log, que escriu el contingut complet del correu (HTML, capçaleres, destinataris) al fitxer de log de Laravel. Això és útil quan no necessites veure el format visual del correu i només vols verificar que el contingut i els destinataris són correctes:

# Escriu els correus al log
MAIL_MAILER=log

Testing de correus#

A més de les eines de previsualització, Laravel ofereix eines potents per testejar l'enviament de correus als tests automatitzats. La facade Mail::fake() intercepta tots els correus i evita que s'enviïn realment, permetent-te fer assertions sobre quins correus s'haurien enviat:

use App\Mail\WelcomeEmail;
use Illuminate\Support\Facades\Mail;
 
test('envia correu de benvinguda al registrar-se', function () {
    Mail::fake();
 
    // Executar l'acció que hauria d'enviar el correu
    $user = User::factory()->create();
    $this->post('/register', [
        'name' => 'Joan',
        'email' => 'joan@example.com',
        'password' => 'password',
        'password_confirmation' => 'password',
    ]);
 
    // Verificar que s'ha enviat el correu correcte
    Mail::assertSent(WelcomeEmail::class, function ($mail) use ($user) {
        return $mail->hasTo($user->email)
            && $mail->user->id === $user->id;
    });
 
    // Verificar que no s'han enviat correus no esperats
    Mail::assertNotSent(NewsletterEmail::class);
 
    // Verificar el nombre total de correus
    Mail::assertSentCount(1);
});

Amb Mail::fake(), mai no s'envia cap correu real durant els tests. Totes les assertions treballen sobre els correus que s'haurien enviat, cosa que fa que els tests siguin ràpids, fiables i no depenguin d'un servidor de correu extern.

Renderitzar contingut d'un Mailable#

De vegades necessites obtenir el contingut HTML d'un correu sense enviar-lo. Això és útil per mostrar previsualitzacions als administradors, guardar el contingut a la base de dades o enviar-lo a través d'un altre canal:

$mailable = new WelcomeEmail($user);
 
// Obtenir l'HTML renderitzat
$html = $mailable->render();
 
// Guardar-lo o processar-lo
EmailLog::create([
    'user_id' => $user->id,
    'subject' => 'Benvingut a Laravel Andorra!',
    'html_body' => $html,
    'sent_at' => now(),
]);

El mètode render() retorna el contingut HTML complet del correu, incloent els estils inline i el layout. Això és exactament el que rebria el destinatari al seu client de correu.