Phase 2: Core Send API — POST /send, templates, email log #3

Closed
opened 2026-03-22 16:59:19 +00:00 by forgejo_admin · 0 comments

Type

Feature

Lineage

plan-pal-e-mail → Phase 2

Repo

forgejo_admin/pal-e-mail

User Story

As a platform service
I want a centralized email API with Gmail OAuth, CDN templates, and brand wrapping
So that all projects (westside, mcd-tracker, etc.) can send branded emails through one service

Context

Phase 1 deployed pal-e-mail with /healthz, Postgres (email_log table via alembic), writable Gmail OAuth PVC, Woodpecker CI, and ArgoCD. Phase 2 adds the actual email sending: POST /send with multi-tenant Gmail OAuth, template fetching from MinIO CDN, brand wrapper (extracted from basketball-api), and an email log query API. The gmail-sdk-ldraney package handles OAuth token management and Gmail API calls. Templates are fetched via httpx from MinIO CDN with simple {{key}} placeholder replacement.

File Targets

Files to create:

  • src/pal_e_mail/schemas.py — Pydantic request/response models (SendRequest, SendResponse, LogEntry, LogResponse)
  • src/pal_e_mail/services/__init__.py — empty package init
  • src/pal_e_mail/services/sender.py — GmailClient cache, send_email() orchestration + EmailLog writing
  • src/pal_e_mail/services/template.py — fetch_template(), brand_wrapper(), html_to_plain_text()
  • src/pal_e_mail/routes/__init__.py — empty package init
  • src/pal_e_mail/routes/send.py — POST /send endpoint
  • src/pal_e_mail/routes/log.py — GET /log endpoint
  • tests/test_send.py — send endpoint tests (mocked gmail-sdk + httpx)
  • tests/test_template.py — template service tests
  • tests/test_log.py — log query tests

Files to modify:

  • src/pal_e_mail/main.py — include send and log routers

Files NOT to touch:

  • src/pal_e_mail/models.py — EmailLog schema from Phase 1 already has all needed fields
  • src/pal_e_mail/database.py — no changes needed
  • src/pal_e_mail/config.py — already has gmail_secrets_dir and minio_cdn_base_url
  • alembic/ — no migration needed

Acceptance Criteria

  • POST /send works with template mode (CDN fetch + {{var}} replacement)
  • POST /send works with custom HTML mode (brand wrapper applied)
  • POST /send works with plain text only mode (no template, no html)
  • Template + HTML mutual exclusion returns 422
  • Invalid sender (no token file) returns 400
  • Gmail API failure returns 502 with error detail
  • All sends logged to email_log table regardless of success/failure
  • GET /log returns paginated results with sender/project/status/template filters
  • GET /log orders by sent_at DESC

Test Expectations

  • Unit test: POST /send with mocked GmailClient — template, HTML, plain text modes
  • Unit test: mutual exclusion validation (template + html → 422)
  • Unit test: invalid sender → 400, Gmail API error → 502
  • Unit test: fetch_template() with mocked httpx, {{key}} replacement, 404 handling
  • Unit test: brand_wrapper() output structure (header, body, footer, escaped brand name)
  • Unit test: html_to_plain_text() tag stripping
  • Unit test: GET /log with seeded data, filtering, pagination, ordering
  • Run command: cd ~/pal-e-mail && .venv/bin/pytest tests/ -v

Constraints

  • Sync, not async — gmail-sdk uses sync httpx.Client
  • Client caching via module-level dict (GmailClient handles token refresh internally)
  • Brand wrapper colors hardcoded (extracted from basketball-api) — per-project configs deferred
  • Template variables as dict[str, str] — all values coerced to str
  • No from_addr override — gmail-sdk defaults to authenticated user
  • Use SQLite in-memory for tests (override get_db dependency)
  • Import GmailAPIError from gmail_sdk.client, not gmail_sdk

Checklist

  • PR opened
  • Tests pass
  • No unrelated changes
  • project-pal-e-mail — parent project
  • plan-pal-e-mail — parent plan
  • basketball-api services/email.py lines 332-380 — brand wrapper source
  • gmail-sdk client.py — GmailClient and GmailAPIError API
