Initial commit: webmail client (M0/M1)

Three-pane webmail client.
- Backend: FastAPI + async SQLAlchemy + arq worker (Postgres/Redis), IMAP/SMTP
  sync, threading, search, drafts/outbox with undo, attachments, 2FA.
- Frontend: React 18 + Vite + TypeScript + TanStack Query + Tailwind; three-pane
  UI with rich-text composer, light/dark themes, resizable panels.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 23:20:34 +02:00
co-authored by Claude Opus 4.8
commit a5ec0d21cb
174 changed files with 22239 additions and 0 deletions
+195
View File
@@ -0,0 +1,195 @@
"""HTML sanitization with nh3 (SPEC §6.4, §10.4, invariant I-8).
Mail HTML is hostile: sanitized server-side at index time with a broad-but-safe
allowlist (structural + presentational tags, filtered inline CSS), and rendered
inside a sandboxed iframe. ``cid:`` images are rewritten to the per-message
endpoint; remote images (and CSS ``url()``) are blocked by default and flagged so
the UI can offer "load images" (SPEC §9.4).
"""
from __future__ import annotations
import re
import uuid
from dataclasses import dataclass
import nh3
# Bump when the sanitization policy changes; body_parts.sanitizer_v records it so a
# render re-sanitizes stale content from the raw blob (SPEC §10.4).
SANITIZER_VERSION = 2
_ALLOWED_TAGS: set[str] = {
# structure
"a",
"abbr",
"address",
"article",
"aside",
"b",
"bdi",
"bdo",
"blockquote",
"br",
"caption",
"center",
"cite",
"code",
"col",
"colgroup",
"dd",
"del",
"details",
"dfn",
"div",
"dl",
"dt",
"em",
"figcaption",
"figure",
"font",
"footer",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"header",
"hr",
"i",
"img",
"ins",
"kbd",
"li",
"main",
"mark",
"nav",
"ol",
"p",
"pre",
"q",
"s",
"samp",
"section",
"small",
"span",
"strike",
"strong",
"sub",
"summary",
"sup",
"table",
"tbody",
"td",
"tfoot",
"th",
"thead",
"time",
"tr",
"tt",
"u",
"ul",
"var",
"wbr",
}
# Attributes allowed on any tag (presentational email formatting).
_GENERIC_ATTRS: set[str] = {
"style",
"align",
"valign",
"dir",
"title",
"width",
"height",
"bgcolor",
"color",
"face",
"size",
"span",
"nowrap",
"lang",
}
_TAG_SPECIFIC: dict[str, set[str]] = {
"a": {"href", "target"},
"img": {"src", "alt"},
"td": {"colspan", "rowspan"},
"th": {"colspan", "rowspan", "scope"},
"table": {"cellpadding", "cellspacing", "border"},
"font": {"face", "color", "size"},
}
# nh3 has no "generic attributes" option, so expand the generic set onto every tag.
_ALLOWED_ATTRS: dict[str, set[str]] = {
tag: set(_GENERIC_ATTRS) | _TAG_SPECIFIC.get(tag, set()) for tag in _ALLOWED_TAGS
}
_URL_SCHEMES: set[str] = {"http", "https", "mailto", "cid", "data"}
_STYLE_DANGEROUS = re.compile(
r"(expression\s*\(|javascript:|vbscript:|@import|-moz-binding|behavior\s*:)", re.IGNORECASE
)
_STYLE_URL = re.compile(r"url\s*\([^)]*\)", re.IGNORECASE)
@dataclass(frozen=True)
class SanitizeResult:
html: str
has_remote_images: bool
sanitizer_v: int
def _sanitize_style(value: str, allow_remote: bool) -> str | None:
if _STYLE_DANGEROUS.search(value):
return None # drop the whole style rather than try to repair it
if not allow_remote and _STYLE_URL.search(value):
# Strip CSS url() so backgrounds can't smuggle in remote/tracking images.
return _STYLE_URL.sub("none", value)
return value
def sanitize_html(
html: str, message_id: uuid.UUID, *, allow_remote_images: bool = False
) -> SanitizeResult:
"""Sanitize ``html`` for the message ``message_id``.
Returns the safe HTML, whether any remote images were blocked, and the
sanitizer version. Pass ``allow_remote_images=True`` after the user approves
a sender to render remote images and CSS backgrounds (SPEC §9.4).
"""
state = {"has_remote": False}
def attribute_filter(tag: str, attr: str, value: str) -> str | None:
if attr == "style":
return _sanitize_style(value, allow_remote_images)
if tag == "a" and attr == "target":
return "_blank"
if tag == "img" and attr == "src":
lowered = value.strip().lower()
if lowered.startswith("cid:"):
content_id = value.strip()[4:]
return f"/api/messages/{message_id}/cid/{content_id}"
if lowered.startswith(("http:", "https:")):
state["has_remote"] = True
return value if allow_remote_images else None # block by default (FR-23)
if lowered.startswith("data:image/"):
return value
return None
return value
cleaned = nh3.clean(
html,
tags=_ALLOWED_TAGS,
attributes=_ALLOWED_ATTRS,
url_schemes=_URL_SCHEMES,
link_rel="noopener noreferrer nofollow",
strip_comments=True,
attribute_filter=attribute_filter,
)
return SanitizeResult(
html=cleaned,
has_remote_images=state["has_remote"],
sanitizer_v=SANITIZER_VERSION,
)