diff --git a/include/class.format.php b/include/class.format.php
index e3f7b415a7553f6f77e6da85b31aff4d2c2f9ba5..ac9f94c3b42a9e5f1af87d196d0a2e24ac4fdacb 100644
--- a/include/class.format.php
+++ b/include/class.format.php
@@ -301,19 +301,19 @@ class Format {
     }
 
     //make urls clickable. Mainly for display
-    function clickableurls($text) {
+    function clickableurls($text, $trampoline=true) {
         global $ost;
 
         $token = $ost->getLinkToken();
 
         // Find all text between tags
         $text = preg_replace_callback(':^[^<]+|>[^<]+:',
-            function($match) use ($token) {
+            function($match) use ($token, $trampoline) {
                 // Scan for things that look like URLs
                 return preg_replace_callback(
                     '`(?<!>)(((f|ht)tp(s?)://|(?<!//)www\.)([-+~%/.\w]+)(?:[-?#+=&;%@.\w]*)?)'
                    .'|(\b[_\.0-9a-z-]+@([0-9a-z][0-9a-z-]+\.)+[a-z]{2,4})`',
-                    function ($match) use ($token) {
+                    function ($match) use ($token, $trampoline) {
                         if ($match[1]) {
                             while (in_array(substr($match[1], -1),
                                     array('.','?','-',':',';'))) {
@@ -323,9 +323,13 @@ class Format {
                             if (strpos($match[2], '//') === false) {
                                 $match[1] = 'http://' . $match[1];
                             }
-                            return '<a href="l.php?url='.urlencode($match[1])
-                                .sprintf('&auth=%s" target="_blank">', $token)
-                                .$match[1].'</a>'.$match[9];
+                            if ($trampoline)
+                                return '<a href="l.php?url='.urlencode($match[1])
+                                    .sprintf('&auth=%s" target="_blank">', $token)
+                                    .$match[1].'</a>'.$match[9];
+                            else
+                                return sprintf('<a href="%s">%s</a>%s',
+                                    $match[1], $match[1], $match[9]);
                         } elseif ($match[6]) {
                             return sprintf('<a href="mailto:%1$s" target="_blank">%1$s</a>',
                                 $match[6]);
diff --git a/include/class.pdf.php b/include/class.pdf.php
index 404e4dd7bd942ad0114a919fe0efec83d1c83a28..8f41ec2db867a3e6487b664d7aac37a62ab6a763 100644
--- a/include/class.pdf.php
+++ b/include/class.pdf.php
@@ -293,15 +293,17 @@ class Ticket2PDF extends mPDF
                 $this->WriteCell($w, 7, Format::truncate($entry['title'], 50), 'TB', 0, 'L', true);
                 $this->WriteCell($w/2, 7, $entry['name'] ?: $entry['poster'], 'TBR', 1, 'L', true);
                 $this->SetFont('');
-                $text= $entry['body'];
+                $text = $entry['body']->display('pdf');
                 if($entry['attachments']
                         && ($tentry=$ticket->getThreadEntry($entry['id']))
                         && ($attachments = $tentry->getAttachments())) {
                     $files = array();
                     foreach($attachments as $attachment)
-                        $files[]= $attachment['name'];
+                        if (!$attachment['inline'])
+                            $files[]= $attachment['name'];
 
-                    $text.="<div>Files Attached: [".implode(', ',$files)."]</div>";
+                    if ($files)
+                        $text.="<div>Files Attached: [".implode(', ',$files)."]</div>";
                 }
                 $this->WriteHtml('<div class="thread-body">'.$text.'</div>', 2, false, false);
                 $this->Ln(5);
diff --git a/include/class.thread.php b/include/class.thread.php
index 02c215aae12d0797f7bc4c1fd4aa6746125a09b1..dcb4ec2afd8144c1c3bf06f3d95592b035a513f3 100644
--- a/include/class.thread.php
+++ b/include/class.thread.php
@@ -138,9 +138,12 @@ class Thread {
              .' ORDER BY thread.created '.$order;
 
         $entries = array();
-        if(($res=db_query($sql)) && db_num_rows($res))
-            while($rec=db_fetch_array($res))
+        if(($res=db_query($sql)) && db_num_rows($res)) {
+            while($rec=db_fetch_array($res)) {
+                $rec['body'] = ThreadBody::fromFormattedText($rec['body'], $rec['format']);
                 $entries[] = $rec;
+            }
+        }
 
         return $entries;
     }
@@ -305,15 +308,22 @@ Class ThreadEntry {
     }
 
     function getBody() {
-        return $this->ht['body'];
+        return ThreadBody::fromFormattedText($this->ht['body'], $this->ht['format']);
     }
 
     function setBody($body) {
         global $cfg;
 
+        if (!$body instanceof ThreadBody) {
+            if ($cfg->isHtmlThreadEnabled())
+                $body = new HtmlThreadBody($body);
+            else
+                $body = new TextThreadBody($body);
+        }
+
         $sql='UPDATE '.TICKET_THREAD_TABLE.' SET updated=NOW()'
-            .',body='.db_input(Format::sanitize($body,
-                !$cfg->isHtmlThreadEnabled()))
+            .',format='.db_input($body->getType())
+            .',body='.db_input((string) $body)
             .' WHERE id='.db_input($this->getId());
         return db_query($sql) && db_affected_rows();
     }
@@ -760,7 +770,7 @@ Class ThreadEntry {
     /* variables */
 
     function __toString() {
-        return $this->getBody();
+        return (string) $this->getBody();
     }
 
     function asVar() {
@@ -969,9 +979,7 @@ Class ThreadEntry {
                 $vars['body'] = new TextThreadBody($vars['body']);
         }
 
-        // Drop stripped images. NOTE: This should be done before
-        // ->convert() because the strippedImages list will not propagate to
-        // the newly converted thread body
+        // Drop stripped images
         if ($vars['attachments']) {
             foreach ($vars['body']->getStrippedImages() as $cid) {
                 foreach ($vars['attachments'] as $i=>$a) {
@@ -994,8 +1002,7 @@ Class ThreadEntry {
             }
         }
 
-        if (!($body = Format::sanitize(
-                (string) $vars['body']->convertTo('html'))))
+        if (!($body = $vars['body']->getClean()))
             $body = '-'; //Special tag used to signify empty message as stored.
 
         $poster = $vars['poster'];
@@ -1006,6 +1013,7 @@ Class ThreadEntry {
             .' ,thread_type='.db_input($vars['type'])
             .' ,ticket_id='.db_input($vars['ticketId'])
             .' ,title='.db_input(Format::sanitize($vars['title'], true))
+            .' ,format='.db_input($vars['body']->getType())
             .' ,staff_id='.db_input($vars['staffId'])
             .' ,user_id='.db_input($vars['userId'])
             .' ,poster='.db_input($poster)
@@ -1251,13 +1259,21 @@ class ThreadBody /* extends SplString */ {
     var $type;
     var $stripped_images = array();
     var $embedded_images = array();
+    var $options = array(
+        'strip-embedded' => true
+    );
 
-    function __construct($body, $type='text') {
+    function __construct($body, $type='text', $options=array()) {
         $type = strtolower($type);
         if (!in_array($type, static::$types))
             throw new Exception($type.': Unsupported ThreadBody type');
         $this->body = (string) $body;
         $this->type = $type;
+        $this->options = array_merge($this->options, $options);
+    }
+
+    function isEmpty() {
+        return !$this->body || $this->body == '-';
     }
 
     function convertTo($type) {
@@ -1307,21 +1323,66 @@ class ThreadBody /* extends SplString */ {
         return $this->embedded_images;
     }
 
+    function getType() {
+        return $this->type;
+    }
+
+    function getClean() {
+        return trim($this->body);
+    }
+
     function __toString() {
-        return $this->body;
+        return (string) $this->body;
+    }
+
+    function toHtml() {
+        return $this->display('html');
+    }
+
+    function display($format=false) {
+        throw new Exception('display: Abstract dispplay() method not implemented');
+    }
+
+    static function fromFormattedText($text, $format=false) {
+        switch ($format) {
+        case 'text':
+            return new TextThreadBody($text);
+        case 'html':
+            return new HtmlThreadBody($text, array('strip-embedded'=>false));
+        default:
+            return new ThreadBody($text);
+        }
     }
 }
 
 class TextThreadBody extends ThreadBody {
-    function __construct($body) {
-        parent::__construct(Format::stripEmptyLines($body), 'text');
+    function __construct($body, $options=array()) {
+        parent::__construct($body, 'text', $options);
+    }
+
+    function getClean() {
+        return Format::stripEmptyLines($this->body);
+    }
+
+    function display($output=false) {
+        if ($this->isEmpty())
+            return '(empty)';
+
+        switch ($output) {
+        case 'html':
+            return '<div style="white-space:pre-wrap">'.$this->body.'</div>';
+        case 'pdf':
+            return nl2br($this->body);
+        default:
+            return '<pre>'.$this->body.'</pre>';
+        }
     }
 }
 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 __construct($body, $options=array()) {
+        parent::__construct($body, 'html', $options);
+        if ($this->options['strip-embedded'])
+            $this->body = $this->extractEmbeddedHtmlImages($this->body);
     }
 
     function extractEmbeddedHtmlImages($body) {
@@ -1336,5 +1397,21 @@ class HtmlThreadBody extends ThreadBody {
             return 'src="cid:'.$info['cid'].'"';
         }, $body);
     }
+
+    function getClean() {
+        return trim($body, " <>br/\t\n\r") ? Format::sanitize($body) : '';
+    }
+
+    function display($output=false) {
+        if ($this->isEmpty())
+            return '(empty)';
+
+        switch ($output) {
+        case 'pdf':
+            return Format::clickableurls($this->body, false);
+        default:
+            return Format::display($this->body);
+        }
+    }
 }
 ?>
diff --git a/include/client/view.inc.php b/include/client/view.inc.php
index f26737719309736c0863e03877e7a9a1c302e60a..deaa097d9a92692728e0fe5fb3eddb49b2a27581 100644
--- a/include/client/view.inc.php
+++ b/include/client/view.inc.php
@@ -101,8 +101,6 @@ foreach (DynamicFormEntry::forTicket($ticket->getId()) as $idx=>$form) {
 if($ticket->getThreadCount() && ($thread=$ticket->getClientThread())) {
     $threadType=array('M' => 'message', 'R' => 'response');
     foreach($thread as $entry) {
-        if ($entry['body'] == '-')
-            $entry['body'] = '(EMPTY)';
 
         //Making sure internal notes are not displayed due to backend MISTAKES!
         if(!$threadType[$entry['thread_type']]) continue;
@@ -112,7 +110,7 @@ if($ticket->getThreadCount() && ($thread=$ticket->getClientThread())) {
         ?>
         <table class="thread-entry <?php echo $threadType[$entry['thread_type']]; ?>" cellspacing="0" cellpadding="1" width="800" border="0">
             <tr><th><?php echo Format::db_datetime($entry['created']); ?> &nbsp;&nbsp;<span class="textra"></span><span><?php echo $poster; ?></span></th></tr>
-            <tr><td class="thread-body"><div><?php echo Format::viewableImages(Format::display($entry['body'])); ?></div></td></tr>
+            <tr><td class="thread-body"><div><?php echo $entry['body']->toHtml(); ?></div></td></tr>
             <?php
             if($entry['attachments']
                     && ($tentry=$ticket->getThreadEntry($entry['id']))
diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php
index 1f05d04a5daf6445e1683d083862ca32937510de..f885a7ceff0e3ae2aa1307e9d93e0695137d6408 100644
--- a/include/staff/ticket-view.inc.php
+++ b/include/staff/ticket-view.inc.php
@@ -342,10 +342,7 @@ $tcount+= $ticket->getNumNotes();
     /* -------- Messages & Responses & Notes (if inline)-------------*/
     $types = array('M', 'R', 'N');
     if(($thread=$ticket->getThreadEntries($types))) {
-       foreach($thread as $entry) {
-           if ($entry['body'] == '-')
-               $entry['body'] = '(EMPTY)';
-           ?>
+       foreach($thread as $entry) { ?>
         <table class="thread-entry <?php echo $threadTypes[$entry['thread_type']]; ?>" cellspacing="0" cellpadding="1" width="940" border="0">
             <tr>
                 <th colspan="4" width="100%">
@@ -365,7 +362,7 @@ $tcount+= $ticket->getNumNotes();
             </tr>
             <tr><td colspan="4" class="thread-body" id="thread-id-<?php
                 echo $entry['id']; ?>"><div><?php
-                echo Format::viewableImages(Format::display($entry['body'])); ?></div></td></tr>
+                echo $entry['body']->toHtml(); ?></div></td></tr>
             <?php
             if($entry['attachments']
                     && ($tentry = $ticket->getThreadEntry($entry['id']))
diff --git a/include/upgrader/streams/core/d51f303a-dad45ca2.patch.sql b/include/upgrader/streams/core/d51f303a-dad45ca2.patch.sql
index 19ddac83bf11b1d262c167e6f73b190db63babb7..936b3b0c2273a18e9a9bdd9410cd321121cb1ed1 100644
--- a/include/upgrader/streams/core/d51f303a-dad45ca2.patch.sql
+++ b/include/upgrader/streams/core/d51f303a-dad45ca2.patch.sql
@@ -156,21 +156,9 @@ UPDATE `%TABLE_PREFIX%canned_response`
         '>', '&gt;'),
         '\n', '<br/>');
 
--- Migrate ticket-thread to HTML
--- XXX: Migrate & -> &amp; ? -- the problem is that there's a fix in 1.7.1
--- that properly encodes these characters, so encoding & would mean possible
--- double encoding.
-UPDATE `%TABLE_PREFIX%ticket_thread`
-    SET `body` = REPLACE( REPLACE( REPLACE( REPLACE( REPLACE( REPLACE( REPLACE( REPLACE(
-        `body`,
-        '\r', ''),
-        '\n ', '\n'),
-        '\n\n\n', '\n\n'),
-        '\n\n\n', '\n\n'),
-        '\n\n\n', '\n\n'),
-        '<', '&lt;'),
-        '>', '&gt;'),
-        '\n', '<br/>');
+-- Mark all thread entries as text
+ALTER TABLE `%TABLE_PREFIX%ticket_thread`
+  ADD `format` varchar(16) NOT NULL default 'text' AFTER `body`;
 
 -- Finished with patch
 UPDATE `%TABLE_PREFIX%config`
diff --git a/include/upgrader/streams/core/f5692e24-4323a6a8.patch.sql b/include/upgrader/streams/core/f5692e24-4323a6a8.patch.sql
index ee2296eb5754404acf176b006b250529554a3f55..a75a8372d3bbf205d9d85446a59ae923076f0075 100644
--- a/include/upgrader/streams/core/f5692e24-4323a6a8.patch.sql
+++ b/include/upgrader/streams/core/f5692e24-4323a6a8.patch.sql
@@ -136,6 +136,25 @@ UPDATE `%TABLE_PREFIX%content` SET `content_id` = LAST_INSERT_ID()
 DELETE FROM `%TABLE_PREFIX%email_template`
     WHERE `code_name` IN ('staff.pwreset', 'user.accesslink');
 
+-- The original patch for d51f303a-dad45ca2.patch.sql migrated all the
+-- thread entries from text to html. Now that the format column exists in
+-- the ticket_thread table, we opted to retroactively add the format column
+-- to the dad45ca2 patch. Therefore, anyone upgrading from osTicket < 1.8.0
+-- to v1.8.2 and further will alreay have a `format` column when they arrive
+-- at this patch. In such a case, we'll just change the default to 'html'
+SET @s = (SELECT IF(
+    (SELECT COUNT(*)
+        FROM INFORMATION_SCHEMA.COLUMNS
+        WHERE table_name = '%TABLE_PREFIX%ticket_thread'
+        AND table_schema = DATABASE()
+        AND column_name = 'format'
+    ) > 0,
+    "ALTER TABLE `%TABLE_PREFIX%ticket_thread` CHANGE `format` `format` varchar(16) NOT NULL default 'html'",
+    "ALTER TABLE `%TABLE_PREFIX%ticket_thread` ADD `format` varchar(16) NOT NULL default 'html' AFTER `body`"
+));
+PREPARE stmt FROM @s;
+EXECUTE stmt;
+
 -- Finished with patch
 UPDATE `%TABLE_PREFIX%config`
     SET `value` = '4323a6a81c35efbf7722b7fc4e475440'