add mail
go tool gpsystem add mail <module> <name>
Creates a notifier in internal/modules/<module>/mail/<name>.go: a payload struct, a <Name>Notifier composing an injected mail.Mailer with this module's own templates, and its templates/<name>.html.tmpl + templates/<name>.txt.tmpl. On a project's first mail notifier (in any module) it also wires the shared transport into every entry point. Idempotent: a second notifier, in the same or a different module, reuses the wiring.
go tool gpsystem add mail contact contactNotification
Why the split
Mail content (payload, templates, subject) is business content that belongs to the module that owns the use case; the SMTP transport is infrastructure, exactly like the database driver, so it's centralized once. This is the same shared-code decision ladder the kit applies everywhere; see Project structure for the full reasoning and the mail page's worked example.
What it generates
package mail
//go:embed templates/contactnotification.html.tmpl templates/contactnotification.txt.tmpl
var contactNotificationTemplatesFS embed.FS
type ContactNotification struct {
}
type ContactNotificationNotifier struct{ mailer kitmail.Mailer }
func NewContactNotificationNotifier(m kitmail.Mailer) *ContactNotificationNotifier {
return &ContactNotificationNotifier{mailer: m}
}
func (n *ContactNotificationNotifier) Notify(ctx context.Context, to string, p ContactNotification) error {
msg := kitmail.NewMessage().
WithTo(to).
WithSubject("TODO: subject for ContactNotification").
WithBody(kitmail.Template(contactNotificationTemplatesFS, "templates/contactnotification.html.tmpl", p).
WithTextTemplate("templates/contactnotification.txt.tmpl"))
return n.mailer.Send(ctx, msg)
}
Plus a memory-mailer test (contactnotification_test.go) and two placeholder templates you fill in. The payload struct starts empty, the same way add event generates an empty event struct: add whatever fields your templates need.
On the first notifier in the project:
Mail smtp.Config `envPrefix:"MAIL_"`
mailer := smtp.MustNewIfConfigured(ctx, cfg.Mail)
// ...
deps := contact.Dependencies{
// ...
Mailer: mailer,
// gpsystem:dependencies-init
}
type Dependencies struct {
// ...
Mailer mail.Mailer
// gpsystem:dependencies
}
mailer is constructed once per entry point and shared across every module that has a notifier, the same pattern as the shared database pool. smtp.MustNewIfConfigured builds a real SMTP client when MAIL_HOST is set, or mail.NewDiscard() when it's empty, so dev environments run without a mail server configured. See Mail for the full MAIL_* reference.
Sending it
Nothing calls the notifier automatically: wire it from a service (synchronous mail, e.g. a password reset) or a listener (asynchronous, via add event / add listener):
notifier := contactmail.NewContactNotificationNotifier(deps.Mailer)
return notifier.Notify(ctx, recipientAddress, contactmail.ContactNotification{ /* ... */ })
Older projects
Like add db, add mail writes at the gpsystem:pools, gpsystem:dependencies-init/gpsystem:deps-init(<module>), gpsystem:dependencies and gpsystem:config anchors; a project generated before those anchors existed needs them added by hand first (see a freshly generated project for placement).
Which command to use
Content sent straight to an address, with no recipient concept (this page). Content addressed to a recipient, possibly on several channels at once (database inbox, live push): add notification instead. The two compose: a notification's ToMail reuses the same builders shown above.