# Mailer

Send transactional email through a log-by-default mailer, and switch to SMTP for production.

Use `mailer.Mailer` for transactional outbound mail: receipts, cancellations, password resets. It defaults to `log`, so validated messages go to the log instead of the wire and real delivery is an explicit adapter change. It handles validated delivery only (no template rendering, no lists), so build the finished `Mail` with `NewMail`, or with a `Sender` that binds the default `From`.

Stay on `log` for development and CI, and move to `smtp` for production; there is no in-between adapter. Enqueue sends as jobs so an SMTP blip retries at the job layer instead of failing the booking write.

## Quick start

```sh
MAILER_ADAPTER=log
MAILER_HOST=localhost
MAILER_PORT=25
```

`host` and `port` are required by `Validate` even for the log adapter, which never dials; the bookings default uses the dummy localhost pair for that reason.

```go title="main.go" check
package main

import (
	"context"
	"fmt"

	maillog "github.com/zenta-dev/zever/adapters/mailer/log"
	"github.com/zenta-dev/zever/core/mailer"
)

func main() {
	maillog.Register()
	m, _ := mailer.Open(mailer.Log, mailer.Options{Host: "localhost", Port: 25})
	defer m.Close()

	msg := mailer.NewMail(
		mailer.Address{Address: "bookings@example.com"},
		[]mailer.Address{{Address: "guest@example.com"}},
		"Booking confirmed",
		"Space Loft, Fri to Sun. Receipt inside.",
	)
	_ = m.Send(context.Background(), &msg)
	fmt.Println("logged, not delivered")
}
```

```text
{"from":{"address":"bookings@example.com"},"to":[{"address":"guest@example.com"}],"cc":[],"bcc":[],"subject":"Booking confirmed","body":"Space Loft, Fri to Sun. Receipt inside.","html":"","attachments":[]}
logged, not delivered
```

`Send` validates, writes one JSON line to stdout (attachments log only name and size, never bytes), and returns nil without opening a socket. In tests, use `maillog.NewWithWriter(opts, w)`.

## Send through SMTP

```sh
MAILER_ADAPTER=smtp
MAILER_HOST=mail.example.com
MAILER_PORT=587
MAILER_USERNAME=bookings
MAILER_PASSWORD=prod-secret-here
MAILER_ENCRYPTION=starttls
MAILER_TIMEOUT=30s
MAILER_MAX_MESSAGE_SIZE=10485760
```
```sh
MAILER_ADAPTER=smtp
MAILER_HOST=mail.example.com
MAILER_PORT=587
MAILER_USERNAME=bookings
MAILER_PASSWORD=prod-secret-here
MAILER_ENCRYPTION=starttls
MAILER_TIMEOUT=30s
```

- `encryption`: `starttls` (default when empty), `implicit`, or `none`.
- `timeout`: zero means the 30s default. File durations are integer nanoseconds, env accepts `"30s"`.
- `max_message_size`: zero means the 10 MiB default; larger messages fail with `ErrMessageTooLarge`.

File decoding is strict snake_case. All keys: [mailer config reference](https://zenta-dev.github.io/zever/reference/config/#mailer).

| Adapter | Package | Notes |
|---|---|---|
| `log` | `adapters/mailer/log` | Default, safe sink, writes each message as a JSON line |
| `smtp` | `adapters/mailer/smtp` | Production delivery, honors encryption and timeout |

Both serve `mailer.Mailer` with `Send` plus `Close`; delivery errors stay `errors.Is`-compatible for job-layer retry branching.

## Resolve the mailer from the container

```go title="main.go"
m, err := c.Mailer()
```

`Mailer()` resolves lazily and caches; on error the cache clears. Address parsing helpers live in `core/mailer/address.go` and the attachment shape in `core/mailer/mail.go`. `Close() error` is instant, and the container probes it as a snapshot service with no leaf ordering. See [the container](https://zenta-dev.github.io/zever/concepts/container/).

## Production checklist

- `EncryptionNone` must not carry `username` or `password`; validation rejects that pair so credentials never cross plaintext. Use `implicit` only when the provider documents it.
- Set `username` and `password` together; a lone value fails validation instead of half-authenticating at send time.
- `host` must not contain a scheme (`mail.example.com`, not `smtp://mail.example.com`). Port must be 1 to 65535.
- Address validation rejects header injection and control characters; parse user input through `core/mailer/address.go` before using it as `From`.
- Keep attachments small, or link to `storage` presigned URLs instead of inlining.
- SMTP passwords never appear in logs. Supply them by file or env, display config only through `config.RedactedServices`, and keep error text to names and reasons.
- Retry transient failures at the job layer; bookings `SendConfirmation` uses `max_attempts(3)` with exponential base 30s.

## Troubleshooting

<details>
<summary>host must be non-empty on boot with the log adapter</summary>

`host` and `port` were left zero; validation requires both even though log never dials. Set `MAILER_HOST=localhost` and `MAILER_PORT=25` in your `.env`.
</details>

<details>
<summary>username and password must be set together</summary>

Only one of the pair was supplied, often a password without a username. Set both from the same secret source, or clear both for an anonymous relay with a non-none encryption mode.
</details>

<details>
<summary>encryption none must not carry credentials</summary>

`encryption: none` combined with SMTP auth values. Switch to `starttls` or `implicit` when auth is needed, or drop the credentials when the relay needs none.
</details>

<details>
<summary>Sends pass locally but time out in production</summary>

Wrong port or encryption for the provider, or blocked egress. Confirm the provider tuple (commonly 587 with `starttls`, 465 with `implicit`), raise `timeout` from the 30s default, and move the send behind `job.Dispatcher` so timeouts retry with backoff.
</details>

## Related

- [Notifications](https://zenta-dev.github.io/zever/guides/background/notifications/): Push and SMS alongside mail.
- [Scheduler and jobs](https://zenta-dev.github.io/zever/guides/background/scheduler-jobs/): Retry wrapping around sends.
- [Build a booking API](https://zenta-dev.github.io/zever/tutorials/build-a-booking-api/): The bookings tutorial with SendConfirmation.
