> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Registro y pruebas de transportes de correo en paquetes

> Analiza el MailManager de Laravel 13 y explica el registro de transportes desde un paquete, el paso de la configuración, la retención y resolución de los mailers, y los flujos de envío que fake no permite comprobar.

Para ofrecer un método de envío de correo propio desde un paquete, registra en Laravel **el proceso que genera el Transport de Symfony**. No necesitas reescribir los Mailables ni el Mailer de Laravel.

Esta página se basa en la revisión de la implementación de Laravel Framework `v13.39.0`. Para el envío básico, consulta [Envío de correo](https://kawax.biz/es/mail.md), y para preparar el provider, [Desarrollo de paquetes](https://kawax.biz/es/advanced/package-development.md).

## Separa el nombre del mailer del nombre del transport

| Elemento | Ejemplo en esta página | Función |
| - | - | - |
| Nombre del transport | `acme-memory-mail` | El método de creación que se registra con `extend()` |
| Nombre del mailer | `receipts` / `alerts` | El nombre de la configuración en `mail.mailers` y el identificador del Mailer ya generado |
| Symfony Transport | `MemoryTransport` | El proceso de entrega del correo. En este ejemplo solo lo registra en memoria |
| Laravel Mailer | `Illuminate\Mail\Mailer` | Genera el cuerpo, dispara los eventos de envío y entrega el mensaje al Transport, entre otras cosas |

`Mail::mailer('receipts')` lee `mail.mailers.receipts` y llama al proceso de creación correspondiente a `transport`. `MailManager::build()` pasa ese Transport al Mailer de Laravel y, en la resolución normal por nombre, también configura el remitente global y otros valores.

```mermaid theme={null}
flowchart TD
    A["Mail::mailer('receipts')"] --> B{"¿receipts ya está resuelto?"}
    B -->|Sí| C["Devuelve el Mailer retenido"]
    B -->|No| D["Lee mail.mailers.receipts"]
    D --> E["Creador de acme-memory-mail<br>su único argumento es el array de configuración"]
    E --> F["Devuelve un Symfony Transport"]
    F --> G["Genera el Mailer de Laravel y<br>lo retiene con el nombre receipts"]
    G --> H["Al enviar llama a Transport::send()"]
    C --> H
```

Los creadores personalizados se eligen antes que los Transports integrados. No sobrescribas `smtp` ni `ses`; usa un nombre de transport propio del paquete.

## Construye el flujo de registro sin envíos externos

Primero comprueba la configuración y el flujo de resolución, no la API de destino. En `src/Transport/MemoryTransport.php` del paquete `Acme\Postbox`, crea una clase que herede del `ArrayTransport` estándar.

```php theme={null}
<?php

namespace Acme\Postbox\Transport;

use Illuminate\Mail\Transport\ArrayTransport;

class MemoryTransport extends ArrayTransport
{
    public function __construct(
        private readonly string $label,
    ) {
        parent::__construct();
    }

    public function __toString(): string
    {
        return 'acme-memory-mail:'.$this->label;
    }
}
```

`ArrayTransport::send()` guarda el `SentMessage` de Symfony en una Collection interna. Puedes consultarlo con `messages()` y borrar los registros de esa instancia con `flush()`. La etiqueta sirve para comprobar que la configuración llegó al proceso de creación.

> [!WARNING]
>
> Este ejemplo no entrega el correo al exterior. Está pensado para aprender el registro y las pruebas. Los registros en memoria no se comparten entre procesos ni se persisten, y tampoco confirman que el correo haya llegado al destinatario. Si solo quieres registrar en memoria, basta con el Transport integrado `array`.

### Regístralo en el Manager de carga diferida

La documentación oficial llama a `Mail::extend()` en `boot()`. En un paquete puedes combinarlo con un [hook posterior a la resolución del servicio](https://kawax.biz/es/advanced/package-service-resolution.md) para no generar el Manager innecesariamente en aplicaciones que no usan correo.

Crea `src/PostboxServiceProvider.php`.

```php theme={null}
<?php

namespace Acme\Postbox;

use Acme\Postbox\Transport\MemoryTransport;
use Illuminate\Mail\MailManager;
use Illuminate\Support\ServiceProvider;
use Symfony\Component\Mailer\Transport\TransportInterface;

class PostboxServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->callAfterResolving('mail.manager', static function (
            MailManager $manager,
        ): void {
            $manager->extend('acme-memory-mail', static function (
                array $config,
            ): TransportInterface {
                return new MemoryTransport($config['label'] ?? 'mail');
            });
        });
    }
}
```

El provider se carga, por ejemplo, mediante el [descubrimiento automático de paquetes](https://kawax.biz/es/advanced/package-discovery.md).

* El `MailServiceProvider` de Laravel es un provider diferido y registra `mail.manager` como singleton. La facade `Mail` también resuelve esta clave.
* `callAfterResolving()` espera a la resolución posterior si aún no se ha resuelto, y aplica la extensión de inmediato a un Manager ya resuelto. **No reemplaza los Mailers que ya se hayan generado.**
* Evita convertir sin más en `DeferrableProvider` tu propio provider que registra la extensión. Así te aseguras de que no se solicite el Transport propio antes de registrar el hook.
* Este ejemplo registra el hook en su propio `boot()`. No garantiza un orden de arranque en el que otro provider use el mailer propio antes. Diseña el orden de registro y no envíes correo dentro de `register()`.

### No confundas los argumentos de extend con los de otros Managers

`MailManager::createSymfonyTransport()` llama al creador personalizado con **un único argumento: el array de configuración**. No son los dos argumentos, aplicación y configuración, de `Cache::extend()`. El valor devuelto es un Transport que implementa `TransportInterface`, no un Mailer de Laravel ni el resultado de un envío.

Además, `MailManager::extend()` solo guarda el closure y no lo vuelve a vincular al Manager. El ejemplo usa closures static y argumentos explícitos para no depender de cómo se interprete `$this`.

Si necesitas un cliente para una API externa, aplica la configuración en este proceso de creación y pásalo al Transport. No envíes nada en el momento del registro, y diseña la solución para que, cuando haya varios mailers con el mismo nombre de transport, no se mezclen sus configuraciones ni sus destinos.

### Añade los mailers a la aplicación que usa el paquete

Añade estos dos elementos al array `mailers` existente en `config/mail.php`. El paquete no debe sobrescribir el valor predeterminado del usuario ni el array `mailers` completo.

```php theme={null}
'receipts' => [
    'transport' => 'acme-memory-mail',
    'label' => 'receipts-v1',
],

'alerts' => [
    'transport' => 'acme-memory-mail',
    'label' => 'alerts',
],
```

Si quieres usarlo como predeterminado, lo que se indica en `MAIL_MAILER` es **el nombre del mailer, `receipts`**, no el nombre del transport. Si lo especificas de forma individual, puedes usarlo sin cambiar el valor predeterminado existente.

```php theme={null}
use Illuminate\Mail\Message;
use Illuminate\Support\Facades\Mail;

Mail::mailer('receipts')->raw('Receipt body', static function (Message $message): void {
    $message->from('noreply@example.com')
        ->to('reader@example.com')
        ->subject('Receipt');
});

$transport = Mail::mailer('receipts')->getSymfonyTransport();

$recorded = $transport->messages()->sole();
```

Este `raw()` es un ejemplo para comprobar el flujo de envío síncrono sin preparar una vista. En el envío normal con Mailables, el Mailer también genera el cuerpo y demás contenido y luego lo entrega al mismo Transport.

## Límites al crear un Transport de entrega real

Para una implementación que entregue a un servicio externo, puedes seguir la documentación oficial: heredar de `Symfony\Component\Mailer\Transport\AbstractTransport` e implementar `doSend(SentMessage $message): void` y `__toString(): string`. El método de registro anterior se puede usar tal cual.

| Qué comprobar | Responsabilidad del paquete |
| - | - |
| Mensaje y destinatarios | Pasar correctamente a la API de destino no solo el cuerpo, sino también el HTML, los adjuntos, CC y BCC, y los destinatarios del Envelope |
| Errores | No ocultar los fallos de la API como si fueran éxitos; propagarlos al código que llama |
| Separación de la configuración | Aplicar al Transport el destino de conexión y las credenciales de cada mailer |
| Representación en cadena | No incluir credenciales ni el cuerpo del correo en `__toString()` |
| Pruebas | Además de probar el registro, la configuración y la nueva resolución, verificar la conversión a la API y el manejo de errores |

`Mailer::sendSymfonyMessage()` de Laravel llama al `send()` del Transport. Si un listener de `MessageSending` cancela el envío o el Transport devuelve `null`, el `send()` de Laravel tampoco devuelve un resultado de envío. **No consideres que la entrega fue correcta solo porque no hubo excepciones.** Aunque se devuelva un `SentMessage`, eso no garantiza que haya llegado al buzón del destinatario.

## Distingue el cambio de configuración de la nueva resolución del mailer

`MailManager` retiene cada Mailer y su Transport por nombre de mailer. Cambiar la configuración o volver a registrar `extend()` no actualiza los objetos ya generados.

```php theme={null}
use Illuminate\Support\Facades\Mail;

$before = Mail::mailer('receipts');

config(['mail.mailers.receipts.label' => 'receipts-v2']);

$same = Mail::mailer('receipts');

Mail::purge('receipts');

$after = Mail::mailer('receipts');
```

`$same` es el mismo Mailer que `$before`. En `$after`, obtenido después de `purge('receipts')`, el Transport se genera a partir de la nueva configuración.

| Operación | Alcance |
| - | - |
| `Mail::purge('receipts')` | Elimina la referencia al mailer indicado que retiene el Manager |
| `Mail::forgetMailers()` | Elimina las referencias a todos los mailers que retiene el Manager. Las extensiones registradas se mantienen |
| `$transport->flush()` | Borra los registros de mensajes que retiene el MemoryTransport de este ejemplo |

La implementación revisada de `purge()` solo elimina la referencia del array interno del Manager. No reemplaza el antiguo `$before` ni los servicios a los que ya se haya inyectado ese Mailer. Tampoco cierra explícitamente las conexiones externas.

### Los mailers padre, como failover, también se retienen por separado

`failover` y `roundrobin` leen la configuración de los nombres de mailer hijos, **generan los Transports directamente** y los incorporan al Transport padre. No comparten la instancia `Mail::mailer()` de los hijos.

Por eso, aunque cambies la configuración de un hijo y llames a `purge('receipts')`, el Transport interno del mailer padre ya resuelto no se actualiza. También hay que volver a resolver el mailer padre afectado. Ni siquiera `forgetMailers()` puede reemplazar las referencias al mailer padre que ya conserva el código que lo usa.

En producción, incluye la actualización de la caché de configuración y el reinicio de los workers de larga duración en el procedimiento de despliegue habitual. No pienses en la nueva resolución como una actualización simultánea de todos los procesos, y consulta también [El ciclo de vida de los servicios](https://kawax.biz/es/advanced/package-service-lifetimes.md).

## Prueba el flujo de creación real, no solo Mail::fake

`Mail::fake()` sirve para registrar el envío y la puesta en cola de Mailables. `MailFake::mailer()` no resuelve el Mailer real: registra el nombre del mailer y devuelve el propio fake. `raw()` tampoco realiza ningún envío. Las aserciones de envío de Mailables pueden pasar aunque el Transport propio no esté registrado.

En el [entorno de pruebas del paquete](https://kawax.biz/es/advanced/package-testing.md), haz pasar el flujo por el Manager y el Transport reales **sin usar fake**, como en el siguiente ejemplo. Como el Transport de este ejemplo no envía nada al exterior, puedes comprobar el flujo de registro sin riesgo de entregas reales. Se carga primero un provider auxiliar para verificar también el caso en que el Manager ya está resuelto.

```php theme={null}
<?php

namespace Acme\Postbox\Tests;

use Acme\Postbox\PostboxServiceProvider;
use Acme\Postbox\Transport\MemoryTransport;
use Illuminate\Mail\Message;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\ServiceProvider;
use Orchestra\Testbench\TestCase;
use Symfony\Component\Mime\Email;

class MailTransportTest extends TestCase
{
    protected function getPackageProviders($app): array
    {
        return [EarlyMailManagerProvider::class, PostboxServiceProvider::class];
    }

    protected function defineEnvironment($app): void
    {
        $app['config']->set('mail.default', 'receipts');
        $app['config']->set('mail.from', [
            'address' => 'noreply@example.com',
            'name' => 'Postbox',
        ]);
        $app['config']->set('mail.mailers.receipts', [
            'transport' => 'acme-memory-mail',
            'label' => 'receipts-v1',
        ]);
        $app['config']->set('mail.mailers.alerts', [
            'transport' => 'acme-memory-mail',
            'label' => 'alerts',
        ]);
    }

    public function test_registers_on_an_already_resolved_manager(): void
    {
        $this->assertSame(
            $this->app->make('postbox.test.early-manager'),
            $this->app->make('mail.manager'),
        );

        $transport = Mail::mailer('receipts')->getSymfonyTransport();

        $this->assertInstanceOf(MemoryTransport::class, $transport);
        $this->assertSame('acme-memory-mail:receipts-v1', (string) $transport);
    }

    public function test_message_reaches_the_transport(): void
    {
        $mailer = Mail::mailer('receipts');

        $sent = $mailer->raw('Receipt body', static function (Message $message): void {
            $message->to('reader@example.com')->subject('Receipt');
        });

        $transport = $mailer->getSymfonyTransport();

        $this->assertNotNull($sent);
        $this->assertCount(1, $transport->messages());

        $recorded = $transport->messages()->sole();
        $email = $recorded->getOriginalMessage();

        $this->assertInstanceOf(Email::class, $email);
        $this->assertSame('Receipt', $email->getSubject());
        $this->assertSame('Receipt body', $email->getTextBody());
        $this->assertSame('noreply@example.com', $email->getFrom()[0]->getAddress());
        $this->assertSame(
            'reader@example.com',
            $recorded->getEnvelope()->getRecipients()[0]->getAddress(),
        );
    }

    public function test_mailer_names_have_separate_transports(): void
    {
        $receipts = Mail::mailer('receipts');
        $alerts = Mail::mailer('alerts');

        $this->assertNotSame($receipts->getSymfonyTransport(), $alerts->getSymfonyTransport());
        $this->assertSame('acme-memory-mail:alerts', (string) $alerts->getSymfonyTransport());

        $receipts->raw('Receipt body', static function (Message $message): void {
            $message->to('reader@example.com')->subject('Receipt');
        });

        $this->assertCount(1, $receipts->getSymfonyTransport()->messages());
        $this->assertCount(0, $alerts->getSymfonyTransport()->messages());
    }

    public function test_config_change_requires_purging_the_mailer(): void
    {
        $before = Mail::mailer('receipts');

        config(['mail.mailers.receipts.label' => 'receipts-v2']);

        $this->assertSame($before, Mail::mailer('receipts'));
        $this->assertSame('acme-memory-mail:receipts-v1', (string) $before->getSymfonyTransport());

        Mail::purge('receipts');

        $after = Mail::mailer('receipts');

        $this->assertNotSame($before, $after);
        $this->assertSame('acme-memory-mail:receipts-v2', (string) $after->getSymfonyTransport());
        $this->assertSame('acme-memory-mail:receipts-v1', (string) $before->getSymfonyTransport());
    }
}

class EarlyMailManagerProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->app->instance('postbox.test.early-manager', $this->app->make('mail.manager'));
    }
}
```

Estas pruebas comprueban los límites del registro, la configuración, el envío síncrono y los objetos retenidos. Para un Transport de entrega real, verifica por separado el payload y las respuestas de error con un mock o un fake del cliente de la API. Si ofreces envío en cola, separa también las pruebas de [Colas](https://kawax.biz/es/queues.md) y comprueba que el provider se carga también en los workers.

## Fuentes primarias consultadas

* [Documentación oficial de Laravel: Transports personalizados de correo y pruebas (en el momento de la revisión)](https://github.com/laravel/docs/blob/960eedf0911155ad16326ade49c0ea5d18eda0b8/mail.md#custom-transports)
* [Documentación oficial de Laravel: desarrollo de paquetes](https://github.com/laravel/docs/blob/13.x/packages.md)
* [MailManager: creación, retención de mailers, purge y Transports compuestos](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Mail/MailManager.php)
* [MailServiceProvider: binding del MailManager](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Mail/MailServiceProvider.php)
* [ServiceProvider: callAfterResolving](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Support/ServiceProvider.php)
* [Mailer: envío al Transport y eventos](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Mail/Mailer.php)
* [ArrayTransport: registro de mensajes](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Mail/Transport/ArrayTransport.php)
* [Facade Mail y MailFake](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Support/Facades/Mail.php)
* [MailFake: selección del mailer y registro de Mailables](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Support/Testing/Fakes/MailFake.php)
* [Pruebas oficiales de Laravel: verificación del envío con Mailer y ArrayTransport](https://github.com/laravel/framework/blob/v13.39.0/tests/Mail/MailMailerTest.php)


## Related topics

- [Envío de correo](https://kawax.biz/es/mail.md)
- [Temas avanzados](https://kawax.biz/es/advanced/index.md)
- [Registro y nueva resolución de drivers de Storage en paquetes](https://kawax.biz/es/advanced/package-filesystem-drivers.md)
- [Registro y caché de listeners de eventos en paquetes](https://kawax.biz/es/advanced/package-events.md)
- [Registro de rutas y caché en paquetes](https://kawax.biz/es/advanced/package-routes.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.