Skip to content
You are reading the v0.20 documentation. Go to the latest version (v1.0)

Digging deeper

Mail

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

Last updated:

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

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

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 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#

app.MailLater(ctx, mailable) 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.