From ce8dd42930fc88ef1e975c9a6b4b2a7dabfbb2bc Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Wed, 26 Aug 2026 21:26:48 +0200 Subject: [PATCH] Add HTML signature support to shared mail libraries [no-build] --- html-editor-lib.pl | 69 ++++++++++++++++++++++++++++++++++++---- mailboxes/folders-lib.pl | 31 ++++++++++++------ mailboxes/reply_mail.cgi | 21 +++++++----- 3 files changed, 97 insertions(+), 24 deletions(-) diff --git a/html-editor-lib.pl b/html-editor-lib.pl index 94c85cf5d..09736bcec 100644 --- a/html-editor-lib.pl +++ b/html-editor-lib.pl @@ -242,6 +242,9 @@ my $iframe_styles = $iframe_styles =~ s/\n/ /g; my $navigation_type = $ENV{'HTTP_X_NAVIGATION_TYPE'}; $navigation_type ||= 'reload'; + +# Key for preserving unsaved editor content across page reloads +my $storage_key = $opts->{'storage'} || 'quill=last-message'; my $html_editor_init_script = < @@ -263,6 +266,60 @@ my $html_editor_init_script = qf.whitelist = ["monospace"], Quill.register(qf, true); + // Register signature class attributor, so signature blocks keep their + // marking class across editor round trips, e.g. when editing a draft + const qp = Quill.import('parchment'); + Quill.register(new qp.Attributor.Class('signature', 'x-signature', + { scope: qp.Scope.BLOCK }), true); + + // Convert the signature container into per-block marking classes, + // which the editor preserves via the registered signature attributor. + // Block children are marked directly, while runs of inline content, + // like plain text with line breaks, become marked paragraphs + const signature_marks = function(html) { + if (!html || html.indexOf('x-signature') < 0) { + return html; + } + const doc = document.createElement('div'), + blocks = ['P', 'DIV', 'UL', 'OL', 'TABLE', + 'BLOCKQUOTE', 'PRE', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6']; + doc.innerHTML = html; + doc.querySelectorAll('div.x-signature').forEach(function(sig) { + const frag = document.createDocumentFragment(); + let run = []; + const run_empty = function() { + return !run.some(function(n) { + return n.nodeType === 1 || (n.textContent || '').trim(); + }); + }; + const flush = function() { + if (run.length && !run_empty()) { + const p = document.createElement('p'); + p.className = 'x-signature-mark'; + run.forEach(function(n) { p.appendChild(n); }); + frag.appendChild(p); + } + run = []; + }; + Array.from(sig.childNodes).forEach(function(node) { + if (node.nodeType === 1 && blocks.includes(node.tagName)) { + // Mark block level parts of the signature directly + flush(); + node.classList.add('x-signature-mark'); + frag.appendChild(node); + } else if (node.nodeType === 1 && node.tagName === 'BR') { + // Line break ends the current inline line + flush(); + } else { + run.push(node); + } + }); + flush(); + sig.replaceWith(frag); + }); + return doc.innerHTML; + }; + const editor = new Quill('.ql-container', { modules: { syntax: typeof hljs === 'object', @@ -332,7 +389,7 @@ my $html_editor_init_script = editor.on('text-change', function() { // This should most probably go to onSubmit event targ.value = editor.root.innerHTML + "
"; - sessionStorage.setItem('$module_name/quill=last-message', editor.root.innerHTML); + sessionStorage.setItem('$module_name/$storage_key', editor.root.innerHTML); let extraValue = String(), sync = JSON.parse('@{[&convert_to_json($opts->{'textarea'}->{'sync'}->{'data'})]}'), position = '@{[$opts->{'textarea'}->{'sync'}->{'position'}]}', @@ -386,7 +443,7 @@ my $html_editor_init_script = restore_message = false; } if (restore_message) { - const quill_last_message = sessionStorage.getItem('$module_name/quill=last-message'); + const quill_last_message = sessionStorage.getItem('$module_name/$storage_key'); if (quill_last_message) { editor.pasteHTML(quill_last_message); return; @@ -394,8 +451,8 @@ my $html_editor_init_script = } // Update editor on initial load - editor.pasteHTML(targ.value); - sessionStorage.setItem('$module_name/quill=last-message', editor.root.innerHTML); + editor.pasteHTML(signature_marks(targ.value)); + sessionStorage.setItem('$module_name/$storage_key', editor.root.innerHTML); } @{[$opts->{'load'} ? "fn_${module_name}_html_editor_init()" : '']} @@ -506,13 +563,13 @@ if ($document_styles_string) { } } } - # Fill tags with our inline styles + # Fill tags with our inline styles, preserving existing tag attributes my (@document_styles_tag_names) = $styled_html_email =~ /<(?!style)(?!script)(?!s)(\w+)\s*.*?>/migx; foreach my $tag_name (&unique(@document_styles_tag_names)) { my (%document_styles_tag_names) = $document_styles_string =~ /(\Q$tag_name\E)\s*\{\s*([^}]*?)\s*\}/migx; foreach my $tag (keys %document_styles_tag_names) { my $tag_style = &$style_format($document_styles_tag_names{$tag}); - $styled_html_email =~ s/(<$tag)(?![^>]+style).*?>/$1 style="$tag_style">/mig; + $styled_html_email =~ s/(<$tag\b)(?![^>]+style)([^>]*)>/$1$2 style="$tag_style">/mig; } } } diff --git a/mailboxes/folders-lib.pl b/mailboxes/folders-lib.pl index f897771e0..fa7f12c64 100755 --- a/mailboxes/folders-lib.pl +++ b/mailboxes/folders-lib.pl @@ -3285,6 +3285,14 @@ foreach my $a (@$attach) { return @rv; } +# signature_is_html(sig) +# Returns 1 if the given signature contains HTML markup +sub signature_is_html +{ +local ($sig) = @_; +return 0 if (!$sig); +return $sig =~ /<\/?(?:a|b|i|u|s|p|em|img|br|hr|div|span|font|code|pre|sub|sup|small|big|strong|strike|blockquote|table|tbody|thead|tr|td|th|ul|ol|li|h[1-6]|style)\b[^>]*>/i ? 1 : 0; +} # quoted_message(&mail, quote-mode, sig, 0=any,1=text,2=html, sig-at-top?) # Returns the quoted text, html-flag and body attachment sub quoted_message @@ -3315,20 +3323,22 @@ local $qm = %userconfig ? $userconfig{'html_quote'} : $config{'html_quote'}; if (($cfg->{'html_edit'} == 2 || $cfg->{'html_edit'} == 1 && $htmlbody) && $bodymode != 1) { - # Create quoted body HTML + # Create quoted body HTML, with the signature wrapped in a container + # with a known class so that it can be styled later if needed + $sig =~ s/\n/
\n/g if (!&signature_is_html($sig)); + $sig = "
$sig
" if ($sig); if ($htmlbody) { $body = $htmlbody; - $sig =~ s/\n/
\n/g; if ($qu && $qm == 0) { # Quoted HTML as cite $quote = &html_escape($writer)."\n". "
\n". &safe_html($htmlbody->{'data'}). "
"; - if ($sigtop) { + if ($sig && $sigtop) { $quote = $sig."
\n".$quote; } - else { + elsif ($sig) { $quote = $quote.$sig."
\n"; } } @@ -3341,10 +3351,10 @@ if (($cfg->{'html_edit'} == 2 || else { # Un-quoted HTML $quote = &safe_html($htmlbody->{'data'}); - if ($sigtop) { + if ($sig && $sigtop) { $quote = $sig."
\n".$quote; } - else { + elsif ($sig) { $quote = $quote.$sig."
\n"; } } @@ -3360,10 +3370,10 @@ if (($cfg->{'html_edit'} == 2 || "
\n". "
$pd
". "
"; - if ($sigtop) { + if ($sig && $sigtop) { $quote = $sig."
\n".$quote; } - else { + elsif ($sig) { $quote = $quote.$sig."
\n"; } } @@ -3376,10 +3386,10 @@ if (($cfg->{'html_edit'} == 2 || else { # Un-quoted plain text as HTML $quote = "
$pd
"; - if ($sigtop) { + if ($sig && $sigtop) { $quote = $sig."
\n".$quote; } - else { + elsif ($sig) { $quote = $quote.$sig."
\n"; } } @@ -3388,6 +3398,7 @@ if (($cfg->{'html_edit'} == 2 || } else { # Create quoted body text + $sig = &html_to_text($sig) if (&signature_is_html($sig)); if ($plainbody) { $body = $plainbody; $quote = $plainbody->{'data'}; diff --git a/mailboxes/reply_mail.cgi b/mailboxes/reply_mail.cgi index d4e02a6a3..465ab24ca 100755 --- a/mailboxes/reply_mail.cgi +++ b/mailboxes/reply_mail.cgi @@ -21,10 +21,12 @@ if ($in{'new'}) { } $sig = &get_signature($in{'user'}); if ($html_edit && $sig) { - $sig =~ s/\n/
\n/g; - $quote = "$sig"; + $sig =~ s/\n/
\n/g if (!&signature_is_html($sig)); + $quote = "
$sig
". + ""; } else { + $sig = &html_to_text($sig) if (&signature_is_html($sig)); $quote = "\n\n$sig" if ($sig); } $to = $in{'to'}; @@ -313,8 +315,9 @@ else { $subject = "Fwd: ".$subject if ($subject !~ /^Fwd:/i && ($in{'forward'} || @mailforward)); - # Construct the initial mail text - $sig = &get_signature($in{'user'}); + # Construct the initial mail text, without adding a signature when + # editing an existing draft which already contains one + $sig = &get_signature($in{'user'}) if (!$in{'enew'}); ($quote, $html_edit, $body) = "ed_message($mail, $qu, $sig); # Load images using server in replies $quote = &disable_html_images($quote, 3); @@ -476,10 +479,11 @@ if ($html_edit) { after => { editor => $iframe_quote } }); - $sig =~ s/\n/
/g, - $sig =~ s/^\s+//g, - $sig = "

$sig

" - if ($sig); + if ($sig) { + $sig =~ s/\n/
/g if (!&signature_is_html($sig)); + $sig =~ s/^\s+//g; + $sig = "

$sig


"; + } print &ui_table_row(undef, &ui_textarea("body", $sig, 12, 80, undef, 0, "style='display: none' id=body data-html-mode='$config{'html_edit_mode'}'"). @@ -487,6 +491,7 @@ if ($html_edit) { } else { # Show text editing area + $sig = &html_to_text($sig) if (&signature_is_html($sig)); $wm = $config{'wrap_mode'}; $wm =~ s/^wrap=//g; $wcols = $config{'wrap_compose'};