Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/mail.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristoph Wurst <ChristophWurst@users.noreply.github.com>2021-02-26 18:45:36 +0300
committerGitHub <noreply@github.com>2021-02-26 18:45:36 +0300
commit06842c1ad4fc1b73218d7a49b72343ce6ac92b48 (patch)
tree5f218940949e1b00a56809f59c18c6121dcc3e49
parent164d7be1eefff8d1ea2e12c62b28311b7bfd083b (diff)
parent82ab9d00075bf00d611e819ef9c3a6cc72c12c26 (diff)
Merge pull request #4472 from nextcloud/enh/sievev1.9.0-alpha2
Sieve
-rw-r--r--.github/workflows/test.yml1
-rw-r--r--Makefile1
-rw-r--r--appinfo/info.xml3
-rw-r--r--appinfo/routes.php15
-rw-r--r--composer.json1
-rw-r--r--composer.lock41
-rw-r--r--doc/admin.md5
-rw-r--r--lib/Controller/SettingsController.php14
-rw-r--r--lib/Controller/SieveController.php219
-rw-r--r--lib/Db/MailAccount.php35
-rw-r--r--lib/Exception/CouldNotConnectException.php36
-rw-r--r--lib/Migration/AddSieveToProvisioningConfig.php85
-rw-r--r--lib/Migration/Version1090Date20210127160127.php56
-rw-r--r--lib/Service/Provisioning/Config.php39
-rw-r--r--lib/Service/Provisioning/Manager.php25
-rw-r--r--lib/Service/SetupService.php37
-rw-r--r--lib/Settings/AdminSettings.php5
-rw-r--r--lib/Sieve/SieveClientFactory.php116
-rw-r--r--lib/Sieve/SieveLogger.php46
-rw-r--r--src/components/AccountSettings.vue20
-rw-r--r--src/components/SieveAccountForm.vue221
-rw-r--r--src/components/SieveFilterForm.vue81
-rw-r--r--src/components/settings/ProvisionPreview.vue25
-rw-r--r--src/components/settings/ProvisioningSettings.vue98
-rw-r--r--src/errors/CouldNotConnectError.js31
-rw-r--r--src/errors/ManageSieveError.js31
-rw-r--r--src/errors/convert.js6
-rw-r--r--src/service/SieveService.js58
-rw-r--r--src/store/actions.js15
-rw-r--r--tests/Integration/Db/MailAccountTest.php2
-rw-r--r--tests/Integration/Sieve/SieveClientFactoryTest.php117
-rw-r--r--tests/Integration/Sieve/SieveLoggerTest.php46
-rw-r--r--tests/Unit/Controller/SettingsControllerTest.php14
-rw-r--r--tests/Unit/Controller/SieveControllerTest.php166
-rw-r--r--tests/Unit/Migration/AddSieveToProvisioningConfigTest.php121
-rw-r--r--tests/Unit/Service/Provisioning/ConfigTest.php110
-rw-r--r--tests/Unit/Service/Provisioning/ManagerTest.php5
-rw-r--r--tests/Unit/Service/Provisioning/TestConfig.php5
-rw-r--r--tests/Unit/Service/SetupServiceTest.php6
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:
diff --git a/Makefile b/Makefile
index 62ae765a2..a7273174c 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 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
);
}