From Request Tracker Wiki
Jump to navigation Jump to search


This script connects to the RT database and exports all of the mail into mbox format. An mbox file is created for each queue in the current directory. It does a little bit of subject tweaking to try to encourage good threading habits, and the results imported into Mail.app very nicely. This script also extremely fast, since it doesn't use the RT API.

A to how to use it, well just run it and it will tell you how. :-)


  • DBI
  • Your DBD (DBD::mysql, DBD::Pg, etc.).
  • Mail::Box perl-module
  • POSIX (comes with Perl)
  • Getopt::Long (comes with Perl)


  • I've tested this with my PostgreSQL database and added code so that it should also work with MySQL, but all other databases might have to tweak the $epoch variable.
#!/usr/bin/perl -w
 use strict;
 use DBI;
 use Getopt::Long qw(:config auto_help auto_version);
 use POSIX qw(asctime);
 our $VERSION = '0.01';
     'dsn|d=s'          => \my $dsn,
     'username|u=s'     => \my $username,
     'password|p=s'     => \my $password,
     'rtname|n=s'       => \my $rtname,
     'no-autoreplies|x' => \my $noauto,
 )  or require Pod::Usage && Pod::Usage::pod2usage(2);
 require Pod::Usage && Pod::Usage::pod2usage(1) unless $dsn && $rtname;
 my $dbh = DBI->connect(
     { RaiseError => 1 }
 END {
     $dbh->disconnect if $dbh;
 my $qsel = $dbh->prepare(q{
     SELECT id, name, correspondaddress, commentaddress
       FROM queues
      ORDER BY name
 my $exclude = $noauto ? "\n       AND users.id <> 1" : '';
 my $epoch   = $dsn =~ /^dbi:Pg/
     ? 'EXTRACT( EPOCH FROM attachments.created )'
     : 'UNIX_TIMESTAMP( attachments.created )';
 my $msel = $dbh->prepare(qq{
     SELECT tickets.id, transactions.id, transactions.type, attachments.id,
            parent, users.emailaddress,
            contenttype, COALESCE( contentencoding, '' ),
            headers, content, tickets.subject
       FROM tickets, transactions, attachments, users
      WHERE tickets.queue = ?
        AND tickets.id = transactions.objectid
        AND transactions.objecttype = ?
        AND transactions.id = attachments.transactionid
        AND transactions.creator = users.id$exclude
      ORDER BY transactions.id, attachments.id, parent
 $qsel->bind_columns( \my ( $qid, $qname, $correspond, $comment ) );
 while ($qsel->fetch) {
     # Normalize the name.
     $qname =~ s{[/.]}{}g;
     # Open the mbox file.
     open my $mbox, '>', "$qname.mbox" or die "Cannot open $qname.mbox: $!\n";
     $msel->execute( $qid, 'RT::Ticket' );
     $msel->bind_columns(\my ( $tid, $txid, $ttype, $aid, $parent, $email, $type, $encoding, $time, $headers, $body, $subject));
     my @parents;
     my @bounds;
     while ($msel->fetch) {
         if (!$parent) {
             # Top level of a message. Close the previous message.
             @parents = ();
             while (my $bound = pop @bounds) {
                 print $mbox "--$bound--\n\n";
             # Start the new message.
             ( my $asctime = asctime( gmtime $time ) ) =~ s/\s+$//ms;
             $email ||= $ttype =~ /comment/i ? $comment : $correspond;
             print $mbox "From $email $asctime\n";
             # Make sure that the body ends in a blank line.
             $body =~ s/(\n{0,2})?$/\n\n/ if $body;
             # Adjust headers. They always end with a single newline.
             $headers .= "RT-Transaction-Id: $txid\n";
             if ( $headers !~ /^From:/mi ) {
                 $headers .= "From: $email\n";
             if ( $headers !~ /^Subject:/mi ) {
                 $headers .= "Subject: [$rtname #$tid] $subject\n";
             } else {
                 $headers =~ s/^(Subject:\s*(?:(?:Re|Fwd):\s*)?)/$1\[$rtname #$tid] /mi
                     unless $headers =~ /^Subject:\s*(?:(?:Re|Fwd):\s*)?[[]\Q$rtname\E/mi
         } else {
             # Close any parents.
             while (@parents && $parents[-1] != $parent) {
                 pop @parents;
                 my $bound = pop @bounds;
                 print $mbox "--$bound--\n\n";
         # Add the Attachment ID to the headers, so simplify backtracking.
         $headers .= "RT-Attachment-Id: $aid\n";
         # Handle this part.
         if ($type =~ m{^multipart/}) {
             # Part is multipart. Push the parent ID onto the stack.
             push @parents, $aid;
             # Determine the bounary.
             my $bound;
             if ($headers =~ qr{\bboundary=(.+)}mi) {
                 # Retain the original boundary.
                 ($bound = $1) =~ s/^"//;
                 $bound =~ s/"$//;
             } else {
                 # Just invent a boundary.
                 $bound = join '', ('a'..'z', 'A'..'Z', 0..9)[ map { rand 62 } 0..10];
             # Output the part.
             print $mbox "--$bounds[-1]\n" if @bounds;
             print $mbox "$headers\n";
             print $mbox $body if $body;
             # Push the boundary onto the stack.
             push @bounds, $bound;
         } else {
             # Is a standalone part.
             print $mbox "--$bounds[-1]\n" if @bounds;
             print $mbox "$headers\n";
             print $mbox $body if $body;
     close $mbox or die "Cannot close $qname.mbox: $!\n";
 =head1 Name
 rt2mbox - Export RT messages to mbox files, one for each queue
 =head1 Synopsis
 =begin comment
 Fake-out Pod::Usage
 =head1 SYNOPSIS
 =end comment
   rt2mbox --dsn dbi:Pgdbname=rt3 --username rt_user --password secret
 =head1 Options
   -d --dsn DSN            RT database DSN to which DBI can connect. Required.
   -n --rtname   NAME      RT name, usually domain name. Required.
   -u --username USERNAME  RT database username.
   -p --password PASSWORD  RT database password.
   -x --no-autoreplies     Do not export autoreply messages.
 =head1 Author
 David E. Wheeler <david@kineticode.com>
 =head1 Copyright and License
 Copyright (c) 2008 David E. Wheeler. All Rights Reserved.
 This module is free software; you can redistribute it and/or modify it under
 the same terms as Perl itself.

User Notes To get this to work for mysql in rt 3.8.2 I had to change all the references to tables in the script to start with capitals eg "From queues" changed to From Queues otherwise I got errors saying the tables didn't exist.