#!/usr/bin/php
<?php
/**
 * @author Koen De Vreeze
 * 
 * Special thanks to the PHP community to explain a few things:
 * - https://sites.google.com/a/van-steenbeek.net/archive/php_pcntl_fork
 * - http://be.php.net/manual/en/function.pcntl-wait.php
 *
 * More information on the Google Mail Migration API:
 * - http://code.google.com/intl/en-in/googleapps/domain/email_migration/developers_guide_protocol.html
 * - http://code.google.com/intl/nl-BE/googleapps/domain/email_migration/reference.html#MailItemFeed
 */
// Initialize
error_reporting(E_ALL);
ini_set("display_errors""1");
ini_set("max_execution_time""0");
ini_set("max_input_time""0");
ini_set('memory_limit''64M');
set_time_limit(0);
date_default_timezone_set('Europe/Brussels');
declare(
ticks 1);

// Config
$homedirfolder '/home';
$domain 'domain.tld';
$domainadmin 'admin';
$domainadminpwd 'Passw0rd';

$userlist = array('john.doe''jack.frost'); // set null to process all home folders.
// end Config

GoogleApps::retrieveAuthToken("$domainadmin@$domain"$domainadminpwd);
User::$domain $domain;

$jd = new JobDaemon($homedirfolder$userlist);
$jd->run();
echo 
"\nMail migration done.\n";
exit(
0);

/**
 * A very basic job daemon that you can extend to your needs.  
 * 
 * @author duerra at yahoo dot com
 * Code from http://be.php.net/manual/en/function.pcntl-wait.php
 */
class JobDaemon
{
    public 
$maxProcesses 10;
    protected 
$jobsStarted 0
    protected 
$currentJobs = array();
    protected 
$signalQueue = array();    
    protected 
$parentPID;
    protected 
$homedirfolder;
    protected 
$userlist = array();
    
    
    
/** 
     * Run the Daemon
     *
     * @param $homdirfolder (optional) default='/home'. Folder containing the home directories.
     * @param $userlist (optional) Array with usernames (foldernames) default='' that should be migrated.
     * Profile folders that are not in this list are skipped.
     */
    
public function __construct($homedirfolder '/home'$userlist '')
    {
        
$this->homedirfolder $homedirfolder;
        
$this->parentPID getmypid();
        
pcntl_signal(SIGCHLD, array($this"childSignalHandler"));
        if (
is_array($userlist))
            
$this->userlist $userlist;
    }

    
/** 
     * Run the Daemon
     */
    
public function run()
    {
        echo 
"Running... maxProcesses: {$this->maxProcesses}\n";
        
$startdir $this->homedirfolder;
        
        if (
$handle opendir($startdir)) {
            while (
false !== ($file readdir($handle))) {
                if (
$file != "." && $file != "..") {
                    
$fullpath "$startdir/$file";
                    
// skip users not in list
                    
if (!in_array($file$this->userlist))
                        continue;
                    if (
is_dir($fullpath)) {
                        
$jobID rand(0,10000000000000);
                        
$logfile './mailmigrate-' $file '-' date('Ymd-His') . '.log';
                        
$user = new User($file);
                        
$maildir $fullpath "/Maildir";
                        
$job = new Job($user$maildir$logfile);
                        
$this->launchJob($jobID$job);
                    }
                }
            }
        } else {
            
error_log("Could not open $startdir");
        }
        
        
//Wait for child processes to finish before exiting here
        
while(count($this->currentJobs)){
            
//echo "Waiting for current jobs to finish... \n";
            
sleep(1);
        }
    }

    
/** 
     * Launch a job from the job queue
     */
    
protected function launchJob($jobID$job)
    {
        
$pid pcntl_fork();
        if(
$pid == -1) {
            
//Problem launching the job
            
error_log('Could not launch new job, exiting');
            return 
false;
        } else if (
$pid) {
            
// Parent process
            // Sometimes you can receive a signal to the childSignalHandler function before this code executes if 
            // the child script executes quickly enough!
            //
            
$this->currentJobs[$pid] = $jobID;

            
// In the event that a signal for this pid was caught before we get here, it will be in our signalQueue array
            // So let's go ahead and process it now as if we'd just received the signal
            
if (isset($this->signalQueue[$pid])) {
                echo 
"found $pid in the signal queue, processing it now \n";
                
$this->childSignalHandler(SIGCHLD$pid$this->signalQueue[$pid]);
                unset(
$this->signalQueue[$pid]);
            }
        } else {
            
//Forked child, do your deeds.... 
            
$exitStatus 0//Error code if you need to or whatever
            
echo "Migration thread started for {$job->user->name} (pid:" getmypid() . ")\n";
            
$job->processMessages();
            echo 
"\nMigration completed for {$job->user->name}{$job->msgcounter} msgs processed, {$job->msgfailedcounter} failed.\n";
            exit(
$exitStatus);
        }
        return 
true;
    }

