Keep a mail's styles from drawing over the app around it

Inline styles survive the sanitiser on purpose: in mail they are the
design. In the reading pane that costs nothing, because the message
is in a sandboxed frame. The quote under a reply and the print page
are the sender's HTML in the app's own document, and there
`position: fixed` on a full-size element covered the composer — or
the whole window — with a page of the sender's choosing the moment
somebody pressed Reply. Reported in #34.

Two layers, since either alone has a hole:

  the sanitiser   drops `position: fixed` and `sticky` from every
                  body, and `absolute` and `z-index` too from one
                  being composed. Absolute stays for mail that is
                  only read: real newsletters use it, and in the
                  frame it can only rearrange the message.
  the stylesheet  the composer's editor and the print page's body
                  are containing blocks that clip. This is what
                  holds for a body stored before the sanitiser
                  changed, or a declaration written some way the
                  pattern does not anticipate.

Refs #34

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Paul LütznerandClaude Opus 5.5 committed 2026-10-09 16:03:51 +02:00
1 parent 98ea3948f4
commit 219fc9e283
5 files changed
+193 -2

No files matched your search

+10 -1
View File
@@ -523,7 +523,16 @@ Variables: form, message, ctx (an App\Domain\DTO\Mail\ComposeContext)
aria-multiline="true"
aria-label="{{ 'compose.body_placeholder'|trans }}"
data-placeholder="{{ 'compose.body_placeholder'|trans }}"
class="w-full min-h-[170px] bg-transparent outline-none
{# [contain:paint] makes the editor the containing block for
anything positioned inside it, and clips to its box.
The quote under a reply is the sender's HTML in the
app's own document: without this, `position: fixed`
in a mail covered the composer — or the whole app —
with content of the sender's choosing the moment
somebody pressed Reply. The sanitiser drops such
declarations too; this is what holds when one gets
past it. #}
class="w-full min-h-[170px] bg-transparent outline-none [contain:paint]
text-sm leading-relaxed
text-ink {{ plainText ? 'hidden' : '' }}
empty:before:content-[attr(data-placeholder)]