Files
smtp/README.md
T
Rene Nochebuena df95965066 chore(smtp): bump einherjar deps to v1.1.0 (framework version alignment)
Patch of the coordinated framework release: einherjar modules move to v1.1.0
in lockstep, so smtp is bumped alongside the rest.

- go.mod: contracts, core -> v1.1.0 (via go get + go mod tidy)

No code or API changes. ModuleVersion() reports v1.1.0 automatically from
build info once tagged. Badge bumped to v1.1.0.
2026-08-07 18:56:47 -06:00

3.5 KiB

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())
launcher.Append(mailer)   // OnInit verifies credentials; OnStop is a no-op
health.Register(mailer)   // dial check; LevelDegraded

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.