    public function 
childSignalHandler($signo$pid null$status null)
    {
        
//If no pid is provided, that means we're getting the signal from the system.  Let's figure out
        //which child process ended
        
if (!$pid) {
            
$pid pcntl_waitpid(-1$statusWNOHANG);
        }

        
//Make sure we get all of the exited children
        
while ($pid 0) {
            if (
$pid && isset($this->currentJobs[$pid])) {
                
$exitCode pcntl_wexitstatus($status);
                if (
$exitCode != 0) {
                    echo 
"$pid exited with status ".$exitCode."\n";
                }
                unset(
$this->currentJobs[$pid]);
            } else if (
$pid) {
                
//Oh no, our job has finished before this parent process could even note that it had been launched!
                //Let's make note of it and handle it when the parent process is ready for it
                
echo "..... Adding $pid to the signal queue ..... \n";
                
$this->signalQueue[$pid] = $status;
            }
            
$pid pcntl_waitpid(-1$statusWNOHANG);
        }
        return 
true;
    }
}

class 
Job
{
    public 
$user;
    public 
$maildir;
    public 
$logfile;
    public 
$msgcounter 0;
    public 
$msgfailedcounter 0;
    
    function 
__construct(User $user$maildir$logfile)
    {
        
$this->user $user;
        
$this->maildir $maildir;
        
$this->logfile $logfile;
    }
    
/**
     * Start processing messages. This only works for mails stored in Maildir format (not Maildir+ or others).
     *
     */
    
public function processMessages()
    {
        
$containingFolders = array('new''cur''tmp');
        if (
exec("find {$this->maildir} -type f"$files)) {
            
$maildirstrlen strlen($this->maildir)+1;
            foreach(
$files as $file) {
                
// remove folders before Maildir (Maildir inclusive)
                
$trimmedPath substr($file$maildirstrlen);
                
$path explode('/'$trimmedPath);
                
$countPath count($path);
                if (
$countPath == || $countPath == 3) {
                    
// is a mail file?
                    
if (in_array($path[$countPath 2], $containingFolders)) {
                        
$msg = new Message($file$this->user);
                        
$msg->prepareMaildirMessage($path);
                        
$try 1;
                        
$attempts 4// setting
                        
while ($try <= $attempts) {
                            if (
GoogleApps::migrateMessage($msg)) {
                                
error_log($msg->file ";success;ok\n"3$this->logfile);
                                
$try $attempts 1;
                            } else {
                                
$errorMsgTmp GoogleApps::getErrorMsg();
                                if (
$errorMsgTmp 'The server is currently busy and could not complete your request. Please try again in 30 seconds.') {
                                    
sleep(30);
                                    if (
$try == $attempts) {                                    
                                        
error_log($msg->file ";failed;" GoogleApps::getErrorMsg() . "\n"3$this->logfile);
                                        
$this->msgfailedcounter++;
                                    }
                                    
$try++;
                                }
                            }
                        }
                        
$this->msgcounter++;
                        echo 
"\r" $this->msgcounter " messages processed for {$this->user->name}{$this->msgfailedcounter} failed. ";
                    }
                }
            }
        }
    } 
// end processMessages()
}

class 
User 
{
    public 
$name;
    
//public $pass;
    
static $domain;
    
    function 
__construct($name)
    {
        
$this->name $name;
    }
    public function 
getLoginName()
    {
        return 
$this->name '@' self::$domain;
    }
}

