diff options
37 files changed, 1500 insertions, 1401 deletions
diff --git a/appinfo/info.xml b/appinfo/info.xml index 7f40a03e4..2e952b004 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -12,7 +12,7 @@ - **🙈 We’re not reinventing the wheel!** Based on the great [Horde](https://horde.org) libraries. - **📬 Want to host your own mail server?** We don’t have to reimplement this as you could set up [Mail-in-a-Box](https://mailinabox.email)! ]]></description> - <version>1.10.0-alpha.3</version> + <version>1.10.0-alpha.4</version> <licence>agpl</licence> <author>Christoph Wurst</author> <author>Greta Doçi</author> @@ -43,8 +43,6 @@ <step>OCA\Mail\Migration\FixCollectedAddresses</step> <step>OCA\Mail\Migration\FixBackgroundJobs</step> <step>OCA\Mail\Migration\MakeItineraryExtractorExecutable</step> - <step>OCA\Mail\Migration\MigrateProvisioningConfig</step> - <step>OCA\Mail\Migration\AddSieveToProvisioningConfig</step> <step>OCA\Mail\Migration\ProvisionAccounts</step> </post-migration> </repair-steps> diff --git a/appinfo/routes.php b/appinfo/routes.php index 4f80ed450..7a1d14649 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -230,13 +230,28 @@ return [ 'verb' => 'GET' ], [ - 'name' => 'settings#provisioning', + 'name' => 'settings#index', + 'url' => '/api/settings/provisioning', + 'verb' => 'GET' + ], + [ + 'name' => 'settings#createProvisioning', 'url' => '/api/settings/provisioning', 'verb' => 'POST' ], [ + 'name' => 'settings#updateProvisioning', + 'url' => '/api/settings/provisioning/{id}', + 'verb' => 'POST' + ], + [ + 'name' => 'settings#provision', + 'url' => '/api/settings/provisioning/all', + 'verb' => 'PUT' + ], + [ 'name' => 'settings#deprovision', - 'url' => '/api/settings/provisioning', + 'url' => '/api/settings/provisioning/{id}', 'verb' => 'DELETE' ], [ diff --git a/lib/BackgroundJob/SyncJob.php b/lib/BackgroundJob/SyncJob.php index 8d10c549f..b74e47c14 100644 --- a/lib/BackgroundJob/SyncJob.php +++ b/lib/BackgroundJob/SyncJob.php @@ -82,7 +82,7 @@ class SyncJob extends TimedJob { } $dbAccount = $account->getMailAccount(); - if ($dbAccount->getProvisioned() && $dbAccount->getInboundPassword() === null) { + if (!is_null($dbAccount->getProvisioningId()) && $dbAccount->getInboundPassword() === null) { $this->logger->info("Ignoring cron sync for provisioned account that has no password set yet"); return; } diff --git a/lib/BackgroundJob/TrainImportanceClassifierJob.php b/lib/BackgroundJob/TrainImportanceClassifierJob.php index ca46305d0..abaa11366 100644 --- a/lib/BackgroundJob/TrainImportanceClassifierJob.php +++ b/lib/BackgroundJob/TrainImportanceClassifierJob.php @@ -75,7 +75,7 @@ class TrainImportanceClassifierJob extends TimedJob { } $dbAccount = $account->getMailAccount(); - if ($dbAccount->getProvisioned() && $dbAccount->getInboundPassword() === null) { + if (!is_null($dbAccount->getProvisioningId()) && $dbAccount->getInboundPassword() === null) { $this->logger->info("Ignoring cron training for provisioned account that has no password set yet"); return; } diff --git a/lib/Command/DeleteAccount.php b/lib/Command/DeleteAccount.php index c32b41fa0..2a7018b3e 100644 --- a/lib/Command/DeleteAccount.php +++ b/lib/Command/DeleteAccount.php @@ -70,7 +70,7 @@ class DeleteAccount extends Command { } $output->writeLn("<info>Found account with email: " . $account->getEmail() . "</info>"); - if ($account->getMailAccount()->getProvisioned() === true) { + if (!is_null($account->getMailAccount()->getProvisioningId())) { $output->writeLn('<error>This is a provisioned account which can not be deleted from CLI. Use the Provisioning UI instead.</error>'); return 2; } diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index d65f04f03..2081ea5bb 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -26,10 +26,13 @@ declare(strict_types=1); namespace OCA\Mail\Controller; use OCA\Mail\AppInfo\Application; +use OCA\Mail\Exception\ValidationException; +use OCA\Mail\Http\JsonResponse as HttpJsonResponse; use OCA\Mail\Service\Provisioning\Manager as ProvisioningManager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\JSONResponse; use OCP\IRequest; +use function array_merge; class SettingsController extends Controller { @@ -42,42 +45,45 @@ class SettingsController extends Controller { $this->provisioningManager = $provisioningManager; } - public function provisioning(string $emailTemplate, - string $imapUser, - string $imapHost, - int $imapPort, - string $imapSslMode, - string $smtpUser, - string $smtpHost, - int $smtpPort, - string $smtpSslMode, - bool $sieveEnabled, - string $sieveUser, - string $sieveHost, - int $sievePort, - string $sieveSslMode): JSONResponse { - $this->provisioningManager->newProvisioning( - $emailTemplate, - $imapUser, - $imapHost, - $imapPort, - $imapSslMode, - $smtpUser, - $smtpHost, - $smtpPort, - $smtpSslMode, - $sieveEnabled, - $sieveUser, - $sieveHost, - $sievePort, - $sieveSslMode - ); + public function index(): JSONResponse { + $provisionings = $this->provisioningManager->getConfigs(); + return new JSONResponse($provisionings); + } + + public function provision() : JSONResponse { + $count = $this->provisioningManager->provision(); + return new JSONResponse(['count' => $count]); + } + + public function createProvisioning(array $data): JSONResponse { + try { + $this->provisioningManager->newProvisioning($data); + } catch (ValidationException $e) { + return HttpJsonResponse::fail([$e->getFields()]); + } return new JSONResponse([]); } - public function deprovision(): JSONResponse { - $this->provisioningManager->deprovision(); + public function updateProvisioning(int $id, array $data): JSONResponse { + try { + $this->provisioningManager->updateProvisioning(array_merge( + $data, + ['id' => $id] + )); + } catch (ValidationException $e) { + return HttpJsonResponse::fail([$e->getFields()]); + } + + return new JSONResponse([]); + } + + public function deprovision(int $id): JSONResponse { + $provisioning = $this->provisioningManager->getConfigById($id); + + if ($provisioning !== null) { + $this->provisioningManager->deprovision($provisioning); + } return new JSONResponse([]); } diff --git a/lib/Db/MailAccount.php b/lib/Db/MailAccount.php index a41b0528c..933eb5001 100644 --- a/lib/Db/MailAccount.php +++ b/lib/Db/MailAccount.php @@ -63,8 +63,8 @@ use OCP\AppFramework\Db\Entity; * @method void setLastMailboxSync(int $time) * @method string getEditorMode() * @method void setEditorMode(string $editorMode) - * @method bool|null getProvisioned() - * @method void setProvisioned(bool $provisioned) + * @method int|null getProvisioningId() + * @method void setProvisioningId(int $provisioningId) * @method int getOrder() * @method void setOrder(int $order) * @method bool|null getShowSubscribedOnly() @@ -109,7 +109,6 @@ class MailAccount extends Entity { protected $signature; protected $lastMailboxSync; protected $editorMode; - protected $provisioned; protected $order; protected $showSubscribedOnly; protected $personalNamespace; @@ -138,6 +137,10 @@ class MailAccount extends Entity { /** @var bool */ protected $signatureAboveQuote = false; + /** @var int|null */ + protected $provisioningId; + + /** * @param array $params */ @@ -193,7 +196,7 @@ class MailAccount extends Entity { $this->addType('inboundPort', 'integer'); $this->addType('outboundPort', 'integer'); $this->addType('lastMailboxSync', 'integer'); - $this->addType('provisioned', 'boolean'); + $this->addType('provisioning_id', 'integer'); $this->addType('order', 'integer'); $this->addType('showSubscribedOnly', 'boolean'); $this->addType('personalNamespace', 'string'); @@ -221,7 +224,7 @@ class MailAccount extends Entity { 'imapSslMode' => $this->getInboundSslMode(), 'signature' => $this->getSignature(), 'editorMode' => $this->getEditorMode(), - 'provisioned' => ($this->getProvisioned() === true), + 'provisioningId' => $this->getProvisioningId(), 'showSubscribedOnly' => ($this->getShowSubscribedOnly() === true), 'personalNamespace' => $this->getPersonalNamespace(), 'draftsMailboxId' => $this->getDraftsMailboxId(), diff --git a/lib/Db/MailAccountMapper.php b/lib/Db/MailAccountMapper.php index 1dccbb674..8d6586fcd 100644 --- a/lib/Db/MailAccountMapper.php +++ b/lib/Db/MailAccountMapper.php @@ -110,7 +110,7 @@ class MailAccountMapper extends QBMapper { ->from($this->getTableName()) ->where( $qb->expr()->eq('user_id', $qb->createNamedParameter($user->getUID())), - $qb->expr()->eq('provisioned', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL)) + $qb->expr()->isNotNull('provisioning_id') ); return $this->findEntity($query); @@ -131,11 +131,23 @@ class MailAccountMapper extends QBMapper { return $this->update($account); } - public function deleteProvisionedAccounts(): void { + public function deleteProvisionedAccounts(int $provisioningId): void { $qb = $this->db->getQueryBuilder(); $delete = $qb->delete($this->getTableName()) - ->where($qb->expr()->eq('provisioned', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))); + ->where($qb->expr()->eq('provisioning_id', $qb->createNamedParameter($provisioningId, IQueryBuilder::PARAM_BOOL))); + + $delete->execute(); + } + + public function deleteProvisionedAccountsByUid(string $uid): void { + $qb = $this->db->getQueryBuilder(); + + $delete = $qb->delete($this->getTableName()) + ->where( + $qb->expr()->eq('user_id', $qb->createNamedParameter($uid)), + $qb->expr()->isNotNull('provisioning_id') + ); $delete->execute(); } diff --git a/lib/Db/Provisioning.php b/lib/Db/Provisioning.php new file mode 100644 index 000000000..97d3c7a81 --- /dev/null +++ b/lib/Db/Provisioning.php @@ -0,0 +1,169 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2021 Anna Larch <anna@nextcloud.com> + * + * @author 2021 Anna Larch <anna@nextcloud.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\Mail\Db; + +use JsonSerializable; +use OCP\AppFramework\Db\Entity; +use OCP\IUser; + +/** + * @method string getProvisioningDomain() + * @method void setProvisioningDomain(string $provisioningDomain) + * @method string getEmailTemplate() + * @method void setEmailTemplate(string $emailTemplate) + * @method string getImapUser() + * @method void setImapUser(string $imapUser) + * @method string getImapHost() + * @method void setImapHost(string $imapHost) + * @method int getImapPort() + * @method void setImapPort(int $imapPort) + * @method string getImapSslMode() + * @method void setImapSslMode(string $imapSslMode) + * @method string getSmtpUser() + * @method void setSmtpUser(string $smtpUser) + * @method string getSmtpHost() + * @method void setSmtpHost(string $smtpHost) + * @method int getSmtpPort() + * @method void setSmtpPort(int $smtpPort) + * @method string getSmtpSslMode() + * @method void setSmtpSslMode(string $smtpSslMode) + * @method bool|null getSieveEnabled() + * @method void setSieveEnabled(bool $sieveEnabled) + * @method string|null getSieveHost() + * @method void setSieveHost(?string $sieveHost) + * @method int|null getSievePort() + * @method void setSievePort(?int $sievePort) + * @method string|null getSieveSslMode() + * @method void setSieveSslMode(?string $sieveSslMode) + * @method string|null getSieveUser() + * @method void setSieveUser(?string $sieveUser) + */ +class Provisioning extends Entity implements JsonSerializable { + public const WILDCARD = '*'; + + protected $provisioningDomain; + protected $emailTemplate; + protected $imapUser; + protected $imapHost; + protected $imapPort; + protected $imapSslMode; + protected $smtpUser; + protected $smtpHost; + protected $smtpPort; + protected $smtpSslMode; + protected $sieveEnabled; + protected $sieveUser; + protected $sieveHost; + protected $sievePort; + protected $sieveSslMode; + + public function __construct() { + $this->addType('imapPort', 'integer'); + $this->addType('smtpPort', 'integer'); + $this->addType('sieveEnabled', 'boolean'); + $this->addType('sievePort', 'integer'); + } + /** + * @return array + */ + public function jsonSerialize() { + return [ + 'id' => $this->getId(), + 'provisioningDomain' => $this->getProvisioningDomain(), + 'emailTemplate' => $this->getEmailTemplate(), + 'imapUser' => $this->getImapUser(), + 'imapHost' => $this->getImapHost(), + 'imapPort' => $this->getImapPort(), + 'imapSslMode' => $this->getImapSslMode(), + 'smtpUser' => $this->getSmtpUser(), + 'smtpHost' => $this->getSmtpHost(), + 'smtpPort' => $this->getSmtpPort(), + 'smtpSslMode' => $this->getSmtpSslMode(), + 'sieveEnabled' => $this->getSieveEnabled(), + 'sieveUser' => $this->getSieveUser(), + 'sieveHost' => $this->getSieveHost(), + 'sievePort' => $this->getSievePort(), + 'sieveSslMode' => $this->getSieveSslMode(), + ]; + } + + /** + * @return string + */ + public function buildImapUser(IUser $user) { + if (!is_null($this->getImapUser())) { + return $this->buildUserEmail($this->getImapUser(), $user); + } + return $this->buildEmail($user); + } + + /** + * @param IUser $user + * @return string + */ + public function buildEmail(IUser $user) { + return $this->buildUserEmail($this->getEmailTemplate(), $user); + } + + /** + * Replace %USERID% and %EMAIL% to allow special configurations + * + * @param string $original + * @param IUser $user + * @return string + */ + private function buildUserEmail(string $original, IUser $user) { + if ($user->getUID() !== null) { + $original = str_replace('%USERID%', $user->getUID(), $original); + } + if ($user->getEMailAddress() !== null) { + $original = str_replace('%EMAIL%', $user->getEMailAddress(), $original); + } + return $original; + } + + /** + * @param IUser $user + * @return string + */ + public function buildSmtpUser(IUser $user) { + if (!is_null($this->getSmtpUser())) { + return $this->buildUserEmail($this->getSmtpUser(), $user); + } + return $this->buildEmail($user); + } + + /** + * @param IUser $user + * @return string + */ + public function buildSieveUser(IUser $user) { + if (!is_null($this->getSieveUser())) { + return $this->buildUserEmail($this->getSieveUser(), $user); + } + return $this->buildEmail($user); + } +} diff --git a/lib/Db/ProvisioningMapper.php b/lib/Db/ProvisioningMapper.php new file mode 100644 index 000000000..09eec6281 --- /dev/null +++ b/lib/Db/ProvisioningMapper.php @@ -0,0 +1,145 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2021 Anna Larch <anna@nextcloud.com> + * + * @author 2021 Anna Larch <anna@nextcloud.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\Mail\Db; + +use OCA\Mail\Exception\ValidationException; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use Psr\Log\LoggerInterface; + +/** + * @template-extends QBMapper<Provisioning> + */ +class ProvisioningMapper extends QBMapper { + + + /** @var LoggerInterface */ + private $logger; + + public function __construct(IDBConnection $db, LoggerInterface $logger) { + parent::__construct($db, 'mail_provisionings'); + $this->logger = $logger; + } + + /** + * Should return the wildcard domain last! + * + * @return Provisioning[] + */ + public function getAll() : array { + $qb = $this->db->getQueryBuilder(); + $qb = $qb->select('*') + ->from($this->getTableName()) + ->orderBy('provisioning_domain', 'desc'); + try { + return $this->findEntities($qb); + } catch (DoesNotExistException $e) { + $this->logger->error('No provisioning configs available'); + return []; + } + } + + /** + * @param array $data + * @return Provisioning + * @throws ValidationException + */ + public function validate(array $data) : Provisioning { + $exception = new ValidationException(); + + if (!isset($data['provisioningDomain']) || $data['provisioningDomain'] === '') { + $exception->setField('provisioningDomain', false); + } + if (!isset($data['emailTemplate']) || $data['emailTemplate'] === '') { + $exception->setField('emailTemplate', false); + } + if (!isset($data['imapUser']) || $data['imapUser'] === '') { + $exception->setField('imapUser', false); + } + if (!isset($data['imapHost']) || $data['imapHost'] === '') { + $exception->setField('imapHost', false); + } + if (!isset($data['imapPort']) || (int)$data['imapPort'] === 0) { + $exception->setField('imapHost', false); + } + if (!isset($data['imapSslMode']) || $data['imapSslMode'] === '') { + $exception->setField('imapSslMode', false); + } + if (!isset($data['smtpUser']) || $data['smtpUser'] === '') { + $exception->setField('smtpUser', false); + } + if (!isset($data['smtpHost']) || $data['smtpHost'] === '') { + $exception->setField('smtpHost', false); + } + if (!isset($data['smtpPort']) || (int)$data['smtpPort'] === 0) { + $exception->setField('smtpPort', false); + } + if (!isset($data['smtpSslMode']) || $data['smtpSslMode'] === '') { + $exception->setField('smtpSslMode', false); + } + + if (!empty($exception->getFields())) { + throw $exception; + } + + + $provisioning = new Provisioning(); + $provisioning->setId($data['id'] ?? null); + $provisioning->setProvisioningDomain($data['provisioningDomain']); + $provisioning->setEmailTemplate($data['emailTemplate']); + $provisioning->setImapUser($data['imapUser']); + $provisioning->setImapHost($data['imapHost']); + $provisioning->setImapPort((int)$data['imapPort']); + $provisioning->setImapSslMode($data['imapSslMode']); + $provisioning->setSmtpUser($data['smtpUser']); + $provisioning->setSmtpHost($data['smtpHost']); + $provisioning->setSmtpPort((int)$data['smtpPort']); + $provisioning->setSmtpSslMode($data['smtpSslMode']); + + $provisioning->setSieveEnabled((bool)$data['sieveEnabled']); + $provisioning->setSieveHost($data['sieveHost'] ?? ''); + $provisioning->setSieveUser($data['sieveUser'] ?? ''); + $provisioning->setSievePort($data['sievePort'] ?? null); + $provisioning->setSieveSslMode($data['sieveSslMode'] ?? ''); + + return $provisioning; + } + + public function get(int $id): ?Provisioning { + $qb = $this->db->getQueryBuilder(); + $qb = $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id), IQueryBuilder::PARAM_INT)); + try { + return $this->findEntity($qb); + } catch (DoesNotExistException $e) { + $this->logger->error('Could not find entry with ID #' . $id); + return null; + } + } +} diff --git a/tests/Unit/Service/Provisioning/TestConfig.php b/lib/Exception/ValidationException.php index 9ebf3157a..217ca635a 100644 --- a/tests/Unit/Service/Provisioning/TestConfig.php +++ b/lib/Exception/ValidationException.php @@ -3,9 +3,9 @@ declare(strict_types=1); /** - * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> + * @copyright 2021 Anna Larch <anna@nextcloud.com> * - * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at> + * @author 2021 Anna Larch <anna@nextcloud.com> * * @license GNU AGPL version 3 or any later version * @@ -21,29 +21,37 @@ declare(strict_types=1); * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. + * */ -namespace OCA\mail\tests\Unit\Service\Provisioning; +namespace OCA\Mail\Exception; + +use Exception; +use OCP\AppFramework\Http; -use OCA\Mail\Service\Provisioning\Config; +class ValidationException extends Exception { + /** @var bool[] */ + private $fields; -class TestConfig extends Config { public function __construct() { - parent::__construct([ - 'email' => '%USERID%@domain.com', - 'imapUser' => '%USERID%@domain.com', - 'imapHost' => 'mx.domain.com', - 'imapPort' => 993, - 'imapSslMode' => 'ssl', - 'smtpUser' => '%USERID%@domain.com', - 'smtpHost' => 'mx.domain.com', - 'smtpPort' => 567, - 'smtpSslMode' => 'tls', - 'sieveEnabled' => false, - 'sieveHost' => '', - 'sievePort' => 4190, - 'sieveUser' => '', - 'sieveSslMode' => 'tls', - ]); + $this->fields = []; + } + + public function getHttpCode(): int { + return Http::STATUS_BAD_REQUEST; + } + + public function getFields(): array { + return $this->fields; + } + + public function setField(string $key, bool $validates): void { + $this->fields[$key] = $validates; + } + + public function setFields(array $fields): void { + foreach ($fields as $key => $validates) { + $this->setField($key, $validates); + } } } diff --git a/lib/Http/Middleware/ProvisioningMiddleware.php b/lib/Http/Middleware/ProvisioningMiddleware.php index 47ff642fe..3acc1c8d9 100644 --- a/lib/Http/Middleware/ProvisioningMiddleware.php +++ b/lib/Http/Middleware/ProvisioningMiddleware.php @@ -64,12 +64,12 @@ class ProvisioningMiddleware extends Middleware { // Nothing to update return; } - $config = $this->provisioningManager->getConfig(); - if ($config === null || !$config->isActive()) { - return; + $configs = $this->provisioningManager->getConfigs(); + if (empty($configs)) { + return null; } try { - $this->provisioningManager->provisionSingleUser($config, $user); + $this->provisioningManager->provisionSingleUser($configs, $user); $this->provisioningManager->updatePassword( $user, $this->credentialStore->getLoginCredentials()->getPassword() diff --git a/lib/Migration/AddSieveToProvisioningConfig.php b/lib/Migration/AddSieveToProvisioningConfig.php deleted file mode 100644 index e871ff1cc..000000000 --- a/lib/Migration/AddSieveToProvisioningConfig.php +++ /dev/null @@ -1,85 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * - * Mail - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Mail\Migration; - -use OCA\Mail\Service\Provisioning\Config as ProvisioningConfig; -use OCA\Mail\Service\Provisioning\ConfigMapper as ProvisioningConfigMapper; -use OCP\IConfig; -use OCP\Migration\IOutput; -use OCP\Migration\IRepairStep; - -class AddSieveToProvisioningConfig implements IRepairStep { - - /** @var IConfig */ - private $config; - - /** @var ProvisioningConfigMapper */ - private $configMapper; - - public function __construct(IConfig $config, ProvisioningConfigMapper $configMapper) { - $this->config = $config; - $this->configMapper = $configMapper; - } - - public function getName(): string { - return 'Add sieve defaults to provisioning config'; - } - - public function run(IOutput $output) { - if (!$this->shouldRun()) { - return; - } - - $config = $this->configMapper->load(); - if ($config === null) { - return; - } - - $reflectionClass = new \ReflectionClass(ProvisioningConfig::class); - $reflectionProperty = $reflectionClass->getProperty('data'); - - $reflectionProperty->setAccessible(true); - $data = $reflectionProperty->getValue($config); - - if (!isset($data['sieveEnabled'])) { - $data = array_merge($data, [ - 'sieveEnabled' => false, - 'sieveHost' => '', - 'sievePort' => 4190, - 'sieveUser' => '', - 'sieveSslMode' => 'tls', - ]); - } - - $reflectionProperty->setValue($config, $data); - $this->configMapper->save($config); - - $output->info('added sieve defaults to provisioning config'); - } - - protected function shouldRun(): bool { - $appVersion = $this->config->getAppValue('mail', 'installed_version', '0.0.0'); - return version_compare($appVersion, '1.9.0', '<'); - } -} diff --git a/lib/Migration/MigrateProvisioningConfig.php b/lib/Migration/MigrateProvisioningConfig.php deleted file mode 100644 index 12b90c8f8..000000000 --- a/lib/Migration/MigrateProvisioningConfig.php +++ /dev/null @@ -1,65 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * Mail - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Mail\Migration; - -use OCA\Mail\Service\Provisioning\Manager as ProvisioningManager; -use OCP\IConfig; -use OCP\Migration\IOutput; -use OCP\Migration\IRepairStep; - -class MigrateProvisioningConfig implements IRepairStep { - - /** @var ProvisioningManager */ - private $provisioningManager; - - /** @var IConfig */ - private $config; - - public function __construct(ProvisioningManager $provisioningManager, - IConfig $config) { - $this->provisioningManager = $provisioningManager; - $this->config = $config; - } - - public function getName(): string { - return 'Migrate Mail provisioning config from config.php to the database'; - } - - public function run(IOutput $output) { - $fromConfigRaw = $this->config->getSystemValue('app.mail.accounts.default'); - if ($fromConfigRaw === '') { - $output->info("No old config found"); - return; - } - - if ($this->provisioningManager->getConfig() !== null) { - $output->info("Mail provisioning config already set, ignoring old config"); - return; - } - - $this->provisioningManager->importConfig($fromConfigRaw); - $this->config->deleteSystemValue('app.mail.accounts.default'); - $output->info("Config migrated. Accounts not updated yet"); - } -} diff --git a/lib/Migration/ProvisionAccounts.php b/lib/Migration/ProvisionAccounts.php index 8487492ca..268e1a99d 100644 --- a/lib/Migration/ProvisionAccounts.php +++ b/lib/Migration/ProvisionAccounts.php @@ -43,17 +43,7 @@ class ProvisionAccounts implements IRepairStep { } public function run(IOutput $output) { - $config = $this->provisioningManager->getConfig(); - if ($config === null) { - $output->info("No Mail provisioning config set"); - return; - } - if (!$config->isActive()) { - $output->info("Mail provisioning is disabled"); - return; - } - - $cnt = $this->provisioningManager->provision($config); + $cnt = $this->provisioningManager->provision(); $output->info("$cnt accounts provisioned"); } } diff --git a/lib/Migration/Version1100Date20210419080523.php b/lib/Migration/Version1100Date20210419080523.php new file mode 100644 index 000000000..5c84fee7b --- /dev/null +++ b/lib/Migration/Version1100Date20210419080523.php @@ -0,0 +1,196 @@ +<?php + +declare(strict_types=1); + +namespace OCA\Mail\Migration; + +use Closure; +use JsonException; +use OCA\Mail\AppInfo\Application; +use OCP\DB\ISchemaWrapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; +use Psr\Log\LoggerInterface; + +class Version1100Date20210419080523 extends SimpleMigrationStep { + + /** @var IConfig */ + protected $config; + + /** @var IDBConnection */ + protected $connection; + + /** @var LoggerInterface */ + protected $logger; + + public function __construct(IConfig $config, IDBConnection $connection, LoggerInterface $logger) { + $this->config = $config; + $this->connection = $connection; + $this->logger = $logger; + } + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + $schema = $schemaClosure(); + + if (!$schema->hasTable('mail_provisionings')) { + $provisioningTable = $schema->createTable('mail_provisionings'); + $provisioningTable->addColumn('id', 'integer', [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 4, + ]); + $provisioningTable->addColumn('provisioning_domain', 'string', [ + 'notnull' => true, + 'length' => 63, + 'default' => '', + ]); + $provisioningTable->addColumn('email_template', 'string', [ + 'notnull' => true, + 'length' => 255, + 'default' => '', + ]); + $provisioningTable->addColumn('imap_user', 'string', [ + 'notnull' => true, + 'length' => 128, + 'default' => '', + ]); + $provisioningTable->addColumn('imap_host', 'string', [ + 'notnull' => true, + 'length' => 255, + 'default' => '', + ]); + $provisioningTable->addColumn('imap_port', 'smallint', [ + 'notnull' => true, + 'unsigned' => true, + ]); + $provisioningTable->addColumn('imap_ssl_mode', 'string', [ + 'notnull' => true, + 'length' => 64, + 'default' => '', + ]); + $provisioningTable->addColumn('smtp_user', 'string', [ + 'notnull' => true, + 'length' => 128, + 'default' => '', + ]); + $provisioningTable->addColumn('smtp_host', 'string', [ + 'notnull' => true, + 'length' => 255, + 'default' => '', + ]); + $provisioningTable->addColumn('smtp_port', 'smallint', [ + 'notnull' => true, + 'unsigned' => true, + ]); + $provisioningTable->addColumn('smtp_ssl_mode', 'string', [ + 'notnull' => true, + 'length' => 64, + 'default' => '', + ]); + $provisioningTable->addColumn('sieve_enabled', 'boolean', [ + 'notnull' => false, + 'default' => false, + ]); + $provisioningTable->addColumn('sieve_user', 'string', [ + 'notnull' => false, + 'length' => 128, + ]); + $provisioningTable->addColumn('sieve_host', 'string', [ + 'notnull' => false, + 'length' => 128, + ]); + $provisioningTable->addColumn('sieve_port', 'smallint', [ + 'notnull' => false, + 'unsigned' => true, + ]); + $provisioningTable->addColumn('sieve_ssl_mode', 'string', [ + 'notnull' => false, + 'length' => 64, + ]); + $provisioningTable->setPrimaryKey(['id']); + $provisioningTable->addUniqueIndex( + [ + 'provisioning_domain', + ], + 'mail_provsng_dm_idx' + ); + } + + $accountsTable = $schema->getTable('mail_accounts'); + $accountsTable->addColumn('provisioning_id', 'integer', [ + 'length' => 4, + 'notnull' => false, + ]); + + return $schema; + } + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + // Fetch old config + $raw = $this->config->getAppValue( + Application::APP_ID, + 'provisioning_settings' + ); + if ($raw === '') { + // Not config set yet + return; + } + + try { + $conf = json_decode($raw, true, 10, JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + $this->logger->error('Json decode for old provisioning config failed: ' . $e->getMessage() . ' - building manual config', [ + 'exception' => $e, + ]); + // build config manually + $conf = []; + } + + // create first entry + $insertQb = $this->connection->getQueryBuilder(); + $insertQb->insert('mail_provisionings'); + $insertQb->setValue('provisioning_domain', $insertQb->createNamedParameter('*')); // wildcard domain for this provisioning + $insertQb->setValue('email_template', $insertQb->createNamedParameter($conf['email'] ?? '%USERID%@domain.com')); + $insertQb->setValue('imap_user', $insertQb->createNamedParameter($conf['imapUser'] ?? '%USERID%@domain.com')); + $insertQb->setValue('imap_host', $insertQb->createNamedParameter($conf['imapHost'] ?? 'imap.domain.com')); + $insertQb->setValue('imap_port', $insertQb->createNamedParameter($conf['imapPort'] ?? 993, IQueryBuilder::PARAM_INT)); + $insertQb->setValue('imap_ssl_mode', $insertQb->createNamedParameter($conf['imapSslMode'] ?? 'ssl')); + $insertQb->setValue('smtp_user', $insertQb->createNamedParameter($conf['smtpUser'] ?? '%USERID%@domain.com')); + $insertQb->setValue('smtp_host', $insertQb->createNamedParameter($conf['smtpHost'] ?? 'smtp.domain.com')); + $insertQb->setValue('smtp_port', $insertQb->createNamedParameter($conf['smtpPort'] ?? 587, IQueryBuilder::PARAM_INT)); + $insertQb->setValue('smtp_ssl_mode', $insertQb->createNamedParameter($conf['smtpSslMode'] ?? 'tls')); + $insertQb->setValue('sieve_enabled', $insertQb->createNamedParameter((bool)($conf['sieveEnabled'] ?? false),IQueryBuilder::PARAM_BOOL)); + $insertQb->setValue('sieve_user', $insertQb->createNamedParameter($conf['sieveUser'] ?? '')); + $insertQb->setValue('sieve_host', $insertQb->createNamedParameter($conf['sieveHost'] ?? '')); + $insertQb->setValue('sieve_port', $insertQb->createNamedParameter($conf['sievePort'] ?? 4190, IQueryBuilder::PARAM_INT)); + $insertQb->setValue('sieve_ssl_mode', $insertQb->createNamedParameter($conf['sieveSslMode'] ?? '')); + $insertQb->execute(); + $id = $insertQb->getLastInsertId(); + + // set wildcard provisioning config for all provisioned accounts so we don't use state + $updateQb = $this->connection->getQueryBuilder(); + $updateQb = $updateQb->update('mail_accounts') + ->set('provisioning_id', $updateQb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) + ->where($updateQb->expr()->eq('provisioned', $updateQb->createNamedParameter('true', IQueryBuilder::PARAM_BOOL))); + $updateQb->execute(); + + $this->config->deleteAppValue( + Application::APP_ID, + 'provisioning_settings' + ); + } +} diff --git a/lib/Migration/Version1100Date20210419121734.php b/lib/Migration/Version1100Date20210419121734.php new file mode 100644 index 000000000..80bf9509d --- /dev/null +++ b/lib/Migration/Version1100Date20210419121734.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); + +namespace OCA\Mail\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version1100Date20210419121734 extends SimpleMigrationStep { + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $accounts = $schema->getTable('mail_accounts'); + $accounts->dropColumn('provisioned'); + + return $schema; + } +} diff --git a/lib/Model/IMAPMessage.php b/lib/Model/IMAPMessage.php index 42d82834c..e1393d8ac 100644 --- a/lib/Model/IMAPMessage.php +++ b/lib/Model/IMAPMessage.php @@ -766,7 +766,7 @@ class IMAPMessage implements IMessage, JsonSerializable { $t = []; foreach ($tags as $keyword) { // Map the old $important to $label1 until we have caught them all - if ($keyword === '$important' || $keyword === 'important') { + if ($keyword === '$important') { $keyword = Tag::LABEL_IMPORTANT; } $tag = new Tag(); diff --git a/lib/Service/Provisioning/Config.php b/lib/Service/Provisioning/Config.php deleted file mode 100644 index 74b8cda8d..000000000 --- a/lib/Service/Provisioning/Config.php +++ /dev/null @@ -1,187 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * Mail - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Mail\Service\Provisioning; - -use JsonSerializable; -use OCP\IUser; - -class Config implements JsonSerializable { - private const VERSION = 1; - - /** @var mixed[] */ - private $data; - - /** - * @param array $data - */ - public function __construct(array $data) { - $this->data = $data; - } - - /** - * @param IUser $user - * @return string - */ - public function buildEmail(IUser $user) { - return $this->buildUserEmail($this->data['email'], $user); - } - - /** - * @return string - */ - public function getImapHost() { - return $this->data['imapHost']; - } - - /** - * @return int - */ - public function getImapPort(): int { - return (int) $this->data['imapPort']; - } - - /** - * @return string - */ - public function buildImapUser(IUser $user) { - if (isset($this->data['imapUser'])) { - return $this->buildUserEmail($this->data['imapUser'], $user); - } - return $this->buildEmail($user); - } - - /** - * @return string - */ - public function getImapSslMode() { - return $this->data['imapSslMode']; - } - - /** - * @return string - */ - public function getSmtpHost() { - return $this->data['smtpHost']; - } - - /** - * @return int - */ - public function getSmtpPort(): int { - return (int) $this->data['smtpPort']; - } - - /** - * @param IUser $user - * @return string - */ - public function buildSmtpUser(IUser $user) { - if (isset($this->data['smtpUser'])) { - return $this->buildUserEmail($this->data['smtpUser'], $user); - } - return $this->buildEmail($user); - } - - /** - * @return string - */ - public function getSmtpSslMode() { - return $this->data['smtpSslMode']; - } - - /** - * @return boolean - */ - public function getSieveEnabled(): bool { - return (bool)$this->data['sieveEnabled']; - } - - /** - * @return string - */ - public function getSieveHost() { - return $this->data['sieveHost']; - } - - /** - * @return int - */ - public function getSievePort(): int { - return (int)$this->data['sievePort']; - } - - /** - * @param IUser $user - * @return string - */ - public function buildSieveUser(IUser $user) { - if (isset($this->data['sieveUser'])) { - return $this->buildUserEmail($this->data['sieveUser'], $user); - } - return $this->buildEmail($user); - } - - /** - * @return string - */ - public function getSieveSslMode() { - return $this->data['sieveSslMode']; - } - - /** - * Replace %USERID% and %EMAIL% to allow special configurations - * - * @param string $original - * @param IUser $user - * @return string - */ - private function buildUserEmail(string $original, IUser $user) { - if ($user->getUID() !== null) { - $original = str_replace('%USERID%', $user->getUID(), $original); - } - if ($user->getEMailAddress() !== null) { - $original = str_replace('%EMAIL%', $user->getEMailAddress(), $original); - } - return $original; - } - - public function setActive(bool $state): self { - $this->data['active'] = $state; - return $this; - } - - public function isActive(): bool { - return (bool) ($this->data['active'] ?? true); - } - - public function jsonSerialize() { - return array_merge( - [ - 'active' => false, - 'version' => self::VERSION, - ], - $this->data - ); - } -} diff --git a/lib/Service/Provisioning/ConfigMapper.php b/lib/Service/Provisioning/ConfigMapper.php deleted file mode 100644 index 2f9b2de31..000000000 --- a/lib/Service/Provisioning/ConfigMapper.php +++ /dev/null @@ -1,62 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -namespace OCA\Mail\Service\Provisioning; - -use OCA\Mail\AppInfo\Application; -use OCP\IConfig; - -class ConfigMapper { - private const CONFIG_KEY = 'provisioning_settings'; - - /** @var IConfig */ - private $config; - - public function __construct(IConfig $config) { - $this->config = $config; - } - - public function load(): ?Config { - $raw = $this->config->getAppValue( - Application::APP_ID, - self::CONFIG_KEY - ); - if ($raw === '') { - // Not config set yet - return null; - } - return new Config(json_decode($raw, true)); - } - - public function save(Config $config): Config { - $this->config->setAppValue( - Application::APP_ID, - self::CONFIG_KEY, - json_encode($config) - ); - - return $config; - } -} diff --git a/lib/Service/Provisioning/Manager.php b/lib/Service/Provisioning/Manager.php index 742c87f13..8cc9c2e06 100644 --- a/lib/Service/Provisioning/Manager.php +++ b/lib/Service/Provisioning/Manager.php @@ -23,9 +23,14 @@ declare(strict_types=1); namespace OCA\Mail\Service\Provisioning; +use Horde_Mail_Rfc822_Address; use OCA\Mail\Db\MailAccount; use OCA\Mail\Db\MailAccountMapper; +use OCA\Mail\Db\Provisioning; +use OCA\Mail\Db\ProvisioningMapper; +use OCA\Mail\Exception\ValidationException; use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\IUser; use OCP\IUserManager; use OCP\Security\ICrypto; @@ -36,8 +41,8 @@ class Manager { /** @var IUserManager */ private $userManager; - /** @var ConfigMapper */ - private $configMapper; + /** @var ProvisioningMapper */ + private $provisioningMapper; /** @var MailAccountMapper */ private $mailAccountMapper; @@ -49,85 +54,106 @@ class Manager { private $logger; public function __construct(IUserManager $userManager, - ConfigMapper $configMapper, + ProvisioningMapper $provisioningMapper, MailAccountMapper $mailAccountMapper, ICrypto $crypto, LoggerInterface $logger) { $this->userManager = $userManager; - $this->configMapper = $configMapper; + $this->provisioningMapper = $provisioningMapper; $this->mailAccountMapper = $mailAccountMapper; $this->crypto = $crypto; $this->logger = $logger; } - public function getConfig(): ?Config { - return $this->configMapper->load(); + public function getConfigById(int $provisioningId): ?Provisioning { + return $this->provisioningMapper->get($provisioningId); } - public function provision(Config $config): int { + public function getConfigs(): array { + return $this->provisioningMapper->getAll(); + } + + public function provision(): int { $cnt = 0; - $this->userManager->callForAllUsers(function (IUser $user) use ($config, &$cnt) { - $this->provisionSingleUser($config, $user); - $cnt++; + $configs = $this->getConfigs(); + $this->userManager->callForAllUsers(function (IUser $user) use ($configs, &$cnt) { + if ($this->provisionSingleUser($configs, $user) === true) { + $cnt++; + } }); return $cnt; } - public function provisionSingleUser(Config $config, IUser $user): void { + /** + * @param Provisioning[] $provisionings + */ + public function provisionSingleUser(array $provisionings, IUser $user): bool { + $provisioning = $this->findMatchingConfig($provisionings, $user); + + if ($provisioning === null) { + return false; + } + try { + // TODO: match by UID only, catch multiple objects returned below and delete all those accounts $existing = $this->mailAccountMapper->findProvisionedAccount($user); $this->mailAccountMapper->update( - $this->updateAccount($user, $existing, $config) + $this->updateAccount($user, $existing, $provisioning) ); + return true; } catch (DoesNotExistException $e) { // Fine, then we create a new one $new = new MailAccount(); $new->setUserId($user->getUID()); - $new->setProvisioned(true); $this->mailAccountMapper->insert( - $this->updateAccount($user, $new, $config) + $this->updateAccount($user, $new, $provisioning) ); + return true; + } catch (MultipleObjectsReturnedException $e) { + // This is unlikely to happen but not impossible. + // Let's wipe any existing accounts and start fresh + $this->mailAccountMapper->deleteProvisionedAccountsByUid($user->getUID()); + + $new = new MailAccount(); + $new->setUserId($user->getUID()); + + $this->mailAccountMapper->insert( + $this->updateAccount($user, $new, $provisioning) + ); + return true; } + return false; } - public function newProvisioning(string $email, - string $imapUser, - string $imapHost, - int $imapPort, - string $imapSslMode, - string $smtpUser, - string $smtpHost, - int $smtpPort, - string $smtpSslMode, - bool $sieveEnabled, - string $sieveUser, - string $sieveHost, - int $sievePort, - string $sieveSslMode): void { - $config = $this->configMapper->save(new Config([ - 'active' => true, - 'email' => $email, - 'imapUser' => $imapUser, - 'imapHost' => $imapHost, - 'imapPort' => $imapPort, - 'imapSslMode' => $imapSslMode, - 'smtpUser' => $smtpUser, - 'smtpHost' => $smtpHost, - 'smtpPort' => $smtpPort, - 'smtpSslMode' => $smtpSslMode, - 'sieveEnabled' => $sieveEnabled, - 'sieveUser' => $sieveUser, - 'sieveHost' => $sieveHost, - 'sievePort' => $sievePort, - 'sieveSslMode' => $sieveSslMode, - ])); - - $this->provision($config); + public function newProvisioning(array $data): void { + try { + $provisioning = $this->provisioningMapper->validate( + $data + ); + } catch (ValidationException $e) { + throw $e; + } + $this->provisioningMapper->insert($provisioning); } - private function updateAccount(IUser $user, MailAccount $account, Config $config): MailAccount { + public function updateProvisioning(array $data): void { + try { + $provisioning = $this->provisioningMapper->validate( + $data + ); + } catch (ValidationException $e) { + throw $e; + } + + $this->provisioningMapper->update($provisioning); + } + + private function updateAccount(IUser $user, MailAccount $account, Provisioning $config): MailAccount { + // Set the ID to make sure it reflects when the account switches from one config to another + $account->setProvisioningId($config->getId()); + $account->setEmail($config->buildEmail($user)); $account->setName($user->getDisplayName()); $account->setInboundUser($config->buildImapUser($user)); @@ -155,26 +181,9 @@ class Manager { return $account; } - public function deprovision(): void { - $this->mailAccountMapper->deleteProvisionedAccounts(); - - $config = $this->configMapper->load(); - if ($config !== null) { - $config->setActive(false); - $this->configMapper->save($config); - } - } - - public function importConfig(array $data): Config { - if (!isset($data['imapUser'])) { - $data['imapUser'] = $data['email']; - } - if (!isset($data['smtpUser'])) { - $data['smtpUser'] = $data['email']; - } - $data['active'] = true; - - return $this->configMapper->save(new Config($data)); + public function deprovision(Provisioning $provisioning): void { + $this->mailAccountMapper->deleteProvisionedAccounts($provisioning->getId()); + $this->provisioningMapper->delete($provisioning); } public function updatePassword(IUser $user, string $password): void { @@ -198,4 +207,26 @@ class Manager { // Nothing to update } } + + /** + * @param Provisioning[] $provisionings + */ + private function findMatchingConfig(array $provisionings, IUser $user): ?Provisioning { + foreach ($provisionings as $provisioning) { + if ($provisioning->getProvisioningDomain() === Provisioning::WILDCARD) { + return $provisioning; + } + + $email = $user->getEMailAddress(); + if ($email === null) { + continue; + } + $rfc822Address = new Horde_Mail_Rfc822_Address($email); + if ($rfc822Address->matchDomain($provisioning->getProvisioningDomain())) { + return $provisioning; + } + } + + return null; + } } diff --git a/lib/Settings/AdminSettings.php b/lib/Settings/AdminSettings.php index 7e4c47c98..2a8d0fd4e 100644 --- a/lib/Settings/AdminSettings.php +++ b/lib/Settings/AdminSettings.php @@ -26,7 +26,6 @@ declare(strict_types=1); namespace OCA\Mail\Settings; use OCA\Mail\AppInfo\Application; -use OCA\Mail\Service\Provisioning\Config; use OCA\Mail\Service\Provisioning\Manager as ProvisioningManager; use OCP\AppFramework\Http\TemplateResponse; use OCP\IInitialStateService; @@ -50,23 +49,7 @@ class AdminSettings implements ISettings { $this->initialStateService->provideInitialState( Application::APP_ID, 'provisioning_settings', - $this->provisioningManager->getConfig() ?? new Config([ - 'active' => false, - 'email' => '%USERID%@domain.com', - 'imapUser' => '%USERID%@domain.com', - 'imapHost' => 'imap.domain.com', - 'imapPort' => 993, - 'imapSslMode' => 'ssl', - 'smtpUser' => '%USERID%@domain.com', - 'smtpHost' => 'smtp.domain.com', - 'smtpPort' => 587, - 'smtpSslMode' => 'tls', - 'sieveEnabled' => false, - 'sieveUser' => '%USERID%@domain.com', - 'sieveHost' => 'imap.domain.com', - 'sievePort' => 4190, - 'sieveSslMode' => 'tls', - ]) + $this->provisioningManager->getConfigs() ); return new TemplateResponse(Application::APP_ID, 'settings-admin'); diff --git a/src/components/settings/AdminSettings.vue b/src/components/settings/AdminSettings.vue index e93acd457..dd5641d79 100644 --- a/src/components/settings/AdminSettings.vue +++ b/src/components/settings/AdminSettings.vue @@ -20,34 +20,212 @@ --> <template> - <div id="mail-admin-settings" class="section"> - <h2>{{ t('mail', 'Mail app') }}</h2> - <h3>{{ t('mail', 'The mail app allows users to read mails on their IMAP accounts.') }}</h3> - <h3> + <SettingsSection + :title="t( 'mail', 'Mail app')" + :description="t( 'mail', 'The mail app allows users to read mails on their IMAP accounts.')"> + <p> {{ t( 'mail', 'Here you can find instance-wide settings. User specific settings are found in the app itself (bottom-left corner).' ) }} - </h3> - <ProvisioningSettings :settings="provisioningSettings" /> - </div> + </p> + <div class="ap-description"> + <h3>Account provisioning</h3> + <article> + <p> + {{ + t( + 'mail', + 'A provisioning configuration will provision all accounts with a matching email address.' + ) + }} + {{ + t( + 'mail', + 'Using the wildcard (*) in the provisioning domain field will create a configuration that applies to all users, provided they do not match another configuration.' + ) + }} + <br> + {{ + t( + 'mail', + 'The provisioning mechanism will prioritise specific domain configurations over the wildcard domain configuration.' + ) + }} + {{ + t( + 'mail', + 'Should a new matching configuration be found after the user was already provisioned with another configuration, the new configuration will take precedence and the user will be reprovisioned with the configuration.' + ) + }} + <br> + {{ + t( + 'mail', + 'There can only be one configuartion per domain, and only one wildcard domain configuration.' + ) + }} + <br> + {{ + t( + 'mail', + 'These settings can be used in conjuction with each other.' + ) + }} + <br> + {{ + t( + 'mail', + 'If you only want to provision one domain for all users, use the wildcard (*).' + ) + }} + <br> + {{ + t( + 'mail', + "This setting only makes most sense if you use the same user back-end for your organization's Nextcloud and mail server." + ) + }} + </p> + </article> + </div> + <h3>Provisioning Configurations</h3> + <p> + <button class="config-button icon-add" @click="addNew=true"> + {{ t('mail', 'Add new config') }} + </button> + <button class="config-button icon-settings" @click="provisionAll"> + {{ t('mail', 'Provision all accounts') }} + </button> + <ProvisioningSettings v-if="addNew" + :key="formKey" + :setting="preview" + :submit="saveNewSettings" + :delete-button="false" /> + <ProvisioningSettings v-for="setting in configs" + :id="setting.id" + :key="setting.id" + :setting="setting" + :submit="saveSettings" + :disable="deleteProvisioning" /> + </p> + </SettingsSection> </template> <script> +import logger from '../../logger' +import { showError, showSuccess } from '@nextcloud/dialogs' import ProvisioningSettings from './ProvisioningSettings' +import SettingsSection from '@nextcloud/vue/dist/Components/SettingsSection' +import { + disableProvisioning, + createProvisioningSettings, + updateProvisioningSettings, + provisionAll, +} from '../../service/SettingsService' export default { name: 'AdminSettings', components: { ProvisioningSettings, + SettingsSection, }, props: { provisioningSettings: { - type: Object, + type: Array, required: true, }, }, + data() { + return { + addNew: false, + formKey: Math.random(), + configs: this.provisioningSettings, + preview: { + provisioningDomain: '', + emailTemplate: '', + imapHost: 'mx.domain.com', + imapPort: 993, + imapUser: '%USERID%domain.com', + imapSslMode: 'ssl', + smtpHost: 'mx.domain.com', + smtpPort: 587, + smtpUser: '%USERID%domain.com', + smtpSslMode: 'tls', + previewData1: { + uid: 'user123', + email: '', + }, + previewData2: { + uid: 'user321', + email: 'user@domain.com', + }, + loading: false, + }, + } + }, + methods: { + async saveSettings(settings) { + try { + await updateProvisioningSettings(settings) + showSuccess(t('mail', 'Successfully updated config for "{domain}"', { domain: settings.provisioningDomain })) + } catch (error) { + showError(t('mail', 'Error saving config')) + logger.error('Could not save provisioning setting', { error }) + } + }, + async saveNewSettings(settings) { + try { + await createProvisioningSettings(settings) + this.configs.unshift(settings) + this.addNew = false + this.resetForm() + showSuccess(t('mail', 'Saved config for "{domain}"', { domain: settings.provisioningDomain })) + } catch (error) { + showError(t('mail', 'Could not save provisioning setting')) + logger.error('Could not save provisioning setting', { error }) + } + + }, + resetForm() { + this.formKey = Math.random() + }, + async provisionAll() { + try { + const count = await provisionAll() + showSuccess(t('mail', 'Successfully provisioned {count} accounts.', { count: count.count })) + + } catch (error) { + showError(t('mail', 'There was an error when provisioning accounts.')) + logger.error('Could not provision accounts', { error }) + } + }, + async deleteProvisioning(id) { + const deleted = this.configs.find(c => c.id === id) + try { + await disableProvisioning(id) + logger.info('deprovisioned successfully') + this.configs = this.configs.filter(c => c.id !== id) + showSuccess(t('mail', 'Sucessfully deleted and deprovisioned accounts for "{domain}"', { domain: deleted.provisioningDomain })) + } catch (error) { + showError(t('mail', 'Error when deleting and deprovisioning accounts for "{domain}"', { domain: deleted.provisioningDomain })) + logger.error('Could not delete provisioning config', { error }) + } + }, + }, } </script> +<style lang="scss" scoped> + .ap-description { + margin-bottom: 24px; + } + .config-button { + line-height: 24px; + padding-left: 48px; + padding-top: 6px; + padding-bottom: 6px; + background-position: 24px; + } +</style> diff --git a/src/components/settings/ProvisionPreview.vue b/src/components/settings/ProvisionPreview.vue index 8d562e404..1c75be12b 100644 --- a/src/components/settings/ProvisionPreview.vue +++ b/src/components/settings/ProvisionPreview.vue @@ -26,6 +26,8 @@ <span v-if="data.email">email={{ email }}</span> </b> <br> + {{ t('mail', 'Domain Match: {provisioningDomain}', {provisioningDomain}) }} + <br> {{ t('mail', 'Email: {email}', {email}) }}<br> {{ t('mail', 'IMAP: {user} on {host}:{port} ({ssl} encryption)', { @@ -73,6 +75,9 @@ export default { email() { return this.templates.email.replace('%USERID%', this.data.uid).replace('%EMAIL%', this.data.email) }, + provisioningDomain() { + return this.templates.provisioningDomain + }, imapHost() { return this.templates.imapHost }, diff --git a/src/components/settings/ProvisioningSettings.vue b/src/components/settings/ProvisioningSettings.vue index 5d6a14d07..9b963940d 100644 --- a/src/components/settings/ProvisioningSettings.vue +++ b/src/components/settings/ProvisioningSettings.vue @@ -20,45 +20,32 @@ --> <template> - <div> - <h3>Account provisioning</h3> - <p> - {{ - t( - 'mail', - 'You can configure a template for account settings, from which all users will get an account provisioned from.' - ) - }} - {{ - t( - 'mail', - "This setting only makes most sense if you use the same user back-end for your organization's Nextcloud and mail server." - ) - }} - </p> - <div> - <input id="mail-provision-toggle" - v-model="active" - type="checkbox" - class="checkbox"> - <label for="mail-provision-toggle"> - {{ t('mail', 'Provision an account for every user') }} - </label> - </div> - <div v-if="active" class="form-preview-row"> - <form @submit.prevent="submit"> + <div class="section"> + <h4>{{ t('mail','Configuration for "{provisioningDomain}"', {provisioningDomain}) }}</h4> + <div class="form-preview-row"> + <form :id="'prov-form-' + setting.id" @submit.prevent="submitForm"> <div class="settings-group"> <div class="group-title"> {{ t('mail', 'General') }} </div> <div class="group-inputs"> - <label for="mail-provision-email"> {{ t('mail', 'Email address') }}* </label> + <br> + <label :for="'mail-provision-domain' + setting.id"> {{ t('mail', 'Provisioning domain') }}* </label> + <br> + <input + :id="'mail-provision-domain' + setting.id" + v-model="provisioningDomain" + :disabled="loading" + name="provisioningDomain" + type="text"> + <br> + <label :for="'mail-provision-email' + setting.id"> {{ t('mail', 'Email address template') }}* </label> <br> <input - id="mail-provision-email" + :id="'mail-provision-email' + setting.id" v-model="emailTemplate" :disabled="loading" - name="email" + name="emailTemplate" type="text"> </div> </div> @@ -67,32 +54,32 @@ {{ t('mail', 'IMAP') }} </div> <div class="group-inputs"> - <label for="mail-provision-imap-user"> + <label :for="'mail-provision-imap-user' + setting.id"> {{ t('mail', 'User') }}* <br> <input - id="mail-provision-imap-user" + :id="'mail-provision-imap-user' + setting.id" v-model="imapUser" :disabled="loading" name="email" type="text"> </label> <div class="flex-row"> - <label for="mail-provision-imap-host"> + <label :for="'mail-provision-imap-host' + setting.id"> {{ t('mail', 'Host') }} <br> <input - id="mail-provision-imap-host" + :id="'mail-provision-imap-host' + setting.id" v-model="imapHost" :disabled="loading" name="email" type="text"> </label> - <label for="mail-provision-imap-port"> + <label :for="'mail-provision-imap-port' + setting.id"> {{ t('mail', 'Port') }} <br> <input - id="mail-provision-imap-port" + :id="'mail-provision-imap-port' + setting.id" v-model="imapPort" :disabled="loading" name="email" @@ -101,7 +88,7 @@ </div> <div class="flex-row"> <input - id="mail-provision-imap-user-none" + :id="'mail-provision-imap-user-none' + setting.id" v-model="imapSslMode" type="radio" name="man-imap-sec" @@ -109,10 +96,10 @@ value="none"> <label class="button" - for="mail-provision-imap-user-none" + :for="'mail-provision-imap-user-none' + setting.id" :class="{primary: imapSslMode === 'none'}">{{ t('mail', 'None') }}</label> <input - id="mail-provision-imap-user-ssl" + :id="'mail-provision-imap-user-ssl' + setting.id" v-model="imapSslMode" type="radio" name="man-imap-sec" @@ -120,10 +107,10 @@ value="ssl"> <label class="button" - for="mail-provision-imap-user-ssl" + :for="'mail-provision-imap-user-ssl' + setting.id" :class="{primary: imapSslMode === 'ssl'}">{{ t('mail', 'SSL/TLS') }}</label> <input - id="mail-provision-imap-user-tls" + :id="'mail-provision-imap-user-tls' + setting.id" v-model="imapSslMode" type="radio" name="man-imap-sec" @@ -131,7 +118,7 @@ value="tls"> <label class="button" - for="mail-provision-imap-user-tls" + :for="'mail-provision-imap-user-tls' + setting.id" :class="{primary: imapSslMode === 'tls'}">{{ t('mail', 'STARTTLS') }}</label> </div> </div> @@ -141,32 +128,32 @@ {{ t('mail', 'SMTP') }} </div> <div class="group-inputs"> - <label for="mail-provision-smtp-user"> + <label :for="'mail-provision-smtp-user' + setting.id"> {{ t('mail', 'User') }}* <br> <input - id="mail-provision-smtp-user" + :id="'mail-provision-smtp-user' + setting.id" v-model="smtpUser" :disabled="loading" name="email" type="text"> </label> <div class="flex-row"> - <label for="mail-provision-smtp-host"> + <label :for="'mail-provision-smtp-host' + setting.id"> {{ t('mail', 'Host') }} <br> <input - id="mail-provision-smtp-host" + :id="'mail-provision-smtp-host' + setting.id" v-model="smtpHost" :disabled="loading" name="email" type="text"> </label> - <label for="mail-provision-smtp-port"> + <label :for="'mail-provision-smtp-port' + setting.id"> {{ t('mail', 'Port') }} <br> <input - id="mail-provision-smtp-port" + :id="'mail-provision-smtp-port' + setting.id" v-model="smtpPort" :disabled="loading" name="email" @@ -175,7 +162,7 @@ </div> <div class="flex-row"> <input - id="mail-provision-smtp-user-none" + :id="'mail-provision-smtp-user-none' + setting.id" v-model="smtpSslMode" type="radio" name="man-smtp-sec" @@ -183,10 +170,10 @@ value="none"> <label class="button" - for="mail-provision-smtp-user-none" + :for="'mail-provision-smtp-user-none' + setting.id" :class="{primary: smtpSslMode === 'none'}">{{ t('mail', 'None') }}</label> <input - id="mail-provision-smtp-user-ssl" + :id="'mail-provision-smtp-user-ssl' + setting.id" v-model="smtpSslMode" type="radio" name="man-smtp-sec" @@ -194,10 +181,10 @@ value="ssl"> <label class="button" - for="mail-provision-smtp-user-ssl" + :for="'mail-provision-smtp-user-ssl' + setting.id" :class="{primary: smtpSslMode === 'ssl'}">{{ t('mail', 'SSL/TLS') }}</label> <input - id="mail-provision-smtp-user-tls" + :id="'mail-provision-smtp-user-tls' + setting.id" v-model="smtpSslMode" type="radio" name="man-smtp-sec" @@ -205,7 +192,7 @@ value="tls"> <label class="button" - for="mail-provision-smtp-user-tls" + :for="'mail-provision-smtp-user-tls' + setting.id" :class="{primary: smtpSslMode === 'tls'}">{{ t('mail', 'STARTTLS') }}</label> </div> </div> @@ -216,40 +203,41 @@ </div> <div class="group-inputs"> <div> - <input id="mail-provision-sieve-enabled" + <input + :id="'mail-provision-sieve-enabled' + setting.id" v-model="sieveEnabled" type="checkbox" class="checkbox"> - <label for="mail-provision-sieve-enabled"> + <label :for="'mail-provision-sieve-enabled' + setting.id"> {{ t('mail', 'Enable sieve integration') }} </label> </div> - <label for="mail-provision-sieve-user"> + <label :for="'mail-provision-sieve-user' + setting.id"> {{ t('mail', 'User') }}* <br> <input - id="mail-provision-sieve-user" + :id="'mail-provision-sieve-user' + setting.id" v-model="sieveUser" :disabled="loading" name="email" type="text"> </label> <div class="flex-row"> - <label for="mail-provision-sieve-host"> + <label :for="'mail-provision-sieve-host' + setting.id"> {{ t('mail', 'Host') }} <br> <input - id="mail-provision-sieve-host" + :id="'mail-provision-sieve-host' + setting.id" v-model="sieveHost" :disabled="loading" name="email" type="text"> </label> - <label for="mail-provision-sieve-port"> + <label :for="'mail-provision-sieve-port' + setting.id"> {{ t('mail', 'Port') }} <br> <input - id="mail-provision-sieve-port" + :id="'mail-provision-sieve-port' + setting.id" v-model="sievePort" :disabled="loading" name="email" @@ -258,7 +246,7 @@ </div> <div class="flex-row"> <input - id="mail-provision-sieve-user-none" + :id="'mail-provision-sieve-user-none' + setting.id" v-model="sieveSslMode" type="radio" name="man-sieve-sec" @@ -266,10 +254,10 @@ value="none"> <label class="button" - for="mail-provision-sieve-user-none" + :for="'mail-provision-sieve-user-none' + setting.id" :class="{primary: sieveSslMode === 'none'}">{{ t('mail', 'None') }}</label> <input - id="mail-provision-sieve-user-ssl" + :id="'mail-provision-sieve-user-ssl' + setting.id" v-model="sieveSslMode" type="radio" name="man-sieve-sec" @@ -277,10 +265,10 @@ value="ssl"> <label class="button" - for="mail-provision-sieve-user-ssl" + :for="'mail-provision-sieve-user-ssl' + setting.id" :class="{primary: sieveSslMode === 'ssl'}">{{ t('mail', 'SSL/TLS') }}</label> <input - id="mail-provision-sieve-user-tls" + :id="'mail-provision-sieve-user-tls' + setting.id" v-model="sieveSslMode" type="radio" name="man-sieve-sec" @@ -288,7 +276,7 @@ value="tls"> <label class="button" - for="mail-provision-sieve-user-tls" + :for="'mail-provision-sieve-user-tls' + setting.id" :class="{primary: sieveSslMode === 'tls'}">{{ t('mail', 'STARTTLS') }}</label> </div> </div> @@ -297,15 +285,17 @@ <div class="group-title" /> <div class="group-inputs"> <input + class="button config-button icon-upload" type="submit" - class="primary" :disabled="loading" - :value="t('mail', 'Apply and create/update for all users')"> + :value="t('mail', 'Save Config')"> <input + v-if="deleteButton" type="button" + class="button config-button icon-delete" :disabled="loading" - :value="t('mail', 'Disable and un-provision existing accounts')" - @click="disable"> + :value="t('mail', 'Unprovision & Delete Config')" + @click="disableConfig()"> <br> <small>{{ t('mail', "* %USERID% and %EMAIL% will be replaced with the user's UID and email") @@ -328,38 +318,53 @@ </div> </div> </template> - <script> import logger from '../../logger' import ProvisionPreview from './ProvisionPreview' -import { disableProvisioning, saveProvisioningSettings } from '../../service/SettingsService' export default { name: 'ProvisioningSettings', - components: { ProvisionPreview }, + components: { + ProvisionPreview, + }, props: { - settings: { + setting: { type: Object, required: true, }, + submit: { + type: Function, + required: true, + }, + disable: { + type: Function, + required: false, + default: undefined, + }, + deleteButton: { + type: Boolean, + required: false, + default: true, + }, }, data() { return { - active: !!this.settings.active, - emailTemplate: this.settings.email || '', - imapHost: this.settings.imapHost || 'mx.domain.com', - imapPort: this.settings.imapPort || 993, - imapUser: this.settings.imapUser || '%USERID%domain.com', - imapSslMode: this.settings.imapSslMode || 'ssl', - smtpHost: this.settings.smtpHost || 'mx.domain.com', - smtpPort: this.settings.smtpPort || 587, - smtpUser: this.settings.smtpUser || '%USERID%domain.com', - smtpSslMode: this.settings.smtpSslMode || 'tls', - sieveEnabled: this.settings.sieveEnabled, - sieveHost: this.settings.sieveHost, - sievePort: this.settings.sievePort, - sieveSslMode: this.settings.sieveSslMode, - sieveUser: this.settings.sieveUser, + active: !!this.setting.active, + provisioningDomain: this.setting.provisioningDomain || '', + emailTemplate: this.setting.emailTemplate || '', + imapHost: this.setting.imapHost || 'mx.domain.com', + imapPort: this.setting.imapPort || 993, + imapUser: this.setting.imapUser || '%USERID%domain.com', + imapSslMode: this.setting.imapSslMode || 'ssl', + smtpHost: this.setting.smtpHost || 'mx.domain.com', + smtpPort: this.setting.smtpPort || 587, + smtpUser: this.setting.smtpUser || '%USERID%domain.com', + smtpSslMode: this.setting.smtpSslMode || 'tls', + sieveEnabled: this.setting.sieveEnabled || '', + sieveHost: this.setting.sieveHost || '', + sievePort: this.setting.sievePort || '', + sieveSslMode: this.setting.sieveSslMode || '', + sieveUser: this.setting.sieveUser || '', previewData1: { uid: 'user123', email: '', @@ -375,6 +380,7 @@ export default { previewTemplates() { return { email: this.emailTemplate, + provisioningDomain: this.provisioningDomain, imapUser: this.imapUser, imapHost: this.imapHost, imapPort: this.imapPort, @@ -392,53 +398,49 @@ export default { }, }, beforeMount() { - logger.debug('provisioning settings loaded', { settings: this.settings }) + logger.debug('provisioning setting loaded', { setting: this.setting }) }, methods: { - submit() { + async submitForm() { this.loading = true - return saveProvisioningSettings({ - emailTemplate: this.emailTemplate, - imapUser: this.imapUser, - imapHost: this.imapHost, - imapPort: this.imapPort, - imapSslMode: this.imapSslMode, - smtpUser: this.smtpUser, - smtpHost: this.smtpHost, - smtpPort: this.smtpPort, - smtpSslMode: this.smtpSslMode, - sieveEnabled: this.sieveEnabled, - sieveUser: this.sieveUser, - sieveHost: this.sieveHost, - sievePort: this.sievePort, - sieveSslMode: this.sieveSslMode, - }) - .then(() => { - logger.info('provisioning settings updated') - }) - .catch((error) => { - // TODO: show user feedback - logger.error('Could not save provisioning settings', { error }) - }) - .then(() => { - this.loading = false + try { + await this.submit({ + id: this.setting.id || null, + active: this.setting.active || true, + emailTemplate: this.emailTemplate, + provisioningDomain: this.provisioningDomain, + imapUser: this.imapUser, + imapHost: this.imapHost, + imapPort: this.imapPort, + imapSslMode: this.imapSslMode, + smtpUser: this.smtpUser, + smtpHost: this.smtpHost, + smtpPort: this.smtpPort, + smtpSslMode: this.smtpSslMode, + sieveEnabled: this.sieveEnabled, + sieveUser: this.sieveUser, + sieveHost: this.sieveHost, + sievePort: this.sievePort, + sieveSslMode: this.sieveSslMode, }) + + logger.info('provisioning setting updated') + } catch (error) { + logger.error('Could not save provisioning setting', { error }) + } finally { + this.loading = false + } }, - disable() { + async disableConfig() { this.loading = true - - return disableProvisioning() - .then(() => { - logger.info('deprovisioned successfully') - }) - .catch((error) => { - logger.error('could not deprovision accounts', { error }) - }) - .then(() => { - this.active = false - this.loading = false - }) + try { + await this.disable(this.setting.id) + } catch (error) { + logger.error('Could not delete provisioning setting', { error }) + } finally { + this.loading = false + } }, }, } @@ -447,8 +449,6 @@ export default { <style lang="scss" scoped> .form-preview-row { display: flex; - flex-direction: row; - flex-wrap: wrap; div:last-child { margin-top: 10px; @@ -473,6 +473,13 @@ export default { input[type='text'] { min-width: 200px; } + .config-button { + line-height: 24px; + padding-left: 48px; + padding-top: 6px; + padding-bottom: 6px; + background-position: 24px; + } } } @@ -499,7 +506,6 @@ input[type='radio'] { .flex-row { display: flex; } - form { label { color: var(--color-text-maxcontrast); diff --git a/src/main-settings.js b/src/main-settings.js index a30e72601..998474ac5 100644 --- a/src/main-settings.js +++ b/src/main-settings.js @@ -22,6 +22,7 @@ import { generateFilePath } from '@nextcloud/router' import { getRequestToken } from '@nextcloud/auth' import { loadState } from '@nextcloud/initial-state' +import '@nextcloud/dialogs/styles/toast.scss' import Vue from 'vue' import AdminSettings from './components/settings/AdminSettings' @@ -37,6 +38,6 @@ Vue.mixin(Nextcloud) const View = Vue.extend(AdminSettings) new View({ propsData: { - provisioningSettings: loadState('mail', 'provisioning_settings') || {}, + provisioningSettings: loadState('mail', 'provisioning_settings') || [], }, }).$mount('#mail-admin-settings') diff --git a/src/service/SettingsService.js b/src/service/SettingsService.js index 926a35052..73cfd3f42 100644 --- a/src/service/SettingsService.js +++ b/src/service/SettingsService.js @@ -22,14 +22,39 @@ import axios from '@nextcloud/axios' import { generateUrl } from '@nextcloud/router' -export const saveProvisioningSettings = (config) => { +export const getProvisioningSettings = (config) => { const url = generateUrl('/apps/mail/api/settings/provisioning') - return axios.post(url, config).then((resp) => resp.data) + return axios.get(url, config).then((resp) => resp.data) } -export const disableProvisioning = () => { +export const provisionAll = () => { + const url = generateUrl('/apps/mail/api/settings/provisioning/all') + + return axios.put(url).then((resp) => resp.data) +} + +export const createProvisioningSettings = (config) => { const url = generateUrl('/apps/mail/api/settings/provisioning') + const data = { + data: config, + } + return axios.post(url, data).then((resp) => resp.data) +} + +export const updateProvisioningSettings = (config) => { + const url = generateUrl('/apps/mail/api/settings/provisioning/{id}', { + id: config.id, + }) + const data = { + data: config, + } + return axios.post(url, data).then((resp) => resp.data) +} +export const disableProvisioning = (id) => { + const url = generateUrl('/apps/mail/api/settings/provisioning/{id}', { + id, + }) return axios.delete(url).then((resp) => resp.data) } diff --git a/tests/Integration/Db/MailAccountMapperTest.php b/tests/Integration/Db/MailAccountMapperTest.php index 9dc9b5021..7dc69b580 100644 --- a/tests/Integration/Db/MailAccountMapperTest.php +++ b/tests/Integration/Db/MailAccountMapperTest.php @@ -68,7 +68,7 @@ class MailAccountMapperTest extends TestCase { $this->account->setOutboundSslMode('ssl'); $this->account->setUserId('user12345'); $this->account->setEditorMode('plaintext'); - $this->account->setProvisioned(false); + $this->account->setProvisioningId(null); $this->account->setOrder(27); } diff --git a/tests/Integration/Db/MailAccountTest.php b/tests/Integration/Db/MailAccountTest.php index 8db2729f8..7eea6142a 100644 --- a/tests/Integration/Db/MailAccountTest.php +++ b/tests/Integration/Db/MailAccountTest.php @@ -44,7 +44,7 @@ class MailAccountTest extends TestCase { $a->setOutboundPassword('xxxx'); $a->setOutboundSslMode('ssl'); $a->setEditorMode('html'); - $a->setProvisioned(false); + $a->setProvisioningId(null); $a->setOrder(13); $this->assertEquals([ @@ -62,7 +62,7 @@ class MailAccountTest extends TestCase { 'smtpSslMode' => 'ssl', 'signature' => null, 'editorMode' => 'html', - 'provisioned' => false, + 'provisioningId' => null, 'order' => 13, 'showSubscribedOnly' => null, 'personalNamespace' => null, @@ -90,15 +90,15 @@ class MailAccountTest extends TestCase { 'smtpSslMode' => 'ssl', 'signature' => null, 'editorMode' => null, - 'provisioned' => false, + 'provisioningId' => null, 'order' => null, - 'showSubscribedOnly' => null, + 'showSubscribedOnly' => false, 'personalNamespace' => null, 'draftsMailboxId' => null, 'sentMailboxId' => null, 'trashMailboxId' => null, 'sieveEnabled' => false, - 'signatureAboveQuote' => false, + 'signatureAboveQuote' => false ]; $a = new MailAccount($expected); // TODO: fix inconsistency diff --git a/tests/Integration/Db/ProvisioningMapperTest.php b/tests/Integration/Db/ProvisioningMapperTest.php new file mode 100644 index 000000000..6ed040f04 --- /dev/null +++ b/tests/Integration/Db/ProvisioningMapperTest.php @@ -0,0 +1,198 @@ +<?php + +declare(strict_types=1); + +/** + * @author Anna Larch <anna.larch@nextcloud.com> + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Mail\Tests\Integration\Db; + +use OC; +use ChristophWurst\Nextcloud\Testing\DatabaseTransaction; +use ChristophWurst\Nextcloud\Testing\TestCase; +use OCA\Mail\Db\Provisioning; +use OCA\Mail\Db\ProvisioningMapper; +use OCA\Mail\Exception\ValidationException; +use OCP\IDBConnection; +use Psr\Log\LoggerInterface; + +/** + * @group DB + */ +class ProvisioningMapperTest extends TestCase { + use DatabaseTransaction; + + /** @var ProvisioningMapper */ + private $mapper; + + /** @var IDBConnection */ + private $db; + + /** @var MockObject */ + private $logger; + + /** @var Alias */ + private $alias; + + /** @var [] */ + public $data = []; + + /** + * Initialize Mapper + */ + public function setup(): void { + parent::setUp(); + $this->db = OC::$server->getDatabaseConnection(); + $this->logger = $this->createMock(LoggerInterface::class); + $this->mapper = new ProvisioningMapper($this->db, $this->logger); + $this->data['provisioningDomain'] = 'heart-of-gold.com' ; + $this->data['emailTemplate'] = '%USERID%@heart-of-gold.com'; + $this->data['imapUser'] = 'marvin@heart-of-gold.com'; + $this->data['imapHost'] = 'imap.heart-of-gold.com'; + $this->data['imapPort'] = '42'; + $this->data['imapSslMode'] = 'none'; + $this->data['smtpUser'] = 'trillian@heart-of-gold.com'; + $this->data['smtpHost'] = 'smtp.heart-of-gold.com'; + $this->data['smtpPort'] = 24; + $this->data['smtpSslMode'] = 'none'; + $this->data['sieveEnabled'] = false; + } + + public function testValidate() { + $provisioning = new Provisioning(); + $provisioning->setId(1); + $provisioning->setProvisioningDomain($this->data['provisioningDomain']); + $provisioning->setEmailTemplate($this->data['emailTemplate']); + $provisioning->setImapUser($this->data['imapUser']); + $provisioning->setImapHost($this->data['imapHost']); + $provisioning->setImapPort(42); + $provisioning->setImapSslMode($this->data['imapSslMode']); + $provisioning->setSmtpUser($this->data['smtpUser']); + $provisioning->setSmtpHost($this->data['smtpHost']); + $provisioning->setSmtpPort(24); + $provisioning->setSmtpSslMode($this->data['smtpSslMode']); + $provisioning->setSieveEnabled($this->data['sieveEnabled']); + + $provisioning = $this->mapper->validate($this->data); + + $this->assertInstanceOf(Provisioning::class, $provisioning); + foreach ($this->data as $key => $value) { + $getter = 'get' . ucfirst($key); + if ($key === 'imapPort' || $key === 'smtpPort') { + $this->assertIsInt($provisioning->$getter()); + $this->assertEquals($provisioning->$getter(), (int)$value); + } else { + $this->assertEquals($provisioning->$getter(), $value); + } + } + } + + public function testValidateException() { + $data = []; + $data['provisioningDomain'] = 'heart-of-gold.com' ; + + $this->expectException(ValidationException::class); + + $provisioning = $this->mapper->validate($data); + } + + public function testGetNoResult() { + $db = $this->mapper->get(99999); + $this->assertNull($db); + } + + /** + * @return void + */ + public function testUpdate() { + $provisioning = new Provisioning(); + $provisioning->setProvisioningDomain($this->data['provisioningDomain']); + $provisioning->setEmailTemplate($this->data['emailTemplate']); + $provisioning->setImapUser($this->data['imapUser']); + $provisioning->setImapHost($this->data['imapHost']); + $provisioning->setImapPort(42); + $provisioning->setImapSslMode($this->data['imapSslMode']); + $provisioning->setSmtpUser($this->data['smtpUser']); + $provisioning->setSmtpHost($this->data['smtpHost']); + $provisioning->setSmtpPort(24); + $provisioning->setSmtpSslMode($this->data['smtpSslMode']); + $provisioning->setSieveEnabled($this->data['sieveEnabled']); + $provisioning = $this->mapper->insert($provisioning); + $id = $provisioning->getId(); + $provisioning->setProvisioningDomain('arthur-dent.com'); + + $update = $this->mapper->update($provisioning); + + $this->assertInstanceOf(Provisioning::class, $update); + $this->assertEquals($id, $update->getId()); + $this->assertEquals('arthur-dent.com', $update->getProvisioningDomain()); + } + + public function testGetAll() { + $provisioning = new Provisioning(); + $provisioning->setProvisioningDomain($this->data['provisioningDomain']); + $provisioning->setEmailTemplate($this->data['emailTemplate']); + $provisioning->setImapUser($this->data['imapUser']); + $provisioning->setImapHost($this->data['imapHost']); + $provisioning->setImapPort(42); + $provisioning->setImapSslMode($this->data['imapSslMode']); + $provisioning->setSmtpUser($this->data['smtpUser']); + $provisioning->setSmtpHost($this->data['smtpHost']); + $provisioning->setSmtpPort(24); + $provisioning->setSmtpSslMode($this->data['smtpSslMode']); + $provisioning->setSieveEnabled($this->data['sieveEnabled']); + $provisioning = $this->mapper->insert($provisioning); + + $results = $this->mapper->getAll(); + + $this->assertIsArray($results); + $this->assertNotEmpty($results); + foreach ($results as $result) { + $this->assertInstanceOf(Provisioning::class, $result); + } + } + + public function testGet() { + $provisioning = new Provisioning(); + $provisioning->setProvisioningDomain($this->data['provisioningDomain']); + $provisioning->setEmailTemplate($this->data['emailTemplate']); + $provisioning->setImapUser($this->data['imapUser']); + $provisioning->setImapHost($this->data['imapHost']); + $provisioning->setImapPort(42); + $provisioning->setImapSslMode($this->data['imapSslMode']); + $provisioning->setSmtpUser($this->data['smtpUser']); + $provisioning->setSmtpHost($this->data['smtpHost']); + $provisioning->setSmtpPort(24); + $provisioning->setSmtpSslMode($this->data['smtpSslMode']); + $provisioning->setSieveEnabled($this->data['sieveEnabled']); + $provisioning = $this->mapper->insert($provisioning); + + $db = $this->mapper->get($provisioning->id); + + $this->assertInstanceOf(Provisioning::class, $db); + foreach ($this->data as $key => $value) { + $getter = 'get' . ucfirst($key); + if ($key === 'imapPort' || $key === 'smtpPort') { + $this->assertEquals($db->$getter(), (int)$value); + } else { + $this->assertEquals($db->$getter(), $value); + } + } + } +} diff --git a/tests/Unit/Controller/SettingsControllerTest.php b/tests/Unit/Controller/SettingsControllerTest.php index c19b459ca..5d1947459 100644 --- a/tests/Unit/Controller/SettingsControllerTest.php +++ b/tests/Unit/Controller/SettingsControllerTest.php @@ -27,6 +27,7 @@ namespace OCA\Mail\Tests\Unit\Controller; use ChristophWurst\Nextcloud\Testing\ServiceMockObject; use OCA\Mail\Controller\SettingsController; +use OCA\Mail\Db\Provisioning; use OCA\Mail\Tests\Integration\TestCase; use OCP\AppFramework\Http\JSONResponse; @@ -48,50 +49,25 @@ class SettingsControllerTest extends TestCase { public function testProvisioning() { $this->mock->getParameter('provisioningManager') ->expects($this->once()) - ->method('newProvisioning') - ->with( - '%USERID%@domain.com', - '%USERID%@domain.com', - 'mx.domain.com', - 993, - 'ssl', - '%USERID%@domain.com', - 'mx.domain.com', - 567, - 'tls', - false, - '', - '', - 0, - '' - ); + ->method('provision') + ->with(); - $response = $this->controller->provisioning( - '%USERID%@domain.com', - '%USERID%@domain.com', - 'mx.domain.com', - 993, - 'ssl', - '%USERID%@domain.com', - 'mx.domain.com', - 567, - 'tls', - false, - '', - '', - 0, - '' - ); + $response = $this->controller->provision(); $this->assertInstanceOf(JSONResponse::class, $response); } public function testDeprovision() { + $provisioning = new Provisioning(); $this->mock->getParameter('provisioningManager') ->expects($this->once()) - ->method('deprovision'); - - $response = $this->controller->deprovision(); + ->method('getConfigById') + ->willReturn($provisioning); + $this->mock->getParameter('provisioningManager') + ->expects($this->once()) + ->method('deprovision') + ->with($provisioning); + $response = $this->controller->deprovision(1); $this->assertInstanceOf(JSONResponse::class, $response); } diff --git a/tests/Unit/Http/Middleware/ProvisioningMiddlewareTest.php b/tests/Unit/Http/Middleware/ProvisioningMiddlewareTest.php index d8d4eaf8c..97425c908 100644 --- a/tests/Unit/Http/Middleware/ProvisioningMiddlewareTest.php +++ b/tests/Unit/Http/Middleware/ProvisioningMiddlewareTest.php @@ -27,8 +27,8 @@ namespace OCA\Mail\Tests\Unit\Http\Middleware; use ChristophWurst\Nextcloud\Testing\TestCase; use OCA\Mail\Controller\PageController; +use OCA\Mail\Db\Provisioning; use OCA\Mail\Http\Middleware\ProvisioningMiddleware; -use OCA\Mail\Service\Provisioning\Config; use OCA\Mail\Service\Provisioning\Manager; use OCP\Authentication\Exceptions\CredentialsUnavailableException; use OCP\Authentication\Exceptions\PasswordUnavailableException; @@ -85,17 +85,19 @@ class ProvisioningMiddlewareTest extends TestCase { } public function testBeforeControllerNoCredentialsAvailable() { - $user = $this->createMock(IUser::class); + $user = $this->createConfiguredMock(IUser::class, [ + 'getEmailAddress' => 'bruce.wayne@batman.com' + ]); $this->userSession->expects($this->once()) ->method('getUser') ->willReturn($user); - $config = $this->createMock(Config::class); + $configs = [new Provisioning()]; $this->provisioningManager->expects($this->once()) - ->method('getConfig') - ->willReturn($config); - $config->expects($this->once()) - ->method('isActive') - ->willReturn(true); + ->method('getConfigs') + ->willReturn($configs); + $this->provisioningManager->expects($this->once()) + ->method('provisionSingleUser') + ->with($configs, $user); $this->credentialStore->expects($this->once()) ->method('getLoginCredentials') ->willThrowException($this->createMock(CredentialsUnavailableException::class)); @@ -109,17 +111,19 @@ class ProvisioningMiddlewareTest extends TestCase { } public function testBeforeControllerNoPasswordAvailable() { - $user = $this->createMock(IUser::class); + $user = $this->createConfiguredMock(IUser::class, [ + 'getEmailAddress' => 'bruce.wayne@batman.com' + ]); $this->userSession->expects($this->once()) ->method('getUser') ->willReturn($user); - $config = $this->createMock(Config::class); + $configs = [new Provisioning()]; + $this->provisioningManager->expects($this->once()) + ->method('getConfigs') + ->willReturn($configs); $this->provisioningManager->expects($this->once()) - ->method('getConfig') - ->willReturn($config); - $config->expects($this->once()) - ->method('isActive') - ->willReturn(true); + ->method('provisionSingleUser') + ->with($configs, $user); $credentials = $this->createMock(ICredentials::class); $this->credentialStore->expects($this->once()) ->method('getLoginCredentials') @@ -137,36 +141,15 @@ class ProvisioningMiddlewareTest extends TestCase { } public function testBeforeControllerNoConfigAvailable() { - $user = $this->createMock(IUser::class); + $user = $this->createConfiguredMock(IUser::class, [ + 'getEmailAddress' => 'bruce.wayne@batman.com' + ]); $this->userSession->expects($this->once()) ->method('getUser') ->willReturn($user); - $this->provisioningManager->expects($this->once()) - ->method('getConfig') - ->willReturn(null); - $this->credentialStore->expects($this->never()) - ->method('getLoginCredentials'); - $this->provisioningManager->expects($this->never()) - ->method('updatePassword'); - - $this->middleware->beforeController( - $this->createMock(PageController::class), - 'index' - ); - } - - public function testBeforeControllerNotActive() { - $user = $this->createMock(IUser::class); - $this->userSession->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $config = $this->createMock(Config::class); - $this->provisioningManager->expects($this->once()) - ->method('getConfig') - ->willReturn($config); - $config->expects($this->once()) - ->method('isActive') - ->willReturn(false); + $this->provisioningManager->expects($this->any()) + ->method('getConfigs') + ->willReturn([]); $this->credentialStore->expects($this->never()) ->method('getLoginCredentials'); $this->provisioningManager->expects($this->never()) @@ -179,17 +162,21 @@ class ProvisioningMiddlewareTest extends TestCase { } public function testBeforeController() { - $user = $this->createMock(IUser::class); + $user = $this->createConfiguredMock(IUser::class, [ + 'getEmailAddress' => 'bruce.wayne@batman.com' + ]); $this->userSession->expects($this->once()) ->method('getUser') ->willReturn($user); - $config = $this->createMock(Config::class); + $config = new Provisioning(); + $config->setId(1); + $configs = [$config]; + $this->provisioningManager->expects($this->once()) + ->method('getConfigs') + ->willReturn($configs); $this->provisioningManager->expects($this->once()) - ->method('getConfig') - ->willReturn($config); - $config->expects($this->once()) - ->method('isActive') - ->willReturn(true); + ->method('provisionSingleUser') + ->with($configs, $user); $credentials = $this->createMock(ICredentials::class); $this->credentialStore->expects($this->once()) ->method('getLoginCredentials') diff --git a/tests/Unit/Migration/AddSieveToProvisioningConfigTest.php b/tests/Unit/Migration/AddSieveToProvisioningConfigTest.php deleted file mode 100644 index a8ddcec3c..000000000 --- a/tests/Unit/Migration/AddSieveToProvisioningConfigTest.php +++ /dev/null @@ -1,121 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * - * Mail - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Mail\Tests\Unit\Migration; - -use ChristophWurst\Nextcloud\Testing\ServiceMockObject; -use ChristophWurst\Nextcloud\Testing\TestCase; -use OCA\Mail\Migration\AddSieveToProvisioningConfig; -use OCA\Mail\Service\Provisioning\Config; -use OCP\Migration\IOutput; -use PHPUnit\Framework\MockObject\MockObject; - -class AddSieveToProvisioningConfigTest extends TestCase { - - /** @var ServiceMockObject */ - private $mock; - - /** @var AddSieveToProvisioningConfig */ - private $repairStep; - - protected function setUp(): void { - parent::setUp(); - - $this->mock = $this->createServiceMock(AddSieveToProvisioningConfig::class); - $this->repairStep = $this->mock->getService(); - } - - - public function testRunNoConfigToMigrate() { - $this->mock->getParameter('config') - ->expects($this->once()) - ->method('getAppValue') - ->with('mail', 'installed_version') - ->willReturn('1.8.0'); - - /** @var IOutput|MockObject $output */ - $output = $this->createMock(IOutput::class); - $output->expects($this->never()) - ->method('info'); - - $this->repairStep->run($output); - } - - public function testRun() { - $this->mock->getParameter('config') - ->expects($this->once()) - ->method('getAppValue') - ->with('mail', 'installed_version') - ->willReturn('1.8.0'); - - $this->mock->getParameter('configMapper') - ->expects($this->once()) - ->method('load') - ->willReturn(new Config([ - 'email' => '%USERID%@domain.com', - 'imapUser' => '%USERID%@domain.com', - 'imapHost' => 'mx.domain.com', - 'imapPort' => 993, - 'imapSslMode' => 'ssl', - 'smtpUser' => '%USERID%@domain.com', - 'smtpHost' => 'mx.domain.com', - 'smtpPort' => 567, - 'smtpSslMode' => 'tls', - ])); - - $this->mock->getParameter('configMapper') - ->expects($this->once()) - ->method('save') - ->with(new Config([ - 'email' => '%USERID%@domain.com', - 'imapUser' => '%USERID%@domain.com', - 'imapHost' => 'mx.domain.com', - 'imapPort' => 993, - 'imapSslMode' => 'ssl', - 'smtpUser' => '%USERID%@domain.com', - 'smtpHost' => 'mx.domain.com', - 'smtpPort' => 567, - 'smtpSslMode' => 'tls', - 'sieveEnabled' => false, - 'sieveHost' => '', - 'sievePort' => 4190, - 'sieveUser' => '', - 'sieveSslMode' => 'tls', - ])); - - /** @var IOutput|MockObject $output */ - $output = $this->createMock(IOutput::class); - $output->expects($this->once()) - ->method('info') - ->with('added sieve defaults to provisioning config'); - - $this->repairStep->run($output); - } - - public function testGetName() { - $this->assertEquals( - 'Add sieve defaults to provisioning config', - $this->repairStep->getName() - ); - } -} diff --git a/tests/Unit/Migration/MigrateProvisioningConfigTest.php b/tests/Unit/Migration/MigrateProvisioningConfigTest.php deleted file mode 100644 index 7a0dc7862..000000000 --- a/tests/Unit/Migration/MigrateProvisioningConfigTest.php +++ /dev/null @@ -1,107 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -namespace OCA\Mail\Tests\Unit\Migration; - -use ChristophWurst\Nextcloud\Testing\ServiceMockObject; -use ChristophWurst\Nextcloud\Testing\TestCase; -use OCA\Mail\Migration\MigrateProvisioningConfig; -use OCA\Mail\Service\Provisioning\Config; -use OCP\Migration\IOutput; -use PHPUnit\Framework\MockObject\MockObject; - -class MigrateProvisioningConfigTest extends TestCase { - - /** @var ServiceMockObject */ - private $mock; - - /** @var MigrateProvisioningConfig */ - private $repairStep; - - protected function setUp(): void { - parent::setUp(); - - $this->mock = $this->createServiceMock(MigrateProvisioningConfig::class); - $this->repairStep = $this->mock->getService(); - } - - - public function testRunNoConfigToMigrate() { - /** @var IOutput|MockObject $output */ - $output = $this->createMock(IOutput::class); - $this->mock->getParameter('config') - ->expects($this->once()) - ->method('getSystemValue') - ->with('app.mail.accounts.default') - ->willReturn(''); - - $this->repairStep->run($output); - } - - public function testRunAlreadyMigrated() { - /** @var IOutput|MockObject $output */ - $output = $this->createMock(IOutput::class); - $this->mock->getParameter('config') - ->expects($this->once()) - ->method('getSystemValue') - ->with('app.mail.accounts.default') - ->willReturn([]); - $this->mock->getParameter('provisioningManager') - ->expects($this->once()) - ->method('getConfig') - ->willReturn($this->createMock(Config::class)); - - $this->repairStep->run($output); - } - - public function testRun() { - /** @var IOutput|MockObject $output */ - $output = $this->createMock(IOutput::class); - $this->mock->getParameter('config') - ->expects($this->once()) - ->method('getSystemValue') - ->with('app.mail.accounts.default') - ->willReturn([]); - $this->mock->getParameter('provisioningManager') - ->expects($this->once()) - ->method('getConfig') - ->willReturn(null); - $this->mock->getParameter('provisioningManager') - ->expects($this->once()) - ->method('importConfig'); - $this->mock->getParameter('config') - ->expects($this->once()) - ->method('deleteSystemValue') - ->with('app.mail.accounts.default'); - - $this->repairStep->run($output); - } - - public function testGetName() { - $name = $this->repairStep->getName(); - - $this->assertEquals('Migrate Mail provisioning config from config.php to the database', $name); - } -} diff --git a/tests/Unit/Service/Provisioning/ConfigMapperTest.php b/tests/Unit/Service/Provisioning/ConfigMapperTest.php deleted file mode 100644 index c311cb810..000000000 --- a/tests/Unit/Service/Provisioning/ConfigMapperTest.php +++ /dev/null @@ -1,86 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -namespace OCA\Mail\Tests\Unit\Service\Provisioning; - -use ChristophWurst\Nextcloud\Testing\ServiceMockObject; -use ChristophWurst\Nextcloud\Testing\TestCase; -use OCA\Mail\Service\Provisioning\Config; -use OCA\Mail\Service\Provisioning\ConfigMapper; -use PHPUnit\Framework\MockObject\MockObject; - -class ConfigMapperTest extends TestCase { - - /** @var ServiceMockObject */ - private $mock; - - /** @var ConfigMapper */ - private $mapper; - - protected function setUp(): void { - parent::setUp(); - - $this->mock = $this->createServiceMock(ConfigMapper::class); - $this->mapper = $this->mock->getService(); - } - - public function testSave() { - /** @var Config|MockObject $config */ - $config = $this->createMock(Config::class); - $config->expects($this->once()) - ->method('jsonSerialize') - ->willReturn([]); - $this->mock->getParameter('config') - ->expects($this->once()) - ->method('setAppValue') - ->with('mail', 'provisioning_settings', '[]'); - - $this->mapper->save($config); - } - - public function testLoadNoConfig() { - $this->mock->getParameter('config') - ->expects($this->once()) - ->method('getAppValue') - ->with('mail', 'provisioning_settings') - ->willReturn(''); - - $config = $this->mapper->load(); - - $this->assertNull($config); - } - - public function testLoad() { - $this->mock->getParameter('config') - ->expects($this->once()) - ->method('getAppValue') - ->with('mail', 'provisioning_settings') - ->willReturn('[]'); - - $config = $this->mapper->load(); - - $this->assertInstanceOf(Config::class, $config); - } -} diff --git a/tests/Unit/Service/Provisioning/ConfigTest.php b/tests/Unit/Service/Provisioning/ConfigTest.php deleted file mode 100644 index 839bfcb76..000000000 --- a/tests/Unit/Service/Provisioning/ConfigTest.php +++ /dev/null @@ -1,220 +0,0 @@ -<?php - -/** - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * Mail - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Mail\Tests\Unit\Service\Provisioning; - -use ChristophWurst\Nextcloud\Testing\TestCase; -use OCA\Mail\Service\Provisioning\Config; -use OCP\IUser; - -class ConfigTest extends TestCase { - public function testBuildEmailWithUserId() { - $user = $this->createMock(IUser::class); - $config = new Config([ - 'email' => '%USERID%@domain.se', - ]); - $user->expects($this->exactly(2)) - ->method('getUID') - ->willReturn('test'); - $user->expects($this->exactly(2)) - ->method('getEMailAddress') - ->willReturn('user@domain.se'); - - $this->assertEquals('test@domain.se', $config->buildEmail($user)); - } - - public function testBuildEmailWithEmailPlaceholder() { - $user = $this->createMock(IUser::class); - $config = new Config([ - 'email' => '%EMAIL%', - ]); - $user->expects($this->exactly(2)) - ->method('getUID') - ->willReturn('user'); - $user->expects($this->any()) - ->method('getEMailAddress') - ->willReturn('user@domain.se'); - - $this->assertEquals('user@domain.se', $config->buildEmail($user)); - } - - public function testBuildImapUserWithUserId() { - $user = $this->createMock(IUser::class); - $config = new Config([ - 'imapUser' => '%USERID%@domain.se', - ]); - $user->expects($this->exactly(2)) - ->method('getUID') - ->willReturn('test'); - $user->expects($this->once()) - ->method('getEMailAddress') - ->willReturn(null); - - $this->assertEquals('test@domain.se', $config->buildImapUser($user)); - } - - public function testBuildImapUserWithEmailPlaceholder() { - $user = $this->createMock(IUser::class); - $config = new Config([ - 'imapUser' => '%EMAIL%', - ]); - $user->expects($this->exactly(2)) - ->method('getUID') - ->willReturn('user'); - $user->expects($this->any()) - ->method('getEMailAddress') - ->willReturn('user@domain.se'); - - $this->assertEquals('user@domain.se', $config->buildImapUser($user)); - } - - public function testBuildImapUserFromDefaultEmail() { - $user = $this->createMock(IUser::class); - $config = new Config([ - 'email' => '%EMAIL%', - ]); - $user->expects($this->exactly(2)) - ->method('getUID') - ->willReturn('user'); - $user->expects($this->exactly(2)) - ->method('getEMailAddress') - ->willReturn('user@domain.se'); - - $this->assertEquals('user@domain.se', $config->buildImapUser($user)); - } - - public function testBuildSmtpUserWithUserId() { - $user = $this->createMock(IUser::class); - $config = new Config([ - 'smtpUser' => '%USERID%@domain.se', - ]); - $user->expects($this->exactly(2)) - ->method('getUID') - ->willReturn('test'); - $user->expects($this->once()) - ->method('getEMailAddress') - ->willReturn(null); - - $this->assertEquals('test@domain.se', $config->buildSmtpUser($user)); - } - - public function testBuilldSmtpUserWithEmailPlaceholder() { - $user = $this->createMock(IUser::class); - $config = new Config([ - 'smtpUser' => '%EMAIL%', - ]); - $user->expects($this->any()) - ->method('getUID') - ->willReturn(null); - $user->expects($this->any()) - ->method('getEMailAddress') - ->willReturn('user@domain.se'); - - $this->assertEquals('user@domain.se', $config->buildSmtpUser($user)); - } - - public function testBuildSmtpUserFromDefaultEmail() { - $user = $this->createMock(IUser::class); - $config = new Config([ - 'email' => '%EMAIL%', - ]); - $user->expects($this->exactly(2)) - ->method('getUID') - ->willReturn('user'); - $user->expects($this->exactly(2)) - ->method('getEMailAddress') - ->willReturn('user@domain.se'); - - $this->assertEquals('user@domain.se', $config->buildImapUser($user)); - } - - public function testBuildSieveUserWithUserId(): void { - $user = $this->createMock(IUser::class); - $config = new Config([ - 'sieveUser' => '%USERID%@domain.se', - ]); - $user->expects($this->exactly(2)) - ->method('getUID') - ->willReturn('test'); - $user->expects($this->once()) - ->method('getEMailAddress') - ->willReturn(null); - - $this->assertEquals('test@domain.se', $config->buildSieveUser($user)); - } - - public function testBuilldSieveUserWithEmailPlaceholder(): void { - $user = $this->createMock(IUser::class); - $config = new Config([ - 'sieveUser' => '%EMAIL%', - ]); - $user->expects($this->once()) - ->method('getUID') - ->willReturn(null); - $user->expects($this->exactly(2)) - ->method('getEMailAddress') - ->willReturn('user@domain.se'); - - $this->assertEquals('user@domain.se', $config->buildSieveUser($user)); - } - - public function testBuildSieveUserFromDefaultEmail(): void { - $user = $this->createMock(IUser::class); - $config = new Config([ - 'email' => '%EMAIL%', - ]); - $user->expects($this->exactly(2)) - ->method('getUID') - ->willReturn('user'); - $user->expects($this->exactly(2)) - ->method('getEMailAddress') - ->willReturn('user@domain.se'); - - $this->assertEquals('user@domain.se', $config->buildSieveUser($user)); - } - - /** - * @param string $key - * @param mixed $value - * @dataProvider providerTestGetter - */ - public function testGetter(string $key, $value): void { - $config = new Config([ - $key => $value - ]); - $this->assertEquals($value, $config->{'get' . ucfirst($key)}()); - } - - public function providerTestGetter(): array { - return [ - 'smtpHost' => ['smtpHost', 'smtp.domain.com'], - 'smtpPort' => ['smtpPort', 465], - 'smtpSslMode' => ['smtpSslMode', 'tls'], - 'imapHost' => ['imapHost', 'imap.domain.com'], - 'imapPort' => ['imapPort', 993], - 'imapSslMode' => ['imapSslMode', 'tls'], - 'sieveEnabled' => ['sieveEnabled', true], - 'sieveHost' => ['sieveHost', 'imap.domain.com'], - 'sievePort' => ['sieveHost', 4190], - 'sieveSslMode' => ['sieveSslMode', 'tls'], - ]; - } -} diff --git a/tests/Unit/Service/Provisioning/ManagerTest.php b/tests/Unit/Service/Provisioning/ManagerTest.php index 81b0ede1e..2b2dd9449 100644 --- a/tests/Unit/Service/Provisioning/ManagerTest.php +++ b/tests/Unit/Service/Provisioning/ManagerTest.php @@ -25,7 +25,7 @@ namespace OCA\Mail\Tests\Unit\Service\Provisioning; use ChristophWurst\Nextcloud\Testing\ServiceMockObject; use ChristophWurst\Nextcloud\Testing\TestCase; use OCA\Mail\Db\MailAccount; -use OCA\Mail\Service\Provisioning\Config; +use OCA\Mail\Db\Provisioning; use OCA\Mail\Service\Provisioning\Manager; use OCP\AppFramework\Db\DoesNotExistException; use OCP\IUser; @@ -43,24 +43,30 @@ class ManagerTest extends TestCase { parent::setUp(); $this->mock = $this->createServiceMock(Manager::class); + /** @var MockObject $manager */ $this->manager = $this->mock->getService(); } public function testProvision() { - $config = new TestConfig(); $this->mock->getParameter('userManager') ->expects($this->once()) ->method('callForAllUsers'); - $cnt = $this->manager->provision($config); + $cnt = $this->manager->provision(); $this->assertEquals(0, $cnt); } public function testUpdateProvisionSingleUser() { /** @var IUser|MockObject $user */ - $user = $this->createMock(IUser::class); - $config = new TestConfig(); + $user = $this->createConfiguredMock(IUser::class, [ + 'getEmailAddress' => 'bruce.wayne@batman.com' + ]); + $config = new Provisioning(); + $config->setId(1); + $config->setProvisioningDomain('batman.com'); + $config->setEmailTemplate('%USER%@batman.com'); + $configs = [$config]; $account = $this->createMock(MailAccount::class); $this->mock->getParameter('mailAccountMapper') ->expects($this->once()) @@ -71,13 +77,20 @@ class ManagerTest extends TestCase { ->method('update') ->with($account); - $this->manager->provisionSingleUser($config, $user); + $result = $this->manager->provisionSingleUser($configs, $user); + $this->assertTrue($result); } public function testProvisionSingleUser() { /** @var IUser|MockObject $user */ - $user = $this->createMock(IUser::class); - $config = new TestConfig(); + $user = $this->createConfiguredMock(IUser::class, [ + 'getEmailAddress' => 'bruce.wayne@batman.com' + ]); + $config = new Provisioning(); + $config->setId(1); + $config->setProvisioningDomain('batman.com'); + $config->setEmailTemplate('%USER%@batman.com'); + $configs = [$config]; $this->mock->getParameter('mailAccountMapper') ->expects($this->once()) ->method('findProvisionedAccount') @@ -86,54 +99,89 @@ class ManagerTest extends TestCase { ->expects($this->once()) ->method('insert'); - $this->manager->provisionSingleUser($config, $user); - } - - public function testGetNoConfig() { - $config = $this->manager->getConfig(); - - $this->assertNull($config); + $result = $this->manager->provisionSingleUser($configs, $user); + $this->assertTrue($result); } - public function testGetConfig() { - $config = $this->createMock(Config::class); - $this->mock->getParameter('configMapper') + public function testUpdateProvisionSingleUserWithWildcard() { + /** @var IUser|MockObject $user */ + $user = $this->createConfiguredMock(IUser::class, [ + 'getEmailAddress' => 'bruce.wayne@batman.com' + ]); + $config = new Provisioning(); + $config->setId(1); + $config->setProvisioningDomain('*'); + $config->setEmailTemplate('%USER%@batman.com'); + $configs = [$config]; + $account = $this->createMock(MailAccount::class); + $this->mock->getParameter('mailAccountMapper') ->expects($this->once()) - ->method('load') - ->willReturn($config); - - $cfg = $this->manager->getConfig(); + ->method('findProvisionedAccount') + ->willReturn($account); + $this->mock->getParameter('mailAccountMapper') + ->expects($this->once()) + ->method('update') + ->with($account); - $this->assertSame($config, $cfg); + $result = $this->manager->provisionSingleUser($configs, $user); + $this->assertTrue($result); } - public function testDeprovision() { - $config = new TestConfig(); + public function testProvisionSingleUserWithWildcard() { + /** @var IUser|MockObject $user */ + $user = $this->createConfiguredMock(IUser::class, [ + 'getEmailAddress' => 'bruce.wayne@batman.com' + ]); + $config = new Provisioning(); + $config->setId(1); + $config->setProvisioningDomain('*'); + $config->setEmailTemplate('%USER%@batman.com'); + $configs = [$config]; $this->mock->getParameter('mailAccountMapper') ->expects($this->once()) - ->method('deleteProvisionedAccounts'); - $this->mock->getParameter('configMapper') - ->expects($this->once()) - ->method('load') - ->willReturn($config); - $this->mock->getParameter('configMapper') + ->method('findProvisionedAccount') + ->willThrowException($this->createMock(DoesNotExistException::class)); + $this->mock->getParameter('mailAccountMapper') ->expects($this->once()) - ->method('save') - ->willReturn($config); + ->method('insert'); - $this->manager->deprovision(); + $result = $this->manager->provisionSingleUser($configs, $user); + $this->assertTrue($result); + } - $this->assertEquals(false, $config->jsonSerialize()['active']); + public function testProvisionSingleUserNoDomainMatch() { + /** @var IUser|MockObject $user */ + $user = $this->createConfiguredMock(IUser::class, [ + 'getEmailAddress' => 'bruce.wayne@batman.com' + ]); + $config = new Provisioning(); + $config->setId(1); + $config->setProvisioningDomain('arkham-asylum.com'); + $config->setEmailTemplate('%USER%@batman.com'); + $configs = [$config]; + $this->mock->getParameter('mailAccountMapper') + ->expects($this->never()) + ->method('findProvisionedAccount'); + $this->mock->getParameter('mailAccountMapper') + ->expects($this->never()) + ->method('update'); + $this->mock->getParameter('mailAccountMapper') + ->expects($this->never()) + ->method('insert'); + + $result = $this->manager->provisionSingleUser($configs, $user); + $this->assertFalse($result); } - public function testImportConfig() { - $this->mock->getParameter('configMapper') + public function testDeprovision() { + $config = new Provisioning(); + $config->setProvisioningDomain('*'); + $config->setId(1); + $this->mock->getParameter('mailAccountMapper') ->expects($this->once()) - ->method('save'); + ->method('deleteProvisionedAccounts'); - $this->manager->importConfig([ - 'email' => '%USERID%@domain.com', - ]); + $this->manager->deprovision($config); } public function testUpdatePasswordNotProvisioned() { @@ -165,25 +213,48 @@ class ManagerTest extends TestCase { } public function testNewProvisioning() { - $this->mock->getParameter('configMapper') + $config = new Provisioning([ + 'active' => true, + 'email' => '%USERID%@domain.com', + 'imapUser' => '%USERID%@domain.com', + 'imapHost' => 'mx.domain.com', + 'imapPort' => 993, + 'imapSslMode' => 'ssl', + 'smtpUser' => '%USERID%@domain.com', + 'smtpHost' => 'mx.domain.com', + 'smtpPort' => 567, + 'smtpSslMode' => 'tls', + 'sieveEnabled' => false, + 'sieveUser' => '', + 'sieveHost' => '', + 'sievePort' => 0, + 'sieveSslMode' => 'tls' + ]); + $this->mock->getParameter('provisioningMapper') ->expects($this->once()) - ->method('save'); - - $this->manager->newProvisioning( - '%USERID%@domain.com', - '%USERID%@domain.com', - 'mx.domain.com', - 993, - 'ssl', - '%USERID%@domain.com', - 'mx.domain.com', - 567, - 'tls', - false, - '', - '', - 0, - 'tls' - ); + ->method('validate') + ->willReturn($config); + + $this->mock->getParameter('provisioningMapper') + ->expects($this->once()) + ->method('insert'); + + $this->manager->newProvisioning([ + 'active' => true, + 'email' => '%USERID%@domain.com', + 'imapUser' => '%USERID%@domain.com', + 'imapHost' => 'mx.domain.com', + 'imapPort' => 993, + 'imapSslMode' => 'ssl', + 'smtpUser' => '%USERID%@domain.com', + 'smtpHost' => 'mx.domain.com', + 'smtpPort' => 567, + 'smtpSslMode' => 'tls', + 'sieveEnabled' => false, + 'sieveUser' => '', + 'sieveHost' => '', + 'sievePort' => 0, + 'sieveSslMode' => 'tls' + ]); } } |