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
34 lines
1.1 KiB
Go
34 lines
1.1 KiB
Go
package smtp
|
|
|
|
import (
|
|
"html/template"
|
|
"io/fs"
|
|
"strings"
|
|
|
|
"code.nochebuena.dev/einherjar/core/xerrors"
|
|
)
|
|
|
|
// Template wraps stdlib html/template for HTML email rendering.
|
|
// Rendering is intentionally separate from [Sender.Send] — callers render
|
|
// a template to a string and assign it to [Message.Body].
|
|
type Template struct{ t *template.Template }
|
|
|
|
// ParseFS parses the named pattern files from fsys into a Template.
|
|
// Returns an error if any pattern matches no files or a file fails to parse.
|
|
func ParseFS(fsys fs.FS, patterns ...string) (*Template, error) {
|
|
t, err := template.ParseFS(fsys, patterns...)
|
|
if err != nil {
|
|
return nil, xerrors.New(xerrors.ErrInvalidInput, "smtp: parse template").WithError(err)
|
|
}
|
|
return &Template{t: t}, nil
|
|
}
|
|
|
|
// Render executes the named template with data and returns the rendered string.
|
|
func (t *Template) Render(name string, data any) (string, error) {
|
|
var buf strings.Builder
|
|
if err := t.t.ExecuteTemplate(&buf, name, data); err != nil {
|
|
return "", xerrors.New(xerrors.ErrInternal, "smtp: render template").WithError(err)
|
|
}
|
|
return buf.String(), nil
|
|
}
|