search: email address search + address-first ranking; inline images; folder i18n; link security
Search (PG backend):
- '@' terms match address substrings (from/to/cc), so a partial email like
"dyrektor@wfdif" resolves and narrows predictably as you type
- index each address split into parts, so a bare domain/local word ("wfdif")
finds the address (indexed as one lexeme otherwise); dotted terms fall back to
an address substring
- rank sender/recipient name+address matches above body/subject-only ones
(addr_hit tier, then recency); cursor extended to keep keyset pagination correct
- scripts/reindex_address_parts.py backfills address tokens into existing rows
Inline images:
- extract attachments via a full MIME walk instead of shallow iter_attachments(),
which missed inline images nested in mixed>related>alternative (cid: rendered
broken); has_attachments counts only real (non-cid) attachments
- scripts/backfill_inline_attachments.py stores previously-missed inline images
Folder-name i18n (SPEC §12 M2):
- accent/punctuation-insensitive, multi-delimiter (/ \ .) special-use inference
across ~13 languages; curated names (no substring false positives)
Link security (reading panel):
- links never navigate the message iframe: a parent-side click interceptor opens
http/https in a new tab (noopener) and mailto in the composer (recipient
pre-filled); all other schemes blocked
Backend 292 tests green, ruff + mypy --strict clean; frontend tsc/eslint/vitest clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U72FZiF1W1pb8XxyWBTREA
This commit is contained in:
@@ -93,6 +93,39 @@ def test_attachment_extracted() -> None:
|
||||
assert parsed.attachments[0].payload == b"\x00\x01\x02data"
|
||||
|
||||
|
||||
def test_inline_image_nested_in_related_is_extracted() -> None:
|
||||
# Real-world structure: mixed > related > alternative(text, html) + inline image.
|
||||
# iter_attachments() misses the nested image; a full walk must find it so the
|
||||
# cid: reference resolves (regression: broken inline image).
|
||||
from email.mime.image import MIMEImage
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
root = MIMEMultipart("mixed")
|
||||
root["Subject"] = "newsletter"
|
||||
root["From"] = "biuro@kipa.pl"
|
||||
root["To"] = "me@example.com"
|
||||
root["Message-ID"] = "<n@x.com>"
|
||||
related = MIMEMultipart("related")
|
||||
alt = MIMEMultipart("alternative")
|
||||
alt.attach(MIMEText("text version", "plain"))
|
||||
alt.attach(MIMEText('<p>Hi <img src="cid:img001"></p>', "html"))
|
||||
related.attach(alt)
|
||||
img = MIMEImage(b"\x89PNG\r\n\x1a\ndata", "png")
|
||||
img.add_header("Content-ID", "<img001>")
|
||||
img.add_header("Content-Disposition", "inline", filename="image001.png")
|
||||
related.attach(img)
|
||||
root.attach(related)
|
||||
|
||||
parsed = parse_message(root.as_bytes())
|
||||
assert len(parsed.attachments) == 1
|
||||
att = parsed.attachments[0]
|
||||
assert att.content_id == "img001"
|
||||
assert att.mime_type == "image/png"
|
||||
# Inline-only: no attachment chip / paperclip.
|
||||
assert parsed.has_attachments is False
|
||||
|
||||
|
||||
def test_missing_message_id_returns_none() -> None:
|
||||
msg = EmailMessage()
|
||||
msg["Subject"] = "no id"
|
||||
|
||||
Reference in New Issue
Block a user