The package-doc examples showed a fictional launcher.Register / health.Register API (and launcher.Append as a package func). Corrected to the real wiring: lc.Append + web/health.NewHandler(...).ServeHTTP. Documentation-only. No code, API, or dependency changes; version stays v1.1.0 (the v1.1.0 tag is moved to include this fix). Verified by compiling the corrected pattern against the real API. Reviewed-on: #2 Co-authored-by: Rene Nochebuena Guerrero <rene@nochebuena.dev> Co-committed-by: Rene Nochebuena Guerrero <rene@nochebuena.dev>
3.6 KiB
einherjar/smtp
A raven sent from Valhalla reaches its destination. The sender does not wait at the window.
code.nochebuena.dev/einherjar/smtp is the email sender component of the Einherjar framework. It is built entirely on the Go standard library (net/smtp, mime/multipart, html/template) with no external dependencies. When EINHERJAR_SMTP_HOST is empty, New returns a silent no-op — email absence never blocks a transaction, user registration, or order placement. Health priority is LevelDegraded.
Usage
Setup
import "code.nochebuena.dev/einherjar/smtp"
mailer := smtp.New(logger, smtp.DefaultConfig())
lc.Append(mailer) // OnInit verifies credentials; OnStop is a no-op
// mailer is observability.Checkable (dial check, LevelDegraded):
srv.Get("/health", health.NewHandler(logger, mailer).ServeHTTP)
When cfg.Host is empty, New returns a no-op that logs every Send call at debug level and always returns nil. No code changes are needed between environments.
Sending a plain-text email
err := mailer.Send(ctx, smtp.Message{
To: []string{"user@example.com"},
Subject: "Welcome to the service",
Body: "Your account is ready.",
})
Sending HTML with attachments
err := mailer.Send(ctx, smtp.Message{
To: []string{"user@example.com"},
CC: []string{"support@example.com"},
BCC: []string{"audit@example.com"}, // envelope only — never in headers
Subject: "Your invoice",
Body: renderedHTML,
ContentType: "text/html",
Attachments: []smtp.Attachment{
{
Name: "invoice.pdf",
ContentType: "application/pdf",
Data: pdfReader, // consumed once; do not reuse
},
},
})
Template rendering
Template rendering is intentionally separate from Send. Render to a string first, then assign to Message.Body. This keeps the transport and rendering concerns independent.
tmpl, err := smtp.ParseFS(os.DirFS("templates"), "*.html")
if err != nil {
return err
}
body, err := tmpl.Render("welcome.html", map[string]any{
"Name": user.Name,
"URL": activationURL,
})
if err != nil {
return err
}
err = mailer.Send(ctx, smtp.Message{
To: []string{user.Email},
Subject: "Activate your account",
Body: body,
ContentType: "text/html",
})
Environment variables
| Variable | Required | Default | Description |
|---|---|---|---|
EINHERJAR_SMTP_HOST |
No | "" |
SMTP host. Empty → no-op sender |
EINHERJAR_SMTP_PORT |
No | 587 |
SMTP port |
EINHERJAR_SMTP_USER |
No | "" |
Auth username |
EINHERJAR_SMTP_PASSWORD |
No | "" |
Auth password |
EINHERJAR_SMTP_FROM |
No | "" |
Default From address |
Dependency graph
contracts (zero dependencies)
↑
core
↑
smtp (contracts, core, stdlib only)
↑
your app
No external dependencies beyond the Go standard library.
Verification
cd smtp/
go build ./...
go vet ./...
go test ./...
gofmt -l .
The message matters. The raven is only the path. Make the path reliable. Do not let it stop the battle.