Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
304d5af
feat(mail): open recognized HWPX text from email attachments
cursoragent Aug 17, 2026
d2e201c
merge: retarget #1406 onto live #1404 head
cursoragent Aug 18, 2026
b83a0da
fix(mail): reuse live #1404 pending preview refresh
cursoragent Aug 18, 2026
1ff9fc7
merge(stack): inherit repaired HWPX preview head
seonghobae Sep 5, 2026
6878fc4
fix(docs): restore canonical AGENTS ownership
seonghobae Sep 6, 2026
4c03dec
merge(stack): inherit current HWPX preview parent
seonghobae Sep 6, 2026
26895b6
merge(stack): inherit current Data preview parent
seonghobae Sep 6, 2026
55f489a
docs(mail): restore mail preview traceability
seonghobae Sep 6, 2026
0edb98f
merge(mail): restore preview E2E fixture delta
seonghobae Sep 6, 2026
4059475
merge(mail): adopt current HWPX preview parent
seonghobae Sep 6, 2026
cc7d2ce
fix(mail): preserve current preview parent tree
seonghobae Sep 6, 2026
63fd00a
test(mail): forbid attachment-body loads in detail and thread
seonghobae Sep 6, 2026
6d3bcc5
fix(mail): load attachment metadata without bodies
seonghobae Sep 6, 2026
7615754
test(mail): clean PostgreSQL preview smoke rows
seonghobae Sep 6, 2026
bc24bf6
test(mail): keep latest attachment preview response
seonghobae Sep 6, 2026
298bbba
fix(mail): ignore stale attachment preview responses
seonghobae Sep 6, 2026
b0ba575
chore(mail): preserve preview source newline
seonghobae Sep 6, 2026
c5fde35
chore(mail): preserve preview test newline
seonghobae Sep 6, 2026
501d019
test(mail): exercise real signed attachment session
seonghobae Sep 6, 2026
edd3134
test(mail): verify cookie-backed attachment preview path
seonghobae Sep 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,12 @@ in this repo.
topic components, or label evidence by a bare document, model, topic, rank,
label, or display value.
- When reviews find public/private identifier leaks, stale API fixture shapes, or recurring bug patterns, update tests, frontend mocks, E2E mocks, README examples, architecture docs, and explicitly record the anti-pattern in `AGENTS.md` so the same bug pattern does not reappear in copied examples.
- Mail attachment surfaces must list the current email's files with opaque
`asset_key` values and open the signed
`/api/data/repository-assets/{asset_key}/preview` contract. Do not expose
sequential attachment ids, render missing preview text as empty document
content, or send buyers only to the Data repository list as a substitute for
opening the HWPX file on the selected mail.
- Memoized id-to-record Maps must be first-wins (`if (!map.has(key)) map.set(...)`).
`new Map(items.map((item) => [String(item.id), item]))` is last-wins and
desynchronizes first-wins label maps from the selected node or edge when
Expand Down
33 changes: 32 additions & 1 deletion backend/api/emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import func, or_, select
from sqlalchemy.orm import selectinload
from db.session import get_db
from db.models import Email
from api.data import _opaque_asset_key
from pydantic import BaseModel, EmailStr, Field, field_validator
import datetime
import time
Expand Down Expand Up @@ -165,6 +167,22 @@ def _email_list_item(
)


def _mail_attachment_refs(email: Email) -> list["EmailAttachmentRef"]:
"""Return opaque, display-safe attachment refs for one scoped email."""

refs: list[EmailAttachmentRef] = []
for attachment in getattr(email, "attachments", None) or []:
file_name = _safe_email_display_text(getattr(attachment, "filename", None))
refs.append(
EmailAttachmentRef(
asset_key=_opaque_asset_key(email, attachment),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include attachment identity in each opaque asset key

When one email contains two attachments with the same filename—which the attachment schema permits—both references receive the same key because _opaque_asset_key hashes only the owner, message ID, and filename. MailAttachmentPreview subsequently uses that key for React identity, selection, and caching, while the preview endpoint returns the first matching row, so clicking either attachment can display the wrong document. Include an immutable attachment-specific value in the key and lookup, then add a duplicate-filename case to backend/tests/test_email_attachment_preview.py.

AGENTS.md reference: AGENTS.md:L449-L454

Useful? React with 👍 / 👎.

file_name=file_name or "email attachment",
parser_family=str(getattr(attachment, "parser_key", "") or "") or None,
)
)
return refs


def _email_detail_response(email: Email) -> "EmailDetailResponse":
return EmailDetailResponse(
id=email.id,
Expand All @@ -178,6 +196,7 @@ def _email_detail_response(email: Email) -> "EmailDetailResponse":
thread_id=canonical_thread_key(email),
in_reply_to=email.in_reply_to,
references=email.references,
attachments=_mail_attachment_refs(email),
)


Expand All @@ -197,6 +216,14 @@ class EmailListItem(BaseModel):
schedule_conflict: bool = False


class EmailAttachmentRef(BaseModel):
"""Opaque mail-attachment handle that opens the read-only preview contract."""

asset_key: str
file_name: str
parser_family: str | None = None


class EmailDetailResponse(BaseModel):
id: int
message_id: str
Expand All @@ -211,6 +238,7 @@ class EmailDetailResponse(BaseModel):
references: str | None = None
requires_reply: bool = False
schedule_conflict: bool = False
attachments: list[EmailAttachmentRef] = Field(default_factory=list)


class UniqueThreadCandidateRequest(BaseModel):
Expand Down Expand Up @@ -645,7 +673,9 @@ async def get_email(
):
# Ensure auth context validates the request payload and scopes access
result = await db.execute(
select(Email).where(
select(Email)
.options(selectinload(Email.attachments))
Comment thread
seonghobae marked this conversation as resolved.
Outdated
.where(
Email.id == email_id,
*Email.owner_filters(auth_context.user_id, auth_context.organization_id),
)
Expand All @@ -668,6 +698,7 @@ async def get_email_thread(
lookup_values = thread_lookup_values(thread_id)
result = await db.execute(
select(Email)
.options(selectinload(Email.attachments))
.where(
*Email.owner_filters(auth_context.user_id, auth_context.organization_id),
or_(
Expand Down
Loading