Implement the go/smtp module following the standard go-kit module pattern: a single *Client type returned by New() that satisfies launcher.Component, health.Checkable, and the module's own smtp.Sender interface.
SMTP is infrastructure — it must attach to the launcher lifecycle and report health. Inline implementation inside a consuming project (e.g. iron-dough-api) is explicitly out of scope.
// Sender is the interface consumers depend on — services only see this.typeSenderinterface{Send(ctxcontext.Context,msgMessage)error}
*Client also implements launcher.Component (Start/Stop) and health.Checkable (Check). Callers depend only on the subset they need — the health registry only sees Checkable, services only see Sender.
Message and Attachment
typeMessagestruct{To[]stringCC[]stringBCC[]stringReplyTostringSubjectstringBodystring// pre-rendered: plain text or HTML stringContentTypestring// "text/plain" | "text/html"Attachments[]Attachment}typeAttachmentstruct{NamestringContentTypestringDataio.Reader// flexible: file, bytes.Reader, generated PDF, S3 stream}
No-op mode
When Config.Host is empty, New() returns a no-op Sender that logs a warning and returns nil on every Send call. This allows the restaurant to run without configuring SMTP — email failure must never block a transaction.
Health check
Implements health.Checkable
Dials TCP to Host:Port (or sends SMTP NOOP) to verify reachability
Returns degraded (not down) — email is non-critical infrastructure
No-op client always returns healthy
Template helper
A thin Template wrapper around stdlib html/template for HTML email rendering. Rendering is intentionally separate from Send — the caller renders a template to a string and puts it in Message.Body. This keeps Send unit-testable without template involvement.
Message supports To, CC, BCC, ReplyTo, Subject, Body, ContentType, Attachments
Attachment.Data is io.Reader (not []byte)
Health check returns degraded (not down) when SMTP host is unreachable
Template wrapper over html/template with ParseFS + Render
No-op mode logs warning and returns nil on Send
Unit tests: Send with no-op client, health check degraded path, template rendering
## Overview
Implement the `go/smtp` module following the standard go-kit module pattern: a single `*Client` type returned by `New()` that satisfies `launcher.Component`, `health.Checkable`, and the module's own `smtp.Sender` interface.
SMTP is infrastructure — it must attach to the launcher lifecycle and report health. Inline implementation inside a consuming project (e.g. iron-dough-api) is explicitly out of scope.
---
## Module structure
```
go/smtp/
config.go — smtp.Config (env struct tags)
smtp.go — New(), *Client type
message.go — Message, Attachment types
template.go — Template helper (html/template wrapper)
noop.go — no-op Sender when SMTP_HOST is empty
```
---
## Config
`smtp.Config` owns its own struct with env tags. Consuming projects embed this type directly:
```go
// smtp/config.go
type Config struct {
Host string `env:"SMTP_HOST"`
Port int `env:"SMTP_PORT" envDefault:"587"`
User string `env:"SMTP_USER"`
Password string `env:"SMTP_PASSWORD"`
From string `env:"SMTP_FROM"`
}
```
```go
// consuming project internal/config/config.go
type Config struct {
SMTP smtp.Config
// ...
}
```
---
## Interfaces
```go
// Sender is the interface consumers depend on — services only see this.
type Sender interface {
Send(ctx context.Context, msg Message) error
}
```
`*Client` also implements `launcher.Component` (Start/Stop) and `health.Checkable` (Check). Callers depend only on the subset they need — the health registry only sees `Checkable`, services only see `Sender`.
---
## Message and Attachment
```go
type Message struct {
To []string
CC []string
BCC []string
ReplyTo string
Subject string
Body string // pre-rendered: plain text or HTML string
ContentType string // "text/plain" | "text/html"
Attachments []Attachment
}
type Attachment struct {
Name string
ContentType string
Data io.Reader // flexible: file, bytes.Reader, generated PDF, S3 stream
}
```
---
## No-op mode
When `Config.Host` is empty, `New()` returns a no-op `Sender` that logs a warning and returns `nil` on every `Send` call. This allows the restaurant to run without configuring SMTP — email failure must never block a transaction.
---
## Health check
- Implements `health.Checkable`
- Dials TCP to `Host:Port` (or sends SMTP NOOP) to verify reachability
- Returns `degraded` (not `down`) — email is non-critical infrastructure
- No-op client always returns `healthy`
---
## Template helper
A thin `Template` wrapper around stdlib `html/template` for HTML email rendering. Rendering is intentionally **separate** from `Send` — the caller renders a template to a string and puts it in `Message.Body`. This keeps `Send` unit-testable without template involvement.
```go
type Template struct{ t *template.Template }
func ParseFS(fsys fs.FS, patterns ...string) (*Template, error)
func (t *Template) Render(name string, data any) (string, error)
```
---
## Wiring example (consuming project)
```go
// internal/wire/launcher.go
client := smtp.New(cfg.SMTP)
launcher.Register(client) // launcher.Component — Start/Stop lifecycle
health.Register("smtp", client) // health.Checkable — degraded on unreachable
container.Provide(client) // smtp.Sender — consumed by receipt/notification services
```
---
## Acceptance criteria
- [ ] `smtp.Config` struct with env tags; no config defined outside this module
- [ ] `New(cfg Config) *Client` — returns no-op when `cfg.Host` is empty
- [ ] `*Client` implements `smtp.Sender`, `launcher.Component`, `health.Checkable`
- [ ] `Message` supports `To`, `CC`, `BCC`, `ReplyTo`, `Subject`, `Body`, `ContentType`, `Attachments`
- [ ] `Attachment.Data` is `io.Reader` (not `[]byte`)
- [ ] Health check returns `degraded` (not `down`) when SMTP host is unreachable
- [ ] `Template` wrapper over `html/template` with `ParseFS` + `Render`
- [ ] No-op mode logs warning and returns `nil` on `Send`
- [ ] Unit tests: `Send` with no-op client, health check degraded path, template rendering
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Overview
Implement the
go/smtpmodule following the standard go-kit module pattern: a single*Clienttype returned byNew()that satisfieslauncher.Component,health.Checkable, and the module's ownsmtp.Senderinterface.SMTP is infrastructure — it must attach to the launcher lifecycle and report health. Inline implementation inside a consuming project (e.g. iron-dough-api) is explicitly out of scope.
Module structure
Config
smtp.Configowns its own struct with env tags. Consuming projects embed this type directly:Interfaces
*Clientalso implementslauncher.Component(Start/Stop) andhealth.Checkable(Check). Callers depend only on the subset they need — the health registry only seesCheckable, services only seeSender.Message and Attachment
No-op mode
When
Config.Hostis empty,New()returns a no-opSenderthat logs a warning and returnsnilon everySendcall. This allows the restaurant to run without configuring SMTP — email failure must never block a transaction.Health check
health.CheckableHost:Port(or sends SMTP NOOP) to verify reachabilitydegraded(notdown) — email is non-critical infrastructurehealthyTemplate helper
A thin
Templatewrapper around stdlibhtml/templatefor HTML email rendering. Rendering is intentionally separate fromSend— the caller renders a template to a string and puts it inMessage.Body. This keepsSendunit-testable without template involvement.Wiring example (consuming project)
Acceptance criteria
smtp.Configstruct with env tags; no config defined outside this moduleNew(cfg Config) *Client— returns no-op whencfg.Hostis empty*Clientimplementssmtp.Sender,launcher.Component,health.CheckableMessagesupportsTo,CC,BCC,ReplyTo,Subject,Body,ContentType,AttachmentsAttachment.Dataisio.Reader(not[]byte)degraded(notdown) when SMTP host is unreachableTemplatewrapper overhtml/templatewithParseFS+RendernilonSendSendwith no-op client, health check degraded path, template rendering