IMAPCopy
/imapcopy/imapcopy.php
This is an old script that I used in the past to copy messages between IMAP accounts. It has migrated thousands of messages from old email accounts to new ones in many occasions and it’s pretty reliable: it can be run multiple times if some messages aren’t copied for some reason, and it takes care not to copy already copied messages.
Even though it’s a PHP script, it’s a command-line utility. I have a web version of it, but the CLI version is way more useful.
The script has four mandatory arguments and a few optional ones:
--src-host
: The source host.--src-user
: The source account user name.--src-password
: The source account password.--src-use-tls
: (Optional) If specified, a SSL connection is made to the source host.--dst-host
: The destination host.--dst-user
: (Optional) The destination account user name.--dst-password
: (Optional) The destination account password.--dst-use-tls
: (Optional) If specified, a SSL connection is made to the destination host.--help
: Displays the usage information.
If any of the --dst-user
or --dst-password
options is not specified, the
source username and/or password are used by default.
Download
this script
Secondary click/Save as…
#!/usr/bin/env php
<?php
/*
* Copyright 2019 eth0 <ethernet.zero@gmail.com>
*
* This work is free. You can redistribute it and/or modify it under the terms of
* the ISC License. See the COPYING file for more details.
*/
class IMAPTools
{
private $_stream = null;
private $_index = 0;
private $_namespace = null;
private $_separator = null;
private $_capability = null;
private $_dialog = array();
public function __construct($server, $user, $pass, $ssl=false)
{
$errno = null;
$errstr = null;
$contextOptions = array(
'ssl' => array(
'verify_peer' => false,
),
);
$context = stream_context_create($contextOptions);
if ($ssl)
{
$this->_stream = stream_socket_client("ssl://{$server}:993", $errno, $errstr, 10, STREAM_CLIENT_CONNECT, $context);
}
else
{
$this->_stream = stream_socket_client("tcp://{$server}:143", $errno, $errstr, 10);
}
if ($this->_stream)
{
if ($this->_exec("LOGIN {$user} {$pass}"))
{
$response = $this->_exec('CAPABILITY');
if ($response !== false)
{
$this->_capability = $this->_parseCapability($response);
}
if ($this->hasCapability('NAMESPACE'))
{
$response = $this->_exec('NAMESPACE');
if ($response !== false)
{
$this->_namespace = $this->_parseNamespace($response);
}
}
}
fclose($this->_stream);
}
}
private function _exec($command)
{
if (!$this->_stream)
{
return false;
}
$this->_index++;
$command = "{$this->_index} {$command}";
$this->_dialog[] = $command;
$command = "{$command}\r\n";
fwrite($this->_stream, $command);
$response = array();
while ($line = stream_get_line($this->_stream, 1024, "\r\n"))
{
$this->_dialog[] = $line;
if ($line[0] == '*')
{
$response[] = $line;
}
elseif (preg_match("#^{$this->_index} OK #", $line))
{
return (count($response) == 1) ? $response[0] : $response;
}
elseif (preg_match("#^{$this->_index} (?:BAD|NO) #", $line))
{
return false;
}
}
}
private function _parseNamespace($response)
{
$namespace = array();
if (!is_array($response))
{
$response = array($response);
}
foreach ($response as $line)
{
$matches = array();
preg_match('#^\* NAMESPACE \(\("(?P<namespace>[^"]*)" "(?P<separator>[^"]+)"#', $line, $matches);
if (isset($matches['namespace']))
{
$namespace['namespace'] = $matches['namespace'];
}
if (isset($matches['separator']))
{
$namespace['separator'] = $matches['separator'];
}
}
return $namespace;
}
private function _parseCapability($response)
{
if (!is_array($response))
{
$response = array($response);
}
foreach ($response as $line)
{
$matches = array();
preg_match('#^\* CAPABILITY (?P<capability>.+)#', $line, $matches);
if (isset($matches['capability']))
{
return explode(' ', $matches['capability']);
}
}
return false;
}
public function getNamespace()
{
return is_array($this->_namespace) ? $this->_namespace['namespace'] : null;
}
public function getSeparator()
{
return is_array($this->_namespace) ? $this->_namespace['separator'] : null;
}
public function hasCapability($capability)
{
return ($this->_capability && in_array($capability, $this->_capability));
}
}
function printmsg($message)
{
echo "** {$message}\n";
}
function printnote($message)
{
echo " {$message}\n";
}
function printerr($message)
{
fprintf(STDERR, $message);
}
function usage()
{
$me = $GLOBALS['argv'][0];
echo <<<EOF
Usage: $me \
--src-host SOURCE_HOST \
--src-user SRC_USER \
--src-password SRC_PASSWORD \
[--src-use-tls] \
--dst-host DST_HOST \
[--dst-user DST_USER] \
[--dst-password DST_PASSWORD] \
[--dst-use-tls] \
[--help]
EOF;
}
set_time_limit(0);
ini_set('display_errors', 0);
mb_internal_encoding('UTF-8');
$errors = array();
$not_copied_list = array();
$copied_messages = 0;
$copied_mailboxes = 0;
$args = getopt('', [
'src-host:',
'src-user:',
'src-password:',
'src-use-tls',
'dst-host:',
'dst-user:',
'dst-password:',
'dst-use-tls',
'help',
]);
if (isset($args['help']))
{
usage();
exit(0);
}
foreach (['src-host', 'src-user', 'src-password', 'dst-host'] as $k)
{
if (!isset($args[$k]))
{
printerr("ERROR: you must specify the --$k option\n\n");
die(usage());
}
}
if (!isset($args['dst-user']))
{
$args['dst-user'] = $args['src-user'];
echo "NOTE: No destination username was specified, using source username also on the destination host\n";
}
if (!isset($args['dst-password']))
{
$args['dst-password'] = $args['src-password'];
echo "NOTE: No destination account password was specified, using source account password also on the destination host\n";
}
$src_host = $args['src-host'];
$src_user = $args['src-user'];
$src_pass = $args['src-password'];
$src_ssl = isset($args['src-use-tls']);
$dst_host = $args['dst-host'];
$dst_user = $args['dst-user'];
$dst_pass = $args['dst-password'];
$dst_ssl = isset($args['dst-use-tls']);
if ($src_host == $dst_host && $src_user == $dst_user)
{
die("ERROR: the source and destination accounts cannot be the exact same account");
}
$src_imap = new IMAPTools($src_host, $src_user, $src_pass, $src_ssl);
if ($src_ssl)
{
$src_server = "{{$src_host}:993/imap/ssl/novalidate-cert}";
}
else
{
$src_server = "{{$src_host}:143/imap/notls}";
}
$src_namespace = $src_imap->getNamespace();
$src_separator = $src_imap->getSeparator() or '.';
$dst_imap = new IMAPTools($dst_host, $dst_user, $dst_pass, $dst_ssl);
if ($dst_ssl)
{
$dst_server = "{{$dst_host}:993/imap/ssl/novalidate-cert}";
}
else
{
$dst_server = "{{$dst_host}:143/imap/notls}";
}
$dst_namespace = $dst_imap->getNamespace();
$dst_separator = $dst_imap->getSeparator() or '.';
printmsg("Copying messages from {$src_user}@{$src_host} to {$dst_user}@{$dst_host}");
$src = imap_open($src_server, $src_user, $src_pass);
$dst = imap_open($dst_server, $dst_user, $dst_pass);
if (!$src || !$dst)
{
$errors = array_merge($errors, imap_errors());
}
else
{
$src_mailboxes = imap_list($src, $src_server, '*');
$dst_mailboxes = imap_list($dst, $dst_server, '*') or array();
if ($src_mailboxes)
{
foreach ($src_mailboxes as $src_mailbox)
{
$not_copied = 0;
$mailbox = mb_convert_encoding($src_mailbox, 'UTF-8', 'UTF7-IMAP');
$mailbox = mb_substr($mailbox, mb_strlen($src_server));
printmsg('Opening mailbox ' . $mailbox);
if (mb_substr($mailbox, mb_strlen($src_namespace)) == '')
{
$mailbox = mb_substr($dst_namespace, 0, -mb_strlen($dst_separator));
}
else
{
$mailbox = mb_substr($mailbox, mb_strlen($src_namespace));
$mailbox = $dst_namespace . str_replace($src_separator, $dst_separator, $mailbox);
}
$dst_mailbox = mb_convert_encoding($dst_server . $mailbox, 'UTF7-IMAP', 'UTF-8');
if (!in_array($dst_mailbox, $dst_mailboxes))
{
printnote("Creating mailbox {$mailbox} in the destination account");
$result = imap_createmailbox($dst, $dst_mailbox);
if (!$result)
{
$errors = array_merge($errors, imap_errors());
$errors[] = "Error creating mailbox {$dst_mailbox}.";
}
else
{
imap_subscribe($dst, $dst_mailbox);
}
}
imap_reopen($dst, $dst_mailbox);
$mc = imap_check($dst);
$messages = imap_fetch_overview($dst, "1:{$mc->Nmsgs}", 0);
printnote("The destination mailbox contains {$mc->Nmsgs} messages");
printnote('Obtaining message IDs to avoid duplicates');
$dst_ids = array();
foreach ($messages as $message)
{
$dst_ids[$message->message_id] = true;
}
imap_reopen($src, $src_mailbox);
$mc = imap_check($src);
$messages = imap_fetch_overview($src, "1:{$mc->Nmsgs}", 0);
foreach ($messages as $message)
{
if (!isset($dst_ids[$message->message_id]))
{
printnote("Copying message {$message->message_id}");
// \Seen, \Answered, \Flagged, \Deleted, y \Draft
$flags = array();
if ($message->seen)
{
$flags[] = '\Seen';
}
if ($message->answered)
{
$flags[] = '\Answered';
}
if ($message->flagged)
{
$flags[] = '\Flagged';
}
if ($message->deleted)
{
$flags[] = '\Deleted';
}
if ($message->draft)
{
$flags[] = '\Draft';
}
if (!imap_append($dst, $dst_mailbox, imap_fetchheader($src, $message->msgno) . imap_body($src, $message->msgno, FT_PEEK), implode(' ', $flags)))
{
$not_copied++;
$not_copied_list[] = "[{$mailbox}] {$message->date} <{$message->from}> → <{$message->to}>";
}
else
{
$copied_messages++;
}
}
else
{
printnote("Message ID {$message->message_id} already exists, so it won't be copied");
}
}
if ($not_copied)
{
$errors[] = "{$not_copied} {$whmcs->get_lang('imapcopy_notcopied')} {$mailbox}";
}
$copied_mailboxes++;
}
}
imap_close($src);
imap_close($dst);
}
foreach ($errors as $error)
{
printerr("ERROR: {$error}");
}
printmsg("Copied {$copied_messages} messages to {$copied_mailboxes} mailboxes");
if (isset($not_copied) && $not_copied > 0)
{
printmsg("The following {$not_copied} messages couldn't be copied:");
foreach ($not_copied_list as $msg)
{
printnote($msg);
}
}
exit($copied_messages > 0 ? 0 : 1);