// 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) }mailer, err := app.Mailer()
err = mailer.Send(ctx, Welcome{User: u})Content#
mail.View(name, data)renders anhtml/templateview; the text part comes from<name>_textwhen it exists, otherwise from the HTML (links becometext (url)). Mail views use{{/* layout: none */}}.mail.HTML(html, text)andmail.Text(text)for literal bodies.- Attachments: implement
Attachments() []mail.Attachment; inline parts setInlineandContentIDand are referenced ascid:....
Transports#
MAIL_MAILER | Transport | Settings |
|---|---|---|
smtp | net/smtp with required STARTTLS (or implicit TLS on 465), AUTH PLAIN, multipart MIME | SMTP_HOST, SMTP_PORT (587), SMTP_USERNAME, SMTP_PASSWORD, SMTP_INSECURE (local catchers only) |
resend | Resend HTTP API: tags, reply-to, attachments, the message id as Idempotency-Key, retries on 429/5xx, typed *mail.ResendError | RESEND_API_KEY |
log | logs recipients and subject (bodies at debug) | default outside production |
array | in memory | default 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).Bccis never written as a header. - The Resend key and SMTP password are
config.Secretand never appear in logs or errors.
Previews and tests#
if !app.Config.IsProduction() {
r.Handle("/_mail/", http.StripPrefix("/_mail/", mail.Preview(mailer, map[string]mail.Mailable{"welcome": sample})))
}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.