Using PHP to migrate email between servers

Let’s say you want to move your email from one provider to another (Google to Outlook, your ISP to your new ISP). If you don’t run your own mail server1, you might have to do this every few years. I’ve done it three or four times in my life, and I’ve only been using email regularly for maybe fifteen years.

The first couple times I did this, I just put both accounts in an IMAP client (probably Thunderbird, maybe Outlook, I don’t remember), and did a bunch of drag-and-dropping between the two. And it worked, but it required some babysitting. When I had to do this for work (several years ago), though, that wasn’t terribly practical for a couple hundred other users’ mailboxes.

There’s probably better ways to do this, most likely with Perl2, but I never really got into Perl. I’ve done more than a bit of Web stuff, though, and PHP is famous/notorious for its kitchen-sink nature, so what the heck.


// usage: mailmigr.php srcuser srcpass [dstuser dstpass]
// if dstuser and dstpass aren't provided, we just copy srcuser/srcpass

// Use to learn how
// to craft these mailbox definition strings
define ('SRC', "{old.mail.server:143/imap4rev1/novalidate-cert/debug}") ;
define ('DST', "{new.mail.server:143/imap4rev1/notls/debug}") ;

// a couple examples I've used in the past:
// define ('SRC', "{}") ;
// define ('DST', "{}") ;

// Create an exit handler for later, after we're in the IMAP transaction phase
function bail_out($sig) {
  print "Exiting via signal $sig ...n";
  global $mb1;
  global $mb2;
  print "Closing IMAP connections...n" ;

ini_set("memory_limit","128M"); // yes, there are people with emails this big
$srcuser = $argv[1];
$srcpass = $argv[2];

$dstuser = $argv[1];
$dstpass = $argv[2];

if(@$argv[3] != '') $dstuser = $argv[3];
if(@$argv[4] != '') $dstpass = $argv[4];

define ('VER', "MVN IMAP Migration Tool v.4.20110218") ;

function new_rcvd_line () {
  $s = "X-Migration: Migrated via " . VER . " at " . date(DATE_RFC822) ;
  return $s;

// This is a good place for whatever custom stuff you need to accommodate different source and dest server types.
// For instance, these were needed for an Imail -> Plesk migration.
function fix_mbx_name($i) {
  switch($i) {
    case "INBOX" : return "INBOX"; break;
    case "Inbox" : return "INBOX"; break;
    case "Sent"  : return "INBOX.sent-mail"; break;
    default: return "INBOX." . $i; break;

// make sure we can talk to both servers or bail out now
$mb1 = imap_open(SRC, $srcuser, $srcpass, OP_HALFOPEN) or die ("Unable to open src svr:" . imap_last_error() . "n");
$mb2 = imap_open(DST, $dstuser, $dstpass, OP_HALFOPEN) or die ("Unable to open dst svr:" . imap_last_error() . "n");

// now that we're talking to IMAP servers we have to be careful how we exit
pcntl_signal(SIGTERM, "bail_out");
pcntl_signal(SIGINT, "bail_out");

print "Beginning migration for $srcuser.n";
$mbx_list = imap_getmailboxes($mb1, SRC, "*");
foreach ($mbx_list as $mbx) {
  print "Found src mbx " . $mbx->name . "n" ;

  $new_mbx = imap_utf7_encode(DST . fix_mbx_name(substr($mbx->name, strlen(SRC))));
  imap_reopen($mb1, $mbx->name);
  if (imap_list($mb2, DST, $new_mbx) == FALSE) {
    print "Creating $new_mbxn" ;
    $test1 = imap_createmailbox($mb2, imap_utf7_encode($new_mbx));
    if (!$test1) die("Unable to create new_mbxn");
  imap_reopen($mb2, $new_mbx);
  $src_status = imap_status($mb1, $mbx->name, SA_ALL);
  $num_msg = $src_status->messages;
  print "Mailbox contains $num_msg messages.n" ;
  for ($i = 1; $i <= $num_msg; $i++) {
    // this is the "copy one msg" juicy part
    $hdr = imap_fetchheader($mb1, $i);
    // $hdr = new_rcvd_line() . "rn" . $hdr ;
    $hdr_info = imap_headerinfo($mb1, $i);
    $bdy = imap_body($mb1, $i, FT_PEEK);
    $export = $hdr . "rn" . $bdy ;

    unset($flags); $flags="";
    if ($hdr_info->Recent == 'R') $flags .= "Seen " ;
    if (($hdr_info->Recent == ' ') && ($hdr_info->Unseen == ' ')) $flags .= "Seen " ;
    if ($hdr_info->Draft == 'X') $flags .= "Draft ";
    if ($hdr_info->Answered == 'A') $flags .= "Answered ";
    if ($hdr_info->Flagged == 'F') $flags .= "Flagged ";
    if ($hdr_info->Deleted == 'D') $flags .= "Deleted "; // why :(

    imap_append($mb2, $new_mbx, $export, $flags);
    print "Migrated message $i.n" ;
    // uncomment this if you want to flag messages as deleted on the source server
    // imap_delete($mb1, $i);
  // uncomment this if you want to commit your changes (i.e. delete messages on the source server)
  // imap_expunge ($mb1);
print "Migration completed, disconnecting...n" ;


  1. Wouldn’t blame you for not doing this, setting up your mail server and keeping it clean and spam-free is a right royal pain. 
  2. I’m sure it could be a Perl one-liner, because everything is a Perl one-liner. 
Using PHP to migrate email between servers