Thursday, September 24, 2009

PHP Gmail E-mail Delete Script


/* Gmail Spring Cleaning Script
* delete all messages before a given year (default: current year)
*
* Usage:
* (1) create INI file with values for 'user', 'pass' and 'year'
* named after this script (except using ini instead of php)
* (2) run script as: php-cli script.php user pass [year]
* (3) run script as: php-cli script.php
* then enter values when prompted by script
*
* Notes:
* - Username must end with @gmail.com
* - Year must be between 2000 and 2999
* (Gmail didn't exist before 2004 anyway)
* - PHP 5.2+ is recommended (untested on earlier versions)
* - Extensions imap required; pcntl advised
*/


/* globals */
$flaggedForDelete = 0;
$imap = null;
$user = '';
$pass = '';

/* entry point */
function main($argc, $argv) {
global $user, $pass;

/* display errors only */
error_reporting(E_ALL ^ (E_NOTICE | E_WARNING));

/* read configuration file (don't want to store login info in script)*/
$inifile = preg_replace('/php$/', 'ini', basename(__FILE__));
if(($ini = script_init($inifile)) != false) {
$user = $ini[0];
$pass = $ini[1];
$year = $ini[2];
} else {
if($argc > 2) {
$user = $argv[1];
$pass = $argv[2];
$year = ($argv[3]) ? $argv[3] : date('Y');
if(!$user || !$pass) {
echo "Usage: ".basename(__FILE__)." []\n";
exit(1);
}
} else {
echo "$inifile and command line arguments not found. please enter data manually.\n\n";
fwrite(STDOUT, "gmail user: "); $user = trim(fgets(STDIN));
fwrite(STDOUT, "password: "); $pass = trim(fgets(STDIN));
fwrite(STDOUT, "min year: "); $year = trim(fgets(STDIN));
}
}

/* command line arguments override the INI file */

/* set signal handlers and time execution of the core routines */
$timeBegin = script_microtime();
signal_handler_set('signal_handler');
script_core($year);
script_cleanup();
$timeEnd = script_microtime();
echo 'Executed in: '.round($timeEnd - $timeStart,3)." seconds\n";

exit(0);
} main($argc, $argv);

/* set signal handler if we can */
function signal_handler_set($handler = SIG_DFL) {
if(function_exists('pcntl_signal')) {
if(function_exists('pcntl_signal_dispatch'))
pcntl_signal_dispatch();
else
declare(ticks = 1);
pcntl_signal(SIGTERM, $handler);
pcntl_signal(SIGINT, $handler);
pcntl_signal(SIGKILL, $handler);
}
}

/* handle signals that can terminate the script; we want to die gracefully */
function signal_handler($signal) {
if($signal == SIGTERM || $signal == SIGINT || $signal = SIGKILL) {
echo "\nScript interrupted! Cleaning up...\n";
script_cleanup();
exit(0);
}
}

/* return a float for microtime() in order to time the script */
function script_microtime() {
list($msec,$sec) = explode(' ', microtime());
return ((float)$msec + (float)$sec);
}

/* read our INI file which defines 'user', 'pass' and 'year' keys
* ini file name is always the same as the script except w/ 'ini' extension instead of 'php'
*/
function script_init($inifile) {
if(file_exists($inifile)) {
$ini = parse_ini_file($inifile);
if(strlen(trim($ini['user'])) == 0) {
echo "blank user in ini? i don't think so.\n";
exit(1);
}
if(strlen(trim($ini['pass'])) == 0) {
echo "blank pass in ini? i don't think so.\n";
exit(1);
}
if(strlen(trim($ini['year'])) == 0) {
echo "blank year? well let's use this year...\n";
$ini['year'] = date('Y');
}
} else {
return false;
}
return array($ini['user'], $ini['pass'], $ini['year']);
}

