diff --git a/ajax.php b/ajax.php
index 8ea5226439f971b9cd917a13f53c099579394ed1..80e3a7838693ca355859285019aa861043b0b057 100644
--- a/ajax.php
+++ b/ajax.php
@@ -33,6 +33,7 @@ $dispatcher = patterns('',
         url_post('^(?P<id>\d+)$', 'updateDraftClient'),
         url_delete('^(?P<id>\d+)$', 'deleteDraftClient'),
         url_post('^(?P<id>\d+)/attach$', 'uploadInlineImageClient'),
+        url_post('^(?P<namespace>[\w.]+)/attach$', 'uploadInlineImageEarlyClient'),
         url_get('^(?P<namespace>[\w.]+)$', 'getDraftClient'),
         url_post('^(?P<namespace>[\w.]+)$', 'createDraftClient')
     )),
diff --git a/include/ajax.config.php b/include/ajax.config.php
index 733fa014d2c18af5e09d92c5b3ba497561f1b8ca..d2547d98c4abd967837d9a49073ff29b64bc5d32 100644
--- a/include/ajax.config.php
+++ b/include/ajax.config.php
@@ -13,6 +13,7 @@
 
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
+require_once INCLUDE_DIR . 'class.ajax.php';
 
 if(!defined('INCLUDE_DIR')) die('!');
 
@@ -42,7 +43,7 @@ class ConfigAjaxAPI extends AjaxController {
         return $this->json_encode($config);
     }
 
-    function client() {
+    function client($headers=true) {
         global $cfg;
 
         $lang = Internationalization::getCurrentLanguage();
@@ -62,8 +63,10 @@ class ConfigAjaxAPI extends AjaxController {
         );
 
         $config = $this->json_encode($config);
-        Http::cacheable(md5($config), $cfg->lastModified());
-        header('Content-Type: application/json; charset=UTF-8');
+        if ($headers) {
+            Http::cacheable(md5($config), $cfg->lastModified());
+            header('Content-Type: application/json; charset=UTF-8');
+        }
 
         return $config;
     }
diff --git a/include/ajax.draft.php b/include/ajax.draft.php
index f727e2cc9a10040786f6abb3502a8a21f7e04806..5fc24e77a9c5e08d3e8607a6613c4e1a7fdaec6b 100644
--- a/include/ajax.draft.php
+++ b/include/ajax.draft.php
@@ -7,58 +7,40 @@ require_once(INCLUDE_DIR.'class.draft.php');
 class DraftAjaxAPI extends AjaxController {
 
     function _createDraft($vars) {
-        $field_list = array('response', 'note', 'answer', 'body',
-             'message', 'issue');
-        foreach ($field_list as $field) {
-            if (isset($_POST[$field])) {
-                $vars['body'] = urldecode($_POST[$field]);
-                break;
-            }
-        }
-        if (!isset($vars['body']))
-            return Http::response(422, "Draft body not found in request");
-
-        $errors = array();
-        if (!($draft = Draft::create($vars, $errors)))
-            Http::response(500, print_r($errors, true));
-
-        // If the draft is created from an existing document, ensure inline
-        // attachments from the cloned document are attachned to the draft
-        // XXX: Actually, I think this is just wasting time, because the
-        //     other object already has the items attached, so the database
-        //     won't clean up the files. They don't have to be attached to
-        //     the draft for Draft::getAttachmentIds to return the id of the
-        //     attached file
-        //$draft->syncExistingAttachments();
+        if (false === ($vars['body'] = self::_findDraftBody($_POST)))
+            return JsonDataEncoder::encode(array(
+                'error' => __("Draft body not found in request"),
+                'code' => 422,
+                ));
+
+        if (!($draft = Draft::create($vars)) || !$draft->save())
+            Http::response(500, 'Unable to create draft');
 
         echo JsonDataEncoder::encode(array(
             'draft_id' => $draft->getId(),
         ));
     }
 
-    function _getDraft($id) {
-        if (!($draft = Draft::lookup($id)))
+    function _getDraft($draft) {
+        if (!$draft || !$draft instanceof Draft)
             Http::response(205, "Draft not found. Create one first");
 
         $body = Format::viewableImages($draft->getBody());
 
         echo JsonDataEncoder::encode(array(
             'body' => $body,
-            'draft_id' => (int)$id,
+            'draft_id' => $draft->getId(),
         ));
     }
 
     function _updateDraft($draft) {
-        $field_list = array('response', 'note', 'answer', 'body',
-             'message', 'issue');
-        foreach ($field_list as $field) {
-            if (isset($_POST[$field])) {
-                $body = urldecode($_POST[$field]);
-                break;
-            }
-        }
-        if (!isset($body))
-            return Http::response(422, "Draft body not found in request");
+        if (false === ($body = self::_findDraftBody($_POST)))
+            return JsonDataEncoder::encode(array(
+                'error' => array(
+                    'message' => "Draft body not found in request",
+                    'code' => 422,
+                )
+            ));
 
         if (!$draft->setBody($body))
             return Http::response(500, "Unable to update draft body");
@@ -129,6 +111,8 @@ class DraftAjaxAPI extends AjaxController {
 
         echo JsonDataEncoder::encode(array(
             'content_id' => 'cid:'.$f->getKey(),
+            // Return draft_id to connect the auto draft creation
+            'draft_id' => $draft->getId(),
             'filelink' => sprintf('image.php?h=%s', $f->getDownloadHash())
         ));
     }
@@ -141,30 +125,36 @@ class DraftAjaxAPI extends AjaxController {
             Http::response(403, "Valid session required");
 
         $vars = array(
-            'staff_id' => ($thisclient) ? $thisclient->getId() : 0,
+            'staff_id' => ($thisclient) ? $thisclient->getId() : 1<<31,
             'namespace' => $namespace,
         );
 
-        $info = self::_createDraft($vars);
-        $info['draft_id'] = $namespace;
+        return self::_createDraft($vars);
     }
 
     function getDraftClient($namespace) {
         global $thisclient;
 
         if ($thisclient) {
-            if (!($id = Draft::findByNamespaceAndStaff($namespace,
-                    $thisclient->getId())))
+            try {
+                $draft = Draft::lookupByNamespaceAndStaff($namespace,
+                    $thisclient->getId());
+            }
+            catch (DoesNotExist $e) {
                 Http::response(205, "Draft not found. Create one first");
+            }
         }
         else {
             if (substr($namespace, -12) != substr(session_id(), -12))
                 Http::response(404, "Draft not found");
-            elseif (!($id = Draft::findByNamespaceAndStaff($namespace, 0)))
+            try {
+                $draft = Draft::lookupByNamespaceAndStaff($namespace, 0);
+            }
+            catch (DoesNotExist $e) {
                 Http::response(205, "Draft not found. Create one first");
+            }
         }
-
-        return self::_getDraft($id);
+        return self::_getDraft($draft);
     }
 
     function updateDraftClient($id) {
@@ -220,6 +210,22 @@ class DraftAjaxAPI extends AjaxController {
         return self::_uploadInlineImage($draft);
     }
 
+    function uploadInlineImageEarlyClient($namespace) {
+        global $thisclient;
+
+        if (!$thisclient && substr($namespace, -12) != substr(session_id(), -12))
+            Http::response(403, "Valid session required");
+
+        $draft = Draft::create(array(
+            'staff_id' => ($thisclient) ? $thisclient->getId() : 1<<31,
+            'namespace' => $namespace,
+        ));
+        if (!$draft->save())
+            Http::response(500, 'Unable to create draft');
+
+        return $this->uploadInlineImageClient($draft->getId());
+    }
+
     // Staff interface for drafts ========================================
     function createDraft($namespace) {
         global $thisstaff;
@@ -240,11 +246,15 @@ class DraftAjaxAPI extends AjaxController {
 
         if (!$thisstaff)
             Http::response(403, "Login required for draft creation");
-        elseif (!($id = Draft::findByNamespaceAndStaff($namespace,
-                $thisstaff->getId())))
+        try {
+            $draft = Draft::lookupByNamespaceAndStaff($namespace,
+                $thisstaff->getId());
+        }
+        catch (DoesNotExist $e) {
             Http::response(205, "Draft not found. Create one first");
+        }
 
-        return self::_getDraft($id);
+        return self::_getDraft($draft);
     }
 
     function updateDraft($id) {
@@ -273,6 +283,22 @@ class DraftAjaxAPI extends AjaxController {
         return self::_uploadInlineImage($draft);
     }
 
+    function uploadInlineImageEarly($namespace) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, "Login required for image upload");
+
+        $draft = Draft::create(array(
+            'staff_id' => $thisstaff->getId(),
+            'namespace' => $namepace
+        ));
+        if (!$draft->save())
+            Http::response(500, 'Unable to create draft');
+
+        return $this->uploadInlineImage($draft->getId());
+    }
+
     function deleteDraft($id) {
         global $thisstaff;
 
@@ -320,5 +346,27 @@ class DraftAjaxAPI extends AjaxController {
         echo JsonDataEncoder::encode($files);
     }
 
+    function _findDraftBody($vars) {
+        if (isset($vars['name'])) {
+            $parts = array();
+            if (preg_match('`(\w+)(?:\[(\w+)\])?(?:\[(\w+)\])?`', $_POST['name'], $parts)) {
+                array_shift($parts);
+                $focus = $vars;
+                foreach ($parts as $p)
+                    $focus = $focus[$p];
+                return urldecode($focus);
+            }
+        }
+        $field_list = array('response', 'note', 'answer', 'body',
+             'message', 'issue');
+        foreach ($field_list as $field) {
+            if (isset($vars[$field])) {
+                return urldecode($vars[$field]);
+            }
+        }
+
+        return false;
+    }
+
 }
 ?>
diff --git a/include/class.draft.php b/include/class.draft.php
index cdb5d43f440081d3e047993dd0df006cf23c39d6..fae8fd8d72389d6c37f2dc2892e564a99f246dec 100644
--- a/include/class.draft.php
+++ b/include/class.draft.php
@@ -1,28 +1,59 @@
 <?php
 
-class Draft {
-
-    var $id;
-    var $ht;
-
-    var $_attachments;
-
-    function Draft($id) {
-        $this->id = $id;
-        $this->load();
-    }
-
-    function load() {
-        $this->attachments = new GenericAttachments($this->id, 'D');
-        $sql = 'SELECT * FROM '.DRAFT_TABLE.' WHERE id='.db_input($this->id);
-        return (($res = db_query($sql))
-            && ($this->ht = db_fetch_array($res)));
+/**
+ * Class: Draft
+ *
+ * Defines a simple draft-saving mechanism for osTicket which supports draft
+ * fetch and update via an ajax mechanism (include/ajax.draft.php).
+ *
+ * Fields:
+ * id - (int:auto:pk) Draft ID number
+ * body - (text) Body of the draft
+ * namespace - (string) Identifier of draft grouping — useful for multiple
+ *      drafts on the same document by different users
+ * staff_id - (int:null) Staff owner of the draft
+ * extra - (text:json) Extra attributes of the draft
+ * created - (date) Date draft was initially created
+ * updated - (date:null) Date draft was last updated
+ */
+class Draft extends VerySimpleModel {
+
+    static $meta = array(
+        'table' => DRAFT_TABLE,
+        'pk' => array('id'),
+    );
+
+    var $attachments;
+
+    function __construct() {
+        call_user_func_array(array('parent', '__construct'), func_get_args());
+        if (isset($this->id))
+            $this->attachments = new GenericAttachments($this->id, 'D');
     }
 
     function getId() { return $this->id; }
-    function getBody() { return $this->ht['body']; }
-    function getStaffId() { return $this->ht['staff_id']; }
-    function getNamespace() { return $this->ht['namespace']; }
+    function getBody() { return $this->body; }
+    function getStaffId() { return $this->staff_id; }
+    function getNamespace() { return $this->namespace; }
+
+    static function getDraftAndDataAttrs($namespace, $id=0, $original='') {
+        $draft_body = null;
+        $attrs = array(sprintf('data-draft-namespace="%s"', Format::htmlchars($namespace)));
+        $criteria = array('namespace'=>$namespace);
+        if ($id) {
+            $attrs[] = sprintf('data-draft-object-id="%s"', Format::htmlchars($id));
+            $criteria['namespace'] .= '.' . $id;
+        }
+        if ($draft = static::lookup($criteria)) {
+            $attrs[] = sprintf('data-draft-id="%s"', $draft->getId());
+            $draft_body = $draft->getBody();
+        }
+        $attrs[] = sprintf('data-draft-original="%s"',
+            Format::htmlchars(Format::viewableImages($original)));
+
+        return array(Format::htmlchars(Format::viewableImages($draft_body)),
+            implode(' ', $attrs));
+    }
 
     function getAttachmentIds($body=false) {
         $attachments = array();
@@ -63,69 +94,49 @@ class Draft {
     function setBody($body) {
         // Change image.php urls back to content-id's
         $body = Format::sanitize($body, false);
-        $this->ht['body'] = $body;
 
-        $sql='UPDATE '.DRAFT_TABLE.' SET updated=NOW()'
-            .',body='.db_input($body)
-            .' WHERE id='.db_input($this->getId());
-        return db_query($sql) && db_affected_rows() == 1;
+        $this->body = $body ?: ' ';
+        $this->updated = SqlFunction::NOW();
+        return $this->save();
     }
 
     function delete() {
         $this->attachments->deleteAll();
-        $sql = 'DELETE FROM '.DRAFT_TABLE
-            .' WHERE id='.db_input($this->getId());
-        return (db_query($sql) && db_affected_rows() == 1);
+        return parent::delete();
     }
 
-    function save($id, $vars, &$errors) {
+    function isValid() {
         // Required fields
-        if (!$vars['namespace'] || !isset($vars['body']) || !isset($vars['staff_id']))
-            return false;
+        return $this->namespace && isset($this->staff_id);
+    }
 
-        $sql = ' SET `namespace`='.db_input($vars['namespace'])
-            .' ,body='.db_input(Format::sanitize($vars['body'], false))
-            .' ,staff_id='.db_input($vars['staff_id']);
+    function save($refetch=false) {
+        if (!$this->isValid())
+            return false;
 
-        if (!$id) {
-            $sql = 'INSERT INTO '.DRAFT_TABLE.$sql
-                .' ,created=NOW()';
-            if(!db_query($sql) || !($draft=self::lookup(db_insert_id())))
-                return false;
+        return parent::save($refetch);
+    }
 
-            // Cloned attachments...
-            if($vars['attachments'] && is_array($vars['attachments']))
-                $draft->attachments->upload($vars['attachments'], true);
+    static function create($vars) {
+        $attachments = @$vars['attachments'];
+        unset($vars['attachments']);
 
-            return $draft;
-        }
-        else {
-            $sql = 'UPDATE '.DRAFT_TABLE.$sql
-                .' WHERE id='.db_input($id);
-            if (db_query($sql) && db_affected_rows() == 1)
-                return $this;
-        }
-    }
+        $vars['created'] = SqlFunction::NOW();
+        $draft = parent::create($vars);
 
-    function create($vars, &$errors) {
-        return self::save(0, $vars, $errors);
-    }
+        // Cloned attachments ...
+        if (false && $attachments && is_array($attachments))
+            // XXX: This won't work until the draft is saved
+            $draft->attachments->upload($attachments, true);
 
-    function lookup($id) {
-        return ($id && is_numeric($id)
-                && ($d = new Draft($id))
-                && $d->getId()==$id
-                ) ? $d : null;
+        return $draft;
     }
 
-    function findByNamespaceAndStaff($namespace, $staff_id) {
-        $sql = 'SELECT id FROM '.DRAFT_TABLE
-            .' WHERE `namespace`='.db_input($namespace)
-            .' AND staff_id='.db_input($staff_id);
-        if (($res = db_query($sql)) && (list($id) = db_fetch_row($res)))
-            return $id;
-        else
-            return false;
+    static function lookupByNamespaceAndStaff($namespace, $staff_id) {
+        return static::lookup(array(
+            'namespace'=>$namespace,
+            'staff_id'=>$staff_id
+        ));
     }
 
     /**
@@ -134,8 +145,7 @@ class Draft {
      * closing a ticket, the staff_id should be left null so that all drafts
      * are cleaned up.
      */
-    /* static */
-    function deleteForNamespace($namespace, $staff_id=false) {
+    static function deleteForNamespace($namespace, $staff_id=false) {
         $sql = 'DELETE attach FROM '.ATTACHMENT_TABLE.' attach
                 INNER JOIN '.DRAFT_TABLE.' draft
                 ON (attach.object_id = draft.id AND attach.`type`=\'D\')
@@ -145,11 +155,10 @@ class Draft {
         if (!db_query($sql))
             return false;
 
-        $sql = 'DELETE FROM '.DRAFT_TABLE
-             .' WHERE `namespace` LIKE '.db_input($namespace);
+        $criteria = array('namespace__like'=>$namespace);
         if ($staff_id)
-            $sql .= ' AND staff_id='.db_input($staff_id);
-        return (!db_query($sql) || !db_affected_rows());
+            $criteria['staff_id'] = $staff_id;
+        return static::objects()->filter($criteria)->delete();
     }
 
     static function cleanup() {
diff --git a/include/class.forms.php b/include/class.forms.php
index c80c34ba1567cea62f7cb38150e11b020ca39228..73afeaa0eca493abcfe50d8d415af01bbd03f89d 100644
--- a/include/class.forms.php
+++ b/include/class.forms.php
@@ -2014,20 +2014,25 @@ class ThreadEntryWidget extends Widget {
     function render($client=null) {
         global $cfg;
 
+        $object_id = false;
+        if (!$client) {
+            $namespace = 'ticket.staff';
+        }
+        else {
+            $namespace = 'ticket.client';
+            $object_id = substr(session_id(), -12);
+        }
+        list($draft, $attrs) = Draft::getDraftAndDataAttrs($namespace, $object_id, $this->value);
         ?><div style="margin-bottom:0.5em;margin-top:0.5em"><strong><?php
         echo Format::htmlchars($this->field->get('label'));
         ?></strong>:</div>
+
         <textarea style="width:100%;" name="<?php echo $this->field->get('name'); ?>"
             placeholder="<?php echo Format::htmlchars($this->field->get('hint')); ?>"
-            <?php if (!$client) { ?>
-                data-draft-namespace="ticket.staff"
-            <?php } else { ?>
-                data-draft-namespace="ticket.client"
-                data-draft-object-id="<?php echo substr(session_id(), -12); ?>"
-            <?php } ?>
-            class="richtext draft draft-delete ifhtml"
+            class="<?php if ($cfg->isHtmlThreadEnabled()) echo 'richtext';
+                ?> draft draft-delete" <?php echo $attrs; ?>
             cols="21" rows="8" style="width:80%;"><?php echo
-            Format::htmlchars($this->value); ?></textarea>
+            $draft ?: Format::htmlchars($this->value); ?></textarea>
     <?php
         $config = $this->field->getConfiguration();
         if (!$config['attachments'])
diff --git a/include/client/footer.inc.php b/include/client/footer.inc.php
index 9ff4ad15265c68a7a7938535786cae3ea9f910db..9521fbfc73a61d03735241bf4a6890b98c13b4a0 100644
--- a/include/client/footer.inc.php
+++ b/include/client/footer.inc.php
@@ -14,5 +14,12 @@ if (($lang = Internationalization::getCurrentLanguage()) && $lang != 'en_US') {
     <script type="text/javascript" src="ajax.php/i18n/<?php
         echo $lang; ?>/js"></script>
 <?php } ?>
+<script type="text/javascript">
+    getConfig().resolve(<?php
+        include INCLUDE_DIR . 'ajax.config.php';
+        $api = new ConfigAjaxAPI();
+        print $api->client(false);
+    ?>);
+</script>
 </body>
 </html>
diff --git a/include/client/view.inc.php b/include/client/view.inc.php
index dc3a0419f97a353471b6a87f7dc4ad1453b5b881..efebfe775a2945433fe1c06bf5665d9171f4bc19 100644
--- a/include/client/view.inc.php
+++ b/include/client/view.inc.php
@@ -172,9 +172,13 @@ if (!$ticket->isClosed() || $ticket->isReopenable()) { ?>
                 <span id="msg"><em><?php echo $msg; ?> </em></span><font class="error">*&nbsp;<?php echo $errors['message']; ?></font>
                 <br/>
                 <textarea name="message" id="message" cols="50" rows="9" wrap="soft"
-                    data-draft-namespace="ticket.client"
-                    data-draft-object-id="<?php echo $ticket->getId(); ?>"
-                    class="richtext ifhtml draft"><?php echo $info['message']; ?></textarea>
+                    class="<?php if ($cfg->isHtmlThreadEnabled()) echo 'richtext';
+                        ?> draft" <?php
+    list($draft, $attrs) = Draft::getDraftAndDataAttrs('ticket.client', $ticket->getId(), $info['message']);
+    echo $attrs; ?>><?php echo $draft ?: $info['message'];
+                ?></textarea>
+            </td>
+        </tr>
         <?php
         if ($messageField->isAttachmentsEnabled()) { ?>
 <?php
diff --git a/include/staff/cannedresponse.inc.php b/include/staff/cannedresponse.inc.php
index 6be5a63b1e7b2834971715c939e1e3073af0d78c..ad0d784fb5e0babcce5366938c7d0d5516dbe0fa 100644
--- a/include/staff/cannedresponse.inc.php
+++ b/include/staff/cannedresponse.inc.php
@@ -83,10 +83,12 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                     &nbsp;&nbsp;&nbsp;(<a class="tip" href="#ticket_variables"><?php echo __('Supported Variables'); ?></a>)
                     </div>
                 <textarea name="response" class="richtext draft draft-delete" cols="21" rows="12"
-                    data-draft-namespace="canned"
-                    data-draft-object-id="<?php if (isset($canned)) echo $canned->getId(); ?>"
-                    style="width:98%;" class="richtext draft"><?php
-                        echo $info['response']; ?></textarea>
+                    style="width:98%;" class="richtext draft" <?php
+    list($draft, $attrs) = Draft::getDraftAndDataAttrs('canned',
+        is_object($canned) ? $canned->getId() : false, $info['response']);
+    echo $attrs; ?>><?php echo $draft ?: $info['response'];
+                ?></textarea>
+                <br><br>
                 <div><h3><?php echo __('Canned Attachments'); ?> <?php echo __('(optional)'); ?>
                 &nbsp;<i class="help-tip icon-question-sign" href="#canned_attachments"></i></h3>
                 <div class="error"><?php echo $errors['files']; ?></div>
diff --git a/include/staff/faq.inc.php b/include/staff/faq.inc.php
index 4fcea8138b348ae8bdd24ddd3316cc4e940826ab..ee27bfab24df1d25ba9804b5c28be058cbc9cae8 100644
--- a/include/staff/faq.inc.php
+++ b/include/staff/faq.inc.php
@@ -88,10 +88,11 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                     <b><?php echo __('Answer');?></b>&nbsp;<font class="error">*&nbsp;<?php echo $errors['answer']; ?></font></div>
                 </div>
                 <textarea name="answer" cols="21" rows="12"
-                    style="width:98%;" class="richtext draft"
-                    data-draft-namespace="faq"
-                    data-draft-object-id="<?php if (is_object($faq)) echo $faq->getId(); ?>"
-                    ><?php echo $info['answer']; ?></textarea>
+                    style="width:98%;" class="richtext draft" <?php
+    list($draft, $attrs) = Draft::getDraftAndDataAttrs('faq',
+        is_object($faq) ? $faq->getId() : false, $info['answer']);
+    echo $attrs; ?>><?php echo $draft ?: $info['answer'];
+                ?></textarea>
             </td>
         </tr>
         <tr>
diff --git a/include/staff/page.inc.php b/include/staff/page.inc.php
index 532bfc0af8c4c9b9435b9be741ec01c433e2f3c9..49f2cbea471ad5fa51e3cd9fa18092b076581070 100644
--- a/include/staff/page.inc.php
+++ b/include/staff/page.inc.php
@@ -105,13 +105,14 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
          <tr>
             <td colspan=2 style="padding-left:3px;">
                 <textarea name="body" cols="21" rows="12" style="width:98%;" class="richtext draft"
-                    data-draft-namespace="page" data-draft-object-id="<?php echo $info['id']; ?>"
-                    ><?php echo $info['body']; ?></textarea>
+<?php
+    list($draft, $attrs) = Draft::getDraftAndDataAttrs('page', $info['id'], $info['body']);
+    echo $attrs; ?>><?php echo $draft ?: $info['body']; ?></textarea>
             </td>
         </tr>
         <tr>
             <th colspan="2">
-                <em><strong><?php echo __('Internal Notes'); ?></strong>: 
+                <em><strong><?php echo __('Internal Notes'); ?></strong>:
                 <?php echo __("be liberal, they're internal"); ?></em>
             </th>
         </tr>
diff --git a/include/staff/templates/ticket-status.tmpl.php b/include/staff/templates/ticket-status.tmpl.php
index 5e0d84ae3c82be28c5880b5444ffc65ea513a279..eae1b3272e99c586e0abdf88e08bdf9995a6c83b 100644
--- a/include/staff/templates/ticket-status.tmpl.php
+++ b/include/staff/templates/ticket-status.tmpl.php
@@ -84,7 +84,8 @@ $action = $info['action'] ?: ('#tickets/status/'. $state);
                         ?>
                         <textarea name="comments" id="comments"
                             cols="50" rows="3" wrap="soft" style="width:100%"
-                            class="richtext ifhtml no-bar"
+                            class="<?php if ($cfg->isHtmlThreadEnabled()) echo 'richtext';
+                            ?> no-bar"
                             placeholder="<?php echo $placeholder; ?>"><?php
                             echo $info['comments']; ?></textarea>
                     </td>
diff --git a/include/staff/ticket-open.inc.php b/include/staff/ticket-open.inc.php
index 39acb0864519519c278c7c4d28771476a4042c20..d70af212e8d692ada02827e3423978bb4d28ed3d 100644
--- a/include/staff/ticket-open.inc.php
+++ b/include/staff/ticket-open.inc.php
@@ -302,13 +302,17 @@ if ($_POST)
                 $signature = '';
                 if ($thisstaff->getDefaultSignatureType() == 'mine')
                     $signature = $thisstaff->getSignature(); ?>
-                <textarea class="richtext ifhtml draft draft-delete"
-                    data-draft-namespace="ticket.staff.response"
-                    data-signature="<?php
+                <textarea
+                    class="<?php if ($cfg->isHtmlThreadEnabled()) echo 'richtext';
+                        ?> draft draft-delete" data-signature="<?php
                         echo Format::htmlchars(Format::viewableImages($signature)); ?>"
                     data-signature-field="signature" data-dept-field="deptId"
                     placeholder="<?php echo __('Initial response for the ticket'); ?>"
                     name="response" id="response" cols="21" rows="8"
+                    style="width:80%;" <?php
+    list($draft, $attrs) = Draft::getDraftAndDataAttrs('ticket.staff.response', false, $info['response']);
+    echo $attrs; ?>><?php echo $draft ?: $info['response'];
+                ?></textarea>
                     style="width:80%;"><?php echo $info['response']; ?></textarea>
                     <div class="attachments">
 <?php
@@ -371,11 +375,14 @@ print $response_form->getField('attachments')->render();
         </tr>
         <tr>
             <td colspan=2>
-                <textarea class="richtext ifhtml draft draft-delete"
+                <textarea
+                    class="<?php if ($cfg->isHtmlThreadEnabled()) echo 'richtext';
+                        ?> draft draft-delete"
                     placeholder="<?php echo __('Optional internal note (recommended on assignment)'); ?>"
-                    data-draft-namespace="ticket.staff.note" name="note"
-                    cols="21" rows="6" style="width:80%;"
-                    ><?php echo $info['note']; ?></textarea>
+                    name="note" cols="21" rows="6" style="width:80%;" <?php
+    list($draft, $attrs) = Draft::getDraftAndDataAttrs('ticket.staff.note', false, $info['note']);
+    echo $attrs; ?>><?php echo $draft ?: $info['note'];
+                ?></textarea>
             </td>
         </tr>
     </tbody>
diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php
index 078af3a0a268bf45bed22653fcbecfdaef703ba2..83f7622edf35589db3d2e267977b42b3e738f822 100644
--- a/include/staff/ticket-view.inc.php
+++ b/include/staff/ticket-view.inc.php
@@ -561,17 +561,18 @@ $tcount+= $ticket->getNumNotes();
                     } ?>
                     <input type="hidden" name="draft_id" value=""/>
                     <textarea name="response" id="response" cols="50"
-                        data-draft-namespace="ticket.response"
                         data-signature-field="signature" data-dept-id="<?php echo $dept->getId(); ?>"
                         data-signature="<?php
                             echo Format::htmlchars(Format::viewableImages($signature)); ?>"
                         placeholder="<?php echo __(
                         'Start writing your response here. Use canned responses from the drop-down above'
                         ); ?>"
-                        data-draft-object-id="<?php echo $ticket->getId(); ?>"
                         rows="9" wrap="soft"
-                        class="richtext ifhtml draft draft-delete"><?php
-                        echo $info['response']; ?></textarea>
+                        class="<?php if ($cfg->isHtmlThreadEnabled()) echo 'richtext';
+                            ?> draft draft-delete" <?php
+    list($draft, $attrs) = Draft::getDraftAndDataAttrs('ticket.response', $ticket->getId(), $info['response']);
+    echo $attrs; ?>><?php echo $draft ?: $info['response'];
+                    ?></textarea>
                 <div id="reply_form_attachments" class="attachments">
 <?php
 print $response_form->getField('attachments')->render();
@@ -671,9 +672,11 @@ print $response_form->getField('attachments')->render();
                     <div class="error"><?php echo $errors['note']; ?></div>
                     <textarea name="note" id="internal_note" cols="80"
                         placeholder="<?php echo __('Note details'); ?>"
-                        rows="9" wrap="soft" data-draft-namespace="ticket.note"
-                        data-draft-object-id="<?php echo $ticket->getId(); ?>"
-                        class="richtext ifhtml draft draft-delete"><?php echo $info['note'];
+                        rows="9" wrap="soft"
+                        class="<?php if ($cfg->isHtmlThreadEnabled()) echo 'richtext';
+                            ?> draft draft-delete" <?php
+    list($draft, $attrs) = Draft::getDraftAndDataAttrs('ticket.note', $ticket->getId(), $info['note']);
+    echo $attrs; ?>><?php echo $draft ?: $info['note'];
                         ?></textarea>
                 <div class="attachments">
 <?php
@@ -764,7 +767,8 @@ print $note_form->getField('attachments')->render();
                 <td>
                     <textarea name="transfer_comments" id="transfer_comments"
                         placeholder="<?php echo __('Enter reasons for the transfer'); ?>"
-                        class="richtext ifhtml no-bar" cols="80" rows="7" wrap="soft"><?php
+                        class="<?php if ($cfg->isHtmlThreadEnabled()) echo 'richtext';
+                            ?> no-bar" cols="80" rows="7" wrap="soft"><?php
                         echo $info['transfer_comments']; ?></textarea>
                     <span class="error"><?php echo $errors['transfer_comments']; ?></span>
                 </td>
@@ -861,7 +865,8 @@ print $note_form->getField('attachments')->render();
                     <textarea name="assign_comments" id="assign_comments"
                         cols="80" rows="7" wrap="soft"
                         placeholder="<?php echo __('Enter reasons for the assignment or instructions for assignee'); ?>"
-                        class="richtext ifhtml no-bar"><?php echo $info['assign_comments']; ?></textarea>
+                        class="<?php if ($cfg->isHtmlThreadEnabled()) echo 'richtext';
+                            ?> no-bar"><?php echo $info['assign_comments']; ?></textarea>
                     <span class="error"><?php echo $errors['assign_comments']; ?></span><br>
                 </td>
             </tr>
diff --git a/include/staff/tpl.inc.php b/include/staff/tpl.inc.php
index fe048decd06309b9f2f3383aa3950bb0bcbed687..37e29177e80d850efb54bbd6e8a0c79c891db421 100644
--- a/include/staff/tpl.inc.php
+++ b/include/staff/tpl.inc.php
@@ -108,9 +108,10 @@ $tpl=$msgtemplates[$selected];
     </div>
     <input type="hidden" name="draft_id" value=""/>
     <textarea name="body" cols="21" rows="16" style="width:98%;" wrap="soft"
-        data-toolbar-external="#toolbar"
-        class="richtext draft" data-draft-namespace="tpl.<?php echo Format::htmlchars($selected); ?>"
-        data-draft-object-id="<?php echo $tpl_id; ?>"><?php echo $info['body']; ?></textarea>
+        data-toolbar-external="#toolbar" class="richtext draft" <?php
+    list($draft, $attrs) = Draft::getDraftAndDataAttrs('tpl.'.$selected, $tpl_id, $info['body']);
+    echo $attrs; ?>><?php echo $draft ?: $info['body'];
+    ?></textarea>
 </div>
 
 <p style="text-align:center">
diff --git a/js/osticket.js b/js/osticket.js
index f4aeebd1a1075541fadb34c70f4f49ce6fd70452..911cbf4e758ab7b185837dfe8a51855e692d48c4 100644
--- a/js/osticket.js
+++ b/js/osticket.js
@@ -78,39 +78,6 @@ $(document).ready(function(){
 
     });
 
-    getConfig = (function() {
-        var dfd = $.Deferred(),
-            requested = false;
-        return function() {
-            if (dfd.state() != 'resolved' && !requested)
-                requested = $.ajax({
-                    url: "ajax.php/config/client",
-                    dataType: 'json',
-                    success: function (json_config) {
-                        dfd.resolve(json_config);
-                    }
-                });
-            return dfd;
-        }
-    })();
-
-    $.translate_format = function(str) {
-        var translation = {
-            'd':'dd',
-            'j':'d',
-            'z':'o',
-            'm':'mm',
-            'F':'MM',
-            'n':'m',
-            'Y':'yy'
-        };
-        // Change PHP formats to datepicker ones
-        $.each(translation, function(php, jqdp) {
-            str = str.replace(php, jqdp);
-        });
-        return str;
-    };
-
     var showNonLocalImage = function(div) {
         var $div = $(div),
             $img = $div.append($('<img>')
@@ -199,7 +166,32 @@ showImagesInline = function(urls, thread_id) {
             e.data('wrapped', true);
         }
     });
-}
+};
+
+getConfig = (function() {
+    var dfd = $.Deferred(),
+        requested = false;
+    return function() {
+        return dfd;
+    };
+})();
+
+$.translate_format = function(str) {
+    var translation = {
+        'd':'dd',
+        'j':'d',
+        'z':'o',
+        'm':'mm',
+        'F':'MM',
+        'n':'m',
+        'Y':'yy'
+    };
+    // Change PHP formats to datepicker ones
+    $.each(translation, function(php, jqdp) {
+        str = str.replace(php, jqdp);
+    });
+    return str;
+};
 
 $.sysAlert = function (title, msg, cb) {
     var $dialog =  $('.dialog#alert');
diff --git a/js/redactor-osticket.js b/js/redactor-osticket.js
index cded2e1f3ffdc7d425428155eec3cf2a0815777d..6e50a61e399c697ab7fe16e7dabf1dddf7ab4ff5 100644
--- a/js/redactor-osticket.js
+++ b/js/redactor-osticket.js
@@ -21,10 +21,24 @@ RedactorPlugins.draft = {
         var autosave_url = 'ajax.php/draft/' + this.opts.draftNamespace;
         if (this.opts.draftObjectId)
             autosave_url += '.' + this.opts.draftObjectId;
-        this.opts.autosave = autosave_url;
-        this.opts.autosaveInterval = 10;
-        this.opts.autosaveCallback = this.setupDraftUpdate;
-        this.opts.initCallback = this.recoverDraft;
+        this.opts.autosave = this.opts.autoCreateUrl = autosave_url;
+        this.opts.autosaveInterval = 30;
+        this.opts.autosaveCallback = this.afterUpdateDraft;
+        this.opts.autosaveErrorCallback = this.autosaveFailed;
+        this.opts.imageUploadErrorCallback = this.displayError;
+        if (this.opts.draftId) {
+            this.opts.autosave = 'ajax.php/draft/'+this.opts.draftId;
+            this.opts.clipboardUploadUrl =
+            this.opts.imageUpload =
+                'ajax.php/draft/'+this.opts.draftId+'/attach';
+        }
+        else {
+            // Just upload the file. A draft will be created automatically
+            // and will be configured locally in the afterUpateDraft()
+            this.opts.clipboardUploadUrl =
+            this.opts.imageUpload = this.opts.autoCreateUrl + '/attach';
+            this.opts.imageUploadCallback = this.afterUpdateDraft;
+        }
 
         this.$draft_saved = $('<span>')
             .addClass("pull-right draft-saved")
@@ -33,73 +47,60 @@ RedactorPlugins.draft = {
                 .text(__('Draft Saved')));
         // Float the [Draft Saved] box with the toolbar
         this.$toolbar.append(this.$draft_saved);
+        // Add [Delete Draft] button to the toolbar
         if (this.opts.draftDelete) {
-            var trash = this.buttonAdd('deleteDraft', __('Delete Draft'), this.deleteDraft);
+            var trash = this.draftDeleteButton =
+                this.buttonAdd('deleteDraft', __('Delete Draft'),
+                    this.deleteDraft);
             this.buttonAwesome('deleteDraft', 'icon-trash');
             trash.parent().addClass('pull-right');
             trash.addClass('delete-draft');
+            if (!this.opts.draftId)
+                trash.hide();
         }
     },
-    recoverDraft: function() {
-        var self = this;
-        $.ajax(this.opts.autosave, {
-            dataType: 'json',
-            statusCode: {
-                200: function(json) {
-                    self.draft_id = json.draft_id;
-                    // Replace the current content with the draft, sync, and make
-                    // images editable
-                    self.setupDraftUpdate(json);
-                    if (!json.body) return;
-                    self.set(json.body, false);
-                    self.observeStart();
-                },
-                205: function() {
-                    // Save empty draft immediately;
-                    var ai = self.opts.autosaveInterval;
-
-                    // Save immediately -- capture the created autosave
-                    // interval and clear it as soon as possible. Note that
-                    // autosave()ing doesn't happen immediately. It happens
-                    // async after the autosaveInterval expires.
-                    self.opts.autosaveInterval = 0;
-                    self.autosave();
-                    var interval = self.autosaveInterval;
-                    setTimeout(function() {
-                        clearInterval(interval);
-                    }, 1);
-
-                    // Reinstate previous autosave interval timing
-                    self.opts.autosaveInterval = ai;
-                }
-            }
-        });
-    },
-    setupDraftUpdate: function(data) {
-        if (this.get())
-            this.$draft_saved.show().delay(5000).fadeOut();
-
+    afterUpdateDraft: function(data) {
         // Slight workaround. Signal the 'keyup' event normally signaled
         // from typing in the <textarea>
-        if ($.autoLock && this.opts.draftNamespace == 'ticket.response')
+        if ($.autoLock && this.opts.draftNamespace == 'ticket.response') {
             if (this.get())
                 $.autoLock.handleEvent();
+        }
 
-        if (typeof data != 'object')
-            data = $.parseJSON(data);
-
-        if (!data || !data.draft_id)
+        // If the draft was created, a draft_id will be sent back — update
+        // the URL to send updates in the future
+        if (!this.opts.draftId && data.draft_id) {
+            this.opts.draftId = data.draft_id;
+            this.opts.autosave = 'ajax.php/draft/' + data.draft_id;
+            this.opts.clipboardUploadUrl =
+            this.opts.imageUpload =
+                'ajax.php/draft/'+this.opts.draftId+'/attach';
+            if (!this.get())
+                this.set(' ', false);
+        }
+        // Only show the [Draft Saved] notice if there is content in the
+        // field that has been touched
+        if (!this.firstSave) {
+            this.firstSave = true;
+            // No change yet — dont't show the button
+            return;
+        }
+        if (data && this.get()) {
+            this.$draft_saved.show().delay(5000).fadeOut();
+        }
+        // Show the button if there is a draft to delete
+        if (this.opts.draftId && this.opts.draftDelete)
+            this.draftDeleteButton.show();
+    },
+    autosaveFailed: function(error) {
+        if (error.code == 422)
+            // Unprocessable request (Empty message)
             return;
 
-        $('input[name=draft_id]', this.$box.closest('form'))
-            .val(data.draft_id);
-        this.draft_id = data.draft_id;
-        this.opts.clipboardUploadUrl =
-        this.opts.imageUpload =
-            'ajax.php/draft/'+data.draft_id+'/attach';
-        this.opts.imageUploadErrorCallback = this.displayError;
-        this.opts.original_autosave = this.opts.autosave;
-        this.opts.autosave = 'ajax.php/draft/'+data.draft_id;
+        this.displayError(error);
+        // Cancel autosave
+        clearInterval(this.autosaveInterval);
+        this.hideDraftSaved();
     },
 
     displayError: function(json) {
@@ -111,18 +112,20 @@ RedactorPlugins.draft = {
     },
 
     deleteDraft: function() {
-        if (!this.draft_id)
+        if (!this.opts.draftId)
             // Nothing to delete
             return;
         var self = this;
-        $.ajax('ajax.php/draft/'+this.draft_id, {
+        $.ajax('ajax.php/draft/'+this.opts.draftId, {
             type: 'delete',
             async: false,
             success: function() {
-                self.draft_id = undefined;
+                self.draft_id = self.opts.draftId = undefined;
                 self.hideDraftSaved();
-                self.set('', false, false);
-                self.opts.autosave = self.opts.original_autosave;
+                self.set(self.opts.draftOriginal || '', false, false);
+                self.opts.autosave = self.opts.autoCreateUrl;
+                self.draftDeleteButton.hide();
+                self.firstSave = false;
             }
         });
     }
@@ -141,10 +144,10 @@ RedactorPlugins.signature = {
             else
                 this.$signatureBox.hide();
             $('input[name='+$el.data('signatureField')+']', $el.closest('form'))
-                .on('change', false, false, $.proxy(this.updateSignature, this))
+                .on('change', false, false, $.proxy(this.updateSignature, this));
             if ($el.data('deptField'))
                 $(':input[name='+$el.data('deptField')+']', $el.closest('form'))
-                    .on('change', false, false, $.proxy(this.updateSignature, this))
+                    .on('change', false, false, $.proxy(this.updateSignature, this));
             // Expand on hover
             var outer = this.$signatureBox,
                 inner = $('.inner', this.$signatureBox).get(0),
@@ -181,14 +184,14 @@ RedactorPlugins.signature = {
             url += 'dept/' + $el.data('deptId');
         else if (selected == 'dept' && $el.data('deptField')) {
             if (dept)
-                url += 'dept/' + dept
+                url += 'dept/' + dept;
             else
                 return inner.empty().parent().hide();
         }
         else if (type == 'none')
            return inner.empty().parent().hide();
         else
-            url += selected
+            url += selected;
 
         inner.load(url).parent().show();
     }
@@ -206,10 +209,6 @@ $(function() {
                       .attr('height',img.clientHeight);
             html = html.replace(before, img.outerHTML);
         });
-        // Drop <inline> elements if found in the text (shady mojo happening
-        // inside the Redactor editor)
-        // DELME: When this is fixed upstream in Redactor
-        html = html.replace(/<inline /, '<span ').replace(/<\/inline>/, '</span>');
         return html;
     },
     redact = $.redact = function(el, options) {
diff --git a/scp/ajax.php b/scp/ajax.php
index 1168513601eb45882783c339934006856b36c550..09dce15dd85103fadcc55bb0b2b25c0a7cc08aac 100644
--- a/scp/ajax.php
+++ b/scp/ajax.php
@@ -155,6 +155,7 @@ $dispatcher = patterns('',
         url_post('^(?P<id>\d+)$', 'updateDraft'),
         url_delete('^(?P<id>\d+)$', 'deleteDraft'),
         url_post('^(?P<id>\d+)/attach$', 'uploadInlineImage'),
+        url_post('^(?P<namespace>[\w.]+)/attach$', 'uploadInlineImageEarly'),
         url_get('^(?P<namespace>[\w.]+)$', 'getDraft'),
         url_post('^(?P<namespace>[\w.]+)$', 'createDraft'),
         url_get('^images/browse$', 'getFileList')
diff --git a/scp/emailtest.php b/scp/emailtest.php
index c714ef1d0643d6c480aac316b511c851244754ef..1246dff088799736f62941acf470f2901b335e1c 100644
--- a/scp/emailtest.php
+++ b/scp/emailtest.php
@@ -116,8 +116,10 @@ require(STAFFINC_DIR.'header.inc.php');
                 <div style="padding-top:0.5em;padding-bottom:0.5em">
                 <em><strong><?php echo __('Message');?></strong>: <?php echo __('email message to send.');?></em>&nbsp;<span class="error">*&nbsp;<?php echo $errors['message']; ?></span></div>
                 <textarea class="richtext draft draft-delete" name="message" cols="21"
-                    data-draft-namespace="email.diag"
-                    rows="10" style="width: 90%;"><?php echo $info['message']; ?></textarea>
+                    rows="10" style="width: 90%;" <?php
+    list($draft, $attrs) = Draft::getDraftAndDataAttrs('email.diag', false, $info['message']);
+    echo $attrs; ?>><?php echo $draft ?: $info['message'];
+                 ?></textarea>
             </td>
         </tr>
     </tbody>