mail

SMTP

SMTP transport configuration for mail.

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_"`
}
VariableDefaultMeaning
MAIL_HOST(empty = no transport)SMTP server address
MAIL_PORT587SMTP port
MAIL_USERNAME / MAIL_PASSWORDemptyempty: no auth (Mailpit, internal relays)
MAIL_AUTHautoauto | plain | login | cram-md5 | none
MAIL_TLSstarttlsstarttls | starttls-opportunistic (dev, TLS-less server) | tls (implicit TLS, 465) | none
MAIL_FROM_ADDRESSrequired once MAIL_HOST is setdefault sender address
MAIL_FROM_NAMEemptydefault sender display name
MAIL_TIMEOUT15sdial/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.

A project generated with 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.
  • Overview: the Mailer interface, the Message builder, and the dev mailers.
  • MJML: compiling MJML templates into the HTML this driver sends.
  • add mail: the generator that wires a notifier and this driver into a module.
Copyright © 2026