Files
webmail-client/backend/app/sync/folder_names.py
T
lukasandClaude Opus 4.8 c023cfa15e 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
2026-07-26 09:28:48 +02:00

121 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Special-use inference from folder names (SPEC §6.3 / §12 M2 fallback).
Used only when the server does not advertise the SPECIAL-USE extension. Matching
is accent- and punctuation-insensitive (so "Wysłane", "Éléments envoyés" and
"Junk E-mail" all resolve), covers the hierarchy leaf under any of the common
IMAP delimiters (``/ \\ .``), and recognises provider prefixes like
``[Gmail]/`` implicitly via the leaf. Names are curated per language rather than
substring-matched, so a folder like "Sent to John" is *not* misfiled as Sent.
"""
from __future__ import annotations
import unicodedata
# Latin letters NFKD doesn't decompose to ASCII; fold them explicitly.
_LATIN_SPECIAL = str.maketrans(
{"ł": "l", "ø": "o", "đ": "d", "ð": "d", "ß": "ss", "æ": "ae", "œ": "oe", "ı": "i", "þ": "th"}
)
def _compact(name: str) -> str:
"""Fold to a comparable key: lowercase, strip accents/diacritics, drop every
non-alphanumeric char. "Éléments envoyés" -> "elementsenvoyes"."""
folded = unicodedata.normalize("NFKD", name.strip().lower()).translate(_LATIN_SPECIAL)
return "".join(ch for ch in folded if ch.isalnum())
# Localized folder names by special_use. Written readably; compacted at load.
_NAMES: dict[str, list[str]] = {
"sent": [
"sent", "sent items", "sent mail", "sent messages", # English / Outlook / Gmail
"wysłane", "elementy wysłane", "wiadomości wysłane", # Polish
"gesendet", "gesendete objekte", "gesendete elemente", # German
"envoyés", "éléments envoyés", "messages envoyés", "courrier envoyé", # French
"enviados", "elementos enviados", "correo enviado", # Spanish
"inviata", "posta inviata", "messaggi inviati", # Italian
"verzonden", "verzonden items", "verzonden berichten", # Dutch
"enviadas", "itens enviados", "mensagens enviadas", # Portuguese
"отправленные", "исходящие", # Russian
"skickat", "skickade", "skickade objekt", # Swedish
"sendt", "sendte", "sendte elementer", # Norwegian / Danish
"lähetetyt", # Finnish
"odeslané", "odeslaná pošta", # Czech
],
"drafts": [
"drafts", "draft",
"robocze", "kopie robocze", "wersje robocze",
"entwürfe", "entwurf",
"brouillons",
"borradores",
"bozze",
"concepten",
"rascunhos",
"черновики",
"utkast",
"kladder", # Danish
"luonnokset", # Finnish
"koncepty", # Czech
],
"trash": [
"trash", "deleted", "deleted items", "deleted messages", "bin", "recycle bin",
"kosz",
"papierkorb", "gelöschte objekte", "gelöschte elemente",
"corbeille", "éléments supprimés",
"papelera", "elementos eliminados",
"cestino", "posta eliminata",
"prullenbak", "verwijderde items",
"lixeira", "lixo", "itens excluídos",
"корзина", "удаленные",
"papperskorg", "borttaget",
"papirkurv", "slettet",
"roskakori", "poistetut",
"koš", # Czech
],
"junk": [
"junk", "junk email", "junk e-mail", "spam", "bulk mail", "bulk",
"wiadomości-śmieci", "niechciane",
"unerwünscht", "werbung",
"courrier indésirable", "indésirables", "pourriel",
"correo no deseado", "no deseado", "correo basura",
"posta indesiderata", "indesiderata",
"ongewenst", "ongewenste e-mail",
"lixo eletrônico",
"спам", "нежелательная почта",
"skräp", "skräppost",
"uønsket",
"roskaposti",
"nevyžádaná pošta",
],
"archive": [
"archive", "archives", "all mail",
"archiwum",
"archiv",
"archivio",
"archivo",
"archief",
"arquivo", "arquivo morto",
"архив", "вся почта",
"arkiv",
"arkisto",
],
}
_NAME_MAP: dict[str, str] = {
_compact(name): special for special, names in _NAMES.items() for name in names
}
def infer_special_use(path: str) -> str | None:
"""Infer a special_use value from a mailbox path/name (accent-insensitive)."""
if path.strip().upper().rstrip("/.") == "INBOX":
return "inbox"
# Take the hierarchy leaf under any common IMAP delimiter (/ \ or . for
# Dovecot/Courier namespaces), then fold for lookup.
normalized = path
for delim in ("/", "\\", "."):
normalized = normalized.replace(delim, "\n")
segments = [s for s in normalized.split("\n") if s.strip()]
tail = segments[-1] if segments else path
return _NAME_MAP.get(_compact(tail)) or _NAME_MAP.get(_compact(path))