Skip to content

Decision Log: Direct Email Send Integration

Decision Log: Direct Email Send Integration

Section titled “Decision Log: Direct Email Send Integration”

Tracks the design decisions for wiring the Email Order composer to the ShopAccess/Email backend (PDEV-969): how the direct-send capability is detected and cached, how the composed message maps onto the EmailJobInput contract, and how the front-end validates and protects the send. Decisions here are mirrored in the Design Document Decision Summary.

#QuestionStatusDecisionRound
DQ-001What drives the direct-send vs copy-only toggleResolvedOperational config whose slug contains the procurement tokenR1
DQ-002When the toggle is resolvedResolvedAt sign-in / tenant switch; cached in the SPA per tenantR1
DQ-003Source of the required subjectResolvedEditable field, default Order for {supplier} — {MMM d, yyyy}R1
DQ-004What the UI “From” field becomesResolvedSent as replyToEmail (Reply-To)R1
DQ-005How the email body is producedResolvedApp-generated inline-styled HTML + plain textR1
DQ-006Injection / validation handlingResolvedEscape on compose + validate addresses; block otherwiseR1
DQ-007Tenant + idempotency headersResolvedBFF injects X-Tenant-Id; SPA generates stable Idempotency-KeyR1
DQ-008Non-Operational config statesResolvedTreated as restricted (copy-only)R1
DQ-009attachments / bccResolvedEmpty ([]) for this featureR1
DQ-010Body HTML/script injection protectionResolvedAllow-list sanitize + reject-if-stripped at the shared BFF; backend hardening tracked by PDEV-976R2
DQ-011Send format (HTML vs plain text)ResolvedSend split button: default HTML (+ text alternate); menu HTML / plain text. Copy writes both (unchanged)R3

DQ-001: What drives the direct-send vs copy-only toggle

Section titled “DQ-001: What drives the direct-send vs copy-only toggle”

Context: Some tenants can send directly and some cannot. The signal must be derivable from the Email Module without a new endpoint.

OptionDescriptionTrade-offs
AExistence of an Operational EmailConfiguration whose identity.sendingDomainSlug contains a procurement marker (a constant)Uses the as-built contract; marker keeps it easily retargetable
BA dedicated capability flag/endpointCleaner semantically, but no such endpoint exists; new backend work

Recommendation: Option A — fits the shipped contract; the marker is a single constant.

Decision: Option A. Marker is PROCUREMENT_EMAIL_SLUG_TOKEN = "procurement", matched as a substring of sendingDomainSlug; the config must be in Operational state (see DQ-008).

Applied to: design.md (Overview, Structural Design, DQ-001).


Context: The capability changes very rarely. Re-checking on every panel open would be wasteful.

OptionDescriptionTrade-offs
AResolve at sign-in / tenant switch, cache in the SPA per tenantCheap; matches the slow-moving nature; small staleness window
BResolve on every Email Order openAlways fresh; needless repeated backend calls

Recommendation: Option A.

Decision: Option A. A stale cache is tolerated; a backend not-sendable response on Send is the backstop (DQ-008 narrative in design.md).

Applied to: design.md (Overview, toggle-resolution sequence).


Context: EmailJobInput.subject is required, but this send path has no server-side subject composer.

OptionDescriptionTrade-offs
AEditable Subject field, prefilled with a sensible defaultUser control; satisfies the required field; one extra row
BAuto-composed, not shownLess UI; no user control
CFixed constantSimplest; least informative

Recommendation: Option A.

Decision: Option A. Default text Order for {supplier} — {MMM d, yyyy} (en-US date, e.g. “Order for Stark Industries — Jun 26, 2026”); editable thereafter. Non-empty subject is a validation rule (DQ-006).

Applied to: design.md (DQ-003); email-order-ui.md mock (Subject field).


DQ-004: What the UI “From” field becomes

Section titled “DQ-004: What the UI “From” field becomes”

Context: The backend From is fixed by the configuration’s sender identity and is not part of EmailJobInput. The composer nonetheless shows an editable “From”.

OptionDescriptionTrade-offs
AMap the UI From to the message Reply-To (replyToEmail)Gives the field real meaning; replies route to the chosen address
BDisplay-only senderHonest, but the field becomes inert
CRemove the From rowSimplest; loses the reply-routing affordance

Recommendation: Option A.

Decision: Option A. The UI From value is sent as replyToEmail; an empty From yields null. The actual From remains the configuration’s sender.

Applied to: design.md (Overview, DQ-004, API Contract).


Context: “Collect the resulting HTML static form” — but the rendered DOM uses CSS-module classes, which mail clients strip.

OptionDescriptionTrade-offs
AApp-generated email-safe HTML with inlined styles (+ plain textBody) from the live composer stateRenders reliably across mail clients; deterministic
BSerialize the live class-based DOM as-isLiteral, but renders poorly once classes/<style> are stripped

Recommendation: Option A.

Decision: Option A. Compose both htmlBody (inline-styled) and textBody (plain) from the same live state used by the copy/print path.

