diff --git a/src/app/Backends/IMAP.php b/src/app/Backends/IMAP.php index fd0b56d9..fd4d590b 100644 --- a/src/app/Backends/IMAP.php +++ b/src/app/Backends/IMAP.php @@ -1,785 +1,796 @@ 'lrs', 'read-write' => 'lrswitedn', 'full' => 'lrswipkxtecdn', ]; /** * Delete a group. * * @param \App\Group $group Group * * @return bool True if a group was deleted successfully, False otherwise * @throws \Exception */ public static function deleteGroup(Group $group): bool { $domainName = explode('@', $group->email, 2)[1]; // Cleanup ACL // FIXME: Since all groups in Kolab4 have email address, // should we consider using it in ACL instead of the name? // Also we need to decide what to do and configure IMAP appropriately, // right now groups in ACL does not work for me at all. // Commented out in favor of a nightly cleanup job, for performance reasons // \App\Jobs\IMAP\AclCleanupJob::dispatch($group->name, $domainName); return true; } + + public static function folderInfo(string $user, string $mailbox): array + { + $config = self::getConfig(); + $imap = self::initIMAP($config, $user); + $imap->select($mailbox); + $result = $imap->data; + $imap->closeConnection(); + return $result; + } + /** * Create a mailbox. * * @param \App\User $user User * * @return bool True if a mailbox was created successfully, False otherwise * @throws \Exception */ public static function createUser(User $user): bool { $config = self::getConfig(); $imap = self::initIMAP($config); $mailbox = self::toUTF7('user/' . $user->email); // Mailbox already exists if (self::folderExists($imap, $mailbox)) { $imap->closeConnection(); self::createDefaultFolders($user); return true; } // Create the mailbox if (!$imap->createFolder($mailbox)) { \Log::error("Failed to create mailbox {$mailbox}"); $imap->closeConnection(); return false; } // Wait until it's propagated (for Cyrus Murder setup) // FIXME: Do we still need this? if (strpos($imap->conn->data['GREETING'] ?? '', 'Cyrus IMAP Murder') !== false) { $tries = 30; while ($tries-- > 0) { $folders = $imap->listMailboxes('', $mailbox); if (is_array($folders) && count($folders)) { break; } sleep(1); $imap->closeConnection(); $imap = self::initIMAP($config); } } // Set quota $quota = $user->countEntitlementsBySku('storage') * 1048576; if ($quota) { $imap->setQuota($mailbox, ['storage' => $quota]); } self::createDefaultFolders($user); $imap->closeConnection(); return true; } /** * Create default folders for the user. * * @param \App\User $user User */ public static function createDefaultFolders(User $user): void { if ($defaultFolders = \config('imap.default_folders')) { $config = self::getConfig(); // Log in as user to set private annotations and subscription state $imap = self::initIMAP($config, $user->email); foreach ($defaultFolders as $name => $folderconfig) { try { $mailbox = self::toUTF7($name); self::createFolder($imap, $mailbox, true, $folderconfig['metadata']); } catch (\Exception $e) { \Log::warning("Failed to create the default folder. " . $e->getMessage()); } } $imap->closeConnection(); } } /** * Delete a mailbox. * * @param \App\User $user User * * @return bool True if a mailbox was deleted successfully, False otherwise * @throws \Exception */ public static function deleteUser(User $user): bool { $config = self::getConfig(); $imap = self::initIMAP($config); $mailbox = self::toUTF7('user/' . $user->email); // To delete the mailbox cyrus-admin needs extra permissions $imap->setACL($mailbox, $config['user'], 'c'); // Delete the mailbox (no need to delete subfolders?) $result = $imap->deleteFolder($mailbox); if (!$result) { // Ignore the error if the folder doesn't exist (maybe it was removed already). if (!self::folderExists($imap, $mailbox)) { \Log::info("The mailbox to delete was already removed: $mailbox"); $result = true; } } $imap->closeConnection(); // Cleanup ACL // Commented out in favor of a nightly cleanup job, for performance reasons // \App\Jobs\IMAP\AclCleanupJob::dispatch($user->email); return $result; } /** * Update a mailbox (quota). * * @param \App\User $user User * * @return bool True if a mailbox was updated successfully, False otherwise * @throws \Exception */ public static function updateUser(User $user): bool { $config = self::getConfig(); $imap = self::initIMAP($config); $mailbox = self::toUTF7('user/' . $user->email); $result = true; // Set quota $quota = $user->countEntitlementsBySku('storage') * 1048576; if ($quota) { $result = $imap->setQuota($mailbox, ['storage' => $quota]); } $imap->closeConnection(); return $result; } /** * Create a resource. * * @param \App\Resource $resource Resource * * @return bool True if a resource was created successfully, False otherwise * @throws \Exception */ public static function createResource(Resource $resource): bool { $config = self::getConfig(); $imap = self::initIMAP($config); $settings = $resource->getSettings(['invitation_policy', 'folder']); $mailbox = self::toUTF7($settings['folder']); $metadata = ['/shared/vendor/kolab/folder-type' => 'event']; $acl = []; if (!empty($settings['invitation_policy'])) { if (preg_match('/^manual:(\S+@\S+)$/', $settings['invitation_policy'], $m)) { $acl = ["{$m[1]}, full"]; } } self::createFolder($imap, $mailbox, false, $metadata, Utils::ensureAclPostPermission($acl)); $imap->closeConnection(); return true; } /** * Update a resource. * * @param \App\Resource $resource Resource * @param array $props Old resource properties * * @return bool True if a resource was updated successfully, False otherwise * @throws \Exception */ public static function updateResource(Resource $resource, array $props = []): bool { $config = self::getConfig(); $imap = self::initIMAP($config); $settings = $resource->getSettings(['invitation_policy', 'folder']); $folder = $settings['folder']; $mailbox = self::toUTF7($folder); // Rename the mailbox (only possible if we have the old folder) if (!empty($props['folder']) && $props['folder'] != $folder) { $oldMailbox = self::toUTF7($props['folder']); if (!$imap->renameFolder($oldMailbox, $mailbox)) { \Log::error("Failed to rename mailbox {$oldMailbox} to {$mailbox}"); $imap->closeConnection(); return false; } } // ACL $acl = []; if (!empty($settings['invitation_policy'])) { if (preg_match('/^manual:(\S+@\S+)$/', $settings['invitation_policy'], $m)) { $acl = ["{$m[1]}, full"]; } } self::aclUpdate($imap, $mailbox, Utils::ensureAclPostPermission($acl)); $imap->closeConnection(); return true; } /** * Delete a resource. * * @param \App\Resource $resource Resource * * @return bool True if a resource was deleted successfully, False otherwise * @throws \Exception */ public static function deleteResource(Resource $resource): bool { $config = self::getConfig(); $imap = self::initIMAP($config); $settings = $resource->getSettings(['folder']); $mailbox = self::toUTF7($settings['folder']); // To delete the mailbox cyrus-admin needs extra permissions $imap->setACL($mailbox, $config['user'], 'c'); // Delete the mailbox (no need to delete subfolders?) $result = $imap->deleteFolder($mailbox); $imap->closeConnection(); return $result; } /** * Create a shared folder. * * @param \App\SharedFolder $folder Shared folder * * @return bool True if a falder was created successfully, False otherwise * @throws \Exception */ public static function createSharedFolder(SharedFolder $folder): bool { $config = self::getConfig(); $imap = self::initIMAP($config); $settings = $folder->getSettings(['acl', 'folder']); $acl = !empty($settings['acl']) ? json_decode($settings['acl'], true) : []; $mailbox = self::toUTF7($settings['folder']); $metadata = ['/shared/vendor/kolab/folder-type' => $folder->type]; self::createFolder($imap, $mailbox, false, $metadata, Utils::ensureAclPostPermission($acl)); $imap->closeConnection(); return true; } /** * Update a shared folder. * * @param \App\SharedFolder $folder Shared folder * @param array $props Old folder properties * * @return bool True if a falder was updated successfully, False otherwise * @throws \Exception */ public static function updateSharedFolder(SharedFolder $folder, array $props = []): bool { $config = self::getConfig(); $imap = self::initIMAP($config); $settings = $folder->getSettings(['acl', 'folder']); $acl = !empty($settings['acl']) ? json_decode($settings['acl'], true) : []; $folder = $settings['folder']; $mailbox = self::toUTF7($folder); // Rename the mailbox if (!empty($props['folder']) && $props['folder'] != $folder) { $oldMailbox = self::toUTF7($props['folder']); if (!$imap->renameFolder($oldMailbox, $mailbox)) { \Log::error("Failed to rename mailbox {$oldMailbox} to {$mailbox}"); $imap->closeConnection(); return false; } } // Note: Shared folder type does not change // ACL self::aclUpdate($imap, $mailbox, Utils::ensureAclPostPermission($acl)); $imap->closeConnection(); return true; } /** * Delete a shared folder. * * @param \App\SharedFolder $folder Shared folder * * @return bool True if a falder was deleted successfully, False otherwise * @throws \Exception */ public static function deleteSharedFolder(SharedFolder $folder): bool { $config = self::getConfig(); $imap = self::initIMAP($config); $settings = $folder->getSettings(['folder']); $mailbox = self::toUTF7($settings['folder']); // To delete the mailbox cyrus-admin needs extra permissions $imap->setACL($mailbox, $config['user'], 'c'); // Delete the mailbox $result = $imap->deleteFolder($mailbox); $imap->closeConnection(); return $result; } /** * Check if a shared folder is set up. * * @param string $folder Folder name, e.g. shared/Resources/Name@domain.tld * * @return bool True if a folder exists and is set up, False otherwise */ public static function verifySharedFolder(string $folder): bool { $config = self::getConfig(); $imap = self::initIMAP($config); // Convert the folder from UTF8 to UTF7-IMAP if (\preg_match('#^(shared/|shared/Resources/)(.+)(@[^@]+)$#', $folder, $matches)) { $folderName = self::toUTF7($matches[2]); $folder = $matches[1] . $folderName . $matches[3]; } // FIXME: just listMailboxes() does not return shared folders at all $metadata = $imap->getMetadata($folder, ['/shared/vendor/kolab/folder-type']); $imap->closeConnection(); // Note: We have to use error code to distinguish an error from "no mailbox" response if ($imap->errornum === \rcube_imap_generic::ERROR_NO) { return false; } if ($imap->errornum !== \rcube_imap_generic::ERROR_OK) { throw new \Exception("Failed to get folder metadata from IMAP"); } return true; } /** * Convert UTF8 string to UTF7-IMAP encoding */ public static function toUTF7(string $string): string { return \mb_convert_encoding($string, 'UTF7-IMAP', 'UTF8'); } /** * Check if an account is set up * * @param string $username User login (email address) * * @return bool True if an account exists and is set up, False otherwise */ public static function verifyAccount(string $username): bool { $config = self::getConfig(); $imap = self::initIMAP($config); $mailbox = self::toUTF7('user/' . $username); // Mailbox already exists if (self::folderExists($imap, $mailbox)) { $imap->closeConnection(); return true; } $imap->closeConnection(); return false; } /** * Check if an account is set up * * @param string $username User login (email address) * * @return bool True if an account exists and is set up, False otherwise */ public static function verifyDefaultFolders(string $username): bool { $config = self::getConfig(); $imap = self::initIMAP($config, $username); foreach (\config('imap.default_folders') as $mb => $_metadata) { $mailbox = self::toUTF7($mb); if (!self::folderExists($imap, $mailbox)) { $imap->closeConnection(); return false; } } $imap->closeConnection(); return true; } /** * Check if we can connect to the imap server * * @return bool True on success */ public static function healthcheck(): bool { $config = self::getConfig(); $imap = self::initIMAP($config); $imap->closeConnection(); return true; } /** * Remove ACL for a specified user/group anywhere in the IMAP * * @param string $ident ACL identifier (user email or e.g. group name) * @param string $domain ACL domain */ public static function aclCleanup(string $ident, string $domain = ''): void { $config = self::getConfig(); $imap = self::initIMAP($config); if (strpos($ident, '@')) { $domain = explode('@', $ident, 2)[1]; } $callback = function ($folder) use ($imap, $ident) { $acl = $imap->getACL($folder); if (is_array($acl) && isset($acl[$ident])) { \Log::info("Cleanup: Removing {$ident} from ACL on {$folder}"); $imap->deleteACL($folder, $ident); } }; $folders = $imap->listMailboxes('', "user/*@{$domain}"); if (!is_array($folders)) { $imap->closeConnection(); throw new \Exception("Failed to get IMAP folders"); } array_walk($folders, $callback); $folders = $imap->listMailboxes('', "shared/*@{$domain}"); if (!is_array($folders)) { $imap->closeConnection(); throw new \Exception("Failed to get IMAP folders"); } array_walk($folders, $callback); $imap->closeConnection(); } /** * Remove ACL entries pointing to non-existent users/groups, for a specified domain * * @param string $domain Domain namespace * @param bool $dry_run Output ACL entries to delete, but do not delete */ public static function aclCleanupDomain(string $domain, bool $dry_run = false): void { $config = self::getConfig(); $imap = self::initIMAP($config); // Collect available (existing) users/groups // FIXME: Should we limit this to the requested domain or account? // FIXME: For groups should we use name or email? $idents = User::pluck('email') // ->concat(Group::pluck('name')) ->concat(['anyone', 'anonymous', $config['user']]) ->all(); $callback = function ($folder) use ($imap, $idents, $dry_run) { $acl = $imap->getACL($folder); if (is_array($acl)) { $owner = null; if (preg_match('|^user/([^/@]+).*@([^@/]+)$|', $folder, $m)) { $owner = $m[1] . '@' . $m[2]; } foreach (array_keys($acl) as $key) { if ($owner && $key === $owner) { // Don't even try to remove the folder's owner entry continue; } if (!in_array($key, $idents)) { if ($dry_run) { echo "{$folder} {$key} {$acl[$key]}\n"; } else { \Log::info("Cleanup: Removing {$key} from ACL on {$folder}"); $imap->deleteACL($folder, $key); } } } } }; $folders = $imap->listMailboxes('', "user/*@{$domain}"); if (!is_array($folders)) { $imap->closeConnection(); throw new \Exception("Failed to get IMAP folders"); } array_walk($folders, $callback); $folders = $imap->listMailboxes('', "shared/*@{$domain}"); if (!is_array($folders)) { $imap->closeConnection(); throw new \Exception("Failed to get IMAP folders"); } array_walk($folders, $callback); $imap->closeConnection(); } /** * Create a folder and set some default properties * * @param \rcube_imap_generic $imap The imap instance * @param string $mailbox Mailbox name * @param bool $subscribe Subscribe to the folder * @param array $metadata Metadata to set on the folder * @param array $acl Acl to set on the folder * * @return bool True when having a folder created, False if it already existed. * @throws \Exception */ private static function createFolder($imap, string $mailbox, $subscribe = false, $metadata = null, $acl = null) { if (self::folderExists($imap, $mailbox)) { return false; } if (!$imap->createFolder($mailbox)) { throw new \Exception("Failed to create mailbox {$mailbox}"); } if (!empty($acl)) { self::aclUpdate($imap, $mailbox, $acl, true); } if ($subscribe) { $imap->subscribe($mailbox); } foreach ($metadata as $key => $value) { $imap->setMetadata($mailbox, [$key => $value]); } return true; } /** * Convert Kolab ACL into IMAP user->rights array */ private static function aclToImap($acl): array { if (empty($acl)) { return []; } return \collect($acl) ->mapWithKeys(function ($item, $key) { list($user, $rights) = explode(',', $item, 2); $rights = trim($rights); return [trim($user) => self::ACL_MAP[$rights] ?? $rights]; }) ->all(); } /** * Update folder ACL */ private static function aclUpdate($imap, $mailbox, $acl, bool $isNew = false) { $imapAcl = $isNew ? [] : $imap->getACL($mailbox); if (is_array($imapAcl)) { foreach (self::aclToImap($acl) as $user => $rights) { if (empty($imapAcl[$user]) || implode('', $imapAcl[$user]) !== $rights) { $imap->setACL($mailbox, $user, $rights); } unset($imapAcl[$user]); } foreach ($imapAcl as $user => $rights) { $imap->deleteACL($mailbox, $user); } } } /** * Check if an IMAP folder exists */ private static function folderExists($imap, string $folder): bool { $folders = $imap->listMailboxes('', $folder); if (!is_array($folders)) { $imap->closeConnection(); throw new \Exception("Failed to get IMAP folders"); } return count($folders) > 0; } /** * Initialize connection to IMAP */ private static function initIMAP(array $config, string $login_as = null) { $imap = new \rcube_imap_generic(); if (\config('app.debug')) { $imap->setDebug(true, 'App\Backends\IMAP::logDebug'); } if ($login_as) { $config['options']['auth_cid'] = $config['user']; $config['options']['auth_pw'] = $config['password']; $config['options']['auth_type'] = 'PLAIN'; $config['user'] = $login_as; } $imap->connect($config['host'], $config['user'], $config['password'], $config['options']); if (!$imap->connected()) { $message = sprintf("Login failed for %s against %s. %s", $config['user'], $config['host'], $imap->error); \Log::error($message); throw new \Exception("Connection to IMAP failed"); } return $imap; } /** * Get LDAP configuration for specified access level */ private static function getConfig() { $uri = \parse_url(\config('imap.uri')); $default_port = 143; $ssl_mode = null; if (isset($uri['scheme'])) { if (preg_match('/^(ssl|imaps)/', $uri['scheme'])) { $default_port = 993; $ssl_mode = 'ssl'; } elseif ($uri['scheme'] === 'tls') { $ssl_mode = 'tls'; } } $config = [ 'host' => $uri['host'], 'user' => \config('imap.admin_login'), 'password' => \config('imap.admin_password'), 'options' => [ 'port' => !empty($uri['port']) ? $uri['port'] : $default_port, 'ssl_mode' => $ssl_mode, 'socket_options' => [ 'ssl' => [ 'verify_peer' => \config('imap.verify_peer'), 'verify_peer_name' => \config('imap.verify_peer'), 'verify_host' => \config('imap.verify_host') ], ], ], ]; return $config; } /** * Debug logging callback */ public static function logDebug($conn, $msg): void { $msg = '[IMAP] ' . $msg; \Log::debug($msg); } } diff --git a/src/app/Backends/Roundcube.php b/src/app/Backends/Roundcube.php index 6ed0e2e3..d782d9d6 100644 --- a/src/app/Backends/Roundcube.php +++ b/src/app/Backends/Roundcube.php @@ -1,282 +1,352 @@ table(self::FILESTORE_TABLE) ->where('user_id', self::userId($email)) ->where('context', 'enigma') ->delete(); } /** * List all files from the Enigma filestore. * * @param string $email User email address * * @return array List of Enigma filestore records */ public static function enigmaList(string $email): array { return self::dbh()->table(self::FILESTORE_TABLE) ->where('user_id', self::userId($email)) ->where('context', 'enigma') ->orderBy('filename') ->get() ->all(); } /** * Synchronize Enigma filestore from/to specified directory * * @param string $email User email address * @param string $homedir Directory location */ public static function enigmaSync(string $email, string $homedir): void { $db = self::dbh(); $debug = \config('app.debug'); $user_id = self::userId($email); $root = \config('filesystems.disks.pgp.root'); $fs = Storage::disk('pgp'); $files = []; $result = $db->table(self::FILESTORE_TABLE)->select('file_id', 'filename', 'mtime') ->where('user_id', $user_id) ->where('context', 'enigma') ->get(); foreach ($result as $record) { $file = $homedir . '/' . $record->filename; $mtime = $fs->exists($file) ? $fs->lastModified($file) : 0; $files[] = $record->filename; if ($mtime < $record->mtime) { $file_id = $record->file_id; $record = $db->table(self::FILESTORE_TABLE)->select('file_id', 'data', 'mtime') ->where('file_id', $file_id) ->first(); $data = $record ? base64_decode($record->data) : false; if ($data === false) { \Log::error("Failed to sync $file ({$file_id}). Decode error."); continue; } if ($fs->put($file, $data, true)) { // Note: Laravel Filesystem API does not provide touch method touch("$root/$file", $record->mtime); if ($debug) { \Log::debug("[SYNC] Fetched file: $file"); } } } } // Remove files not in database foreach (array_diff(self::enigmaFilesList($homedir), $files) as $file) { $file = $homedir . '/' . $file; if ($fs->delete($file)) { if ($debug) { \Log::debug("[SYNC] Removed file: $file"); } } } // No records found, do initial sync if already have the keyring if (empty($file)) { self::enigmaSave($email, $homedir); } } /** * Save the keys database * * @param string $email User email address * @param string $homedir Directory location * @param bool $is_empty Set to Tre if it is a initial save */ public static function enigmaSave(string $email, string $homedir, bool $is_empty = false): void { $db = self::dbh(); $debug = \config('app.debug'); $user_id = self::userId($email); $fs = Storage::disk('pgp'); $records = []; if (!$is_empty) { $records = $db->table(self::FILESTORE_TABLE)->select('file_id', 'filename', 'mtime') ->where('user_id', $user_id) ->where('context', 'enigma') ->get() ->keyBy('filename') ->all(); } foreach (self::enigmaFilesList($homedir) as $filename) { $file = $homedir . '/' . $filename; $mtime = $fs->exists($file) ? $fs->lastModified($file) : 0; $existing = !empty($records[$filename]) ? $records[$filename] : null; unset($records[$filename]); if ($mtime && (empty($existing) || $mtime > $existing->mtime)) { $data = base64_encode($fs->get($file)); /* if (empty($maxsize)) { $maxsize = min($db->get_variable('max_allowed_packet', 1048500), 4*1024*1024) - 2000; } if (strlen($data) > $maxsize) { \Log::error("Failed to save $file. Size exceeds max_allowed_packet."); continue; } */ $result = $db->table(self::FILESTORE_TABLE)->updateOrInsert( ['user_id' => $user_id, 'context' => 'enigma', 'filename' => $filename], ['mtime' => $mtime, 'data' => $data] ); if ($debug) { \Log::debug("[SYNC] Pushed file: $file"); } } } // Delete removed files from database foreach (array_keys($records) as $filename) { $file = $homedir . '/' . $filename; $result = $db->table(self::FILESTORE_TABLE) ->where('user_id', $user_id) ->where('context', 'enigma') ->where('filename', $filename) ->delete(); if ($debug) { \Log::debug("[SYNC] Removed file: $file"); } } } /** * Delete a Roundcube user. * * @param string $email User email address */ public static function deleteUser(string $email): void { $db = self::dbh(); $db->table(self::USERS_TABLE)->where('username', \strtolower($email))->delete(); } /** * Find the Roundcube user identifier for the specified user. * * @param string $email User email address * @param bool $create Make sure the user record exists * * @returns ?int Roundcube user identifier */ public static function userId(string $email, bool $create = true): ?int { $db = self::dbh(); $user = $db->table(self::USERS_TABLE)->select('user_id') ->where('username', \strtolower($email)) ->first(); // Create a user record, without it we can't use the Roundcube storage if (empty($user)) { if (!$create) { return null; } $uri = \parse_url(\config('imap.uri')); $user_id = (int) $db->table(self::USERS_TABLE)->insertGetId( [ 'username' => $email, 'mail_host' => $uri['host'], 'created' => now()->toDateTimeString(), ], 'user_id' ); $username = \App\User::where('email', $email)->first()->name(); $db->table(self::IDENTITIES_TABLE)->insert([ 'user_id' => $user_id, 'email' => $email, 'name' => $username, 'changed' => now()->toDateTimeString(), 'standard' => 1, ]); return $user_id; } return (int) $user->user_id; } /** * Returns list of Enigma user homedir files to backup/sync */ private static function enigmaFilesList(string $homedir) { $files = []; $fs = Storage::disk('pgp'); foreach (self::$enigma_files as $file) { if ($fs->exists($homedir . '/' . $file)) { $files[] = $file; } } foreach ($fs->files($homedir . '/private-keys-v1.d') as $file) { if (preg_match('/\.key$/', $file)) { $files[] = substr($file, strlen($homedir . '/')); } } return $files; } + + /** + * Returns list of Enigma user homedir files to backup/sync + */ + public static function syncrotonInspect(string $email) + { + $db = self::dbh(); + + $user = $db->table(self::USERS_TABLE)->select('user_id') + ->where('username', \strtolower($email)) + ->first(); + + $devices = $db->table(self::SYNCROTON_DEVICE_TABLE)->select('id', 'deviceid', 'devicetype') + ->where('owner_id', $user->user_id) + ->get() + ->all(); + + foreach ($devices as $device) { + $result[$device->id]['deviceid'] = $device->deviceid; + $result[$device->id]['devicetype'] = $device->devicetype; + + $synckey = $db->table(self::SYNCROTON_SYNCKEY_TABLE)->select() + ->where('device_id', $device->id) + ->where('type', 'FolderSync') + ->orderBy('counter', 'desc') + ->first(); + + $result[$device->id]['FolderSync'] = [ + "counter" => $synckey->counter, + "lastsync" => $synckey->lastsync, + ]; + + $folders = $db->table(self::SYNCROTON_FOLDER_TABLE)->select() + ->where('device_id', $device->id) + ->get()->all(); + foreach ($folders as $folder) { + $synckey = $db->table(self::SYNCROTON_SYNCKEY_TABLE)->select() + ->where('device_id', $device->id) + ->where('type', $folder->id) + ->orderBy('counter', 'desc') + ->first(); + + if ($synckey) { + $result[$device->id]['folders'][$folder->id] = [ + "counter" => $synckey->counter, + "lastsync" => $synckey->lastsync, + "modseq" => $synckey->extra_data ? json_decode($synckey->extra_data)->modseq : null, + ]; + } + $result[$device->id]['folders'][$folder->id]['name'] = $folder->displayname; + + $imapInfo = IMAP::folderInfo("admin@kolab.local", $folder->displayname); + $result[$device->id]['folders'][$folder->id]['imapModseq'] = $imapInfo['HIGHESTMODSEQ'] ?? null; + $result[$device->id]['folders'][$folder->id]['imapMessagecount'] = $imapInfo['EXISTS'] ?? null; + + $contentCount = $db->table(self::SYNCROTON_CONTENT_TABLE)->select() + ->where('device_id', $device->id) + ->where('folder_id', $folder->id) + ->count(); + $result[$device->id]['folders'][$folder->id]['contentCount'] = $contentCount; + } + } + + return $result; + } } diff --git a/src/app/Console/Commands/SyncrotonCommand.php b/src/app/Console/Commands/SyncrotonCommand.php new file mode 100644 index 00000000..b2583a02 --- /dev/null +++ b/src/app/Console/Commands/SyncrotonCommand.php @@ -0,0 +1,43 @@ +argument('user')); + $this->info(json_encode($data, JSON_PRETTY_PRINT)); + } +}