diff options
41 files changed, 2216 insertions, 398 deletions
diff --git a/appinfo/info.xml b/appinfo/info.xml index d51bdf020..1872e1c30 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -12,7 +12,7 @@ - **🙈 We’re not reinventing the wheel!** Based on the great [Horde](http://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>0.19.1</version> + <version>0.20.0</version> <licence>agpl</licence> <author>Christoph Wurst</author> <author>Jan-Christoph Borchardt</author> @@ -34,10 +34,15 @@ <repair-steps> <post-migration> <step>OCA\Mail\Migration\FixCollectedAddresses</step> + <step>OCA\Mail\Migration\MigrateProvisioningConfig</step> + <step>OCA\Mail\Migration\ProvisionAccounts</step> </post-migration> </repair-steps> <commands> <command>OCA\Mail\Command\CreateAccount</command> <command>OCA\Mail\Command\ExportAccount</command> </commands> + <settings> + <admin>OCA\Mail\Settings\AdminSettings</admin> + </settings> </info> diff --git a/appinfo/routes.php b/appinfo/routes.php index 5812cb9d7..8ce7275c8 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -113,6 +113,16 @@ return [ 'url' => '/proxy', 'verb' => 'GET' ], + [ + 'name' => 'settings#provisioning', + 'url' => '/api/settings/provisioning', + 'verb' => 'POST' + ], + [ + 'name' => 'settings#deprovision', + 'url' => '/api/settings/provisioning', + 'verb' => 'DELETE' + ], ], 'resources' => [ 'accounts' => ['url' => '/api/accounts'], diff --git a/doc/admin.md b/doc/admin.md index acebe76bf..9496b43ee 100644 --- a/doc/admin.md +++ b/doc/admin.md @@ -9,52 +9,6 @@ Then open the Mail app from the app menu. Put in your mail account credentials a Certain advanced or experimental features need to be specifically enabled in your `config.php`: -### Automatic account creation - -In cases where an external user back-end is used for both your Nextcloud and your mail server you may want to have imap accounts set up automatically for your users. - -### Available patterns - -Two patterns are available to automatically construct credentials: -* `%USERID%`, e.g. `jan` -* `%EMAIL%`, e.g. `jan@domain.tld` - -### Minimal configuration - -The following minimal configuration will add such an account as soon as the user logs in. The login password is used for the IMAP and SMTP authentication. - -Note: Valid values for SSL are `'none'`, `'ssl'` and `'tls'`. - -``` - 'app.mail.accounts.default' => [ - 'email' => '%USERID%@domain.tld', - 'imapHost' => 'imap.domain.tld', - 'imapPort' => 993, - 'imapSslMode' => 'ssl', - 'smtpHost' => 'smtp.domain.tld', - 'smtpPort' => 486, - 'smtpSslMode' => 'tls', - ], -``` - -### Advanced configuration - -In case you have to tweak IMAP and SMTP username, you can do that too. - -``` - 'app.mail.accounts.default' => [ - 'email' => '%USERID%@domain.tld', - 'imapHost' => 'imap.domain.tld', - 'imapPort' => 993, - 'imapUser' => '%USERID%@domain.tld', - 'imapSslMode' => 'ssl', - 'smtpHost' => 'smtp.domain.tld', - 'smtpPort' => 486, - 'smtpUser' => '%USERID%@domain.tld', - 'smtpSslMode' => 'tls', - ], -``` - ### Timeouts Depending on your mail host, it may be necessary to increase your IMAP and/or SMTP timeout threshold. Currently IMAP defaults to 20 seconds and SMTP defaults to 2 seconds. They can be changed as follows: diff --git a/lib/AppInfo/BootstrapSingleton.php b/lib/AppInfo/BootstrapSingleton.php index 448a2fc68..114d0f897 100644 --- a/lib/AppInfo/BootstrapSingleton.php +++ b/lib/AppInfo/BootstrapSingleton.php @@ -23,6 +23,7 @@ namespace OCA\Mail\AppInfo; +use OC\Hooks\PublicEmitter; use OCA\Mail\Contracts\IAttachmentService; use OCA\Mail\Contracts\IAvatarService; use OCA\Mail\Contracts\IMailManager; @@ -34,6 +35,7 @@ use OCA\Mail\Events\DraftSavedEvent; use OCA\Mail\Events\MessageSentEvent; use OCA\Mail\Events\SaveDraftEvent; use OCA\Mail\Http\Middleware\ErrorMiddleware; +use OCA\Mail\Http\Middleware\ProvisioningMiddleware; use OCA\Mail\Listener\AddressCollectionListener; use OCA\Mail\Listener\DeleteDraftListener; use OCA\Mail\Listener\DraftMailboxCreatorListener; @@ -50,7 +52,8 @@ use OCA\Mail\Service\MailTransmission; use OCA\Mail\Service\UserPreferenceSevice; use OCP\AppFramework\IAppContainer; use OCP\EventDispatcher\IEventDispatcher; -use OCP\IContainer; +use OCP\IUser; +use OCP\IUserManager; use OCP\Util; class BootstrapSingleton { @@ -108,6 +111,7 @@ class BootstrapSingleton { $container->registerAlias('ErrorMiddleware', ErrorMiddleware::class); $container->registerMiddleWare('ErrorMiddleware'); + $container->registerMiddleWare(ProvisioningMiddleware::class); $container->registerAlias(IGroupService::class, NextcloudGroupService::class); } diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php new file mode 100644 index 000000000..ccd80549d --- /dev/null +++ b/lib/Controller/SettingsController.php @@ -0,0 +1,73 @@ +<?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\Controller; + +use OCA\Mail\AppInfo\Application; +use OCA\Mail\Service\Provisioning\Manager as ProvisioningManager; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; + +class SettingsController extends Controller { + + /** @var ProvisioningManager */ + private $provisioningManager; + + public function __construct(IRequest $request, + ProvisioningManager $provisioningManager) { + parent::__construct(Application::APP_ID, $request); + $this->provisioningManager = $provisioningManager; + } + + public function provisioning(string $emailTemplate, + string $imapUser, + string $imapHost, + int $imapPort, + string $imapSslMode, + string $smtpUser, + string $smtpHost, + int $smtpPort, + string $smtpSslMode): JSONResponse { + $this->provisioningManager->newProvisioning( + $emailTemplate, + $imapUser, + $imapHost, + $imapPort, + $imapSslMode, + $smtpUser, + $smtpHost, + $smtpPort, + $smtpSslMode + ); + + return new JSONResponse(null); + } + + public function deprovision(): JSONResponse { + $this->provisioningManager->deprovision(); + + return new JSONResponse(null); + } + +} diff --git a/lib/Db/MailAccount.php b/lib/Db/MailAccount.php index 1de82e356..ec09c09aa 100644 --- a/lib/Db/MailAccount.php +++ b/lib/Db/MailAccount.php @@ -45,7 +45,7 @@ use OCP\AppFramework\Db\Entity; * @method void setInboundSslMode(string $inboundSslMode) * @method string getInboundUser() * @method void setInboundUser(string $inboundUser) - * @method string getInboundPassword() + * @method string|null getInboundPassword() * @method void setInboundPassword(string $inboundPassword) * @method string getOutboundHost() * @method void setOutboundHost(string $outboundHost) @@ -55,7 +55,7 @@ use OCP\AppFramework\Db\Entity; * @method void setOutboundSslMode(string $outboundSslMode) * @method string getOutboundUser() * @method void setOutboundUser(string $outboundUser) - * @method string getOutboundPassword() + * @method string|null getOutboundPassword() * @method void setOutboundPassword(string $outboundPassword) * @method string|null getSignature() * @method void setSignature(string|null $signature) @@ -63,6 +63,8 @@ use OCP\AppFramework\Db\Entity; * @method void setLastMailboxSync(int $time) * @method string getEditorMode() * @method void setEditorMode(string $editorMode) + * @method bool getProvisioned() + * @method void setProvisioned(bool $provisioned) */ class MailAccount extends Entity { @@ -82,12 +84,12 @@ class MailAccount extends Entity { protected $signature; protected $lastMailboxSync; protected $editorMode; + protected $provisioned; /** * @param array $params */ public function __construct(array $params=[]) { - if (isset($params['accountId'])) { $this->setId($params['accountId']); } @@ -131,6 +133,7 @@ class MailAccount extends Entity { } $this->addType('lastMailboxSync', 'integer'); + $this->addType('provisioned', 'bool'); } /** @@ -147,6 +150,7 @@ class MailAccount extends Entity { 'imapSslMode' => $this->getInboundSslMode(), 'signature' => $this->getSignature(), 'editorMode' => $this->getEditorMode(), + 'provisioned' => $this->getProvisioned(), ]; if (!is_null($this->getOutboundHost())) { diff --git a/lib/Db/MailAccountMapper.php b/lib/Db/MailAccountMapper.php index 409aa135d..bd10a408f 100644 --- a/lib/Db/MailAccountMapper.php +++ b/lib/Db/MailAccountMapper.php @@ -25,8 +25,12 @@ declare(strict_types=1); namespace OCA\Mail\Db; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; +use OCP\IUser; class MailAccountMapper extends QBMapper { @@ -73,6 +77,24 @@ class MailAccountMapper extends QBMapper { } /** + * @throws DoesNotExistException + * @throws MultipleObjectsReturnedException + */ + public function findProvisionedAccount(IUser $user): MailAccount { + $qb = $this->db->getQueryBuilder(); + + $query = $qb + ->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->eq('user_id', $qb->createNamedParameter($user->getUID())), + $qb->expr()->eq('provisioned', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL)) + ); + + return $this->findEntity($query); + } + + /** * Saves an User Account into the database * * @param MailAccount $account @@ -80,12 +102,20 @@ class MailAccountMapper extends QBMapper { * @return MailAccount */ public function save(MailAccount $account): MailAccount { - if (is_null($account->getId())) { + if ($account->getId() === null) { return $this->insert($account); - } else { - $this->update($account); - return $account; } + + return $this->update($account); + } + + public function deleteProvisionedAccounts(): void { + $qb = $this->db->getQueryBuilder(); + + $delete = $qb->delete($this->getTableName()) + ->where($qb->expr()->eq('provisioned', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))); + + $delete->execute(); } } diff --git a/lib/Http/Middleware/ProvisioningMiddleware.php b/lib/Http/Middleware/ProvisioningMiddleware.php new file mode 100644 index 000000000..73d976042 --- /dev/null +++ b/lib/Http/Middleware/ProvisioningMiddleware.php @@ -0,0 +1,79 @@ +<?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\Http\Middleware; + +use OCA\Mail\Service\Provisioning\Manager as ProvisioningManager; +use OCP\AppFramework\Middleware; +use OCP\Authentication\Exceptions\CredentialsUnavailableException; +use OCP\Authentication\Exceptions\PasswordUnavailableException; +use OCP\Authentication\LoginCredentials\ICredentials; +use OCP\Authentication\LoginCredentials\IStore as ICredentialStore; +use OCP\ILogger; +use OCP\IUserSession; + +class ProvisioningMiddleware extends Middleware { + + /** @var IUserSession */ + private $userSession; + + /** @var ICredentialStore */ + private $credentialStore; + + /** @var ProvisioningManager */ + private $provisioningManager; + + /** @var ILogger */ + private $logger; + + public function __construct(IUserSession $userSession, + ICredentialStore $credentialStore, + ProvisioningManager $provisioningManager, + ILogger $logger) { + $this->userSession = $userSession; + $this->credentialStore = $credentialStore; + $this->provisioningManager = $provisioningManager; + $this->logger = $logger; + } + + public function beforeController($controller, $methodName) { + $user = $this->userSession->getUser(); + if ($user === null) { + // Nothing to update + return; + } + try { + $this->provisioningManager->updatePassword( + $user, + $this->credentialStore->getLoginCredentials()->getPassword() + ); + } catch (CredentialsUnavailableException|PasswordUnavailableException $e) { + // Nothing to update + return; + } + } + +} diff --git a/lib/Migration/MigrateProvisioningConfig.php b/lib/Migration/MigrateProvisioningConfig.php new file mode 100644 index 000000000..7f8bbe29d --- /dev/null +++ b/lib/Migration/MigrateProvisioningConfig.php @@ -0,0 +1,66 @@ +<?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 new file mode 100644 index 000000000..11e14eb1d --- /dev/null +++ b/lib/Migration/ProvisionAccounts.php @@ -0,0 +1,56 @@ +<?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\Migration; + +use OCA\Mail\Service\Provisioning\Manager as ProvisioningManager; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; + +class ProvisionAccounts implements IRepairStep { + + /** @var ProvisioningManager */ + private $provisioningManager; + + public function __construct(ProvisioningManager $provisioningManager) { + $this->provisioningManager = $provisioningManager; + } + + public function getName(): string { + return 'Create or update provisioned Mail accounts'; + } + + public function run(IOutput $output) { + $config = $this->provisioningManager->getConfig(); + if ($config === null) { + $output->info("No Mail provisioning config set"); + return; + } + + $cnt = $this->provisioningManager->provision($config); + $output->info("$cnt accounts provisioned"); + } + +} diff --git a/lib/Migration/Version0100Date20180825194217.php b/lib/Migration/Version0100Date20180825194217.php index e5ff6a1a3..86a86cc8f 100644 --- a/lib/Migration/Version0100Date20180825194217.php +++ b/lib/Migration/Version0100Date20180825194217.php @@ -54,7 +54,6 @@ class Version0100Date20180825194217 extends SimpleMigrationStep { $table->addColumn('user_id', 'string', [ 'notnull' => true, 'length' => 64, - 'default' => '', ]); $table->addColumn('name', 'string', [ 'notnull' => false, diff --git a/lib/Migration/Version0190Date20191118160843.php b/lib/Migration/Version0190Date20191118160843.php new file mode 100644 index 000000000..e66d42d3d --- /dev/null +++ b/lib/Migration/Version0190Date20191118160843.php @@ -0,0 +1,60 @@ +<?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\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\IDBConnection; +use OCP\Migration\SimpleMigrationStep; +use OCP\Migration\IOutput; + +class Version0190Date20191118160843 extends SimpleMigrationStep { + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * + * @return ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $accountsTable = $schema->getTable('mail_accounts'); + $accountsTable->addColumn('provisioned', 'boolean', [ + 'notnull' => true, + 'default' => false, + ]); + $accountsTable->changeColumn('inbound_password', [ + 'notnull' => false, + 'default' => null, + ]); + + return $schema; + } + +} diff --git a/lib/Service/AccountService.php b/lib/Service/AccountService.php index 2a869dee2..6b7d5dcf6 100644 --- a/lib/Service/AccountService.php +++ b/lib/Service/AccountService.php @@ -24,13 +24,12 @@ declare(strict_types=1); namespace OCA\Mail\Service; -use Exception; use OCA\Mail\Account; use OCA\Mail\Db\MailAccount; use OCA\Mail\Db\MailAccountMapper; use OCA\Mail\Exception\ServiceException; -use OCA\Mail\Service\DefaultAccount\Manager; use OCP\AppFramework\Db\DoesNotExistException; +use function array_map; class AccountService { @@ -44,17 +43,12 @@ class AccountService { */ private $accounts; - /** @var Manager */ - private $defaultAccountManager; - /** @var AliasesService */ private $aliasesService; public function __construct(MailAccountMapper $mapper, - Manager $defaultAccountManager, AliasesService $aliasesService) { $this->mapper = $mapper; - $this->defaultAccountManager = $defaultAccountManager; $this->aliasesService = $aliasesService; } @@ -64,16 +58,9 @@ class AccountService { */ public function findByUserId(string $currentUserId): array { if ($this->accounts === null) { - $accounts = array_map(function ($a) { + return $this->accounts = array_map(function ($a) { return new Account($a); - }, $this->mapper->findByUserId($currentUserId)); - - $defaultAccount = $this->defaultAccountManager->getDefaultAccount(); - if (!is_null($defaultAccount)) { - $accounts[] = new Account($defaultAccount); - } - - $this->accounts = $accounts; + }, $this->mapper->findByUserId($currentUserId));; } return $this->accounts; @@ -96,13 +83,6 @@ class AccountService { throw new DoesNotExistException("Invalid account id <$accountId>"); } - if ($accountId === Manager::ACCOUNT_ID) { - $defaultAccount = $this->defaultAccountManager->getDefaultAccount(); - if (is_null($defaultAccount)) { - throw new DoesNotExistException('Default account config missing'); - } - return new Account($defaultAccount); - } return new Account($this->mapper->find($uid, $accountId)); } @@ -110,10 +90,6 @@ class AccountService { * @param int $accountId */ public function delete(string $currentUserId, int $accountId): void { - if ($accountId === Manager::ACCOUNT_ID) { - return; - } - $mailAccount = $this->mapper->find($currentUserId, $accountId); $this->aliasesService->deleteAll($accountId); $this->mapper->delete($mailAccount); diff --git a/lib/Service/DefaultAccount/Manager.php b/lib/Service/DefaultAccount/Manager.php deleted file mode 100644 index a188e0fdb..000000000 --- a/lib/Service/DefaultAccount/Manager.php +++ /dev/null @@ -1,127 +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\DefaultAccount; - -use OCA\Mail\Db\MailAccount; -use OCP\Authentication\Exceptions\CredentialsUnavailableException; -use OCP\Authentication\LoginCredentials\IStore; -use OCP\IConfig; -use OCP\ILogger; -use OCP\IUserSession; -use OCP\Security\ICrypto; - -class Manager { - - const ACCOUNT_ID = -2; - - /** @var IConfig */ - private $config; - - /** @var IStore */ - private $credentialStore; - - /** @var ILogger */ - private $logger; - - /** @var IUserSession */ - private $userSession; - - /** @var ICrypto */ - private $crypto; - - /** - * @param IConfig $config - * @param IStore $credentialStore - * @param ILogger $logger - * @param IUserSession $userSession - * @param ICrypto $crypto - */ - public function __construct(IConfig $config, - IStore $credentialStore, - ILogger $logger, - IUserSession $userSession, - ICrypto $crypto) { - $this->config = $config; - $this->logger = $logger; - $this->userSession = $userSession; - $this->crypto = $crypto; - $this->credentialStore = $credentialStore; - } - - /** - * @return Config|null - */ - private function getConfig() { - $config = $this->config->getSystemValue('app.mail.accounts.default', null); - if (is_null($config)) { - $this->logger->debug('no default config found'); - return null; - } else { - $this->logger->debug('default config to create a default account found'); - // TODO: check if config is complete - return new Config($config); - } - } - - /** - * @return MailAccount|null - */ - public function getDefaultAccount() { - $config = $this->getConfig(); - if (is_null($config)) { - return null; - } - try { - $credentials = $this->credentialStore->getLoginCredentials(); - } catch (CredentialsUnavailableException $ex) { - $this->logger->debug('login credentials not available for default account'); - return null; - } - - $user = $this->userSession->getUser(); - $password = $this->crypto->encrypt($credentials->getPassword()); - $this->logger->info('building default account for user ' . $user->getUID()); - - $account = new MailAccount(); - $account->setId(self::ACCOUNT_ID); - $account->setUserId($user->getUID()); - $account->setEmail($config->buildEmail($user)); - $account->setName($user->getDisplayName()); - - $account->setInboundUser($config->buildImapUser($user)); - $account->setInboundHost($config->getImapHost()); - $account->setInboundPort($config->getImapPort()); - $account->setInboundSslMode($config->getImapSslMode()); - $account->setInboundPassword($password); - - $account->setOutboundUser($config->buildSmtpUser($user)); - $account->setOutboundHost($config->getSmtpHost()); - $account->setOutboundPort($config->getSmtpPort()); - $account->setOutboundSslMode($config->getSmtpSslMode()); - $account->setOutboundPassword($password); - - return $account; - } - -} diff --git a/lib/Service/DefaultAccount/Config.php b/lib/Service/Provisioning/Config.php index 4a95929a2..b6a67191e 100644 --- a/lib/Service/DefaultAccount/Config.php +++ b/lib/Service/Provisioning/Config.php @@ -21,12 +21,16 @@ declare(strict_types=1); * */ -namespace OCA\Mail\Service\DefaultAccount; +namespace OCA\Mail\Service\Provisioning; +use JsonSerializable; use OCP\IUser; -class Config { +class Config implements JsonSerializable { + private const VERSION = 1; + + /** @var string[] */ private $data; /** @@ -115,13 +119,28 @@ class Config { * @return string */ private function buildUserEmail(string $original, IUser $user) { - if (!is_null($user->getUID())) { + if ($user->getUID() !== null) { $original = str_replace('%USERID%', $user->getUID(), $original); } - if (!is_null($user->getEMailAddress())) { + 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 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 new file mode 100644 index 000000000..5aef20f8f --- /dev/null +++ b/lib/Service/Provisioning/ConfigMapper.php @@ -0,0 +1,62 @@ +<?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 new file mode 100644 index 000000000..60fafc332 --- /dev/null +++ b/lib/Service/Provisioning/Manager.php @@ -0,0 +1,183 @@ +<?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 OCA\Mail\Db\MailAccount; +use OCA\Mail\Db\MailAccountMapper; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\MultipleObjectsReturnedException; +use OCP\IDBConnection; +use OCP\ILogger; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Security\ICrypto; + +class Manager { + + /** @var IUserManager */ + private $userManager; + + /** @var ConfigMapper */ + private $configMapper; + + /** @var MailAccountMapper */ + private $mailAccountMapper; + + /** @var ICrypto */ + private $crypto; + + /** @var ILogger */ + private $logger; + + public function __construct(IUserManager $userManager, + ConfigMapper $configMapper, + MailAccountMapper $mailAccountMapper, + ICrypto $crypto, + ILogger $logger) { + $this->userManager = $userManager; + $this->configMapper = $configMapper; + $this->mailAccountMapper = $mailAccountMapper; + $this->crypto = $crypto; + $this->logger = $logger; + } + + public function getConfig(): ?Config { + return $this->configMapper->load(); + } + + public function provision(Config $config): int { + $cnt = 0; + $this->userManager->callForAllUsers(function (IUser $user) use ($config, &$cnt) { + $this->provisionSingleUser($config, $user); + $cnt++; + }); + return $cnt; + } + + public function provisionSingleUser(Config $config, IUser $user): void { + try { + $existing = $this->mailAccountMapper->findProvisionedAccount($user); + + $this->mailAccountMapper->update( + $this->updateAccount($user, $existing, $config) + ); + } catch (DoesNotExistException $e) { + // Fine, then we create a new one + $new = new MailAccount(); + $new->setUserId($user->getUID()); + if ($user->getDisplayName() !== $user->getUID()) { + // Only set if it's something meaningful + $new->setName($user->getDisplayName()); + } + $new->setProvisioned(true); + + $this->mailAccountMapper->insert( + $this->updateAccount($user, $new, $config) + ); + } + } + + public function newProvisioning(string $email, + string $imapUser, + string $imapHost, + int $imapPort, + string $imapSslMode, + string $smtpUser, + string $smtpHost, + int $smtpPort, + string $smtpSslMode): 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, + ])); + + $this->provision($config); + } + + private function updateAccount(IUser $user, MailAccount $account, Config $config): MailAccount { + $account->setEmail($config->buildEmail($user)); + $account->setInboundUser($config->buildImapUser($user)); + $account->setInboundHost($config->getImapHost()); + $account->setInboundPort($config->getImapPort()); + $account->setInboundSslMode($config->getImapSslMode()); + $account->setOutboundUser($config->buildSmtpUser($user)); + $account->setOutboundHost($config->getSmtpHost()); + $account->setOutboundPort($config->getSmtpPort()); + $account->setOutboundSslMode($config->getSmtpSslMode()); + + 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']; + } + + return $this->configMapper->save(new Config($data)); + } + + public function updatePassword(IUser $user, string $password): void { + try { + $account = $this->mailAccountMapper->findProvisionedAccount($user); + + if ($account->getInboundPassword() !== null + && $this->crypto->decrypt($account->getInboundPassword()) === $password + && $account->getOutboundPassword() !== null + && $this->crypto->decrypt($account->getOutboundPassword()) === $password) { + $this->logger->debug('Password of provisioned account is up to date'); + return; + } + + $account->setInboundPassword($this->crypto->encrypt($password)); + $account->setOutboundPassword($this->crypto->encrypt($password)); + $this->mailAccountMapper->update($account); + + $this->logger->debug('Provisioned account password udpated'); + } catch (DoesNotExistException $e) { + // Nothing to update + } + } + +} diff --git a/lib/Settings/AdminSettings.php b/lib/Settings/AdminSettings.php new file mode 100644 index 000000000..582532471 --- /dev/null +++ b/lib/Settings/AdminSettings.php @@ -0,0 +1,78 @@ +<?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\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; +use OCP\Settings\ISettings; + +class AdminSettings implements ISettings { + + /** @var IInitialStateService */ + private $initialStateService; + + /** @var ProvisioningManager */ + private $provisioningManager; + + public function __construct(IInitialStateService $initialStateService, + ProvisioningManager $provisioningManager) { + $this->initialStateService = $initialStateService; + $this->provisioningManager = $provisioningManager; + } + + public function getForm() { + $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', + ]) + ); + + return new TemplateResponse(Application::APP_ID, 'settings-admin'); + } + + public function getSection() { + return 'groupware'; + } + + public function getPriority() { + return 90; + } + +} diff --git a/src/components/NavigationAccount.vue b/src/components/NavigationAccount.vue index 023ce9447..3c9a2fe81 100644 --- a/src/components/NavigationAccount.vue +++ b/src/components/NavigationAccount.vue @@ -37,7 +37,7 @@ <ActionRouter :to="settingsRoute" icon="icon-settings"> {{ t('mail', 'Edit account') }} </ActionRouter> - <ActionButton icon="icon-delete" @click="deleteAccount"> + <ActionButton v-if="!account.provisioned" icon="icon-delete" @click="deleteAccount"> {{ t('mail', 'Delete account') }} </ActionButton> <ActionInput icon="icon-add" @submit="createFolder"> diff --git a/src/components/settings/AdminSettings.vue b/src/components/settings/AdminSettings.vue new file mode 100644 index 000000000..e93acd457 --- /dev/null +++ b/src/components/settings/AdminSettings.vue @@ -0,0 +1,53 @@ +<!-- + - @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/>. + --> + +<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> + {{ + 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> +</template> + +<script> +import ProvisioningSettings from './ProvisioningSettings' + +export default { + name: 'AdminSettings', + components: { + ProvisioningSettings, + }, + props: { + provisioningSettings: { + type: Object, + required: true, + }, + }, +} +</script> diff --git a/src/components/settings/ProvisionPreview.vue b/src/components/settings/ProvisionPreview.vue new file mode 100644 index 000000000..bf7cf391a --- /dev/null +++ b/src/components/settings/ProvisionPreview.vue @@ -0,0 +1,99 @@ +<!-- + - @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/>. + --> + +<template> + <div class="provision-preview"> + <b> + <span v-if="data.uid">uid={{ data.uid }}</span> + <span v-if="data.email">email={{ email }}</span> + </b> + <br /> + {{ t('mail', 'Email: {email}', {email}) }}<br /> + {{ + t('mail', 'IMAP: {user} on {host}:{port} ({ssl} encryption)', { + user: imapUser, + host: imapHost, + port: imapPort, + ssl: imapSslMode, + }) + }}<br /> + {{ + t('mail', 'SMTP: {user} on {host}:{port} ({ssl} encryption)', { + user: smtpUser, + host: smtpHost, + port: smtpPort, + ssl: smtpSslMode, + }) + }}<br /> + </div> +</template> + +<script> +export default { + name: 'ProvisionPreview', + props: { + data: { + type: Object, + required: true, + }, + templates: { + type: Object, + required: true, + }, + }, + computed: { + email() { + return this.templates.email.replace('%USERID%', this.data.uid).replace('%EMAIL%', this.data.email) + }, + imapHost() { + return this.templates.imapHost + }, + imapPort() { + return this.templates.imapPort + }, + imapSslMode() { + return this.templates.imapSslMode + }, + imapUser() { + return this.templates.imapUser.replace('%USERID%', this.data.uid).replace('%EMAIL%', this.data.email) + }, + smtpHost() { + return this.templates.smtpHost + }, + smtpPort() { + return this.templates.smtpPort + }, + smtpSslMode() { + return this.templates.smtpSslMode + }, + smtpUser() { + return this.templates.smtpUser.replace('%USERID%', this.data.uid).replace('%EMAIL%', this.data.email) + }, + }, +} +</script> + +<style lang="scss" scoped> +.provision-preview { + border: 1px solid var(--color-border-dark); + border-radius: var(--border-radius); +} +</style> diff --git a/src/components/settings/ProvisioningSettings.vue b/src/components/settings/ProvisioningSettings.vue new file mode 100644 index 000000000..07027b861 --- /dev/null +++ b/src/components/settings/ProvisioningSettings.vue @@ -0,0 +1,428 @@ +<!-- + - @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/>. + --> + +<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="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 /> + <input + id="mail-provision-email" + v-model="emailTemplate" + :disabled="loading" + name="email" + type="text" + /> + </div> + </div> + <div class="settings-group"> + <div class="group-title">{{ t('mail', 'IMAP') }}</div> + <div class="group-inputs"> + <label for="mail-provision-imap-user"> + {{ t('mail', 'User') }}* + <br /> + <input + id="mail-provision-imap-user" + v-model="imapUser" + :disabled="loading" + name="email" + type="text" + /> + </label> + <div class="flex-row"> + <label for="mail-provision-imap-host"> + {{ t('mail', 'Host') }} + <br /> + <input + id="mail-provision-imap-host" + v-model="imapHost" + :disabled="loading" + name="email" + type="text" + /> + </label> + <label for="mail-provision-imap-port"> + {{ t('mail', 'Port') }} + <br /> + <input + id="mail-provision-imap-port" + v-model="imapPort" + :disabled="loading" + name="email" + type="number" + /> + </label> + </div> + <div class="flex-row"> + <input + id="mail-provision-imap-user-none" + v-model="imapSslMode" + type="radio" + name="man-imap-sec" + :disabled="loading" + value="none" + /> + <label + class="button" + for="mail-provision-imap-user-none" + :class="{primary: imapSslMode === 'none'}" + >{{ t('mail', 'None') }}</label + > + <input + id="mail-provision-imap-user-ssl" + v-model="imapSslMode" + type="radio" + name="man-imap-sec" + :disabled="loading" + value="ssl" + /> + <label + class="button" + for="mail-provision-imap-user-ssl" + :class="{primary: imapSslMode === 'ssl'}" + >{{ t('mail', 'SSL/TLS') }}</label + > + <input + id="mail-provision-imap-user-tls" + v-model="imapSslMode" + type="radio" + name="man-imap-sec" + :disabled="loading" + value="tls" + /> + <label + class="button" + for="mail-provision-imap-user-tls" + :class="{primary: imapSslMode === 'tls'}" + >{{ t('mail', 'STARTTLS') }}</label + > + </div> + </div> + </div> + <div class="settings-group"> + <div class="group-title">{{ t('mail', 'SMTP') }}</div> + <div class="group-inputs"> + <label for="mail-provision-smtp-user"> + {{ t('mail', 'User') }}* + <br /> + <input + id="mail-provision-smtp-user" + v-model="smtpUser" + :disabled="loading" + name="email" + type="text" + /> + </label> + <div class="flex-row"> + <label for="mail-provision-imap-host"> + {{ t('mail', 'Host') }} + <br /> + <input + id="mail-provision-smtp-host" + v-model="smtpHost" + :disabled="loading" + name="email" + type="text" + /> + </label> + <label for="mail-provision-smtp-port"> + {{ t('mail', 'Port') }} + <br /> + <input + id="mail-provision-smtp-port" + v-model="smtpPort" + :disabled="loading" + name="email" + type="number" + /> + </label> + </div> + <div class="flex-row"> + <input + id="mail-provision-smtp-user-none" + v-model="smtpSslMode" + type="radio" + name="man-smtp-sec" + :disabled="loading" + value="none" + /> + <label + class="button" + for="mail-provision-smtp-user-none" + :class="{primary: smtpSslMode === 'none'}" + >{{ t('mail', 'None') }}</label + > + <input + id="mail-provision-smtp-user-ssl" + v-model="smtpSslMode" + type="radio" + name="man-smtp-sec" + :disabled="loading" + value="ssl" + /> + <label + class="button" + for="mail-provision-smtp-user-ssl" + :class="{primary: smtpSslMode === 'ssl'}" + >{{ t('mail', 'SSL/TLS') }}</label + > + <input + id="mail-provision-smtp-user-tls" + v-model="smtpSslMode" + type="radio" + name="man-smtp-sec" + :disabled="loading" + value="tls" + /> + <label + class="button" + for="mail-provision-smtp-user-tls" + :class="{primary: smtpSslMode === 'tls'}" + >{{ t('mail', 'STARTTLS') }}</label + > + </div> + </div> + </div> + <div class="settings-group"> + <div class="group-title"></div> + <div class="group-inputs"> + <input + type="submit" + class="primary" + :disabled="loading" + :value="t('mail', 'Apply and create/update for all users')" + /> + <input + type="button" + :disabled="loading" + :value="t('mail', 'Disable and un-provision existing accounts')" + @click="disable" + /> + <br /> + <small>{{ + t('mail', "* %USERID% and %EMAIL% will be replaced with the user's UID and email") + }}</small> + </div> + </div> + </form> + <div> + <h4>Preview</h4> + <p> + {{ + t('mail', 'With the settings above, the app will create account settings in the following way:') + }} + </p> + <div class="previews"> + <ProvisionPreview class="preview-item" :templates="previewTemplates" :data="previewData1" /> + <ProvisionPreview class="preview-item" :templates="previewTemplates" :data="previewData2" /> + </div> + </div> + </div> + </div> +</template> + +<script> +import logger from '../../logger' +import ProvisionPreview from './ProvisionPreview' +import {disableProvisioning, saveProvisioningSettings} from '../../service/SettingsService' + +export default { + name: 'ProvisioningSettings', + components: {ProvisionPreview}, + props: { + settings: { + type: Object, + required: 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', + previewData1: { + uid: 'user123', + email: '', + }, + previewData2: { + uid: 'user321', + email: 'user@domain.com', + }, + loading: false, + } + }, + computed: { + previewTemplates() { + return { + email: 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, + } + }, + }, + beforeMount() { + logger.debug('provisioning settings loaded', {settings: this.settings}) + }, + methods: { + submit() { + 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, + }) + .then(() => { + logger.info('provisioning settings updated') + }) + .catch(error => { + // TODO: show user feedback + logger.error('Could not save provisioning settings', {error}) + }) + .then(() => { + this.loading = false + }) + }, + disable() { + 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 + }) + }, + }, +} +</script> + +<style lang="scss" scoped> +.form-preview-row { + display: flex; + flex-direction: row; + flex-wrap: wrap; + + div:last-child { + margin-top: 10px; + } +} + +.settings-group { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + + .group-title { + min-width: 100px; + text-align: right; + margin: 10px; + font-weight: bold; + } + .group-inputs { + margin: 10px; + flex-grow: 1; + + input[type='text'] { + min-width: 200px; + } + } +} + +h4 { + font-weight: bold; +} + +.previews { + display: flex; + flex-direction: row; + flex-wrap: wrap; + margin: 0 -10px; + + .preview-item { + flex-grow: 1; + margin: 10px; + padding: 25px; + } +} +input[type='radio'] { + display: none; +} + +.flex-row { + display: flex; +} + +form { + label { + color: var(--color-text-maxcontrast); + } +} +</style> diff --git a/src/main-settings.js b/src/main-settings.js new file mode 100644 index 000000000..17c222193 --- /dev/null +++ b/src/main-settings.js @@ -0,0 +1,40 @@ +/* + * @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/>. + */ + +import {generateFilePath} from '@nextcloud/router' +import {getRequestToken} from '@nextcloud/auth' +import {loadState} from '@nextcloud/initial-state' +import Vue from 'vue' + +import AdminSettings from './components/settings/AdminSettings' +import Nextcloud from './mixins/Nextcloud' + +__webpack_nonce__ = btoa(getRequestToken()) +__webpack_public_path__ = generateFilePath('mail', '', 'js/') + +Vue.mixin(Nextcloud) + +const View = Vue.extend(AdminSettings) +new View({ + propsData: { + provisioningSettings: loadState('mail', 'provisioning_settings') || {}, + }, +}).$mount('#mail-admin-settings') diff --git a/src/main.js b/src/main.js index b80824a8b..064e04a35 100644 --- a/src/main.js +++ b/src/main.js @@ -21,16 +21,16 @@ */ import Vue from 'vue' -import App from './App' import {getRequestToken} from '@nextcloud/auth' -import router from './router' -import store from './store' import {sync} from 'vuex-router-sync' -import {translate, translatePlural} from '@nextcloud/l10n' import {generateFilePath} from '@nextcloud/router' import VueShortKey from 'vue-shortkey' import VTooltip from 'v-tooltip' +import App from './App' +import Nextcloud from './mixins/Nextcloud' +import router from './router' +import store from './store' import {fixAccountId} from './service/AccountService' __webpack_nonce__ = btoa(getRequestToken()) @@ -38,12 +38,7 @@ __webpack_public_path__ = generateFilePath('mail', '', 'js/') sync(store, router) -Vue.mixin({ - methods: { - t: translate, - n: translatePlural, - }, -}) +Vue.mixin(Nextcloud) Vue.use(VueShortKey, {prevent: ['input', 'div']}) Vue.use(VTooltip) diff --git a/src/mixins/Nextcloud.js b/src/mixins/Nextcloud.js new file mode 100644 index 000000000..2b676c866 --- /dev/null +++ b/src/mixins/Nextcloud.js @@ -0,0 +1,29 @@ +/* + * @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/>. + */ + +import {translate as t, translatePlural as n} from '@nextcloud/l10n' + +export default { + methods: { + t, + n, + }, +} diff --git a/src/service/SettingsService.js b/src/service/SettingsService.js new file mode 100644 index 000000000..e83cc3bf2 --- /dev/null +++ b/src/service/SettingsService.js @@ -0,0 +1,35 @@ +/* + * @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/>. + */ + +import axios from '@nextcloud/axios' +import {generateUrl} from '@nextcloud/router' + +export const saveProvisioningSettings = config => { + const url = generateUrl('/apps/mail/api/settings/provisioning') + + return axios.post(url, config).then(resp => resp.data) +} + +export const disableProvisioning = () => { + const url = generateUrl('/apps/mail/api/settings/provisioning') + + return axios.delete(url).then(resp => resp.data) +} diff --git a/src/views/AccountSettings.vue b/src/views/AccountSettings.vue index 4a3266c0c..9ddd15c88 100644 --- a/src/views/AccountSettings.vue +++ b/src/views/AccountSettings.vue @@ -6,12 +6,17 @@ <h2>{{ t('mail', 'Account settings') }}</h2> <p> <strong>{{ displayName }}</strong> <{{ email }}> - <a class="button icon-rename" href="#account-form" :title="t('mail', 'Change name')"></a> + <a + v-if="!account.provisioned" + class="button icon-rename" + href="#account-form" + :title="t('mail', 'Change name')" + ></a> </p> </div> <SignatureSettings :account="account" /> <EditorSettings :account="account" /> - <div class="section"> + <div v-if="!account.provisioned" class="section"> <h2>{{ t('mail', 'Mail server') }}</h2> <div id="mail-settings"> <AccountForm diff --git a/templates/settings-admin.php b/templates/settings-admin.php new file mode 100644 index 000000000..ca9b2feb1 --- /dev/null +++ b/templates/settings-admin.php @@ -0,0 +1,28 @@ +<?php + +/** + * @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/>. + */ + +script(\OCA\Mail\AppInfo\Application::APP_ID, 'settings'); + +?> +<div id="mail-admin-settings"> +</div> diff --git a/tests/Integration/Service/MailTransmissionIntegrationTest.php b/tests/Integration/Service/MailTransmissionIntegrationTest.php index 8c7cd1837..3261ef44e 100644 --- a/tests/Integration/Service/MailTransmissionIntegrationTest.php +++ b/tests/Integration/Service/MailTransmissionIntegrationTest.php @@ -64,12 +64,14 @@ class MailTransmissionIntegrationTest extends TestCase { parent::setUp(); $this->resetImapAccount(); + $this->user = $this->createTestUser(); /** @var ICrypto $crypo */ $crypo = OC::$server->getCrypto(); /** @var MailAccountMapper $mapper */ $mapper = OC::$server->query(MailAccountMapper::class); $mailAccount = MailAccount::fromParams([ + 'userId' => $this->user->getUID(), 'email' => 'user@domain.tld', 'inboundHost' => 'localhost', 'inboundPort' => '993', @@ -86,7 +88,6 @@ class MailTransmissionIntegrationTest extends TestCase { $this->account = new Account($mailAccount); $this->attachmentService = OC::$server->query(IAttachmentService::class); - $this->user = $this->createTestUser(); $userFolder = OC::$server->getUserFolder($this->user->getUID()); $this->transmission = new MailTransmission( $userFolder, diff --git a/tests/Unit/Controller/SettingsControllerTest.php b/tests/Unit/Controller/SettingsControllerTest.php new file mode 100644 index 000000000..4e3280c11 --- /dev/null +++ b/tests/Unit/Controller/SettingsControllerTest.php @@ -0,0 +1,86 @@ +<?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\Controller; + +use ChristophWurst\Nextcloud\Testing\ServiceMockObject; +use OCA\Mail\Controller\SettingsController; +use OCA\Mail\Tests\Integration\TestCase; +use OCP\AppFramework\Http\JSONResponse; + +class SettingsControllerTest extends TestCase { + + /** @var ServiceMockObject */ + private $mock; + + /** @var SettingsController */ + private $controller; + + protected function setUp(): void { + parent::setUp(); + + $this->mock = $this->createServiceMock(SettingsController::class); + $this->controller = $this->mock->getService(); + } + + 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' + ); + + $response = $this->controller->provisioning( + '%USERID%@domain.com', + '%USERID%@domain.com', + 'mx.domain.com', + 993, + 'ssl', + '%USERID%@domain.com', + 'mx.domain.com', + 567, + 'tls' + ); + + $this->assertInstanceOf(JSONResponse::class, $response); + } + + public function testDeprovision() { + $this->mock->getParameter('provisioningManager') + ->expects($this->once()) + ->method('deprovision'); + + $response = $this->controller->deprovision(); + + $this->assertInstanceOf(JSONResponse::class, $response); + } +} diff --git a/tests/Unit/Db/MailAccountTest.php b/tests/Unit/Db/MailAccountTest.php index 3c6d01558..a4d1d7fcc 100644 --- a/tests/Unit/Db/MailAccountTest.php +++ b/tests/Unit/Db/MailAccountTest.php @@ -42,6 +42,7 @@ class MailAccountTest extends TestCase { $a->setOutboundPassword('xxxx'); $a->setOutboundSslMode('ssl'); $a->setEditorMode('html'); + $a->setProvisioned(false); $this->assertEquals(array( 'accountId' => 12345, @@ -57,6 +58,7 @@ class MailAccountTest extends TestCase { 'smtpSslMode' => 'ssl', 'signature' => null, 'editorMode' => 'html', + 'provisioned' => false, ), $a->toJson()); } @@ -75,6 +77,7 @@ class MailAccountTest extends TestCase { 'smtpSslMode' => 'ssl', 'signature' => null, 'editorMode' => null, + 'provisioned' => false, ]; $a = new MailAccount($expected); // TODO: fix inconsistency diff --git a/tests/Unit/Http/Middleware/ProvisioningMiddlewareTest.php b/tests/Unit/Http/Middleware/ProvisioningMiddlewareTest.php new file mode 100644 index 000000000..1fac60021 --- /dev/null +++ b/tests/Unit/Http/Middleware/ProvisioningMiddlewareTest.php @@ -0,0 +1,147 @@ +<?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\Http\Middleware; + +use ChristophWurst\Nextcloud\Testing\TestCase; +use OCA\Mail\Controller\PageController; +use OCA\Mail\Http\Middleware\ProvisioningMiddleware; +use OCA\Mail\Service\Provisioning\Manager; +use OCP\Authentication\Exceptions\CredentialsUnavailableException; +use OCP\Authentication\Exceptions\PasswordUnavailableException; +use OCP\Authentication\LoginCredentials\ICredentials; +use OCP\Authentication\LoginCredentials\IStore; +use OCP\ILogger; +use OCP\IUser; +use OCP\IUserSession; +use PHPUnit\Framework\MockObject\MockObject; + +class ProvisioningMiddlewareTest extends TestCase { + + /** @var IUserSession|MockObject */ + private $userSession; + + /** @var IStore|MockObject */ + private $credentialStore; + + /** @var Manager|MockObject */ + private $provisioningManager; + + /** @var ILogger|MockObject */ + private $logger; + + /** @var ProvisioningMiddleware */ + private $middleware; + + protected function setUp(): void { + parent::setUp(); + + $this->userSession = $this->createMock(IUserSession::class); + $this->credentialStore = $this->createMock(IStore::class); + $this->provisioningManager = $this->createMock(Manager::class); + $this->logger = $this->createMock(ILogger::class); + + $this->middleware = new ProvisioningMiddleware( + $this->userSession, + $this->credentialStore, + $this->provisioningManager, + $this->logger + ); + } + + public function testBeforeControllerNotLoggedIn() { + $this->credentialStore->expects($this->never()) + ->method('getLoginCredentials'); + $this->provisioningManager->expects($this->never()) + ->method('updatePassword'); + + $this->middleware->beforeController( + $this->createMock(PageController::class), + 'index' + ); + } + + public function testBeforeControllerNoCredentialsAvailable() { + $user = $this->createMock(IUser::class); + $this->userSession->expects($this->once()) + ->method('getUser') + ->willReturn($user); + $this->credentialStore->expects($this->once()) + ->method('getLoginCredentials') + ->willThrowException($this->createMock(CredentialsUnavailableException::class)); + $this->provisioningManager->expects($this->never()) + ->method('updatePassword'); + + $this->middleware->beforeController( + $this->createMock(PageController::class), + 'index' + ); + } + + public function testBeforeControllerNoPasswordAvailable() { + $user = $this->createMock(IUser::class); + $this->userSession->expects($this->once()) + ->method('getUser') + ->willReturn($user); + $credentials = $this->createMock(ICredentials::class); + $this->credentialStore->expects($this->once()) + ->method('getLoginCredentials') + ->willReturn($credentials); + $credentials->expects($this->once()) + ->method('getPassword') + ->willThrowException($this->createMock(PasswordUnavailableException::class)); + $this->provisioningManager->expects($this->never()) + ->method('updatePassword'); + + $this->middleware->beforeController( + $this->createMock(PageController::class), + 'index' + ); + } + + public function testBeforeController() { + $user = $this->createMock(IUser::class); + $this->userSession->expects($this->once()) + ->method('getUser') + ->willReturn($user); + $credentials = $this->createMock(ICredentials::class); + $this->credentialStore->expects($this->once()) + ->method('getLoginCredentials') + ->willReturn($credentials); + $credentials->expects($this->once()) + ->method('getPassword') + ->willReturn('123456'); + $this->provisioningManager->expects($this->once()) + ->method('updatePassword') + ->with( + $user, + '123456' + ); + + $this->middleware->beforeController( + $this->createMock(PageController::class), + 'index' + ); + } + +} diff --git a/tests/Unit/Migration/MigrateProvisioningConfigTest.php b/tests/Unit/Migration/MigrateProvisioningConfigTest.php new file mode 100644 index 000000000..b75d1bc06 --- /dev/null +++ b/tests/Unit/Migration/MigrateProvisioningConfigTest.php @@ -0,0 +1,106 @@ +<?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/AccountServiceTest.php b/tests/Unit/Service/AccountServiceTest.php index 887659b21..a07be412a 100644 --- a/tests/Unit/Service/AccountServiceTest.php +++ b/tests/Unit/Service/AccountServiceTest.php @@ -27,44 +27,42 @@ use OCA\Mail\Db\MailAccount; use OCA\Mail\Db\MailAccountMapper; use OCA\Mail\Service\AccountService; use OCA\Mail\Service\AliasesService; -use OCA\Mail\Service\DefaultAccount\Manager; use OCP\IL10N; -use PHPUnit_Framework_MockObject_MockObject; +use PHPUnit\Framework\MockObject\MockObject; class AccountServiceTest extends TestCase { /** @var string */ private $user = 'herbert'; - /** @var MailAccountMapper|PHPUnit_Framework_MockObject_MockObject */ + /** @var MailAccountMapper|MockObject */ private $mapper; - /** @var IL10N|PHPUnit_Framework_MockObject_MockObject */ + /** @var IL10N|MockObject */ private $l10n; - /** @var AccountService|PHPUnit_Framework_MockObject_MockObject */ + /** @var AccountService|MockObject */ private $accountService; - /** @var AliasesService|PHPUnit_Framework_MockObject_MockObject */ + /** @var AliasesService|MockObject */ private $aliasesService; - /** @var MailAccount|PHPUnit_Framework_MockObject_MockObject */ + /** @var MailAccount|MockObject */ private $account1; - /** @var MailAccount|PHPUnit_Framework_MockObject_MockObject */ + /** @var MailAccount|MockObject */ private $account2; - /** @var Manager|PHPUnit_Framework_MockObject_MockObject */ - private $defaultAccountManager; - protected function setUp(): void { parent::setUp(); $this->mapper = $this->createMock(MailAccountMapper::class); $this->l10n = $this->createMock(IL10N::class); - $this->defaultAccountManager = $this->createMock(Manager::class); $this->aliasesService = $this->createMock(AliasesService::class); - $this->accountService = new AccountService($this->mapper, $this->defaultAccountManager, $this->aliasesService); + $this->accountService = new AccountService( + $this->mapper, + $this->aliasesService + ); $this->account1 = $this->createMock(MailAccount::class); $this->account2 = $this->createMock(MailAccount::class); diff --git a/tests/Unit/Service/DefaultAccount/ManagerTest.php b/tests/Unit/Service/DefaultAccount/ManagerTest.php deleted file mode 100644 index 4e2ab7bbb..000000000 --- a/tests/Unit/Service/DefaultAccount/ManagerTest.php +++ /dev/null @@ -1,154 +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\DefaultAccount; - -use ChristophWurst\Nextcloud\Testing\TestCase; -use OCA\Mail\Db\MailAccount; -use OCA\Mail\Service\DefaultAccount\Manager; -use OCP\Authentication\Exceptions\CredentialsUnavailableException; -use OCP\Authentication\LoginCredentials\ICredentials; -use OCP\Authentication\LoginCredentials\IStore; -use OCP\IConfig; -use OCP\ILogger; -use OCP\IUser; -use OCP\IUserSession; -use OCP\Security\ICrypto; -use PHPUnit_Framework_MockObject_MockObject; - -class ManagerTest extends TestCase { - - /** @var IConfig|PHPUnit_Framework_MockObject_MockObject */ - private $config; - - /** @var IStore|PHPUnit_Framework_MockObject_MockObject */ - private $credentialStore; - - /** @var ILogger|PHPUnit_Framework_MockObject_MockObject */ - private $logger; - - /** @var IUserSession|PHPUnit_Framework_MockObject_MockObject */ - private $userSession; - - /** @var ICrypto|PHPUnit_Framework_MockObject_MockObject */ - private $crypto; - - /** @var Manager|PHPUnit_Framework_MockObject_MockObject */ - private $manager; - - protected function setUp(): void { - parent::setUp(); - - $this->config = $this->createMock(IConfig::class); - $this->credentialStore = $this->createMock(IStore::class); - $this->logger = $this->createMock(ILogger::class); - $this->userSession = $this->createMock(IUserSession::class); - $this->crypto = $this->createMock(ICrypto::class); - - $this->manager = new Manager($this->config, $this->credentialStore, $this->logger, $this->userSession, $this->crypto); - } - - public function testGetDefaultAccountWithoutConfigAvailble() { - $this->config->expects($this->once()) - ->method('getSystemValue') - ->with($this->equalTo('app.mail.accounts.default'), $this->equalTo(null)) - ->willReturn(null); - - $account = $this->manager->getDefaultAccount(); - - $this->assertSame(null, $account); - } - - public function testGetDefaultAccountWithCredentialsUnavailable() { - $this->config->expects($this->once()) - ->method('getSystemValue') - ->with($this->equalTo('app.mail.accounts.default'), $this->equalTo(null)) - ->willReturn([ - 'email' => '%EMAIL%', - ]); - $this->credentialStore->expects($this->once()) - ->method('getLoginCredentials') - ->willThrowException(new CredentialsUnavailableException()); - - $account = $this->manager->getDefaultAccount(); - - $this->assertSame(null, $account); - } - - public function testGetDefaultAccount() { - $this->config->expects($this->once()) - ->method('getSystemValue') - ->with($this->equalTo('app.mail.accounts.default'), $this->equalTo(null)) - ->willReturn([ - 'email' => '%EMAIL%', - 'imapHost' => 'imap.domain.tld', - 'imapPort' => 993, - 'imapSslMode' => 'ssl', - 'smtpHost' => 'smtp.domain.tld', - 'smtpPort' => 465, - 'smtpSslMode' => 'tls', - ]); - $credentials = $this->createMock(ICredentials::class); - $user = $this->createMock(IUser::class); - $this->userSession->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $this->credentialStore->expects($this->once()) - ->method('getLoginCredentials') - ->willReturn($credentials); - $credentials->expects($this->once()) - ->method('getPassword') - ->willReturn('123456'); - $this->crypto->expects($this->once()) - ->method('encrypt') - ->with($this->equalTo('123456')) - ->willReturn('encrypted'); - $expected = new MailAccount(); - $expected->setId(Manager::ACCOUNT_ID); - $user->expects($this->any()) - ->method('getUID') - ->willReturn('user123'); - $user->expects($this->any()) - ->method('getEMailAddress') - ->willReturn('user@domain.tld'); - $user->expects($this->once()) - ->method('getDisplayName') - ->willReturn('Test User'); - $expected->setUserId('user123'); - $expected->setEmail('user@domain.tld'); - $expected->setName('Test User'); - $expected->setInboundUser('user@domain.tld'); - $expected->setInboundHost('imap.domain.tld'); - $expected->setInboundPort(993); - $expected->setInboundSslMode('ssl'); - $expected->setInboundPassword('encrypted'); - $expected->setOutboundUser('user@domain.tld'); - $expected->setOutboundHost('smtp.domain.tld'); - $expected->setOutboundPort(465); - $expected->setOutboundSslMode('tls'); - $expected->setOutboundPassword('encrypted'); - - $account = $this->manager->getDefaultAccount(); - - $this->assertEquals($expected, $account); - } - -} diff --git a/tests/Unit/Service/Provisioning/ConfigMapperTest.php b/tests/Unit/Service/Provisioning/ConfigMapperTest.php new file mode 100644 index 000000000..45e9090cd --- /dev/null +++ b/tests/Unit/Service/Provisioning/ConfigMapperTest.php @@ -0,0 +1,85 @@ +<?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/DefaultAccount/ConfigTest.php b/tests/Unit/Service/Provisioning/ConfigTest.php index a05a4d26b..064843780 100644 --- a/tests/Unit/Service/DefaultAccount/ConfigTest.php +++ b/tests/Unit/Service/Provisioning/ConfigTest.php @@ -19,10 +19,10 @@ * */ -namespace OCA\Mail\Tests\Unit\Service\DefaultAccount; +namespace OCA\Mail\Tests\Unit\Service\Provisioning; use ChristophWurst\Nextcloud\Testing\TestCase; -use OCA\Mail\Service\DefaultAccount\Config; +use OCA\Mail\Service\Provisioning\Config; use OCP\IUser; class ConfigTest extends TestCase { diff --git a/tests/Unit/Service/Provisioning/ManagerTest.php b/tests/Unit/Service/Provisioning/ManagerTest.php new file mode 100644 index 000000000..66b0accfc --- /dev/null +++ b/tests/Unit/Service/Provisioning/ManagerTest.php @@ -0,0 +1,184 @@ +<?php +/** + * @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\Db\MailAccount; +use OCA\Mail\Service\Provisioning\Config; +use OCA\Mail\Service\Provisioning\Manager; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\IUser; +use PHPUnit\Framework\MockObject\MockObject; + +class ManagerTest extends TestCase { + + /** @var ServiceMockObject */ + private $mock; + + /** @var Manager */ + private $manager; + + protected function setUp(): void { + parent::setUp(); + + $this->mock = $this->createServiceMock(Manager::class); + $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); + + $this->assertEquals(0, $cnt); + } + + public function testUpdateProvisionSingleUser() { + /** @var IUser|MockObject $user */ + $user = $this->createMock(IUser::class); + $config = new TestConfig(); + $account = $this->createMock(MailAccount::class); + $this->mock->getParameter('mailAccountMapper') + ->expects($this->once()) + ->method('findProvisionedAccount') + ->willReturn($account); + $this->mock->getParameter('mailAccountMapper') + ->expects($this->once()) + ->method('update') + ->with($account); + + $this->manager->provisionSingleUser($config, $user); + } + + public function testProvisionSingleUser() { + /** @var IUser|MockObject $user */ + $user = $this->createMock(IUser::class); + $config = new TestConfig(); + $this->mock->getParameter('mailAccountMapper') + ->expects($this->once()) + ->method('findProvisionedAccount') + ->willThrowException($this->createMock(DoesNotExistException::class)); + $this->mock->getParameter('mailAccountMapper') + ->expects($this->once()) + ->method('insert'); + + $this->manager->provisionSingleUser($config, $user); + } + + public function testGetNoConfig() { + $config = $this->manager->getConfig(); + + $this->assertNull($config); + } + + public function testGetConfig() { + $config = $this->createMock(Config::class); + $this->mock->getParameter('configMapper') + ->expects($this->once()) + ->method('load') + ->willReturn($config); + + $cfg = $this->manager->getConfig(); + + $this->assertSame($config, $cfg); + } + + public function testDeprovision() { + $config = new TestConfig(); + $this->mock->getParameter('mailAccountMapper') + ->expects($this->once()) + ->method('deleteProvisionedAccounts'); + $this->mock->getParameter('configMapper') + ->expects($this->once()) + ->method('load') + ->willReturn($config); + $this->mock->getParameter('configMapper') + ->expects($this->once()) + ->method('save') + ->willReturn($config); + + $this->manager->deprovision(); + + $this->assertEquals(false, $config->jsonSerialize()['active']); + } + + public function testImportConfig() { + $this->mock->getParameter('configMapper') + ->expects($this->once()) + ->method('save'); + + $this->manager->importConfig([ + 'email' => '%USERID%@domain.com', + ]); + } + + public function testUpdatePasswordNotProvisioned() { + /** @var IUser|MockObject $user */ + $user = $this->createMock(IUser::class); + $this->mock->getParameter('mailAccountMapper') + ->expects($this->once()) + ->method('findProvisionedAccount') + ->with($user) + ->willThrowException($this->createMock(DoesNotExistException::class)); + + $this->manager->updatePassword($user, '123456'); + } + + public function testUpdatePassword() { + /** @var IUser|MockObject $user */ + $user = $this->createMock(IUser::class); + $account = $this->createMock(MailAccount::class); + $this->mock->getParameter('mailAccountMapper') + ->expects($this->once()) + ->method('findProvisionedAccount') + ->willReturn($account); + $this->mock->getParameter('mailAccountMapper') + ->expects($this->once()) + ->method('update') + ->with($account); + + $this->manager->updatePassword($user, '123456'); + } + + public function testNewProvisioning() { + $this->mock->getParameter('configMapper') + ->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' + ); + } +} diff --git a/tests/Unit/Service/Provisioning/TestConfig.php b/tests/Unit/Service/Provisioning/TestConfig.php new file mode 100644 index 000000000..66357a724 --- /dev/null +++ b/tests/Unit/Service/Provisioning/TestConfig.php @@ -0,0 +1,44 @@ +<?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 OCA\Mail\Service\Provisioning\Config; + +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', + ]); + } + +} diff --git a/tests/Unit/Settings/AdminSettingsTest.php b/tests/Unit/Settings/AdminSettingsTest.php new file mode 100644 index 000000000..c2e038e6c --- /dev/null +++ b/tests/Unit/Settings/AdminSettingsTest.php @@ -0,0 +1,74 @@ +<?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\Settings; + +use ChristophWurst\Nextcloud\Testing\ServiceMockObject; +use ChristophWurst\Nextcloud\Testing\TestCase; +use OCA\Mail\AppInfo\Application; +use OCA\Mail\Settings\AdminSettings; +use OCP\AppFramework\Http\TemplateResponse; + +class AdminSettingsTest extends TestCase { + + /** @var ServiceMockObject */ + private $serviceMock; + + /** @var AdminSettings */ + private $settings; + + protected function setUp(): void { + parent::setUp(); + + $this->serviceMock = $this->createServiceMock(AdminSettings::class); + + $this->settings = $this->serviceMock->getService(); + } + + public function testGetSection() { + $section = $this->settings->getSection(); + + $this->assertSame('groupware', $section); + } + + public function testGetForm() { + $this->serviceMock->getParameter('initialStateService')->expects($this->once()) + ->method('provideInitialState') + ->with( + Application::APP_ID, + 'provisioning_settings', + $this->anything() + ); + $expected = new TemplateResponse(Application::APP_ID, 'settings-admin'); + + $form = $this->settings->getForm(); + + $this->assertEquals($expected, $form); + } + + public function testGetPriority() { + $priority = $this->settings->getPriority(); + + $this->assertIsInt($priority); + } +} diff --git a/webpack.common.js b/webpack.common.js index 28b61b7cd..42a0777c0 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -20,7 +20,8 @@ if (process.env.BUNDLE_ANALYZER_TOKEN) { module.exports = { entry: { autoredirect: path.join(__dirname, 'src/autoredirect.js'), - mail: path.join(__dirname, 'src/main.js') + mail: path.join(__dirname, 'src/main.js'), + settings: path.join(__dirname, 'src/main-settings') }, output: { path: path.resolve(__dirname, 'js'), |