add mail
Reach for this generator whenever a module needs to send an email straight to an address, no recipient concept attached: a contact-form confirmation, an invoice, a password-reset link.
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 framework 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{ /* ... */ })
Anchor comments
Like add db, add mail writes at the gpsystem:pools, gpsystem:dependencies-init/gpsystem:deps-init(<module>), gpsystem:dependencies and gpsystem:config anchors. If a module is missing one of these anchor comments, the command stops and prints the exact block to paste in by hand, at the right spot.
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.