├── README.md ├── imap-move.sh └── imap-move.php /README.md: -------------------------------------------------------------------------------- 1 | # IMAP Move 2 | 3 | This will move messages from one IMAP system to another 4 | 5 | php ./imap-move.php \ 6 | --source imap-ssl://userA:secret-password@imap.example.com:993/ \ 7 | --target imap-ssl://userB:secret-passwrod@imap.example.com:993/sub-folder \ 8 | [ --wipe --fake --copy ] 9 | 10 | --fake to just list what would be copied 11 | --wipe to remove messages after they are copied (move) 12 | --copy to store copies of the messages in a path 13 | 14 | 15 | ## Shell Wrapper 16 | 17 | Included is a shell wrapper to make life a bit easier, see `imap-move.sh` 18 | -------------------------------------------------------------------------------- /imap-move.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | 3 | s_user="old.name@example.net" 4 | s_pass="some-secret-word" 5 | s_host="imap.gmail.com:993" 6 | 7 | t_user="old.name@example.com" 8 | t_pass="some-secret-word" 9 | t_host="imap.gmail.com:993" 10 | 11 | s="imap-ssl://${s_user}:${s_pass}@${s_host}/" 12 | t="imap-ssl://${t_user}:${t_pass}@${t_host}/" 13 | 14 | # Move Mailbox to Mailbox 15 | php ./imap-move.php --wipe --source $s --target $t 16 | 17 | # Move Mailbox to Subfolder 18 | php ./imap-move.php --wipe --source $s --target "$t/some-folder" 19 | 20 | # Or Copy Source 21 | php ./imap-move.php --copy --source $s --target $d 22 | -------------------------------------------------------------------------------- /imap-move.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | listPath(); 37 | //print_r($tgt_path_list); 38 | 39 | $src_path_list = $S->listPath(); 40 | // print_r($src_path_list); 41 | // exit; 42 | 43 | foreach ($src_path_list as $path) { 44 | 45 | echo "S: {$path['name']} = {$path['attribute']}\n"; 46 | 47 | // Skip Logic Below 48 | if (_path_skip($path)) { 49 | echo "S: Skip\n"; 50 | continue; 51 | } 52 | 53 | // Source Path 54 | $S->setPath($path['name']); 55 | $src_path_stat = $S->pathStat(); 56 | // print_r($src_path_stat); 57 | if (empty($src_path_stat['mail_count'])) { 58 | echo "S: Skip {$src_path_stat['mail_count']} messages\n"; 59 | continue; 60 | } 61 | echo "S: {$src_path_stat['mail_count']} messages\n"; 62 | 63 | // Target Path 64 | $tgt_path = _path_map($path['name']); 65 | echo "T: Indexing: $tgt_path\n"; 66 | $T->setPath($tgt_path); // Creates if needed 67 | // Show info on Target 68 | $tgt_path_stat = $T->pathStat(); 69 | echo "T: {$tgt_path_stat['mail_count']} messages\n"; 70 | // Build Index of Target 71 | $tgt_mail_list = array(); 72 | for ($i=1;$i<=$tgt_path_stat['mail_count'];$i++) { 73 | $mail = $T->mailStat($i); 74 | $tgt_mail_list[ $mail['message_id'] ] = !empty($mail['subject']) ? $mail['subject'] : "[ No Subject ] Message $i"; 75 | } 76 | 77 | // print_r($tgt_mail_list); 78 | 79 | // for ($src_idx=1;$src_idx<=$src_path_stat['mail_count'];$src_idx++) { 80 | for ($src_idx=$src_path_stat['mail_count'];$src_idx>=1;$src_idx--) { 81 | 82 | $stat = $S->mailStat($src_idx); 83 | $stat['answered'] = trim($stat['Answered']); 84 | $stat['unseen'] = trim($stat['Unseen']); 85 | if (empty($stat['subject'])) $stat['subject'] = "[ No Subject ] Message $src_idx"; 86 | // print_r($stat['message_id']); exit; 87 | 88 | if (array_key_exists($stat['message_id'],$tgt_mail_list)) { 89 | echo "S:$src_idx Mail: {$stat['subject']} Copied Already\n"; 90 | $S->mailWipe($i); 91 | continue; 92 | } 93 | 94 | echo "S:$src_idx {$stat['subject']} ({$stat['MailDate']})\n {$src_path_stat['path']} => "; 95 | if ($_ENV['fake']) { 96 | echo "\n"; 97 | continue; 98 | } 99 | 100 | $S->mailGet($src_idx); 101 | $opts = array(); 102 | if (empty($stat['unseen'])) $opts[] = '\Seen'; 103 | if (!empty($stat['answered'])) { 104 | $opts[] = '\Answered'; 105 | } 106 | $opts = implode(' ',$opts); 107 | $date = strftime('%d-%b-%Y %H:%M:%S +0000',strtotime($stat['MailDate'])); 108 | 109 | if ($res = $T->mailPut(file_get_contents('mail'),$opts,$date)) { 110 | // echo "T: $res\n"; 111 | $S->mailWipe($src_idx); 112 | echo "{$tgt_path_stat['path']}\n"; 113 | } else { 114 | die("Fail to Put $res\n"); 115 | } 116 | 117 | if ($_ENV['once']) die("--one and done\n"); 118 | 119 | } 120 | } 121 | 122 | class IMAP 123 | { 124 | private $_c; // Connection Handle 125 | private $_c_host; // Server Part {} 126 | private $_c_base; // Base Path Requested 127 | /** 128 | Connect to an IMAP 129 | */ 130 | function __construct($uri) 131 | { 132 | $this->_c = null; 133 | $this->_c_host = sprintf('{%s',$uri['host']); 134 | if (!empty($uri['port'])) { 135 | $this->_c_host.= sprintf(':%d',$uri['port']); 136 | } 137 | switch (strtolower(@$uri['scheme'])) { 138 | case 'imap-ssl': 139 | $this->_c_host.= '/ssl'; 140 | break; 141 | case 'imap-tls': 142 | $this->_c_host.= '/tls'; 143 | break; 144 | default: 145 | } 146 | $this->_c_host.= '}'; 147 | 148 | $this->_c_base = $this->_c_host; 149 | // Append Path? 150 | if (!empty($uri['path'])) { 151 | $x = ltrim($uri['path'],'/'); 152 | if (!empty($x)) { 153 | $this->_c_base = $x; 154 | } 155 | } 156 | echo "imap_open($this->_c_host)\n"; 157 | $this->_c = imap_open($this->_c_host,$uri['user'],$uri['pass']); 158 | // echo implode(', ',imap_errors()); 159 | } 160 | 161 | /** 162 | List folders matching pattern 163 | @param $pat * == all folders, % == folders at current level 164 | */ 165 | function listPath($pat='*') 166 | { 167 | $ret = array(); 168 | $list = imap_getmailboxes($this->_c, $this->_c_host,$pat); 169 | foreach ($list as $x) { 170 | $ret[] = array( 171 | 'name' => $x->name, 172 | 'attribute' => $x->attributes, 173 | 'delimiter' => $x->delimiter, 174 | ); 175 | } 176 | return $ret; 177 | } 178 | 179 | /** 180 | Get a Message 181 | */ 182 | function mailGet($i) 183 | { 184 | // return imap_body($this->_c,$i,FT_PEEK); 185 | return imap_savebody($this->_c,'mail',$i,null,FT_PEEK); 186 | } 187 | 188 | /** 189 | Store a Message with proper date 190 | */ 191 | function mailPut($mail,$opts,$date) 192 | { 193 | $stat = $this->pathStat(); 194 | // print_r($stat); 195 | // $opts = '\\Draft'; // And Others? 196 | // $opts = null; 197 | // exit; 198 | $ret = imap_append($this->_c,$stat['check_path'],$mail,$opts,$date); 199 | if ($buf = imap_errors()) { 200 | die(print_r($buf,true)); 201 | } 202 | return $ret; 203 | 204 | } 205 | 206 | /** 207 | Message Info 208 | */ 209 | function mailStat($i) 210 | { 211 | $head = imap_headerinfo($this->_c,$i); 212 | return (array)$head; 213 | // $stat = imap_fetch_overview($this->_c,$i); 214 | // return (array)$stat[0]; 215 | } 216 | 217 | /** 218 | Immediately Delete and Expunge the message 219 | */ 220 | function mailWipe($i) 221 | { 222 | if ( ($_ENV['wipe']) && (imap_delete($this->_c,$i)) ) return imap_expunge($this->_c); 223 | } 224 | 225 | /** 226 | Sets the Current Mailfolder, Creates if Needed 227 | */ 228 | function setPath($p,$make=false) 229 | { 230 | // echo "setPath($p);\n"; 231 | if (substr($p,0,1)!='{') { 232 | $p = $this->_c_host . trim($p,'/'); 233 | } 234 | // echo "setPath($p);\n"; 235 | 236 | $ret = imap_reopen($this->_c,$p); // Always returns true :( 237 | $buf = imap_errors(); 238 | if (empty($buf)) { 239 | return true; 240 | } 241 | 242 | $buf = implode(', ',$buf); 243 | if (preg_match('/NONEXISTENT/',$buf)) { 244 | // Likley Couldn't Open on Gmail Side, So Create 245 | $ret = imap_createmailbox($this->_c,$p); 246 | $buf = imap_errors(); 247 | if (empty($buf)) { 248 | // Reopen Again 249 | imap_reopen($this->_c,$p); 250 | return true; 251 | } 252 | die(print_r($buf,true)."\nFailed to Create setPath($p)\n"); 253 | } 254 | die(print_r($buf,true)."\nFailed to Switch setPath($p)\n"); 255 | } 256 | 257 | /** 258 | Returns Information about the current Path 259 | */ 260 | function pathStat() 261 | { 262 | $res = imap_mailboxmsginfo($this->_c); 263 | $ret = array( 264 | 'date' => $res->Date, 265 | 'path' => $res->Mailbox, 266 | 'mail_count' => $res->Nmsgs, 267 | 'size' => $res->Size, 268 | ); 269 | $res = imap_check($this->_c); 270 | $ret['check_date'] = $res->Date; 271 | $ret['check_mail_count'] = $res->Nmsgs; 272 | $ret['check_path'] = $res->Mailbox; 273 | // $ret = array_merge($ret,$res); 274 | return $ret; 275 | } 276 | } 277 | 278 | /** 279 | Process CLI Arguments 280 | */ 281 | function _args($argc,$argv) 282 | { 283 | 284 | $_ENV['src'] = null; 285 | $_ENV['tgt'] = null; 286 | $_ENV['copy'] = false; 287 | $_ENV['fake'] = false; 288 | $_ENV['once'] = false; 289 | $_ENV['wipe'] = false; 290 | 291 | for ($i=1;$i<$argc;$i++) { 292 | switch ($argv[$i]) { 293 | case '--source': 294 | case '-s': 295 | $i++; 296 | if (!empty($argv[$i])) { 297 | $_ENV['src'] = parse_url($argv[$i]); 298 | } 299 | break; 300 | case '--target': 301 | case '-t': // Destination 302 | $i++; 303 | if (!empty($argv[$i])) { 304 | $_ENV['tgt'] = parse_url($argv[$i]); 305 | } 306 | break; 307 | case '--copy': 308 | // Given a Path to Copy To? 309 | $chk = $argv[$i+1]; 310 | if (substr($chk,0,1)!='-') { 311 | $_ENV['copy_path'] = $chk; 312 | if (!is_dir($chk)) { 313 | echo "Creating Copy Directory\n"; 314 | mkdir($chk,0755,true); 315 | } 316 | $i++; 317 | } 318 | break; 319 | case '--fake': 320 | $_ENV['fake'] = true; 321 | break; 322 | case '--once': 323 | $_ENV['once'] = true; 324 | break; 325 | case '--wipe': 326 | $_ENV['wipe'] = true; 327 | break; 328 | default: 329 | echo "arg: {$argv[$i]}\n"; 330 | } 331 | } 332 | 333 | if ( (empty($_ENV['src']['path'])) || ($_ENV['src']['path']=='/') ) { 334 | $_ENV['src']['path'] = '/INBOX'; 335 | } 336 | if ( (empty($_ENV['tgt']['path'])) || ($_ENV['tgt']['path']=='/') ) { 337 | $_ENV['tgt']['path'] = '/INBOX'; 338 | } 339 | } 340 | 341 | /** 342 | @return mapped path name 343 | */ 344 | function _path_map($x) 345 | { 346 | if (preg_match('/}(.+)$/',$x,$m)) { 347 | switch (strtolower($m[1])) { 348 | // case 'inbox': return null; 349 | case 'deleted items': return '[Gmail]/Trash'; 350 | case 'drafts': return '[Gmail]/Drafts'; 351 | case 'junk e-mail': return '[Gmail]/Spam'; 352 | case 'sent items': return '[Gmail]/Sent Mail'; 353 | } 354 | $x = str_replace('INBOX/',null,$m[1]); 355 | } 356 | return $x; 357 | } 358 | 359 | /** 360 | @return true if we should skip this path 361 | */ 362 | function _path_skip($path) 363 | { 364 | if ( ($path['attribute'] & LATT_NOSELECT) == LATT_NOSELECT) { 365 | return true; 366 | } 367 | // All Mail, Trash, Starred have this attribute 368 | if ( ($path['attribute'] & 96) == 96) { 369 | return true; 370 | } 371 | 372 | // Skip by Pattern 373 | if (preg_match('/}(.+)$/',$path['name'],$m)) { 374 | switch (strtolower($m[1])) { 375 | case '[gmail]/all mail': 376 | case '[gmail]/sent mail': 377 | case '[gmail]/spam': 378 | case '[gmail]/starred': 379 | return true; 380 | } 381 | } 382 | 383 | // By First Folder Part of Name 384 | if (preg_match('/}([^\/]+)/',$path['name'],$m)) { 385 | switch (strtolower($m[1])) { 386 | // This bundle is from Exchange 387 | case 'journal': 388 | case 'notes': 389 | case 'outbox': 390 | case 'rss feeds': 391 | case 'sync issues': 392 | return true; 393 | } 394 | } 395 | 396 | return false; 397 | } 398 | --------------------------------------------------------------------------------