# Implementation Plan: PIP Email via AWS SES

**Feature**: Transactional email delivery for PIP decision notifications (and auth emails) via AWS Simple Email Service  
**Milestone**: PIPs  
**Issue**: See GitHub `feat(email): wire AWS SES as Django EMAIL_BACKEND for PIP decision notifications`  
**SAO reference**: `docs/architecture/SAO.md` → "Email Architecture"

---

## Status

| Slice | Description | Status |
|-------|-------------|--------|
| S1 | Add `django-ses` dependency + wire `EMAIL_BACKEND` in prod settings | ✅ Done |
| S2 | Dev settings: console default + `USE_SES_IN_DEV=1` override | ✅ Done |
| S3 | SAO documentation (Email Architecture section) | ✅ Done |
| S4 | Integration tests — locmem layer (7 tests, always runs in CI) | ✅ Done |
| S5 | Integration tests — live SES smoke layer (1 test, opt-in) | ✅ Done (skipped in CI) |
| S6 | AWS SES provisioning (infra) | ❌ TODO |
| S7 | EB environment variables set in AWS Console / SSM | ❌ TODO |
| S8 | End-to-end production verification | ❌ TODO |

---

## Problem Statement

`pip_notification_service.py` calls `django.core.mail.send_mail` after an admin finalises a PIP. In production, `EMAIL_BACKEND` was unset — Django would have fallen back to SMTP with no host configured, silently dropping every email. `boto3` was listed in `requirements.txt` for SES but never wired to Django's email dispatch.

---

## Design Decisions

### Why `django-ses` over raw boto3?

| Criterion | `django-ses` | Raw boto3 in `EmailService` |
|-----------|-------------|---------------------------|
| Works with Django `send_mail` / `send_mass_mail` | ✅ | ❌ (bypass Django mail) |
| Works with Django `mail_admins` / `mail_managers` | ✅ | ❌ |
| Works with `django.contrib.auth` password reset | ✅ | ❌ |
| SES bounce/complaint webhook handler | ✅ built-in | ❌ manual |
| Test backend swap (locmem) | ✅ automatic | ❌ requires monkeypatching |
| Maintenance burden | Low (maintained Django package) | High |

`accounts/services/email_service.py` (auth welcome/password-reset) still uses raw boto3 — it pre-dates this feature. It will be migrated in a follow-up ticket to use `send_mail` so the same backend logic covers it.

### Why NOT SMTP relay?

SES SMTP interface exists but adds one network hop and requires storing SMTP credentials. Native SES API via boto3 is simpler, cheaper, and more observable.

---

## Files Changed / Added

```
requirements.txt                           # django-ses added
mimir/settings/base.py                     # django_ses in INSTALLED_APPS
mimir/settings/prod.py                     # EMAIL_BACKEND + SES settings
mimir/settings/dev.py                      # console backend + USE_SES_IN_DEV override
docs/architecture/SAO.md                   # Email Architecture section
tests/integration/test_pip_email.py        # NEW — 7 locmem tests + 1 live SES test
docs/plans/PIP-EMAIL-SES_IMPLEMENTATION_PLAN.md  # this file
```

---

## Remaining Work (S6–S8)

### S6: AWS SES Provisioning

Steps (one-time, done in AWS Console or via CloudFormation/Terraform):

1. **Verify sending domain** `featurefactory.io`:
   - Add SES DNS TXT record for domain verification.
   - Add DKIM CNAME records (3 records generated by SES).
   - Confirm in SES Console: `featurefactory.io` → Verified.

2. **Request SES production access** (exit sandbox):
   - AWS Support → Service Limit Increase → SES Sending Limits.
   - Justification: transactional, user-initiated, low volume.
   - Without this, emails only reach verified addresses.

3. **Create IAM policy `MimirSESSendEmail`**:
   ```json
   {
     "Version": "2012-10-17",
     "Statement": [{
       "Effect": "Allow",
       "Action": ["ses:SendEmail", "ses:SendRawEmail"],
       "Resource": "arn:aws:ses:us-east-1:<ACCOUNT>:identity/featurefactory.io"
     }]
   }
   ```

4. **Attach policy to EB instance role** `aws-elasticbeanstalk-ec2-role` (preferred over long-lived access keys).

5. **(Optional) Create SES Configuration Set** `mimir-transactional`:
   - CloudWatch event destination for delivery, bounce, complaint metrics.
   - Set `AWS_SES_CONFIGURATION_SET=mimir-transactional` in EB env vars.

### S7: EB Environment Variables

Set in AWS Console → Elastic Beanstalk → Environment → Configuration → Software:

| Variable | Value |
|----------|-------|
| `AWS_SES_REGION_NAME` | `us-east-1` |
| `DEFAULT_FROM_EMAIL` | `noreply@featurefactory.io` |
| `FRONTEND_URL` | `https://mimir.featurefactory.io` |
| `AWS_SES_CONFIGURATION_SET` | `mimir-transactional` (optional) |
| `GITHUB_TOKEN` | PAT with Issues: write (optional but recommended — bug-report widget / API) |
| `GITHUB_BUG_REPO` | `phainestai/mimir` (optional) |

Do **not** set `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` when using instance role.

### S8: Production Verification

1. Deploy new build.
2. Create a test PIP as admin user.
3. Admin finalise it → check Django logs for `Sending PIP decision email`.
4. Verify email arrives at submitter inbox.
5. Check SES Console → Send Statistics for bounce/complaint rates.

---

## Integration Tests (Reference)

### Always-runs (CI-safe) — `tests/integration/test_pip_email.py`

| Test ID | Scenario |
|---------|---------|
| `test_email_01` | FOB-PIP-EMAIL-01: finalize all-accept → exactly 1 email queued |
| `test_email_02` | FOB-PIP-EMAIL-02: subject contains PIP title + "Accepted ✓" |
| `test_email_03` | FOB-PIP-EMAIL-03: To: == submitter.email |
| `test_email_04` | FOB-PIP-EMAIL-04: full reject → subject says "Rejected ✗" |
| `test_email_05` | FOB-PIP-EMAIL-05: body renders change decisions + Galdr reasoning |
| `test_email_06` | FOB-PIP-EMAIL-06: submitter without email → no crash, no email |
| `test_email_07` | FOB-PIP-EMAIL-07: From: == settings.DEFAULT_FROM_EMAIL |

### Live SES (opt-in, requires env vars)

| Test ID | How to run |
|---------|-----------|
| `test_live_ses_pip_decision_email_sends` | `USE_SES_IN_DEV=1 AWS_SES_REGION_NAME=us-east-1 TEST_EMAIL_RECIPIENT=you@verified.io pytest tests/integration/test_pip_email.py::test_live_ses_pip_decision_email_sends -v` |

---

## Acceptance Criteria

- [ ] `pytest tests/integration/test_pip_email.py` — all 7 locmem tests pass, live test skipped.
- [ ] `prod.py` sets `EMAIL_BACKEND = "django_ses.SESBackend"`.
- [ ] `django-ses` is in `requirements.txt`.
- [ ] SAO.md documents the email architecture, env vars, and test strategy.
- [ ] After S6–S7: deploy to staging EB → finalize a real PIP → email received.
