1#!/usr/bin/env php 2<?php 3/** 4 * Compare status of multiple/all Dovecot mailboxes eg. after a migration 5 * 6 * Usage: dovecot-mailbox-status-compare.php -c <dovecot.conf to compare with> [-p <prefix eg "INBOX">] [-s <hierarchy separator, default />] \ 7 * <status fields, eg. "messages"> [mailbox or wildcard] [user(s)] 8 * 9 * If no users are given they are read from stdin 10 */ 11 12$args = $_SERVER['argv']; 13$cmd = array_shift($args); 14 15$options = [ 16 '-c' => true, // required 17 '-p' => null, // optional prefix incl. hierarchy seperator 18 0 => ['messages', 'unseen'], // first arg: fields 19 1 => '*', // second, optional arg: mailbox/folder to compare 20]; 21$num_options = count($options); 22$fields =& $options[0]; 23$mailbox_pattern = &$options[1]; 24$prefix =& $options['-p']; 25 26for($n = 0; ($arg = array_shift($args)) !== null; ) 27{ 28 if ($arg[0] == '-') 29 { 30 if (!array_key_exists($arg, $options)) 31 { 32 die("Unknown arg '$arg'!\n\n"); 33 } 34 $options[$arg] = array_shift($args); 35 } 36 else 37 { 38 $options[$n++] = $arg; 39 } 40} 41$users = array_slice($options, $num_options); 42 43foreach($options as $opt => $value) 44{ 45 if ($value === true) die("Required option $opt missing!\n\n". 46 "Usage: $cmd -c <dovecot.conf to compare with> [-p <prefix incl. hiearachy seperator eg \"INBOX/\">] \\\n". 47 "\t<status fields, eg. \"messages\"> [mailbox or wildcard] [user(s)]\n\n"); 48} 49 50if (empty($users)) 51{ 52 $users = preg_split('/\r?\n/', file_get_contents('php://stdin')); 53 array_pop($users); 54} 55 56//var_dump($options); 57//var_dump($users); 58 59foreach((array)$users as $user) 60{ 61 $cmd_opts = ['mailbox', 'status', '-u', $user, implode(' ', (array)$fields), $mailbox_pattern]; 62 $cmd = 'doveadm '.implode(' ', array_map('escapeshellarg', $cmd_opts)); 63 echo $cmd."\n"; 64 $lines = $ret = null; 65 exec($cmd, $lines, $ret); 66 if ($ret) die("Error: $cmd\n\n".implode("\n", $lines)."\n"); 67 $mailboxes = parse_flow($lines); 68 //var_dump($mailboxes); 69 70 $cmd2_opts = ['-c', $options['-c'], 'mailbox', 'status', '-u', $user, implode(' ', (array)$fields), $prefix.$mailbox_pattern]; 71 if (!empty($prefix) && $mailbox_pattern == '*') $cmd2_opts[] = 'INBOX'; // wont get reported otherwise 72 $cmd2 = 'doveadm '.implode(' ', array_map('escapeshellarg', $cmd2_opts)); 73 echo $cmd2."\n"; 74 $lines2 = $ret2 = null; 75 exec($cmd2, $lines2, $ret2); 76 if ($ret2) die("Error: $cmd2\n\n".implode("\n", $lines2)."\n"); 77 $mailboxes2 = parse_flow($lines2, $prefix); 78 //var_dump($mailboxes2); 79 80 // first check for missing folders from mailboxes (existing in mailboxes2) 81 $missing = array_diff_key($mailboxes2, $mailboxes); 82 foreach($missing as $mailbox => $values) 83 { 84 echo "$user: missing folder $mailbox with ".format_fields($values)."\n"; 85 } 86 87 // check mailboxes in both for field values 88 $differences = []; 89 foreach(array_intersect_key($mailboxes2, $mailboxes) as $mailbox => $values) 90 { 91 foreach($values as $name => $value) 92 { 93 if ($mailboxes[$mailbox][$name] != $value) 94 { 95 $differences[$mailbox][$name] = $mailboxes[$mailbox][$name] - $value; 96 } 97 } 98 if (isset($differences[$mailbox])) 99 { 100 echo "$user: folder $mailbox differs with ".format_fields($differences[$mailbox])."\n"; 101 } 102 } 103 104 // check for new mailboxes not in mailboxes2 105 $new = array_diff_key($mailboxes, $mailboxes2); 106 foreach($new as $mailbox => $values) 107 { 108 echo "$user: new folder $mailbox with ".format_fields($values)."\n"; 109 } 110 // write summary to stderr 111 fprintf(STDERR, "$user: %d missing, %d different, %d new folders\n", count($missing), count($differences), count($new)); 112} 113 114/** 115 * Parse flow formatted output from doveadm mailbox status 116 * 117 * @param array $lines output 118 * @param string $prefix ='' prefix incl. hierarchy seperator to strip 119 * @return array mailbox => [ field => value ] 120 */ 121function parse_flow($lines, $prefix='') 122{ 123 // -f flow returns space separated name=value pairs prefixed with mailbox name (can contain space!) 124 $parsed = array_map(function($line) 125 { 126 $matches = null; 127 if (preg_match_all("/([^= ]+)=([^ ]*) */", $line, $matches)) 128 { 129 $values = array_combine($matches[1], $matches[2]); 130 $values['mailbox'] = substr($line, 0, strlen($line)-strlen(implode('', $matches[0]))-1); 131 return $values; 132 } 133 }, $lines); 134 135 $mailboxes = []; 136 foreach($parsed as $values) 137 { 138 $mailbox = $values['mailbox']; 139 unset($values['mailbox']); 140 if (!empty($prefix) && strpos($mailbox, $prefix) === 0) 141 { 142 $mailbox = substr($mailbox, strlen($prefix)); 143 } 144 $mailboxes[$mailbox] = $values; 145 } 146 return $mailboxes; 147} 148 149function format_fields(array $fields) 150{ 151 return implode(', ', array_map(function($value, $key) 152 { 153 return "$key: $value"; 154 }, $fields, array_keys($fields))); 155}