{event.image?.url ? (
, whose own
+ // fade-in animation creates a CSS stacking context that would otherwise
+ // trap this fixed overlay below the mobile tab bar.
+ return createPortal(
This message links to images hosted elsewhere. Loading them tells
the sender you opened it.
@@ -263,13 +310,23 @@ function MessageView({
)}
{/* Sandboxed: the body is untrusted even after sanitising. srcdoc rather
- than src so the app's own X-Frame-Options cannot block it. */}
+ than src so the app's own X-Frame-Options cannot block it.
+ allow-popups (+ allow-popups-to-escape-sandbox so the opened tab
+ isn't itself sandboxed) lets target="_blank" links in the body
+ actually open. allow-same-origin lets the parent measure the
+ document so the iframe can be sized to its content and scroll as
+ one page with the header above it, instead of being squeezed into
+ whatever space was left over; there's still no allow-scripts, so
+ nothing inside the body can ever run regardless of origin. */}
);
diff --git a/app/admin/mail/page.tsx b/app/admin/mail/page.tsx
index 42b121ee..75eebf81 100644
--- a/app/admin/mail/page.tsx
+++ b/app/admin/mail/page.tsx
@@ -2,7 +2,18 @@
import { Suspense, useCallback, useEffect, useRef, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
-import { ArrowLeft, Eye, MailOpen, Search, Star } from "lucide-react";
+import {
+ Archive,
+ ArrowLeft,
+ Eye,
+ Forward as ForwardIcon,
+ MailOpen,
+ Reply as ReplyIcon,
+ ReplyAll as ReplyAllIcon,
+ Search,
+ Star,
+ Trash2,
+} from "lucide-react";
import { logout } from "@/lib/api";
import type { MailDeletionRequest } from "@/lib/api/types";
import type { Mailbox, MessageSummary } from "@/lib/mail/jmap-mail";
@@ -937,22 +948,32 @@ function MessageActions({
<>
{replyAll && (
)}
-