Applied to: design.md (Overview, DQ-005, Implementation Scope).


Context: User-editable fields feed an HTML email; the front-end must not emit unsafe markup and must ensure a well-formed envelope.

OptionDescriptionTrade-offs
AEscape user content on compose + validate (≥1 To, RFC addresses); block the send with an error otherwiseInjection structurally impossible; permissive about stray < in prose
BEscape + hard-reject any field containing markupStricter; annoys on legitimate </> usage
CEscape + non-blocking warningSofter; relies on the user heeding the warning

Recommendation: Option A.

Decision: Option A. Escape every user field when composing the HTML; validate ≥1 To, RFC-valid To/Cc/Reply-To, and non-empty Subject; a failure shows a blocking error toast and the send is not attempted.

Applied to: design.md (Overview, DQ-006, Testing Strategy).


Context: POST /job requires X-Tenant-Id and Idempotency-Key.

OptionDescriptionTrade-offs
ABFF injects X-Tenant-Id from the session; SPA generates the Idempotency-Key, stable across retries of one SendTenant trust stays server-side; retries don’t double-send
BSPA supplies bothTenant id on the client is less trustworthy

Recommendation: Option A.

Decision: Option A. The SPA mints one Idempotency-Key per Send attempt and reuses it on retry; the BFF derives X-Tenant-Id from the authenticated session.

Applied to: design.md (DQ-007, send sequence, API Contract).


DQ-008: Non-Operational configuration states

Section titled “DQ-008: Non-Operational configuration states”

Context: A matching config may be in Draft, Provisioning, AwaitingVerification, or a *Failed state rather than Operational.

OptionDescriptionTrade-offs
AOnly Operational enables direct send; all other states ⇒ restricted (copy-only)Safe; never offers Send when delivery can’t succeed
BEnable for any matching config regardless of stateRisks offering Send that the backend will reject

Recommendation: Option A.

Decision: Option A. Non-Operational ⇒ restricted form; provisioning is PDEV-971’s responsibility.

Applied to: design.md (DQ-008, error/edge narrative).


Context: Both are present in the contract (attachments required as an array; bcc required in recipients) but unused by this feature.

OptionDescriptionTrade-offs
ASend attachments: [] and bcc: []Satisfies the contract; no extra UI

Recommendation: Option A.

Decision: Option A. Attachments and BCC are out of scope for PDEV-969.

Applied to: design.md (DQ-009, Implementation Scope → Out of Scope).


DQ-010: Protecting the body against HTML / script injection

Section titled “DQ-010: Protecting the body against HTML / script injection”

Context: Users edit text that becomes the email htmlBody, opening an HTML/script-injection surface. The body is also shown in the app’s own preview and may be stored and re-rendered — an XSS surface independent of the recipient’s mail client. An assessment of operations @ origin/main (880790f6) found the Email module does not sanitize or validate the body (it forwards htmlBody verbatim to Postmark), and Postmark does not sanitize either (it relays as-is).

OptionDescriptionTrade-offs
AAllow-list sanitize htmlBody at the shared BFF route and reject (400) if anything was stripped; SPA escape-on-compose (DQ-006) stays the first layerStrongest; protects every client of the route; clear signal on injection attempts
BSanitize only (silently clean)Never blocks, but silently alters the message and hides attempts
CReject only, no sanitizeNo mutation, but brittle — must enumerate every dangerous pattern

Recommendation: Option A.

Decision: Option A. The shared BFF send route allow-list-sanitizes htmlBody (safe formatting tags only; no script / iframe / style / event handlers / javascript: URLs) and rejects the request if sanitizing removed anything. The backend should protect itself at the source of truth — tracked by PDEV-976 (High, Cycle 12).

Applied to: design.md (DQ-010, BFF routes + API contract + Out of Scope), goal.md (constraint 7), ui-implementation-analysis.md (§2 G13, §3.4, §4, §5).


DQ-011: How the user chooses HTML vs plain text for direct send

Section titled “DQ-011: How the user chooses HTML vs plain text for direct send”

Context: Today’s EmailPanel Copy puts both a rich-HTML and a plain-text version on the clipboard. Direct send should mirror that — letting the user send either format — without cluttering the footer.

OptionDescriptionTrade-offs
ASend is a split button (like the Items page “Add Item”): default action sends HTML; a caret menu offers “Send as HTML” / “Send as plain text”Familiar pattern; one prominent default; both formats one click away
BTwo separate buttons (Send HTML, Send plain text)More footer clutter; no clear default
CA format toggle/radio elsewhere in the panelDetaches the choice from the action

Recommendation: Option A.

Decision: Option A. Default action sends HTML (htmlBody + a plain-text alternate in textBody); the menu’s “Send as plain text” sends textBody only. Built with the canary SplitButton. Copy is unchanged and writes both text/html and text/plain to the clipboard.

Applied to: design.md (DQ-011, Decision Summary + API contract), email-order-ui.md (capabilities + live mock), ui-implementation-analysis.md (§3.2).