/* connect to gmail via IMAP and mark old messages to be moved to the trash.
* gmail won't let you delete messages in the '[Gmail]/All Mail' folder.
*/
function script_core($minYear) {
global $imap, $flaggedForDelete, $user, $pass;

echo "Connecting as {$user}...\n";
$imap = imap_open("{imap.gmail.com:993/imap/ssl/novalidate-cert}[Gmail]/All Mail", $user, $pass) or die("Cannot connect: " . imap_last_error() . "\n");

echo "Checking current mailbox...\n";
$mbox = imap_check($imap);

$bar = new Console_ProgressBar('- Processing %fraction% [%bar%] %percent% ETA: %estimate%', '=>', '-', 74, $mbox->Nmsgs);
for($n = 1; $n < $mbox->Nmsgs; $n++) {
//echo "Processing {$n} of {$mbox->Nmsgs} ({$flaggedForDelete} flagged so far)...\r";
$bar->update($n);
$hdr = imap_fetchheader($imap, $n);
preg_match('/Date: .*? (2\d{3}).*?$/m', $hdr, $matches);
$year = $matches[1];
if($year < $minYear) {
if(!imap_mail_move($imap, $n, '[Gmail]/Trash')) {
preg_match('/Message-ID: \<(.*?)\>/', $hdr, $matches);
$msgid = $matches[1];
echo("\nimap_mail_move ({$msgid}/{$n}): " . imap_last_error() . "\n");
}
$flaggedForDelete++;
}
}
}

/* access the '[Gmail]/Trash' folder and delete all messages */
function script_empty_trash() {
global $user, $pass;

signal_handler_set(SIG_DFL);

$imap = imap_open("{imap.gmail.com:993/imap/ssl/novalidate-cert}[Gmail]/Trash", $user, $pass) or die("can't connect: " . imap_last_error() . "\n");
$mbox = imap_check($imap);
$bar = new Console_ProgressBar('- Deleting %fraction% [%bar%] %percent% ETA: %estimate%', '=>', '-', 74, $mbox->Nmsgs);
for($n = 1; $n < $mbox->Nmsgs; $n++) {
//echo "Deleting message {$n} of {$mbox->Nmsgs} in trash...\r";
$bar->update($n);
imap_delete($imap, $n);
}
imap_close($imap, CL_EXPUNGE);
}

/* script_core() can be time consuming; user may interrupt
* if so, expunge and close the connection (moving flagged messages to trash)
* next, try to empty the trash -- may be time consuming, however,
* script_empty_trash() will die w/o any hold-ups if the user tries killing again
*/
function script_cleanup() {
global $imap, $flaggedForDelete;

echo "Expunging mailbox ({$flaggedForDelete} messages flagged)...\n";
imap_close($imap, CL_EXPUNGE);

echo "Emptying trash...\n";
script_empty_trash();

echo "\n";
}

/* list all mail boxes on the gmail imap server
* used for development, but not needed in the final version
*/
function script_list_boxes() {
global $user, $pass;

$imap = imap_open("{imap.gmail.com:993/imap/ssl/novalidate-cert}", $user, $pass) or die("Cannot connect: " . imap_last_error() . "\n");
$boxes = imap_list($imap, '{imap.gmail.com}', '*');
print_r($boxes);
imap_close($imap);
}


