diff --git a/include/class.export.php b/include/class.export.php
new file mode 100644
index 0000000000000000000000000000000000000000..7d6a7e7ec87b5939d8a7657370ea61626c06700c
--- /dev/null
+++ b/include/class.export.php
@@ -0,0 +1,135 @@
+<?php
+/*************************************************************************
+    class.export.php
+    
+    Exports stuff (details to follow)
+
+    Jared Hancock <jared@osticket.com>
+    Copyright (c)  2006-2012 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:
+**********************************************************************/
+
+class Export {
+    
+    /* static */ function dumpQuery($sql, $headers, $how='csv') {
+        $exporters = array(
+            'csv' => CsvResultsExporter,
+            'json' => JsonResultsExporter
+        );
+        $exp = new $exporters[$how]($sql, $headers);
+        return $exp->dump();
+    }
+
+    # XXX: Think about facilitated exporting. For instance, we might have a
+    #      TicketExporter, which will know how to formulate or lookup a
+    #      formatl query (SQL), and cooperate with the output process to add
+    #      extra (recursive) information. In this funciton, the top-level
+    #      SQL is exported, but for something like tickets, we will need to
+    #      export attached messages, reponses, and notes, as well as
+    #      attachments associated with each, ...
+    /* static */ function dumpTickets($sql, $how='csv') {
+        return self::dumpQuery($sql,
+            array(
+                'ticketID' =>       'Ticket Id',
+                'created' =>        'Date',
+                'subject' =>        'Subject',
+                'name' =>           'From',
+                'priority_desc' =>  'Priority',
+                'dept_name' =>      'Department',
+                'source' =>         'Source',
+                'status' =>         'Current Status'
+            ),
+            $how);
+    }
+
+    /* static */ function saveTickets($sql, $filename, $how='csv') {
+        ob_start();
+        self::dumpTickets($sql, $how);
+        $stuff = ob_get_contents();
+        ob_end_clean();
+        if ($stuff)
+            Http::download($filename, "text/$how", $stuff);
+
+        return false;
+    }
+}
+
+class ResultSetExporter {
+    function ResultSetExporter($sql, $headers) {
+        $this->headers = array_values($headers);
+        $this->_res = db_query($sql);
+        if ($row = db_fetch_array($this->_res)) {
+            $query_fields = array_keys($row);
+            $this->headers = array();
+            $this->keys = array();
+            $this->lookups = array();
+            foreach ($headers as $field=>$name) {
+                if (isset($row[$field])) {
+                    $this->headers[] = $name;
+                    $this->keys[] = $field;
+                    # Remember the location of this header in the query results
+                    # (column-wise) so we don't have to do hashtable lookups for every
+                    # column of every row.
+                    $this->lookups[] = array_search($field, $query_fields);
+                }
+            }
+            db_data_reset($this->_res);
+        }
+    }
+
+    function getHeaders() {
+        return $this->headers;
+    }
+
+    function next() {
+        if (!($row = db_fetch_row($this->_res)))
+            return false;
+
+        $record = array();
+        foreach ($this->lookups as $idx)
+            $record[] = $row[$idx];
+        return $record;
+    }
+
+    function nextArray() {
+        if (!($row = $this->next()))
+            return false;
+        return array_combine($this->keys, $row);
+    }
+
+    function dump() {
+        # Useful for debug output
+        while ($row=$this->nextArray()) {
+            var_dump($row);
+        }
+    }
+}
+
+class CsvResultsExporter extends ResultSetExporter {
+    function dump() {
+        echo '"' . implode('","', $this->getHeaders()) . "\"\n";
+        while ($row=$this->next()) {
+            foreach ($row as &$val)
+                # Escape enclosed double-quotes
+                $val = str_replace('"','""',$val);
+            echo '"' . implode('","', $row) . "\"\n";
+        }
+    }
+}
+
+class JsonResultsExporter extends ResultSetExporter {
+    function dump() {
+        require_once(INCLUDE_DIR.'class.json.php');
+        $exp = new JsonDataEncoder();
+        $rows = array();
+        while ($row=$this->nextArray()) {
+            $rows[] = $row;
+        }
+        echo $exp->encode($rows);
+    }
+}
diff --git a/include/class.http.php b/include/class.http.php
index d134e6b8f3aef21f50440b175d8aa7249dc08676..37ad0302719cd8c96586dd03ed864c0dd07bc3ee 100644
--- a/include/class.http.php
+++ b/include/class.http.php
@@ -50,5 +50,27 @@ class Http {
         }
         exit;
     }
