---
status: "proposed"
date: 2026-04-07
decision-makers: Jade, Eliana, Catherine
---

# Data Subject Correspondence — Architecture Decisions

## Context and Problem Statement

Privacy teams using Fides cannot communicate directly with data subjects from within the platform. [RMP-142](https://ethyca.atlassian.net/browse/RMP-142) adds outbound email correspondence and inbound reply handling to the privacy request workflow. 

Full design spec: [`plans/RMP-142/RMP-142-data-subject-correspondence.md`](../../plans/RMP-142/RMP-142-data-subject-correspondence.md)

## Considered Options

1. Inbound reply transport: IMAP polling only vs. provider webhooks only vs. dual-path (IMAP + managed inbound)
2. Data model: new Correspondence entity vs. extend existing Comment model
3. Inbound processing: transport-coupled vs. transport-agnostic (`InboundMessageHandler` interface)
4. Reply-to address scope: per-request vs. per-message
5. PII encryption: defer to v2 vs. encrypt `comment_text` in v1
6. Bounce surfacing: new timeline entry vs. inline on comment card
7. Messaging provider refactor: patch existing dispatchers vs. refactor into ABC (Eli's `LJ-352-improvements` patterns)

## Decision Outcome

All seven decisions are accepted as a package — they are interdependent.

1. **Inbound reply transport:** Dual-path (IMAP polling + SendGrid Inbound Parse), because on-prem can't expose public endpoints and cloud can't provision IMAP mailboxes
2. **Data model:** Extend Comment model + `correspondence_metadata` satellite table, because it reuses existing attachments, cascade deletes, and timeline integration
3. **Inbound processing:** `InboundMessageHandler` protocol with IMAP + webhook adapters, because adding/swapping transports is a config change, not a rewrite
4. **Reply-to scope:** Per-message (not per-request), because it limits blast radius of leaked addresses and enables RFC 5322 threading
5. **PII encryption:** `StringEncryptedType` on `comment_text` in v1, because retrofitting encryption after plaintext data exists is a compliance risk
6. **Bounce surfacing:** Inline on comment card, because it co-locates status with the message and avoids timeline clutter
7. **Messaging providers:** Refactor into ABC (Eli's `LJ-352-improvements` patterns), because it pays down tech debt and bakes in threading header support for all providers

## Pros and Cons of the Options

### 1. Inbound reply transport

**Chosen: Dual-path — IMAP polling for on-prem, managed inbound (SendGrid Inbound Parse) for cloud.**

Pros:
* On-prem customers work behind firewalls with no public endpoint required
* Cloud customers avoid the prohibitive 6-step IMAP mailbox provisioning on a third-party provider
* The `InboundMessageHandler` interface makes both paths share all downstream logic (threading, notifications, abuse protection)

Cons:
* Two transport adapters must be implemented and tested
* IMAP polling has ~30s worst-case latency vs. near-real-time for webhooks

**Rejected: IMAP polling only.** Cloud customers (the primary launch targets) would need to provision and maintain a separate IMAP mailbox on a third-party provider that still supports basic auth — prohibitive operational burden.

**Rejected: Provider webhooks only.** Requires a publicly accessible endpoint, which is a non-starter for on-prem deployments behind firewalls.

### 2. Data model: extend Comment vs. new Correspondence entity

**Chosen: Extend the Comment model with two new `CommentType` values (`message_to_subject`, `reply_from_subject`) and a satellite `correspondence_metadata` table.**

Pros:
* Outbound messages and replies inherit existing attachment support, cascade deletes, reference system, and activity timeline integration
* The timeline already merges comments — no new data source to integrate
* `correspondence_metadata` keeps email-specific fields (Message-ID, delivery status, reply-to address) off the comment model — internal notes never carry nullable email columns

Cons:
* The Comment model gains complexity — code that filters on `comment_type` must be audited for assumptions that only `note` exists

**Rejected: New Correspondence entity.** Would require new reference tables, cascade delete logic, attachment support, and timeline integration — all of which the Comment system already provides. The semantic purity of a dedicated model does not justify the infrastructure duplication.

### 3. Inbound processing: transport-agnostic interface

**Chosen: `InboundMessageHandler` protocol with separate IMAP and webhook adapter implementations.**

Pros:
* Adding a new inbound mechanism (or swapping one out) is a config change + new adapter, not a rewrite of threading, notification, and abuse protection logic
* Both adapters are testable in isolation and must produce identical `InboundMessage` dataclasses

Neutral:
* The interface adds one level of indirection — acceptable given two adapters exist from day one

Cons:
* The protocol must be designed before either adapter, adding to foundation story scope

### 4. Reply-to address scope: per-message vs. per-request

**Chosen: One cryptographically random reply-to address per outbound message (`secrets.token_hex(16)` — 128 bits).**

Pros:
* A leaked address exposes only one message's reply channel, not the entire request's
* Enables proper RFC 5322 email threading (each outbound message has a unique Message-ID and reply-to, so replies thread correctly via `In-Reply-To`)

Cons:
* More reply-to addresses to track — one per outbound message vs. one per request
* Inbound routing requires an indexed lookup on `reply_to_address` (mitigated by the index on `correspondence_metadata.reply_to_address`)

**Rejected: Per-request reply-to address.** Simpler to manage but prevents per-message threading and means a single leaked address exposes the entire request's reply channel.

### 5. PII encryption: v1 vs. deferred

**Chosen: Encrypt `comment_text` with `StringEncryptedType` in v1, consistent with the existing identity chain (`ProvidedIdentity.encrypted_value`, `PrivacyRequest.encrypted_identity`, `RequestTask.result`).**

Pros:
* Correspondence text (which contains data subject PII) is protected at rest from day one
* Retrofitting encryption after plaintext data exists is operationally costly and risks a compliance gap
* Maintains consistency with the existing encryption pattern across the identity chain

Cons:
* `StringEncryptedType` is column-level — all comment text is encrypted, including internal notes (no functional impact, but slightly over-broad)
* Encrypted columns cannot be searched with SQL LIKE/ILIKE — any future comment search feature requires a separate approach

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

**Chosen: Render delivery status inline on the correspondence comment card via the `correspondence_metadata` join.**

Pros:
* Delivery status is co-located with the message it describes — no scanning the timeline for a separate entry
* The timeline doesn't need to understand a new entry type
* `bounced`/`failed` renders as a visible error state directly on the comment card, which is where the analyst's attention already is

Cons:
* The comment card UI gains complexity (delivery status indicator, bounce reason tooltip)

**Rejected: New timeline entry per delivery status transition.** Would clutter the timeline with system events (pending → sent → delivered) for every outbound message, and requires the timeline to understand a new entry type alongside `REQUEST_UPDATE`, `INTERNAL_COMMENT`, and `MANUAL_TASK`.

### 7. Messaging provider refactor: ABC in fides vs. patching existing dispatchers

**Chosen: Refactor all 5 dispatchers into provider classes behind a two-level ABC in fides OSS, adopting patterns from Eli's `LJ-352-improvements` branch.**

The existing `message_dispatch_service.py` (957 lines) has 5 private dispatcher functions with duplicated validation, no common interface, and no support for custom email headers. Adding correspondence threading headers requires touching all 4 email dispatchers.

Pros:
* Eli already wrote a complete implementation (`LJ-352-improvements` branch, ~Feb 2025) — the architecture is proven, we're updating it to current main rather than designing from scratch
* The two-level split (`BaseEmailProviderService` / `BaseSMSProviderService`) correctly models the different signatures (email takes `EmailForActionType`, SMS takes `str`)
* Threading headers (`reply_to`, `message_id`, `in_reply_to`, `references`) and plaintext MIME (`body_text`) are baked into the ABC from day one — every provider supports them
* The refactor lives in fides OSS, avoiding dual-maintenance of dispatcher functions in fides and provider classes in fidesplus

Neutral:
* This is a larger change than just patching the 4 email dispatchers, but the existing code was already identified as tech debt

Cons:
* The refactor touches all messaging paths, not just correspondence — requires comprehensive regression testing
* AWS SES currently uses the simple `send_email` API which doesn't support custom headers — must switch to `send_raw_email()` or SES v2 API

**Rejected: Patch existing dispatcher functions.** Would add threading header support as one-off additions to each of the 4 email dispatcher functions, leaving the god function intact and making future provider additions equally painful. The 957-line file would only grow.
