# Recipe: E-mail > A Next.js-style web framework for Go: a folder under app/ is a route, HTML is written in Go, and nothing outside the standard library is imported. > /trilha/llms/recipes/email.txt - [E-mail](/trilha/cookbook/email): trilha/mail — the body is an h.Node, the plain text writes itself, dev writes .eml files, and a test asserts on what was sent without a server. · 2147 tokens est. 1 page(s) · 2147 tokens est. in all --- Sending mail is three problems wearing one coat: talking to a server, assembling a message that is valid, and not sending anything from a test. `trilha/mail` answers all three, and the part worth reading is which decisions it takes away from you. ## Or: `trilha add mail` ```bash trilha add mail --dry-run ``` ```text + internal/correio/correio.go + internal/correio/correio_test.go --dry-run: nothing was written ``` Two files and no screen — `mail` has no listing and no route, so it does not touch `app/setup.go`. What lands is `internal/correio/correio.go`: the `Mailer` variable below, already built from `mail.FromEnv()`, and a test that proves it against `mail.Outbox` with no network. What this page adds is everything the recipe cannot decide for you: the message itself, `mail.Layout` and `mail.Button`, the invitation flow, and how to swap in another provider through `Transport`. Run the recipe for the file that owns the `Mailer`; write the messages here, because a recipe cannot guess what your app has to say. ## The mailer ```go // Mailer is the one an app holds: built once, at startup, from the // environment. New opens no connection and reads no file, so it belongs in a // var and not behind a sync.Once. var Mailer = mail.New(mail.FromEnv()) ``` `FromEnv` reads one variable: ```bash TRILHA_MAIL_URL='smtp://user:senha@smtp.org.br:587?from=Acervo ' ``` Port 587 is STARTTLS, 465 (or `smtps://`) is implicit TLS. With the variable unset the behaviour depends on where the app is running, and the difference is the point: in dev it writes `.eml` files into `./mail` and says where; anywhere else `Send` answers `mail.ErrNotConfigured`. That asymmetry is deliberate. A production app that quietly files invitations into a directory is an app whose users are never invited, and nobody finds out for a week. ## Sending ```go // SendWelcome is what a handler calls. The message is an h.Node — the same // nodes the pages are written with — and mail.Layout is what keeps the tables // and the inline CSS out of here. func SendWelcome(c *trilha.Ctx, nome, email, link string) error { return Mailer.Send(c.Context(), mail.Message{ To: []string{email}, Subject: "Sua conta está pronta", Body: mail.Layout("Acervo", h.P(h.Textf("Olá, %s.", nome)), mail.Button("Definir minha senha", link), mail.Muted(h.Text("O link vale por uma hora.")), ), }) } ``` `mail.Layout` is a centred table with widths in pixels and every rule inline, because Outlook renders with Word — no flexbox, no grid, no float — and Gmail strips `