diff --git a/include/class.attachment.php b/include/class.attachment.php
index 09d9826fd0444748830e63b18e5caf9bb5441f54..937d09edd5346d331170752077e1b00f9a354e51 100644
--- a/include/class.attachment.php
+++ b/include/class.attachment.php
@@ -175,6 +175,7 @@ class GenericAttachments {
         foreach ($this->attachments as $a) {
             if ($a['inline'] != $separate || $a['inline'] == $inlines) {
                 $a['file_id'] = $a['id'];
+                $a['hash'] = md5($a['file_id'].session_id().strtolower($a['key']));
                 $attachments[] = $a;
             }
         }
diff --git a/include/class.i18n.php b/include/class.i18n.php
index b7e7289430bf993618579a5b2f5726d7c682e100..59c2061ee6c5e789f6bcc6be04ff7becae332276 100644
--- a/include/class.i18n.php
+++ b/include/class.i18n.php
@@ -158,7 +158,7 @@ class Internationalization {
 
         // Consider all subdirectories and .phar files in the base dir
         $dirs = glob(I18N_DIR . '*', GLOB_ONLYDIR | GLOB_NOSORT);
-        $phars = glob(I18N_DIR . '*.phar', GLOB_NOSORT);
+        $phars = glob(I18N_DIR . '*.phar', GLOB_NOSORT) ?: array();
 
         $installed = array();
         foreach (array_merge($dirs, $phars) as $f) {
diff --git a/include/class.mailfetch.php b/include/class.mailfetch.php
index f1ea77d74ac9979bdffccfd195ca76eabd98c3a9..dedfba84c028c19a8f1bbe62a091e0ab337bffff 100644
--- a/include/class.mailfetch.php
+++ b/include/class.mailfetch.php
@@ -541,7 +541,8 @@ class MailFetcher {
             $body = new TextThreadBody(
                     Format::html2text(Format::safe_html($html),
                         100, false));
-        else
+
+        if (!isset($body))
             $body = new TextThreadBody('');
 
         if ($cfg->stripQuotedReply())
diff --git a/include/class.mailparse.php b/include/class.mailparse.php
index 0ec0b4f190217b90c633f220f6440a7c89e625ce..654e0e05ed66d7a8bca9af76d3e9e3db9ba601dc 100644
--- a/include/class.mailparse.php
+++ b/include/class.mailparse.php
@@ -292,7 +292,8 @@ class Mail_Parse {
             $body = new TextThreadBody(
                     Format::html2text(Format::safe_html($html),
                         100, false));
-        else
+
+        if (!isset($body))
             $body = new TextThreadBody('');
 
         if ($cfg && $cfg->stripQuotedReply())
diff --git a/include/class.osticket.php b/include/class.osticket.php
index 98979448ccf100f271caa41670046301ebf55e63..abd563839d6be9910ae4446f4aaf71694b1dc96b 100644
--- a/include/class.osticket.php
+++ b/include/class.osticket.php
@@ -144,7 +144,7 @@ class osTicket {
         $allowed = array_map('trim', explode(',', strtolower($allowedFileTypes)));
         $filename = is_array($file)?$file['name']:$file;
 
-        $ext = strtolower(preg_replace("/.*\.(.{3,4})$/", "$1", $filename));
+        $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
 
         //TODO: Check MIME type - file ext. shouldn't be solely trusted.
 
diff --git a/include/class.thread.php b/include/class.thread.php
index 0722c80d2027ca5691afe4dc37e805911f86658b..fbe50bc87cdfaf94447b1167837041d65aa53574 100644
--- a/include/class.thread.php
+++ b/include/class.thread.php
@@ -973,12 +973,23 @@ Class ThreadEntry {
                 foreach ($vars['attachments'] as $i=>$a) {
                     if (@$a['cid'] && $a['cid'] == $cid) {
                         // Inline referenced attachment was stripped
-                        unset($vars['attachments']);
+                        unset($vars['attachments'][$i]);
                     }
                 }
             }
         }
 
+        // Handle extracted embedded images (<img src="data:base64,..." />).
+        // The extraction has already been performed in the ThreadBody
+        // class. Here they should simply be added to the attachments list
+        if ($atts = $vars['body']->getEmbeddedHtmlImages()) {
+            if (!is_array($vars['attachments']))
+                $vars['attachments'] = array();
+            foreach ($atts as $info) {
+                $vars['attachments'][] = $info;
+            }
+        }
+
         if (!($body = Format::sanitize(
                 (string) $vars['body']->convertTo('html'))))
             $body = '-'; //Special tag used to signify empty message as stored.
@@ -1230,6 +1241,7 @@ class ThreadBody /* extends SplString */ {
     var $body;
     var $type;
     var $stripped_images = array();
+    var $embedded_images = array();
 
     function __construct($body, $type='text') {
         $type = strtolower($type);
@@ -1282,6 +1294,10 @@ class ThreadBody /* extends SplString */ {
         return $this->stripped_images;
     }
 
+    function getEmbeddedHtmlImages() {
+        return $this->embedded_images;
+    }
+
     function __toString() {
         return $this->body;
     }
@@ -1294,8 +1310,22 @@ class TextThreadBody extends ThreadBody {
 }
 class HtmlThreadBody extends ThreadBody {
     function __construct($body) {
+        $body = $this->extractEmbeddedHtmlImages($body);
         $body = trim($body, " <>br/\t\n\r") ? Format::safe_html($body) : '';
         parent::__construct($body, 'html');
     }
+
+    function extractEmbeddedHtmlImages($body) {
+        $self = $this;
+        return preg_replace_callback('/src="(data:[^"]+)"/',
+        function ($m) use ($self) {
+            $info = Format::parseRfc2397($m[1], false, false);
+            $info['cid'] = 'img'.Misc::randCode(12);
+            list(,$type) = explode('/', $info['type'], 2);
+            $info['name'] = 'image'.Misc::randCode(4).'.'.$type;
+            $self->embedded_images[] = $info;
+            return 'src="cid:'.$info['cid'].'"';
+        }, $body);
+    }
 }
 ?>
diff --git a/js/redactor-osticket.js b/js/redactor-osticket.js
index d41c87ba68370adcbf36ae21f7a167025d72d0a7..4a919612bce3612a623f62a60085f04e883a5018 100644
--- a/js/redactor-osticket.js
+++ b/js/redactor-osticket.js
@@ -87,6 +87,10 @@ RedactorPlugins.draft = {
                 self.opts.imageUpload =
                     'ajax.php/draft/'+data.draft_id+'/attach';
                 self.opts.imageUploadErrorCallback = self.displayError;
+                // XXX: This happens in ::buildBindKeyboard() from
+                // ::buildAfter(). However, the imageUpload option is not
+                // known when the Redactor is init()'d
+                self.$editor.on('drop.redactor', $.proxy(self.buildEventDrop, self));
             }
         });
         this.opts.original_autosave = this.opts.autosave;
diff --git a/kb/file.php b/kb/file.php
index 21336765817fa588a82a983af5e8b52dc8da2a85..b06b256a35a6ebdd9c40137f9fec0b6a2f797343 100644
--- a/kb/file.php
+++ b/kb/file.php
@@ -23,7 +23,7 @@ $h=trim($_GET['h']);
 //basic checks
 if(!$h  || strlen($h)!=64  //32*2
         || !($file=AttachmentFile::lookup(substr($h,0,32))) //first 32 is the file hash.
-        || strcasecmp(substr($h,-32),md5($file->getId().session_id().$file->getKey()))) //next 32 is file id + session hash.
+        || strcasecmp($h, $file->getDownloadHash())) //next 32 is file id + session hash.
     die('Unknown or invalid file. #'.Format::htmlchars($_GET['h']));
 
 $file->download();
diff --git a/scp/file.php b/scp/file.php
index 9d6518d0ae4f4d53656389503bb83c71682a8963..68197cc566cf05f707d7d0d458b7097422b7b8ae 100644
--- a/scp/file.php
+++ b/scp/file.php
@@ -23,7 +23,7 @@ $h=trim($_GET['h']);
 //basic checks
 if(!$h  || strlen($h)!=64  //32*2
         || !($file=AttachmentFile::lookup(substr($h,0,32))) //first 32 is the file hash.
-        || $file->getDownloadHash() != $h) //next 32 is file id + session hash.
+        || strcasecmp($file->getDownloadHash(), $h)) //next 32 is file id + session hash.
     die('Unknown or invalid file. #'.Format::htmlchars($_GET['h']));
 
 $file->download();
diff --git a/scp/js/scp.js b/scp/js/scp.js
index 2119131553d6c7f48d4601be8f8a529f2d7f3114..79f5804f2c5727ecf13694b4f4f5112020aa2987 100644
--- a/scp/js/scp.js
+++ b/scp/js/scp.js
@@ -231,7 +231,7 @@ $(document).ready(function(){
                             if(!$('.canned_attachments #f'+j.id,fObj).length) {
                                 var file='<span><label><input type="checkbox" name="cannedattachments[]" value="' + j.id+'" id="f'+j.id+'" checked="checked">';
                                     file+= ' '+ j.name + '</label>';
-                                    file+= ' (<a href="file.php?h=' + j.hash + j.key+ '">view</a>) </span>';
+                                    file+= ' (<a href="file.php?h=' + j.key + j.hash + '">view</a>) </span>';
                                 $('.canned_attachments', fObj).append(file);
                             }
 
diff --git a/setup/test/tests/test.validation.php b/setup/test/tests/test.validation.php
index 6323cd1590856e82ef091e7cce983558fd28c91c..27e61af8438a4fc8a75bcb3003013af6ab56801d 100644
--- a/setup/test/tests/test.validation.php
+++ b/setup/test/tests/test.validation.php
@@ -32,6 +32,7 @@ class TestValidation extends Test {
         $this->assert(Validator::is_email('jared.12@domain.tld'));
         $this->assert(Validator::is_email('jared_12@domain.tld'));
         $this->assert(Validator::is_email('jared-12@domain.tld'));
+        $this->assert(Validator::is_email('jared+ost@domain.tld'));
 
         // Illegal or unsupported
         $this->assert(!Validator::is_email('jared r@domain.tld'));