mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-01 00:13:37 +00:00
A meeting invitation puts the join URL in the description - Teams, Meet, Zoom and the plain iTIP fallbacks all do - and the three read-only description renders (invitation RSVP view, event detail view, detail popover) printed it as bare text. The one actionable thing in the event was not clickable: you had to select the URL by hand, across the line wraps, and paste it into the address bar. Render those three through the same pipeline the plain-text mail body already uses (`plainTextToSafeHtml` + `sanitizePlainTextRenderedHtml`), wrapped in a small `LinkifiedText` component. The text stays plain text: it is escaped, only `<a href="http(s)://...">` is emitted, and the result is sanitized again before it reaches the DOM - an event description is attacker-controlled, since anyone can send an iTIP invitation. Two details along the way: `break-words` on the paragraphs, so a long join URL wraps instead of widening the panel, and `whitespace-pre-line` on the invitation view, which was the only one of the three dropping the description's own line breaks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018QPPAw4SK4M9Z7euVPM2cL
34 lines
1.3 KiB
TypeScript
34 lines
1.3 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo } from "react";
|
|
import { plainTextToSafeHtml, sanitizePlainTextRenderedHtml } from "@/lib/email-sanitization";
|
|
|
|
interface LinkifiedTextProps {
|
|
/** Plain text, as stored on the object (no markup is interpreted). */
|
|
text: string;
|
|
className?: string;
|
|
}
|
|
|
|
/**
|
|
* Plain text rendered with its http(s) URLs turned into real links.
|
|
*
|
|
* Meeting invitations carry the join URL in the event description (Teams,
|
|
* Meet, Zoom and the iTIP fallbacks all do), so a text-only render leaves the
|
|
* one actionable thing in the event un-clickable: the reader has to select the
|
|
* URL by hand, and it usually wraps across lines.
|
|
*
|
|
* Same pipeline as a plain-text mail body: `plainTextToSafeHtml` escapes the
|
|
* whole string and emits nothing but `<a href="http(s)://...">`, then
|
|
* `sanitizePlainTextRenderedHtml` re-checks the result. That second pass is
|
|
* defense in depth - unlike message bodies this renders into the main
|
|
* document, not the sandboxed iframe.
|
|
*/
|
|
export function LinkifiedText({ text, className }: LinkifiedTextProps) {
|
|
const html = useMemo(
|
|
() => sanitizePlainTextRenderedHtml(plainTextToSafeHtml(text, "text-primary hover:underline")),
|
|
[text],
|
|
);
|
|
|
|
return <span className={className} dangerouslySetInnerHTML={{ __html: html }} />;
|
|
}
|