Infraestrutura & Ferramentas

Mail

Envio de emails transacionais.

E-mail — MailService

Envio de e-mails transacionais (cadastro, redefinição de senha, 2FA, convites) sobre o illuminate/mail nativo do Laravel. A camada do MAD é fina: App\Service\Email\MailService sanitiza/valida anexos e enfileira a entrega — a request do usuário nunca espera a latência do SMTP.

Como funciona

MailService::send($to, $subject, $html, $cc, $attachments)
    └── sanitiza anexos (tamanho somado + MIME real via finfo + path legível)
    └── Mail::to($to)->queue(new RawHtmlMail($subject, $html, $cc, $attachments))
            └── grava 1 registro na tabela jobs (conexão `iam`)

php artisan queue:work (worker em background)
    └── lê o job, monta o e-mail e entrega via MAIL_MAILER
        ├── sucesso → remove da tabela jobs
        └── falha   → retry com backoff (10s, 30s, 90s) até 3 tentativas
                       esgotou → RawHtmlMail::failed() loga + vai pra failed_jobs

Sem worker rodando, os e-mails ficam parados na tabela jobs e nunca saem — ver Filas para subir o worker.


Configuração

O transporte é 100% nativo do Laravel — config/mail.php + variáveis de ambiente:

Variável Exemplo Descrição
MAIL_MAILER log (dev) / smtp (prod) Driver de transporte
MAIL_HOST smtp.gmail.com Host SMTP
MAIL_PORT 587 Porta SMTP
MAIL_SCHEME tls / ssl / null Encriptação
MAIL_USERNAME / MAIL_PASSWORD — Credenciais SMTP
MAIL_FROM_ADDRESS / MAIL_FROM_NAME contato@empresa.com.br Remetente padrão
QUEUE_CONNECTION database (dev/prod) / sync (testes) Backend da fila que entrega o e-mail

Em desenvolvimento, MAIL_MAILER=log escreve o e-mail renderizado em storage/logs/laravel.log em vez de enviar de verdade — útil para conferir o HTML sem depender de SMTP.

Anexos — validação antes de enfileirar

config('mad.mail.*') controla os limites aplicados pelo MailService antes de colocar o job na fila — um anexo inválido nunca incha o payload nem trava o worker:

Config Default Descrição
mad.mail.attachment_max_bytes 10 * 1024 * 1024 (10MB) Teto somado de todos os anexos
mad.mail.attachment_allowed_mime PDF, imagens, texto/CSV, ZIP, Office Allowlist de MIME (detectado via finfo, não confia na extensão)

Anexo que falha (vazio, MIME fora da lista, acima do teto, path ilegível) é descartado com Log::warning, e o e-mail segue sem ele — nunca derruba o envio inteiro por causa de 1 anexo ruim.


Enviando um e-mail

MailService::send() é o ponto de entrada — nunca lança exceção (fail-safe: se nem enfileirar der certo, loga e retorna false, sem quebrar o fluxo do usuário):

use App\Service\Email\MailService;

MailService::send(
    to: 'cliente@email.com',
    subject: 'Bem-vindo!',
    html: '<p>Sua conta foi criada com sucesso.</p>',
);

Assinatura

MailService::send(
    string|array $to,
    string $subject,
    string $html,
    array $cc = [],
    array $attachments = [],
): bool
Parâmetro Descrição
$to Destinatário (string) ou array de destinatários
$subject Assunto
$html Corpo HTML já renderizado
$cc Cópia(s) — e-mails ou ['address' => ..., 'name' => ...]
$attachments Cada item: string (caminho) | ['path' => ..., 'as' => ..., 'mime' => ...] | ['data' => ..., 'name' => ..., 'mime' => ...]

Múltiplos destinatários e cópia

MailService::send(
    to: ['joao@email.com', 'maria@email.com'],
    subject: 'Aviso importante',
    html: '<p>Conteúdo do aviso.</p>',
    cc: ['supervisor@empresa.com'],
);

Com anexos

MailService::send(
    to: 'cliente@email.com',
    subject: 'Seu pedido em PDF',
    html: '<p>Segue o pedido em anexo.</p>',
    attachments: [
        '/caminho/absoluto/pedido-123.pdf',                 // por caminho
        ['path' => $caminho, 'as' => 'nota-fiscal.pdf'],     // com nome customizado
        ['data' => $pdfBytes, 'name' => 'relatorio.pdf'],    // gerado em memória
    ],
);

