2026-07-26 09:28:48 +02:00
|
|
|
|
"""Special-use inference from folder names (SPEC §6.3 / §12 M2 fallback).
|
2026-07-17 23:20:34 +02:00
|
|
|
|
|
2026-07-26 09:28:48 +02:00
|
|
|
|
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.
|
2026-07-17 23:20:34 +02:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-07-26 09:28:48 +02:00
|
|
|
|
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": [
|
2026-10-14 22:08:06 +02:00
|
|
|
|
"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
|
2026-07-26 09:28:48 +02:00
|
|
|
|
"lähetetyt", # Finnish
|
2026-10-14 22:08:06 +02:00
|
|
|
|
"odeslané",
|
|
|
|
|
|
"odeslaná pošta", # Czech
|
2026-07-26 09:28:48 +02:00
|
|
|
|
],
|
|
|
|
|
|
"drafts": [
|
2026-10-14 22:08:06 +02:00
|
|
|
|
"drafts",
|
|
|
|
|
|
"draft",
|
|
|
|
|
|
"robocze",
|
|
|
|
|
|
"kopie robocze",
|
|
|
|
|
|
"wersje robocze",
|
|
|
|
|
|
"entwürfe",
|
|
|
|
|
|
"entwurf",
|
2026-07-26 09:28:48 +02:00
|
|
|
|
"brouillons",
|
|
|
|
|
|
"borradores",
|
|
|
|
|
|
"bozze",
|
|
|
|
|
|
"concepten",
|
|
|
|
|
|
"rascunhos",
|
|
|
|
|
|
"черновики",
|
|
|
|
|
|
"utkast",
|
|
|
|
|
|
"kladder", # Danish
|
|
|
|
|
|
"luonnokset", # Finnish
|
|
|
|
|
|
"koncepty", # Czech
|
|
|
|
|
|
],
|
|
|
|
|
|
"trash": [
|
2026-10-14 22:08:06 +02:00
|
|
|
|
"trash",
|
|
|
|
|
|
"deleted",
|
|
|
|
|
|
"deleted items",
|
|
|
|
|
|
"deleted messages",
|
|
|
|
|
|
"bin",
|
|
|
|
|
|
"recycle bin",
|
2026-07-26 09:28:48 +02:00
|
|
|
|
"kosz",
|
2026-10-14 22:08:06 +02:00
|
|
|
|
"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",
|
2026-07-26 09:28:48 +02:00
|
|
|
|
"koš", # Czech
|
|
|
|
|
|
],
|
|
|
|
|
|
"junk": [
|
2026-10-14 22:08:06 +02:00
|
|
|
|
"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",
|
2026-07-26 09:28:48 +02:00
|
|
|
|
"lixo eletrônico",
|
2026-10-14 22:08:06 +02:00
|
|
|
|
"спам",
|
|
|
|
|
|
"нежелательная почта",
|
|
|
|
|
|
"skräp",
|
|
|
|
|
|
"skräppost",
|
2026-07-26 09:28:48 +02:00
|
|
|
|
"uønsket",
|
|
|
|
|
|
"roskaposti",
|
|
|
|
|
|
"nevyžádaná pošta",
|
|
|
|
|
|
],
|
|
|
|
|
|
"archive": [
|
2026-10-14 22:08:06 +02:00
|
|
|
|
"archive",
|
|
|
|
|
|
"archives",
|
|
|
|
|
|
"all mail",
|
2026-07-26 09:28:48 +02:00
|
|
|
|
"archiwum",
|
|
|
|
|
|
"archiv",
|
|
|
|
|
|
"archivio",
|
|
|
|
|
|
"archivo",
|
|
|
|
|
|
"archief",
|
2026-10-14 22:08:06 +02:00
|
|
|
|
"arquivo",
|
|
|
|
|
|
"arquivo morto",
|
|
|
|
|
|
"архив",
|
|
|
|
|
|
"вся почта",
|
2026-07-26 09:28:48 +02:00
|
|
|
|
"arkiv",
|
|
|
|
|
|
"arkisto",
|
|
|
|
|
|
],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-17 23:20:34 +02:00
|
|
|
|
_NAME_MAP: dict[str, str] = {
|
2026-07-26 09:28:48 +02:00
|
|
|
|
_compact(name): special for special, names in _NAMES.items() for name in names
|
2026-07-17 23:20:34 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def infer_special_use(path: str) -> str | None:
|
2026-07-26 09:28:48 +02:00
|
|
|
|
"""Infer a special_use value from a mailbox path/name (accent-insensitive)."""
|
|
|
|
|
|
if path.strip().upper().rstrip("/.") == "INBOX":
|
2026-07-17 23:20:34 +02:00
|
|
|
|
return "inbox"
|
2026-07-26 09:28:48 +02:00
|
|
|
|
# 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))
|