diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php
index 27e4a0afe186e550db4ae74051f9fea50fa40b09..a2bef2a62da7e27e60559d17c4f7bece216fe8ab 100644
--- a/include/class.dynamic_forms.php
+++ b/include/class.dynamic_forms.php
@@ -271,7 +271,7 @@ class TicketForm extends DynamicForm {
     }
 }
 // Add fields from the standard ticket form to the ticket filterable fields
-Filter::addSupportedMatches('Custom Fields', function() {
+Filter::addSupportedMatches('Ticket Data', function() {
     $matches = array();
     foreach (TicketForm::getInstance()->getFields() as $f) {
         if (!$f->hasData())
diff --git a/include/class.filter.php b/include/class.filter.php
index a17962cf2e9c44e50f44c1b1f3571c3fd4eadd0c..8854ea89e03095d003f2cb3bea407c8b5e062a91 100644
--- a/include/class.filter.php
+++ b/include/class.filter.php
@@ -19,11 +19,11 @@ class Filter {
     var $ht;
 
     static $match_types = array(
-        'Basic Fields' => array(
+        'User Information' => array(
             'name'      => 'Name',
             'email'     => 'Email',
-            'subject'   => 'Subject',
-            'body'      => 'Body/Text',
+        ),
+        'Email Meta-Data' => array(
             'reply-to'  => 'Reply-To Email',
             'reply-to-name' => 'Reply-To Name',
         ),
@@ -242,12 +242,14 @@ class Filter {
 
         $how = array(
             # how => array(function, null or === this, null or !== this)
-            'equal'     => array('strcmp', 0),
-            'not_equal' => array('strcmp', null, 0),
-            'contains'  => array('strpos', null, false),
-            'dn_contain'=> array('strpos', false),
-            'starts'    => array('strpos', 0),
-            'ends'      => array('endsWith', true)
+            'equal'     => array('strcasecmp', 0),
+            'not_equal' => array('strcasecmp', null, 0),
+            'contains'  => array('stripos', null, false),
+            'dn_contain'=> array('stripos', false),
+            'starts'    => array('stripos', 0),
+            'ends'      => array('iendsWith', true),
+            'match'     => array('pregMatchB', 1),
+            'not_match' => array('pregMatchB', null, 0),
         );
 
         $match = false;
@@ -260,12 +262,8 @@ class Filter {
         foreach ($this->getRules() as $rule) {
             if (!isset($how[$rule['h']])) continue;
             list($func, $pos, $neg) = $how[$rule['h']];
-            # TODO: convert $what and $rule['v'] to mb_strtoupper and do
-            #       case-sensitive, binary-safe comparisons. Would be really
-            #       nice to do $rule['v'] on the database side for
-            #       performance -- but ::getFlatRules() is a blocker
-            $result = call_user_func($func, strtoupper($what[$rule['w']]),
-                strtoupper($rule['v']));
+
+            $result = call_user_func($func, $what[$rule['w']], $rule['v']);
             if (($pos === null && $result !== $neg) or ($result === $pos)) {
                 # Match.
                 $match = true;
@@ -341,7 +339,9 @@ class Filter {
             'contains'=>    'Contains',
             'dn_contain'=>  'Does Not Contain',
             'starts'=>      'Starts With',
-            'ends'=>        'Ends With'
+            'ends'=>        'Ends With',
+            'match'=>       'Matches Regex',
+            'not_match'=>   'Does Not Match Regex',
         );
     }
 
@@ -404,6 +404,14 @@ class Filter {
         $rules=array();
         for($i=1; $i<=25; $i++) { //Expecting no more than 25 rules...
             if($vars["rule_w$i"] || $vars["rule_h$i"]) {
+                // Check for REGEX compile errors
+                if (in_array($vars["rule_h$i"], array('match','not_match'))) {
+                    $wrapped = "/".$vars["rule_v$i"]."/iu";
+                    if (false === @preg_match($vars["rule_v$i"], ' ')
+                            && (false !== @preg_match($wrapped, ' ')))
+                        $vars["rule_v$i"] = $wrapped;
+                }
+
                 if(!$vars["rule_w$i"] || !in_array($vars["rule_w$i"],$matches))
                     $errors["rule_$i"]='Invalid match selection';
                 elseif(!$vars["rule_h$i"] || !in_array($vars["rule_h$i"],$types))
@@ -414,6 +422,12 @@ class Filter {
                         && $vars["rule_h$i"]=='equal'
                         && !Validator::is_email($vars["rule_v$i"]))
                     $errors["rule_$i"]='Valid email required for the match type';
+                elseif (in_array($vars["rule_h$i"], array('match','not_match'))
+                        && (false === @preg_match($vars["rule_v$i"], ' ')))
+                    $errors["rule_$i"] = sprintf('Regex compile error: (#%s)',
+                        preg_last_error());
+
+
                 else //for everything-else...we assume it's valid.
                     $rules[]=array('what'=>$vars["rule_w$i"],
                         'how'=>$vars["rule_h$i"],'val'=>$vars["rule_v$i"]);
@@ -902,13 +916,17 @@ class TicketFilter {
  * Returns TRUE if the haystack ends with needle and FALSE otherwise.
  * Thanks, http://stackoverflow.com/a/834355
  */
-function endsWith($haystack, $needle)
+function iendsWith($haystack, $needle)
 {
-    $length = strlen($needle);
+    $length = mb_strlen($needle);
     if ($length == 0) {
         return true;
     }
 
-    return (substr($haystack, -$length) === $needle);
+    return (strcasecmp(mb_substr($haystack, -$length), $needle) === 0);
+}
+
+function pregMatchB($subject, $pattern) {
+    return preg_match($pattern, $subject);
 }
 ?>
diff --git a/include/staff/filter.inc.php b/include/staff/filter.inc.php
index 37ac6c69925bf4dc15711f0a76767803aeacc66a..74e72106859d68501fbc6e490d79a9ddf90a4869 100644
--- a/include/staff/filter.inc.php
+++ b/include/staff/filter.inc.php
@@ -121,7 +121,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
         for($i=1; $i<=$n; $i++){ ?>
         <tr id="r<?php echo $i; ?>">
             <td colspan="2">
-                <div  style="width:700px; float:left;">
+                <div>
                     <select name="rule_w<?php echo $i; ?>">
                         <option value="">&mdash; Select One &dash;</option>
                         <?php
@@ -143,15 +143,14 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                         }
                         ?>
                     </select>
-                    <input type="text" size="30" name="rule_v<?php echo $i; ?>" value="<?php echo $info["rule_v$i"]; ?>">
+                    <input type="text" size="60" name="rule_v<?php echo $i; ?>" value="<?php echo $info["rule_v$i"]; ?>">
                     &nbsp;<span class="error">&nbsp;<?php echo $errors["rule_$i"]; ?></span>
-                </div>
                 <?php
                 if($info["rule_w$i"] || $info["rule_h$i"] || $info["rule_v$i"]){ ?>
                 <div style="float:right;text-align:right;padding-right:20px;"><a href="#" class="clearrule">(clear)</a></div>
                 <?php
                 } ?>
-                <div class="clear"></div>
+                </div>
             </td>
         </tr>
         <?php
diff --git a/include/upgrader/streams/core.sig b/include/upgrader/streams/core.sig
index 6dad7bd6c28059aac49c966dd472d51ddcf6e66a..3ac67bf82846c663c880c6fc441e5e108e310989 100644
--- a/include/upgrader/streams/core.sig
+++ b/include/upgrader/streams/core.sig
@@ -1 +1 @@
-f1ccd3bb620e314b0ae1dbd0a1a99177
+f5692e24c7afba7ab6168dde0b3bb3c8
diff --git a/include/upgrader/streams/core/ed60ba20-934954de.patch.sql b/include/upgrader/streams/core/ed60ba20-934954de.patch.sql
index a9b2ec364841d77ef62fe4f13b01367796d5da0f..39c210f4d1d7d9ffa65150243cb4cf81db96d552 100644
--- a/include/upgrader/streams/core/ed60ba20-934954de.patch.sql
+++ b/include/upgrader/streams/core/ed60ba20-934954de.patch.sql
@@ -17,11 +17,8 @@ UPDATE `%TABLE_PREFIX%filter_rule`
 -- [#331](https://github.com/osTicket/osTicket-1.8/issues/331)
 -- Previously there was no primary key on the %ticket_email_info table, so
 -- clean up any junk records before adding one
-DELETE FROM `%TABLE_PREFIX%ticket_email_info` WHERE
-    `message_id` = 0 OR `message_id` IS NULL;
 ALTER TABLE `%TABLE_PREFIX%ticket_email_info`
     CHANGE `message_id` `thread_id` int(11) unsigned NOT NULL,
-    ADD PRIMARY KEY (`thread_id`),
     DROP INDEX  `message_id`,
     ADD INDEX  `email_mid` (`email_mid`);
 
diff --git a/include/upgrader/streams/core/f1ccd3bb-f5692e24.cleanup.sql b/include/upgrader/streams/core/f1ccd3bb-f5692e24.cleanup.sql
new file mode 100644
index 0000000000000000000000000000000000000000..109cf6c02c663533571b1e15c28086e213ddbcae
--- /dev/null
+++ b/include/upgrader/streams/core/f1ccd3bb-f5692e24.cleanup.sql
@@ -0,0 +1,18 @@
+/**
+ * @version v1.8.1
+ * @signature f5692e24c7afba7ab6168dde0b3bb3c8
+ * @title Add regex field to ticket filters
+ *
+ * This fixes a glitch introduced @934954de8914d9bd2bb8343e805340ae where
+ * a primary key was added to the %ticket_email_info table so that deleting
+ * can be supported in a clustered environment. The patch added the
+ * `thread_id` column as the primary key, which was incorrect, because the
+ * `thread_id` may be null when rejected emails are recorded so they are
+ * never considered again if found in the inbox.
+ */
+
+-- Add the primary key. The PK on `thread_id` would have been removed in the
+-- task if it existed
+ALTER TABLE `%TABLE_PREFIX%ticket_email_info`
+    ADD `id` int(11) unsigned not null auto_increment FIRST,
+    ADD PRIMARY KEY (`id`);
diff --git a/include/upgrader/streams/core/f1ccd3bb-f5692e24.patch.sql b/include/upgrader/streams/core/f1ccd3bb-f5692e24.patch.sql
new file mode 100644
index 0000000000000000000000000000000000000000..fab41f1bc758901bfc0a7537643556a1a90dadb9
--- /dev/null
+++ b/include/upgrader/streams/core/f1ccd3bb-f5692e24.patch.sql
@@ -0,0 +1,46 @@
+/**
+ * @version v1.8.1
+ * @signature f5692e24c7afba7ab6168dde0b3bb3c8
+ * @title Add regex field to ticket filters
+ *
+ * This fixes a glitch introduced @934954de8914d9bd2bb8343e805340ae where
+ * a primary key was added to the %ticket_email_info table so that deleting
+ * can be supported in a clustered environment. The patch added the
+ * `thread_id` column as the primary key, which was incorrect, because the
+ * `thread_id` may be null when rejected emails are recorded so they are
+ * never considered again if found in the inbox.
+ */
+
+-- [#479](https://github.com/osTicket/osTicket-1.8/issues/479)
+-- Add (not)_match to the filter_rule `how`
+ALTER TABLE `%TABLE_PREFIX%filter_rule`
+    CHANGE `how` `how` enum('equal','not_equal','contains','dn_contain','starts','ends','match','not_match')
+    NOT NULL;
+
+-- Allow `isactive` to be `-1` for collaborators, which might indicate
+-- something like 'unsubscribed'
+ALTER TABLE `%TABLE_PREFIX%ticket_collaborator`
+    CHANGE `isactive` `isactive` tinyint(1) NOT NULL DEFAULT '1';
+
+-- There is no `subject` available in the filter::apply method for anything but email
+UPDATE `%TABLE_PREFIX%filter_rule`
+    SET `what` = CONCAT('field.', (
+        SELECT field.`id` FROM `%TABLE_PREFIX%form_field` field
+        JOIN `%TABLE_PREFIX%form` form ON (field.form_id = form.id)
+        WHERE field.`name` = 'subject' AND form.`type` = 'T'
+        ))
+    WHERE `what` = 'subject';
+
+-- There is no `body` available in the filter::apply method for anything but emails
+UPDATE `%TABLE_PREFIX%filter_rule`
+    SET `what` = CONCAT('field.', (
+        SELECT field.`id` FROM `%TABLE_PREFIX%form_field` field
+        JOIN `%TABLE_PREFIX%form` form ON (field.form_id = form.id)
+        WHERE field.`name` = 'message' AND form.`type` = 'T'
+        ))
+    WHERE `what` = 'body';
+
+-- Finished with patch
+UPDATE `%TABLE_PREFIX%config`
+    SET `value` = 'f5692e24c7afba7ab6168dde0b3bb3c8'
+    WHERE `key` = 'schema_signature' AND `namespace` = 'core';
diff --git a/include/upgrader/streams/core/f1ccd3bb-f5692e24.task.php b/include/upgrader/streams/core/f1ccd3bb-f5692e24.task.php
new file mode 100644
index 0000000000000000000000000000000000000000..fd58fe77064f1a81e41e6dc7a31ba0daac3a1de9
--- /dev/null
+++ b/include/upgrader/streams/core/f1ccd3bb-f5692e24.task.php
@@ -0,0 +1,27 @@
+<?php
+
+/*
+ * Drops the `thread_id` primary key on the ticket_email_info table if it
+ * exists
+ */
+
+class DropTicketEmailInfoPk extends MigrationTask {
+    var $description = "Reticulating splines";
+
+    function run($max_time) {
+        $sql = 'SELECT `INDEX_NAME` FROM information_schema.statistics
+          WHERE table_schema = '.db_input(DBNAME)
+           .' AND table_name = '.db_input(TICKET_EMAIL_INFO_TABLE)
+           .' AND column_name = '.db_input('thread_id');
+        if ($name = db_result(db_query($sql))) {
+            if ($name == 'PRIMARY') {
+                db_query('ALTER TABLE `'.TICKET_EMAIL_INFO_TABLE
+                    .'` DROP PRIMARY KEY');
+            }
+        }
+    }
+}
+
+return 'DropTicketEmailInfoPk';
+
+?>
diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql
index 4b51a63c9410e742ac716eb759a61670ef968799..85f06999698f21093930575429e967ff7e28ab29 100644
--- a/setup/inc/streams/core/install-mysql.sql
+++ b/setup/inc/streams/core/install-mysql.sql
@@ -276,7 +276,7 @@ CREATE TABLE `%TABLE_PREFIX%filter_rule` (
   `id` int(11) unsigned NOT NULL auto_increment,
   `filter_id` int(10) unsigned NOT NULL default '0',
   `what` varchar(32) NOT NULL,
-  `how` enum('equal','not_equal','contains','dn_contain','starts','ends') NOT NULL,
+  `how` enum('equal','not_equal','contains','dn_contain','starts','ends','match','not_match') NOT NULL,
   `val` varchar(255) NOT NULL,
   `isactive` tinyint(1) unsigned NOT NULL DEFAULT '1',
   `notes` tinytext NOT NULL,
@@ -572,10 +572,11 @@ CREATE TABLE `%TABLE_PREFIX%ticket_lock` (
 
 DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_email_info`;
 CREATE TABLE `%TABLE_PREFIX%ticket_email_info` (
+  `id` int(11) unsigned NOT NULL auto_increment,
   `thread_id` int(11) unsigned NOT NULL,
   `email_mid` varchar(255) NOT NULL,
   `headers` text,
-  PRIMARY KEY (`thread_id`),
+  PRIMARY KEY (`id`),
   KEY `email_mid` (`email_mid`)
 ) DEFAULT CHARSET=utf8;
 
@@ -631,7 +632,7 @@ CREATE TABLE `%TABLE_PREFIX%ticket_thread` (
 
 CREATE TABLE `%TABLE_PREFIX%ticket_collaborator` (
   `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
-  `isactive` tinyint(1) unsigned NOT NULL DEFAULT '1',
+  `isactive` tinyint(1) NOT NULL DEFAULT '1',
   `ticket_id` int(11) unsigned NOT NULL DEFAULT '0',
   `user_id` int(11) unsigned NOT NULL DEFAULT '0',
   -- M => (message) clients, N => (note) 3rd-Party, R => (reply) external authority