SMTP
This is the subpackage a project imports once it actually wants to send mail, rather than just working against the Mailer interface mail defines.
import "github.com/gp-system/mail/smtp"
mail/smtp is the concrete driver that actually leaves the process: it sends over SMTP via wneessen/go-mail. It is the only mailer in the module that opens a network connection; see Dev mailers on the overview page for the no-op alternatives used in tests and in mail-less environments.
Constructors
mailer, err := smtp.New(ctx, cfg.Mail) // errors on empty Host or FromAddress
mailer, err := smtp.NewIfConfigured(ctx, cfg.Mail) // empty Host -> mail.NewDiscard(), no error
mailer := smtp.MustNewIfConfigured(ctx, cfg.Mail) // panics on error; the usual main() constructor
Configuration
Compose smtp.Config into your project config under a MAIL_ prefix:
type Config struct {
// ...
Mail smtp.Config `envPrefix:"MAIL_"`
}
| Variable | Default | Meaning |
|---|---|---|
MAIL_HOST | (empty = no transport) | SMTP server address |
MAIL_PORT | 587 | SMTP port |
MAIL_USERNAME / MAIL_PASSWORD | empty | empty: no auth (Mailpit, internal relays) |
MAIL_AUTH | auto | auto | plain | login | cram-md5 | none |
MAIL_TLS | starttls | starttls | starttls-opportunistic (dev, TLS-less server) | tls (implicit TLS, 465) | none |
MAIL_FROM_ADDRESS | required once MAIL_HOST is set | default sender address |
MAIL_FROM_NAME | empty | default sender display name |
MAIL_TIMEOUT | 15s | dial/send timeout |
The full list lives in the configuration reference.
TLS and auth modes
MAIL_TLS picks the connection's encryption from four modes: starttls upgrades a plaintext connection after the initial handshake (the common port-587 setup, and the default here); starttls-opportunistic attempts the same upgrade but tolerates a server that doesn't offer it, useful against a dev relay (Mailpit) that speaks plain SMTP; tls dials with implicit TLS from the first byte (port 465); none sends in the clear, for a trusted internal network only.
MAIL_AUTH picks the SASL mechanism: auto negotiates the strongest mechanism the server advertises, and is the right default for anything real. plain and login are explicit fallbacks for servers with a narrower advertisement; cram-md5 is a legacy challenge-response mechanism kept for older relays. none skips authentication entirely, the same "no credentials" posture MAIL_USERNAME/MAIL_PASSWORD being empty already implies, useful when the relay itself gates access by network location rather than by credentials.
Timeouts and the startup connection check
New verifies the connection with a dial+close at construction, the same convention as pg.NewPool/queue.NewClient's ping: a broken config fails at startup, not at the first email. MAIL_TIMEOUT bounds both that startup check and every subsequent dial/send. Every Send opens its own OTel span (smtp.send) with a recipient count; never with addresses.
NewIfConfigured: the dev/prod switch
Host and FromAddress are not required env tags: an empty Host is how a project signals "no mail transport configured" (usually in dev, or in a service that never actually sends mail). NewIfConfigured (and its panicking counterpart MustNewIfConfigured) turns that into mail.NewDiscard() instead of an error; New still errors on an empty FromAddress once Host is set, since that would silently mis-address every message. NewIfConfigured is what add mail wires into a generated project.
new project already runs a mailpit service in compose.yml (accepts SMTP, UI at MAILPIT_HOST), and .env.example ships the matching MAIL_* lines, but wiring smtp.Config into your own project config, and calling smtp.NewIfConfigured/MustNewIfConfigured, happens once, on the first add mail call.