diff options
39 files changed, 1899 insertions, 59 deletions
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ef408dc5a..5a7a84ccf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -68,6 +68,7 @@ jobs: - 25:25 - 143:143 - 993:993 + - 4190:4190 mysql-service: image: mariadb:10 env: @@ -49,6 +49,7 @@ start-docker: -p 25:25 \ -p 143:143 \ -p 993:993 \ + -p 4190:4190 \ --hostname mail.domain.tld \ -e MAILNAME=mail.domain.tld \ -e MAIL_ADDRESS=user@domain.tld \ diff --git a/appinfo/info.xml b/appinfo/info.xml index 100ec7f30..3b4d9c5cb 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>1.9.0-alpha.1</version> + <version>1.9.0-alpha.2</version> <licence>agpl</licence> <author>Christoph Wurst</author> <author>Greta Doçi</author> @@ -42,6 +42,7 @@ <step>OCA\Mail\Migration\FixBackgroundJobs</step> <step>OCA\Mail\Migration\MakeItineraryExtractorExecutable</step> <step>OCA\Mail\Migration\MigrateProvisioningConfig</step> + <step>OCA\Mail\Migration\AddSieveToProvisioningConfig</step> <step>OCA\Mail\Migration\ProvisionAccounts</step> </post-migration> </repair-steps> diff --git a/appinfo/routes.php b/appinfo/routes.php index c33149e43..4b06dfa57 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -204,6 +204,21 @@ return [ 'url' => '/api/trustedsenders', 'verb' => 'GET' ], + [ + 'name' => 'sieve#updateAccount', + 'url' => '/api/sieve/account/{id}', + 'verb' => 'PUT' + ], + [ + 'name' => 'sieve#getActiveScript', + 'url' => '/api/sieve/active/{id}', + 'verb' => 'GET' + ], + [ + 'name' => 'sieve#updateActiveScript', + 'url' => '/api/sieve/active/{id}', + 'verb' => 'PUT' + ] ], 'resources' => [ 'accounts' => ['url' => '/api/accounts'], diff --git a/composer.json b/composer.json index cf5f2fe0f..1d11c42dc 100644 --- a/composer.json +++ b/composer.json @@ -32,6 +32,7 @@ "pear-pear.horde.org/horde_exception": "^2.0.8@stable", "pear-pear.horde.org/horde_imap_client": "^2.29.16@stable", "pear-pear.horde.org/horde_mail": "^2.6.4@stable", + "pear-pear.horde.org/horde_managesieve": "^1.0", "pear-pear.horde.org/horde_mime": "^2.11.0@stable", "pear-pear.horde.org/horde_nls": "^2.2.1@stable", "pear-pear.horde.org/horde_smtp": "^1.9.5@stable", diff --git a/composer.lock b/composer.lock index 2706020a5..92bf0eff6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "cc28b5306e0d004371b1e45d030863db", + "content-hash": "ca2a0ba92e885ecd04f1f702fb04ff47", "packages": [ { "name": "amphp/amp", @@ -1256,6 +1256,36 @@ "description": "Provides interfaces for sending e-mail messages and parsing e-mail addresses." }, { + "name": "pear-pear.horde.org/Horde_ManageSieve", + "version": "1.0.3", + "dist": { + "type": "file", + "url": "https://pear.horde.org/get/Horde_ManageSieve-1.0.3.tgz" + }, + "require": { + "pear-pear.horde.org/horde_exception": "<3.0.0.0", + "pear-pear.horde.org/horde_socket_client": "<3.0.0.0", + "pear-pear.horde.org/horde_util": "<3.0.0.0", + "php": ">=5.4.0.0" + }, + "replace": { + "pear-horde/horde_managesieve": "== 1.0.3.0" + }, + "type": "pear-library", + "autoload": { + "classmap": [ + "" + ] + }, + "include-path": [ + "/" + ], + "license": [ + "BSD-2-Clause" + ], + "description": "A library that implements the ManageSieve protocol (RFC 5804)." + }, + { "name": "pear-pear.horde.org/Horde_Mime", "version": "2.11.1", "dist": { @@ -4733,12 +4763,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "1e48e1beacb6122df93aa61a6cc291254984be2a" + "reference": "640ff0b5dcacc0958534c8c0255b90697f3eb2a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/1e48e1beacb6122df93aa61a6cc291254984be2a", - "reference": "1e48e1beacb6122df93aa61a6cc291254984be2a", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/640ff0b5dcacc0958534c8c0255b90697f3eb2a8", + "reference": "640ff0b5dcacc0958534c8c0255b90697f3eb2a8", "shasum": "" }, "conflict": { @@ -4756,6 +4786,7 @@ "barrelstrength/sprout-forms": "<3.9", "baserproject/basercms": ">=4,<=4.3.6|>=4.4,<4.4.1", "bolt/bolt": "<3.7.1", + "bolt/core": "<4.1.13", "brightlocal/phpwhois": "<=4.2.5", "buddypress/buddypress": "<5.1.2", "bugsnag/bugsnag-laravel": ">=2,<2.0.2", @@ -5047,7 +5078,7 @@ "type": "tidelift" } ], - "time": "2021-02-16T17:17:25+00:00" + "time": "2021-02-18T21:02:27+00:00" }, { "name": "sabre/event", diff --git a/doc/admin.md b/doc/admin.md index 84986d3f8..67a9a4d61 100644 --- a/doc/admin.md +++ b/doc/admin.md @@ -28,6 +28,11 @@ Depending on your mail host, it may be necessary to increase your IMAP and/or SM ```php 'app.mail.smtp.timeout' => 2 ``` +#### Sieve timeout +```php +'app.mail.sieve.timeout' => 2 +``` + ### Use php-mail for sending mail You can use the php-mail function to send mails. This is needed for some webhosters (1&1 (1und1)): ```php diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index 829068fc2..d65f04f03 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -50,7 +50,12 @@ class SettingsController extends Controller { string $smtpUser, string $smtpHost, int $smtpPort, - string $smtpSslMode): JSONResponse { + string $smtpSslMode, + bool $sieveEnabled, + string $sieveUser, + string $sieveHost, + int $sievePort, + string $sieveSslMode): JSONResponse { $this->provisioningManager->newProvisioning( $emailTemplate, $imapUser, @@ -60,7 +65,12 @@ class SettingsController extends Controller { $smtpUser, $smtpHost, $smtpPort, - $smtpSslMode + $smtpSslMode, + $sieveEnabled, + $sieveUser, + $sieveHost, + $sievePort, + $sieveSslMode ); return new JSONResponse([]); diff --git a/lib/Controller/SieveController.php b/lib/Controller/SieveController.php new file mode 100644 index 000000000..11fc5b186 --- /dev/null +++ b/lib/Controller/SieveController.php @@ -0,0 +1,219 @@ +<?php + +declare(strict_types=1); + +/** + * @author Daniel Kesselberg <mail@danielkesselberg.de> + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Mail\Controller; + +use Horde\ManageSieve\Exception as ManagesieveException; +use OCA\Mail\AppInfo\Application; +use OCA\Mail\Db\MailAccountMapper; +use OCA\Mail\Exception\ClientException; +use OCA\Mail\Exception\CouldNotConnectException; +use OCA\Mail\Service\AccountService; +use OCA\Mail\Sieve\SieveClientFactory; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; +use OCP\Security\ICrypto; + +class SieveController extends Controller { + + /** @var AccountService */ + private $accountService; + + /** @var MailAccountMapper */ + private $mailAccountMapper; + + /** @var SieveClientFactory */ + private $sieveClientFactory; + + /** @var string */ + private $currentUserId; + + /** @var ICrypto */ + private $crypto; + + /** + * AccountsController constructor. + * + * @param IRequest $request + * @param string $UserId + * @param AccountService $accountService + * @param MailAccountMapper $mailAccountMapper + * @param SieveClientFactory $sieveClientFactory + * @param ICrypto $crypto + */ + public function __construct(IRequest $request, + string $UserId, + AccountService $accountService, + MailAccountMapper $mailAccountMapper, + SieveClientFactory $sieveClientFactory, + ICrypto $crypto + ) { + parent::__construct(Application::APP_ID, $request); + $this->currentUserId = $UserId; + $this->accountService = $accountService; + $this->mailAccountMapper = $mailAccountMapper; + $this->sieveClientFactory = $sieveClientFactory; + $this->crypto = $crypto; + } + + /** + * @NoAdminRequired + * @TrapError + * + * @param int $id account id + * + * @return JSONResponse + * + * @throws CouldNotConnectException + * @throws ClientException + */ + public function getActiveScript(int $id): JSONResponse { + $sieve = $this->getClient($id); + + $scriptName = $sieve->getActive(); + if ($scriptName === null) { + $script = ''; + } else { + $script = $sieve->getScript($scriptName); + } + + return new JSONResponse([ + 'scriptName' => $scriptName, + 'script' => $script, + ]); + } + + /** + * @NoAdminRequired + * @TrapError + * + * @param int $id account id + * @param string $script + * + * @return JSONResponse + * + * @throws ClientException + * @throws CouldNotConnectException + * @throws ManagesieveException + */ + public function updateActiveScript(int $id, string $script): JSONResponse { + $sieve = $this->getClient($id); + + $scriptName = $sieve->getActive() ?? 'nextcloud'; + $sieve->installScript($scriptName, $script, true); + + return new JSONResponse(); + } + + /** + * @NoAdminRequired + * @TrapError + * + * @param int $id account id + * @param bool $sieveEnabled + * @param string $sieveHost + * @param int $sievePort + * @param string $sieveUser + * @param string $sievePassword + * @param string $sieveSslMode + * + * @return JSONResponse + * + * @throws CouldNotConnectException + * @throws DoesNotExistException + */ + public function updateAccount(int $id, + bool $sieveEnabled, + string $sieveHost, + int $sievePort, + string $sieveUser, + string $sievePassword, + string $sieveSslMode + ): JSONResponse { + $mailAccount = $this->mailAccountMapper->find($this->currentUserId, $id); + + if ($sieveEnabled === false) { + $mailAccount->setSieveEnabled(false); + $mailAccount->setSieveHost(null); + $mailAccount->setSievePort(null); + $mailAccount->setSieveUser(null); + $mailAccount->setSievePassword(null); + $mailAccount->setSieveSslMode(null); + + $this->mailAccountMapper->save($mailAccount); + return new JSONResponse(['sieveEnabled' => $mailAccount->isSieveEnabled()]); + } + + if (empty($sieveUser)) { + $sieveUser = $mailAccount->getInboundUser(); + } + + if (empty($sievePassword)) { + $sievePassword = $mailAccount->getInboundPassword(); + } else { + $sievePassword = $this->crypto->encrypt($sievePassword); + } + + try { + $this->sieveClientFactory->createClient($sieveHost, $sievePort, $sieveUser, $sievePassword, $sieveSslMode); + } catch (ManagesieveException $e) { + throw CouldNotConnectException::create($e, 'ManageSieve', $sieveHost, $sievePort); + } + + $mailAccount->setSieveEnabled(true); + $mailAccount->setSieveHost($sieveHost); + $mailAccount->setSievePort($sievePort); + $mailAccount->setSieveUser($mailAccount->getInboundUser() === $sieveUser ? null : $sieveUser); + $mailAccount->setSievePassword($mailAccount->getInboundPassword() === $sievePassword ? null : $sievePassword); + $mailAccount->setSieveSslMode($sieveSslMode); + + $this->mailAccountMapper->save($mailAccount); + return new JSONResponse(['sieveEnabled' => $mailAccount->isSieveEnabled()]); + } + + /** + * @param int $id + * + * @return \Horde\ManageSieve + * + * @throws ClientException + * @throws CouldNotConnectException + */ + protected function getClient(int $id): \Horde\ManageSieve { + $account = $this->accountService->find($this->currentUserId, $id); + + if (!$account->getMailAccount()->isSieveEnabled()) { + throw new CouldNotConnectException('ManageSieve is disabled.'); + } + + try { + $sieve = $this->sieveClientFactory->getClient($account); + } catch (ManagesieveException $e) { + throw CouldNotConnectException::create($e, 'ManageSieve', $account->getMailAccount()->getSieveHost(), $account->getMailAccount()->getSievePort()); + } + + return $sieve; + } +} diff --git a/lib/Db/MailAccount.php b/lib/Db/MailAccount.php index b5aa8ecae..3df3c1a67 100644 --- a/lib/Db/MailAccount.php +++ b/lib/Db/MailAccount.php @@ -77,6 +77,18 @@ use OCP\AppFramework\Db\Entity; * @method int|null getSentMailboxId() * @method void setTrashMailboxId(?int $id) * @method int|null getTrashMailboxId() + * @method bool isSieveEnabled() + * @method void setSieveEnabled(bool $sieveEnabled) + * @method string|null getSieveHost() + * @method void setSieveHost(?string $sieveHost) + * @method int|null getSievePort() + * @method void setSievePort(?int $sievePort) + * @method string|null getSieveSslMode() + * @method void setSieveSslMode(?string $sieveSslMode) + * @method string|null getSieveUser() + * @method void setSieveUser(?string $sieveUser) + * @method string|null getSievePassword() + * @method void setSievePassword(?string $sievePassword) */ class MailAccount extends Entity { protected $userId; @@ -109,6 +121,19 @@ class MailAccount extends Entity { /** @var int|null */ protected $trashMailboxId; + /** @var bool */ + protected $sieveEnabled = false; + /** @var string|null */ + protected $sieveHost; + /** @var integer|null */ + protected $sievePort; + /** @var string|null */ + protected $sieveSslMode; + /** @var string|null */ + protected $sieveUser; + /** @var string|null */ + protected $sievePassword; + /** * @param array $params */ @@ -168,6 +193,8 @@ class MailAccount extends Entity { $this->addType('draftsMailboxId', 'integer'); $this->addType('sentMailboxId', 'integer'); $this->addType('trashMailboxId', 'integer'); + $this->addType('sieveEnabled', 'boolean'); + $this->addType('sievePort', 'integer'); } /** @@ -192,6 +219,7 @@ class MailAccount extends Entity { 'draftsMailboxId' => $this->getDraftsMailboxId(), 'sentMailboxId' => $this->getSentMailboxId(), 'trashMailboxId' => $this->getTrashMailboxId(), + 'sieveEnabled' => $this->isSieveEnabled(), ]; if (!is_null($this->getOutboundHost())) { @@ -201,6 +229,13 @@ class MailAccount extends Entity { $result['smtpSslMode'] = $this->getOutboundSslMode(); } + if ($this->isSieveEnabled()) { + $result['sieveHost'] = $this->getSieveHost(); + $result['sievePort'] = $this->getSievePort(); + $result['sieveUser'] = $this->getSieveUser(); + $result['sieveSslMode'] = $this->getSieveSslMode(); + } + return $result; } } diff --git a/lib/Exception/CouldNotConnectException.php b/lib/Exception/CouldNotConnectException.php new file mode 100644 index 000000000..c996095ee --- /dev/null +++ b/lib/Exception/CouldNotConnectException.php @@ -0,0 +1,36 @@ +<?php + +declare(strict_types=1); + +/** + * @author Daniel Kesselberg <mail@danielkesselberg.de> + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Mail\Exception; + +use Throwable; + +class CouldNotConnectException extends ServiceException { + public static function create(Throwable $exception, string $service, string $host, int $port): self { + return new self( + "Connection to {$service} at {$host}:{$port} failed. {$exception->getMessage()}", + (int)$exception->getCode(), + $exception + ); + } +} diff --git a/lib/Migration/AddSieveToProvisioningConfig.php b/lib/Migration/AddSieveToProvisioningConfig.php new file mode 100644 index 000000000..e871ff1cc --- /dev/null +++ b/lib/Migration/AddSieveToProvisioningConfig.php @@ -0,0 +1,85 @@ +<?php + +declare(strict_types=1); + +/** + * @author Daniel Kesselberg <mail@danielkesselberg.de> + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Mail\Migration; + +use OCA\Mail\Service\Provisioning\Config as ProvisioningConfig; +use OCA\Mail\Service\Provisioning\ConfigMapper as ProvisioningConfigMapper; +use OCP\IConfig; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; + +class AddSieveToProvisioningConfig implements IRepairStep { + + /** @var IConfig */ + private $config; + + /** @var ProvisioningConfigMapper */ + private $configMapper; + + public function __construct(IConfig $config, ProvisioningConfigMapper $configMapper) { + $this->config = $config; + $this->configMapper = $configMapper; + } + + public function getName(): string { + return 'Add sieve defaults to provisioning config'; + } + + public function run(IOutput $output) { + if (!$this->shouldRun()) { + return; + } + + $config = $this->configMapper->load(); + if ($config === null) { + return; + } + + $reflectionClass = new \ReflectionClass(ProvisioningConfig::class); + $reflectionProperty = $reflectionClass->getProperty('data'); + + $reflectionProperty->setAccessible(true); + $data = $reflectionProperty->getValue($config); + + if (!isset($data['sieveEnabled'])) { + $data = array_merge($data, [ + 'sieveEnabled' => false, + 'sieveHost' => '', + 'sievePort' => 4190, + 'sieveUser' => '', + 'sieveSslMode' => 'tls', + ]); + } + + $reflectionProperty->setValue($config, $data); + $this->configMapper->save($config); + + $output->info('added sieve defaults to provisioning config'); + } + + protected function shouldRun(): bool { + $appVersion = $this->config->getAppValue('mail', 'installed_version', '0.0.0'); + return version_compare($appVersion, '1.9.0', '<'); + } +} diff --git a/lib/Migration/Version1090Date20210127160127.php b/lib/Migration/Version1090Date20210127160127.php new file mode 100644 index 000000000..8a52ae4fc --- /dev/null +++ b/lib/Migration/Version1090Date20210127160127.php @@ -0,0 +1,56 @@ +<?php + +declare(strict_types=1); + +namespace OCA\Mail\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version1090Date20210127160127 extends SimpleMigrationStep { + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $table = $schema->getTable('mail_accounts'); + $table->addColumn('sieve_enabled', 'boolean', [ + 'notnull' => true, + 'default' => false, + ]); + $table->addColumn('sieve_host', 'string', [ + 'notnull' => false, + 'length' => 64, + 'default' => null, + ]); + $table->addColumn('sieve_port', 'string', [ + 'notnull' => false, + 'length' => 6, + 'default' => null, + ]); + $table->addColumn('sieve_ssl_mode', 'string', [ + 'notnull' => false, + 'length' => 10, + 'default' => null, + ]); + $table->addColumn('sieve_user', 'string', [ + 'notnull' => false, + 'length' => 64, + 'default' => null, + ]); + $table->addColumn('sieve_password', 'string', [ + 'notnull' => false, + 'length' => 2048, + 'default' => null, + ]); + + return $schema; + } +} diff --git a/lib/Service/Provisioning/Config.php b/lib/Service/Provisioning/Config.php index 2af2f1827..74b8cda8d 100644 --- a/lib/Service/Provisioning/Config.php +++ b/lib/Service/Provisioning/Config.php @@ -111,6 +111,45 @@ class Config implements JsonSerializable { } /** + * @return boolean + */ + public function getSieveEnabled(): bool { + return (bool)$this->data['sieveEnabled']; + } + + /** + * @return string + */ + public function getSieveHost() { + return $this->data['sieveHost']; + } + + /** + * @return int + */ + public function getSievePort(): int { + return (int)$this->data['sievePort']; + } + + /** + * @param IUser $user + * @return string + */ + public function buildSieveUser(IUser $user) { + if (isset($this->data['sieveUser'])) { + return $this->buildUserEmail($this->data['sieveUser'], $user); + } + return $this->buildEmail($user); + } + + /** + * @return string + */ + public function getSieveSslMode() { + return $this->data['sieveSslMode']; + } + + /** * Replace %USERID% and %EMAIL% to allow special configurations * * @param string $original diff --git a/lib/Service/Provisioning/Manager.php b/lib/Service/Provisioning/Manager.php index c948f0e69..742c87f13 100644 --- a/lib/Service/Provisioning/Manager.php +++ b/lib/Service/Provisioning/Manager.php @@ -100,7 +100,12 @@ class Manager { string $smtpUser, string $smtpHost, int $smtpPort, - string $smtpSslMode): void { + string $smtpSslMode, + bool $sieveEnabled, + string $sieveUser, + string $sieveHost, + int $sievePort, + string $sieveSslMode): void { $config = $this->configMapper->save(new Config([ 'active' => true, 'email' => $email, @@ -112,6 +117,11 @@ class Manager { 'smtpHost' => $smtpHost, 'smtpPort' => $smtpPort, 'smtpSslMode' => $smtpSslMode, + 'sieveEnabled' => $sieveEnabled, + 'sieveUser' => $sieveUser, + 'sieveHost' => $sieveHost, + 'sievePort' => $sievePort, + 'sieveSslMode' => $sieveSslMode, ])); $this->provision($config); @@ -128,6 +138,19 @@ class Manager { $account->setOutboundHost($config->getSmtpHost()); $account->setOutboundPort($config->getSmtpPort()); $account->setOutboundSslMode($config->getSmtpSslMode()); + $account->setSieveEnabled($config->getSieveEnabled()); + + if ($config->getSieveEnabled()) { + $account->setSieveUser($config->buildSieveUser($user)); + $account->setSieveHost($config->getSieveHost()); + $account->setSievePort($config->getSievePort()); + $account->setSieveSslMode($config->getSieveSslMode()); + } else { + $account->setSieveUser(null); + $account->setSieveHost(null); + $account->setSievePort(null); + $account->setSieveSslMode(null); + } return $account; } diff --git a/lib/Service/SetupService.php b/lib/Service/SetupService.php index 0be5984bf..ffd6146ac 100644 --- a/lib/Service/SetupService.php +++ b/lib/Service/SetupService.php @@ -26,9 +26,14 @@ declare(strict_types=1); namespace OCA\Mail\Service; +use Horde_Imap_Client_Exception; +use Horde_Mail_Exception; +use Horde_Mail_Transport_Smtphorde; use OCA\Mail\Account; use OCA\Mail\Db\MailAccount; +use OCA\Mail\Exception\CouldNotConnectException; use OCA\Mail\Exception\ServiceException; +use OCA\Mail\IMAP\IMAPClientFactory; use OCA\Mail\Service\AutoConfig\AutoConfig; use OCA\Mail\SMTP\SmtpClientFactory; use OCP\Security\ICrypto; @@ -48,6 +53,9 @@ class SetupService { /** @var SmtpClientFactory */ private $smtpClientFactory; + /** @var IMAPClientFactory */ + private $imapClientFactory; + /** var LoggerInterface */ private $logger; @@ -55,11 +63,13 @@ class SetupService { AccountService $accountService, ICrypto $crypto, SmtpClientFactory $smtpClientFactory, + IMAPClientFactory $imapClientFactory, LoggerInterface $logger) { $this->autoConfig = $autoConfig; $this->accountService = $accountService; $this->crypto = $crypto; $this->smtpClientFactory = $smtpClientFactory; + $this->imapClientFactory = $imapClientFactory; $this->logger = $logger; } @@ -124,12 +134,35 @@ class SetupService { $account = new Account($newAccount); $this->logger->debug('Connecting to account {account}', ['account' => $newAccount->getEmail()]); - $transport = $this->smtpClientFactory->create($account); - $account->testConnectivity($transport); + $this->testConnectivity($account); $this->accountService->save($newAccount); $this->logger->debug("account created " . $newAccount->getId()); return $account; } + + /** + * @param Account $account + * @throws CouldNotConnectException + */ + protected function testConnectivity(Account $account): void { + $mailAccount = $account->getMailAccount(); + + $imapClient = $this->imapClientFactory->getClient($account); + try { + $imapClient->login(); + } catch (Horde_Imap_Client_Exception $e) { + throw CouldNotConnectException::create($e, 'IMAP', $mailAccount->getInboundHost(), $mailAccount->getInboundPort()); + } + + $transport = $this->smtpClientFactory->create($account); + if ($transport instanceof Horde_Mail_Transport_Smtphorde) { + try { + $transport->getSMTPObject(); + } catch (Horde_Mail_Exception $e) { + throw CouldNotConnectException::create($e, 'SMTP', $mailAccount->getOutboundHost(), $mailAccount->getOutboundPort()); + } + } + } } diff --git a/lib/Settings/AdminSettings.php b/lib/Settings/AdminSettings.php index 532749c85..7e4c47c98 100644 --- a/lib/Settings/AdminSettings.php +++ b/lib/Settings/AdminSettings.php @@ -61,6 +61,11 @@ class AdminSettings implements ISettings { 'smtpHost' => 'smtp.domain.com', 'smtpPort' => 587, 'smtpSslMode' => 'tls', + 'sieveEnabled' => false, + 'sieveUser' => '%USERID%@domain.com', + 'sieveHost' => 'imap.domain.com', + 'sievePort' => 4190, + 'sieveSslMode' => 'tls', ]) ); diff --git a/lib/Sieve/SieveClientFactory.php b/lib/Sieve/SieveClientFactory.php new file mode 100644 index 000000000..4e7359741 --- /dev/null +++ b/lib/Sieve/SieveClientFactory.php @@ -0,0 +1,116 @@ +<?php + +declare(strict_types=1); + +/** + * @author Daniel Kesselberg <mail@danielkesselberg.de> + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Mail\Sieve; + +use Horde\ManageSieve; +use OCA\Mail\Account; +use OCP\IConfig; +use OCP\Security\ICrypto; + +class SieveClientFactory { + + /** @var ICrypto */ + private $crypto; + + /** @var IConfig */ + private $config; + + private $cache = []; + + /** + * @param ICrypto $crypto + * @param IConfig $config + */ + public function __construct(ICrypto $crypto, IConfig $config) { + $this->crypto = $crypto; + $this->config = $config; + } + + /** + * @param Account $account + * @return ManageSieve + * @throws ManageSieve\Exception + */ + public function getClient(Account $account): ManageSieve { + if (!isset($this->cache[$account->getId()])) { + $user = $account->getMailAccount()->getSieveUser(); + if (empty($user)) { + $user = $account->getMailAccount()->getInboundUser(); + } + $password = $account->getMailAccount()->getSievePassword(); + if (empty($password)) { + $password = $account->getMailAccount()->getInboundPassword(); + } + + $this->cache[$account->getId()] = $this->createClient( + $account->getMailAccount()->getSieveHost(), + $account->getMailAccount()->getSievePort(), + $user, + $password, + $account->getMailAccount()->getSieveSslMode() + ); + } + + return $this->cache[$account->getId()]; + } + + /** + * @param string $host + * @param int $port + * @param string $user + * @param string $password + * @param string $sslMode + * @return ManageSieve + * @throws ManageSieve\Exception + */ + public function createClient(string $host, int $port, string $user, string $password, string $sslMode): ManageSieve { + if (empty($sslMode)) { + $sslMode = true; + } elseif ($sslMode === 'none') { + $sslMode = false; + } + + $params = [ + 'host' => $host, + 'port' => $port, + 'user' => $user, + 'password' => $this->crypto->decrypt($password), + 'secure' => $sslMode, + 'timeout' => (int)$this->config->getSystemValue('app.mail.sieve.timeout', 5), + 'context' => [ + 'ssl' => [ + 'verify_peer' => $this->config->getSystemValueBool('app.mail.verify-tls-peer', true), + 'verify_peer_name' => $this->config->getSystemValueBool('app.mail.verify-tls-peer', true), + + ] + ], + ]; + + if ($this->config->getSystemValue('debug', false)) { + $params['logger'] = new SieveLogger($this->config->getSystemValue('datadirectory') . '/horde_sieve.log'); + } + + return new ManageSieve($params); + } +} diff --git a/lib/Sieve/SieveLogger.php b/lib/Sieve/SieveLogger.php new file mode 100644 index 000000000..752467cdf --- /dev/null +++ b/lib/Sieve/SieveLogger.php @@ -0,0 +1,46 @@ +<?php + +declare(strict_types=1); + +/** + * @author Daniel Kesselberg <mail@danielkesselberg.de> + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Mail\Sieve; + +class SieveLogger { + /** @var resource */ + protected $stream; + + public function __construct(string $logFile) { + $stream = @fopen($logFile, 'ab'); + if ($stream === false) { + throw new \InvalidArgumentException('Unable to use "' . $logFile . '" as log file for sieve.'); + } + $this->stream = $stream; + } + + public function debug(string $message): void { + fwrite($this->stream, $message); + } + + public function __destruct() { + fflush($this->stream); + fclose($this->stream); + } +} diff --git a/src/components/AccountSettings.vue b/src/components/AccountSettings.vue index 2081a1244..2ef346848 100644 --- a/src/components/AccountSettings.vue +++ b/src/components/AccountSettings.vue @@ -67,6 +67,22 @@ :account="account" /> </div> </AppSettingsSection> + <AppSettingsSection v-if="account && !account.provisioned" :title="t('mail', 'Sieve filter server')"> + <div id="sieve-settings"> + <SieveAccountForm + :key="account.accountId" + ref="sieveAccountForm" + :account="account" /> + </div> + </AppSettingsSection> + <AppSettingsSection v-if="account && account.sieveEnabled" :title="t('mail', 'Sieve filter rules')"> + <div id="sieve-filter"> + <SieveFilterForm + :key="account.accountId" + ref="sieveFilterForm" + :account="account" /> + </div> + </AppSettingsSection> <AppSettingsSection :title="t('mail', 'Trusted senders')"> <TrustedSenders /> </AppSettingsSection> @@ -83,9 +99,13 @@ import AliasSettings from '../components/AliasSettings' import AppSettingsDialog from '@nextcloud/vue/dist/Components/AppSettingsDialog' import AppSettingsSection from '@nextcloud/vue/dist/Components/AppSettingsSection' import TrustedSenders from './TrustedSenders' +import SieveAccountForm from './SieveAccountForm' +import SieveFilterForm from './SieveFilterForm' export default { name: 'AccountSettings', components: { + SieveAccountForm, + SieveFilterForm, TrustedSenders, AccountForm, AliasSettings, diff --git a/src/components/SieveAccountForm.vue b/src/components/SieveAccountForm.vue new file mode 100644 index 000000000..21b128b0c --- /dev/null +++ b/src/components/SieveAccountForm.vue @@ -0,0 +1,221 @@ +<template> + <form id="sieve-form"> + <p> + <input + id="sieve-disabled" + v-model="sieveConfig.sieveEnabled" + type="radio" + class="radio" + name="sieve-active" + :value="false"> + <label + :class="{primary: !sieveConfig.sieveEnabled}" + for="sieve-disabled"> + {{ t('mail', 'Disabled') }} + </label> + <input + id="sieve-enabled" + v-model="sieveConfig.sieveEnabled" + type="radio" + class="radio" + name="sieve-active" + :value="true"> + <label + :class="{primary: sieveConfig.sieveEnabled}" + for="sieve-enabled"> + {{ t('mail', 'Enabled') }} + </label> + </p> + <template v-if="sieveConfig.sieveEnabled"> + <label for="sieve-host">{{ t('mail', 'Sieve Host') }}</label> + <input + id="sieve-host" + v-model="sieveConfig.sieveHost" + type="text" + required> + <h4>{{ t('mail', 'Sieve Security') }}</h4> + <div class="flex-row"> + <input + id="sieve-sec-none" + v-model="sieveConfig.sieveSslMode" + type="radio" + name="sieve-sec" + value="none"> + <label + class="button" + for="sieve-sec-none" + :class="{primary: sieveConfig.sieveSslMode === 'none'}">{{ + t('mail', 'None') + }}</label> + <input + id="sieve-sec-ssl" + v-model="sieveConfig.sieveSslMode" + type="radio" + name="sieve-sec" + value="ssl"> + <label + class="button" + for="sieve-sec-ssl" + :class="{primary: sieveConfig.sieveSslMode === 'ssl'}"> + {{ t('mail', 'SSL/TLS') }} + </label> + <input + id="sieve-sec-tls" + v-model="sieveConfig.sieveSslMode" + type="radio" + name="sieve-sec" + value="tls"> + <label + class="button" + for="sieve-sec-tls" + :class="{primary: sieveConfig.sieveSslMode === 'tls'}"> + {{ t('mail', 'STARTTLS') }} + </label> + </div> + <label for="sieve-port">{{ t('mail', 'Sieve Port') }}</label> + <input + id="sieve-port" + v-model="sieveConfig.sievePort" + type="text" + required> + <h4>{{ t('mail', 'Sieve Credentials') }}</h4> + <p> + <input + id="sieve-credentials-imap" + v-model="useImapCredentials" + type="radio" + class="radio" + :value="true"> + <label + :class="{primary: useImapCredentials}" + for="sieve-credentials-imap"> + {{ t('mail', 'IMAP credentials') }} + </label> + <input + id="sieve-credentials-custom" + v-model="useImapCredentials" + type="radio" + class="radio" + :value="false"> + <label + :class="{primary: !useImapCredentials}" + for="sieve-credentials-custom"> + {{ t('mail', 'Custom') }} + </label> + </p> + <template v-if="!useImapCredentials"> + <label for="sieve-user">{{ t('mail', 'Sieve User') }}</label> + <input + id="sieve-user" + v-model="sieveConfig.sieveUser" + type="text" + required> + <label for="sieve-password">{{ + t('mail', 'Sieve Password') + }}</label> + <input + id="sieve-password" + v-model="sieveConfig.sievePassword" + type="password" + required> + </template> + </template> + <slot name="feedback" /> + <p v-if="errorMessage"> + {{ t('mail', 'Oh Snap!') }} + {{ errorMessage }} + </p> + <input type="submit" + class="primary" + :disabled="loading" + :value="submitButtonText" + @click.prevent="onSubmit"> + </form> +</template> + +<script> +export default { + name: 'SieveAccountForm', + props: { + account: { + type: Object, + required: true, + }, + }, + data() { + return { + sieveConfig: { + sieveEnabled: this.account.sieveEnabled, + sieveHost: this.account.sieveHost || this.account.imapHost, + sievePort: this.account.sievePort || 4190, + sieveUser: this.account.sieveUser || '', + sievePassword: '', + sieveSslMode: this.account.sieveSslMode || 'tls', + }, + loading: false, + useImapCredentials: !this.account.sieveUser, + errorMessage: '', + submitButtonText: t('mail', 'Save sieve settings'), + } + }, + methods: { + async onSubmit() { + this.loading = true + this.errorMessage = '' + + // empty user and password => use imap credentials + if (this.sieveConfig.sieveUser === '' && this.sieveConfig.sievePassword === '') { + this.useImapCredentials = true + } + + // clear user and password if imap credentials are used + if (this.useImapCredentials) { + this.sieveConfig.sieveUser = '' + this.sieveConfig.sievePassword = '' + } + + try { + await this.$store.dispatch('updateSieveAccount', { + account: this.account, + data: this.sieveConfig, + }) + } catch (error) { + this.errorMessage = error.message + } + + this.loading = false + }, + }, +} +</script> + +<style scoped> +form { + width: 250px +} + +label { + display: inline-block; +} + +input { + width: 100%; +} + +.flex-row { + display: flex; +} + +label.button { + text-align: center; + flex-grow: 1; +} + +label.error { + color: red; +} + +input[type='radio'] { + display: none; +} +</style> diff --git a/src/components/SieveFilterForm.vue b/src/components/SieveFilterForm.vue new file mode 100644 index 000000000..adfb2a371 --- /dev/null +++ b/src/components/SieveFilterForm.vue @@ -0,0 +1,81 @@ +<template> + <div class="section"> + <textarea + id="sieve-text-area" + v-model="active.script" + v-shortkey.avoid + rows="20" + :disabled="loading" /> + <p v-if="errorMessage"> + {{ t('mail', 'Oh Snap!') }} + {{ errorMessage }} + </p> + <button + class="primary" + :class="loading ? 'icon-loading-small-dark' : 'icon-checkmark-white'" + :disabled="loading" + @click="saveActiveScript"> + {{ t('mail', 'Save sieve script') }} + </button> + </div> +</template> + +<script> +import { getActiveScript, updateActiveScript } from '../service/SieveService' + +export default { + name: 'SieveFilterForm', + props: { + account: { + type: Object, + required: true, + }, + }, + data() { + return { + active: {}, + loading: false, + errorMessage: '', + } + }, + async mounted() { + this.active = await getActiveScript(this.account.id) + }, + methods: { + async saveActiveScript() { + this.loading = true + this.errorMessage = '' + + try { + await updateActiveScript(this.account.id, this.active) + } catch (error) { + this.errorMessage = error.message + } + + this.loading = false + }, + }, +} +</script> + +<style lang="scss" scoped> +.section { + display: block; + padding: 0; + margin-bottom: 23px; +} + +textarea { + width: 100%; +} + +.primary { + padding-left: 26px; + background-position: 6px; + color: var(--color-main-background); + + &:after { + left: 14px; + } +} +</style> diff --git a/src/components/settings/ProvisionPreview.vue b/src/components/settings/ProvisionPreview.vue index c25b68750..8d562e404 100644 --- a/src/components/settings/ProvisionPreview.vue +++ b/src/components/settings/ProvisionPreview.vue @@ -43,6 +43,16 @@ ssl: smtpSslMode, }) }}<br> + <span v-if="sieveEnabled"> + {{ + t('mail', 'Sieve: {user} on {host}:{port} ({ssl} encryption)', { + user: sieveUser, + host: sieveHost, + port: sievePort, + ssl: sieveSslMode, + }) + }}<br> + </span> </div> </template> @@ -87,6 +97,21 @@ export default { smtpUser() { return this.templates.smtpUser.replace('%USERID%', this.data.uid).replace('%EMAIL%', this.data.email) }, + sieveEnabled() { + return this.templates.sieveEnabled + }, + sieveHost() { + return this.templates.sieveHost + }, + sievePort() { + return this.templates.sievePort + }, + sieveSslMode() { + return this.templates.sieveSslMode + }, + sieveUser() { + return this.templates.sieveUser.replace('%USERID%', this.data.uid).replace('%EMAIL%', this.data.email) + }, }, } </script> diff --git a/src/components/settings/ProvisioningSettings.vue b/src/components/settings/ProvisioningSettings.vue index 7bdd0f9be..5d6a14d07 100644 --- a/src/components/settings/ProvisioningSettings.vue +++ b/src/components/settings/ProvisioningSettings.vue @@ -211,6 +211,89 @@ </div> </div> <div class="settings-group"> + <div class="group-title"> + {{ t('mail', 'Sieve') }} + </div> + <div class="group-inputs"> + <div> + <input id="mail-provision-sieve-enabled" + v-model="sieveEnabled" + type="checkbox" + class="checkbox"> + <label for="mail-provision-sieve-enabled"> + {{ t('mail', 'Enable sieve integration') }} + </label> + </div> + <label for="mail-provision-sieve-user"> + {{ t('mail', 'User') }}* + <br> + <input + id="mail-provision-sieve-user" + v-model="sieveUser" + :disabled="loading" + name="email" + type="text"> + </label> + <div class="flex-row"> + <label for="mail-provision-sieve-host"> + {{ t('mail', 'Host') }} + <br> + <input + id="mail-provision-sieve-host" + v-model="sieveHost" + :disabled="loading" + name="email" + type="text"> + </label> + <label for="mail-provision-sieve-port"> + {{ t('mail', 'Port') }} + <br> + <input + id="mail-provision-sieve-port" + v-model="sievePort" + :disabled="loading" + name="email" + type="number"> + </label> + </div> + <div class="flex-row"> + <input + id="mail-provision-sieve-user-none" + v-model="sieveSslMode" + type="radio" + name="man-sieve-sec" + :disabled="loading" + value="none"> + <label + class="button" + for="mail-provision-sieve-user-none" + :class="{primary: sieveSslMode === 'none'}">{{ t('mail', 'None') }}</label> + <input + id="mail-provision-sieve-user-ssl" + v-model="sieveSslMode" + type="radio" + name="man-sieve-sec" + :disabled="loading" + value="ssl"> + <label + class="button" + for="mail-provision-sieve-user-ssl" + :class="{primary: sieveSslMode === 'ssl'}">{{ t('mail', 'SSL/TLS') }}</label> + <input + id="mail-provision-sieve-user-tls" + v-model="sieveSslMode" + type="radio" + name="man-sieve-sec" + :disabled="loading" + value="tls"> + <label + class="button" + for="mail-provision-sieve-user-tls" + :class="{primary: sieveSslMode === 'tls'}">{{ t('mail', 'STARTTLS') }}</label> + </div> + </div> + </div> + <div class="settings-group"> <div class="group-title" /> <div class="group-inputs"> <input @@ -272,6 +355,11 @@ export default { smtpPort: this.settings.smtpPort || 587, smtpUser: this.settings.smtpUser || '%USERID%domain.com', smtpSslMode: this.settings.smtpSslMode || 'tls', + sieveEnabled: this.settings.sieveEnabled, + sieveHost: this.settings.sieveHost, + sievePort: this.settings.sievePort, + sieveSslMode: this.settings.sieveSslMode, + sieveUser: this.settings.sieveUser, previewData1: { uid: 'user123', email: '', @@ -295,6 +383,11 @@ export default { smtpHost: this.smtpHost, smtpPort: this.smtpPort, smtpSslMode: this.smtpSslMode, + sieveEnabled: this.sieveEnabled, + sieveUser: this.sieveUser, + sieveHost: this.sieveHost, + sievePort: this.sievePort, + sieveSslMode: this.sieveSslMode, } }, }, @@ -315,6 +408,11 @@ export default { smtpHost: this.smtpHost, smtpPort: this.smtpPort, smtpSslMode: this.smtpSslMode, + sieveEnabled: this.sieveEnabled, + sieveUser: this.sieveUser, + sieveHost: this.sieveHost, + sievePort: this.sievePort, + sieveSslMode: this.sieveSslMode, }) .then(() => { logger.info('provisioning settings updated') diff --git a/src/errors/CouldNotConnectError.js b/src/errors/CouldNotConnectError.js new file mode 100644 index 000000000..94a8c9a50 --- /dev/null +++ b/src/errors/CouldNotConnectError.js @@ -0,0 +1,31 @@ +/** + * @author Daniel Kesselberg <mail@danielkesselberg.de> + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +export default class CouldNotConnectError extends Error { + + constructor(message) { + super(message) + this.name = CouldNotConnectError.getName() + } + + static getName() { + return 'CouldNotConnectError' + } + +} diff --git a/src/errors/ManageSieveError.js b/src/errors/ManageSieveError.js new file mode 100644 index 000000000..e6f716e03 --- /dev/null +++ b/src/errors/ManageSieveError.js @@ -0,0 +1,31 @@ +/** + * @author Daniel Kesselberg <mail@danielkesselberg.de> + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +export default class ManageSieveError extends Error { + + constructor(message) { + super(message) + this.name = ManageSieveError.getName() + } + + static getName() { + return 'ManageSieveError' + } + +} diff --git a/src/errors/convert.js b/src/errors/convert.js index a860999a3..c4f52fbb5 100644 --- a/src/errors/convert.js +++ b/src/errors/convert.js @@ -24,6 +24,8 @@ import MailboxNotCachedError from './MailboxNotCachedError' import NoDraftsMailboxConfiguredError from './NoDraftsMailboxConfiguredError' import NoSentMailboxConfiguredError from './NoSentMailboxConfiguredError' import NoTrashMailboxConfiguredError from './NoTrashMailboxConfiguredError' +import CouldNotConnectError from './CouldNotConnectError' +import ManageSieveError from './ManageSieveError' const map = { 'OCA\\Mail\\Exception\\DraftsMailboxNotSetException': NoDraftsMailboxConfiguredError, @@ -31,6 +33,8 @@ const map = { 'OCA\\Mail\\Exception\\MailboxNotCachedException': MailboxNotCachedError, 'OCA\\Mail\\Exception\\SentMailboxNotSetException': NoSentMailboxConfiguredError, 'OCA\\Mail\\Exception\\TrashMailboxNotSetException': NoTrashMailboxConfiguredError, + 'OCA\\Mail\\Exception\\CouldNotConnectException': CouldNotConnectError, + 'Horde\\ManageSieve\\Exception': ManageSieveError, } /** @@ -54,5 +58,5 @@ export const convertAxiosError = (axiosError) => { return axiosError } - return new map[response.data.data.type]() + return new map[response.data.data.type](response.data.message) } diff --git a/src/service/SieveService.js b/src/service/SieveService.js new file mode 100644 index 000000000..62af4821a --- /dev/null +++ b/src/service/SieveService.js @@ -0,0 +1,58 @@ +/** + * @author Daniel Kesselberg <mail@danielkesselberg.de> + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +import { generateUrl } from '@nextcloud/router' +import axios from '@nextcloud/axios' +import { convertAxiosError } from '../errors/convert' + +export async function updateAccount(id, data) { + const url = generateUrl('/apps/mail/api/sieve/account/{id}', { + id, + }) + + try { + return (await axios.put(url, data)).data + } catch (error) { + throw convertAxiosError(error) + } +} + +export async function getActiveScript(id) { + const url = generateUrl('/apps/mail/api/sieve/active/{id}', { + id, + }) + + try { + return (await axios.get(url)).data + } catch (error) { + throw convertAxiosError(error) + } +} + +export async function updateActiveScript(id, data) { + const url = generateUrl('/apps/mail/api/sieve/active/{id}', { + id, + }) + + try { + return (await axios.put(url, data)).data + } catch (error) { + throw convertAxiosError(error) + } +} diff --git a/src/store/actions.js b/src/store/actions.js index 85ab40fa1..545ca31bd 100644 --- a/src/store/actions.js +++ b/src/store/actions.js @@ -62,10 +62,10 @@ import { fetchEnvelope, fetchEnvelopes, fetchMessage, - setEnvelopeFlag, - syncEnvelopes, fetchThread, moveMessage, + setEnvelopeFlag, + syncEnvelopes, } from '../service/MessageService' import { createAlias, deleteAlias } from '../service/AliasService' import logger from '../logger' @@ -76,6 +76,7 @@ import SyncIncompleteError from '../errors/SyncIncompleteError' import MailboxLockedError from '../errors/MailboxLockedError' import { wait } from '../util/wait' import { UNIFIED_INBOX_ID } from './constants' +import { updateAccount as updateSieveAccount } from '../service/SieveService' const PAGE_SIZE = 20 @@ -715,4 +716,14 @@ export default { commit('removeEnvelope', { id }) commit('removeMessage', { id }) }, + async updateSieveAccount({ commit }, { account, data }) { + logger.debug(`update sieve settings for account ${account.id}`) + try { + await updateSieveAccount(account.id, data) + commit('patchAccount', { account, data }) + } catch (error) { + logger.error('failed to update sieve account: ', { error }) + throw error + } + }, } diff --git a/tests/Integration/Db/MailAccountTest.php b/tests/Integration/Db/MailAccountTest.php index 8bf2af949..fd2ad11e3 100644 --- a/tests/Integration/Db/MailAccountTest.php +++ b/tests/Integration/Db/MailAccountTest.php @@ -69,6 +69,7 @@ class MailAccountTest extends TestCase { 'draftsMailboxId' => null, 'sentMailboxId' => null, 'trashMailboxId' => null, + 'sieveEnabled' => false, ], $a->toJson()); } @@ -95,6 +96,7 @@ class MailAccountTest extends TestCase { 'draftsMailboxId' => null, 'sentMailboxId' => null, 'trashMailboxId' => null, + 'sieveEnabled' => false, ]; $a = new MailAccount($expected); // TODO: fix inconsistency diff --git a/tests/Integration/Sieve/SieveClientFactoryTest.php b/tests/Integration/Sieve/SieveClientFactoryTest.php new file mode 100644 index 000000000..47964c32c --- /dev/null +++ b/tests/Integration/Sieve/SieveClientFactoryTest.php @@ -0,0 +1,117 @@ +<?php + +declare(strict_types=1); + +/** + * @author Daniel Kesselberg <mail@danielkesselberg.de> + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Mail\Tests\Integration\Sieve; + +use ChristophWurst\Nextcloud\Testing\TestCase; +use Horde\ManageSieve; +use OC; +use OCA\Mail\Account; +use OCA\Mail\Db\MailAccount; +use OCA\Mail\Sieve\SieveClientFactory; +use OCP\IConfig; +use OCP\Security\ICrypto; +use PHPUnit\Framework\MockObject\MockObject; + +class SieveClientFactoryTest extends TestCase { + + /** @var ICrypto|MockObject */ + private $crypto; + + /** @var IConfig|MockObject */ + private $config; + + /** @var SieveClientFactory */ + private $factory; + + protected function setUp(): void { + parent::setUp(); + + $this->crypto = $this->createMock(ICrypto::class); + $this->config = $this->createMock(IConfig::class); + + $this->config->method('getSystemValue') + ->willReturnCallback(static function ($key, $default) { + if ($key === 'app.mail.sieve.timeout') { + return 5; + } + if ($key === 'debug') { + return false; + } + return null; + }); + + $this->config->method('getSystemValueBool') + ->with('app.mail.verify-tls-peer', true) + ->willReturn(false); + + $this->factory = new SieveClientFactory($this->crypto, $this->config); + } + + /** + * @return Account + */ + private function getTestAccount() { + $mailAccount = new MailAccount(); + $mailAccount->setId(123); + $mailAccount->setEmail('user@domain.tld'); + $mailAccount->setInboundHost('127.0.0.1'); + $mailAccount->setInboundPort(993); + $mailAccount->setInboundSslMode('ssl'); + $mailAccount->setInboundUser('user@domain.tld'); + $mailAccount->setInboundPassword(OC::$server->get(ICrypto::class)->encrypt('mypassword')); + $mailAccount->setSieveHost('127.0.0.1'); + $mailAccount->setSievePort(4190); + $mailAccount->setSieveSslMode(''); + $mailAccount->setSieveUser(''); + $mailAccount->setSievePassword(''); + return new Account($mailAccount); + } + + public function testClientConnectivity() { + $account = $this->getTestAccount(); + $this->crypto->expects($this->once()) + ->method('decrypt') + ->with($account->getMailAccount()->getInboundPassword()) + ->willReturn('mypassword'); + + $client = $this->factory->getClient($account); + $this->assertInstanceOf(ManageSieve::class, $client); + } + + public function testClientInstallScript() { + $account = $this->getTestAccount(); + $this->crypto->expects($this->once()) + ->method('decrypt') + ->with($account->getMailAccount()->getInboundPassword()) + ->willReturn('mypassword'); + + $client = $this->factory->getClient($account); + + $client->installScript('test', '#test'); + $this->assertCount(1, $client->listScripts()); + + $client->removeScript('test'); + $this->assertCount(0, $client->listScripts()); + } +} diff --git a/tests/Integration/Sieve/SieveLoggerTest.php b/tests/Integration/Sieve/SieveLoggerTest.php new file mode 100644 index 000000000..fbb526a76 --- /dev/null +++ b/tests/Integration/Sieve/SieveLoggerTest.php @@ -0,0 +1,46 @@ +<?php + +declare(strict_types=1); + +/** + * @author Daniel Kesselberg <mail@danielkesselberg.de> + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Mail\Tests\Integration\Sieve; + +use ChristophWurst\Nextcloud\Testing\TestCase; +use OCA\Mail\Sieve\SieveLogger; + +class SieveLoggerTest extends TestCase { + public function testOpenInvalidFile(): void { + $this->expectException(\InvalidArgumentException::class); + $this->expectDeprecationMessage('Unable to use "/root/horde_sieve.log" as log file for sieve.'); + new SieveLogger('/root/horde_sieve.log'); + } + + public function testWriteLog(): void { + $logFile = sys_get_temp_dir() . '/horde_sieve.log'; + @unlink($logFile); + + $logger = new SieveLogger($logFile); + $logger->debug('Test'); + unset($logger); + + $this->assertStringEqualsFile($logFile, 'Test'); + } +} diff --git a/tests/Unit/Controller/SettingsControllerTest.php b/tests/Unit/Controller/SettingsControllerTest.php index 0c97f60f7..c19b459ca 100644 --- a/tests/Unit/Controller/SettingsControllerTest.php +++ b/tests/Unit/Controller/SettingsControllerTest.php @@ -58,7 +58,12 @@ class SettingsControllerTest extends TestCase { '%USERID%@domain.com', 'mx.domain.com', 567, - 'tls' + 'tls', + false, + '', + '', + 0, + '' ); $response = $this->controller->provisioning( @@ -70,7 +75,12 @@ class SettingsControllerTest extends TestCase { '%USERID%@domain.com', 'mx.domain.com', 567, - 'tls' + 'tls', + false, + '', + '', + 0, + '' ); $this->assertInstanceOf(JSONResponse::class, $response); diff --git a/tests/Unit/Controller/SieveControllerTest.php b/tests/Unit/Controller/SieveControllerTest.php new file mode 100644 index 000000000..2d80afcbb --- /dev/null +++ b/tests/Unit/Controller/SieveControllerTest.php @@ -0,0 +1,166 @@ +<?php + +declare(strict_types=1); + +/** + * @author Daniel Kesselberg <mail@danielkesselberg.de> + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Mail\Tests\Unit\Controller; + +use ChristophWurst\Nextcloud\Testing\ServiceMockObject; +use Horde\ManageSieve\Exception; +use OCA\Mail\Account; +use OCA\Mail\Controller\SieveController; +use OCA\Mail\Db\MailAccount; +use OCA\Mail\Exception\CouldNotConnectException; +use OCA\Mail\Tests\Integration\TestCase; + +class SieveControllerTest extends TestCase { + + /** @var ServiceMockObject */ + private $serviceMock; + + /** @var SieveController */ + private $sieveController; + + + protected function setUp(): void { + parent::setUp(); + $this->serviceMock = $this->createServiceMock( + SieveController::class, + ['UserId' => '1'] + ); + $this->sieveController = $this->serviceMock->getService(); + } + + public function testUpdateAccountDisable(): void { + $mailAccountMapper = $this->serviceMock->getParameter('mailAccountMapper'); + $mailAccountMapper->expects($this->once()) + ->method('find') + ->with('1', 2) + ->willReturn(new MailAccount()); + $mailAccountMapper->expects($this->once()) + ->method('save'); + + $response = $this->sieveController->updateAccount(2, false, '', 0, '', '', ''); + $this->assertEquals(false, $response->getData()['sieveEnabled']); + } + + public function testUpdateAccountEnable(): void { + $mailAccountMapper = $this->serviceMock->getParameter('mailAccountMapper'); + $mailAccountMapper->expects($this->once()) + ->method('find') + ->with('1', 2) + ->willReturn(new MailAccount()); + $mailAccountMapper->expects($this->once()) + ->method('save'); + + $response = $this->sieveController->updateAccount(2, true, 'localhost', 4190, 'user', 'password', ''); + $this->assertEquals(true, $response->getData()['sieveEnabled']); + } + + public function testUpdateAccountEnableImapCredentials(): void { + $mailAccount = new MailAccount(); + $mailAccount->setInboundUser('imap_user'); + $mailAccount->setInboundPassword('imap_password'); + + $mailAccountMapper = $this->serviceMock->getParameter('mailAccountMapper'); + $mailAccountMapper->expects($this->once()) + ->method('find') + ->with('1', 2) + ->willReturn($mailAccount); + $mailAccountMapper->expects($this->once()) + ->method('save'); + + $response = $this->sieveController->updateAccount(2, true, 'localhost', 4190, '', '', ''); + $this->assertEquals(true, $response->getData()['sieveEnabled']); + } + + public function testUpdateAccountEnableNoConnection(): void { + $this->expectException(CouldNotConnectException::class); + $this->expectExceptionMessage('Connection to ManageSieve at localhost:4190 failed. Computer says no'); + + $mailAccountMapper = $this->serviceMock->getParameter('mailAccountMapper'); + $mailAccountMapper->expects($this->once()) + ->method('find') + ->with('1', 2) + ->willReturn(new MailAccount()); + + $sieveClientFactory = $this->serviceMock->getParameter('sieveClientFactory'); + $sieveClientFactory->expects($this->once()) + ->method('createClient') + ->willThrowException(new Exception('Computer says no')); + + $this->sieveController->updateAccount(2, true, 'localhost', 4190, 'user', 'password', ''); + } + + public function testGetActiveScript(): void { + $mailAccount = new MailAccount(); + $mailAccount->setSieveEnabled(true); + $mailAccount->setSieveHost('localhost'); + $mailAccount->setSievePort(4190); + $mailAccount->setSieveUser('user'); + $mailAccount->setSievePassword('password'); + $mailAccount->setSieveSslMode(''); + + $accountService = $this->serviceMock->getParameter('accountService'); + $accountService->expects($this->once()) + ->method('find') + ->with('1', 2) + ->willReturn(new Account($mailAccount)); + + $response = $this->sieveController->getActiveScript(2); + $this->assertEquals(['scriptName' => '', 'script' => ''], $response->getData()); + } + + public function testGetActiveScriptNoSieve(): void { + $this->expectException(CouldNotConnectException::class); + $this->expectExceptionMessage('ManageSieve is disabled'); + + $mailAccount = new MailAccount(); + $mailAccount->setSieveEnabled(false); + + $accountService = $this->serviceMock->getParameter('accountService'); + $accountService->expects($this->once()) + ->method('find') + ->with('1', 2) + ->willReturn(new Account($mailAccount)); + + $this->sieveController->getActiveScript(2); + } + + public function testUpdateActiveScript(): void { + $mailAccount = new MailAccount(); + $mailAccount->setSieveEnabled(true); + $mailAccount->setSieveHost('localhost'); + $mailAccount->setSievePort(4190); + $mailAccount->setSieveUser('user'); + $mailAccount->setSievePassword('password'); + $mailAccount->setSieveSslMode(''); + + $accountService = $this->serviceMock->getParameter('accountService'); + $accountService->expects($this->once()) + ->method('find') + ->with('1', 2) + ->willReturn(new Account($mailAccount)); + + $response = $this->sieveController->updateActiveScript(2, 'sieve script'); + $this->assertEquals([], $response->getData()); + } +} diff --git a/tests/Unit/Migration/AddSieveToProvisioningConfigTest.php b/tests/Unit/Migration/AddSieveToProvisioningConfigTest.php new file mode 100644 index 000000000..a8ddcec3c --- /dev/null +++ b/tests/Unit/Migration/AddSieveToProvisioningConfigTest.php @@ -0,0 +1,121 @@ +<?php + +declare(strict_types=1); + +/** + * @author Daniel Kesselberg <mail@danielkesselberg.de> + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Mail\Tests\Unit\Migration; + +use ChristophWurst\Nextcloud\Testing\ServiceMockObject; +use ChristophWurst\Nextcloud\Testing\TestCase; +use OCA\Mail\Migration\AddSieveToProvisioningConfig; +use OCA\Mail\Service\Provisioning\Config; +use OCP\Migration\IOutput; +use PHPUnit\Framework\MockObject\MockObject; + +class AddSieveToProvisioningConfigTest extends TestCase { + + /** @var ServiceMockObject */ + private $mock; + + /** @var AddSieveToProvisioningConfig */ + private $repairStep; + + protected function setUp(): void { + parent::setUp(); + + $this->mock = $this->createServiceMock(AddSieveToProvisioningConfig::class); + $this->repairStep = $this->mock->getService(); + } + + + public function testRunNoConfigToMigrate() { + $this->mock->getParameter('config') + ->expects($this->once()) + ->method('getAppValue') + ->with('mail', 'installed_version') + ->willReturn('1.8.0'); + + /** @var IOutput|MockObject $output */ + $output = $this->createMock(IOutput::class); + $output->expects($this->never()) + ->method('info'); + + $this->repairStep->run($output); + } + + public function testRun() { + $this->mock->getParameter('config') + ->expects($this->once()) + ->method('getAppValue') + ->with('mail', 'installed_version') + ->willReturn('1.8.0'); + + $this->mock->getParameter('configMapper') + ->expects($this->once()) + ->method('load') + ->willReturn(new Config([ + 'email' => '%USERID%@domain.com', + 'imapUser' => '%USERID%@domain.com', + 'imapHost' => 'mx.domain.com', + 'imapPort' => 993, + 'imapSslMode' => 'ssl', + 'smtpUser' => '%USERID%@domain.com', + 'smtpHost' => 'mx.domain.com', + 'smtpPort' => 567, + 'smtpSslMode' => 'tls', + ])); + + $this->mock->getParameter('configMapper') + ->expects($this->once()) + ->method('save') + ->with(new Config([ + 'email' => '%USERID%@domain.com', + 'imapUser' => '%USERID%@domain.com', + 'imapHost' => 'mx.domain.com', + 'imapPort' => 993, + 'imapSslMode' => 'ssl', + 'smtpUser' => '%USERID%@domain.com', + 'smtpHost' => 'mx.domain.com', + 'smtpPort' => 567, + 'smtpSslMode' => 'tls', + 'sieveEnabled' => false, + 'sieveHost' => '', + 'sievePort' => 4190, + 'sieveUser' => '', + 'sieveSslMode' => 'tls', + ])); + + /** @var IOutput|MockObject $output */ + $output = $this->createMock(IOutput::class); + $output->expects($this->once()) + ->method('info') + ->with('added sieve defaults to provisioning config'); + + $this->repairStep->run($output); + } + + public function testGetName() { + $this->assertEquals( + 'Add sieve defaults to provisioning config', + $this->repairStep->getName() + ); + } +} diff --git a/tests/Unit/Service/Provisioning/ConfigTest.php b/tests/Unit/Service/Provisioning/ConfigTest.php index b5222be64..839bfcb76 100644 --- a/tests/Unit/Service/Provisioning/ConfigTest.php +++ b/tests/Unit/Service/Provisioning/ConfigTest.php @@ -56,30 +56,6 @@ class ConfigTest extends TestCase { $this->assertEquals('user@domain.se', $config->buildEmail($user)); } - public function testGetImapHost() { - $config = new Config([ - 'imapHost' => 'imap.domain.com', - ]); - - $this->assertEquals('imap.domain.com', $config->getImapHost()); - } - - public function testGetImapPort() { - $config = new Config([ - 'imapPort' => 993, - ]); - - $this->assertEquals(993, $config->getImapPort()); - } - - public function testGetImapSslMode() { - $config = new Config([ - 'imapSslMode' => 'ssl', - ]); - - $this->assertEquals('ssl', $config->getImapSslMode()); - } - public function testBuildImapUserWithUserId() { $user = $this->createMock(IUser::class); $config = new Config([ @@ -125,34 +101,55 @@ class ConfigTest extends TestCase { $this->assertEquals('user@domain.se', $config->buildImapUser($user)); } - public function testGetSmtpSslMode() { + public function testBuildSmtpUserWithUserId() { + $user = $this->createMock(IUser::class); $config = new Config([ - 'smtpSslMode' => 'tls', + 'smtpUser' => '%USERID%@domain.se', ]); + $user->expects($this->exactly(2)) + ->method('getUID') + ->willReturn('test'); + $user->expects($this->once()) + ->method('getEMailAddress') + ->willReturn(null); - $this->assertEquals('tls', $config->getSmtpSslMode()); + $this->assertEquals('test@domain.se', $config->buildSmtpUser($user)); } - public function testGetSmtpHost() { + public function testBuilldSmtpUserWithEmailPlaceholder() { + $user = $this->createMock(IUser::class); $config = new Config([ - 'smtpHost' => 'smtp.domain.com', + 'smtpUser' => '%EMAIL%', ]); + $user->expects($this->any()) + ->method('getUID') + ->willReturn(null); + $user->expects($this->any()) + ->method('getEMailAddress') + ->willReturn('user@domain.se'); - $this->assertEquals('smtp.domain.com', $config->getSmtpHost()); + $this->assertEquals('user@domain.se', $config->buildSmtpUser($user)); } - public function testGetSmtpPort() { + public function testBuildSmtpUserFromDefaultEmail() { + $user = $this->createMock(IUser::class); $config = new Config([ - 'smtpPort' => 465, + 'email' => '%EMAIL%', ]); + $user->expects($this->exactly(2)) + ->method('getUID') + ->willReturn('user'); + $user->expects($this->exactly(2)) + ->method('getEMailAddress') + ->willReturn('user@domain.se'); - $this->assertEquals(465, $config->getSmtpPort()); + $this->assertEquals('user@domain.se', $config->buildImapUser($user)); } - public function testBuildSmtpUserWithUserId() { + public function testBuildSieveUserWithUserId(): void { $user = $this->createMock(IUser::class); $config = new Config([ - 'smtpUser' => '%USERID%@domain.se', + 'sieveUser' => '%USERID%@domain.se', ]); $user->expects($this->exactly(2)) ->method('getUID') @@ -161,25 +158,25 @@ class ConfigTest extends TestCase { ->method('getEMailAddress') ->willReturn(null); - $this->assertEquals('test@domain.se', $config->buildSmtpUser($user)); + $this->assertEquals('test@domain.se', $config->buildSieveUser($user)); } - public function testBuilldSmtpUserWithEmailPlaceholder() { + public function testBuilldSieveUserWithEmailPlaceholder(): void { $user = $this->createMock(IUser::class); $config = new Config([ - 'smtpUser' => '%EMAIL%', + 'sieveUser' => '%EMAIL%', ]); - $user->expects($this->any()) + $user->expects($this->once()) ->method('getUID') ->willReturn(null); - $user->expects($this->any()) + $user->expects($this->exactly(2)) ->method('getEMailAddress') ->willReturn('user@domain.se'); - $this->assertEquals('user@domain.se', $config->buildSmtpUser($user)); + $this->assertEquals('user@domain.se', $config->buildSieveUser($user)); } - public function testBuildSmtpUserFromDefaultEmail() { + public function testBuildSieveUserFromDefaultEmail(): void { $user = $this->createMock(IUser::class); $config = new Config([ 'email' => '%EMAIL%', @@ -191,6 +188,33 @@ class ConfigTest extends TestCase { ->method('getEMailAddress') ->willReturn('user@domain.se'); - $this->assertEquals('user@domain.se', $config->buildImapUser($user)); + $this->assertEquals('user@domain.se', $config->buildSieveUser($user)); + } + + /** + * @param string $key + * @param mixed $value + * @dataProvider providerTestGetter + */ + public function testGetter(string $key, $value): void { + $config = new Config([ + $key => $value + ]); + $this->assertEquals($value, $config->{'get' . ucfirst($key)}()); + } + + public function providerTestGetter(): array { + return [ + 'smtpHost' => ['smtpHost', 'smtp.domain.com'], + 'smtpPort' => ['smtpPort', 465], + 'smtpSslMode' => ['smtpSslMode', 'tls'], + 'imapHost' => ['imapHost', 'imap.domain.com'], + 'imapPort' => ['imapPort', 993], + 'imapSslMode' => ['imapSslMode', 'tls'], + 'sieveEnabled' => ['sieveEnabled', true], + 'sieveHost' => ['sieveHost', 'imap.domain.com'], + 'sievePort' => ['sieveHost', 4190], + 'sieveSslMode' => ['sieveSslMode', 'tls'], + ]; } } diff --git a/tests/Unit/Service/Provisioning/ManagerTest.php b/tests/Unit/Service/Provisioning/ManagerTest.php index 66b0accfc..81b0ede1e 100644 --- a/tests/Unit/Service/Provisioning/ManagerTest.php +++ b/tests/Unit/Service/Provisioning/ManagerTest.php @@ -178,6 +178,11 @@ class ManagerTest extends TestCase { '%USERID%@domain.com', 'mx.domain.com', 567, + 'tls', + false, + '', + '', + 0, 'tls' ); } diff --git a/tests/Unit/Service/Provisioning/TestConfig.php b/tests/Unit/Service/Provisioning/TestConfig.php index 26cf72bdc..9ebf3157a 100644 --- a/tests/Unit/Service/Provisioning/TestConfig.php +++ b/tests/Unit/Service/Provisioning/TestConfig.php @@ -39,6 +39,11 @@ class TestConfig extends Config { 'smtpHost' => 'mx.domain.com', 'smtpPort' => 567, 'smtpSslMode' => 'tls', + 'sieveEnabled' => false, + 'sieveHost' => '', + 'sievePort' => 4190, + 'sieveUser' => '', + 'sieveSslMode' => 'tls', ]); } } diff --git a/tests/Unit/Service/SetupServiceTest.php b/tests/Unit/Service/SetupServiceTest.php index 0bf424a57..70288db2a 100644 --- a/tests/Unit/Service/SetupServiceTest.php +++ b/tests/Unit/Service/SetupServiceTest.php @@ -28,6 +28,7 @@ namespace OCA\Mail\Tests\Unit\Service; use OCA\Mail\Account; use OCA\Mail\Db\MailAccount; +use OCA\Mail\IMAP\IMAPClientFactory; use OCA\Mail\Service\AccountService; use OCA\Mail\Service\AutoConfig\AutoConfig; use OCA\Mail\Service\SetupService; @@ -51,6 +52,9 @@ class SetupServiceTest extends TestCase { /** @var SmtpClientFactory|MockObject */ private $smtpClientFactory; + /** @var IMAPClientFactory|MockObject */ + private $imapClientFactory; + /** @var LoggerInterface|MockObject */ private $logger; @@ -64,6 +68,7 @@ class SetupServiceTest extends TestCase { $this->accountService = $this->createMock(AccountService::class); $this->crypto = $this->createMock(ICrypto::class); $this->smtpClientFactory = $this->createMock(SmtpClientFactory::class); + $this->imapClientFactory = $this->createMock(IMAPClientFactory::class); $this->logger = $this->createMock(LoggerInterface::class); $this->service = new SetupService( @@ -71,6 +76,7 @@ class SetupServiceTest extends TestCase { $this->accountService, $this->crypto, $this->smtpClientFactory, + $this->imapClientFactory, $this->logger ); } |