/* Console/ProgressBar 0.5.2 Copyright (c) 2007 Stefan Walk ; released under MIT license; downloded from PEAR */
class Console_ProgressBar {var $_skeleton;var $_bar;var $_blen;var $_tlen;var $_target_num;var $_options = array();var $_rlen = 0;var $_start_time = null;var $_rate_datapoints = array();var $_last_update_time = 0.0;function Console_ProgressBar($formatstring, $bar, $prefill, $width,$target_num, $options = array()){$this->reset($formatstring, $bar, $prefill, $width, $target_num,$options);}function reset($formatstring, $bar, $prefill, $width, $target_num,$options = array()){if ($target_num == 0) {trigger_error("PEAR::Console_ProgressBar: Using a target number equal to 0 is invalid, setting to 1 instead");$this->_target_num = 1;} else {$this->_target_num = $target_num;}$default_options = array('percent_precision' => 2,'fraction_precision' => 0,'percent_pad' => ' ','fraction_pad' => ' ','width_absolute' => true,'ansi_terminal' => false,'ansi_clear' => false,'num_datapoints' => 5,'min_draw_interval' => 0.0,);$intopts = array();foreach ($default_options as $key => $value) {if (!isset($options[$key])) {$intopts[$key] = $value;} else {settype($options[$key], gettype($value));$intopts[$key] = $options[$key];}}$this->_options = $options = $intopts;$cur = '%2$\''.$options['fraction_pad']{0}.strlen((int)$target_num).'.'.$options['fraction_precision'].'f';$max = $cur; $max{1} = 3;if (version_compare(PHP_VERSION, '4.3.7', 'ge')) {$padding = 4 + $options['percent_precision'];} else {$padding = 3;}$perc = '%4$\''.$options['percent_pad']{0}.$padding.'.'.$options['percent_precision'].'f';$transitions = array('%%' => '%%','%fraction%' => $cur.'/'.$max,'%current%' => $cur,'%max%' => $max,'%percent%' => $perc.'%%','%bar%' => '%1$s','%elapsed%' => '%5$s','%estimate%' => '%6$s',);$this->_skeleton = strtr($formatstring, $transitions);$slen = strlen(sprintf($this->_skeleton, '', 0, 0, 0, '00:00:00','00:00:00'));if ($options['width_absolute']) {$blen = $width - $slen;$tlen = $width;} else {$tlen = $width + $slen;$blen = $width;}$lbar = str_pad($bar, $blen, $bar{0}, STR_PAD_LEFT);$rbar = str_pad($prefill, $blen, substr($prefill, -1, 1));$this->_bar = substr($lbar,-$blen).substr($rbar,0,$blen);$this->_blen = $blen;$this->_tlen = $tlen;$this->_first = true;return true;}function update($current){$time = $this->_fetchTime();$this->_addDatapoint($current, $time);if ($this->_first) {if ($this->_options['ansi_terminal']) {echo "\x1b[s"; }$this->_first = false;$this->_start_time = $this->_fetchTime();$this->display($current);return;}if ($time - $this->_last_update_time <$this->_options['min_draw_interval'] and $current != $this->_target_num) {return;}$this->erase();$this->display($current);$this->_last_update_time = $time;}function display($current){$percent = $current / $this->_target_num;$filled = round($percent * $this->_blen);$visbar = substr($this->_bar, $this->_blen - $filled, $this->_blen);$elapsed = $this->_formatSeconds($this->_fetchTime() - $this->_start_time);$estimate = $this->_formatSeconds($this->_generateEstimate());$this->_rlen = printf($this->_skeleton,$visbar, $current, $this->_target_num, $percent * 100, $elapsed,$estimate);if (is_null($this->_rlen)) {$this->_rlen = $this->_tlen;} elseif ($this->_rlen < $this->_tlen) {echo str_repeat(' ', $this->_tlen - $this->_rlen);$this->_rlen = $this->_tlen;}return true;}function erase($clear = false){if ($this->_options['ansi_terminal'] and !$clear) {if ($this->_options['ansi_clear']) {echo "\x1b[2K\x1b[u"; } else {echo "\x1b[u"; }} elseif (!$clear) {echo str_repeat(chr(0x08), $this->_rlen);} else {echo str_repeat(chr(0x08), $this->_rlen),str_repeat(chr(0x20), $this->_rlen),str_repeat(chr(0x08), $this->_rlen);}}function _formatSeconds($seconds){$hou = floor($seconds/3600);$min = floor(($seconds - $hou * 3600) / 60);$sec = $seconds - $hou * 3600 - $min * 60;if ($hou == 0) {if (version_compare(PHP_VERSION, '4.3.7', 'ge')) {$format = '%2$02d:%3$05.2f';} else {$format = '%2$02d:%3$02.2f';}} elseif ($hou < 100) {$format = '%02d:%02d:%02d';} else {$format = '%05d:%02d';}return sprintf($format, $hou, $min, $sec);}function _fetchTime() {if (!function_exists('microtime')) {return time();}if (version_compare(PHP_VERSION, '5.0.0', 'ge')) {return microtime(true);}return array_sum(explode(' ', microtime()));}function _addDatapoint($val, $time) {if (count($this->_rate_datapoints)== $this->_options['num_datapoints']) {array_shift($this->_rate_datapoints);}$this->_rate_datapoints[] = array('time' => $time,'value' => $val,);}function _generateEstimate() {if (count($this->_rate_datapoints) < 2) {return 0.0;}$first = $this->_rate_datapoints[0];$last = end($this->_rate_datapoints);return ($this->_target_num - $last['value'])/($last['value'] - $first['value']) * ($last['time'] - $first['time']);}}