# Lifecycle email: verification, password reset, usage alerts

Transactional email for the developer portal, implemented in
`internal/mail` (delivery) and `internal/api/ext_lifecycle.go` (the
feature). Every message is plain text with an HTML twin rendered by
`mail.Wrap`.

## Delivery

| Variable | Meaning |
|----------|---------|
| `SMTP_HOST` | SMTP server. **Empty = outbox mode**: nothing is sent, every message is appended to `DATA_DIR/outbox.jsonl` (one JSON object per line) and the last 200 stay in memory. Use this locally and in tests. |
| `SMTP_PORT` | 587 (STARTTLS, default) or 465 (implicit TLS). |
| `SMTP_USER`, `SMTP_PASS` | PLAIN auth; leave empty for an unauthenticated relay. |
| `SMTP_FROM` | Sender, e.g. `BitCall <no-reply@bitcall.stream>`. |
| `SMTP_INSECURE` | `true` skips TLS certificate verification. Development only. |
| `PUBLIC_URL` | Base of every link in an email (`https://api.example.com`). The portal pages `/portal/verify.html` and `/portal/reset.html` must be served from it. |

## Email kinds

| `kind` (outbox tag) | Subject | When |
|---|---|---|
| `verify_email` | Verify your BitCall email | right after signup, and on `POST /v1/verify-email/resend` |
| `welcome` | Your BitCall account is verified | the first time the address is verified: three next steps and the docs link |
| `password_reset` | Reset your BitCall password | `POST /v1/password/forgot` for an account or a team member |
| `usage.warning` | BitCall: `<app>` has used 80% of its monthly minutes | an app with `monthly_minutes > 0` reaches 80 % of its quota |
| `usage.quota_reached` | BitCall: `<app>` reached its monthly minute limit | the app reaches 100 % |

`Meta` on each outbox entry carries `account_id`, and `app_id`, `month`,
`threshold` or `member_id` where relevant.

## Email verification

1. Signup (`POST /v1/signup`) creates the account as before. The lifecycle
   extension observes the `201` and mails a verification link to every
   account that is not verified and never received one (in practice, the
   account that just signed up).
2. The link is `<PUBLIC_URL>/portal/verify.html?token=<64 hex chars>`. The
   portal page posts the token to the API.
3. `POST /v1/verify-email {"token"}` → `200 {"email_verified":true, "email",
   "first_verification"}` and sets `email_verified` / `email_verified_at` on
   the account; `400` for an unknown, used, replaced or expired token.
   The first verification also sends the welcome mail and records an audit
   entry (`verified email`, actor `owner`).
4. `POST /v1/verify-email/resend {"email"}` always answers `200` (no account
   enumeration). A new link is mailed at most once per 5 minutes per email;
   a new link replaces the previous one.
5. `GET /v1/account/verification` (portal session, every member role) →
   `{"email_verified", "pending", "email_verified_at"?}`. `pending` is true
   while an unexpired link is outstanding.

Tokens live 24 hours, are single use and are stored SHA-256 hashed in
`DATA_DIR/lifecycle.json` (never in clear).

**Live access gate:** `POST /v1/account/live-request` answers
`403 {"error":{"code":"forbidden","message":"verify your email first"}}`
until the account email is verified. Sandbox keys and everything else in the
portal work before verification.

## Password reset

- `POST /v1/password/forgot {"email"}` → always `200`. When the email is an
  account login or a team member login, a reset link
  `<PUBLIC_URL>/portal/reset.html?token=…` valid for **1 hour** is mailed, at
  most once per 5 minutes per login. Disabled logins get nothing.
- `POST /v1/password/reset {"token", "new_password"}` (8+ characters) →
  `200 {"ok":true, "kind":"account"|"member", "email"}`. Only the password
  changes (2FA, sessions and the verification flag are untouched). The token
  and every other outstanding reset token of the same login are invalidated.
  `400` for a bad, used or expired token. An audit entry `reset password via
  email link` is recorded with actor `owner` or `member`.

## Usage alerts

A goroutine runs every 5 minutes (`Lifecycle().RunUsageAlerts(now)` runs one
pass; tests call it with a fake clock). For every enabled app with
`settings.monthly_minutes > 0` it compares the current month's relayed
minutes with the quota:

- **≥ 80 %** → `usage.warning` to the account email and to enabled team
  members with role `owner` or `admin`.
- **100 %** → `usage.quota_reached` to the same recipients. An app that jumps
  straight past 80 % gets the quota mail only.

Each threshold fires once per app and month; the state is in
`DATA_DIR/usage-alerts.json` so a restart does not repeat a mail. The text
names the app, used/limit minutes, the month and links to the portal.

## Audit

Successful verifications and password resets are recorded in the audit log
(`docs/features/security.md`) targeted at the account, so they show up in the
portal's audit view.

## Files

- `internal/api/ext_lifecycle.go`, tests in `ext_lifecycle_test.go`
- `DATA_DIR/lifecycle.json` (hashed tokens), `DATA_DIR/usage-alerts.json`,
  `DATA_DIR/outbox.jsonl` (outbox mode only)
