Introduces code.nochebuena.dev/einherjar/smtp — the email delivery starter for the Einherjar framework. New module with no micro-lib counterpart. Pure stdlib — no external dependencies beyond contracts and core. Interfaces (CT-6: one TypeSpec per file): - Sender — Send(ctx context.Context, msg Message) error - Component — lifecycle.Component + observability.Checkable + Sender - Message — To, CC, BCC, ReplyTo, Subject, Body, ContentType, Attachments - Attachment — Name, ContentType string; Data io.Reader (consumed once on Send) - Template — wraps html/template for email rendering Implementation: - New(logger, cfg) Component — returns noopClient when cfg.Host is empty; SMTP absence must never block a transaction (e.g. account creation) - noopClient: all lifecycle and Send methods return nil; Send logs Warn; Priority LevelDegraded; Name "smtp" - smtpClient: OnInit/OnStart/OnStop are no-ops (stateless stdlib client); HealthCheck: TCP dial to Host:Port; Priority LevelDegraded - Send: buildRawMessage → netsmtp.SendMail; BCC passed in SMTP envelope (RCPT TO) but never written to message headers (RFC 5321/5322 compliance) - buildRawMessage: plain message without attachments; multipart/mixed with quoted-printable body + base64 attachments when Attachments non-empty; multipart.NewWriter created first to obtain boundary before headers are written - ParseFS(fsys, patterns...) (*Template, error) — wraps html/template.ParseFS - Template.Render(name, data) (string, error) — executes named template - net/smtp aliased as netsmtp to avoid package-name collision in package smtp Config (EINHERJAR_SMTP_* env vars): Host, Port(587), User, Password, From - Component interface embeds observability.Identifiable; identifiable.go implements ModulePath and ModuleVersion on both smtpClient and noopClient via runtime/debug.ReadBuildInfo() — prints in launcher banner
53 lines
1.9 KiB
Go
53 lines
1.9 KiB
Go
// Package smtp provides a lifecycle-managed SMTP client for sending email
|
|
// in Einherjar applications.
|
|
//
|
|
// # Overview
|
|
//
|
|
// [New] returns a [Component] that satisfies [lifecycle.Component] lifecycle
|
|
// hooks, [observability.Checkable] with degraded priority, and the [Sender]
|
|
// interface for dispatching email. When [Config.Host] is empty, [New] returns
|
|
// a no-op implementation that logs a warning and silently discards every message —
|
|
// email failure must never block a transaction.
|
|
//
|
|
// # Lifecycle Registration
|
|
//
|
|
// client := smtp.New(logger, cfg)
|
|
// launcher.Register(client)
|
|
// health.Register(client)
|
|
//
|
|
// # Sending
|
|
//
|
|
// Services depend only on [Sender]. Build a [Message] with a pre-rendered body,
|
|
// then call Send:
|
|
//
|
|
// msg := smtp.Message{
|
|
// To: []string{"alice@example.com"},
|
|
// Subject: "Your receipt",
|
|
// Body: rendered, // pre-rendered string from Template.Render
|
|
// ContentType: "text/html",
|
|
// }
|
|
// if err := mailer.Send(ctx, msg); err != nil {
|
|
// return err
|
|
// }
|
|
//
|
|
// # Template Rendering
|
|
//
|
|
// [ParseFS] wraps stdlib html/template for HTML email rendering. Rendering is
|
|
// intentionally separate from Send — callers render to a string and assign it
|
|
// to Message.Body. This keeps Send unit-testable without template involvement.
|
|
//
|
|
// tmpl, err := smtp.ParseFS(os.DirFS("templates"), "*.html")
|
|
// body, err := tmpl.Render("receipt.html", data)
|
|
// msg := smtp.Message{Body: body, ContentType: "text/html", ...}
|
|
//
|
|
// # Configuration
|
|
//
|
|
// All fields are read from environment variables with the EINHERJAR_SMTP_* prefix:
|
|
//
|
|
// - EINHERJAR_SMTP_HOST — SMTP server hostname (empty = no-op mode)
|
|
// - EINHERJAR_SMTP_PORT — default: 587 (STARTTLS)
|
|
// - EINHERJAR_SMTP_USER — auth username (optional)
|
|
// - EINHERJAR_SMTP_PASSWORD — auth password (optional)
|
|
// - EINHERJAR_SMTP_FROM — envelope sender address
|
|
package smtp
|