128 lines
3.5 KiB
Markdown
128 lines
3.5 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())
|
||
|
|
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
|
||
|
|
|
||
|
|
```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.*
|