Template HTML — EmailTemplateService

Para não escrever <html>/<body> em todo lugar, envolva o conteúdo num shell padrão (header com nome da app, rodapé, etc.) com EmailTemplateService::wrap(). Se a preference email_template_shell não estiver configurada, wrap() retorna o HTML original sem alterar.

use App\Service\Email\EmailTemplateService;
use App\Service\Email\MailService;

$content = EmailTemplateService::interpolate(
    '<h2>Olá, {$name}!</h2><p>Seu pedido #{$pedido} foi aprovado.</p>',
    ['{$name}' => $cliente->name, '{$pedido}' => $pedido->id],
);

MailService::send(
    to: $cliente->email,
    subject: 'Pedido aprovado',
    html: EmailTemplateService::wrap($content, subject: 'Pedido aprovado'),
);

interpolate() faz htmlspecialchars em cada valor antes de injetar no template — protege contra XSS quando o conteúdo vem de dado controlado pelo usuário (nome, login, etc.). Use só no corpo HTML; o assunto é texto puro e não deve passar por aqui.

Placeholders disponíveis no shell: {$content} {$subject} {$app_name} {$app_url} {$current_year} {$logo_url}.


Exemplo em um MadComponent

Envio de e-mail de boas-vindas ao salvar um cadastro, dentro da action de um MadComponent (não bloqueia a resposta — MailService::send() apenas enfileira):

use App\Models\Iam\User;
use App\Service\Email\EmailTemplateService;
use App\Service\Email\MailService;
use Mad\Component\MadComponent;
use Mad\Http\MadResponse;
use Mad\Ui\MadToast;

class ClienteForm extends MadComponent
{
    public MadForm $form;

    public function onSave(): MadResponse
    {
        $data = $this->form->getData();
        $this->form->validate(User::rules());

        $user = User::create($data);

        $content = EmailTemplateService::interpolate(
            EmailTemplateService::getDefaultInvitationContent(),
            ['{$name}' => $user->name, '{$login}' => $user->email, '{$link}' => url('/login')],
        );

        MailService::send(
            to: $user->email,
            subject: 'Bem-vindo!',
            html: EmailTemplateService::wrap($content, subject: 'Bem-vindo!'),
        );

        return (new MadResponse())
            ->toast(MadToast::success('Cliente cadastrado! E-mail de boas-vindas enviado.'))
            ->closeDrawer();
    }
}

Mailables customizados

MailService cobre o caso comum (HTML pronto + anexos). Para um e-mail mais elaborado, crie um Mailable padrão do Laravel (php artisan make:mail) e despache do jeito nativo — o MailService não é obrigatório:

use Illuminate\Mail\Mailable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Bus\Queueable;
use Illuminate\Support\Facades\Mail;

class PedidoAprovadoMail extends Mailable implements ShouldQueue
{
    use Queueable;

    public function __construct(public Pedido $pedido) {}

    public function build(): self
    {
        return $this->subject("Pedido #{$this->pedido->id} aprovado")
            ->view('emails.pedido-aprovado', ['pedido' => $this->pedido]);
    }
}

Mail::to($cliente->email)->queue(new PedidoAprovadoMail($pedido));

Testando o envio

Em desenvolvimento, deixe MAIL_MAILER=log no .env — nenhum e-mail real sai, e o HTML renderizado fica em storage/logs/laravel.log. Para testar entrega de verdade, troque para smtp (ou um serviço como Mailtrap) e suba o worker:

php artisan queue:work --once   # processa o próximo job enfileirado e sai

Em produção — exige worker

Com QUEUE_CONNECTION=database (padrão), o e-mail só sai quando um worker estiver rodando. Suba php artisan queue:work via systemd ou Supervisor — units prontas em deploy/:

sudo cp deploy/mad-queue-worker.service /etc/systemd/system/
sudo systemctl enable --now mad-queue-worker

Reinicie o worker a cada deploy para carregar o código novo (php artisan queue:restart). Ver Filas para detalhes de retry, jobs falhados e as duas opções de supervisor de processo (systemd/Supervisor).