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 @@
+
+
+
+
+ {{ t('mail', 'Oh Snap!') }}
+ {{ errorMessage }}
+
+
+
+
+
+
+
+
diff --git a/src/components/settings/ProvisionPreview.vue b/src/components/settings/ProvisionPreview.vue
index c25b687506..8d562e404b 100644
--- a/src/components/settings/ProvisionPreview.vue
+++ b/src/components/settings/ProvisionPreview.vue
@@ -43,6 +43,16 @@
ssl: smtpSslMode,
})
}}
+
+ {{
+ t('mail', 'Sieve: {user} on {host}:{port} ({ssl} encryption)', {
+ user: sieveUser,
+ host: sieveHost,
+ port: sievePort,
+ ssl: sieveSslMode,
+ })
+ }}
+
@@ -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)
+ },
},
}
diff --git a/src/components/settings/ProvisioningSettings.vue b/src/components/settings/ProvisioningSettings.vue
index 7bdd0f9bef..5d6a14d07c 100644
--- a/src/components/settings/ProvisioningSettings.vue
+++ b/src/components/settings/ProvisioningSettings.vue
@@ -210,6 +210,89 @@
+
+
+ {{ t('mail', 'Sieve') }}
+
+
+
@@ -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 0000000000..94a8c9a508
--- /dev/null
+++ b/src/errors/CouldNotConnectError.js
@@ -0,0 +1,31 @@
+/**
+ * @author Daniel Kesselberg
+ *
+ * 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
+ *
+ */
+
+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 0000000000..e6f716e036
--- /dev/null
+++ b/src/errors/ManageSieveError.js
@@ -0,0 +1,31 @@
+/**
+ * @author Daniel Kesselberg
+ *
+ * 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
+ *
+ */
+
+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 a860999a33..c4f52fbb5b 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 0000000000..62af4821af
--- /dev/null
+++ b/src/service/SieveService.js
@@ -0,0 +1,58 @@
+/**
+ * @author Daniel Kesselberg
+ *
+ * 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
+ *
+ */
+
+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 85ab40fa13..545ca31bdd 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 8bf2af9493..fd2ad11e3c 100644
--- a/tests/Integration/Db/MailAccountTest.php
+++ b/tests/Integration/Db/MailAccountTest.php
@@ -69,6 +69,7 @@ public function testToAPI() {
'draftsMailboxId' => null,
'sentMailboxId' => null,
'trashMailboxId' => null,
+ 'sieveEnabled' => false,
], $a->toJson());
}
@@ -95,6 +96,7 @@ public function testMailAccountConstruct() {
'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 0000000000..47964c32c4
--- /dev/null
+++ b/tests/Integration/Sieve/SieveClientFactoryTest.php
@@ -0,0 +1,117 @@
+
+ *
+ * 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\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 0000000000..fbb526a761
--- /dev/null
+++ b/tests/Integration/Sieve/SieveLoggerTest.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\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 0c97f60f7b..c19b459ca8 100644
--- a/tests/Unit/Controller/SettingsControllerTest.php
+++ b/tests/Unit/Controller/SettingsControllerTest.php
@@ -58,7 +58,12 @@ public function testProvisioning() {
'%USERID%@domain.com',
'mx.domain.com',
567,
- 'tls'
+ 'tls',
+ false,
+ '',
+ '',
+ 0,
+ ''
);
$response = $this->controller->provisioning(
@@ -70,7 +75,12 @@ public function testProvisioning() {
'%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 0000000000..2d80afcbb8
--- /dev/null
+++ b/tests/Unit/Controller/SieveControllerTest.php
@@ -0,0 +1,166 @@
+
+ *
+ * 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\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 0000000000..a8ddcec3c6
--- /dev/null
+++ b/tests/Unit/Migration/AddSieveToProvisioningConfigTest.php
@@ -0,0 +1,121 @@
+
+ *
+ * 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\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 b5222be645..839bfcb767 100644
--- a/tests/Unit/Service/Provisioning/ConfigTest.php
+++ b/tests/Unit/Service/Provisioning/ConfigTest.php
@@ -56,30 +56,6 @@ public function testBuildEmailWithEmailPlaceholder() {
$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 @@ public function testBuildImapUserFromDefaultEmail() {
$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 @@ public function testBuildSmtpUserWithUserId() {
->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 @@ public function testBuildSmtpUserFromDefaultEmail() {
->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 66b0accfc7..81b0ede1e6 100644
--- a/tests/Unit/Service/Provisioning/ManagerTest.php
+++ b/tests/Unit/Service/Provisioning/ManagerTest.php
@@ -178,6 +178,11 @@ public function testNewProvisioning() {
'%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 26cf72bdc3..9ebf3157a1 100644
--- a/tests/Unit/Service/Provisioning/TestConfig.php
+++ b/tests/Unit/Service/Provisioning/TestConfig.php
@@ -39,6 +39,11 @@ public function __construct() {
'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 0bf424a578..70288db2a6 100644
--- a/tests/Unit/Service/SetupServiceTest.php
+++ b/tests/Unit/Service/SetupServiceTest.php
@@ -28,6 +28,7 @@
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 @@ protected function setUp(): void {
$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 @@ protected function setUp(): void {
$this->accountService,
$this->crypto,
$this->smtpClientFactory,
+ $this->imapClientFactory,
$this->logger
);
}