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).