Pular para o conteúdo
Esta página ainda não foi traduzida. O conteúdo abaixo está em inglês. Ler em inglês

Digging deeper

Mail

Mailables rendered from views and sent through SMTP, Resend, Mailtrap, log or an in-memory transport for tests.

Última atualização:

app/mail/welcome.go
// vukem make:mail Welcome (also creates resources/views/mail/welcome.html)
type Welcome struct{ User models.User }

func (m Welcome) Envelope() mail.Envelope {
	return mail.Envelope{To: []string{m.User.Email}, Subject: "Welcome!", Tags: map[string]string{"kind": "welcome"}}
}

func (m Welcome) Content() mail.Content { return mail.View("mail/welcome", m) }
Go
mailer, err := app.Mailer()
err = mailer.Send(ctx, Welcome{User: u})

Content#

  • mail.View(name, data) renders an html/template view; the text part comes from <name>_text when it exists, otherwise from the HTML (links become text (url)). Mail views use {{/* layout: none */}}.
  • mail.HTML(html, text) and mail.Text(text) for literal bodies.
  • Attachments: implement Attachments() []mail.Attachment; inline parts set Inline and ContentID and are referenced as cid:....
  • Context: implement EnvelopeContext(ctx) mail.Envelope (mail.ContextEnveloper) to build the envelope from the send's context, e.g. a subject in the request locale with i18n.T(ctx, key). The Mailer calls it instead of Envelope, which stays required as the context-free fallback. SendLater renders before queueing, so it uses the context of the call.

Transports#

MAIL_MAILERTransportSettings
smtpnet/smtp with required STARTTLS (or implicit TLS on 465), AUTH PLAIN, multipart MIMESMTP_HOST, SMTP_PORT (587), SMTP_USERNAME, SMTP_PASSWORD, SMTP_INSECURE (local catchers only)
resendResend HTTP API: tags, reply-to, attachments, the message id as Idempotency-Key, retries on 429/5xx, typed *mail.ResendErrorRESEND_API_KEY
mailtrapMailtrap Email API: cc/bcc, one reply-to, attachments and inline images, the category tag as category and other tags as custom variables, retries on 429/5xx, typed *mail.MailtrapErrorMAILTRAP_API_TOKEN, MAILTRAP_INBOX_ID (sandbox)
loglogs recipients and subject (bodies at debug)default outside production
arrayin memorydefault in vukemtest

MAIL_FROM sets the default sender. Production must set MAIL_MAILER; a missing or unknown value is an error, never silent loss of mail. vukem.WithMailTransport(t) plugs in your own mail.Transport.

Mailtrap#

Some hosts block outbound SMTP (Railway on some plans, other PaaS): smtp then fails with a dial timeout. Mailtrap's HTTP API goes over HTTPS on port 443 instead.

.env
MAIL_MAILER=mailtrap
MAILTRAP_API_TOKEN=...      # Email API token, required: a missing token stops the app at boot
MAILTRAP_INBOX_ID=1234567   # optional: deliver to this Email Sandbox inbox instead of real recipients
MAIL_FROM="App <hello@your-verified-domain.com>"
  • Without MAILTRAP_INBOX_ID mail goes to https://send.api.mailtrap.io/api/send (transactional stream); the sender domain must be verified in Mailtrap.
  • With MAILTRAP_INBOX_ID it goes to https://sandbox.api.mailtrap.io/api/send/{inbox}, for staging. Use a token that can access that inbox. With APP_ENV=production the app logs a warning at boot, since recipients receive nothing.
  • A category tag becomes Mailtrap's category; other tags are sent as custom_variables (they show up in webhooks and logs).
  • Mailtrap accepts a single reply-to address; more than one is an error rather than a silently dropped address.
  • The API has no idempotency key, so a 5xx answered after delivery can send twice when retried.
  • Errors are *mail.MailtrapError with the HTTP status and Mailtrap's errors list (or its message when the shape differs), at most 5 one-line items of 200 characters. They never carry the message body or the token but may contain addresses quoted by the API. Retryable() is true for 429 and 5xx; a 2xx answering success: false is a non-retryable error.

Security#

  • Addresses go through net/mail; subjects, tags and content ids with line breaks are rejected (no header injection). Bcc is never written as a header.
  • The Resend key, Mailtrap token and SMTP password are config.Secret and never appear in logs or errors.

Previews and tests#

Go
if !app.Config.IsProduction() {
	r.Handle("/_mail/", http.StripPrefix("/_mail/", mail.Preview(mailer, map[string]mail.Mailable{"welcome": sample})))
}
Go
app := vukemtest.New(t, bootstrap.New)
// ...
if len(app.Mails().SentTo("ana@example.com")) != 1 {
	t.Fatal("welcome mail not sent")
}

Queued mail#

mailer.SendLater(ctx, mailable, opts...) (or app.MailLater) renders the message now and sends it from a retried queue job. The message ID stays the same across retries, so Resend's idempotency key prevents duplicates. The job payload is the rendered message encrypted with app.Crypt, so reset links and other tokens never sit in the jobs tables or queue:failed output in clear text. Queue options apply to the mail job: queue.OnQueue("mail"), queue.Retries(5). mailer.SendLaterTx(ctx, tx, mailable) (or app.MailLaterTx) queues the mail only if the transaction commits, so a rolled-back order or payment sends nothing: the database queue inserts the job in the transaction, other drivers queue it right after the commit. With QUEUE_CONNECTION=sync the mail is sent during the call (after the commit for SendLaterTx); use database so requests do not wait for the mail server (memory too, but a restart loses queued mail). serve runs the worker that sends it. Clear-text mail jobs queued before v0.24.0 fail unless QUEUE_ALLOW_LEGACY_MAIL=true (an upgrade window: drain the queue before upgrading, or set it until they are gone). Someone with write access to the jobs table can replay a sealed mail; that access already allows worse.