# Email

Send email with `mail.Sender` and an explicit `mail.Transport`, such as SMTP.
Delivery is synchronous and context-aware, and creating a sender makes no network
calls.

```go
sender := mail.Sender{Transport: mail.SMTP{
    Address: "smtp.example.com:587",
    Username: smtpUsername,
    Password: smtpPassword,
}}
err := sender.Send(ctx, mail.Message{
    From: "App <app@example.com>",
    To: []string{"recipient@example.com"},
    Subject: "Account invitation",
    Text: "Your account is ready.",
    HTML: "<p>Your account is ready.</p>",
})
```

- `mail.Message` holds the envelope and the text and HTML content.
- `mail.Sender` validates each message before handing it to the transport.
- Credentials and recipients come from application configuration or trusted data.

The accounts starter wires recovery email in `project/accounts.go`. The
[authentication guide](https://cicadel.fauzaanu.com/en/dev/topics/authentication/index.md) lists its environment settings.

## Ask your agent

```text
Read https://cicadel.fauzaanu.com/en/dev/topics/email/index.md.
Add email delivery for [event] using Cicadel mail.Sender.
Read SMTP credentials from secret configuration, send after successful persistence,
and test recipients and message content with an in-memory transport.
```

## Build a message

Messages support `To`, `Cc`, `Bcc`, `ReplyTo`, plain text, HTML alternatives,
attachments and extension headers:

```go
message.Attachments = []mail.Attachment{{
    Filename: "report.csv",
    ContentType: "text/csv",
    Data: csvBytes,
}}
message.Headers = map[string]string{"X-Application": "reports"}
```

### Attachments

- Attachment names cannot include path separators.
- You supply the bytes. The API does not read arbitrary filesystem paths.
- You own attachment size limits.

### Headers

- Extra headers accept `X-*` headers, `Message-ID`, `In-Reply-To` and `References`.
- They cannot override envelope, MIME or Bcc fields.
- Duplicate header names, compared case-insensitively, are rejected.
- CR, LF and NUL header injection is rejected.

> [!NOTE]
> Bcc addresses are used only for the SMTP envelope. They never appear in the
> serialized MIME.

Do not change a message while it is being sent.

## Render the body

Use `mail.RenderComponent(ctx, message, content)` to render a compiled typed
component into the HTML body. Set `message.Text` yourself for the text
alternative.

`mail.Render` remains a compatibility helper. It uses separate Go text and
escaped HTML templates.

- Neither helper turns arbitrary user HTML into trusted markup.
- Email clients support less CSS than browsers. Tailor email components to the
  clients you target.

## MIME output

`Message.Bytes()` validates and serializes the message. The output has:

- Date and Message-ID headers, and UTF-8 headers;
- quoted-printable bodies;
- multipart alternatives and base64 attachments;
- CRLF line endings and standard line wrapping.

The sender and recipient mailboxes are parsed and validated.

## SMTP

`SMTP.Encryption` controls the connection:

| Mode | Behavior |
| :--- | :--- |
| `starttls` (default) | Requires STARTTLS and certificate verification. |
| `tls` | Implicit TLS, commonly on port 465. |
| `plain` | Only to an actual loopback address, and refuses credentials. For local mail capture tools. |

- A cloned `TLSConfig` can supply a private certificate pool.
- Disabling verification is rejected. The minimum TLS version is 1.2.
- Credentials are sent only after TLS.

## Send in batches

`Sender.SendMany` validates the whole input before delivering anything. It reuses
one SMTP connection when the server supports it, and returns the number of
messages accepted before an error.

- The default timeout is 30 seconds for the whole batch.
- Context cancellation closes the connection, even while waiting for an SMTP
  response.
- A failed QUIT after every DATA command was accepted does not mark those
  messages as failed.
- A connection failure during DATA acknowledgement can leave delivery uncertain.

> [!WARNING]
> The framework never silently retries or swallows delivery errors. You own the
> retry and idempotency policy.

## Development backends

| Backend | Behavior |
| --- | --- |
| `mail.Capture` | Thread-safe in-memory outbox with deep copies on write and read |
| `&mail.Console{Writer: writer}` | Serialized MIME to an explicit writer, with synchronized writes |
| `mail.File{Directory: path}` | One mode-0600 `.eml` file per message in a private directory |
| `mail.Discard{}` | Explicitly validate without delivery |
| Custom `mail.Transport` | Implement `Send(context.Context, mail.Message) error` |

Console and file messages include private bodies and reset links. Select them
explicitly for development. The accounts starter uses SMTP. Writers,
filesystems, credentials and provider accounts stay application-owned.

## Testing

Tests send no real email. The SMTP tests use isolated loopback servers with
locally created certificates, and account tests inject a capture callback.

`mail/delivery_test.go` covers MIME parsing, attachment decoding, Bcc privacy,
header rejection, outbox isolation, file permissions, STARTTLS and implicit TLS,
connection reuse, partial delivery, failed upgrades and cancellation. Run the
package under the race detector alongside the framework CI suite.
