# Data Subject Correspondence — Design Doc

  **Status:** Draft
  **Ticket:** [RMP-142](https://ethyca.atlassian.net/browse/RMP-142)

  ---

  ## Overview

  Privacy teams using Fides cannot communicate directly with data subjects from within the platform. This design extends the existing privacy request workflow to support outbound email correspondence and
   inbound reply handling — keeping the full conversation in the activity timeline alongside internal notes and system events, with no external tools required.

  ---

  ## Background & Motivation

  When a privacy analyst needs to request additional information from a data subject, or provide a status update on a request, they must today leave Fides, compose an email in an external tool, and
  manually copy relevant context back into Fides after the fact. This produces fragmented audit trails, slows request resolution, and increases the risk of lost or misdirected communications.

  Keeping correspondence inside Fides improves audit completeness, reduces context-switching, and positions the product as a more complete privacy operations platform. This is a top request from
  customers evaluating Fides against competitors with built-in correspondence.

  ---

  ## Goals

  - Privacy analysts can compose and send an email to a data subject from within a privacy request, without leaving Fides
  - Data subject replies are automatically ingested and appear in the activity timeline within 60 seconds of receipt
  - All correspondence is auditable and exportable by privacy teams for audit and legal purposes (not included in the data subject's access package)
  - File attachments are supported on both outbound messages and inbound replies
  - Admins can define reusable message templates with placeholder variable support
  - Analysts receive in-app notifications when a data subject replies
  - Outbound email achieves >95% deliverability with proper authentication (SPF/DKIM/DMARC)
  - The feature works for on-prem deployments, not just cloud-hosted instances

  ---

  ## Non-Goals

  - **CC/BCC support** — excluded per PRD; follow-up if customer demand warrants it
  - **Cross-request correspondence view** — a unified inbox showing all communications with a data subject across requests; future consideration
  - **Privacy Center integration** — data subjects do not see correspondence in self-service; they have their side of the conversation in their email inbox
  - **Data retention policies for correspondence** — handled separately
  - **SMS or non-email channels** — email only for initial release
  - **Multi-language templates** — English only for initial release
  - **Real-time notification delivery (WebSocket/SSE)** — start with polling; upgrade path exists if latency requirements change
  - ~~**Inbound via provider webhooks (e.g., SendGrid Inbound Parse)**~~ — **Resolved: now in scope for v1.** IMAP polling remains primary for on-prem; SendGrid Inbound Parse is the managed inbound path for cloud-hosted deployments. See Alternatives Considered
  - **IMAP OAuth2/XOAUTH2** — v1 supports username/password only; Google Workspace and Microsoft 365 have deprecated basic auth for IMAP, so customers who want to use one of those providers as their
  reply mailbox will need to stand up a dedicated mailbox. On-prem mail servers (Dovecot, Postfix, Exchange on-prem) are unaffected. OAuth2 support is a v2 consideration

  ---

  ## Design

  ### Approach

  The design layers onto three existing systems rather than building parallel infrastructure:

  1. **Comment system** — outbound messages and inbound replies are modeled as new comment types on the privacy request. This gives correspondence the existing reference system, attachment support,
  cascade deletes, and activity timeline integration without new entity infrastructure. **Important: the existing `Comment` model does not have a `parent_id` or `thread_id` column. Adding threading
  support (so inbound replies nest under their originating outbound message) requires a new nullable `parent_id` FK on the `comment` table. This is a prerequisite schema change and should be its own
  story — see Threading Prerequisite below.**

  2. **Messaging infrastructure** — outbound email is dispatched via the existing Celery email task (with auto-retry) through the configured email provider. The existing dispatchers are refactored into provider classes behind an ABC (adopting patterns from Eli's `LJ-352-improvements` branch — two-level ABC, per-provider files, config validation in `__init__`). `EmailForActionType` is extended with `reply_to`, `message_id`, `in_reply_to`, `references`, and `body_text` fields; each provider class forwards these as provider-specific RFC 5322 email headers.

  3. **Messaging templates** — the existing admin-configurable template system is extended with a new "Correspondence" template type, rather than building a separate template model, CRUD API, and admin
  UI page.

  Net new infrastructure is limited to:
  - A `correspondence_metadata` table for email threading and delivery tracking fields that don't belong on the comment model
  - An IMAP mailbox polling task for inbound reply ingestion (following the existing Jira polling task pattern — see `fidesplus/jira/jira_polling_registration.py`)
  - A delivery webhook endpoint for delivery status updates (cloud/SendGrid only; optional for on-prem)
  - A `notification` table and in-app notification bell (deferrable — see below)

  ### Key Components

  **Outbound correspondence**
  An analyst composes a message in the privacy request activity tab. On send, the message is persisted as a comment and the email is dispatched as a post-commit side effect via the existing Celery email
  task. A unique, cryptographically random reply-to address is generated per outbound message and stored alongside the comment for inbound reply routing. Outbound emails use the organisation's configured
   sender address (a shared "from" address) — not a per-analyst address.

  **Inbound reply handling**
  A dedicated reply mailbox receives data subject replies. A scheduled polling task checks the mailbox on a configurable interval (default: 30 seconds) and processes new messages. Each message's reply-to
   address is matched to the originating privacy request and comment. Valid replies are persisted as comments threaded under the original outbound message. The inbound path is designed behind a common `InboundMessageHandler` interface — IMAP polling and webhook-based ingestion (e.g., SendGrid Inbound Parse) are separate adapter implementations, so the transport mechanism is a deployment configuration choice, not an architectural commitment.

  **Delivery tracking**
  For cloud-hosted deployments using SendGrid, delivery status (pending → sent → delivered / bounced) is updated via a webhook from the email provider. For on-prem deployments without a public endpoint,
  status remains "sent" with no further tracking. Delivery status changes — including bounces — are surfaced in the activity timeline by rendering the delivery status from the joined
  `correspondence_metadata` row inline on the correspondence comment card. A `bounced` or `failed` status renders as a visible error state on the comment; no separate timeline entry is needed.

  **Templates**
  Admins create reusable correspondence templates with placeholder variables (request ID, subject name, request type, etc.) through the existing notifications settings area. Analysts select a template
  when composing or write free-text.

  **Correspondence UI**
  Outbound correspondence is composed via a new tab in the existing privacy request activity tab, separate from the internal notes composer. The composer displays the recipient's email address so the
  analyst knows who they are about to contact. Before sending, a confirmation step is presented — this is deliberate friction to prevent an analyst accidentally dispatching an email to a data subject
  instead of saving an internal note. In the activity timeline, outbound messages and inbound replies are visually distinct from internal notes and system events (different colour, label, and author
  attribution), so the correspondence thread is immediately scannable in a mixed timeline.

  **Attachments**
  Both outbound messages and inbound replies support file attachments. Outbound attachments are composed and uploaded in the correspondence UI. Inbound attachments are extracted from the MIME payload
  during reply processing. File type and size validation applies to both paths (see Security Model).

  **Correspondence export**
  All correspondence for a privacy request — outbound messages, inbound replies, delivery status, timestamps, and attachments — can be exported by the privacy team for audit and legal purposes. This is
  an admin-facing export; it is not included in the data subject's access package. The data subject already has their side of the conversation in their own email inbox, and internal notes must never be
  exposed to data subjects.

  The existing diagnostics export pipeline (`diagnostics/gather.py`) is intentionally non-PII and cannot serve this requirement — it strips `comment_text` and all email metadata by design. The
  correspondence export is a new, separate endpoint that reuses existing ZIP infrastructure:

  - **Route:** `GET /plus/privacy-request/{id}/correspondence/export`
  - **Format:** ZIP archive containing a structured JSON manifest (all messages with timestamps, delivery status, sender/recipient) and any attachment files. Follows the same pattern as `fides/src/fides/service/privacy_request/diagnostics/export.py` (gather → serialize to ZIP → upload to storage provider → return presigned URL). Attachment streaming uses the existing `SmartOpenStreamingStorage` / `stream_zip` pipeline, which already handles cloud-to-cloud attachment streaming with no local disk.
  - **Access control:** requires `CORRESPONDENCE_READ` scope; same role(s) that can view correspondence can export it
  - **Audit logging:** every export emits a `correspondence_exported` audit event with `resource_type`, `user_id`, and `resource_identifier` (privacy request ID). This event type is added to the existing audit log infrastructure — no new audit model needed.
  - **Scope:** correspondence only — no internal notes, no system events, no DSR results data

  **In-app notifications**
  When a reply arrives, analysts are notified via a notification bell in the admin nav. Clicking a notification navigates directly to the relevant privacy request. This is designed as a general-purpose
  notification framework for reuse across other features, not correspondence-specific. If timeline is tight, a v1 shortcut exists: derive an unread badge count from the existing comments endpoint
  (filtering on the new reply comment type) with no new model or migration — the full framework becomes a fast-follow.

  Notification recipients: all users with `NOTIFICATION_READ` scope receive correspondence notifications. This is the simplest model and avoids requiring request-to-user assignment infrastructure that doesn't exist yet. Scoping to assigned analysts is a v2 refinement once request assignment is implemented.

  ### Threading Prerequisite

  The existing `Comment` model has no `parent_id` or `thread_id`. To thread inbound replies under their originating outbound message, a new nullable self-referential FK is required:

  ```sql
  ALTER TABLE comment ADD COLUMN parent_id VARCHAR(255) REFERENCES comment(id) ON DELETE SET NULL;
  CREATE INDEX ix_comment_parent_id ON comment(parent_id);
  ```

  This is an OSS migration and should be its own story, delivered before any correspondence story that depends on threading. Note: `CommentType.reply` remains reserved for future internal comment threading and is not reused here — correspondence uses two new enum values (`message_to_subject`, `reply_from_subject`).

  ### Rough API Surface

  POST   /plus/privacy-request/{id}/correspondence         # Send outbound message
  GET    /plus/privacy-request/{id}/comment                # Existing endpoint — extended with parent_id (Comment column) and CorrespondenceMetadata LEFT JOIN (delivery_status, sender_email, recipient_email) for correspondence types
  GET    /plus/privacy-request/{id}/correspondence/export  # Export correspondence as ZIP (admin, audit)
  POST   /webhooks/sendgrid/events                         # Delivery status webhook (SendGrid only)
  POST   /webhooks/sendgrid/inbound                        # SendGrid Inbound Parse (managed inbound for cloud)
  GET    /notifications                                     # Paginated notification list
  GET    /notifications/unread-count                        # Unread badge count
  PATCH  /notifications/{id}/read                          # Mark notification read

  No new GET endpoint for listing correspondence — the existing comment endpoint is extended with a `CorrespondenceMetadata` LEFT JOIN for correspondence types, returning delivery status and threading fields inline. This avoids a parallel endpoint and keeps the activity timeline's existing data source intact.

  Correspondence templates are managed through the existing `/notifications/templates/` routes — no new template endpoints needed.

  Note: the delivery webhook endpoint is SendGrid-specific (ECDSA verification via SendGrid SDK). It is named accordingly to avoid implying multi-provider support. Mailgun, AWS SES, and Mailchimp
  Transactional do not have a corresponding webhook path in v1; on-prem and non-SendGrid deployments show delivery status as "sent" only.

  ### Data Model

  Two new tables (`correspondence_metadata`, `notification`), two new `CommentType` enum values, and a new `MessagingActionType.CORRESPONDENCE` template type. No new entity infrastructure — correspondence reuses the Comment system.

  Key design constraints:
  - **Delivery status is monotonic:** `pending → sent → delivered` happy path; `bounced`/`failed` are terminal. Webhooks cannot overwrite terminal states.
  - **`correspondence_metadata`** is a satellite table (one-to-one with Comment) keeping email-specific fields off the comment model.
  - **`notification`** uses soft references (no FK). Cleanup: eager `delete_for_resource()` at deletion sites (primary), daily orphan cleanup job (safety net).
  - The existing `CommentType.reply` value remains reserved for future internal comment threading — not reused here.

  For full model definitions, column specs, and migration notes, see [RMP-142-implementation-plan.md](RMP-142-implementation-plan.md#data-model).

  ### Security Model

  **PII encryption at rest**
  The `comment_text` column on the `Comment` model is encrypted at rest using `StringEncryptedType`, consistent with the rest of the identity chain (`ProvidedIdentity.encrypted_value`, `PrivacyRequest.encrypted_identity`, `RequestTask.result`). This encrypts all comment text — including internal notes — because `StringEncryptedType` is a column-level type decorator and cannot selectively encrypt per row. This is acceptable: encrypting internal notes has no functional impact, improves overall security posture, and avoids a separate encrypted column that would break the Comment reuse story. A one-time data migration encrypts existing plaintext rows. This is a v1 requirement — retrofitting encryption after correspondence data is stored in plaintext is operationally costly and risks a compliance gap. **Migration requirements:** The migration must handle `comment_text IS NULL` rows (skip, do not encrypt NULL). Rollback after this migration goes live is not possible without a reverse decryption migration using the application encryption key — this is a one-way door. Before the migration runs, an audit of code that queries or filters on `comment_type` assuming only `note` exists is required as an acceptance criterion, since the migration coincides with the introduction of new comment types.

  **Reply-to address generation**
  One cryptographically random address per outbound message, not per privacy request. Scoping to the message enables per-message threading and limits the blast radius of a leaked address. Format:
  `reply+{token}@replies.{domain}`, where the token is 128 bits of cryptographic randomness (32 hex characters). Plus addressing must be supported by the reply mailbox; a dedicated subdomain format
  (`{token}@replies.{domain}`) is available as a config option for mail servers that strip the plus suffix.

  **Reply-to address lifetime**
  Addresses are active for the lifetime of the privacy request and deactivated when the request completes or is canceled. Privacy request deletion does not purge correspondence — correspondence is
  retained for audit purposes; only the reply-to address is deactivated. Deactivation is evaluated at runtime: inbound processing checks the privacy request's status at message processing time rather than maintaining a separate `is_active` flag. This avoids requiring every request completion/cancellation code path to update correspondence metadata — the privacy request status is the single source of truth.

  **Inbound sanitization**
  Inbound email bodies are stored as plaintext only in v1 — HTML is stripped after MIME decoding (stripping before would allow encoded payload bypasses via base64/quoted-printable). Email headers are
  sanitized before storage. Rich HTML rendering of inbound content is a v2 consideration.

  **Inbound spam filtering**
  First-line spam filtering is handled by the email provider before messages reach the reply mailbox. Fides does not implement its own spam classifier — provider-side filtering is a prerequisite for any
  reply mailbox configuration.

  **Attachment safety**
  File types are validated against the existing allowlist (no executables). Size validation requires a two-phase IMAP fetch: first `UID FETCH <uid> BODYSTRUCTURE` to check part sizes from metadata only, then `UID FETCH <uid> BODY[<part>]` to fetch only parts that passed the size check — fetching the full body first and checking size after defeats the purpose and risks worker memory exhaustion on oversized MIME parts. Existing directory
  traversal protection applies to stored filenames. Oversized inbound attachments are dropped with a visible warning in the timeline (the analyst sees the warning; the data subject is not notified —
  this is a known limitation and should be documented for legal review).

  **Outbound rate limiting**
  The `POST /plus/privacy-request/{id}/correspondence` endpoint enforces a per-privacy-request rate limit (default: 25 outbound messages per hour per privacy request). This prevents both accidental spam (e.g., a script or repeated clicks) and deliberate misuse. The limit is configurable and enforced server-side at the API handler level (reject before queuing the Celery task). Implementation uses a Redis counter with TTL (consistent with the Redis lock used for IMAP polling) — loss on Redis restart is acceptable for a rate limit.

  **Inbound rate limiting**
  Per-reply-to-address rate limiting is applied during IMAP message processing (not at the network edge — there is no network edge for IMAP polling). A global cap on messages processed per poll cycle
  prevents mailbox flooding; excess messages remain unprocessed until the next cycle.

  **Sender validation**
  The sender address of an inbound reply is validated against the data subject's email identity on file. Replies from unrecognized senders are not silently discarded — they are moved to a `Review/`
  folder in the reply mailbox and a timeline entry is created indicating "unmatched inbound reply — manual review required", with no message content surfaced. This prevents compliance gaps where a data
  subject replies from a different address (e.g. forwarded from work email) and the reply is silently lost. In v1, analysts review the `Review/` folder contents by accessing the reply mailbox directly (IMAP client or webmail). A dedicated admin UI for reviewing and manually importing flagged messages is a v2 consideration. The timeline entry serves as the trigger for the analyst to check the mailbox — it is not an actionable UI element in v1.

  **Delivery webhook authentication**
  SendGrid Event Webhook signatures are verified using ECDSA via the SendGrid SDK. This is a different mechanism from the HMAC-based Slack webhook verification already in the codebase — do not conflate
  the two.

  **IMAP transport**
  TLS/STARTTLS only. Certificate verification is enforced by default, with a documented opt-out for self-signed certificates in development/testing environments.

  **IMAP credentials storage**
  Reply mailbox credentials are stored encrypted alongside existing messaging provider credentials — not in the data connectors system, which would surface them incorrectly in the connectors UI.

  **Inbound idempotency**
  The `message_id` unique constraint in `correspondence_metadata` is the primary idempotency mechanism — inbound processing upserts on `message_id`, making re-processing safe. If the IMAP polling task crashes after fetching messages but before marking them as processed (SEEN/MOVE), the next cycle will re-fetch them but the upsert prevents duplicate comment creation. The IMAP polling task uses `BODY.PEEK[]` (not `BODY[]`) so the server does not auto-set the SEEN flag on fetch. The full pipeline runs first — comment creation, metadata upsert, notification — and only after a successful commit does the task set the SEEN flag and move the message to `Processed/`. This is an at-least-once delivery model: a crash between fetch and commit means the message is re-fetched on the next cycle, but the `message_id` upsert makes re-processing safe. The SEEN flag is a performance optimization to avoid redundant re-fetches, not a correctness mechanism.

  **No-email guard**
  The correspondence composer is hidden when the privacy request has no email identity on file. The `POST /plus/privacy-request/{id}/correspondence` handler must also validate that the request has a verified email identity before resolving the recipient address and dispatching — UI guards are UX, not security controls. The recipient address is resolved from encrypted identity storage at send time and is not cached in the UI.

  **Authorization**
  Correspondence CRUD reuses the existing `COMMENT_CREATE` and `COMMENT_READ` scopes. A new `CORRESPONDENCE_SEND` scope gates the email-dispatch action specifically (sending email is more consequential
  than creating an internal note). Notification read access is gated by a new `NOTIFICATION_READ` scope. Correspondence export is gated by `CORRESPONDENCE_READ` — this is intentional, as the export contains the same data visible in the list view (correspondence + attachments), just packaged as a ZIP. If audit requirements later demand export-specific restrictions, a `CORRESPONDENCE_EXPORT` scope can be added without breaking the existing model. Template management reuses the existing
  messaging template scope. `COMMENT_CREATE` without `CORRESPONDENCE_SEND` still permits internal notes — the scopes are additive.

  ### OSS / Plus Split

  **Fides OSS:** models, migrations, enums, messaging provider refactor, task skeletons with hook points, all frontend (gated behind `useFeatures().plus`).
  **Fidesplus:** all business logic — correspondence service, IMAP polling, reply ingestion, delivery webhooks, notifications.
  **Without Plus:** correspondence saves as a comment with no email dispatch; IMAP polling and notifications are no-ops. Same pattern as the Jira integration.

  For the full component table, hook pattern details, and repository split rationale, see [RMP-142-implementation-plan.md](RMP-142-implementation-plan.md#repository-split) and [RMP-142-architecture-conventions.md](RMP-142-architecture-conventions.md#hook-pattern-scope).

  ### Non-Functional Requirements

  | Requirement | Target |
  |---|---|
  | Reply latency | < 60 seconds from receipt to timeline (30-second default poll interval gives ≤ 30s + processing time) |
  | Outbound deliverability | > 95% with proper SPF/DKIM/DMARC via existing email provider infrastructure |
  | Poll concurrency | Single writer enforced via Redis lock; same pattern as Jira polling |
  | Poll interval | Configurable; default 30s |
  | Unread count query | Efficient via composite index on `(user_id, is_read)` |
  | Poll observability | Metrics emitted per cycle: messages fetched, processed, failed, processing latency. Required for SLA monitoring against the 60s reply latency target. |

  ---

  ## Error Handling

  | Failure Mode | Handling |
  |---|---|
  | Email send failure | Celery auto-retry (3×, exponential backoff). After exhaustion: mark delivery status `failed`, render as error state on comment card in timeline |
  | Bounce notification | Process via SendGrid delivery webhook, update `correspondence_metadata` status to `bounced` with reason, render as error state on comment card in timeline |
  | IMAP connection failure | Retry on next poll cycle; log via audit trail. Alert after configurable number of consecutive failures |
  | IMAP message parse failure | Move to `Failed/` folder for manual review; do not retry automatically |
  | Invalid reply-to token | Skip message, mark as read, log the attempt, increment rate limit counter |
  | Unrecognized sender (valid token, wrong sender) | Move to `Review/` folder; create timeline entry "unmatched inbound reply — manual review required"; do not surface message content |
  | Inbound rate limit exceeded | Reject processing for that reply-to address; log; existing sender continues to receive delivery confirmation from their email client |
  | Attachment too large (outbound) | Reject with error message in UI |
  | Attachment too large (inbound) | Drop attachment; create visible warning in timeline noting attachment was not captured. Known limitation — data subject is not notified. |

  ---

  ## Alternatives Considered

  ### 1. IMAP polling vs. inbound provider webhooks (e.g., SendGrid Inbound Parse)

  | | IMAP polling | Provider webhooks |
  |---|---|---|
  | On-prem support | Works behind firewalls, no public endpoint | Requires publicly accessible endpoint |
  | Provider dependency | Provider-agnostic | Provider-specific per vendor |
  | Latency | ~30s worst case at default interval | Near real-time |
  | Infrastructure reuse | Reuses existing APScheduler + Celery pattern | New webhook endpoint |
  | Mailbox setup | Customer configures IMAP credentials | Customer configures MX redirect to provider |
  | Cloud setup burden | Prohibitive — 6 steps to provision a third-party IMAP mailbox | Minimal — provider webhook config only |

  **Decision: IMAP polling for on-prem; managed inbound for cloud.** On-prem is a primary deployment target — requiring a public endpoint is a non-starter for many customers. However, requiring cloud customers to provision and maintain a separate IMAP mailbox on a third-party provider is prohibitive (six setup steps, ongoing vendor maintenance, basic auth dependency). For cloud-hosted deployments, Fides will offer a managed inbound path — either a Fides-operated reply mailbox or SendGrid Inbound Parse — so cloud customers need only configure their outbound email provider, not a separate inbound one. The managed inbound path is a v1 requirement for cloud; IMAP polling remains the on-prem path. This resolves PRD Open Question #1.

  The inbound processing architecture must be mechanism-agnostic from day one. A common `InboundMessageHandler` interface accepts parsed inbound messages (sender, body, attachments, reply-to token) regardless of how they were received. IMAP polling and webhook ingestion are separate adapter implementations behind this interface. This means adding a new inbound mechanism (or swapping one out) is a configuration change and a new adapter — not a rewrite of reply processing, threading, notification, or abuse protection logic.

  ### 2. New Correspondence entity vs. extending the Comment model

  A dedicated `Correspondence` model would be semantically cleaner but would require new reference tables, cascade delete logic, timeline integration, and attachment support — all of which the comment
  system already provides. Outbound messages and inbound replies are functionally comments with additional email metadata attached.

  **Decision: Extend the Comment model.** Add two new comment types and a satellite `correspondence_metadata` table for email-specific fields. No new entity, no new reference system.

  ### 3. New CorrespondenceTemplate model vs. extending MessagingTemplate

  A separate template model would add a new table, migration, CRUD API, and admin UI page. The existing `MessagingTemplate` model already covers the full requirement: subject/body content storage, admin
  UI, CRUD routes, property linking, and variable substitution.

  **Decision: Extend MessagingTemplate.** Add a new `CORRESPONDENCE` action type. Correspondence templates get the existing admin UI and API for free — no new table.

  ### 4. Per-request reply-to address vs. per-message

  A single reply-to address per privacy request is simpler to manage but prevents threading replies to the specific message they respond to, and means a leaked address exposes the entire request's reply
  channel.

  **Decision: Per-message reply-to address.** One cryptographically random address per outbound message. Enables proper RFC 5322 email threading and limits the scope of any address misuse.

  ### 5. General-purpose Notification model vs. v1 shortcut

  Building a general-purpose notification framework is the right long-term decision but carries model, migration, and API cost up front. A shortcut exists: derive unread reply count by filtering the
  existing comments endpoint on the new reply comment type — no new table, no new migration, badge in the nav bar.

  **Decision: Full model, but explicitly deferrable.** The PRD calls for a reusable framework designed for future use cases beyond correspondence. If timeline is tight, the shortcut ships as v1 and the
  full model follows. The two paths are compatible — the shortcut does not create tech debt that blocks the full version.

  ### 6. Bounce surfacing: new timeline entry vs. inline comment status

  A bounce event could create a new timeline entry (similar to ExecutionLog entries for DSR tasks), or it could update the delivery status on the existing comment card via the `correspondence_metadata`
  join.

  **Decision: Inline on the comment card.** Creating a new timeline entry for each delivery status transition would clutter the timeline and requires the timeline to understand a new entry type. Rendering
  `correspondence_metadata.delivery_status` inline on the comment card is simpler and keeps delivery status co-located with the message it describes.

  ---

  ## Open Questions

  1. **IMAP OAuth2 — confirm deferral to v2.** The proposed position (in Non-Goals) is that v1 supports IMAP username/password only, with OAuth2 as a v2 consideration. Gmail and M365 have deprecated
  basic auth, so customers using those providers would need to stand up a dedicated reply mailbox. Is this limitation acceptable for launch?

  2. **Reply-to domain configuration.** The PRD calls out "Fides-managed and customer-owned domains for self-hosted instances." For self-hosted deployments, the customer configures their own domain and
  IMAP credentials — but what is the expected model for cloud-hosted customers? Do cloud customers also bring their own reply domain, or is a Fides-managed shared domain in scope? The answer affects
  infrastructure and data residency considerations. Setup complexity (MX records, TLS, dedicated subdomain) should be acknowledged in the configuration guide regardless.

  3. **Reply-to address format?** Default is `reply+{token}@replies.{domain}` (plus addressing). Some mail servers strip the plus suffix. Should we default to a dedicated subdomain format
  (`{token}@replies.{domain}`) instead, or support both?

  4. **Notification recipients?** ~~TBD~~ **Resolved:** all users with `NOTIFICATION_READ` scope (option a). Simplest model; avoids requiring request assignment infrastructure. Scoping to assigned analysts (option b) is a v2 refinement.

  5. **Delivery tracking for on-prem?** Delivery status beyond "sent" requires a public webhook endpoint. On-prem customers without SendGrid would see status permanently as "sent". Is this acceptable?

  6. **Template variables?** Reuse existing `__VARIABLE__` syntax. Proposed initial set: `__REQUEST_ID__`, `__SUBJECT_NAME__`, `__REQUEST_TYPE__`, `__REQUEST_STATUS__`, `__DUE_DATE__`,
  `__ORGANIZATION_NAME__`. Anything missing?

  ---

  ## Architecture Diagram

  ```mermaid
  sequenceDiagram
      participant User as Privacy Analyst
      participant UI as Admin UI
      participant API as Fidesplus API
      participant Svc as CorrespondenceService
      participant Celery as Celery Worker
      participant Email as Email Provider (SendGrid)
      participant DS as Data Subject

      Note over User,DS: Outbound Flow
      User->>UI: Compose outbound message
      UI->>API: POST /plus/privacy-request/{id}/correspondence
      API->>Svc: create_message_to_subject()
      Svc->>Svc: Generate reply-to address
      Svc->>Svc: Create Comment + CorrespondenceMetadata
      Svc->>Svc: Build EmailForActionType (subject, body, reply_to, message_id)
      Svc->>Celery: dispatch_message_task.delay() (reuse existing task)
      Celery->>Email: Send email (with threading headers via updated dispatchers)
      Email->>DS: Deliver email
      Email-->>API: Delivery webhook (delivered/bounced) → update metadata, render inline on comment card

      Note over User,DS: Inbound Flow (IMAP Polling)
      DS->>Email: Reply to correspondence
      Note over Celery: APScheduler triggers poll_reply_mailbox (interval)
      Celery->>Email: IMAP FETCH new messages (BODY.PEEK[], no auto-SEEN)
      Email-->>Celery: New reply emails
      Celery->>Svc: process_inbound_reply()
      Svc->>Svc: Match reply-to token → privacy_request
      Svc->>Svc: Upsert on message_id (idempotency)
      Svc->>Svc: Create reply_from_subject Comment (parent_id → outbound comment)
      Svc->>Svc: Create CorrespondenceMetadata
      Svc->>Svc: Create Notification for all users with NOTIFICATION_READ scope
      Celery->>Email: Move to Processed/ folder
      UI-->>User: Notification badge updates
  ```

  ---

  ## Companion Document

  - RMP-142-implementation-plan.md — Story breakdown (23 stories in 7 demoable slices), component reuse reference, existing infrastructure analysis, data model detail, integration edge cases, frontend
  component map, and testing strategy

  ## Open Question Tracking

  | # | Question | Status | Blocking Story |
  |---|----------|--------|----------------|
  | 1 | IMAP OAuth2 — confirm deferral to v2 | Open | 2-BE-a |
  | 2 | Reply-to domain configuration (cloud vs self-hosted) | Open | 1-BE |
  | 3 | Reply-to address format (plus addressing vs dedicated subdomain) | Open | 1-BE |
  | 4 | Notification recipients | **Resolved** — all users with `NOTIFICATION_READ` scope | — |
  | 5 | Delivery tracking for on-prem | Open | 3 |
  | 6 | Template variables — final set | Open | 4 |
