diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ef408dc5ac..5a7a84ccfa 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: diff --git a/Makefile b/Makefile index 62ae765a22..a7273174ce 100644 --- a/Makefile +++ b/Makefile @@ -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 100ec7f303..3b4d9c5cb1 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)! ]]> - 1.9.0-alpha.1 + 1.9.0-alpha.2 agpl Christoph Wurst Greta Doçi @@ -42,6 +42,7 @@ OCA\Mail\Migration\FixBackgroundJobs OCA\Mail\Migration\MakeItineraryExtractorExecutable OCA\Mail\Migration\MigrateProvisioningConfig + OCA\Mail\Migration\AddSieveToProvisioningConfig OCA\Mail\Migration\ProvisionAccounts diff --git a/appinfo/routes.php b/appinfo/routes.php index c33149e432..4b06dfa57a 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -204,6 +204,21 @@ '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 cf5f2fe0fc..1d11c42dc0 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 2706020a57..92bf0eff6b 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", @@ -1255,6 +1255,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", @@ -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 84986d3f8d..67a9a4d61e 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 829068fc2b..d65f04f038 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -50,7 +50,12 @@ public function provisioning(string $emailTemplate, 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 @@ public function provisioning(string $emailTemplate, $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 0000000000..11fc5b1866 --- /dev/null +++ b/lib/Controller/SieveController.php @@ -0,0 +1,219 @@ + + * + * 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 + * + */ + +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 b5aa8ecae2..3df3c1a678 100644 --- a/lib/Db/MailAccount.php +++ b/lib/Db/MailAccount.php @@ -77,6 +77,18 @@ * @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 @@ public function __construct(array $params = []) { $this->addType('draftsMailboxId', 'integer'); $this->addType('sentMailboxId', 'integer'); $this->addType('trashMailboxId', 'integer'); + $this->addType('sieveEnabled', 'boolean'); + $this->addType('sievePort', 'integer'); } /** @@ -192,6 +219,7 @@ public function toJson() { 'draftsMailboxId' => $this->getDraftsMailboxId(), 'sentMailboxId' => $this->getSentMailboxId(), 'trashMailboxId' => $this->getTrashMailboxId(), + 'sieveEnabled' => $this->isSieveEnabled(), ]; if (!is_null($this->getOutboundHost())) { @@ -201,6 +229,13 @@ public function toJson() { $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 0000000000..c996095ee8 --- /dev/null +++ b/lib/Exception/CouldNotConnectException.php @@ -0,0 +1,36 @@ + + * + * 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 + * + */ + +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 0000000000..e871ff1cc5 --- /dev/null +++ b/lib/Migration/AddSieveToProvisioningConfig.php @@ -0,0 +1,85 @@ + + * + * 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 + * + */ + +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 0000000000..8a52ae4fc2 --- /dev/null +++ b/lib/Migration/Version1090Date20210127160127.php @@ -0,0 +1,56 @@ +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 2af2f1827b..74b8cda8db 100644 --- a/lib/Service/Provisioning/Config.php +++ b/lib/Service/Provisioning/Config.php @@ -110,6 +110,45 @@ public function getSmtpSslMode() { return $this->data['smtpSslMode']; } + /** + * @return boolean + */ + public function getSieveEnabled(): bool { + return (bool)$this->data['sieveEnabled']; + } + + /** + * @return string + */ + public function getSieveHost() { + return $this->data['sieveHost']; + } + + /** + * @return int + */ + public function getSievePort(): int { + return (int)$this->data['sievePort']; + } + + /** + * @param IUser $user + * @return string + */ + public function buildSieveUser(IUser $user) { + if (isset($this->data['sieveUser'])) { + return $this->buildUserEmail($this->data['sieveUser'], $user); + } + return $this->buildEmail($user); + } + + /** + * @return string + */ + public function getSieveSslMode() { + return $this->data['sieveSslMode']; + } + /** * Replace %USERID% and %EMAIL% to allow special configurations * diff --git a/lib/Service/Provisioning/Manager.php b/lib/Service/Provisioning/Manager.php index c948f0e695..742c87f130 100644 --- a/lib/Service/Provisioning/Manager.php +++ b/lib/Service/Provisioning/Manager.php @@ -100,7 +100,12 @@ public function newProvisioning(string $email, 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 @@ public function newProvisioning(string $email, 'smtpHost' => $smtpHost, 'smtpPort' => $smtpPort, 'smtpSslMode' => $smtpSslMode, + 'sieveEnabled' => $sieveEnabled, + 'sieveUser' => $sieveUser, + 'sieveHost' => $sieveHost, + 'sievePort' => $sievePort, + 'sieveSslMode' => $sieveSslMode, ])); $this->provision($config); @@ -128,6 +138,19 @@ private function updateAccount(IUser $user, MailAccount $account, Config $config $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 0be5984bf4..ffd6146ac2 100644 --- a/lib/Service/SetupService.php +++ b/lib/Service/SetupService.php @@ -26,9 +26,14 @@ 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 @@ public function __construct(AutoConfig $autoConfig, 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 @@ public function createNewAccount($accountName, $emailAddress, $imapHost, $imapPo $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 532749c85f..7e4c47c98e 100644 --- a/lib/Settings/AdminSettings.php +++ b/lib/Settings/AdminSettings.php @@ -61,6 +61,11 @@ public function getForm() { '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 0000000000..4e7359741d --- /dev/null +++ b/lib/Sieve/SieveClientFactory.php @@ -0,0 +1,116 @@ + + * + * 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 + * + */ + +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 0000000000..752467cdfe --- /dev/null +++ b/lib/Sieve/SieveLogger.php @@ -0,0 +1,46 @@ + + * + * 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 + * + */ + +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 2081a12446..2ef346848a 100644 --- a/src/components/AccountSettings.vue +++ b/src/components/AccountSettings.vue @@ -67,6 +67,22 @@ :account="account" /> + +
+ +
+
+ +
+ +
+
@@ -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 0000000000..21b128b0ca --- /dev/null +++ b/src/components/SieveAccountForm.vue @@ -0,0 +1,221 @@ + + + + + diff --git a/src/components/SieveFilterForm.vue b/src/components/SieveFilterForm.vue new file mode 100644 index 0000000000..adfb2a3712 --- /dev/null +++ b/src/components/SieveFilterForm.vue @@ -0,0 +1,81 @@ +