# StepUp Email Delivery Pipeline — Manual Live-Test Checklist

The multi-account Graph/Gmail send pipeline is **live**: `MAIL_MAILER=emailhub`, so
every `Mail::` send now routes through `App\Mail\Transport\EmailHubTransport` →
`SenderAccountResolver` → `EmailDeliveryService` → the account's provider
(Microsoft Graph / Gmail API). Per-mailbox delegated OAuth: each mailbox holds its
own token, so `/me/sendMail` lands the sent copy in that mailbox's Sent Items.

Current config (dev):
- Default account: **info@stepupautism.ca** (microsoft, `is_default=1`, `connection_status=connected`).
- Category routes (`email_category_routes`) all point to the default account until an
  admin assigns per-category mailboxes (billing@, admissions@, …).

> NOTE: During the automated build, a verification step already performed one real
> send: **info@stepupautism.ca → vmwebsent@gmail.com** (subject "test", category
> `default`), which Graph accepted. Check that it appears in info@'s **Sent Items**
> as a first real-world confirmation.

---

## A. Send a controlled test email (Diagnostics screen)

Prereqs: logged in as a Super Admin; a mailbox connected (green in Diagnostics).

1. Open **Email Hub → Diagnostics** (route `GET /api/email-hub/diagnostics`).
   - Confirm info@stepupautism.ca shows: provider *microsoft*, **Default**,
     status **connected**, a valid token expiry, and NO token/secret values.
2. Click **Send Test** on info@stepupautism.ca → enter a controlled external
   address you can read (e.g. your own inbox) → Send
   (`POST /api/email-hub/diagnostics/test`).
3. Verify ALL of:
   - [ ] Recipient **receives** the email.
   - [ ] It appears in **info@stepupautism.ca → Sent Items** (Microsoft 365 / OWA).
   - [ ] It does **not** appear in any other mailbox's Sent Items.
   - [ ] It appears in **Email Hub → Sent** attributed to info@stepupautism.ca.
   - [ ] The `emails` row has `account_id` = info@'s id, `from_email` =
         info@stepupautism.ca, `category=default`, `send_status=sent`.

CLI equivalent (no UI):
```bash
php artisan tinker
>>> app(\App\Services\Email\EmailDeliveryService::class)->send(
...   \App\Models\EmailAccount::where('email','info@stepupautism.ca')->first(),
...   ['subject'=>'Pipeline test','body'=>'<p>Hello from the new pipeline</p>',
...    'to'=>['YOU@example.com'],'category'=>'default','correlation_id'=>'manual-'.uniqid()]
... );
```
Expect the returned `Email` to have `send_status === 'sent'`.

## B. Password reset path (§11)

1. Trigger a parent/staff password reset (admin "send reset link", or parent
   self-service). This sends `PortalAccessEmail` (category `password_reset`).
2. Verify:
   - [ ] The reset email is delivered (no longer only written to `laravel.log`).
   - [ ] Sent copy is in info@stepupautism.ca Sent Items (until a `password_reset`
         category mailbox is configured).
   - [ ] The reset **token/URL still works** (delivery mechanism changed, tokens did not).
   - [ ] No reset token appears in `storage/logs/laravel.log`.

## C. Per-category routing (§9) — once extra mailboxes exist

1. Connect billing@stepupautism.ca (Email Hub → Settings → add account → OAuth
   consent **as billing@**). Confirm Diagnostics shows it connected.
2. In `email_category_routes`, set `billing → billing@`'s account id
   (admin UI or `EmailCategoryRoute::where('category','billing')->update([...])`).
3. Trigger a `BillingNotification` (invoice/payment). Verify:
   - [ ] Sent copy lands in **billing@ Sent Items only** — not info@.
   - [ ] `emails.account_id` = billing@'s id, `category=billing`.

## D. Ownership isolation (§8)

- [ ] A `default`/`password_reset` send appears ONLY in info@ Sent Items.
- [ ] A `billing` send appears ONLY in billing@ Sent Items.
- [ ] No single provider-side message is duplicated across mailboxes.
- [ ] Email Hub → Sent (combined view) can show both, but each row keeps its own
      `account_id`.

## E. Queue + status (§12)

1. Ensure a worker runs: `php artisan queue:work` (or Horizon/supervisor).
   `QUEUE_CONNECTION` is currently `sync` in tests; check prod `.env`.
2. Force a transient failure (e.g. temporarily bad token) and confirm:
   - [ ] Row is `send_status=failed` with a sanitized `error_message` (no tokens).
   - [ ] `SendEmailJob` retries with backoff and flips to `sent` on success.
   - [ ] A permanent failure ends as `failed` (not stuck "sending").
   - [ ] A queued Mailable is not delivered twice (SendEmailJob is the single
         retry path; the transport no longer double-throws).

## F. Authorization (§15)

- [ ] A non-super-admin without an `email_account_user` row for an account is
      rejected when explicitly selecting it (UnauthorizedAccountException).
- [ ] Super Admin can use any account.
- [ ] Requested-but-disconnected account fails with
      "The selected email account is not connected or is unavailable." — it does
      NOT silently send from a different mailbox.

---

## Automated tests

```bash
# One-time: apply the 4 new migrations to the test DB
php artisan migrate --env=testing --force
php artisan test tests/Feature/Email      # 18 passing (mocked HTTP, no real send)
```

## Rollback

Set `MAIL_MAILER=log` (or `smtp`) in `.env` and run `php artisan optimize:clear`.
All new tables/columns are additive and harmless if the transport is disabled.

## Follow-ups (not yet done)

- Frontend: account-aware **Email Hub → Sent** view + **Diagnostics** screen
  (blueprint in the workflow output; backend endpoints already exist).
- Category-mailbox **admin UI** to edit `email_category_routes` without tinker.
- Security cleanup: `.env` still contains the legacy Office365 `MAIL_PASSWORD`
  (now unused). Remove it — the pipeline uses OAuth only, never basic auth.
