267:async def preview_attachment(att_id: int, user: User = Depends(get_current_user),
268-                             db: AsyncSession = Depends(get_db)):
269-    """Preview: images and PDFs return base64 data URI; text/html return content."""
270-    att = await _get_own(db, user, att_id)
271-
272-    if att.is_html_attachment:
273-        body = att.html_attachment_body or ""
274:        return {"previewable": True, "kind": "html",
275:                "content": sanitize_preview(body)[:50000]}
276-
277-    content = await asyncio.get_event_loop().run_in_executor(
278-        None, lambda: att_svc.read_attachment_bytes(att) or b"")
279-    if not content:
280:        return {"previewable": False, "reason": "no_content"}
281-    if len(content) > 2 * 1024 * 1024:
282:        return {"previewable": False, "reason": "too_large", "size": len(content)}
283-    if att.kind in ("image", "pdf"):
284-        mime = att.mime_type or ("application/pdf" if att.kind == "pdf" else "image/png")
285:        return {"previewable": True, "kind": att.kind,
286-                "data_uri": f"data:{mime};base64,{base64.b64encode(content).decode()}"}
287-    if att.kind in ("text", "html", "file"):
288-        try:
289-            text = content.decode("utf-8")
290-        except UnicodeDecodeError:
291:            return {"previewable": False, "reason": "binary"}
292-        if att.mime_type == "text/html":
