diff --git a/setup/cli/modules/export.php b/setup/cli/modules/export.php
index 3b7589536fc8da44fd43b000e1be9c9412158e30..33d1320523b36db0b2299261093bc9cc391b25e2 100644
--- a/setup/cli/modules/export.php
+++ b/setup/cli/modules/export.php
@@ -18,6 +18,8 @@ require_once dirname(__file__) . "/class.module.php";
 require_once dirname(__file__) . '/../../../main.inc.php';
 
 require_once INCLUDE_DIR . 'class.json.php';
+require_once INCLUDE_DIR . 'class.migrater.php';
+require_once INCLUDE_DIR . 'class.signal.php';
 
 define('OSTICKET_BACKUP_SIGNATURE', 'osTicket-Backup');
 define('OSTICKET_BACKUP_VERSION', 'A');
@@ -30,16 +32,19 @@ class Exporter extends Module {
         FILE_CHUNK_TABLE, STAFF_TABLE, DEPT_TABLE, TOPIC_TABLE, GROUP_TABLE,
         GROUP_DEPT_TABLE, TEAM_TABLE, TEAM_MEMBER_TABLE, FAQ_TABLE,
         FAQ_ATTACHMENT_TABLE, FAQ_TOPIC_TABLE, FAQ_CATEGORY_TABLE,
-        CANNED_TABLE, CANNED_ATTACHMENT_TABLE, TICKET_TABLE, TICKET_THREAD_TABLE,
-        TICKET_ATTACHMENT_TABLE, TICKET_PRIORITY_TABLE, PRIORITY_TABLE,
+        CANNED_TABLE, CANNED_ATTACHMENT_TABLE, TICKET_TABLE,
+        TICKET_THREAD_TABLE, TICKET_ATTACHMENT_TABLE, TICKET_PRIORITY_TABLE,
         TICKET_LOCK_TABLE, TICKET_EVENT_TABLE, TICKET_EMAIL_INFO_TABLE,
-        EMAIL_TABLE, EMAIL_TEMPLATE_TABLE, FILTER_TABLE, FILTER_RULE_TABLE,
-        SLA_TABLE, API_KEY_TABLE, TIMEZONE_TABLE, PAGE_TABLE);
+        EMAIL_TABLE, EMAIL_TEMPLATE_TABLE, EMAIL_TEMPLATE_GRP_TABLE,
+        FILTER_TABLE, FILTER_RULE_TABLE, SLA_TABLE, API_KEY_TABLE,
+        TIMEZONE_TABLE, SESSION_TABLE, PAGE_TABLE);
 
     var $options = array(
-        'stream' => array('-o', '--output', 'default'=>'compress.zlib://php://stdout',
-            "File or stream to receive the exported output. As a default,
-            zlib compressed output is sent to standard out.")
+        'stream' => array('-o', '--output', 'default'=>'php://stdout',
+            'help'=> "File or stream to receive the exported output. As a
+            default, zlib compressed output is sent to standard out."),
+        'compress' => array('-z', '--compress', 'action'=>'store_true',
+            'help'=> "Send zlib compress data to the output stream"),
     );
 
     var $stream;
@@ -51,13 +56,23 @@ class Exporter extends Module {
 
     function run($args, $options) {
         global $ost;
-        $this->stream = fopen($options['stream'], 'w');
+
+        $stream = $options['stream'];
+        if ($options['compress']) $stream = "compress.zlib://$stream";
+        $this->stream = fopen($stream, 'w');
+
+        // Allow plugins to change the tables exported
+        Signal::send('export.tables', $this, $this->tables);
+
         $header = array(
             array(OSTICKET_BACKUP_SIGNATURE, OSTICKET_BACKUP_VERSION),
             array(
                 'version'=>THIS_VERSION,
                 'table_prefix'=>TABLE_PREFIX,
                 'salt'=>SECRET_SALT,
+                'dbtype'=>DBTYPE,
+                'streams'=>DatabaseMigrater::getUpgradeStreams(
+                    UPGRADE_DIR . 'streams/'),
             ),
         );
         $this->write_block($header);
@@ -77,6 +92,11 @@ class Exporter extends Module {
             $res = db_query("select * from $t");
             $types = array();
 
+            if (!$table) {
+                $this->stderr->write(
+                    $t.': Cannot export table with no fields'."\n");
+                die();
+            }
             $this->write_block(
                 array('table', substr($t, strlen(TABLE_PREFIX)), $table,
                     $indexes));
diff --git a/setup/cli/modules/import.php b/setup/cli/modules/import.php
new file mode 100644
index 0000000000000000000000000000000000000000..461b60ab0005f358885bc4fa2fc5e79a530c2c6c
--- /dev/null
+++ b/setup/cli/modules/import.php
@@ -0,0 +1,241 @@
+<?
+/*********************************************************************
+    cli/import.php
+
+    osTicket data importer, used for migration and backup recovery
+
+    Jared Hancock <jared@osticket.com>
+    Copyright (c)  2006-2013 osTicket
+    http://www.osticket.com
+
+    Released under the GNU General Public License WITHOUT ANY WARRANTY.
+    See LICENSE.TXT for details.
+
+    vim: expandtab sw=4 ts=4 sts=4:
+**********************************************************************/
+require_once dirname(__file__) . "/class.module.php";
+
+require_once dirname(__file__) . '/../../../main.inc.php';
+
+require_once INCLUDE_DIR . 'class.json.php';
+
+class Importer extends Module {
+    var $prologue =
+        "Imports data from a previous backup (using the exporter)";
+
+    var $options = array(
+        'stream' => array('-i', '--input', 'default'=>'php://stdin',
+            'metavar'=>'FILE', 'help'=>
+            "File or stream from which to read the export. As a default,
+            data is received from standard in."),
+        'compress' => array('-z', '--compress', 'action'=>'store_true',
+            'help'=>'Read zlib compressed data (use -z with the export
+                command)'),
+        'tables' => array('-t', '--table', 'action'=>'append',
+            'metavar'=>'TABLE', 'help'=>
+            "Table to be restored from the backup. Default is to restore all
+            tables. This option can be specified more than once"),
+        'drop' => array('-D', '--drop', 'action'=>'store_true', 'help'=>
+            'Issue DROP TABLE statements before the create statemente'),
+    );
+
+    var $epilog =
+        "The SQL of the import is written to standard outout";
+
+    var $stream;
+    var $header;
+    var $source_ost_info;
+
+    function verify_header() {
+        list($header, $info) = $this->read_block();
+        if (!$header || $header[0] != OSTICKET_BACKUP_SIGNATURE) {
+            $this->stderr->write('Header mismatch -- not an osTicket backup');
+            return false;
+        }
+        else
+            $this->header = $header;
+
+        if (!$info || $info['dbtype'] != 'mysql') {
+            $this->stderr->write('Only mysql imports are supported currently');
+            return false;
+        }
+        $this->source_ost_info = $info;
+        return true;
+    }
+
+    function read_block() {
+        $block = '';
+        while (!feof($this->stream) && (($c = fgetc($this->stream)) != "\x1e"))
+            $block .= $c;
+
+        if ($json = JsonDataParser::decode($block))
+            return $json;
+
+        if (strlen($block)) {
+            $this->stderr->write("Unable to read block from input");
+            die();
+        }
+    }
+
+    function send_statement($stmt) {
+        if ($this->getOption('prime-time'))
+            db_query($stmt);
+        else {
+            $this->stdout->write($stmt);
+            $this->stdout->write(";\n");
+        }
+    }
+
+    function import_table() {
+        if (!($header = $this->read_block()))
+            return false;
+
+        else if ($header[0] != 'table') {
+            $this->stderr->write('Unable to read table header');
+            return false;
+        }
+
+        // TODO: Consider included tables and excluded tables
+
+        $this->stderr->write("Importing table: {$header[1]}\n");
+        $this->create_table($header);
+        $this->create_indexes($header);
+
+        while (($row=$this->read_block())) {
+            if (isset($row[0]) && ($row[0] == 'end-table')) {
+                $this->load_row(null, null, true);
+                return true;
+            }
+            $this->load_row($header, $row);
+        }
+        return false;
+    }
+
+    function create_table($info) {
+        if ($this->getOption('drop'))
+            $this->send_statement('DROP TABLE IF EXISTS `'.TABLE_PREFIX.'`');
+        $sql = 'CREATE TABLE `'.TABLE_PREFIX.$info[1].'` (';
+        $pk = array();
+        $fields = array();
+        foreach ($info[2] as $col) {
+            $field = "`{$col['Field']}` {$col['Type']}";
+            if ($col['Null'] == 'NO')
+                $field .= ' NOT NULL ';
+            if ($col['Default'] == 'CURRENT_TIMESTAMP')
+                $field .= ' DEFAULT CURRENT_TIMESTAMP';
+            elseif ($col['Default'] !== null)
+                $field .= ' DEFAULT '.db_input($col['Default']);
+            $field .= ' '.$col['Extra'];
+            $fields[] = $field;
+        }
+        // Generate PRIMARY KEY
+        foreach ($info[3] as $idx) {
+            if ($idx['Key_name'] == 'PRIMARY') {
+                $col = '`'.$idx['Column_name'].'`';
+                if ($idx['Collation'] != 'A')
+                    $col .= ' DESC';
+                $pk[(int)$idx['Seq_in_index']] = $col;
+            }
+        }
+        $sql .= implode(", ", $fields);
+        if ($pk)
+            $sql .= ', PRIMARY KEY ('.implode(',',$pk).')';
+        $sql .= ') DEFAULT CHARSET=utf8';
+        $queries[] = $sql;
+        $this->send_statement($sql);
+    }
+
+    function create_indexes($header) {
+        $indexes = array();
+        foreach ($header[3] as $idx) {
+            if ($idx['Key_name'] == 'PRIMARY')
+                continue;
+            if (!isset($indexes[$idx['Key_name']]))
+                $indexes[$idx['Key_name']] = array(
+                    'cols'=>array(),
+                    // XXX: Drop table-prefix
+                    'table'=>substr($idx['Table'],
+                        strlen($this->source_ost_info['table_prefix'])),
+                    'type'=>$idx['Index_type'],
+                    'unique'=>!$idx['Non_unique']);
+            $index = &$indexes[$idx['Key_name']];
+            $col = '`'.$idx['Column_name'].'`';
+            if ($idx['Collation'] != 'A')
+                $col .= ' DESC';
+            $index[(int)$idx['Seq_in_index']] = $col;
+            $index['cols'][] = $col;
+        }
+        foreach ($indexes as $name=>$info) {
+            $cols = array();
+            $this->send_statement('CREATE '
+                .(($info['unique']) ? 'UNIQUE ' : '')
+                .'INDEX `'.$name
+                .'` USING '.$info['type']
+                .' ON `'.TABLE_PREFIX.$info['table'].'` ('
+                .implode(',', $info['cols'])
+                .')');
+        }
+    }
+
+    function truncate_table($info) {
+        $this->send_statement('TRUNCATE TABLE '.TABLE_PREFIX.$info[1]);
+        $indexes = array();
+        foreach ($info[3] as $idx) {
+            if ($idx['Key_name'] == 'PRIMARY')
+                continue;
+            $indexes[$idx['Key_name']] =
+                '`'.TABLE_PREFIX.$info[1].'`.`'.$idx['Key_name'].'`';
+        }
+        foreach ($indexes as $T=>$fqn)
+            $this->send_statement('DROP INDEX IF EXISTS '.$fqn);
+    }
+
+    function load_row($info, $row, $flush=false) {
+        static $header = null;
+        static $rows = array();
+        static $length = 0;
+
+        if ($info && $header === null) {
+            $header = "INSERT INTO `".TABLE_PREFIX.$info[1].'` (';
+            $cols = array();
+            foreach ($info[2] as $col)
+                $cols[] = "`{$col['Field']}`";
+            $header .= implode(', ', $cols);
+            $header .= ") VALUES ";
+        }
+        if ($row) {
+            $values = array();
+            foreach ($info[2] as $i=>$col)
+                $values[] = (is_numeric($row[$i]))
+                    ? $row[$i]
+                    : ($row[$i] ? '0x'.bin2hex($row[$i]) : "''");
+            $values = "(" . implode(', ', $values) . ")";
+            $length += strlen($values);
+            $rows[] = &$values;
+        }
+        if (($flush || $length > 16000) && $header) {
+            $this->send_statement($header . implode(',', $rows));
+            $header = null;
+            $rows = array();
+            $length = 0;
+        }
+    }
+
+    function run($args, $options) {
+        $stream = $options['stream'];
+        if ($options['compress']) $stream = "compress.zlib://$stream";
+        if (!($this->stream = fopen($stream, 'rb'))) {
+            $this->stderr->write('Unable to open input stream');
+            die();
+        }
+
+        if (!$this->verify_header())
+            die('Unable to verify backup header');
+
+        while ($this->import_table());
+        @fclose($this->stream);
+    }
+}
+
+Module::register('import', 'Importer');
+?>