Ticket #1406: HTML signature support
authorMichael M Slusarz <slusarz@curecanti.org>
Tue, 13 Apr 2010 03:47:26 +0000 (21:47 -0600)
committerMichael M Slusarz <slusarz@curecanti.org>
Tue, 13 Apr 2010 03:48:40 +0000 (21:48 -0600)
13 files changed:
imp/compose-dimp.php
imp/compose.php
imp/config/prefs.php.dist
imp/docs/CHANGES
imp/js/compose-base.js
imp/js/compose-dimp.js
imp/js/signaturehtml.js [new file with mode: 0644]
imp/lib/Ajax/Application.php
imp/lib/Compose.php
imp/lib/Prefs/Identity.php
imp/lib/Prefs/Ui.php
imp/lib/Ui/Compose.php
imp/templates/prefs/signaturehtml.html [new file with mode: 0644]

index e9b51b4..7f44336 100644 (file)
@@ -172,15 +172,13 @@ if ($vars->type == 'redirect') {
     $imp_ui->attachAutoCompleter(array('to', 'cc', 'bcc', 'redirect_to'));
     $imp_ui->attachSpellChecker();
 
-    $sig = $identity->getSignature();
+    $sig = $identity->getSignature($show_editor ? 'html' : 'text');
     if ($get_sig && !empty($sig)) {
-        if ($show_editor) {
-            $sig = '<p><!--begin_signature-->' . $imp_compose->text2html(trim($sig)) . '<!--end_signature--></p>';
+        if ($identity->getValue('sig_first')) {
+            $msg = $sig . $msg;
+        } else {
+            $msg .= $sig;
         }
-
-        $msg = ($identity->getValue('sig_first'))
-            ? "\n" . $sig . $msg
-            : $msg . "\n" . $sig;
     }
 
     if ($show_editor) {
index 974a550..3bdce9f 100644 (file)
@@ -616,24 +616,17 @@ if (empty($msg)) {
     $msg = "\n" . $msg;
 }
 
-/* Get the current signature. */
-$sig = $identity->getSignature();
-
 /* Convert from Text -> HTML or vice versa if RTE mode changed. */
 if (!is_null($oldrtemode) && ($oldrtemode != $rtemode)) {
-    if ($rtemode) {
-        /* Try to find the signature, replace it with a placeholder,
-         * HTML-ize the message, then replace the signature
-         * placeholder with the HTML-ized signature, complete with
-         * marker comment. */
-        $msg = preg_replace('/' . preg_replace('/(?<!^)\s+/', '\\s+', preg_quote($sig, '/')) . '/', '##IMP_SIGNATURE##', $msg, 1);
-        $msg = preg_replace('/\s+##IMP_SIGNATURE##/', '##IMP_SIGNATURE_WS####IMP_SIGNATURE##', $msg);
-        $msg = $imp_compose->text2html($msg);
-        $msg = str_replace(array('##IMP_SIGNATURE_WS##', '##IMP_SIGNATURE##'),
-                           array('<p>&nbsp;</p>', '<p><!--begin_signature-->' . $imp_compose->text2html($sig) . '<!--end_signature--></p>'),
-                           $msg);
-    } else {
-        $msg = Horde_Text_Filter::filter($msg, 'Html2text', array('charset' => Horde_Nls::getCharset(), 'wrap' => false));
+    $msg = $imp_ui->convertComposeText($msg, $rtemode ? 'html' : 'text', $identity->getDefault());
+} elseif ($get_sig) {
+    $sig = $identity->getSignature($rtemode ? 'html' : 'text');
+    if (!empty($sig)) {
+        if ($identity->getValue('sig_first')) {
+            $msg = $sig . $msg;
+        } else {
+            $msg .= "\n" . $sig;
+        }
     }
 }
 
@@ -649,18 +642,6 @@ foreach (array('to', 'cc', 'bcc', 'subject') as $val) {
     }
 }
 
-if ($get_sig && isset($msg) && !empty($sig)) {
-    if ($rtemode) {
-        $sig = '<p>&nbsp;</p><p><!--begin_signature-->' . $imp_compose->text2html(trim($sig)) . '<!--end_signature--></p>';
-    }
-
-    if ($identity->getValue('sig_first')) {
-        $msg = "\n" . $sig . $msg;
-    } else {
-        $msg .= "\n" . $sig;
-    }
-}
-
 /* If PGP encryption is set by default, and we have a recipient list on first
  * load, make sure we have public keys for all recipients. */
 $encrypt_options = $prefs->isLocked('default_encrypt')
index 9507229..8f8b3c2 100644 (file)
@@ -13,8 +13,8 @@ $prefGroups['identities'] = array(
     'desc' => _("Change the name, address, and signature that people see when they read and reply to your email."),
     'members' => array(
         'replyto_addr', 'alias_addr', 'tieto_addr', 'bcc_addr', 'signature',
-        'sig_dashes', 'sig_first', 'save_sent_mail', 'sent_mail_folder',
-        'sentmailselect'
+        'signature_html_select', 'sig_dashes', 'sig_first', 'save_sent_mail',
+        'sent_mail_folder', 'sentmailselect'
     ),
     'type' => 'identities'
 );
@@ -54,6 +54,16 @@ $_prefs['signature'] = array(
     'desc' => _("Your signature:")
 );
 
+// User's HTML signature - UI widget
+$_prefs['signature_html_select'] = array(
+    'type' => 'special'
+);
+
+// User's HTML signature
+$_prefs['signature_html'] = array(
+    'value' => ''
+);
+
 // precede the signature with dashes ('-- ')?
 $_prefs['sig_dashes'] = array(
     'value' => 0,
index 749552b..d306aca 100644 (file)
@@ -2,6 +2,7 @@
 v5.0-git
 --------
 
+[mms] Added HTML signature support (Request #1406).
 [mms] Simplified date sorting display (Ticket #8936).
 [mms] Properly redirect messages pursuant to RFC 5322 [3.6.6].
 [mms] Add redirect message capability to DIMP.
index e8f837c..7ec10f2 100644 (file)
@@ -64,51 +64,49 @@ var IMP_Compose_Base = {
 
     replaceSignature: function(id)
     {
-        var lastsig, msg, nextsig, oldmsg, pos,
-            last = this.getIdentity($F('last_identity')),
+        var lastsig, msg, nextsig, pos, tmp, tmp2,
             next = this.getIdentity(id);
 
         // If the rich text editor is on, we'll use a regexp to find the
         // signature comment and replace its contents.
         if (this.editor_on) {
-            msg = oldmsg = CKEDITOR.instances['composeMessage'].getData().replace(/\r\n/g, '\n');
-
-            lastsig = '<p><!--begin_signature--><!--end_signature--></p>';
-            nextsig = '<p><!--begin_signature-->' + next.sig.replace(/^ ?<br \/>\n/, '').replace(/ +/g, ' ') + '<!--end_signature--></p>';
-
-            // Dot-all functionality achieved with [\s\S], see:
-            // http://simonwillison.net/2004/Sep/20/newlines/
-            msg = msg.replace(/<p>\s*<!--begin_signature-->[\s\S]*?<!--end_signature-->\s*<\/p>/, lastsig);
-            if (msg == oldmsg) {
-                msg = nextsig;
+            // Create a temporary element, import the data from the editor,
+            // search/replace the current imp signature data, and reinsert
+            // into the editor.
+            tmp = new Element('DIV').hide();
+            $(document.body).insert(tmp);
+            tmp.update(CKEDITOR.instances['composeMessage'].getData());
+            tmp2 = tmp.select('DIV.impComposeSignature');
+            if (tmp2.size()) {
+                msg = tmp2.last().update(next.sig);
+            } else {
+                msg = next.id.sig_loc
+                    ? tmp.insert({ top: next.sig })
+                    : tmp.insert({ bottom: next.sig });
             }
+            CKEDITOR.instances['composeMessage'].setData(msg.innerHTML);
+            tmp.remove();
         } else {
             msg = $F('composeMessage').replace(/\r\n/g, '\n');
-
+            last = this.getIdentity($F('last_identity'));
             lastsig = last.sig.replace(/^\n/, '');
             nextsig = next.sig.replace(/^\n/, '');
-        }
-
-        pos = (last.id.sig_loc)
-            ? msg.indexOf(lastsig)
-            : msg.lastIndexOf(lastsig);
 
-        if (pos != -1) {
-            if (next.id.sig_loc == last.id.sig_loc) {
-                msg = msg.substring(0, pos) + nextsig + msg.substring(pos + lastsig.length, msg.length);
-            } else if (next.id.sig_loc) {
-                msg = nextsig + msg.substring(0, pos) + msg.substring(pos + lastsig.length, msg.length);
-            } else {
-                msg = msg.substring(0, pos) + msg.substring(pos + lastsig.length, msg.length) + nextsig;
-            }
+            pos = last.id.sig_loc
+                ? msg.indexOf(lastsig)
+                : msg.lastIndexOf(lastsig);
 
-            msg = msg.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n');
-        }
+            if (pos != -1) {
+                if (next.id.sig_loc == last.id.sig_loc) {
+                    msg = msg.substring(0, pos) + nextsig + msg.substring(pos + lastsig.length, msg.length);
+                } else if (next.id.sig_loc) {
+                    msg = nextsig + msg.substring(0, pos) + msg.substring(pos + lastsig.length, msg.length);
+                } else {
+                    msg = msg.substring(0, pos) + msg.substring(pos + lastsig.length, msg.length) + nextsig;
+                }
 
-        if (this.editor_on) {
-            CKEDITOR.instances['composeMessage'].setData(msg);
-        } else {
-            $('composeMessage').setValue(msg);
+                $('composeMessage').setValue(msg.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n'));
+            }
         }
 
         $('last_identity').setValue(id);
index 3e6005a..f7bb56f 100644 (file)
@@ -337,11 +337,11 @@ var DimpCompose = {
             this.rte.destroy();
 
             this.RTELoading('show');
-            DimpCore.doAction('html2Text', { text: text }, { callback: this.setMessageText.bind(this), ajaxopts: { asynchronous: false } });
+            DimpCore.doAction('html2Text', { identity: $F('identity'), text: text }, { callback: this.setMessageText.bind(this), ajaxopts: { asynchronous: false } });
             this.RTELoading('hide');
         } else {
             if (!noupdate) {
-                DimpCore.doAction('text2Html', { text: $F('composeMessage') }, { callback: this.setMessageText.bind(this), ajaxopts: { asynchronous: false } });
+                DimpCore.doAction('text2Html', { identity: $F('identity'), text: $F('composeMessage') }, { callback: this.setMessageText.bind(this), ajaxopts: { asynchronous: false } });
             }
 
             config = Object.clone(IMP.ckeditor_config);
diff --git a/imp/js/signaturehtml.js b/imp/js/signaturehtml.js
new file mode 100644 (file)
index 0000000..b99e622
--- /dev/null
@@ -0,0 +1,28 @@
+/**
+ * Provides the javascript for managing HTML signature in the preferences UI.
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ */
+
+var ImpHtmlSignaturePrefs = {
+
+    // Variables defined by other code: ready, sigs
+
+    changeIdentity: function(e)
+    {
+        switch (e.memo.pref) {
+        case 'signature_html_select':
+            if (this.ready) {
+                CKEDITOR.instances['signature_html'].setData(this.sigs[e.memo.i]);
+            } else {
+                this.changeIdentity.bind(this, e).defer();
+            }
+            break;
+        }
+    }
+
+};
+
+CKEDITOR.on('instanceReady', function(e) { ImpHtmlSignaturePrefs.ready = true; });
+document.observe('HordeIdentitySelect:change', ImpHtmlSignaturePrefs.changeIdentity.bindAsEventListener(ImpHtmlSignaturePrefs));
index 552e1b1..d1211b0 100644 (file)
@@ -866,10 +866,11 @@ class IMP_Ajax_Application extends Horde_Ajax_Application_Base
     }
 
     /**
-     * AJAX action: Convert HTML to text.
+     * AJAX action: Convert HTML to text (compose data).
      *
      * Variables used:
      * <pre>
+     * 'identity' - (integer) The current identity.
      * 'text' - (string) The text to convert.
      * </pre>
      *
@@ -881,18 +882,17 @@ class IMP_Ajax_Application extends Horde_Ajax_Application_Base
     public function html2Text()
     {
         $result = new stdClass;
-        // Need to replace line endings or else IE won't display line endings
-        // properly.
-        $result->text = str_replace("\n", "\r\n", Horde_Text_Filter::filter($this->_vars->text, 'Html2text', array('charset' => Horde_Nls::getCharset())));
+        $result->text = $GLOBALS['injector']->getInstance('IMP_Ui_Compose')->convertComposeText($this->_vars->text, 'text', intval($this->_vars->identity));
 
         return $result;
     }
 
     /**
-     * AJAX action: Convert text to HTML.
+     * AJAX action: Convert text to HTML (compose data).
      *
      * Variables used:
      * <pre>
+     * 'identity' - (integer) The current identity.
      * 'text' - (string) The text to convert.
      * </pre>
      *
@@ -904,7 +904,7 @@ class IMP_Ajax_Application extends Horde_Ajax_Application_Base
     public function text2Html()
     {
         $result = new stdClass;
-        $result->text = Horde_Text_Filter::filter($this->_vars->text, 'text2html', array('parselevel' => Horde_Text_Filter_Text2html::MICRO_LINKURL));
+        $result->text = $GLOBALS['injector']->getInstance('IMP_Ui_Compose')->convertComposeText($this->_vars->text, 'html', intval($this->_vars->identity));
 
         return $result;
     }
index 49b4760..097aec1 100644 (file)
@@ -2637,7 +2637,7 @@ class IMP_Compose
      *
      * @return string  HTML text.
      */
-    public function text2html($msg)
+    static public function text2html($msg)
     {
         return Horde_Text_Filter::filter($msg, 'text2html', array('parselevel' => Horde_Text_Filter_Text2html::MICRO_LINKURL, 'class' => null, 'callback' => null));
     }
index eb571dd..c6281fa 100644 (file)
@@ -26,28 +26,25 @@ class Imp_Prefs_Identity extends Horde_Prefs_Identity
         'froms' => array(),
         'names' => array(),
         // 'own_addresses'
+        'signatures' => array()
         // 'tie_addresses'
     );
 
     /**
-     * Cached signature list.
-     *
-     * @var array
-     */
-    protected $_signatures = array();
-
-    /**
      * Reads all the user's identities from the prefs object or builds
      * a new identity from the standard values given in prefs.php.
      */
     public function __construct()
     {
         parent::__construct();
+
         $this->_properties = array_merge(
             $this->_properties,
-            array('replyto_addr', 'alias_addr', 'tieto_addr', 'bcc_addr',
-                  'signature', 'sig_first', 'sig_dashes', 'save_sent_mail',
-                  'sent_mail_folder')
+            array(
+                'replyto_addr', 'alias_addr', 'tieto_addr', 'bcc_addr',
+                'signature', 'signature_html', 'sig_first', 'sig_dashes',
+                'save_sent_mail', 'sent_mail_folder'
+            )
         );
     }
 
@@ -404,50 +401,77 @@ class Imp_Prefs_Identity extends Horde_Prefs_Identity
      */
     public function getFullname($ident = null)
     {
-        if (isset($this->_names[$ident])) {
-            return $this->_names[$ident];
+        if (isset($this->_cached['names'][$ident])) {
+            return $this->_cached['names'][$ident];
         }
 
-        $this->_names[$ident] = $this->getValue('fullname', $ident);
+        $this->_cached['names'][$ident] = $this->getValue('fullname', $ident);
 
-        return $this->_names[$ident];
+        return $this->_cached['names'][$ident];
     }
 
     /**
      * Returns the full signature based on the current settings for the
      * signature itself, the dashes and the position.
      *
+     * @param string $type    Either 'text' or 'html'.
      * @param integer $ident  The identity to retrieve the signature from.
      *
      * @return string  The full signature.
      * @throws Horde_Exception
      */
-    public function getSignature($ident = null)
+    public function getSignature($type = 'text', $ident = null)
     {
-        if (isset($this->_signatures[$ident])) {
-            return $this->_signatures[$ident];
+        $convert = false;
+        $key = $ident . '|' . $type;
+        $val = null;
+
+        if (isset($this->_cached['signatures'][$key])) {
+            return $this->_cached['signatures'][$key];
         }
 
-        $val = $this->getValue('signature', $ident);
-        if (!empty($val)) {
-            $sig_first = $this->getValue('sig_first', $ident);
-            $sig_dashes = $this->getValue('sig_dashes', $ident);
-            $val = str_replace("\r\n", "\n", $val);
-            if ($sig_dashes) {
-                $val = "-- \n$val";
+        if ($type == 'html') {
+            $val = $this->getValue('signature_html', $ident);
+            if (!strlen($val)) {
+                $convert = true;
+                $val = null;
             }
-            if (isset($sig_first) && $sig_first) {
-                $val = "\n" . $val . "\n\n\n";
-            } else {
-                $val = "\n" . $val;
+        }
+
+        if (is_null($val)) {
+            $val = $this->getValue('signature', $ident);
+
+            if (!empty($val) && ($type == 'text')) {
+                $sig_first = $this->getValue('sig_first', $ident);
+                $sig_dashes = $this->getValue('sig_dashes', $ident);
+
+                $val = str_replace("\r\n", "\n", $val);
+
+                if ($sig_dashes) {
+                    $val = "-- \n" . $val . "\n";
+                } else {
+                    $val = "\n" . $val;
+                }
+
+                if ($sig_first) {
+                    $val .= "\n\n\n";
+                }
+            }
+        }
+
+        if ($val && ($type == 'html')) {
+            if ($convert) {
+                $val = IMP_Compose::text2html(trim($val));
             }
+
+            $val = '<div class="impComposeSignature">' . $val . '</div>';
         }
 
         try {
             $val = Horde::callHook('prefs_hook_signature', array($val), 'imp');
         } catch (Horde_Exception_HookNotSet $e) {}
 
-        $this->_signatures[$ident] = $val;
+        $this->_cached['signatures'][$key] = $val;
 
         return $val;
     }
@@ -455,18 +479,14 @@ class Imp_Prefs_Identity extends Horde_Prefs_Identity
     /**
      * Returns an array with the signatures from all identities
      *
+     * @param string $type  Either 'text' or 'html'.
+     *
      * @return array  The array with all the signatures.
      */
-    public function getAllSignatures()
+    public function getAllSignatures($type = 'text')
     {
-        static $list;
-
-        if (isset($list)) {
-            return $list;
-        }
-
         foreach ($this->_identities as $key => $identity) {
-            $list[$key] = $this->getSignature($key);
+            $list[$key] = $this->getSignature($type, $key);
         }
 
         return $list;
index 04c97b9..06d6caf 100644 (file)
@@ -121,6 +121,14 @@ class IMP_Prefs_Ui
             } else {
                 Horde::addScriptFile('folderprefs.js', 'imp');
             }
+
+            if ($prefs->isLocked('signature_html') ||
+                empty($_SESSION['imp']['rteavail'])) {
+                $ui->suppress[] = 'signature_html_select';
+            } else {
+                Horde::addScriptFile('signaturehtml.js', 'imp');
+                $GLOBALS['injector']->getInstance('Horde_Editor')->getEditor('Ckeditor', array('id' => 'signature_html'));
+            }
             break;
 
         case 'logintasks':
@@ -309,6 +317,9 @@ class IMP_Prefs_Ui
         case 'smimepublickey':
             return $this->_smimePublicKey($ui);
 
+        case 'signature_html_select':
+            return $this->_signatureHtml();
+
         case 'soundselect':
             return $this->_sound();
 
@@ -385,6 +396,9 @@ class IMP_Prefs_Ui
             $this->_updateSmimePublicKey($ui);
             return false;
 
+        case 'signature_html_select':
+            return Horde_Prefs_Identity::singleton(array('imp', 'imp'))->setValue('signature_html', $ui->vars->signature_html);
+
         case 'soundselect':
             return $prefs->setValue('nav_audio', $ui->vars->nav_audio);
 
@@ -1319,6 +1333,32 @@ class IMP_Prefs_Ui
         }
     }
 
+    /* HTML Signature editing. */
+
+    /**
+     * Create code for HTML Signature editing.
+     *
+     * @return string  HTML UI code.
+     */
+    protected function _signatureHtml()
+    {
+        $identity = Horde_Prefs_Identity::singleton(array('imp', 'imp'));
+
+        $js = array();
+        foreach (array_keys($identity->getAll('id')) as $key) {
+            $js[$key] = $identity->getValue('signature_html', $key);
+        };
+
+        Horde::addInlineScript(array(
+            'ImpHtmlSignaturePrefs.sigs = ' . Horde_Serialize::serialize($js, Horde_Serialize::JSON, Horde_Nls::getCharset())
+        ));
+
+        $t = $GLOBALS['injector']->createInstance('Horde_Template');
+        $t->setOption('gettext', true);
+
+        return $t->fetch(IMP_TEMPLATES . '/prefs/signaturehtml.html');
+    }
+
     /* Sound selection. */
 
     /**
index f7c6da3..d9ae6e1 100644 (file)
@@ -273,12 +273,14 @@ class IMP_Ui_Compose
         $identities = array();
         $identity = Horde_Prefs_Identity::singleton(array('imp', 'imp'));
 
+        $html_sigs = $identity->getAllSignatures('html');
+
         foreach ($identity->getAllSignatures() as $ident => $sig) {
             $identities[] = array(
                 // Plain text signature
                 'sig' => $sig,
                 // HTML signature
-                'sig_html' => str_replace(' target="_blank"', '', Horde_Text_Filter::filter($sig, 'text2html', array('parselevel' => Horde_Text_Filter_Text2html::MICRO_LINKURL, 'class' => null, 'callback' => null))),
+                'sig_html' => $html_sigs[$ident],
                 // Signature location
                 'sig_loc' => (bool)$identity->getValue('sig_first', $ident),
                 // Sent mail folder name
@@ -295,5 +297,67 @@ class IMP_Ui_Compose
         return 'IMP_Compose_Base.identities = ' . Horde_Serialize::serialize($identities, Horde_Serialize::JSON);
     }
 
+    /**
+     * Convert compose data to/from text/HTML.
+     *
+     * @param string $data       The message text.
+     * @param string $to         Either 'text' or 'html'.
+     * @param integer $identity  The current identity.
+     *
+     * @return string  The converted text
+     */
+    public function convertComposeText($data, $to, $identity)
+    {
+        $imp_identity = Horde_Prefs_Identity::singleton(array('imp', 'imp'));
+        $replaced = 0;
+
+        $html_sig = $imp_identity->getSignature('html', $identity);
+        $txt_sig = $imp_identity->getSignature('text', $identity);
+
+        /* Try to find the signature, replace it with a placeholder, convert
+         * the message, and then re-add the signature in the new format. */
+        switch ($to) {
+        case 'html':
+            if ($txt_sig) {
+                $data = preg_replace('/' . preg_replace('/(?<!^)\s+/', '\\s+', preg_quote($txt_sig, '/')) . '/', '###IMP_SIGNATURE###', $data, 1, $replaced);
+            }
+            $data = IMP_Compose::text2html($data);
+            $sig = $html_sig;
+            break;
+
+        case 'text':
+            if ($html_sig) {
+                /* Silence errors from parsing HTML. */
+                $old_error = libxml_use_internal_errors(true);
+                $doc = DOMDocument::loadHTML($data);
+                if (!$old_error) {
+                    libxml_use_internal_errors(false);
+                }
+
+                $xpath = new DOMXPath($doc);
+                $entries = $xpath->query("//div[@class='impComposeSignature']");
+                $node = $entries->item(0);
+                $node->parentNode->replaceChild($doc->createTextNode('###IMP_SIGNATURE###'), $node);
+                $replaced = 1;
+
+                $data = '';
+                foreach ($doc->getElementsByTagName('body')->item(0)->childNodes as $node) {
+                    $data .= $doc->saveXML($node);
+                }
+            }
+
+            $data = Horde_Text_Filter::filter($data, 'Html2text', array('charset' => Horde_Nls::getCharset(), 'wrap' => false));
+            $sig = $txt_sig;
+            break;
+        }
+
+        if ($replaced) {
+            return str_replace('###IMP_SIGNATURE###', $sig, $data);
+        } elseif ($imp_identity->getValue('sig_first', $identity)) {
+            return $sig . $data;
+        } else {
+            return $msg . "\n" . $sig;
+        }
+    }
 
 }
diff --git a/imp/templates/prefs/signaturehtml.html b/imp/templates/prefs/signaturehtml.html
new file mode 100644 (file)
index 0000000..37fb8e8
--- /dev/null
@@ -0,0 +1,7 @@
+<div>
+ <gettext>Your signature to use when composing with the HTML editor (if empty, the text signature will be used):</gettext>
+</div>
+
+<div class="fixed">
+ <textarea id="signature_html" name="signature_html" rows="4" cols="80" class="fixed"></textarea>
+</div>