diff --git a/bin/inspect.php b/bin/inspect.php index a8ed6b1..da348a2 100755 --- a/bin/inspect.php +++ b/bin/inspect.php @@ -1,244 +1,338 @@ #!/usr/bin/env php | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Christian Mollekopf | +--------------------------------------------------------------------------+ */ define('RCUBE_INSTALL_PATH', realpath(dirname(__FILE__) . '/../') . '/'); define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'lib/plugins/'); // Define include path $include_path = RCUBE_INSTALL_PATH . 'lib' . PATH_SEPARATOR; $include_path .= RCUBE_INSTALL_PATH . 'lib/ext' . PATH_SEPARATOR; $include_path .= ini_get('include_path'); set_include_path($include_path); // include composer autoloader (if available) if (@file_exists(RCUBE_INSTALL_PATH . 'vendor/autoload.php')) { require RCUBE_INSTALL_PATH . 'vendor/autoload.php'; } // include global functions from Roundcube Framework require_once 'Roundcube/bootstrap.php'; + +function filterTypeToIMAPSearch($filter_type = 0) +{ + switch ($filter_type) { + case 1: + $mod = '-1 day'; + break; + case 2: + $mod = '-3 days'; + break; + case 3: + $mod = '-1 week'; + break; + case 4: + $mod = '-2 weeks'; + break; + case 5: + $mod = '-1 month'; + break; + } + + if (!empty($mod)) { + $dt = new DateTime('now', new DateTimeZone('UTC')); + $dt->modify($mod); + // RFC3501: IMAP SEARCH + return 'SINCE ' . $dt->format('d-M-Y'); + } + + return ""; +} + + $opts = rcube_utils::get_opt([ 'e' => 'email', 'p' => 'adminpassword', 'd' => 'debug', ]); if (empty($opts['email'])) { rcube::raise_error("Email address not specified (--email).", false, true); } $email = $opts['email']; $proxyAuth = false; if ($password = $opts['adminpassword']) { $proxyAuth = true; $user = "cyrus-admin"; } else { $password = $opts['password']; } if (empty($password)) { rcube::raise_error("Password not specified (--adminpassword/--password).", false, true); } $rcube = rcube::get_instance(); $default_port = $rcube->config->get('default_port', 143); $default_host = $rcube->config->get('default_host'); $imap = new \rcube_imap_generic(); if ($proxyAuth) { $options['auth_cid'] = $user; $options['auth_pw'] = $password; } $options['auth_type'] = 'PLAIN'; $options['port'] = $default_port; $options['socket_options'] = [ 'ssl' => [ 'verify_peer_name' => false, 'verify_peer' => false, 'allow_self_signed' => true ] ]; ini_set('display_errors', 1); error_reporting(E_ALL); $debug = !empty($opts['debug']); $imap->setDebug($debug); if (!$imap->connect($default_host, $email, $password, $options)) { rcube::raise_error("Failed to connect to imap.", false, true); } $rcube = \rcube::get_instance(); $db = $rcube->get_dbh(); $select = $db->query( "SELECT `user_id` FROM `users`" . " WHERE `username` = ?" . " ORDER BY `user_id` DESC", \strtolower($email) ); if ($data = $db->fetch_assoc($select)) { $userid = $data['user_id']; } else { rcube::raise_error("User not found in roundcube database (only available after first login): $email.", false, true); } print("Found the user with id: $userid\n"); $devicesSelect = $db->query( "SELECT `id`, `deviceid`, `devicetype` FROM `syncroton_device`" . " WHERE `owner_id` = ?", $userid ); $result = []; while ($data = $db->fetch_assoc($devicesSelect)) { $deviceid = $data["deviceid"]; $device_id = $data["id"]; $result[$device_id]['deviceid'] = $deviceid; $result[$device_id]['devicetype'] = $data["devicetype"]; $select = $db->limitquery( "SELECT `counter`, `lastsync` FROM `syncroton_synckey`" . " WHERE `device_id` = ? AND `type` = 'FolderSync'" . " ORDER BY `counter` DESC", 0, 1, $device_id ); if ($data = $db->fetch_assoc($select)) { $result[$device_id]['FolderSync'] = [ "counter" => $data['counter'], "lastsync" => $data['lastsync'], ]; } else { echo("Synckey not found.\n"); } $folderSelect = $db->query( "SELECT * FROM `syncroton_folder`" . " WHERE `device_id` = ?", $device_id ); while ($folder = $db->fetch_assoc($folderSelect)) { $select = $db->limitquery( "SELECT `counter`, `lastsync`, `extra_data` FROM `syncroton_synckey`" . " WHERE `device_id` = ? AND `type` = ?" . " ORDER BY `counter` DESC", 0, 1, $device_id, $folder['id'] ); if ($data = $db->fetch_assoc($select)) { $result[$device_id]['folders'][$folder['id']] = [ "counter" => $data['counter'], "lastsync" => $data['lastsync'], - "lastfiltertype" => $data['lastfiltertype'] ?? null, "modseq" => $data['extra_data'] ? json_decode($data['extra_data'])->modseq : null, ]; } $result[$device_id]['folders'][$folder['id']]['name'] = $folder['displayname']; + $result[$device_id]['folders'][$folder['id']]['class'] = $folder['class']; + $result[$device_id]['folders'][$folder['id']]['lastfiltertype'] = $folder['lastfiltertype'] ?? null; $imap->select($folder['displayname']); $result[$device_id]['folders'][$folder['id']]['imapModseq'] = $imap->data['HIGHESTMODSEQ'] ?? null; - $index = $imap->search($folder['displayname'], 'ALL UNDELETED', false, ['COUNT']); + $index = $imap->search( + $folder['displayname'], + 'ALL UNDELETED ' . filterTypeToIMAPSearch($folder['lastfiltertype']), + false, + ['COUNT'] + ); if (!$index->is_error()) { $result[$device_id]['folders'][$folder['id']]['imapMessagecount'] = $index->count(); } $select = $db->query( "SELECT count(*) FROM `syncroton_content`" . " WHERE `device_id` = ? AND `folder_id` = ?", $device_id, $folder['id'] ); if ($data = $db->fetch_assoc($select)) { $result[$device_id]['folders'][$folder['id']]['contentCount'] = array_values($data)[0]; } } } if ($debug) { var_export($result); } function println($output) { print("{$output}\n"); } function filterType($value) { - if (!$value) { - return "No filter"; - } - switch($value) { - case 0: return "No filter"; - case 1: return "1 day"; - case 2: return "3 days"; - case 3: return "1 week"; - case 4: return "2 weeks"; - case 5: return "1 month"; - case 6: return "3 months"; - case 7: return "6 months"; - case 8: return "Filter by incomplete tasks"; - } - return "Unknown value: $value"; + if (!$value) { + return "No filter"; + } + switch($value) { + case 0: return "No filter"; + case 1: return "1 day"; + case 2: return "3 days"; + case 3: return "1 week"; + case 4: return "2 weeks"; + case 5: return "1 month"; + case 6: return "3 months (WARNING: not implemented)"; + case 7: return "6 months (WARNING: not implemented)"; + case 8: return "Filter by incomplete tasks"; + } + return "Unknown value: $value"; +} + +function getContentUids($db, $device_id, $folder_id) { + $contentSelect = $db->query( + "SELECT contentid FROM `syncroton_content`" + . " WHERE `device_id` = ? AND `folder_id` = ? AND `is_deleted` = 0", + $device_id, $folder_id + ); + + $contentUids = []; + while ($content = $db->fetch_assoc($contentSelect)) { + $contentUids[] = explode('::', $content['contentid'])[1]; + } + return $contentUids; +} + +function getImapUids($imap, $folder, $lastfiltertype) { + $imap->select($folder); + $index = $imap->search($folder, 'ALL UNDELETED ' . filterTypeToIMAPSearch($lastfiltertype), true); + if (!$index->is_error()) { + return $index->get(); + } + return []; } println(""); foreach ($result as $deviceId => $values) { println("Device: $deviceId"); println(" Last folder sync: " . $values['FolderSync']['lastsync']); println(" Folder sync count: " . $values['FolderSync']['counter']); println(" Folders:"); foreach ($values['folders'] ?? [] as $folderId => $folder) { println(" " . $folder['name'] . " ($folderId)"); $messageCount = $folder['contentCount']; $totalCount = $folder['imapMessagecount'] ?? "unknown"; $modseq = $folder['modseq'] ?? "none"; $imapModseq = $folder['imapModseq']; // We're not using modseq for groupware folders if ($messageCount == $totalCount && ($modseq == "none" || $modseq == $imapModseq)) { println(" Status: Fully Synced ($messageCount messages)"); } else { println(" Status: Incomplete ($messageCount/$totalCount messages)"); println(" Modseq: " . $modseq . "/" . $imapModseq); } println(" Last sync: " . ($folder['lastsync'] ?? "None")); println(" Number of syncs: " . ($folder['counter'] ?? "None")); println(" Filter type: " . filterType($folder['lastfiltertype'] ?? null)); + + if (($folder['class'] == "Email") && ($folder['counter'] ?? false) && $messageCount != $totalCount && ($modseq == "none" || $modseq == $imapModseq)) { + if (($folder['lastfiltertype'] ?? false) && $messageCount > $totalCount) { + // This doesn't have to indicate an issue, since the timewindow of the filter wanders, so some messages that have been synchronized may no longer match the window. + } else { + println(" Issue Detected: The sync state seems to be inconsistent. The device should be fully synced, but the sync counts differ."); + println(" There are $messageCount ContentParts (should match number of messages on the device), but $totalCount messages in IMAP matching the filter."); + + $contentUids = getContentUids($db, $deviceId, $folderId); + $imapUids = getImapUids($imap, $folder['name'], $folder['lastfiltertype'] ?? null); + + $entries = array_diff($imapUids, $contentUids); + if (!empty($entries)) { + println(" The following messages are on the server, but not the device:"); + foreach ($entries as $uid) { + println(" $uid"); + //TODO get details from imap? + } + } + + $entries = array_diff($contentUids, $imapUids); + if (!empty($entries)) { + println(" The following messages are on the device, but not the server:"); + foreach ($entries as $uid) { + println(" $uid"); + //TODO get details from the content part? + //TODO display creation_synckey? + } + } + println(""); + } + } + println(""); } }