Referência
Mailer, Options, Message, Layout, Transport, SMTP, Dir e Outbox — a API do pacote mail, com os padrões e o que cada campo muda.
import "github.com/emersonjoe/trilha/mail" — o punhado de mensagens que uma aplicação interna manda de verdade: o convite, o link, o aviso de que o fluxo terminou. O corpo é h.Node, a mesma linguagem das páginas, e toda mensagem sai multipart/alternative com o texto gerado do mesmo nó.
O remetente#
func New(o Options) *Mailer // não faz rede, não lê arquivo
func FromEnv() Options // a partir do TRILHA_MAIL_URL
func (m *Mailer) Send(ctx context.Context, msg Message) errorO New mora numa var de pacote, junto do resto do setup do app. Sem Tasks, o Send bloqueia até o servidor aceitar ou o prazo vencer, e pode ser chamado de várias goroutines.
Options | Padrão | O que faz |
|---|---|---|
From string | — | o remetente, "no-reply@org.br" ou "Acervo <no-reply@org.br>" |
Transport Transport | do ambiente | para onde vão as mensagens; nil em produção é ErrNotConfigured |
Timeout time.Duration | 10 s | limita uma tentativa de entrega, como prazo no context — servidor com antispam no envio passa disso; com Tasks ninguém espera, então suba à vontade |
Logger *slog.Logger | slog.Default() | uma linha por tentativa, com assunto, destinatários e quanto demorou |
Tasks *task.Tasks | — | deixa o Send assíncrono: monta, enfileira no runner como mail.TaskName ("mail.send") e volta |
Backoff []time.Duration | 5 s, 30 s, 2 min | a espera antes de cada nova tentativa quando há Tasks |
Envio assíncrono#
var Tarefas = task.New(task.Options{})
var Mail = mail.New(mail.Options{From: "no-reply@org.br", Transport: t, Tasks: Tarefas})Um formulário não deveria esperar 8 segundos por um SMTP com antispam no envio. Com Tasks, o Send continua devolvendo o que dá para saber sem o servidor — sem transporte, sem remetente, sem destinatário, cabeçalho que não se lê, fila cheia — e volta. O runner entrega: falha temporária (resposta 4xx, timeout, servidor que não respondeu) é tentada de novo depois de cada espera do Backoff; falha permanente (5xx) não, porque insistir é como um remetente vai parar numa lista de bloqueio. Um Transport seu marca os erros dele com um método Temporary() bool.
A mensagem espera na memória deste processo, porque tarefa não carrega payload: um reinício antes da entrega a perde, e o runner registra a tarefa como interrompida. É o formato do pacote task, dito em voz alta — caixa de saída durável é trabalho do provedor.
O ambiente#
TRILHA_MAIL_URL='smtp://usuario:senha@smtp.org.br:587?from=Acervo <no-reply@org.br>'
TRILHA_MAIL_URL='smtps://usuario:senha@smtp.org.br' # TLS implícito, porta 465
TRILHA_MAIL_FROM='Acervo <no-reply@org.br>' # ou o from= acima
TRILHA_MAIL_DIR=mail # só em dev, padrão ./mail
# a senha fora do ambiente, dos secrets do compose/Swarm:
TRILHA_MAIL_URL_FILE=/run/secrets/mail_url # a URL inteira num arquivo, ou
TRILHA_MAIL_URL='smtps://usuario@smtp.org.br' # a URL sem a senha e
TRILHA_MAIL_PASSWORD_FILE=/run/secrets/mail_password # a senha num arquivoVazio, o comportamento depende do TRILHA_ENV: em dev escreve .eml em ./mail e diz onde, na saída de erro; em qualquer outro lugar não há transporte e o Send devolve ErrNotConfigured. Um app em produção que arquiva convites numa pasta caladinho é um app cujos usuários nunca são convidados.
URL que ele não consegue ler é panic no boot, e não um remetente fazendo outra coisa. Para autenticar em canal aberto, ?insecure_auth=1 — escrito por extenso, porque uma flag que desliga uma checagem de cifra tem de ser legível no arquivo que a define. Os arquivos são lidos uma vez, sem espaços nas pontas; a mesma coisa definida duas vezes (a URL e o arquivo dela, senha na URL e num arquivo) também é panic.
Message#
type Message struct {
To, Cc, Bcc []string
From string // sobrepõe o Options.From
ReplyTo string
Subject string
Body h.Node // o HTML
Text string // gerado do Body quando vazio
Headers map[string]string // os que esta struct não tem campo para
}Todo endereço é analisado antes de qualquer envio, então um endereço errado falha aqui, com o endereço na mensagem, e não como um 501 três saltos adiante. O Bcc é destinatário do envelope e de cabeçalho nenhum. O Headers não troca os cabeçalhos que o pacote escreve — From, To, Cc, Bcc, Subject, Date, Message-ID, MIME-Version, Content-Type, Content-Transfer-Encoding —, porque mensagem com dois From é mensagem que um servidor recusa e outro entrega para a pessoa errada.
Assunto vai em Q-encoding e corpo em quoted-printable: uma linha de HTML gerado passa dos 998 octetos que o SMTP aceita, e servidor que quebra por você quebra dentro de uma URL.
O layout#
func Layout(marca string, corpo ...h.Node) h.Node
func Button(texto, url string) h.Node
func Muted(filhos ...h.Node) h.Node
func PlainText(n h.Node) stringO Layout é uma tabela centralizada, larguras em pixels, toda regra inline — o Outlook renderiza com o Word, que não conhece flexbox, grid nem float, e o Gmail arranca o <style> do cabeçalho. Fazer isso uma vez aqui é o que mantém isso fora da aplicação. O rodapé diz por que a mensagem chegou, que é a linha que impede uma mensagem legítima de ser denunciada como spam; o idioma dele vem do TRILHA_LANG (e depois LC_ALL, LC_MESSAGES, LANG), e o app que quiser as próprias palavras escreve o próprio layout — são vinte linhas.
O Button é um <a> com cara de botão: controle de formulário em e-mail não faz nada, e link degrada para link nos clientes que recusam o estilo.
O PlainText é o que preenche o Message.Text. Bloco vira quebra de linha, lista vira uma linha por item, <style> e <script> somem, e link vira texto <https://…> — URL que desaparece joga a mensagem inteira fora. As linhas não são quebradas, porque linha quebrada é URL quebrada; o quoted-printable já cuida do limite de tamanho.
Transportes#
type Transport interface {
Deliver(ctx context.Context, from string, to []string, raw []byte) error
}Um método, para que a aplicação que envia por um provedor HTTP escreva o dela em vez de o framework carregar um driver para cada um. O raw é a mensagem completa; from e to são o envelope, que nem sempre é o que os cabeçalhos dizem.
SMTP#
type SMTP struct {
Addr string // host:porta
User, Pass string // vazio é sem autenticação
ImplicitTLS bool // a porta 465 já implica
TLS *tls.Config // para CA privada — não é lugar de desligar verificação
AllowInsecureAuth bool
}A porta 465 (ou o ImplicitTLS) já nasce cifrada; o resto negocia STARTTLS quando o servidor oferece. A autenticação nunca acontece em canal aberto a menos que o AllowInsecureAuth esteja ligado: servidor que oferece PLAIN sem cifra está mal configurado, não é convite. O PLAIN é preferido, e o LOGIN entra quando é só o que o servidor anuncia — é o caso do Office 365 e de alguns appliances, e ele não está na biblioteca padrão.
O prazo vem do context e é posto na conexão, que é o que faz o manipulador que desistiu desligar. O smtp.SendMail não tem prazo nenhum, e esse é o defeito invisível que isto substitui: um servidor lento segura o manipulador até o TCP perceber, minutos depois.
Dir e Outbox#
type Dir string // escreve .eml; é o padrão em desenvolvimento
type Outbox struct{} // guarda em memória, para testes
func (o *Outbox) Messages() []Sent
func (o *Outbox) Last() Sent
func (o *Outbox) Reset()
type Sent struct {
From string
To []string
Subject string
Text string
HTML string
Raw []byte
At time.Time
}O Dir escreve .eml — o formato que todo cliente abre com dois cliques, então o que você confere é a mensagem de verdade, e não uma renderização feita para a conferência.
O Sent já vem desmontado, então o teste afirma sobre o link dentro do corpo e não sobre quoted-printable. O Last devolve um Sent zerado quando nada foi enviado, para o teste que esqueceu de checar a contagem falhar no assunto vazio em vez de estourar.
O Outbox mora aqui, num pacote que não é de teste, de propósito: ele é o que o mail.Sent(t) teria sido, e pacote que não é de teste não pode importar testing — quem importa registra as flags de teste em todo binário do projeto.
Erros#
O ErrNotConfigured é produção sem servidor. É erro, e não um arquivo escrito em algum lugar, porque aplicação que reporta sucesso para uma mensagem que nunca saiu é aplicação que ninguém depura até um cliente perguntar.
O trilha audit avisa quando o código manda e-mail e o TRILHA_MAIL_URL está vazio, e quando o TRILHA_MAIL_URL carrega senha fora de dev.
O que não está aqui#
Fila persistente, DKIM, anexo e IMAP. É trabalho do servidor de e-mail e do provedor, não de um módulo de setecentas linhas.