Skip to content

Fix silent attachment dropping; cap inline images; strip raw email attachments server-side - #91

Merged
wparad merged 3 commits into
mainfrom
claude/sanitizer-large-attachments-6d1lbk
Aug 26, 2026
Merged

Fix silent attachment dropping; cap inline images; strip raw email attachments server-side#91
wparad merged 3 commits into
mainfrom
claude/sanitizer-large-attachments-6d1lbk

Conversation

@wparad

@wparad wparad commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Oversized attachments (> MAX_SINGLE_ATTACHMENT_SIZE, 10MB) and attachments whose S3 upload failed were silently discarded in the content sanitizer — no log line, no trace in the response. They're now collected as droppedAttachments (filename, mimeType, sizeBytes, reason: "too_large" | "upload_failed"), returned in the sanitizer response, logged as a TRACK (content_sanitizer.attachments_dropped) from the sanitizer, and surfaced as a WARN (processor.attachments_dropped) from the processor.
  • Added a TRACK log (content_sanitizer.slow_parse) when MIME parsing itself takes longer than 10s — an earlier signal than the existing 50s near-timeout alert.
  • Inline CID images were previously embedded as unbounded base64 data URIs directly into htmlBody — a large inline logo could blow past DynamoDB's 400KB item cap. Fixed a latent bug where mailparser's default auto-embedding bypassed our own size checks entirely (keepCidLinks: true now disables that), and a key mismatch (attachment.contentId vs the bracket-stripped attachment.cid mailparser actually matches cid: refs against). Now only images ≤100KB, first 3 per message, are inlined as data URIs; the rest are uploaded to S3 like regular attachments (inlineImages) and resolved to a CDN url at API read time (new shared withResolvedContentUrls() in signal-transforms.ts, replacing two duplicated withAttachmentUrls() helpers).
  • "View original email" / download-as-.eml now serves a display-safe copy of the raw MIME built server-side by the content sanitizer at ingestion time: attachments fully stripped, small inline images kept (≤100KB each, 300KB cumulative per message — needed so the .eml still renders if opened in a real mail client) — see raw-email-display.ts. Persisted as InboundEmailSignalData.displayRawS3Key; the .../signals/:id/raw endpoint redirects to it, falling back to the true original for signals processed before this existed. The true original is never served through that endpoint anymore.

Test plan

  • npx tsc -p tsconfig.check.json --noEmit
  • npx eslint on all changed src/ files
  • New specs: content-sanitizer-attachments.spec.ts, content-sanitizer-inline-images.spec.ts, raw-email-display.spec.ts, content-sanitizer-display-raw.spec.ts, plus withResolvedContentUrls cases in signal-transforms.spec.ts
  • Full test suite passes (2856 tests)

🤖 Generated with Claude Code

https://claude.ai/code/session_01V85rptvcLi67TUmaqU1s53


Generated by Claude Code

claude added 3 commits August 25, 2026 23:27
Oversized attachments (>10MB) and attachments whose S3 upload failed
were silently discarded in the content sanitizer with no log line and
no trace in the response. Now they're collected as droppedAttachments
(filename, mimeType, sizeBytes, reason), returned in the sanitizer
response, logged as a TRACK from the sanitizer, and surfaced as a WARN
from the processor when present.

Also add a TRACK log when MIME parsing itself takes longer than 10s,
as an earlier signal than the existing 50s near-timeout alert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V85rptvcLi67TUmaqU1s53
…em limit

Inline CID images were embedded as unbounded base64 data URIs directly in
htmlBody, which is a stored DynamoDB attribute subject to the table's 400KB
item cap. Now only small images (<=100KB, first 3 per message) are inlined;
anything past either cap is uploaded to S3 like a regular attachment via a
new InboundEmailSignalData.inlineImages list (s3Key only, never the bytes),
and its cid: reference is left unresolved in htmlBody until API read time.

Along the way, discovered and fixed two pre-existing issues this surfaced:
- mailparser's default keepCidLinks:false silently base64-embeds every CID
  image into parsed.html before our own code ever runs, which is why the
  existing size-aware logic could never have worked without keepCidLinks:
  true — now set explicitly so the sanitizer's own budget decides.
- The sanitizer's cid resolution keyed off attachment.contentId (the raw,
  angle-bracketed Content-ID header value, e.g. "<logo>") instead of
  attachment.cid (the bracket-stripped form actually referenced by bare
  `cid:` links in the HTML) — a latent mismatch that never surfaced because
  mailparser's own auto-embed ran first and masked it.

signalsApi.ts and threadsApi.ts each had an identical local
withAttachmentUrls() helper; consolidated into one shared
withResolvedContentUrls() in signal-transforms.ts that both computes
Attachment.url from s3Key (as before) and now also resolves any leftover
cid: reference in htmlBody using inlineImages — computed lazily at read
time, never persisted as a baked-in URL, matching the existing convention.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V85rptvcLi67TUmaqU1s53
…ntend

The "view original email" feature previously served the fully original raw
MIME (including base64 attachment bodies) and had the frontend strip
attachments for display only — Copy/Download still shipped the full raw
bytes to the client. Move this to the backend instead: the content
sanitizer now builds a display-safe copy of the raw MIME at ingestion time
(attachments fully stripped; small inline images kept intact so the .eml
still renders in a real mail client if downloaded, capped at 100KB per
image and 300KB cumulative per message — additional images beyond either
cap are truncated), uploads it alongside extracted attachments, and the
processor persists its s3Key as InboundEmailSignalData.displayRawS3Key.
The GET .../signals/:id/raw endpoint now redirects to this display-safe
copy when available, falling back to the true original for signals
processed before this feature existed. The true original is never served
through that path anymore — it stays available server-side only (e.g. for
reprocessing).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V85rptvcLi67TUmaqU1s53
@wparad wparad changed the title Fix silent dropping of oversized attachments; add slow-parse TRACK log Fix silent attachment dropping; cap inline images; strip raw email attachments server-side Aug 26, 2026
@wparad
wparad merged commit f2ea3ff into main Aug 26, 2026
6 checks passed
@wparad
wparad deleted the claude/sanitizer-large-attachments-6d1lbk branch August 26, 2026 12:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants