diff --git a/include/ajax.draft.php b/include/ajax.draft.php
index f727e2cc9a10040786f6abb3502a8a21f7e04806..c50817dac4598a0a4c4ce42b6585f4af61cd5566 100644
--- a/include/ajax.draft.php
+++ b/include/ajax.draft.php
@@ -7,29 +7,24 @@ 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'])) {
+            $field_list = array('response', 'note', 'answer', 'body',
+                 'message', 'issue');
+            foreach ($field_list as $field) {
+                if (isset($_POST[$field])) {
+                    $vars['body'] = $_POST[$field];
+                    break;
+                }
             }
         }
-        if (!isset($vars['body']))
-            return Http::response(422, "Draft body not found in request");
+        if (!isset($vars['body']) || !$vars['body'])
+            return JsonDataEncoder::encode(array(
+                'error' => __("Draft body not found in request"),
+                'code' => 422,
+                ));
 
-        $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 (!($draft = Draft::create($vars)) || !$draft->save())
+            Http::response(500, 'Unable to create draft');
 
         echo JsonDataEncoder::encode(array(
             'draft_id' => $draft->getId(),
@@ -232,6 +227,9 @@ class DraftAjaxAPI extends AjaxController {
             'namespace' => $namespace,
         );
 
+        if (isset($_POST['name']))
+            $vars['body'] = $_POST[$_POST['name']];
+
         return self::_createDraft($vars);
     }
 
diff --git a/include/class.draft.php b/include/class.draft.php
index cdb5d43f440081d3e047993dd0df006cf23c39d6..a58fb52f95828305495d89b7e0ef8e36f0dc401b 100644
--- a/include/class.draft.php
+++ b/include/class.draft.php
@@ -1,28 +1,57 @@
 <?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"', $namespace));
+        $criteria = array('namespace'=>$namespace);
+        if ($id) {
+            $attrs[] = sprintf('data-draft-object-id="%s"', $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($original));
+
+        return array($draft_body, implode(' ', $attrs));
+    }
 
     function getAttachmentIds($body=false) {
         $attachments = array();
@@ -63,69 +92,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 $this->namespace && isset($this->body) && isset($this->staff_id);
+    }
+
+    function save($refetch=false) {
+        if (!$this->isValid())
             return false;
 
-        $sql = ' SET `namespace`='.db_input($vars['namespace'])
-            .' ,body='.db_input(Format::sanitize($vars['body'], false))
-            .' ,staff_id='.db_input($vars['staff_id']);
+        return parent::save($refetch);
+    }
 
-        if (!$id) {
-            $sql = 'INSERT INTO '.DRAFT_TABLE.$sql
-                .' ,created=NOW()';
-            if(!db_query($sql) || !($draft=self::lookup(db_insert_id())))
-                return false;
+    static function create($vars) {
+        $attachments = @$vars['attachments'];
+        unset($vars['attachments']);
 
-            // Cloned attachments...
-            if($vars['attachments'] && is_array($vars['attachments']))
-                $draft->attachments->upload($vars['attachments'], true);
+        $vars['created'] = SqlFunction::NOW();
+        $draft = parent::create($vars);
 
-            return $draft;
-        }
-        else {
-            $sql = 'UPDATE '.DRAFT_TABLE.$sql
-                .' WHERE id='.db_input($id);
-            if (db_query($sql) && db_affected_rows() == 1)
-                return $this;
-        }
-    }
-
-    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
+        ));
     }
 
     /**
diff --git a/js/redactor-osticket.js b/js/redactor-osticket.js
index cded2e1f3ffdc7d425428155eec3cf2a0815777d..9bd0e57d7f844ce3b643b6c85d8a88191effb37a 100644
--- a/js/redactor-osticket.js
+++ b/js/redactor-osticket.js
@@ -21,10 +21,17 @@ 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.autosave = this.opts.autoCreateUrl = autosave_url;
         this.opts.autosaveInterval = 10;
-        this.opts.autosaveCallback = this.setupDraftUpdate;
-        this.opts.initCallback = this.recoverDraft;
+        this.opts.autosaveCallback = this.afterUpdateDraft;
+        this.opts.autosaveErrorCallback = this.autosaveFailed;
+        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';
+        }
+        this.opts.imageUploadErrorCallback = this.displayError;
 
         this.$draft_saved = $('<span>')
             .addClass("pull-right draft-saved")
@@ -33,73 +40,55 @@ 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 the draft was created, a draft_id will be sent back — update
+        // the URL to send updates in the future
+        if (data.draft_id) {
+            this.opts.draftId = data.draft_id;
+            this.opts.autosave = 'ajax.php/draft/' + data.draft_id;
+        }
 
-        if (!data || !data.draft_id)
+        // Only show the [Draft Saved] notice if there is content in the
+        // field that has been touched
+        if (this.opts.draftOriginal && this.opts.draftOriginal == this.get()) {
+            // No change yet — dont't show the button
+            return;
+        }
+        if (data) {
+            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 +100,19 @@ 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.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();
             }
         });
     }
@@ -141,10 +131,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 +171,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 +196,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) {