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("");
}
}