~/.eth0/ 

This is my code. There is a lot like it, but this is mine.

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:

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…

View license file

#!/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);