- Docs
- Background work
- Mailer
Mailer
Send transactional email through a log-by-default mailer, and switch to SMTP for production.
Use mailer.Mailer for transactional outbound mail: receipts, cancellations, password resets. It defaults to log, so validated messages go to the log instead of the wire and real delivery is an explicit adapter change. It handles validated delivery only (no template rendering, no lists), so build the finished Mail with NewMail, or with a Sender that binds the default From.
Stay on log for development and CI, and move to smtp for production; there is no in-between adapter. Enqueue sends as jobs so an SMTP blip retries at the job layer instead of failing the booking write.
Quick start
Section titled “Quick start”MAILER_ADAPTER=logMAILER_HOST=localhostMAILER_PORT=25host and port are required by Validate even for the log adapter, which never dials; the bookings default uses the dummy localhost pair for that reason.
package main
import ( "context" "fmt"
maillog "github.com/zenta-dev/zever/adapters/mailer/log" "github.com/zenta-dev/zever/core/mailer")
func main() { maillog.Register() m, _ := mailer.Open(mailer.Log, mailer.Options{Host: "localhost", Port: 25}) defer m.Close()
msg := mailer.NewMail( mailer.Address{Address: "bookings@example.com"}, []mailer.Address{{Address: "guest@example.com"}}, "Booking confirmed", "Space Loft, Fri to Sun. Receipt inside.", ) _ = m.Send(context.Background(), &msg) fmt.Println("logged, not delivered")}{"from":{"address":"bookings@example.com"},"to":[{"address":"guest@example.com"}],"cc":[],"bcc":[],"subject":"Booking confirmed","body":"Space Loft, Fri to Sun. Receipt inside.","html":"","attachments":[]}logged, not deliveredSend validates, writes one JSON line to stdout (attachments log only name and size, never bytes), and returns nil without opening a socket. In tests, use maillog.NewWithWriter(opts, w).
Send through SMTP
Section titled “Send through SMTP”MAILER_ADAPTER=smtpMAILER_HOST=mail.example.comMAILER_PORT=587MAILER_USERNAME=bookingsMAILER_PASSWORD=prod-secret-hereMAILER_ENCRYPTION=starttlsMAILER_TIMEOUT=30sMAILER_MAX_MESSAGE_SIZE=10485760MAILER_ADAPTER=smtpMAILER_HOST=mail.example.comMAILER_PORT=587MAILER_USERNAME=bookingsMAILER_PASSWORD=prod-secret-hereMAILER_ENCRYPTION=starttlsMAILER_TIMEOUT=30sencryption:starttls(default when empty),implicit, ornone.timeout: zero means the 30s default. File durations are integer nanoseconds, env accepts"30s".max_message_size: zero means the 10 MiB default; larger messages fail withErrMessageTooLarge.
File decoding is strict snake_case. All keys: mailer config reference.
| Adapter | Package | Notes |
|---|---|---|
log |
adapters/mailer/log |
Default, safe sink, writes each message as a JSON line |
smtp |
adapters/mailer/smtp |
Production delivery, honors encryption and timeout |
Both serve mailer.Mailer with Send plus Close; delivery errors stay errors.Is-compatible for job-layer retry branching.
Resolve the mailer from the container
Section titled “Resolve the mailer from the container”m, err := c.Mailer()Mailer() resolves lazily and caches; on error the cache clears. Address parsing helpers live in core/mailer/address.go and the attachment shape in core/mailer/mail.go. Close() error is instant, and the container probes it as a snapshot service with no leaf ordering. See the container.
Production checklist
Section titled “Production checklist”EncryptionNonemust not carryusernameorpassword; validation rejects that pair so credentials never cross plaintext. Useimplicitonly when the provider documents it.- Set
usernameandpasswordtogether; a lone value fails validation instead of half-authenticating at send time. hostmust not contain a scheme (mail.example.com, notsmtp://mail.example.com). Port must be 1 to 65535.- Address validation rejects header injection and control characters; parse user input through
core/mailer/address.gobefore using it asFrom. - Keep attachments small, or link to
storagepresigned URLs instead of inlining. - SMTP passwords never appear in logs. Supply them by file or env, display config only through
config.RedactedServices, and keep error text to names and reasons. - Retry transient failures at the job layer; bookings
SendConfirmationusesmax_attempts(3)with exponential base 30s.
Troubleshooting
Section titled “Troubleshooting”host must be non-empty on boot with the log adapter
host and port were left zero; validation requires both even though log never dials. Set MAILER_HOST=localhost and MAILER_PORT=25 in your .env.
username and password must be set together
Only one of the pair was supplied, often a password without a username. Set both from the same secret source, or clear both for an anonymous relay with a non-none encryption mode.
encryption none must not carry credentials
encryption: none combined with SMTP auth values. Switch to starttls or implicit when auth is needed, or drop the credentials when the relay needs none.
Sends pass locally but time out in production
Wrong port or encryption for the provider, or blocked egress. Confirm the provider tuple (commonly 587 with starttls, 465 with implicit), raise timeout from the 30s default, and move the send behind job.Dispatcher so timeouts retry with backoff.