### Type Feature ### Lineage `plan-pal-e-mail` → Phase 2 ### Repo `forgejo_admin/pal-e-mail` ### User Story As a platform service I want a centralized email API with Gmail OAuth, CDN templates, and brand wrapping So that all projects (westside, mcd-tracker, etc.) can send branded emails through one service ### Context Phase 1 deployed pal-e-mail with /healthz, Postgres (email_log table via alembic), writable Gmail OAuth PVC, Woodpecker CI, and ArgoCD. Phase 2 adds the actual email sending: POST /send with multi-tenant Gmail OAuth, template fetching from MinIO CDN, brand wrapper (extracted from basketball-api), and an email log query API. The gmail-sdk-ldraney package handles OAuth token management and Gmail API calls. Templates are fetched via httpx from MinIO CDN with simple {{key}} placeholder replacement. ### File Targets Files to create: - `src/pal_e_mail/schemas.py` — Pydantic request/response models (SendRequest, SendResponse, LogEntry, LogResponse) - `src/pal_e_mail/services/__init__.py` — empty package init - `src/pal_e_mail/services/sender.py` — GmailClient cache, send_email() orchestration + EmailLog writing - `src/pal_e_mail/services/template.py` — fetch_template(), brand_wrapper(), html_to_plain_text() - `src/pal_e_mail/routes/__init__.py` — empty package init - `src/pal_e_mail/routes/send.py` — POST /send endpoint - `src/pal_e_mail/routes/log.py` — GET /log endpoint - `tests/test_send.py` — send endpoint tests (mocked gmail-sdk + httpx) - `tests/test_template.py` — template service tests - `tests/test_log.py` — log query tests Files to modify: - `src/pal_e_mail/main.py` — include send and log routers Files NOT to touch: - `src/pal_e_mail/models.py` — EmailLog schema from Phase 1 already has all needed fields - `src/pal_e_mail/database.py` — no changes needed - `src/pal_e_mail/config.py` — already has gmail_secrets_dir and minio_cdn_base_url - `alembic/` — no migration needed ### Acceptance Criteria - [ ] POST /send works with template mode (CDN fetch + {{var}} replacement) - [ ] POST /send works with custom HTML mode (brand wrapper applied) - [ ] POST /send works with plain text only mode (no template, no html) - [ ] Template + HTML mutual exclusion returns 422 - [ ] Invalid sender (no token file) returns 400 - [ ] Gmail API failure returns 502 with error detail - [ ] All sends logged to email_log table regardless of success/failure - [ ] GET /log returns paginated results with sender/project/status/template filters - [ ] GET /log orders by sent_at DESC ### Test Expectations - [ ] Unit test: POST /send with mocked GmailClient — template, HTML, plain text modes - [ ] Unit test: mutual exclusion validation (template + html → 422) - [ ] Unit test: invalid sender → 400, Gmail API error → 502 - [ ] Unit test: fetch_template() with mocked httpx, {{key}} replacement, 404 handling - [ ] Unit test: brand_wrapper() output structure (header, body, footer, escaped brand name) - [ ] Unit test: html_to_plain_text() tag stripping - [ ] Unit test: GET /log with seeded data, filtering, pagination, ordering - Run command: `cd ~/pal-e-mail && .venv/bin/pytest tests/ -v` ### Constraints - Sync, not async — gmail-sdk uses sync httpx.Client - Client caching via module-level dict (GmailClient handles token refresh internally) - Brand wrapper colors hardcoded (extracted from basketball-api) — per-project configs deferred - Template variables as dict[str, str] — all values coerced to str - No from_addr override — gmail-sdk defaults to authenticated user - Use SQLite in-memory for tests (override get_db dependency) - Import GmailAPIError from gmail_sdk.client, not gmail_sdk ### Checklist - [ ] PR opened - [ ] Tests pass - [ ] No unrelated changes ### Related - `project-pal-e-mail` — parent project - `plan-pal-e-mail` — parent plan - basketball-api `services/email.py` lines 332-380 — brand wrapper source - gmail-sdk `client.py` — GmailClient and GmailAPIError API
Commenting is not possible because the repository is archived.
No labels
No milestone
No project
No assignees
1 participant
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
forgejo_admin/pal-e-mail#3
No description provided.