class 
Message
{
    public 
$file;
    
/*
    IS_DRAFT
        The message should be marked as a draft when inserted.
    IS_INBOX
        The message should appear in the Inbox, regardless of its labels. (By default, a migrated mail message will appear in the Inbox only if it has no labels.)
    IS_SENT
        The message should be marked as "Sent Mail" when inserted.
    IS_STARRED
        The message should be starred when inserted.
    IS_TRASH
        The message should be marked as "Trash" when inserted.
    IS_UNREAD
        The message should be marked as unread when inserted. Without this property, a migrated mail message is marked as read.
    */
    
public $mailItemProperties//='IS_INBOX';
    
public $labels;
    public 
$fileContents;
    public 
$user//User
    
    
function __construct($fileUser $user)
    {
        
$this->file $file;
        
$this->user $user;
    }
    public function 
addMailItemProperty($property)
    {
        if (!
is_array($this->mailItemProperties))
            
$this->mailItemProperties = array($property);
        else
            if (!
in_array($property$this->mailItemProperties))
                
$this->mailItemProperties[] = $property;
    }
    public function 
addLabel($label)
    {
        if (!
is_array($this->labels))
            
$this->labels = array($label);
        else
            if (!
in_array($label$this->labels))
                
$this->labels[] = $label;
    }
    public function 
getFileContents()
    {
        
// get contents of a file into a string
        
$handle fopen($this->file"r");
        
$emailMsg fread($handlefilesize($this->file));
        
fclose($handle);
        return 
$emailMsg;
    }    
    
/**
     * Prepare a message for sending. Put in right folder (inbox/outbox) add labels.
     * 
     * @param $path Array of subfolders seen from Maildir.
     */
    
public function prepareMaildirMessage($path)
    {
        
$countPath count($path);
        if (
$countPath == 3) {
            
// In folder
            
$foldername substr($path[0], 1);

            switch (
$foldername) {
                case 
'sent-mail':
                case 
'Versturen':
                case 
'Verzonden':
                    
$this->addMailItemProperty('IS_SENT');
                    break;
                    
                case 
'Klad':
                    
$this->addMailItemProperty('IS_DRAFT');
                    break;
                    
                default:
                    
$this->addMailItemProperty('IS_INBOX');
                    
$this->addLabel($foldername);
                    break;
            }
        }
        
        
/*
            "P" - Passed (NL: Reeds verwerkt)
            "D" - Draft (NL: Concept)
            "R" - Reply (NL: Beantwoord)
            "S" - Seen (NL: Gelezen)
            "T" - Trashed (NL: Prullenbak)
            "F" - Flagged (NL: Gemarkeerd om een of andere reden)
        */
        
if (preg_match('/.*:2,[A-Z]*D[A-Z]*+/'$path[$countPath 1]) == 1) {
            
$this->addMailItemProperty('IS_DRAFT');
        }
        if (
preg_match('/.*:2,[A-Z]*F[A-Z]*+/'$path[$countPath 1]) == 1) {
            
//$this->addMailItemProperty('IS_');
        
}
        if (
preg_match('/.*:2,[A-Z]*P[A-Z]*+/'$path[$countPath 1]) == 1) {
            
//$this->addMailItemProperty('IS_');
        
}
        if (
preg_match('/.*:2,[A-Z]*R[A-Z]*+/'$path[$countPath 1]) == 1) {
            
//$this->addMailItemProperty('IS_');
        
}
        if (
preg_match('/.*:2,[A-Z]*S[A-Z]*+/'$path[$countPath 1]) == 0) {
            
$this->addMailItemProperty('IS_UNREAD');
        }
        if (
preg_match('/.*:2,[A-Z]*T[A-Z]*+/'$path[$countPath 1])) {
            
$this->addMailItemProperty('IS_TRASH');
        }
        
//print_r($this);
        
    
// end prepareMessage()
}

class 
GoogleApps
{
    private static 
$last_error '';
    public static 
$authToken;
    
/**
     * Get authToken for user.
     * This should be a domain admin.
     */
    
static function retrieveAuthToken($email$passwd)
    {
        
$email urlencode($email);
        
$passwd urlencode($passwd);
        
        
$post_data "Email=$email&Passwd=$passwd&accountType=HOSTED&service=apps";

        
// create curl resource 
        
$ch curl_init(); 

        
// set url 
        
curl_setopt($chCURLOPT_URL"https://www.google.com/accounts/ClientLogin"); 
        
curl_setopt($chCURLOPT_PORT443);
        
curl_setopt($chCURLOPT_POST1);
        
curl_setopt($chCURLOPT_POSTFIELDS$post_data);
        
curl_setopt($chCURLINFO_CONTENT_LENGTH_UPLOADstrlen($post_data));
        
curl_setopt($chCURLINFO_CONTENT_TYPE"application/x-www-form-urlencoded");
        
//return the transfer as a string 
        
curl_setopt($chCURLOPT_RETURNTRANSFER1); 

        
$token curl_exec($ch); 

        
// close curl resource to free up system resources 
        
curl_close($ch);
        
        
// Extract authToken
        
$pattern '/Auth=(.*)/';
        
preg_match($pattern$token$matches);
        
$auth substr($matches[0], 5);
        
self::$authToken $auth;
        return 
$auth;
    } 
// end retrieveAuthToken()
    /**
     * Send a Message to the Google mail servers.
     */
    
static function migrateMessage(Message $msg)
    {
        
$success false;
        
$authToken self::$authToken;
        
$maxsize 25 1024 1024;
        if (
filesize($msg->file) > $maxsize) {
            
self::$last_error "message exceeds $maxsize bytes";
            return 
false;
        }
        
$httpbody self::getFeed($msg);
        
$fp fsockopen("ssl://apps-apis.google.com"443$errno$errstr30);
        
        if (!
$fp) {
            echo 
"$errstr ($errno)\n";
        } else {
            
$out "POST /a/feeds/migration/2.0/" User::$domain "/{$msg->user->name}/mail HTTP/1.1\r\n";
            
//$out = "POST /a/feeds/migration/2.0/default/mail HTTP/1.1\r\n";
            
$out .= "Host: apps-apis.google.com\r\n";
            
$out .= 'Content-type: multipart/related;boundary="----=_Part_0_25934938.1266495790627"'."\r\n";
            
$out .= "Authorization: GoogleLogin auth=$authToken\r\n";
            
$out .= "Content-length: " .strlen($httpbody) . "\r\n";
            
$out .= "Connection: Close\r\n\r\n";
            
$out .= $httpbody;
            
            
fwrite($fp$out);
            
/*
            while (!feof($fp)) {
                echo fgets($fp, 128);
            }
            */
            
if (fgets($fp128) == "HTTP/1.1 201 Created\r\n") {
                
//echo "No error migrating mail.\r\n";
                
$success true;
            } else {
                
$err '';
                while (!
feof($fp)) {
                    
$err .= fgets($fp128);
                }
                
self::$last_error $err;
            }
            
fclose($fp);
            return 
$success;
        }
    } 
// end migrateMessage()
    
    
static function getFeed(Message $msg)
    {
        
$feed "------=_Part_0_25934938.1266495790627
Content-Type: application/atom+xml

<?xml version='1.0' encoding='UTF-8'?><entry xmlns='http://www.w3.org/2005/Atom'
 xmlns:apps='http://schemas.google.com/apps/2006'>
<category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/apps/2006#mailItem'/>
<atom:content xmlns:atom='http://www.w3.org/2005/Atom' type='message/rfc822'/>\r\n"
;
        if (
is_array($msg->mailItemProperties)) {
            foreach(
$msg->mailItemProperties as $prop) {
                
$feed .= "<apps:mailItemProperty value='$prop'/>\r\n";
            }
        }
        if (
is_array($msg->labels)) {
            foreach(
$msg->labels as $lbl) {
                
$feed .= "<apps:label labelName='$lbl'/>\r\n";
            }
        }
        
$feed .= "</entry>

------=_Part_0_25934938.1266495790627
Content-Type: message/rfc822\r\n\r\n"
;
        
$feed .= $msg->getFileContents();
        
$feed .= "\r\n------=_Part_0_25934938.1266495790627--";

        return 
$feed;
    } 
// end getFeed()
    
    
public static function getErrorMsg()
    {
        
$matches null;
        if (
preg_match('/<H1>(.*)<\/H1>/'self::$last_error$matches) == 0) {
            
$match preg_split('/\n/'self::$last_error);
            return 
$match[count($match)-1];
            
//return self::$last_error;
        
} else
            return 
$matches[0];
    }
}