Rene Nochebuena af6f5ab9e6 docs(smtp): fix fictional launcher.Register/health.Register in doc examples (#2)
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>
2026-08-07 22:19:52 -06:00

einherjar/smtp

version license go health

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.

S
Description
SMTP email sender with attachments, BCC, template rendering, and silent no-op fallback
Readme AGPL-3.0
118 KiB
2026-08-12 15:54:41 -06:00
Languages
Go 100%