diff --git a/src/app/Backends/IMAP.php b/src/app/Backends/IMAP.php --- a/src/app/Backends/IMAP.php +++ b/src/app/Backends/IMAP.php @@ -3,31 +3,362 @@ namespace App\Backends; use App\Domain; +use App\Group; +use App\Resource; +use App\SharedFolder; use App\User; class IMAP { /** - * Check if an account is set up + * Delete a group (cleanup ACL). * - * @param string $username User login (email address) + * @param \App\Group $group Group * - * @return bool True if an account exists and is set up, False otherwise + * @return bool True if a group was deleted successfully, False otherwise + * @throws \Exception */ - public static function verifyAccount(string $username): bool + public static function deleteGroup(Group $group): bool { $config = self::getConfig(); - $imap = self::initIMAP($config, $username); + $imap = self::initIMAP($config); - $folders = $imap->listMailboxes('', '*'); + // TODO: Cleanup ACL $imap->closeConnection(); - if (!is_array($folders)) { - throw new \Exception("Failed to get IMAP folders"); + return true; + } + + /** + * 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(); + return true; } - return count($folders) > 0; + // Create the mailbox + if (!$imap->createFolder($mailbox)) { + // Error + $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]); + } + + $imap->closeConnection(); + + return true; + } + + /** + * 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); + + // TODO: Cleanup ACL + + $imap->closeConnection(); + + 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']); + $domainName = explode('@', $resource->email, 2)[1]; + $mailbox = self::toUTF7($settings['folder']); + + // Mailbox already exists + if (self::folderExists($imap, $mailbox)) { + $imap->closeConnection(); + return true; + } + + // Create the shared folder + if (!$imap->createFolder($mailbox)) { + // Error + $imap->closeConnection(); + return false; + } + + // Set folder type + $imap->setMetadata($mailbox, ['/shared/vendor/kolab/folder-type' => 'event']); + + // Set ACL + if (!empty($acl)) { + // TODO + } + + $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']); + $domainName = explode('@', $resource->email, 2)[1]; + $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)) { + // Error + $imap->closeConnection(); + return false; + } + } + + // Note: Shared folder type does not change + + // TODO: 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']); + $domainName = explode('@', $resource->email, 2)[1]; + $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']); + $domainName = explode('@', $folder->email, 2)[1]; + $acl = !empty($settings['acl']) ? json_decode($settings['acl'], true) : null; + $mailbox = self::toUTF7($settings['folder']); + + // Mailbox already exists + if (self::folderExists($imap, $mailbox)) { + $imap->closeConnection(); + return true; + } + + // Create the mailbox + if (!$imap->createFolder($mailbox)) { + // Error + $imap->closeConnection(); + return false; + } + + // Set folder type + $imap->setMetadata($mailbox, ['/shared/vendor/kolab/folder-type' => $folder->type]); + + // Set ACL + if (!empty($acl)) { + // TODO + } + + $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']); + $domainName = explode('@', $folder->email, 2)[1]; + $acl = !empty($settings['acl']) ? json_decode($settings['acl'], true) : null; + $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)) { + // Error + $imap->closeConnection(); + return false; + } + } + + // Note: Shared folder type does not change + + // TODO: 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']); + $domainName = explode('@', $folder->email, 2)[1]; + $mailbox = self::toUTF7($settings['folder'] ?: "shared/{$folder->name}@{$domainName}"); + + // 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; } /** @@ -44,7 +375,7 @@ // Convert the folder from UTF8 to UTF7-IMAP if (\preg_match('#^(shared/|shared/Resources/)(.+)(@[^@]+)$#', $folder, $matches)) { - $folderName = \mb_convert_encoding($matches[2], 'UTF7-IMAP', 'UTF8'); + $folderName = self::toUTF7($matches[2]); $folder = $matches[1] . $folderName . $matches[3]; } @@ -67,6 +398,70 @@ return true; } + /** + * Get quota for a mailbox. + * + * @param \App\User $user User + * + * @return array Quota information + * @throws \Exception + */ + public static function getQuota(User $user): array + { + $config = self::getConfig(); + $imap = self::initIMAP($config); + + $mailbox = 'user/' . $user->email; + + $quota = $imap->getQuota($mailbox); + + $imap->closeConnection(); + + if (!is_array($quota)) { + throw new \Exception("Failed to get IMAP quota for $mailbox"); + } + + return $quota; + } + + /** + * 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, $username); + + $folders = $imap->listMailboxes('', '*'); + + $imap->closeConnection(); + + if (!is_array($folders)) { + throw new \Exception("Failed to get IMAP folders"); + } + + return count($folders) > 0; + } + + /** + * 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 */ @@ -145,4 +540,12 @@ \Log::debug($msg); } + + /** + * Convert UTF8 string to UTF7-IMAP encoding + */ + private static function toUTF7(string $string): string + { + return \mb_convert_encoding($string, 'UTF7-IMAP', 'UTF8'); + } } diff --git a/src/app/Http/Controllers/API/V4/DomainsController.php b/src/app/Http/Controllers/API/V4/DomainsController.php --- a/src/app/Http/Controllers/API/V4/DomainsController.php +++ b/src/app/Http/Controllers/API/V4/DomainsController.php @@ -299,20 +299,17 @@ * @param \App\Domain $domain Domain object * @param string $step Step identifier (as in self::statusInfo()) * - * @return bool True if the execution succeeded, False otherwise + * @return bool|null True if the execution succeeded, False if not, Null when + * the job has been sent to the worker (result unknown) */ - public static function execProcessStep(Domain $domain, string $step): bool + public static function execProcessStep(Domain $domain, string $step): ?bool { try { switch ($step) { case 'domain-ldap-ready': - // Domain not in LDAP, create it - if (!$domain->isLdapReady()) { - LDAP::createDomain($domain); - $domain->status |= Domain::STATUS_LDAP_READY; - $domain->save(); - } - return $domain->isLdapReady(); + // Use worker to do the job + \App\Jobs\Domain\CreateJob::dispatch($domain->id); + return null; case 'domain-verified': // Domain existence not verified diff --git a/src/app/Http/Controllers/API/V4/ResourcesController.php b/src/app/Http/Controllers/API/V4/ResourcesController.php --- a/src/app/Http/Controllers/API/V4/ResourcesController.php +++ b/src/app/Http/Controllers/API/V4/ResourcesController.php @@ -160,30 +160,10 @@ switch ($step) { case 'resource-ldap-ready': - // Resource not in LDAP, create it - $job = new \App\Jobs\Resource\CreateJob($resource->id); - $job->handle(); - - $resource->refresh(); - - return $resource->isLdapReady(); - case 'resource-imap-ready': - // Resource not in IMAP? Verify again - // Do it synchronously if the imap admin credentials are available - // otherwise let the worker do the job - if (!\config('imap.admin_password')) { - \App\Jobs\Resource\VerifyJob::dispatch($resource->id); - - return null; - } - - $job = new \App\Jobs\Resource\VerifyJob($resource->id); - $job->handle(); - - $resource->refresh(); - - return $resource->isImapReady(); + // Use worker to do the job, frontend might not have the IMAP admin credentials + \App\Jobs\Resource\CreateJob::dispatch($resource->id); + return null; } } catch (\Exception $e) { \Log::error($e); diff --git a/src/app/Http/Controllers/API/V4/SharedFoldersController.php b/src/app/Http/Controllers/API/V4/SharedFoldersController.php --- a/src/app/Http/Controllers/API/V4/SharedFoldersController.php +++ b/src/app/Http/Controllers/API/V4/SharedFoldersController.php @@ -156,30 +156,10 @@ switch ($step) { case 'shared-folder-ldap-ready': - // Shared folder not in LDAP, create it - $job = new \App\Jobs\SharedFolder\CreateJob($folder->id); - $job->handle(); - - $folder->refresh(); - - return $folder->isLdapReady(); - case 'shared-folder-imap-ready': - // Shared folder not in IMAP? Verify again - // Do it synchronously if the imap admin credentials are available - // otherwise let the worker do the job - if (!\config('imap.admin_password')) { - \App\Jobs\SharedFolder\VerifyJob::dispatch($folder->id); - - return null; - } - - $job = new \App\Jobs\SharedFolder\VerifyJob($folder->id); - $job->handle(); - - $folder->refresh(); - - return $folder->isImapReady(); + // Use worker to do the job, frontend might not have the IMAP admin credentials + \App\Jobs\SharedFolder\CreateJob::dispatch($folder->id); + return null; } } catch (\Exception $e) { \Log::error($e); diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php --- a/src/app/Http/Controllers/API/V4/UsersController.php +++ b/src/app/Http/Controllers/API/V4/UsersController.php @@ -530,30 +530,10 @@ switch ($step) { case 'user-ldap-ready': - // User not in LDAP, create it - $job = new \App\Jobs\User\CreateJob($user->id); - $job->handle(); - - $user->refresh(); - - return $user->isLdapReady(); - case 'user-imap-ready': - // User not in IMAP? Verify again - // Do it synchronously if the imap admin credentials are available - // otherwise let the worker do the job - if (!\config('imap.admin_password')) { - \App\Jobs\User\VerifyJob::dispatch($user->id); - - return null; - } - - $job = new \App\Jobs\User\VerifyJob($user->id); - $job->handle(); - - $user->refresh(); - - return $user->isImapReady(); + // Use worker to do the job, frontend might not have the IMAP admin credentials + \App\Jobs\User\CreateJob::dispatch($user->id); + return null; } } catch (\Exception $e) { \Log::error($e); diff --git a/src/app/Jobs/Resource/CreateJob.php b/src/app/Jobs/Resource/CreateJob.php --- a/src/app/Jobs/Resource/CreateJob.php +++ b/src/app/Jobs/Resource/CreateJob.php @@ -30,11 +30,6 @@ return; } - if ($resource->isLdapReady()) { - $this->fail(new \Exception("Resource {$this->resourceId} is already marked as ldap-ready.")); - return; - } - // see if the domain is ready $domain = $resource->domain(); @@ -53,9 +48,21 @@ return; } - \App\Backends\LDAP::createResource($resource); + if (!$resource->isLdapReady()) { + \App\Backends\LDAP::createResource($resource); - $resource->status |= \App\Resource::STATUS_LDAP_READY; - $resource->save(); + $resource->status |= \App\Resource::STATUS_LDAP_READY; + $resource->save(); + } + + if (!$resource->isImapReady()) { + if (!\App\Backends\IMAP::createResource($resource)) { + throw new \Exception("Failed to create mailbox for resource {$this->resourceId}."); + } + + $resource->status |= \App\Resource::STATUS_IMAP_READY; + $resource->status |= \App\Resource::STATUS_ACTIVE; + $resource->save(); + } } } diff --git a/src/app/Jobs/Resource/DeleteJob.php b/src/app/Jobs/Resource/DeleteJob.php --- a/src/app/Jobs/Resource/DeleteJob.php +++ b/src/app/Jobs/Resource/DeleteJob.php @@ -25,18 +25,22 @@ return; } - \App\Backends\LDAP::deleteResource($resource); - - $resource->status |= \App\Resource::STATUS_DELETED; - if ($resource->isLdapReady()) { + \App\Backends\LDAP::deleteResource($resource); + $resource->status ^= \App\Resource::STATUS_LDAP_READY; + $resource->save(); // important } if ($resource->isImapReady()) { + if (!\App\Backends\IMAP::deleteResource($resource)) { + throw new \Exception("Failed to delete mailbox for resource {$this->resourceId}."); + } + $resource->status ^= \App\Resource::STATUS_IMAP_READY; } + $resource->status |= \App\Resource::STATUS_DELETED; $resource->save(); } } diff --git a/src/app/Jobs/Resource/UpdateJob.php b/src/app/Jobs/Resource/UpdateJob.php --- a/src/app/Jobs/Resource/UpdateJob.php +++ b/src/app/Jobs/Resource/UpdateJob.php @@ -19,12 +19,20 @@ return; } - // Cancel the update if the resource is deleted or not yet in LDAP - if (!$resource->isLdapReady() || $resource->isDeleted()) { + // Cancel the update if the resource is deleted + if ($resource->isDeleted()) { $this->delete(); return; } - \App\Backends\LDAP::updateResource($resource); + if ($resource->isLdapReady()) { + \App\Backends\LDAP::updateResource($resource); + } + + if ($resource->isImapReady()) { + if (!\App\Backends\IMAP::updateResource($resource, $this->properties)) { + throw new \Exception("Failed to update mailbox for resource {$this->resourceId}."); + } + } } } diff --git a/src/app/Jobs/ResourceJob.php b/src/app/Jobs/ResourceJob.php --- a/src/app/Jobs/ResourceJob.php +++ b/src/app/Jobs/ResourceJob.php @@ -13,6 +13,13 @@ */ abstract class ResourceJob extends CommonJob { + /** + * Old values of the resource properties on update (key -> value) + * + * @var array + */ + protected $properties = []; + /** * The ID for the \App\Resource. This is the shortest globally unique identifier and saves Redis space * compared to a serialized version of the complete \App\Resource object. @@ -31,13 +38,15 @@ /** * Create a new job instance. * - * @param int $resourceId The ID for the resource to process. + * @param int $resourceId The ID for the resource to process. + * @param array $properties Old values of the resource properties on update * * @return void */ - public function __construct(int $resourceId) + public function __construct(int $resourceId, array $properties = []) { $this->resourceId = $resourceId; + $this->properties = (array) $properties; $resource = $this->getResource(); diff --git a/src/app/Jobs/SharedFolder/CreateJob.php b/src/app/Jobs/SharedFolder/CreateJob.php --- a/src/app/Jobs/SharedFolder/CreateJob.php +++ b/src/app/Jobs/SharedFolder/CreateJob.php @@ -30,11 +30,6 @@ return; } - if ($folder->isLdapReady()) { - $this->fail(new \Exception("Shared folder {$this->folderId} is already marked as ldap-ready.")); - return; - } - // see if the domain is ready $domain = $folder->domain(); @@ -53,9 +48,21 @@ return; } - \App\Backends\LDAP::createSharedFolder($folder); + if (!$folder->isLdapReady()) { + \App\Backends\LDAP::createSharedFolder($folder); - $folder->status |= \App\SharedFolder::STATUS_LDAP_READY; - $folder->save(); + $folder->status |= \App\SharedFolder::STATUS_LDAP_READY; + $folder->save(); + } + + if (!$folder->isImapReady()) { + if (!\App\Backends\IMAP::createSharedFolder($folder)) { + throw new \Exception("Failed to create mailbox for shared folder {$this->folderId}."); + } + + $folder->status |= \App\SharedFolder::STATUS_IMAP_READY; + $folder->status |= \App\SharedFolder::STATUS_ACTIVE; + $folder->save(); + } } } diff --git a/src/app/Jobs/SharedFolder/DeleteJob.php b/src/app/Jobs/SharedFolder/DeleteJob.php --- a/src/app/Jobs/SharedFolder/DeleteJob.php +++ b/src/app/Jobs/SharedFolder/DeleteJob.php @@ -25,18 +25,22 @@ return; } - \App\Backends\LDAP::deleteSharedFolder($folder); - - $folder->status |= \App\SharedFolder::STATUS_DELETED; - if ($folder->isLdapReady()) { + \App\Backends\LDAP::deleteSharedFolder($folder); + $folder->status ^= \App\SharedFolder::STATUS_LDAP_READY; + $folder->save(); } if ($folder->isImapReady()) { + if (!\App\Backends\IMAP::deleteSharedFolder($folder)) { + throw new \Exception("Failed to delete mailbox for shared folder {$this->folderId}."); + } + $folder->status ^= \App\SharedFolder::STATUS_IMAP_READY; } + $folder->status |= \App\SharedFolder::STATUS_DELETED; $folder->save(); } } diff --git a/src/app/Jobs/SharedFolder/UpdateJob.php b/src/app/Jobs/SharedFolder/UpdateJob.php --- a/src/app/Jobs/SharedFolder/UpdateJob.php +++ b/src/app/Jobs/SharedFolder/UpdateJob.php @@ -19,12 +19,20 @@ return; } - // Cancel the update if the folder is deleted or not yet in LDAP - if (!$folder->isLdapReady() || $folder->isDeleted()) { + // Cancel the update if the folder is deleted + if ($folder->isDeleted()) { $this->delete(); return; } - \App\Backends\LDAP::updateSharedFolder($folder); + if ($folder->isLdapReady()) { + \App\Backends\LDAP::updateSharedFolder($folder); + } + + if ($folder->isImapReady()) { + if (!\App\Backends\IMAP::updateSharedFolder($folder, $this->properties)) { + throw new \Exception("Failed to update mailbox for shared folder {$this->folderId}."); + } + } } } diff --git a/src/app/Jobs/SharedFolderJob.php b/src/app/Jobs/SharedFolderJob.php --- a/src/app/Jobs/SharedFolderJob.php +++ b/src/app/Jobs/SharedFolderJob.php @@ -27,16 +27,25 @@ */ protected $folderEmail; + /** + * Old values of the shared folder properties on update (key -> value) + * + * @var array + */ + protected $properties = []; + /** * Create a new job instance. * - * @param int $folderId The ID for the shared folder to process. + * @param int $folderId The ID for the shared folder to process + * @param array $properties Old values of the shared folder properties on update (key -> value) * * @return void */ - public function __construct(int $folderId) + public function __construct(int $folderId, array $properties = []) { $this->folderId = $folderId; + $this->properties = $properties; $folder = $this->getSharedFolder(); diff --git a/src/app/Jobs/User/CreateJob.php b/src/app/Jobs/User/CreateJob.php --- a/src/app/Jobs/User/CreateJob.php +++ b/src/app/Jobs/User/CreateJob.php @@ -42,16 +42,11 @@ return; } - if ($user->deleted_at) { + if ($user->trashed()) { $this->fail(new \Exception("User {$this->userId} is actually deleted.")); return; } - if ($user->isLdapReady()) { - $this->fail(new \Exception("User {$this->userId} is already marked as ldap-ready.")); - return; - } - // see if the domain is ready $domain = $user->domain(); @@ -70,9 +65,21 @@ return; } - \App\Backends\LDAP::createUser($user); + if (!$user->isLdapReady()) { + \App\Backends\LDAP::createUser($user); - $user->status |= \App\User::STATUS_LDAP_READY; - $user->save(); + $user->status |= \App\User::STATUS_LDAP_READY; + $user->save(); + } + + if (!$user->isImapReady()) { + if (!\App\Backends\IMAP::createUser($user)) { + throw new \Exception("Failed to create mailbox for user {$this->userId}."); + } + + $user->status |= \App\User::STATUS_IMAP_READY; + $user->status |= \App\User::STATUS_ACTIVE; + $user->save(); + } } } diff --git a/src/app/Jobs/User/DeleteJob.php b/src/app/Jobs/User/DeleteJob.php --- a/src/app/Jobs/User/DeleteJob.php +++ b/src/app/Jobs/User/DeleteJob.php @@ -30,18 +30,22 @@ return; } - \App\Backends\LDAP::deleteUser($user); - - $user->status |= \App\User::STATUS_DELETED; - if ($user->isLdapReady()) { + \App\Backends\LDAP::deleteUser($user); + $user->status ^= \App\User::STATUS_LDAP_READY; + $user->save(); // important here } if ($user->isImapReady()) { + if (!\App\Backends\IMAP::deleteUser($user)) { + throw new \Exception("Failed to delete mailbox for user {$this->userId}."); + } + $user->status ^= \App\User::STATUS_IMAP_READY; } + $user->status |= \App\User::STATUS_DELETED; $user->save(); } } diff --git a/src/app/Jobs/User/UpdateJob.php b/src/app/Jobs/User/UpdateJob.php --- a/src/app/Jobs/User/UpdateJob.php +++ b/src/app/Jobs/User/UpdateJob.php @@ -24,11 +24,14 @@ return; } - if (!$user->isLdapReady()) { - $this->delete(); - return; + if ($user->isLdapReady()) { + \App\Backends\LDAP::updateUser($user); } - \App\Backends\LDAP::updateUser($user); + if ($user->isImapReady()) { + if (!\App\Backends\IMAP::updateUser($user)) { + throw new \Exception("Failed to update mailbox for user {$this->userId}."); + } + } } } diff --git a/src/app/Observers/EntitlementObserver.php b/src/app/Observers/EntitlementObserver.php --- a/src/app/Observers/EntitlementObserver.php +++ b/src/app/Observers/EntitlementObserver.php @@ -61,6 +61,11 @@ $entitlement->entitleable->save(); $entitlement->createTransaction(\App\Transaction::ENTITLEMENT_CREATED); + + // Update the user IMAP mailbox quota + if ($entitlement->sku->title == 'storage') { + \App\Jobs\User\UpdateJob::dispatch($entitlement->entitleable_id); + } } /** @@ -72,6 +77,13 @@ */ public function deleted(Entitlement $entitlement) { + if (!$entitlement->entitleable->trashed()) { + $entitlement->entitleable->updated_at = Carbon::now(); + $entitlement->entitleable->save(); + + $entitlement->createTransaction(\App\Transaction::ENTITLEMENT_DELETED); + } + // Remove all configured 2FA methods from Roundcube database if ($entitlement->sku->title == '2fa') { // FIXME: Should that be an async job? @@ -79,11 +91,9 @@ $sf->removeFactors(); } - if (!$entitlement->entitleable->trashed()) { - $entitlement->entitleable->updated_at = Carbon::now(); - $entitlement->entitleable->save(); - - $entitlement->createTransaction(\App\Transaction::ENTITLEMENT_DELETED); + // Update the user IMAP mailbox quota + if ($entitlement->sku->title == 'storage') { + \App\Jobs\User\UpdateJob::dispatch($entitlement->entitleable_id); } } diff --git a/src/app/Observers/ResourceObserver.php b/src/app/Observers/ResourceObserver.php --- a/src/app/Observers/ResourceObserver.php +++ b/src/app/Observers/ResourceObserver.php @@ -15,7 +15,7 @@ */ public function creating(Resource $resource): void { - $resource->status |= Resource::STATUS_NEW | Resource::STATUS_ACTIVE; + $resource->status |= Resource::STATUS_NEW; } /** @@ -45,12 +45,8 @@ // Note: This is a single multi-insert query $resource->settings()->insert(array_values($settings)); - // Create resource record in LDAP, then check if it is created in IMAP - $chain = [ - new \App\Jobs\Resource\VerifyJob($resource->id), - ]; - - \App\Jobs\Resource\CreateJob::withChain($chain)->dispatch($resource->id); + // Create the resource in the backend (LDAP and IMAP) + \App\Jobs\Resource\CreateJob::dispatch($resource->id); } /** diff --git a/src/app/Observers/ResourceSettingObserver.php b/src/app/Observers/ResourceSettingObserver.php --- a/src/app/Observers/ResourceSettingObserver.php +++ b/src/app/Observers/ResourceSettingObserver.php @@ -17,7 +17,8 @@ public function created(ResourceSetting $resourceSetting) { if (in_array($resourceSetting->key, LDAP::RESOURCE_SETTINGS)) { - \App\Jobs\Resource\UpdateJob::dispatch($resourceSetting->resource_id); + $props = [$resourceSetting->key => $resourceSetting->value]; + \App\Jobs\Resource\UpdateJob::dispatch($resourceSetting->resource_id, $props); } } @@ -31,7 +32,8 @@ public function updated(ResourceSetting $resourceSetting) { if (in_array($resourceSetting->key, LDAP::RESOURCE_SETTINGS)) { - \App\Jobs\Resource\UpdateJob::dispatch($resourceSetting->resource_id); + $props = [$resourceSetting->key => $resourceSetting->value]; + \App\Jobs\Resource\UpdateJob::dispatch($resourceSetting->resource_id, $props); } } @@ -45,7 +47,8 @@ public function deleted(ResourceSetting $resourceSetting) { if (in_array($resourceSetting->key, LDAP::RESOURCE_SETTINGS)) { - \App\Jobs\Resource\UpdateJob::dispatch($resourceSetting->resource_id); + $props = [$resourceSetting->key => $resourceSetting->value]; + \App\Jobs\Resource\UpdateJob::dispatch($resourceSetting->resource_id, $props); } } } diff --git a/src/app/Observers/SharedFolderObserver.php b/src/app/Observers/SharedFolderObserver.php --- a/src/app/Observers/SharedFolderObserver.php +++ b/src/app/Observers/SharedFolderObserver.php @@ -19,7 +19,7 @@ $folder->type = 'mail'; } - $folder->status |= SharedFolder::STATUS_NEW | SharedFolder::STATUS_ACTIVE; + $folder->status |= SharedFolder::STATUS_NEW; } /** @@ -49,12 +49,8 @@ // Note: This is a single multi-insert query $folder->settings()->insert(array_values($settings)); - // Create folder record in LDAP, then check if it is created in IMAP - $chain = [ - new \App\Jobs\SharedFolder\VerifyJob($folder->id), - ]; - - \App\Jobs\SharedFolder\CreateJob::withChain($chain)->dispatch($folder->id); + // Create the shared folder in the backend (LDAP and IMAP) + \App\Jobs\SharedFolder\CreateJob::dispatch($folder->id); } /** diff --git a/src/app/Observers/SharedFolderSettingObserver.php b/src/app/Observers/SharedFolderSettingObserver.php --- a/src/app/Observers/SharedFolderSettingObserver.php +++ b/src/app/Observers/SharedFolderSettingObserver.php @@ -17,7 +17,8 @@ public function created(SharedFolderSetting $folderSetting) { if (in_array($folderSetting->key, LDAP::SHARED_FOLDER_SETTINGS)) { - \App\Jobs\SharedFolder\UpdateJob::dispatch($folderSetting->shared_folder_id); + $props = [$folderSetting->key => $folderSetting->value]; + \App\Jobs\SharedFolder\UpdateJob::dispatch($folderSetting->shared_folder_id, $props); } } @@ -31,7 +32,8 @@ public function updated(SharedFolderSetting $folderSetting) { if (in_array($folderSetting->key, LDAP::SHARED_FOLDER_SETTINGS)) { - \App\Jobs\SharedFolder\UpdateJob::dispatch($folderSetting->shared_folder_id); + $props = [$folderSetting->key => $folderSetting->value]; + \App\Jobs\SharedFolder\UpdateJob::dispatch($folderSetting->shared_folder_id, $props); } } @@ -45,7 +47,8 @@ public function deleted(SharedFolderSetting $folderSetting) { if (in_array($folderSetting->key, LDAP::SHARED_FOLDER_SETTINGS)) { - \App\Jobs\SharedFolder\UpdateJob::dispatch($folderSetting->shared_folder_id); + $props = [$folderSetting->key => $folderSetting->value]; + \App\Jobs\SharedFolder\UpdateJob::dispatch($folderSetting->shared_folder_id, $props); } } } diff --git a/src/app/Observers/UserObserver.php b/src/app/Observers/UserObserver.php --- a/src/app/Observers/UserObserver.php +++ b/src/app/Observers/UserObserver.php @@ -64,12 +64,8 @@ $user->wallets()->create(); - // Create user record in LDAP, then check if the account is created in IMAP - $chain = [ - new \App\Jobs\User\VerifyJob($user->id), - ]; - - \App\Jobs\User\CreateJob::withChain($chain)->dispatch($user->id); + // Create user record in the backend (LDAP and IMAP) + \App\Jobs\User\CreateJob::dispatch($user->id); if (\App\Tenant::getConfig($user->tenant_id, 'pgp.enable')) { \App\Jobs\PGP\KeyCreateJob::dispatch($user->id, $user->email); @@ -183,12 +179,8 @@ // FIXME: Should we reset user aliases? or re-validate them in any way? - // Create user record in LDAP, then run the verification process - $chain = [ - new \App\Jobs\User\VerifyJob($user->id), - ]; - - \App\Jobs\User\CreateJob::withChain($chain)->dispatch($user->id); + // Create user record in the backend (LDAP and IMAP) + \App\Jobs\User\CreateJob::dispatch($user->id); } /** diff --git a/src/app/Traits/EntitleableTrait.php b/src/app/Traits/EntitleableTrait.php --- a/src/app/Traits/EntitleableTrait.php +++ b/src/app/Traits/EntitleableTrait.php @@ -156,6 +156,24 @@ }); } + /** + * Count entitlements for the specified SKU. + * + * @param string $title The SKU title + * + * @return int Numer of entitlements + */ + public function countEntitlementsBySku(string $title): int + { + $sku = $this->skuByTitle($title); + + if (!$sku) { + return 0; + } + + return $this->entitlements()->where('sku_id', $sku->id)->count(); + } + /** * Entitlements for this object. * @@ -176,13 +194,7 @@ */ public function hasSku(string $title): bool { - $sku = $this->skuByTitle($title); - - if (!$sku) { - return false; - } - - return $this->entitlements()->where('sku_id', $sku->id)->count() > 0; + return $this->countEntitlementsBySku($title) > 0; } /** diff --git a/src/include/rcube_imap_generic.php b/src/include/rcube_imap_generic.php --- a/src/include/rcube_imap_generic.php +++ b/src/include/rcube_imap_generic.php @@ -3180,6 +3180,28 @@ return $result; } + /** + * Send the SETQUOTA command (RFC9208) + * + * @param string $root Quota root + * @param array $quota Quota limits e.g. ['storage' => 1024000'] + * + * @return boolean True on success, False on failure + */ + public function setQuota($root, $quota) + { + $fn = function ($key, $value) { + return strtoupper($key) . ' ' . $value; + }; + + $quota = implode(' ', array_map($fn, array_keys($quota), $quota)); + + $result = $this->execute('SETQUOTA', [$this->escape($root), "({$quota})"], + self::COMMAND_NORESPONSE); + + return ($result == self::ERROR_OK); + } + /** * Send the SETACL command (RFC4314) * diff --git a/src/tests/Feature/Backends/IMAPTest.php b/src/tests/Feature/Backends/IMAPTest.php --- a/src/tests/Feature/Backends/IMAPTest.php +++ b/src/tests/Feature/Backends/IMAPTest.php @@ -7,6 +7,134 @@ class IMAPTest extends TestCase { + private $user; + private $resource; + private $folder; + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + if ($this->user) { + $this->deleteTestUser($this->user->email); + } + if ($this->resource) { + $this->deleteTestResource($this->resource->email); + } + if ($this->folder) { + $this->deleteTestSharedFolder($this->folder->email); + } + + parent::tearDown(); + } + + /** + * Test creating/updating/deleting an IMAP account + * + * @group imap + */ + public function testUsers(): void + { + $this->user = $user = $this->getTestUser('test-' . time() . '@' . \config('app.domain')); + $storage = \App\Sku::withEnvTenantContext()->where('title', 'storage')->first(); + $user->assignSku($storage, 1, $user->wallets->first()); + + $expectedQuota = [ + 'user/' . $user->email => [ + 'storage' => [ + 'used' => 0, + 'total' => 1048576 + ] + ] + ]; + + // Create the mailbox + $result = IMAP::createUser($user); + $this->assertTrue($result); + // $this->assertTrue(IMAP::verifyAccount($user->email)); + + $quota = IMAP::getQuota($user); + $this->assertSame($expectedQuota, $quota['all']); + + // Update the mailbox (increase quota) + $user->assignSku($storage, 1, $user->wallets->first()); + $expectedQuota['user/' . $user->email]['storage']['total'] = 1048576 * 2; + + $result = IMAP::updateUser($user); + $this->assertTrue($result); + + $quota = IMAP::getQuota($user); + $this->assertSame($expectedQuota, $quota['all']); + + // Delete the mailbox + $result = IMAP::deleteUser($user); + $this->assertTrue($result); + + $this->expectException(\Exception::class); + IMAP::verifyAccount($user->email); + } + + /** + * Test creating/updating/deleting a resource + * + * @group imap + */ + public function testResources(): void + { + $this->resource = $resource = $this->getTestResource( + 'test-resource-' . time() . '@' . \config('app.domain'), + ['name' => 'Resource ©' . time()] + ); + + // Create the resource + $this->assertTrue(IMAP::createResource($resource)); + $this->assertTrue(IMAP::verifySharedFolder($imapFolder = $resource->getSetting('folder'))); + + // Update the resource (rename) + $resource->name = 'Resource1 ©'. time(); + $resource->save(); + $newImapFolder = $resource->getSetting('folder'); + + $this->assertTrue(IMAP::updateResource($resource, ['folder' => $imapFolder])); + $this->assertTrue($imapFolder != $newImapFolder); + $this->assertTrue(IMAP::verifySharedFolder($newImapFolder)); + + // Delete the resource + $this->assertTrue(IMAP::deleteResource($resource)); + $this->assertFalse(IMAP::verifySharedFolder($newImapFolder)); + } + + /** + * Test creating/updating/deleting a shared folder + * + * @group imap + */ + public function testSharedFolders(): void + { + $this->folder = $folder = $this->getTestSharedFolder( + 'test-folder-' . time() . '@' . \config('app.domain'), + ['name' => 'SharedFolder ©' . time()] + ); + + // Create the shared folder + $this->assertTrue(IMAP::createSharedFolder($folder)); + $this->assertTrue(IMAP::verifySharedFolder($imapFolder = $folder->getSetting('folder'))); + + // Update the shared folder (rename) + $folder->name = 'SharedFolder1 ©'. time(); + $folder->save(); + $newImapFolder = $folder->getSetting('folder'); + + $this->assertTrue(IMAP::updateSharedFolder($folder, ['folder' => $imapFolder])); + $this->assertTrue($imapFolder != $newImapFolder); + $this->assertTrue(IMAP::verifySharedFolder($newImapFolder)); + + // Delete the shared folder + $this->assertTrue(IMAP::deleteSharedFolder($folder)); + $this->assertFalse(IMAP::verifySharedFolder($newImapFolder)); + } + /** * Test verifying IMAP account existence (existing account) * diff --git a/src/tests/Feature/Console/User/StatusTest.php b/src/tests/Feature/Console/User/StatusTest.php --- a/src/tests/Feature/Console/User/StatusTest.php +++ b/src/tests/Feature/Console/User/StatusTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature\Console\User; +use App\User; use Illuminate\Support\Facades\Queue; use Tests\TestCase; @@ -41,14 +42,20 @@ $this->assertSame(1, $code); $this->assertSame("User not found.", $output); + $user = $this->getTestUser( + 'user@force-delete.com', + ['status' => User::STATUS_NEW | User::STATUS_ACTIVE | User::STATUS_IMAP_READY | User::STATUS_LDAP_READY] + ); + // Existing user - $code = \Artisan::call("user:status john@kolab.org"); + $code = \Artisan::call("user:status {$user->email}"); $output = trim(\Artisan::output()); $this->assertSame(0, $code); $this->assertSame("Status (51): active (2), ldapReady (16), imapReady (32)", $output); - $user = $this->getTestUser('user@force-delete.com'); + $user->status = User::STATUS_ACTIVE; + $user->save(); $user->delete(); // Deleted user @@ -56,6 +63,6 @@ $output = trim(\Artisan::output()); $this->assertSame(0, $code); - $this->assertSame("Status (3): active (2), deleted (8)", $output); + $this->assertSame("Status (2): active (2), deleted (8)", $output); } } diff --git a/src/tests/Feature/Controller/AuthTest.php b/src/tests/Feature/Controller/AuthTest.php --- a/src/tests/Feature/Controller/AuthTest.php +++ b/src/tests/Feature/Controller/AuthTest.php @@ -69,7 +69,7 @@ */ public function testInfo(): void { - $user = $this->getTestUser('UsersControllerTest1@userscontroller.com'); + $user = $this->getTestUser('UsersControllerTest1@userscontroller.com', ['status' => User::STATUS_NEW]); $domain = $this->getTestDomain('userscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_PUBLIC, @@ -85,7 +85,7 @@ $this->assertEquals($user->id, $json['id']); $this->assertEquals($user->email, $json['email']); - $this->assertEquals(User::STATUS_NEW | User::STATUS_ACTIVE, $json['status']); + $this->assertEquals(User::STATUS_NEW, $json['status']); $this->assertTrue(is_array($json['statusInfo'])); $this->assertTrue(is_array($json['settings'])); $this->assertTrue(!isset($json['access_token'])); diff --git a/src/tests/Feature/Controller/ResourcesTest.php b/src/tests/Feature/Controller/ResourcesTest.php --- a/src/tests/Feature/Controller/ResourcesTest.php +++ b/src/tests/Feature/Controller/ResourcesTest.php @@ -339,25 +339,29 @@ $resource->status |= Resource::STATUS_IMAP_READY; $resource->save(); - // Now "reboot" the process and get the resource status + // Now "reboot" the process + Queue::fake(); $response = $this->actingAs($john)->get("/api/v4/resources/{$resource->id}/status?refresh=1"); $response->assertStatus(200); $json = $response->json(); - $this->assertTrue($json['isLdapReady']); + $this->assertFalse($json['isLdapReady']); $this->assertTrue($json['isImapReady']); - $this->assertTrue($json['isReady']); + $this->assertFalse($json['isReady']); $this->assertCount(7, $json['process']); $this->assertSame('resource-ldap-ready', $json['process'][1]['label']); - $this->assertSame(true, $json['process'][1]['state']); + $this->assertSame(false, $json['process'][1]['state']); $this->assertSame('resource-imap-ready', $json['process'][2]['label']); $this->assertSame(true, $json['process'][2]['state']); $this->assertSame('success', $json['status']); - $this->assertSame('Setup process finished successfully.', $json['message']); - $this->assertSame('done', $json['processState']); + $this->assertSame('Setup process has been pushed. Please wait.', $json['message']); + $this->assertSame('waiting', $json['processState']); + + Queue::assertPushed(\App\Jobs\Resource\CreateJob::class, 1); // Test a case when a domain is not ready + Queue::fake(); $domain->status ^= \App\Domain::STATUS_CONFIRMED; $domain->save(); @@ -366,13 +370,16 @@ $json = $response->json(); - $this->assertTrue($json['isLdapReady']); - $this->assertTrue($json['isReady']); + $this->assertFalse($json['isLdapReady']); + $this->assertFalse($json['isReady']); $this->assertCount(7, $json['process']); $this->assertSame('resource-ldap-ready', $json['process'][1]['label']); - $this->assertSame(true, $json['process'][1]['state']); + $this->assertSame(false, $json['process'][1]['state']); $this->assertSame('success', $json['status']); - $this->assertSame('Setup process finished successfully.', $json['message']); + $this->assertSame('Setup process has been pushed. Please wait.', $json['message']); + $this->assertSame('waiting', $json['processState']); + + Queue::assertPushed(\App\Jobs\Resource\CreateJob::class, 1); } /** diff --git a/src/tests/Feature/Controller/SharedFoldersTest.php b/src/tests/Feature/Controller/SharedFoldersTest.php --- a/src/tests/Feature/Controller/SharedFoldersTest.php +++ b/src/tests/Feature/Controller/SharedFoldersTest.php @@ -346,25 +346,29 @@ $folder->status |= SharedFolder::STATUS_IMAP_READY; $folder->save(); - // Now "reboot" the process and get the folder status + // Now "reboot" the process + Queue::fake(); $response = $this->actingAs($john)->get("/api/v4/shared-folders/{$folder->id}/status?refresh=1"); $response->assertStatus(200); $json = $response->json(); - $this->assertTrue($json['isLdapReady']); + $this->assertFalse($json['isLdapReady']); $this->assertTrue($json['isImapReady']); - $this->assertTrue($json['isReady']); + $this->assertFalse($json['isReady']); $this->assertCount(7, $json['process']); $this->assertSame('shared-folder-ldap-ready', $json['process'][1]['label']); - $this->assertSame(true, $json['process'][1]['state']); + $this->assertSame(false, $json['process'][1]['state']); $this->assertSame('shared-folder-imap-ready', $json['process'][2]['label']); $this->assertSame(true, $json['process'][2]['state']); $this->assertSame('success', $json['status']); - $this->assertSame('Setup process finished successfully.', $json['message']); - $this->assertSame('done', $json['processState']); + $this->assertSame('Setup process has been pushed. Please wait.', $json['message']); + $this->assertSame('waiting', $json['processState']); + + Queue::assertPushed(\App\Jobs\SharedFolder\CreateJob::class, 1); // Test a case when a domain is not ready + Queue::fake(); $domain->status ^= \App\Domain::STATUS_CONFIRMED; $domain->save(); @@ -373,13 +377,16 @@ $json = $response->json(); - $this->assertTrue($json['isLdapReady']); - $this->assertTrue($json['isReady']); + $this->assertFalse($json['isLdapReady']); + $this->assertFalse($json['isReady']); $this->assertCount(7, $json['process']); $this->assertSame('shared-folder-ldap-ready', $json['process'][1]['label']); - $this->assertSame(true, $json['process'][1]['state']); + $this->assertSame(false, $json['process'][1]['state']); $this->assertSame('success', $json['status']); - $this->assertSame('Setup process finished successfully.', $json['message']); + $this->assertSame('Setup process has been pushed. Please wait.', $json['message']); + $this->assertSame('waiting', $json['processState']); + + Queue::assertPushed(\App\Jobs\SharedFolder\CreateJob::class, 1); } /** diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php --- a/src/tests/Feature/Controller/UsersTest.php +++ b/src/tests/Feature/Controller/UsersTest.php @@ -493,29 +493,8 @@ $domain->status |= Domain::STATUS_CONFIRMED; $domain->save(); - // Now "reboot" the process and verify the user in imap synchronously - $response = $this->actingAs($john)->get("/api/v4/users/{$john->id}/status?refresh=1"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertTrue($json['isImapReady']); - $this->assertTrue($json['isReady']); - $this->assertCount(7, $json['process']); - $this->assertSame('user-imap-ready', $json['process'][2]['label']); - $this->assertSame(true, $json['process'][2]['state']); - $this->assertSame('success', $json['status']); - $this->assertSame('Setup process finished successfully.', $json['message']); - - Queue::size(1); - - // Test case for when the verify job is dispatched to the worker - $john->refresh(); - $john->status ^= User::STATUS_IMAP_READY; - $john->save(); - - \config(['imap.admin_password' => null]); - + // Now "reboot" the process + Queue::fake(); $response = $this->actingAs($john)->get("/api/v4/users/{$john->id}/status?refresh=1"); $response->assertStatus(200); @@ -523,11 +502,13 @@ $this->assertFalse($json['isImapReady']); $this->assertFalse($json['isReady']); + $this->assertCount(7, $json['process']); + $this->assertSame('user-imap-ready', $json['process'][2]['label']); + $this->assertSame(false, $json['process'][2]['state']); $this->assertSame('success', $json['status']); - $this->assertSame('waiting', $json['processState']); $this->assertSame('Setup process has been pushed. Please wait.', $json['message']); - Queue::assertPushed(\App\Jobs\User\VerifyJob::class, 1); + Queue::assertPushed(\App\Jobs\User\CreateJob::class, 1); } /** diff --git a/src/tests/Feature/Jobs/Resource/CreateTest.php b/src/tests/Feature/Jobs/Resource/CreateTest.php --- a/src/tests/Feature/Jobs/Resource/CreateTest.php +++ b/src/tests/Feature/Jobs/Resource/CreateTest.php @@ -29,6 +29,7 @@ * Test job handle * * @group ldap + * @group imap */ public function testHandle(): void { @@ -41,24 +42,27 @@ $this->assertTrue($job->isReleased()); $this->assertFalse($job->hasFailed()); - $resource = $this->getTestResource('resource-test@' . \config('app.domain')); + $resource = $this->getTestResource( + 'resource-test@' . \config('app.domain'), + ['status' => Resource::STATUS_NEW] + ); $this->assertFalse($resource->isLdapReady()); + $this->assertFalse($resource->isImapReady()); + $this->assertFalse($resource->isActive()); // Test resource creation $job = new \App\Jobs\Resource\CreateJob($resource->id); $job->handle(); - $this->assertTrue($resource->fresh()->isLdapReady()); + $resource->refresh(); + $this->assertFalse($job->hasFailed()); + $this->assertTrue($resource->isLdapReady()); + $this->assertTrue($resource->isImapReady()); + $this->assertTrue($resource->isActive()); // Test job failures - $job = new \App\Jobs\Resource\CreateJob($resource->id); - $job->handle(); - - $this->assertTrue($job->hasFailed()); - $this->assertSame("Resource {$resource->id} is already marked as ldap-ready.", $job->failureMessage); - $resource->status |= Resource::STATUS_DELETED; $resource->save(); @@ -79,5 +83,6 @@ $this->assertSame("Resource {$resource->id} is actually deleted.", $job->failureMessage); // TODO: Test failures on domain sanity checks + // TODO: Test partial execution, i.e. only IMAP or only LDAP } } diff --git a/src/tests/Feature/Jobs/Resource/DeleteTest.php b/src/tests/Feature/Jobs/Resource/DeleteTest.php --- a/src/tests/Feature/Jobs/Resource/DeleteTest.php +++ b/src/tests/Feature/Jobs/Resource/DeleteTest.php @@ -29,6 +29,7 @@ * Test job handle * * @group ldap + * @group imap */ public function testHandle(): void { @@ -52,11 +53,10 @@ $resource->refresh(); $this->assertTrue($resource->isLdapReady()); + $this->assertTrue($resource->isImapReady()); + $this->assertFalse($resource->isDeleted()); // Test successful deletion - $resource->status |= Resource::STATUS_IMAP_READY; - $resource->save(); - $job = new \App\Jobs\Resource\DeleteJob($resource->id); $job->handle(); diff --git a/src/tests/Feature/Jobs/Resource/UpdateTest.php b/src/tests/Feature/Jobs/Resource/UpdateTest.php --- a/src/tests/Feature/Jobs/Resource/UpdateTest.php +++ b/src/tests/Feature/Jobs/Resource/UpdateTest.php @@ -33,6 +33,7 @@ * Test job handle * * @group ldap + * @group imap */ public function testHandle(): void { @@ -45,12 +46,16 @@ $this->assertTrue($job->hasFailed()); $this->assertSame("Resource 123 could not be found in the database.", $job->failureMessage); - $resource = $this->getTestResource('resource-test@' . \config('app.domain')); + $resource = $this->getTestResource( + 'resource-test@' . \config('app.domain'), + ['status' => Resource::STATUS_NEW] + ); // Create the resource in LDAP $job = new \App\Jobs\Resource\CreateJob($resource->id); $job->handle(); + // Run the update with some new config $resource->setConfig(['invitation_policy' => 'accept']); $job = new \App\Jobs\Resource\UpdateJob($resource->id); @@ -60,18 +65,11 @@ $this->assertSame('ACT_ACCEPT', $ldap_resource['kolabinvitationpolicy']); + // TODO: Assert IMAP change worked + // Test that the job is being deleted if the resource is not ldap ready or is deleted $resource->refresh(); - $resource->status = Resource::STATUS_NEW | Resource::STATUS_ACTIVE; - $resource->save(); - - $job = new \App\Jobs\Resource\UpdateJob($resource->id); - $job->handle(); - - $this->assertTrue($job->isDeleted()); - - $resource->status = Resource::STATUS_NEW | Resource::STATUS_ACTIVE - | Resource::STATUS_LDAP_READY | Resource::STATUS_DELETED; + $resource->status |= Resource::STATUS_DELETED; $resource->save(); $job = new \App\Jobs\Resource\UpdateJob($resource->id); diff --git a/src/tests/Feature/Jobs/SharedFolder/CreateTest.php b/src/tests/Feature/Jobs/SharedFolder/CreateTest.php --- a/src/tests/Feature/Jobs/SharedFolder/CreateTest.php +++ b/src/tests/Feature/Jobs/SharedFolder/CreateTest.php @@ -29,6 +29,7 @@ * Test job handle * * @group ldap + * @group imap */ public function testHandle(): void { @@ -41,24 +42,27 @@ $this->assertTrue($job->isReleased()); $this->assertFalse($job->hasFailed()); - $folder = $this->getTestSharedFolder('folder-test@' . \config('app.domain')); + $folder = $this->getTestSharedFolder( + 'folder-test@' . \config('app.domain'), + ['status' => SharedFolder::STATUS_NEW] + ); $this->assertFalse($folder->isLdapReady()); + $this->assertFalse($folder->isImapReady()); + $this->assertFalse($folder->isActive()); // Test shared folder creation $job = new \App\Jobs\SharedFolder\CreateJob($folder->id); $job->handle(); - $this->assertTrue($folder->fresh()->isLdapReady()); + $folder->refresh(); + $this->assertFalse($job->hasFailed()); + $this->assertTrue($folder->isLdapReady()); + $this->assertTrue($folder->isImapReady()); + $this->assertTrue($folder->isActive()); // Test job failures - $job = new \App\Jobs\SharedFolder\CreateJob($folder->id); - $job->handle(); - - $this->assertTrue($job->hasFailed()); - $this->assertSame("Shared folder {$folder->id} is already marked as ldap-ready.", $job->failureMessage); - $folder->status |= SharedFolder::STATUS_DELETED; $folder->save(); @@ -79,5 +83,6 @@ $this->assertSame("Shared folder {$folder->id} is actually deleted.", $job->failureMessage); // TODO: Test failures on domain sanity checks + // TODO: Test partial execution, i.e. only IMAP or only LDAP } } diff --git a/src/tests/Feature/Jobs/SharedFolder/DeleteTest.php b/src/tests/Feature/Jobs/SharedFolder/DeleteTest.php --- a/src/tests/Feature/Jobs/SharedFolder/DeleteTest.php +++ b/src/tests/Feature/Jobs/SharedFolder/DeleteTest.php @@ -29,6 +29,7 @@ * Test job handle * * @group ldap + * @group imap */ public function testHandle(): void { @@ -52,11 +53,10 @@ $folder->refresh(); $this->assertTrue($folder->isLdapReady()); + $this->assertTrue($folder->isImapReady()); + $this->assertFalse($folder->isDeleted()); // Test successful deletion - $folder->status |= SharedFolder::STATUS_IMAP_READY; - $folder->save(); - $job = new \App\Jobs\SharedFolder\DeleteJob($folder->id); $job->handle(); diff --git a/src/tests/Feature/Jobs/SharedFolder/UpdateTest.php b/src/tests/Feature/Jobs/SharedFolder/UpdateTest.php --- a/src/tests/Feature/Jobs/SharedFolder/UpdateTest.php +++ b/src/tests/Feature/Jobs/SharedFolder/UpdateTest.php @@ -33,6 +33,7 @@ * Test job handle * * @group ldap + * @group imap */ public function testHandle(): void { @@ -45,29 +46,28 @@ $this->assertTrue($job->hasFailed()); $this->assertSame("Shared folder 123 could not be found in the database.", $job->failureMessage); - $folder = $this->getTestSharedFolder('folder-test@' . \config('app.domain')); + $folder = $this->getTestSharedFolder( + 'folder-test@' . \config('app.domain'), + ['status' => SharedFolder::STATUS_NEW] + ); // Create the folder in LDAP $job = new \App\Jobs\SharedFolder\CreateJob($folder->id); $job->handle(); - $job = new \App\Jobs\SharedFolder\UpdateJob($folder->id); - $job->handle(); - - $this->assertTrue(is_array(LDAP::getSharedFolder($folder->email))); - - // Test that the job is being deleted if the folder is not ldap ready or is deleted $folder->refresh(); - $folder->status = SharedFolder::STATUS_NEW | SharedFolder::STATUS_ACTIVE; - $folder->save(); + $this->assertTrue($folder->isLdapReady()); + $this->assertTrue($folder->isImapReady()); + + // Run the update job $job = new \App\Jobs\SharedFolder\UpdateJob($folder->id); $job->handle(); - $this->assertTrue($job->isDeleted()); + // TODO: Assert that it worked on both LDAP and IMAP side - $folder->status = SharedFolder::STATUS_NEW | SharedFolder::STATUS_ACTIVE - | SharedFolder::STATUS_LDAP_READY | SharedFolder::STATUS_DELETED; + // Test handling deleted folder + $folder->status |= SharedFolder::STATUS_DELETED; $folder->save(); $job = new \App\Jobs\SharedFolder\UpdateJob($folder->id); diff --git a/src/tests/Feature/Jobs/User/CreateTest.php b/src/tests/Feature/Jobs/User/CreateTest.php --- a/src/tests/Feature/Jobs/User/CreateTest.php +++ b/src/tests/Feature/Jobs/User/CreateTest.php @@ -3,6 +3,7 @@ namespace Tests\Feature\Jobs\User; use App\User; +use Illuminate\Support\Facades\Queue; use Tests\TestCase; class CreateTest extends TestCase @@ -28,26 +29,28 @@ * Test job handle * * @group ldap + * @group imap */ public function testHandle(): void { - $user = $this->getTestUser('new-job-user@' . \config('app.domain')); + Queue::fake(); + $user = $this->getTestUser('new-job-user@' . \config('app.domain'), ['status' => User::STATUS_NEW]); $this->assertFalse($user->isLdapReady()); + $this->assertFalse($user->isImapReady()); + $this->assertFalse($user->isActive()); $job = new \App\Jobs\User\CreateJob($user->id); $job->handle(); - $this->assertTrue($user->fresh()->isLdapReady()); - $this->assertFalse($job->hasFailed()); + $user->refresh(); - // Test job failures - $job = new \App\Jobs\User\CreateJob($user->id); - $job->handle(); - - $this->assertTrue($job->hasFailed()); - $this->assertSame("User {$user->id} is already marked as ldap-ready.", $job->failureMessage); + $this->assertTrue($user->isLdapReady()); + $this->assertTrue($user->isImapReady()); + $this->assertTrue($user->isActive()); + $this->assertFalse($job->hasFailed()); + // Test job failure (user deleted) $user->status |= User::STATUS_DELETED; $user->save(); @@ -57,6 +60,7 @@ $this->assertTrue($job->hasFailed()); $this->assertSame("User {$user->id} is marked as deleted.", $job->failureMessage); + // Test job failure (user removed) $user->status ^= User::STATUS_DELETED; $user->save(); $user->delete(); @@ -67,12 +71,14 @@ $this->assertTrue($job->hasFailed()); $this->assertSame("User {$user->id} is actually deleted.", $job->failureMessage); - // TODO: Test failures on domain sanity checks - + // Test job failure (user unknown) $job = new \App\Jobs\User\CreateJob(123); $job->handle(); $this->assertTrue($job->isReleased()); $this->assertFalse($job->hasFailed()); + + // TODO: Test failures on domain sanity checks + // TODO: Test partial execution, i.e. only IMAP or only LDAP } } diff --git a/src/tests/Feature/Jobs/User/CreateTest.php b/src/tests/Feature/Jobs/User/DeleteTest.php copy from src/tests/Feature/Jobs/User/CreateTest.php copy to src/tests/Feature/Jobs/User/DeleteTest.php --- a/src/tests/Feature/Jobs/User/CreateTest.php +++ b/src/tests/Feature/Jobs/User/DeleteTest.php @@ -3,9 +3,10 @@ namespace Tests\Feature\Jobs\User; use App\User; +use Illuminate\Support\Facades\Queue; use Tests\TestCase; -class CreateTest extends TestCase +class DeleteTest extends TestCase { /** * {@inheritDoc} @@ -17,6 +18,9 @@ $this->deleteTestUser('new-job-user@' . \config('app.domain')); } + /** + * {@inheritDoc} + */ public function tearDown(): void { $this->deleteTestUser('new-job-user@' . \config('app.domain')); @@ -28,51 +32,50 @@ * Test job handle * * @group ldap + * @group imap */ public function testHandle(): void { - $user = $this->getTestUser('new-job-user@' . \config('app.domain')); + Queue::fake(); - $this->assertFalse($user->isLdapReady()); + $user = $this->getTestUser('new-job-user@' . \config('app.domain')); + // Create the user in LDAP+IMAP $job = new \App\Jobs\User\CreateJob($user->id); $job->handle(); - $this->assertTrue($user->fresh()->isLdapReady()); - $this->assertFalse($job->hasFailed()); + $user->refresh(); - // Test job failures - $job = new \App\Jobs\User\CreateJob($user->id); - $job->handle(); - - $this->assertTrue($job->hasFailed()); - $this->assertSame("User {$user->id} is already marked as ldap-ready.", $job->failureMessage); + $this->assertTrue($user->isLdapReady()); + $this->assertTrue($user->isImapReady()); + $this->assertFalse($user->isDeleted()); + // Test job failure (user already deleted) $user->status |= User::STATUS_DELETED; $user->save(); - $job = new \App\Jobs\User\CreateJob($user->id); + $job = new \App\Jobs\User\DeleteJob($user->id); $job->handle(); $this->assertTrue($job->hasFailed()); - $this->assertSame("User {$user->id} is marked as deleted.", $job->failureMessage); + $this->assertSame("User {$user->id} is already marked as deleted.", $job->failureMessage); + // Test success delete from LDAP and IMAP $user->status ^= User::STATUS_DELETED; $user->save(); - $user->delete(); - $job = new \App\Jobs\User\CreateJob($user->id); + $this->assertFalse($user->isDeleted()); + + $job = new \App\Jobs\User\DeleteJob($user->id); $job->handle(); - $this->assertTrue($job->hasFailed()); - $this->assertSame("User {$user->id} is actually deleted.", $job->failureMessage); - - // TODO: Test failures on domain sanity checks + $user->refresh(); - $job = new \App\Jobs\User\CreateJob(123); - $job->handle(); - - $this->assertTrue($job->isReleased()); $this->assertFalse($job->hasFailed()); + $this->assertFalse($user->isLdapReady()); + $this->assertFalse($user->isImapReady()); + $this->assertTrue($user->isDeleted()); + + // TODO: Test partial execution, i.e. only IMAP or only LDAP } } diff --git a/src/tests/Feature/Jobs/User/UpdateTest.php b/src/tests/Feature/Jobs/User/UpdateTest.php --- a/src/tests/Feature/Jobs/User/UpdateTest.php +++ b/src/tests/Feature/Jobs/User/UpdateTest.php @@ -32,6 +32,7 @@ * Test job handle * * @group ldap + * @group imap */ public function testHandle(): void { @@ -90,5 +91,7 @@ $this->assertTrue($job->hasFailed()); $this->assertSame("User 123 could not be found in the database.", $job->failureMessage); + + // TODO: Test IMAP, e.g. quota change } } diff --git a/src/tests/Feature/ResourceTest.php b/src/tests/Feature/ResourceTest.php --- a/src/tests/Feature/ResourceTest.php +++ b/src/tests/Feature/ResourceTest.php @@ -113,7 +113,7 @@ $this->assertMatchesRegularExpression('/^resource-[0-9]{1,20}@kolabnow\.com$/', $resource->email); $this->assertSame('Reśo', $resource->name); $this->assertTrue($resource->isNew()); - $this->assertTrue($resource->isActive()); + $this->assertFalse($resource->isActive()); $this->assertFalse($resource->isDeleted()); $this->assertFalse($resource->isLdapReady()); $this->assertFalse($resource->isImapReady()); @@ -133,13 +133,6 @@ && $resourceId === $resource->id; } ); - - Queue::assertPushedWithChain( - \App\Jobs\Resource\CreateJob::class, - [ - \App\Jobs\Resource\VerifyJob::class, - ] - ); } /** diff --git a/src/tests/Feature/SharedFolderTest.php b/src/tests/Feature/SharedFolderTest.php --- a/src/tests/Feature/SharedFolderTest.php +++ b/src/tests/Feature/SharedFolderTest.php @@ -177,7 +177,7 @@ $this->assertMatchesRegularExpression('/^mail-[0-9]{1,20}@kolabnow\.com$/', $folder->email); $this->assertSame('Reśo', $folder->name); $this->assertTrue($folder->isNew()); - $this->assertTrue($folder->isActive()); + $this->assertFalse($folder->isActive()); $this->assertFalse($folder->isDeleted()); $this->assertFalse($folder->isLdapReady()); $this->assertFalse($folder->isImapReady()); @@ -197,13 +197,6 @@ && $folderId === $folder->id; } ); - - Queue::assertPushedWithChain( - \App\Jobs\SharedFolder\CreateJob::class, - [ - \App\Jobs\SharedFolder\VerifyJob::class, - ] - ); } /** diff --git a/src/tests/Feature/UserTest.php b/src/tests/Feature/UserTest.php --- a/src/tests/Feature/UserTest.php +++ b/src/tests/Feature/UserTest.php @@ -338,28 +338,6 @@ } ); - Queue::assertPushedWithChain( - \App\Jobs\User\CreateJob::class, - [ - \App\Jobs\User\VerifyJob::class, - ] - ); -/* - FIXME: Looks like we can't really do detailed assertions on chained jobs - Another thing to consider is if we maybe should run these jobs - independently (not chained) and make sure there's no race-condition - in status update - - Queue::assertPushed(\App\Jobs\User\VerifyJob::class, 1); - Queue::assertPushed(\App\Jobs\User\VerifyJob::class, function ($job) use ($user) { - $userEmail = TestCase::getObjectProperty($job, 'userEmail'); - $userId = TestCase::getObjectProperty($job, 'userId'); - - return $userEmail === $user->email - && $userId === $user->id; - }); -*/ - // Test invoking KeyCreateJob $this->deleteTestUser("user-test@$domain"); @@ -1149,12 +1127,6 @@ return $userA->id === TestCase::getObjectProperty($job, 'userId'); } ); - Queue::assertPushedWithChain( - \App\Jobs\User\CreateJob::class, - [ - \App\Jobs\User\VerifyJob::class, - ] - ); } /** diff --git a/src/tests/TestCaseTrait.php b/src/tests/TestCaseTrait.php --- a/src/tests/TestCaseTrait.php +++ b/src/tests/TestCaseTrait.php @@ -563,6 +563,12 @@ // Note: we do not want to use user restore here User::where('id', $user->id)->forceDelete(); $user = User::create(['email' => $email] + $attrib); + } else { + foreach ($attrib as $key => $val) { + $user->{$key} = $val; + } + + $user->save(); } return $user;