+
+    function download($filename, $type, $data=null) {
+        header('Pragma: public');
+        header('Expires: 0');
+        header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
+        header('Cache-Control: public');
+        header('Content-Type: '.$type);
+        $user_agent = strtolower ($_SERVER['HTTP_USER_AGENT']);
+        if (strpos($user_agent,'msie') !== false
+                && strpos($user_agent,'win') !== false) {
+            header('Content-Disposition: filename="'.basename($filename).'";');
+        } else {
+            header('Content-Disposition: attachment; filename="'
+                .basename($filename).'"');
+        }
+        header('Content-Transfer-Encoding: binary');
+        if ($data !== null) {
+            header('Content-Length: '.strlen($data));
+            print $data;
+            exit;
+        }
+    }
 }
 ?>
diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php
index 3bfc8156ace212268d53e34be15f6ccaafb4b4c3..14d0514ecb43f7b42eed6b5740c2ce51d423b2fa 100644
--- a/include/staff/tickets.inc.php
+++ b/include/staff/tickets.inc.php
@@ -237,6 +237,8 @@ $qfrom.=' LEFT JOIN '.TICKET_PRIORITY_TABLE.' pri ON (ticket.priority_id=pri.pri
 
 $query="$qselect $qfrom $qwhere $qgroup ORDER BY $order_by $order LIMIT ".$pageNav->getStart().",".$pageNav->getLimit();
 //echo $query;
+$hash = md5($query);
+$_SESSION['search_'.$hash] = $query;
 $res = db_query($query);
 $showing=db_num_rows($res)?$pageNav->showing():"";
 if(!$results_type) {
@@ -486,7 +488,9 @@ $basic_display=!isset($_REQUEST['advance_search'])?true:false;
     </table>
     <?php
     if($num>0){ //if we actually had any tickets returned.
-        echo '<div>&nbsp;Page:'.$pageNav->getPageLinks().'&nbsp;</div>';
+        echo '<div>&nbsp;Page:'.$pageNav->getPageLinks().'&nbsp;';
+        echo '<a class="export-csv" href="?a=export&h='
+            .$hash.'&status='.$_REQUEST['status'] .'">Export</a></div>';
     ?>
         <?php
          if($thisstaff->canManageTickets()) { ?>
diff --git a/scp/tickets.php b/scp/tickets.php
index da9dfa97e1ea8f37df2fcc718a950da6027d12c7..3d271e123d2e1e8ebd2e2353b6635a6c830d5f21 100644
--- a/scp/tickets.php
+++ b/scp/tickets.php
@@ -460,6 +460,16 @@ if($ticket) {
     $inc = 'tickets.inc.php';
     if($_REQUEST['a']=='open' && $thisstaff->canCreateTickets())
         $inc = 'ticket-open.inc.php';
+    elseif($_REQUEST['a'] == 'export') {
+        require_once(INCLUDE_DIR.'class.export.php');
+        $ts = strftime('%Y%m%d');
+        if (!($token=$_REQUEST['h']))
+            $errors['err'] = 'Query token required';
+        elseif (!($query=$_SESSION['search_'.$token]))
+            $errors['err'] = 'Query token not found';
+        elseif (!Export::saveTickets($query, "tickets-$ts.csv", 'csv'))
+            $errors['err'] = 'Internal error: Unable to dump query results';
+    }
     elseif(!$_POST && $_REQUEST['a']!='search'  && ($min=$thisstaff->getRefreshRate()))
         define('AUTO_REFRESH',1); //set refresh rate if the user has it configured
 }