docs(modules): rewrite wristband, bouncer, surf, bonfire, phrasebook, postcard READMEs
This commit is contained in:
@@ -1,3 +1,152 @@
|
||||
# postcard
|
||||
|
||||
`postcard` renders and delivers mail through configured memory, log, SMTP, and template drivers. `party` and Fonoteka's user plugin import it for account mail; configure delivery with `postcard.NewSMTPDriver` in `drivers.go`.
|
||||
Transactional mail from plugin-owned Markdown templates and layouts, delivered through a memory, log or SMTP driver.
|
||||
|
||||
`import "git.golem15.com/golem15/summercms/modules/postcard"`
|
||||
|
||||
## Overview
|
||||
|
||||
postcard is the mail layer of SummerCMS. Plugins that implement `pact.HasMailTemplates` ship WinterCMS-shaped mail files under `views/mail/`; at boot, `postcard.Activate` publishes one app-scoped `postcard.Mailer` built from the compass `mail.*` config, and `postcard.BootPlugin` registers each plugin's templates and layouts as the plugin boots. Callers then send a template by its dotted name with a map of variables. It is the counterpart of WinterCMS's `Mail::send` with `views/mail/*.htm` templates and mail layouts.
|
||||
|
||||
## Features
|
||||
|
||||
- Templates named `<plugin>::mail.<name>` and loaded from `views/mail/<name>.htm` (dots become directories). A plugin may only register names in its own namespace; missing files, duplicates and unknown layout aliases fail at boot.
|
||||
- WinterCMS file format: an INI header (`subject`, `layout`, `description`), a `==` separator, then a Markdown body rendered with Go `html/template` variables such as `{{ .name }}`.
|
||||
- Layouts with a header, a text wrapper and an HTML wrapper, referenced from templates by a short alias (`pact.HasMailTemplates` maps aliases to full names). A neutral default layout is built in.
|
||||
- Every message gets an HTML part (Markdown converted with goldmark) and a plain-text part. `mail.css` and `mail.brandCss` are inlined into the layout's style block.
|
||||
- Safety checks: rendered HTML with script, iframe, object or embed tags, inline event handlers or `javascript:`/`vbscript:`/`data:` URLs is rejected; CR/LF in the subject or address headers is rejected; addresses are parsed with `net/mail`.
|
||||
- Drivers behind the `postcard.Driver` interface: `postcard.MemoryDriver` (keeps messages for tests), `postcard.LogDriver` (logs headers and the text part through `log/slog`, never the HTML or credentials), `postcard.SMTPDriver` (go-mail with an explicit TLS policy) and `postcard.FailDriver` (a deterministic failure for tests).
|
||||
- No locale selection: callers pass the full template name, including any locale suffix.
|
||||
|
||||
## Usage
|
||||
|
||||
A plugin ships `views/mail/welcome.htm`:
|
||||
|
||||
```text
|
||||
subject = "Welcome, {{ .name }}"
|
||||
description = "Sent after registration"
|
||||
==
|
||||
Hi **{{ .name }}**, thanks for joining the blog.
|
||||
```
|
||||
|
||||
and sends it through the mailer the runtime published:
|
||||
|
||||
```go
|
||||
package blog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/postcard"
|
||||
)
|
||||
|
||||
func SendWelcome(ctx context.Context, app *backpack.App, email, name string) error {
|
||||
mailer, ok := app.Lookup[postcard.Mailer]()
|
||||
if !ok {
|
||||
return errors.New("mailer not published")
|
||||
}
|
||||
return mailer.Send(ctx, postcard.Message{
|
||||
Template: "acme.blog::mail.welcome",
|
||||
To: []string{email},
|
||||
Vars: map[string]any{"name": name},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
In tests, build the catalog and mailer directly and inspect what was sent:
|
||||
|
||||
```go
|
||||
package blog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/postcard"
|
||||
)
|
||||
|
||||
func TestWelcomeMail(t *testing.T) {
|
||||
mailFS := fstest.MapFS{"views/mail/welcome.htm": {Data: []byte("subject = \"Welcome, {{ .name }}\"\n==\nHi **{{ .name }}**.\n")}}
|
||||
cat := postcard.NewCatalog()
|
||||
if err := cat.Register("acme.blog", mailFS, []string{"acme.blog::mail.welcome"}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
driver := postcard.NewMemoryDriver()
|
||||
mailer := postcard.NewMailer(cat, driver, postcard.Options{From: "blog@example.com"})
|
||||
msg := postcard.Message{Template: "acme.blog::mail.welcome", To: []string{"ada@example.com"}, Vars: map[string]any{"name": "Ada"}}
|
||||
if err := mailer.Send(context.Background(), msg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := driver.Messages()[0].Subject; got != "Welcome, Ada" {
|
||||
t.Fatalf("subject = %q", got)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API reference
|
||||
|
||||
| Identifier | Description |
|
||||
|------------|-------------|
|
||||
| `postcard.Activate` | Builds the driver from config and publishes the app-scoped `postcard.Mailer` before plugins boot. |
|
||||
| `postcard.BootPlugin` | Registers a booting plugin's declared templates and layouts with the published mailer. |
|
||||
| `postcard.Mailer` | Sends a registered template through the configured driver. |
|
||||
| `postcard.NewMailer` | Builds a mailer from a catalog, a driver and `postcard.Options`. |
|
||||
| `postcard.Message` | What callers send: template name, recipients, reply-to, variables and an optional subject override. |
|
||||
| `postcard.Options` | Mailer settings: sender address, CSS and brand CSS. |
|
||||
| `postcard.Catalog` | Registered templates and layouts, including the built-in default layout. |
|
||||
| `postcard.NewCatalog` | Returns a catalog holding only the default layout. |
|
||||
| `postcard.Catalog.Register` | Loads a plugin's templates and layout aliases from an `fs.FS`. |
|
||||
| `postcard.Driver` | Delivers a `postcard.RenderedMessage`. |
|
||||
| `postcard.RenderedMessage` | The validated payload a driver transmits: headers, HTML part and text part. |
|
||||
| `postcard.NewMemoryDriver` | In-memory driver; `postcard.MemoryDriver.Messages` returns what was sent. |
|
||||
| `postcard.NewLogDriver` | Driver that logs through a `*slog.Logger` (the default logger when nil). |
|
||||
| `postcard.NewSMTPDriver` | SMTP driver built from a `postcard.SMTPConfig`. |
|
||||
| `postcard.SMTPConfig` | Host, port, credentials, TLS policy and timeout for the SMTP driver. |
|
||||
| `postcard.FailDriver` | Driver whose `postcard.FailDriver.Send` always returns `postcard.FailDriver.Err`. |
|
||||
|
||||
## Configuration
|
||||
|
||||
`postcard.Activate` reads these keys from the app's [compass](../compass/README.md) config:
|
||||
|
||||
| Key | Default | Controls |
|
||||
|-----|---------|----------|
|
||||
| `mail.driver` | `memory` | Delivery driver: `memory`, `log` or `smtp`. Any other value fails at boot. |
|
||||
| `mail.from` | empty | Sender address. Required by the SMTP driver. |
|
||||
| `mail.css` | empty | CSS inlined into the layout. |
|
||||
| `mail.brandCss` | empty | Brand CSS inlined before `mail.css`. |
|
||||
| `mail.smtp.host` | none | SMTP host. Required when `mail.driver` is `smtp`. |
|
||||
| `mail.smtp.port` | `587` | SMTP port. |
|
||||
| `mail.smtp.username` | empty | SMTP user. When set, PLAIN authentication is used. |
|
||||
| `mail.smtp.password` | empty | SMTP password. |
|
||||
| `mail.smtp.tls` | `mandatory` | TLS policy: `mandatory` (or `tls`), `starttls` (or `opportunistic`), `none` (or `notls`, `off`). Plain connections are never inferred. |
|
||||
| `mail.smtp.timeout` | `10s` | Connection timeout, as a Go duration or a number of seconds. |
|
||||
|
||||
```yaml
|
||||
mail:
|
||||
driver: smtp
|
||||
from: blog@example.com
|
||||
smtp:
|
||||
host: smtp.example.com
|
||||
port: 587
|
||||
username: blog
|
||||
password: <secret>
|
||||
tls: mandatory
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- SummerCMS modules: [backpack](../backpack/README.md), [compass](../compass/README.md), [pact](../pact/README.md).
|
||||
- Third-party: `github.com/wneessen/go-mail` (SMTP), `github.com/yuin/goldmark` (Markdown).
|
||||
- Standard library: `bytes`, `context`, `embed`, `fmt`, `html/template`, `io/fs`, `log/slog`, `net/mail`, `regexp`, `strings`, `sync`, `text/template`, `time`.
|
||||
- Tests additionally use `github.com/testcontainers/testcontainers-go` (Mailpit container).
|
||||
|
||||
## Testing
|
||||
|
||||
```sh
|
||||
go test ./modules/postcard/...
|
||||
```
|
||||
|
||||
The SMTP integration test starts a Mailpit container through testcontainers-go and needs Docker. Run `go test -short ./modules/postcard/...` to skip it; the remaining tests use the memory and fail drivers and need no external services.
|
||||
|
||||
Reference in New Issue
Block a user