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}