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.
129 lines
3.6 KiB
Markdown
129 lines
3.6 KiB
Markdown
# einherjar/smtp
|
|
|
|
[](https://code.nochebuena.dev/einherjar/smtp)
|
|
[](LICENSE)
|
|
[](https://go.dev)
|
|
[]()
|
|
|
|
> 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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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.
|
|
|
|
```go
|
|
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
|
|
|
|
```bash
|
|
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.*
|