Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117951336
D3785.1775490432.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
74 KB
Referenced Files
None
Subscribers
None
D3785.1775490432.diff
View Options
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;
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Mon, Apr 6, 3:47 PM (4 h, 44 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18836015
Default Alt Text
D3785.1775490432.diff (74 KB)
Attached To
Mode
D3785: [DRAFT] Direct IMAP backend
Attached
Detach File
Event Timeline