Each module README now says where the module sits in the dependency graph, why it is its own module, and lists use cases for application developers.
165 lines
7.9 KiB
Markdown
165 lines
7.9 KiB
Markdown
# postcard
|
|
|
|
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"`
|
|
|
|
**Name:** What you send home in summer; it delivers the application's mail.
|
|
|
|
## 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.
|
|
|
|
## Role in the framework
|
|
|
|
A service module on `backpack`, `compass` and `pact`; `party` registers the templates plugins contribute. It is separate so mail rendering and delivery can be tested and swapped (memory, log, SMTP) without touching the code that sends.
|
|
|
|
Use cases:
|
|
|
|
- Send account mail (welcome, password reset) from Markdown templates owned by the plugin.
|
|
- Share one branded layout across all of an application's mail.
|
|
- Capture sent mail in memory in tests, or log it in development instead of delivering it.
|
|
|
|
## 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.
|