diff --git a/appinfo/routes.php b/appinfo/routes.php
index 5812cb9d76..07dda222ef 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -113,6 +113,11 @@
'url' => '/proxy',
'verb' => 'GET'
],
+ [
+ 'name' => 'sieve#setActiveScript',
+ 'url' => '/api/accounts/{accountId}/scripts/active',
+ 'verb' => 'POST'
+ ],
],
'resources' => [
'accounts' => ['url' => '/api/accounts'],
@@ -122,5 +127,6 @@
'localAttachments' => ['url' => '/api/attachments'],
'messages' => ['url' => '/api/accounts/{accountId}/folders/{folderId}/messages'],
'preferences' => ['url' => '/api/preferences'],
+ 'sieve' => ['url' => '/api/accounts/{accountId}/scripts'],
]
];
diff --git a/composer.json b/composer.json
index c5720d0a5a..1e552c1d5d 100644
--- a/composer.json
+++ b/composer.json
@@ -29,11 +29,13 @@
"pear-pear.horde.org/horde_text_flowed": "^2.0.3@stable",
"pear-pear.horde.org/horde_util": "^2.5.8@stable",
"pear-pear.horde.org/horde_smtp": "^1.9.5@stable",
+ "pear-pear.horde.org/horde_managesieve": "^1.0.2@stable",
"cerdic/css-tidy": "v1.7.1",
"ezyang/htmlpurifier": "4.11.0",
"kwi/urllinker": "dev-bleeding",
"gravatarphp/gravatar": "^2.0",
- "arthurhoaro/favicon": "^1.2"
+ "arthurhoaro/favicon": "^1.2",
+ "protonlabs/libsieve-php": "^2.0"
},
"require-dev": {
"christophwurst/nextcloud": "v17.0.0-beta3",
diff --git a/composer.lock b/composer.lock
index 2641a9a49e..49693d78c3 100644
--- a/composer.lock
+++ b/composer.lock
@@ -471,6 +471,36 @@
],
"description": "Provides interfaces for sending e-mail messages and parsing e-mail addresses."
},
+ {
+ "name": "pear-pear.horde.org/Horde_ManageSieve",
+ "version": "1.0.2",
+ "dist": {
+ "type": "file",
+ "url": "https://pear.horde.org/get/Horde_ManageSieve-1.0.2.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.2.0"
+ },
+ "type": "pear-library",
+ "autoload": {
+ "classmap": [
+ ""
+ ]
+ },
+ "include-path": [
+ "/"
+ ],
+ "license": [
+ "BSD"
+ ],
+ "description": "This library implements the ManageSieve protocol (RFC 5804)."
+ },
{
"name": "pear-pear.horde.org/Horde_Mime",
"version": "2.11.0",
@@ -852,6 +882,70 @@
"LGPL-2.1"
],
"description": "These classes provide functionality useful for all kind of applications."
+ },
+ {
+ "name": "protonlabs/libsieve-php",
+ "version": "v2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ProtonMail/libsieve-php.git",
+ "reference": "b568743d701db534e15b0a3ef378e82162c74a16"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ProtonMail/libsieve-php/zipball/b568743d701db534e15b0a3ef378e82162c74a16",
+ "reference": "b568743d701db534e15b0a3ef378e82162c74a16",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "ext-simplexml": "*",
+ "php": ">=7.1.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7",
+ "protonlabs/php-coding-standard": "^1.0",
+ "squizlabs/php_codesniffer": "^3.3"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Sieve\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "GPL-3.0-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Heiko Hund",
+ "homepage": "https://sourceforge.net/u/heikoh/profile/"
+ },
+ {
+ "name": "Martin Zeman",
+ "email": "martin@protonmail.com",
+ "homepage": "https://protonmail.com"
+ },
+ {
+ "name": "Kay Lukas",
+ "email": "kay@pm.me",
+ "homepage": "https://protonmail.com"
+ },
+ {
+ "name": "Thomas Hareau",
+ "email": "thomas.hareau@protonmail.com",
+ "homepage": "https://thomas.hareau.eu"
+ }
+ ],
+ "description": "libsieve-php is a library to manage and modify sieve (RFC5228) scripts.",
+ "homepage": "https://sourceforge.net/projects/libsieve-php/",
+ "keywords": [
+ "filters",
+ "mail",
+ "sieve"
+ ],
+ "time": "2019-02-08T20:05:14+00:00"
}
],
"packages-dev": [
@@ -3122,6 +3216,7 @@
"pear-pear.horde.org/horde_text_flowed": 0,
"pear-pear.horde.org/horde_util": 0,
"pear-pear.horde.org/horde_smtp": 0,
+ "pear-pear.horde.org/horde_managesieve": 0,
"kwi/urllinker": 20,
"christophwurst/nextcloud": 10
},
diff --git a/lib/Account.php b/lib/Account.php
index 67295b639a..db2903ff54 100644
--- a/lib/Account.php
+++ b/lib/Account.php
@@ -33,6 +33,7 @@
namespace OCA\Mail;
+use Horde\ManageSieve;
use Horde_Imap_Client_Mailbox;
use Horde_Imap_Client_Socket;
use Horde_Mail_Rfc822_List;
@@ -55,7 +56,10 @@ class Account implements JsonSerializable {
private $account;
/** @var Horde_Imap_Client_Socket */
- private $client;
+ private $imapClient;
+
+ /** @var ManageSieve */
+ private $sieveClient;
/** @var ICrypto */
private $crypto;
@@ -114,9 +118,10 @@ public function getEMailAddress() {
/**
* @return Horde_Imap_Client_Socket
+ * @throws \Horde_Imap_Client_Exception
*/
public function getImapConnection() {
- if (is_null($this->client)) {
+ if (is_null($this->imapClient)) {
$host = $this->account->getInboundHost();
$user = $this->account->getInboundUser();
$password = $this->account->getInboundPassword();
@@ -144,10 +149,44 @@ public function getImapConnection() {
))];
}
}
- $this->client = new \Horde_Imap_Client_Socket($params);
- $this->client->login();
+ $this->imapClient = new \Horde_Imap_Client_Socket($params);
+ $this->imapClient->login();
}
- return $this->client;
+ return $this->imapClient;
+ }
+
+ /**
+ * @return ManageSieve
+ * @throws ManageSieve\Exception
+ */
+ public function getSieveConnection()
+ {
+ if ($this->sieveClient === null) {
+ $host = $this->account->getSieveHost();
+ $user = $this->account->getSieveUser();
+ $password = $this->account->getSievePassword();
+ $password = $this->crypto->decrypt($password);
+ $port = $this->account->getSievePort();
+ $ssl_mode = $this->convertSslMode($this->account->getSieveSslMode());
+
+ $params = [
+ 'host' => $host,
+ 'port' => $port,
+ 'user' => $user,
+ 'password' => $password,
+ 'secure' => $ssl_mode,
+ ];
+
+ // TODO: configure sieve logging
+ /*if ($this->config->getSystemValue('debug', false)) {
+ $params['logger'] = $this->config->getSystemValue('datadirectory') . '/horde_sieve.log';
+ }*/
+
+ $this->sieveClient = new ManageSieve($params);
+
+ }
+
+ return $this->sieveClient;
}
/**
@@ -201,6 +240,11 @@ public function testConnectivity(Horde_Mail_Transport $transport) {
if ($transport instanceof Horde_Mail_Transport_Smtphorde) {
$transport->getSMTPObject();
}
+
+ // connect to sieve
+ if ($this->account->getSieveHost() !== null) {
+ $this->getSieveConnection();
+ }
}
/**
diff --git a/lib/Command/CreateAccount.php b/lib/Command/CreateAccount.php
index 010b5b6460..9241065cf0 100644
--- a/lib/Command/CreateAccount.php
+++ b/lib/Command/CreateAccount.php
@@ -46,6 +46,11 @@ class CreateAccount extends Command {
const ARGUMENT_SMTP_SSL_MODE = 'smtp-ssl-mode';
const ARGUMENT_SMTP_USER = 'smtp-user';
const ARGUMENT_SMTP_PASSWORD = 'smtp-password';
+ const ARGUMENT_SIEVE_HOST = 'sieve-host';
+ const ARGUMENT_SIEVE_PORT = 'sieve-port';
+ const ARGUMENT_SIEVE_SSL_MODE = 'sieve-ssl-mode';
+ const ARGUMENT_SIEVE_USER = 'sieve-user';
+ const ARGUMENT_SIEVE_PASSWORD = 'sieve-password';
/** @var AccountService */
private $accountService;
@@ -78,6 +83,12 @@ protected function configure() {
$this->addArgument(self::ARGUMENT_SMTP_SSL_MODE, InputArgument::REQUIRED);
$this->addArgument(self::ARGUMENT_SMTP_USER, InputArgument::REQUIRED);
$this->addArgument(self::ARGUMENT_SMTP_PASSWORD, InputArgument::REQUIRED);
+
+ $this->addArgument(self::ARGUMENT_SIEVE_HOST, InputArgument::OPTIONAL);
+ $this->addArgument(self::ARGUMENT_SIEVE_PORT, InputArgument::OPTIONAL);
+ $this->addArgument(self::ARGUMENT_SIEVE_SSL_MODE, InputArgument::OPTIONAL);
+ $this->addArgument(self::ARGUMENT_SIEVE_USER, InputArgument::OPTIONAL);
+ $this->addArgument(self::ARGUMENT_SIEVE_PASSWORD, InputArgument::OPTIONAL);
}
protected function execute(InputInterface $input, OutputInterface $output) {
@@ -97,6 +108,12 @@ protected function execute(InputInterface $input, OutputInterface $output) {
$smtpUser = $input->getArgument(self::ARGUMENT_SMTP_USER);
$smtpPassword = $input->getArgument(self::ARGUMENT_SMTP_PASSWORD);
+ $sieveHost = $input->getArgument(self::ARGUMENT_SIEVE_HOST);
+ $sievePort = $input->getArgument(self::ARGUMENT_SIEVE_PORT);
+ $sieveSslMode = $input->getArgument(self::ARGUMENT_SIEVE_SSL_MODE);
+ $sieveUser = $input->getArgument(self::ARGUMENT_SIEVE_USER);
+ $sievePassword = $input->getArgument(self::ARGUMENT_SIEVE_PASSWORD);
+
$account = new MailAccount();
$account->setUserId($userId);
$account->setName($name);
@@ -114,6 +131,14 @@ protected function execute(InputInterface $input, OutputInterface $output) {
$account->setOutboundUser($smtpUser);
$account->setOutboundPassword($this->crypto->encrypt($smtpPassword));
+ if ($sieveHost !== null) {
+ $account->setSieveHost($sieveHost);
+ $account->setSievePort($sievePort);
+ $account->setSieveSslMode($sieveSslMode);
+ $account->setSieveUser($sieveUser);
+ $account->setSievePassword($this->crypto->encrypt($sievePassword));
+ }
+
$this->accountService->save($account);
$output->writeln("Account $email created");
diff --git a/lib/Command/ExportAccount.php b/lib/Command/ExportAccount.php
index ce5e4ca2fd..3200b23e57 100644
--- a/lib/Command/ExportAccount.php
+++ b/lib/Command/ExportAccount.php
@@ -64,6 +64,11 @@ protected function execute(InputInterface $input, OutputInterface $output) {
$output->writeln("- IMAP host: " . $account->getMailAccount()->getInboundHost() . ":" . $account->getMailAccount()->getInboundPort() . ", security: " . $account->getMailAccount()->getInboundSslMode());
$output->writeln("- SMTP user: " . $account->getMailAccount()->getOutboundUser());
$output->writeln("- SMTP host: " . $account->getMailAccount()->getOutboundHost() . ":" . $account->getMailAccount()->getOutboundPort() . ", security: " . $account->getMailAccount()->getOutboundSslMode());
+
+ if ($account->getMailAccount()->getSieveHost()) {
+ $output->writeln("- Sieve user: " . $account->getMailAccount()->getSieveUser());
+ $output->writeln("- Sieve host: " . $account->getMailAccount()->getSieveHost() . ":" . $account->getMailAccount()->getSievePort() . ", security: " . $account->getMailAccount()->getSieveSslMode());
+ }
}
}
diff --git a/lib/Controller/AccountsController.php b/lib/Controller/AccountsController.php
index 61fee9a7fd..16e8acc091 100644
--- a/lib/Controller/AccountsController.php
+++ b/lib/Controller/AccountsController.php
@@ -135,6 +135,7 @@ public function show($accountId): JSONResponse {
* @param string $smtpSslMode
* @param string $smtpUser
* @param string $smtpPassword
+ * @param array|null $sieveConfig
* @param bool $autoDetect
*
* @return JSONResponse
@@ -154,14 +155,22 @@ public function update(int $id,
int $smtpPort = null,
string $smtpSslMode = null,
string $smtpUser = null,
- string $smtpPassword = null): JSONResponse {
+ string $smtpPassword = null,
+ array $sieveConfig = null): JSONResponse {
$account = null;
$errorMessage = null;
try {
if ($autoDetect) {
$account = $this->setup->createNewAutoConfiguredAccount($accountName, $emailAddress, $password);
} else {
- $account = $this->setup->createNewAccount($accountName, $emailAddress, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $this->currentUserId, $id);
+
+ $account = $this->setup->createNewAccount(
+ $accountName, $emailAddress,
+ $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword,
+ $smtpHost,$smtpPort, $smtpSslMode, $smtpUser, $smtpPassword,
+ $sieveConfig,
+ $this->currentUserId, $id
+ );
}
} catch (Exception $ex) {
$errorMessage = $ex->getMessage();
@@ -249,19 +258,40 @@ public function destroy($id): JSONResponse {
* @param string $smtpSslMode
* @param string $smtpUser
* @param string $smtpPassword
+ * @param array|null $sieveConfig
* @param bool $autoDetect
*
* @return JSONResponse
* @throws ClientException
*/
- public function create(string $accountName, string $emailAddress, string $password = null, string $imapHost = null, int $imapPort = null, string $imapSslMode = null, string $imapUser = null, string $imapPassword = null, string $smtpHost = null, int $smtpPort = null, string $smtpSslMode = null, string $smtpUser = null, string $smtpPassword = null, bool $autoDetect = true): JSONResponse {
+ public function create(string $accountName,
+ string $emailAddress,
+ string $password = null,
+ string $imapHost = null,
+ int $imapPort = null,
+ string $imapSslMode = null,
+ string $imapUser = null,
+ string $imapPassword = null,
+ string $smtpHost = null,
+ int $smtpPort = null,
+ string $smtpSslMode = null,
+ string $smtpUser = null,
+ string $smtpPassword = null,
+ array $sieveConfig = null,
+ bool $autoDetect = true): JSONResponse {
$account = null;
$errorMessage = null;
try {
if ($autoDetect) {
$account = $this->setup->createNewAutoConfiguredAccount($accountName, $emailAddress, $password);
} else {
- $account = $this->setup->createNewAccount($accountName, $emailAddress, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $this->currentUserId);
+ $account = $this->setup->createNewAccount(
+ $accountName, $emailAddress,
+ $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword,
+ $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword,
+ $sieveConfig,
+ $this->currentUserId
+ );
}
} catch (Exception $ex) {
$errorMessage = $ex->getMessage();
diff --git a/lib/Controller/SieveController.php b/lib/Controller/SieveController.php
new file mode 100644
index 0000000000..5afba6fe9f
--- /dev/null
+++ b/lib/Controller/SieveController.php
@@ -0,0 +1,119 @@
+
+ *
+ * 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 OCA\Mail\Http\JSONResponse;
+use OCA\Mail\Service\AccountService;
+use OCA\Mail\Service\SieveService;
+use OCA\Mail\Service\Sieve\Script;
+use OCP\AppFramework\Controller;
+use OCP\IRequest;
+use OCP\IUser;
+use OCP\IUserSession;
+
+class SieveController extends Controller
+{
+
+ /** @var IUser */
+ private $currentUser;
+
+ /** @var SieveService */
+ private $sieveService;
+
+ /** @var AccountService */
+ private $accountService;
+
+ public function __construct($appName, IRequest $request, IUserSession $userSession,
+ SieveService $filtersService, AccountService $accountService)
+ {
+ parent::__construct($appName, $request);
+
+ $this->currentUser = $userSession->getUser();
+ $this->sieveService = $filtersService;
+ $this->accountService = $accountService;
+
+ }
+
+ /**
+ * @param int $accountId
+ * @return JSONResponse
+ * @throws AccountException
+ * @throws \Horde\ManageSieve\Exception
+ */
+ public function index(int $accountId): JSONResponse
+ {
+ return new JSONResponse($this->getSieveService($accountId)->getScriptNames());
+ }
+
+ /**
+ * @param int $accountId
+ * @param string $script
+ * @param string $script_name
+ * @return JSONResponse
+ * @throws \Horde\ManageSieve\Exception
+ */
+ public function create(int $accountId, string $script, string $script_name = null): JSONResponse
+ {
+ return new JSONResponse(
+ $this->getSieveService($accountId)->createScript($script, $script_name)
+ );
+ }
+
+ /**
+ * @param int $accountId
+ * @param string $id
+ * @return JSONResponse
+ * @throws AccountException
+ * @throws \Horde\ManageSieve\Exception
+ */
+ public function show(int $accountId, string $id): JSONResponse
+ {
+ return new JSONResponse($this->getSieveService($accountId)->getScript($id));
+ }
+
+ /**
+ * @param int $accountId
+ * @param string $scriptName
+ * @return JSONResponse
+ * @throws AccountException
+ * @throws \Horde\ManageSieve\Exception
+ */
+ public function setActiveScript(int $accountId, string $scriptName): JSONResponse
+ {
+ return new JSONResponse($this->getSieveService($accountId)->setActiveScript($scriptName));
+ }
+
+ /**
+ * @param int $accountId
+ * @return SieveService
+ * @throws AccountException
+ * @throws \Horde\ManageSieve\Exception
+ */
+ private function getSieveService(int $accountId): SieveService
+ {
+ $account = $this->accountService->find($this->currentUser->getUID(), $accountId);
+
+ return $this->sieveService->setAccount($account);
+ }
+}
diff --git a/lib/Db/MailAccount.php b/lib/Db/MailAccount.php
index 1de82e3566..ec3762c30b 100644
--- a/lib/Db/MailAccount.php
+++ b/lib/Db/MailAccount.php
@@ -57,6 +57,16 @@
* @method void setOutboundUser(string $outboundUser)
* @method string getOutboundPassword()
* @method void setOutboundPassword(string $outboundPassword)
+ * @method string getSieveHost()
+ * @method void setSieveHost(string|null $sieveHost)
+ * @method integer getSievePort()
+ * @method void setSievePort(integer|null $sievePort)
+ * @method string getSieveSslMode()
+ * @method void setSieveSslMode(string|null $sieveSslMode)
+ * @method string getSieveUser()
+ * @method void setSieveUser(string|null $sieveUser)
+ * @method string getSievePassword()
+ * @method void setSievePassword(string $sievePassword)
* @method string|null getSignature()
* @method void setSignature(string|null $signature)
* @method int getLastMailboxSync()
@@ -79,6 +89,11 @@ class MailAccount extends Entity {
protected $outboundSslMode;
protected $outboundUser;
protected $outboundPassword;
+ protected $sieveHost;
+ protected $sievePort;
+ protected $sieveSslMode;
+ protected $sieveUser;
+ protected $sievePassword;
protected $signature;
protected $lastMailboxSync;
protected $editorMode;
@@ -130,6 +145,22 @@ public function __construct(array $params=[]) {
$this->setOutboundPassword($params['smtpPassword']);
}
+ if (isset($params['sieveHost'])) {
+ $this->setSieveHost($params['sieveHost']);
+ }
+ if (isset($params['sievePort'])) {
+ $this->setSievePort($params['sievePort']);
+ }
+ if (isset($params['sieveSslMode'])) {
+ $this->setSieveSslMode($params['sieveSslMode']);
+ }
+ if (isset($params['sieveUser'])) {
+ $this->setSieveUser($params['sieveUser']);
+ }
+ if (isset($params['sievePassword'])) {
+ $this->setSievePassword($params['sievePassword']);
+ }
+
$this->addType('lastMailboxSync', 'integer');
}
@@ -156,6 +187,13 @@ public function toJson() {
$result['smtpSslMode'] = $this->getOutboundSslMode();
}
+ if ($this->getSieveHost() !== null) {
+ $result['sieveHost'] = $this->getSieveHost();
+ $result['sievePort'] = $this->getSievePort();
+ $result['sieveUser'] = $this->getSieveUser();
+ $result['sieveSslMode'] = $this->getSieveSslMode();
+ }
+
return $result;
}
}
diff --git a/lib/Migration/Version0151Date20190622040212.php b/lib/Migration/Version0151Date20190622040212.php
new file mode 100644
index 0000000000..941e6a24ea
--- /dev/null
+++ b/lib/Migration/Version0151Date20190622040212.php
@@ -0,0 +1,56 @@
+hasTable('mail_accounts')) {
+ $table = $schema->getTable('mail_accounts');
+
+ $table->addColumn('sieve_host', Type::STRING, [
+ 'notnull' => false,
+ 'length' => 64,
+ ]);
+
+ $table->addColumn('sieve_port', Type::STRING, [
+ 'notnull' => false,
+ 'length' => 6,
+ ]);
+
+ $table->addColumn('sieve_ssl_mode', Type::STRING, [
+ 'notnull' => false,
+ 'length' => 10,
+ ]);
+
+ $table->addColumn('sieve_user', Type::STRING, [
+ 'notnull' => false,
+ 'length' => 64,
+ ]);
+
+ $table->addColumn('sieve_password', Type::STRING, [
+ 'notnull' => false,
+ 'length' => 2048,
+ ]);
+ }
+
+ return $schema;
+ }
+}
diff --git a/lib/Service/AutoConfig/MxRecord.php b/lib/Service/AutoConfig/MxRecord.php
index 209c5787a2..e916137aa1 100644
--- a/lib/Service/AutoConfig/MxRecord.php
+++ b/lib/Service/AutoConfig/MxRecord.php
@@ -39,7 +39,10 @@ public function __construct(ILogger $logger) {
* @return bool|array
*/
public function query(string $host) {
- if (getmxrr($host, $mxRecords, $mxWeight) === false) {
+ if (
+ getmxrr($host, $mxRecords, $mxWeight) === false ||
+ $mxRecords[0] === ''
+ ) {
$this->logger->debug("no MX records for host <$host> found");
return false;
}
diff --git a/lib/Service/DefaultAccount/Config.php b/lib/Service/DefaultAccount/Config.php
index 4a95929a28..c5b116825d 100644
--- a/lib/Service/DefaultAccount/Config.php
+++ b/lib/Service/DefaultAccount/Config.php
@@ -107,6 +107,39 @@ public function getSmtpSslMode() {
return $this->data['smtpSslMode'];
}
+ /**
+ * @return string
+ */
+ public function getSieveHost() {
+ return $this->data['sieveHost'];
+ }
+
+ /**
+ * @return string|int
+ */
+ public function getSievePort() {
+ return $this->data['sievePort'];
+ }
+
+ /**
+ * @param IUser $user
+ * @return string|null
+ */
+ public function buildSieveUser(IUser $user) {
+ if (isset($this->data['sieveUser'])) {
+ return $this->buildUserEmail($this->data['sieveUser'], $user);
+ }
+
+ return null;
+ }
+
+ /**
+ * @return string
+ */
+ public function getSieveSslMode() {
+ return $this->data['sieveSslMode'];
+ }
+
/**
* Replace %USERID% and %EMAIL% to allow special configurations
*
diff --git a/lib/Service/DefaultAccount/Manager.php b/lib/Service/DefaultAccount/Manager.php
index a188e0fdbd..90d1815544 100644
--- a/lib/Service/DefaultAccount/Manager.php
+++ b/lib/Service/DefaultAccount/Manager.php
@@ -121,6 +121,12 @@ public function getDefaultAccount() {
$account->setOutboundSslMode($config->getSmtpSslMode());
$account->setOutboundPassword($password);
+ $account->setSieveUser($config->buildSieveUser($user));
+ $account->setSieveHost($config->getSieveHost());
+ $account->setSievePort($config->getSievePort());
+ $account->setSieveSslMode($config->getSieveSslMode());
+ $account->setSievePassword($account->getSieveHost() ? $password : null);
+
return $account;
}
diff --git a/lib/Service/SetupService.php b/lib/Service/SetupService.php
index 1fa5ba8eea..05ed3406b0 100644
--- a/lib/Service/SetupService.php
+++ b/lib/Service/SetupService.php
@@ -93,9 +93,16 @@ public function createNewAutoConfiguredAccount($accountName, $emailAddress, $pas
* @param string $smtpSslMode
* @param string $smtpUser
* @param string $smtpPassword
+ * @param array|null $sieveConfig
* @param string $uid
+ * @param null $accountId
+ * @return null|Account
*/
- public function createNewAccount($accountName, $emailAddress, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $uid, $accountId = null) {
+ public function createNewAccount($accountName, $emailAddress,
+ $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword,
+ $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword,
+ $sieveConfig,
+ $uid, $accountId = null) {
$this->logger->info('Setting up manually configured account');
$newAccount = new MailAccount([
'accountId' => $accountId,
@@ -110,12 +117,20 @@ public function createNewAccount($accountName, $emailAddress, $imapHost, $imapPo
'smtpPort' => $smtpPort,
'smtpSslMode' => $smtpSslMode,
'smtpUser' => $smtpUser,
- 'smtpPassword' => $smtpPassword
+ 'smtpPassword' => $smtpPassword,
]);
$newAccount->setUserId($uid);
$newAccount->setInboundPassword($this->crypto->encrypt($newAccount->getInboundPassword()));
$newAccount->setOutboundPassword($this->crypto->encrypt($newAccount->getOutboundPassword()));
+ if ($sieveConfig !== null) {
+ $newAccount->setSieveHost($sieveConfig['host']);
+ $newAccount->setSievePort($sieveConfig['port']);
+ $newAccount->setSieveSslMode($sieveConfig['sslMode']);
+ $newAccount->setSieveUser($sieveConfig['user']);
+ $newAccount->setSievePassword($this->crypto->encrypt($sieveConfig['password']));
+ }
+
$account = new Account($newAccount);
$this->logger->debug('Connecting to account {account}', ['account' => $newAccount->getEmail()]);
$transport = $this->smtpClientFactory->create($account);
diff --git a/lib/Service/Sieve/Script.php b/lib/Service/Sieve/Script.php
new file mode 100644
index 0000000000..668182379c
--- /dev/null
+++ b/lib/Service/Sieve/Script.php
@@ -0,0 +1,100 @@
+
+ *
+ * @author 2019 Pierre Gordon
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+namespace OCA\Mail\Service\Sieve;
+
+use Sieve\SieveException;
+use Sieve\SieveParser;
+
+class Script
+{
+
+ public const SCRIPT_NAME = 'nextcloud';
+ public const SCRIPT_CUSTOM_BASE_NAME = 'nextcloud.filter.';
+ public const TYPE_CUSTOM = 'custom';
+ public const TYPE_SIMPLE = 'simple';
+
+ /** @var string */
+ private $name;
+
+ /** @var string */
+ private $script;
+
+ /** @var string */
+ private $parseError;
+
+ public function __construct(string $name, string $script)
+ {
+ $this->name = $name;
+ $this->script = $script;
+ $this->parseError = '';
+ }
+
+ /**
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * @return string
+ */
+ public function getScript(): string
+ {
+ return $this->script;
+ }
+
+ /**
+ * @return string
+ */
+ public function getParseError(): string
+ {
+ return $this->parseError;
+ }
+
+ /**
+ * @return int
+ */
+ public function getSize(): int
+ {
+ return strlen($this->script);
+ }
+
+ public function isValid(): bool
+ {
+ $parser = new SieveParser();
+
+ try {
+ $parser->parse($this->script);
+ return true;
+ } catch (SieveException $e) {
+ $this->parseError = $e->getMessage();
+ return false;
+ }
+ }
+}
diff --git a/lib/Service/Sieve/ScriptFactory.php b/lib/Service/Sieve/ScriptFactory.php
new file mode 100644
index 0000000000..875b32ef84
--- /dev/null
+++ b/lib/Service/Sieve/ScriptFactory.php
@@ -0,0 +1,47 @@
+
+ *
+ * @author 2019 Pierre Gordon
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+namespace OCA\Mail\Service\Sieve;
+
+class ScriptFactory {
+
+ /**
+ * @param string $script
+ * @param string $script_name
+ * @return Script
+ */
+ public function createCustom(string $script, string $script_name): Script {
+ return new Script(Script::SCRIPT_CUSTOM_BASE_NAME . $script_name, $script);
+ }
+
+ /**
+ * @param string $script
+ * @return Script
+ */
+ public function createSimple(string $script): Script {
+ return new Script(Script::SCRIPT_NAME, $script);
+ }
+}
diff --git a/lib/Service/SieveService.php b/lib/Service/SieveService.php
new file mode 100644
index 0000000000..b191f97a38
--- /dev/null
+++ b/lib/Service/SieveService.php
@@ -0,0 +1,151 @@
+
+ *
+ * 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\Service;
+
+use Horde\ManageSieve;
+use OCA\Mail\Account;
+use OCA\Mail\Service\Sieve\Script;
+use OCA\Mail\Service\Sieve\ScriptFactory;
+
+class SieveService
+{
+
+ /** @var ManageSieve */
+ private $sieveClient;
+
+ /** @var ScriptFactory */
+ private $scriptFactory;
+
+ /**
+ * FiltersService constructor.
+ * @param ScriptFactory $scriptFactory
+ */
+ public function __construct(ScriptFactory $scriptFactory)
+ {
+ $this->scriptFactory = $scriptFactory;
+ }
+
+ /**
+ * @param Account $account
+ * @return $this
+ * @throws ManageSieve\Exception
+ */
+ public function setAccount(Account $account): SieveService
+ {
+ $this->sieveClient = $account->getSieveConnection();
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getScriptNames(): array
+ {
+ $scriptNames = $this->sieveClient->listScripts();
+ $activeScriptName = $this->sieveClient->getActive();
+
+ $scripts = array_fill_keys(['active', 'entries'], null);
+
+ foreach ($scriptNames as $scriptName) {
+ if ($scriptName === $activeScriptName) {
+ $scripts['active'] = $scriptName;
+ }
+
+ $scripts['entries'][] = $scriptName;
+ }
+
+ return compact('scripts');
+ }
+
+ /**
+ * @param string $scriptName
+ * @return array
+ * @throws ManageSieve\Exception
+ */
+ public function setActiveScript(string $scriptName): array
+ {
+ $isActive = false;
+ $scriptNames = $this->sieveClient->listScripts();
+ $activeScriptName = $this->sieveClient->getActive();
+
+ if ($scriptName !== $activeScriptName && in_array($scriptName, $scriptNames)) {
+ $this->sieveClient->setActive($scriptName);
+ $isActive = true;
+ }
+
+ return compact('isActive');
+ }
+
+ /**
+ * @param string $scriptName
+ * @return array
+ * @throws ManageSieve\Exception
+ */
+ public function getScript(string $scriptName): array
+ {
+ return ['script' => $this->sieveClient->getScript($scriptName)];
+ }
+
+ /**
+ * @param string $filter
+ * @param string $script_name
+ * @return array
+ * @throws ManageSieve\Exception
+ */
+ public function createScript(string $filter, ?string $script_name): array
+ {
+ if ($script_name !== null) {
+ $script = $this->scriptFactory->createCustom($filter, $script_name);
+ return $this->installRawScript($script);
+ }
+
+ return ['status' => 'error', 'message' => 'Not implemented'];
+ }
+
+ /**
+ * @param Script $script
+ * @return array
+ * @throws ManageSieve\Exception
+ */
+ private function installRawScript(Script $script): array
+ {
+ if ($script->isValid()) {
+ $this->installScript($script);
+ return ['status' => 'success'];
+ } else {
+ return ['status' => 'error', 'message' => $script->getParseError()];
+ }
+ }
+
+ /**
+ * @param Script $script
+ * @throws ManageSieve\Exception
+ */
+ private function installScript(Script $script)
+ {
+ if ($this->sieveClient->hasSpace($script->getName(), $script->getSize())) {
+ $this->sieveClient->installScript($script->getName(), $script->getScript(), true);
+ }
+ }
+}
diff --git a/package.json b/package.json
index eadfbf342b..143a6f8a83 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,8 @@
"test:watch": "mochapack -w --webpack-config webpack.test.js --require src/tests/setup.js \"src/tests/**/*.spec.js\""
},
"dependencies": {
+ "@saeris/vue-spinners": "^1.0.8",
+ "@vue/babel-preset-app": "^3.11.0",
"@bundle-analyzer/webpack-plugin": "^0.5.0",
"@ckeditor/ckeditor5-basic-styles": "^11.1.4",
"@ckeditor/ckeditor5-build-balloon": "^12.4.0",
@@ -56,12 +58,15 @@
"vue-autosize": "^1.0.2",
"vue-click-outside": "^1.0.7",
"vue-infinite-scroll": "^2.0.2",
+ "vue-m-message": "^2.0.0",
"vue-on-click-outside": "^1.0.3",
"vue-router": "^3.1.3",
"vue-scroll": "^2.1.12",
+ "vue-select": "^3.1.0",
"vue-shortkey": "^3.1.7",
"vue-slide-up-down": "^2.0.0",
"vue-tabs-component": "^1.5.0",
+ "vuedraggable": "^2.23.0",
"vuex": "^3.1.1",
"vuex-router-sync": "^5.0.0"
},
diff --git a/src/components/AccountForm.vue b/src/components/AccountForm.vue
index ec56627d10..5dfd10b40b 100644
--- a/src/components/AccountForm.vue
+++ b/src/components/AccountForm.vue
@@ -227,6 +227,14 @@
/>
+
+
+
+
+
+
+
+
@@ -235,10 +243,12 @@
import {Tab, Tabs} from 'vue-tabs-component'
import Logger from '../logger'
+import SieveForm from './SieveForm'
export default {
name: 'AccountForm',
components: {
+ SieveForm,
Tab,
Tabs,
},
@@ -263,7 +273,7 @@ export default {
},
data() {
const fromAccountOr = (prop, def) => {
- if (this.account !== undefined) {
+ if (this.account !== undefined && this.account.hasOwnProperty(prop)) {
return this.account[prop]
} else {
return def
@@ -273,6 +283,7 @@ export default {
return {
loading: false,
mode: 'auto',
+ sieveEnabled: false,
autoConfig: {
accountName: this.displayName,
emailAddress: this.email,
@@ -292,14 +303,36 @@ export default {
smtpUser: fromAccountOr('smtpUser', ''),
smtpPassword: '',
},
+ sieveConfig: {
+ host: fromAccountOr('sieveHost', ''),
+ port: fromAccountOr('sievePort', 4190),
+ sslMode: fromAccountOr('sieveSslMode', 'tls'),
+ user: fromAccountOr('sieveUser', ''),
+ password: '',
+ },
submitButtonText: this.account ? t('mail', 'Save') : t('mail', 'Connect'),
}
},
+ created() {
+ this.sieveEnabled = this.sieveConfig.host !== ''
+ },
computed: {
settingsPage() {
return this.account !== undefined
},
},
+ watch: {
+ sieveEnabled(enabled) {
+ if (enabled) {
+ if (this.sieveConfig.user === '') {
+ this.sieveConfig.user = this.autoConfig.emailAddress
+ }
+ if (this.sieveConfig.password === '') {
+ this.sieveConfig.password = this.autoConfig.password
+ }
+ }
+ },
+ },
methods: {
onModeChanged(e) {
this.mode = e.tab.id
@@ -352,17 +385,12 @@ export default {
}
},
saveChanges() {
- if (this.mode === 'auto') {
- return this.save({
- autoDetect: true,
- ...this.autoConfig,
- })
- } else {
- return this.save({
- autoDetect: false,
- ...this.manualConfig,
- })
- }
+ const autoDetect = this.mode === 'auto'
+ const config = autoDetect ? this.autoConfig : this.manualConfig
+
+ config.sieveConfig = this.sieveEnabled ? this.sieveConfig : null
+
+ return this.save({autoDetect, ...config})
},
onSubmit: function() {
this.loading = true
@@ -394,39 +422,44 @@ export default {
padding-top: 20px;
}
-.tabs-component-panels label {
+#account-form input,
+#account-form select {
+ margin-bottom: 10px;
+}
+
+#account-form label {
text-align: left;
width: 100%;
display: inline-block;
}
-.tabs-component-panels input,
-.tabs-component-panels select {
- margin-bottom: 10px;
-}
-
-
-
+
+
diff --git a/src/components/AppSettingsMenu.vue b/src/components/AppSettingsMenu.vue
index bc2fc156cb..9cfcd37bcb 100644
--- a/src/components/AppSettingsMenu.vue
+++ b/src/components/AppSettingsMenu.vue
@@ -23,6 +23,12 @@
+
+
+ {{ t('mail', 'Filter') }}
+
+
+
{{
t('mail', 'Looking for a way to encrypt your emails? Install the Mailvelope browser extension!')
diff --git a/src/components/FilterSettings.vue b/src/components/FilterSettings.vue
new file mode 100644
index 0000000000..1c0f9f0fb7
--- /dev/null
+++ b/src/components/FilterSettings.vue
@@ -0,0 +1,165 @@
+
+
+
{{ t('mail', 'Filters') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/SieveActions.vue b/src/components/SieveActions.vue
new file mode 100644
index 0000000000..e6af4fcdcc
--- /dev/null
+++ b/src/components/SieveActions.vue
@@ -0,0 +1,113 @@
+
+
+
+
{{ t("mail", "Do:") }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t("mail", "Delete") }}
+
+
+
+
+
+ {{ t("mail", "Add") }}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/SieveFilterSets.vue b/src/components/SieveFilterSets.vue
new file mode 100644
index 0000000000..28b44d60dd
--- /dev/null
+++ b/src/components/SieveFilterSets.vue
@@ -0,0 +1,270 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filterSet.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/SieveFilters.vue b/src/components/SieveFilters.vue
new file mode 100644
index 0000000000..d2463a28e6
--- /dev/null
+++ b/src/components/SieveFilters.vue
@@ -0,0 +1,201 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/SieveFiltersRaw.vue b/src/components/SieveFiltersRaw.vue
new file mode 100644
index 0000000000..1f88782ff5
--- /dev/null
+++ b/src/components/SieveFiltersRaw.vue
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+ {{ t("mail", "Filterset not parsable. Reason: ")+$store.getters.getFilterSetByID(accountID,filterSetID).parseError }}
+
+
+
+
+
+ Edit
+
+
+
+ Save
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/SieveForm.vue b/src/components/SieveForm.vue
new file mode 100644
index 0000000000..d05d2568f9
--- /dev/null
+++ b/src/components/SieveForm.vue
@@ -0,0 +1,76 @@
+
+
+
+
+
diff --git a/src/components/SieveTests.vue b/src/components/SieveTests.vue
new file mode 100644
index 0000000000..e71cfa406d
--- /dev/null
+++ b/src/components/SieveTests.vue
@@ -0,0 +1,128 @@
+
+
+
+
{{ t("mail", "For incoming mail:") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t("mail", "Delete") }}
+
+
+
+
+
+ {{ t("mail", "Add") }}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/router.js b/src/router.js
index 7544bcb3a4..cfa15421be 100644
--- a/src/router.js
+++ b/src/router.js
@@ -6,6 +6,7 @@ const AccountSettings = () => import('./views/AccountSettings')
const Home = () => import('./views/Home')
const KeyboardShortcuts = () => import('./views/KeyboardShortcuts')
const Setup = () => import('./views/Setup')
+const SieveView = () => import('./views/SieveView')
Vue.use(Router)
@@ -48,5 +49,10 @@ export default new Router({
name: 'setup',
component: Setup,
},
+ {
+ path: '/filter',
+ name: 'filter',
+ component: SieveView,
+ },
],
})
diff --git a/src/service/FiltersService.js b/src/service/FiltersService.js
new file mode 100644
index 0000000000..f306756bf8
--- /dev/null
+++ b/src/service/FiltersService.js
@@ -0,0 +1,570 @@
+/**
+ * @copyright Copyright (c) 2019, Merlin Mittelbach
+ *
+ * @author 2019, Merlin Mittelbach
+ * @author Pierre Gordon
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+import {generateUrl} from 'nextcloud-router'
+import HttpClient from 'nextcloud-axios'
+
+const generateScriptsUrl = (accountId, path = null) => {
+ const url = `/apps/mail/api/accounts/{id}/scripts${path ? `/${path}` : ''}`
+
+ return generateUrl(url, {id: accountId})
+}
+
+export const getScripts = accountId => {
+ const url = generateScriptsUrl(accountId)
+
+ return HttpClient.get(url).then(resp => resp.data.scripts)
+}
+
+export const getScript = (accountId, scriptName) => {
+ const url = generateScriptsUrl(accountId, scriptName)
+
+ return HttpClient.get(url).then(resp => resp.data.script)
+}
+
+export const setActiveScript = (accountId, scriptName) => {
+ const url = generateScriptsUrl(accountId, 'active')
+
+ return HttpClient.post(url, {scriptName}).then(resp => resp.data.isActive)
+}
+
+export const saveCustomScript = (accountId, script) => {
+ const url = generateScriptsUrl(accountId)
+ return HttpClient.post(url, {script}).then(resp => resp.data)
+}
+
+export const ParseSieveError = function(message) {
+ this.message = message
+}
+
+export const escapeSieve = function(string) {
+ return string
+ // if already escaped unescape it
+ .replace(/\\\\/g, "\\")
+ .replace(/\\"/g, "\"")
+ // then escape
+ .replace(/\\/g, "\\\\")
+ .replace(/"/g, "\\\"")
+}
+
+/*
+register matchtype here
+key: [string] sieve matchtype value
+props:
+ name: [string] matchtype name (required)
+ req: [string] requirement to be imported to sieve script (not required)
+*/
+export const matchTypeBlueprint = {
+ ":is": {
+ "name": t("mail", "is"),
+ },
+ ":contains": {
+ "name": t("mail", "contains"),
+ },
+ ":matches": {
+ "name": t("mail", "matches"),
+ },
+ ":regex": {
+ "req": "regex",
+ "name": t("mail", "matches regular expression"),
+ },
+}
+
+/*
+register test here
+key: [string] sieve test id
+props:
+ name: [string] test name
+ req: [string] requirement to be imported to sieve script
+ matchTypes: [array of matchTypes] all possible matchTypes to this test
+ opts_default: [object] default test options
+ make: method to create sieve script
+ input: test object (name, opts)
+ output: req = array of requirements
+ script = rendered sieve test string
+ parse: method to parse sieve script
+ input: test array e.g.: 'not header ":is" "to" "test"'' -> ["not","header",":is","to","test"]
+ output: if test array is recognised: new test object
+ else: undefined
+*/
+export const sieveTestsBlueprint = {
+ "subject": {
+ "name": t("mail", "Subject"),
+ "matchTypes": [":is", ":contains", ":matches", ":regex"],
+ "opts_default": {
+ "negate": false,
+ "matchType": ":is",
+ "value": "",
+ },
+ "make": function(test) {
+ let reqs = new Set([])
+ if (matchTypeBlueprint[test.opts.matchType].req !== undefined) {
+ reqs.add(req)
+ }
+ const negate = test.opts.negate ? "not " : ""
+ const escapedValue = escapeSieve(test.opts.value)
+ let script = `${negate}header ${test.opts.matchType} \"subject\" \"${escapedValue}\"`
+ return {reqs, script}
+ },
+ "parse": function(test) {
+ let out = {
+ "type": "subject",
+ "opts": {
+ "negate": false,
+ "matchType": ":is",
+ "value": "",
+ }
+ }
+ if (test[0].toLowerCase() === "not") {
+ out.opts.negate = true
+ test.shift()
+ }
+ if (test[0].toLowerCase() === "header" && test[2].toLowerCase() === "subject") {
+ if (this.matchTypes.indexOf(test[1]) > -1) {
+ out.opts.matchType = test[1]
+ out.opts.value = test[3]
+ return out
+ } else {
+ throw new ParseSieveError(`matchtype not supported: '${test[1]}'`)
+ }
+ } else {
+ return undefined
+ }
+ },
+ },
+ "from": {
+ "name": t("mail", "From"),
+ "matchTypes": [":is", ":contains", ":matches", ":regex"],
+ "opts_default": {
+ "negate": false,
+ "matchType": ":is",
+ "value": "",
+ },
+ "make": function(test) {
+ let reqs = new Set([])
+ if (matchTypeBlueprint[test.opts.matchType].req !== undefined) {
+ reqs.add(matchTypeBlueprint[test.opts.matchType].req)
+ }
+ const negate = test.opts.negate ? "not " : ""
+ const escapedValue = escapeSieve(test.opts.value)
+ let script = `${negate}header ${test.opts.matchType} \"from\" \"${escapedValue}\"`
+ return {reqs, script}
+ },
+ "parse": function(test) {
+ let out = {
+ "type": "from",
+ "opts": {
+ "negate": false,
+ "matchType": ":is",
+ "value": "",
+ }
+ }
+ if (test[0].toLowerCase() === "not") {
+ out.opts.negate = true
+ test.shift()
+ }
+ if (test[0].toLowerCase() === "header" && test[2].toLowerCase() === "from") {
+ if (this.matchTypes.indexOf(test[1]) > -1) {
+ out.opts.matchType = test[1]
+ out.opts.value = test[3]
+ return out
+ } else {
+ throw new ParseSieveError(`matchtype not supported: '${test[1]}'`)
+ }
+ } else {
+ return undefined
+ }
+ },
+ },
+ "to": {
+ "name": t("mail", "To"),
+ "matchTypes": [":is", ":contains", ":matches", ":regex"],
+ "opts_default": {
+ "negate": false,
+ "matchType": ":is",
+ "value": "",
+ },
+ "make": function(test) {
+ let reqs = new Set([])
+ if (matchTypeBlueprint[test.opts.matchType].req !== undefined) {
+ reqs.add(matchTypeBlueprint[test.opts.matchType].req)
+ }
+ const negate = test.opts.negate ? "not " : ""
+ const escapedValue = escapeSieve(test.opts.value)
+ let script = `${negate}header ${test.opts.matchType} \"to\" \"${escapedValue}\"`
+ return {reqs, script}
+ },
+ "parse": function(test) {
+ let out = {
+ "type": "to",
+ "opts": {
+ "negate": false,
+ "matchType": ":is",
+ "value": "",
+ }
+ }
+ if (test[0].toLowerCase() === "not") {
+ out.opts.negate = true
+ test.shift()
+ }
+ if (test[0].toLowerCase() === "header" && test[2].toLowerCase() === "to") {
+ if (this.matchTypes.indexOf(test[1]) > -1) {
+ out.opts.matchType = test[1]
+ out.opts.value = test[3]
+ return out
+ } else {
+ throw new ParseSieveError(`matchtype not supported: '${test[1]}'`)
+ }
+ } else {
+ return undefined
+ }
+ },
+ },
+ "exists": {
+ "name": t("mail", "Header exists"),
+ "opts_default": {
+ "negate": false,
+ "value": "",
+ },
+ "make": function(test) {
+ let reqs = new Set([])
+ const negate = test.opts.negate ? "not " : ""
+ const escapedValue = escapeSieve(test.opts.value)
+ let script = `${negate}exists \"${escapedValue}\"`
+ return {reqs, script}
+ },
+ "parse": function(test) {
+ let out = {
+ "type": "exists",
+ "opts": {
+ "negate": false,
+ "value": "",
+ }
+ }
+ if (test[0].toLowerCase() === "not") {
+ out.opts.negate = true
+ test.shift()
+ }
+ if (test[0].toLowerCase() === "exists") {
+ out.opts.value = test[1]
+ return out
+ } else {
+ return undefined
+ }
+ },
+ },
+ "content": {
+ "name": t("mail", "Content"),
+ "matchTypes": [":is", ":contains", ":matches", ":regex"],
+ "req": "body",
+ "opts_default": {
+ "negate": false,
+ "matchType": ":is",
+ "value": "",
+ },
+ "make": function(test) {
+ let reqs = new Set([this.req])
+ if (matchTypeBlueprint[test.opts.matchType].req !== undefined) {
+ reqs.add(matchTypeBlueprint[test.opts.matchType].req)
+ }
+ const negate = test.opts.negate ? "not " : ""
+ const escapedValue = escapeSieve(test.opts.value)
+ let script = `${negate}body :text ${test.opts.matchType} \"${escapedValue}\"`
+ return {reqs, script}
+ },
+ "parse": function(test) {
+ let out = {
+ "type": "content",
+ "opts": {
+ "negate": false,
+ "matchType": ":is",
+ "value": "",
+ }
+ }
+ if (test[0].toLowerCase() === "not") {
+ out.opts.negate = true
+ test.shift()
+ }
+ if (test[0].toLowerCase() === "body" && test.indexOf(":text") > -1) {
+ const matchTypeIndex = test.indexOf(":text") === 1 ? 2 : 1
+ if (this.matchTypes.indexOf(test[matchTypeIndex]) > -1) {
+ out.opts.matchType = test[matchTypeIndex]
+ out.opts.value = test[3]
+ return out
+ } else {
+ throw new ParseSieveError(`matchtype not supported: '${test[1]}'`)
+ }
+ } else {
+ return undefined
+ }
+ },
+ },
+}
+/*
+register action here
+key: [string] sieve action id
+props:
+ name: [string] action name
+ req: [string] requirement to be imported to sieve script
+ opts_default: [object] default action options
+ make: method to create sieve script
+ input: action object (name, opts)
+ output: req = array of requirements
+ script = rendered sieve action string
+ parse: method to parse sieve script
+ input: action array e.g.: 'fileinto :copy "INBOX"' -> ["fileinto", ":copy", "INBOX"]
+ output: if test array is recognised: new test object
+ else: undefined
+*/
+export const sieveActionsBlueprint = {
+ "move": {
+ "name": t("mail", "Move mail into"),
+ "opts_default": {
+ "value": "SU5CT1g=",
+ },
+ "make": function(action) {
+ let reqs = ["fileinto"]
+ let folder = atob(action.opts.value)
+ let script = `fileinto ${folder};`
+ return {reqs, script}
+ },
+ "parse": function(action) {
+ let out = {
+ "type": "move",
+ "opts": {
+ "value": btoa(action[1]),
+ }
+ }
+ if (action[0].toLowerCase() === "fileinto" && action.length === 2) {
+ return out
+ } else {
+ return undefined
+ }
+ },
+ },
+ "copy": {
+ "name": t("mail", "Copy mail into"),
+ "opts_default": {
+ "value": "SU5CT1g=",
+ },
+ "make": function(action) {
+ let reqs = ["fileinto", "copy"]
+ let folder = atob(action.opts.value)
+ let script = `fileinto :copy ${folder};`
+ return {reqs, script}
+ },
+ "parse": function(action) {
+ let out = {
+ "type": "copy",
+ "opts": {
+ "value": btoa(action[2]),
+ }
+ }
+ if (action[0].toLowerCase() === "fileinto" && action[1] === ":copy" && action.length === 3) {
+ return out
+ } else {
+ return undefined
+ }
+ },
+ },
+}
+
+export const makeSieveScript = function(filters){
+ if (filters === undefined) {
+ return ""
+ } else {
+ let raw = ""
+ let reqSet = new Set()
+ for (const filter of filters) {
+ let tests = []
+ for (const test of filter.tests.list){
+ const {reqs, script} = sieveTestsBlueprint[test.type].make(test)
+ tests.push(script)
+ for (const req of reqs) {
+ reqSet.add(req)
+ }
+ }
+ let actions = []
+ for (const action of filter.actions) {
+ const {reqs, script} = sieveActionsBlueprint[action.type].make(action)
+ actions.push(script)
+ for (const req of reqs) {
+ reqSet.add(req)
+ }
+ }
+
+ tests = tests.join(", ")
+ actions = actions.join("\n\t")
+
+ raw +=
+`
+# rule:[${filter.name}]
+if ${filter.tests.type}(${tests})
+{
+ ${actions}
+}
+`
+ }
+ const reqs = Array.from(reqSet).join("\",\"")
+ if (reqs === "") {
+ return `${raw}`
+ } else {
+ return `require [\"${reqs}\"];
+${raw}`
+ }
+ }
+}
+
+export const parseSieveScript = (raw) => {
+ if (raw === "" || raw === undefined) {
+ return []
+ } else {
+ let filters = []
+ let match
+
+ // throw away require list
+ const requireRegex = /require[\s\S]*?;\s*/gm
+ raw = raw.replace(requireRegex,"")
+
+ let strings = {}
+ let counter = 0
+ // separate multiline strings
+ const multilineRegex = /text:\s*?(.[\s\S]*?)^\.\n/gm
+ match = multilineRegex.exec(raw)
+ while (match !== null) {
+ strings[`__${counter}__`] = match[1]
+ raw = raw.replace(match[0], `__${counter}__`)
+ counter += 1
+ match = multilineRegex.exec(raw)
+ }
+ // separate one line strings
+ const matches = raw.match(/"[^"\\]*(?:\\.[^"\\]*)*"/gm)
+ if (matches !== null) {
+ for (const match of matches) {
+ raw = raw.replace(match, `__${counter}__`)
+ strings[`__${counter}__`] = match.slice(1,-1)
+ counter += 1
+ }
+ }
+ console.log
+ // unescape strings
+ for (const key of Object.keys(strings)) {
+ strings[key] = strings[key]
+ .replace(/\\"/g, "\"")
+ .replace(/\\\\/g, "\\")
+ }
+
+ // throw away comments
+ const commentRegex = /#(?!\s+rule:).*|\/\*.*\*\/|\/\*(.*\n)+\*\//gm
+ raw = raw.replace(commentRegex,"")
+
+ // get commands/filters
+ const commandRegex = /(?:# rule:\[(\w+)\]\s*)?^(\w+)\s*([^;]*?)\s*\{\s*([^\}]*?)\s*\}\s*/gm
+ match = commandRegex.exec(raw)
+ let newFilterID = 0
+ while (match !== null) {
+ const [ , name, command_type, testsArray, actionsRaw] = match
+ let filter = {
+ "id": newFilterID,
+ "name": name !== undefined ? name : "Filter_"+newFilterID,
+ "tests": {
+ "type": "allof",
+ "list": [],
+ },
+ "actions": [], /*[{
+ "id": 0,
+ "type": "move",
+ "opts": JSON.parse(JSON.stringify(sieveActionsBlueprint["move"].opts_default))
+ }],*/
+ }
+
+ if (command_type === "if") {
+
+ // parse tests
+ const testsArrayRegex = /(allof|anyof)?\s*\(?\s*([\s\S]*.)\s*\)/gm
+ const testMatch = testsArrayRegex.exec(testsArray)
+ let tests
+ if (testMatch === null) {
+ tests = [ testsArray ]
+ } else {
+ filter.tests.type = testMatch[1]
+ tests = testMatch[2].split(/\s*,\s*/)
+ }
+ let newTestID = 0
+ for (let test of tests) {
+ test = test.split(/\s+/)
+ let newTest
+ for (const testType in sieveTestsBlueprint) {
+ if (newTest === undefined){
+ newTest = sieveTestsBlueprint[testType].parse(test.map(x => strings[x] === undefined ? x : strings[x]))
+ }
+ }
+ if (newTest === undefined) {
+ let error = test.map(x => strings[x] === undefined ? x : strings[x])
+ error = error.join(" ")
+ throw new ParseSieveError(`test couldn't be interpreted: '${error}'`)
+ } else {
+ newTest["id"] = newTestID
+ newTestID += 1
+ }
+ filter.tests.list.push(newTest)
+ }
+
+ // parse actions
+ const actions = actionsRaw.split(/;\s*/).filter(x => x !== "")
+ let newActionID = 0
+ for (let action of actions) {
+ action = action.split(/\s+/)
+ let newAction
+ for (const actionType in sieveActionsBlueprint) {
+ if (newAction === undefined){
+ newAction = sieveActionsBlueprint[actionType].parse(action.map(x => strings[x] === undefined ? x : strings[x]))
+ }
+ }
+ if (newAction === undefined) {
+ let error = action.map(x => strings[x] === undefined ? x : strings[x])
+ error = error.join(" ")
+ throw new ParseSieveError(`action couldn't be interpreted: '${error}'`)
+ } else {
+ newAction["id"] = newActionID
+ newActionID += 1
+ }
+ filter.actions.push(newAction)
+ }
+ } else {
+ throw new ParseSieveError(`command type not supported: '${type}'`)
+ }
+
+ match = commandRegex.exec(raw)
+ newFilterID += 1
+ filters.push(filter)
+ }
+ raw = raw.replace(commandRegex, "")
+ // if anything left parse error
+ if (raw.match(/\S/gm) !== null) {
+ let error = raw.replace(/\n/gm, " ")
+ throw new ParseSieveError(`unknown script part: '${error}'`)
+ } else {
+ return filters
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/store/actions.js b/src/store/actions.js
index b9a5ce870a..3ae16f86ae 100644
--- a/src/store/actions.js
+++ b/src/store/actions.js
@@ -2,6 +2,7 @@
* @copyright 2019 Christoph Wurst
*
* @author 2019 Christoph Wurst
+ * @author 2019, Merlin Mittelbach
*
* @license GNU AGPL version 3 or any later version
*
@@ -35,6 +36,7 @@ import {fetchAll as fetchAllFolders, create as createFolder, markFolderRead} fro
import {deleteMessage, fetchEnvelopes, fetchMessage, setEnvelopeFlag, syncEnvelopes} from '../service/MessageService'
import {showNewMessagesNotification} from '../service/NotificationService'
import {parseUid} from '../util/EnvelopeUidParser'
+import {getScripts, getScript} from '../service/FiltersService'
export default {
savePreference({commit, getters}, {key, value}) {
@@ -494,4 +496,58 @@ export default {
throw err
})
},
+ async fetchSieveScripts({getters, commit}, accountID){
+ commit("setSieveLoading", true)
+ commit("resetFilterAccount", {accountID})
+ let scripts = await getScripts(accountID)
+ if (scripts.entries !== null) {
+ scripts.entries = scripts.entries.filter(x => x !== "nextcloud_includes")
+ if (scripts.active === "nextcloud_includes") {
+ const includes_raw = await getScript(accountID, "nextcloud_includes")
+ const includeRegex = /include\s*:personal\s*"(\S*)";/gm
+ let match = includeRegex.exec(includes_raw)
+ while (match !== null) {
+ if (scripts.entries.indexOf(match[1]) !== -1) {
+ const raw = await getScript(accountID, match[1])
+ commit("newFilterSet", {
+ "accountID": accountID,
+ "name": match[1],
+ "original_name": match[1],
+ "raw": raw,
+ "active": true,
+ "changed": false,
+ })
+ }
+ scripts.entries = scripts.entries.filter(x => x !== match[1])
+ match = includeRegex.exec(includes_raw)
+ }
+ } else {
+ if (scripts.active !== null) {
+ const raw = await getScript(accountID, scripts.active)
+ commit("newFilterSet", {
+ "accountID": accountID,
+ "name": scripts.active,
+ "original_name": scripts.active,
+ "raw": raw,
+ "active": true,
+ "changed": false,
+ })
+ scripts.entries = scripts.entries.filter(x => x !== scripts.active)
+ }
+ }
+ for (const script of scripts.entries) {
+ const raw = await getScript(accountID, script)
+ commit("newFilterSet", {
+ "accountID": accountID,
+ "name": script,
+ "original_name": script,
+ "raw": raw,
+ "changed": false,
+ })
+ }
+ commit("setSieveLoading", false)
+ } else {
+ commit("setSieveLoading", false)
+ }
+ }
}
diff --git a/src/store/constants.js b/src/store/constants.js
index a3c8222f3a..87c22988e9 100644
--- a/src/store/constants.js
+++ b/src/store/constants.js
@@ -22,3 +22,6 @@
export const UNIFIED_ACCOUNT_ID = 0
export const UNIFIED_INBOX_ID = btoa('inbox')
export const UNIFIED_INBOX_UID = UNIFIED_ACCOUNT_ID + '-' + UNIFIED_INBOX_ID
+
+export const SIEVE_NAME = 'nextcloud'
+export const SIEVE_CUSTOM_NAME = 'nextcloud.custom'
diff --git a/src/store/index.js b/src/store/index.js
index 4dc434babc..196cf7c437 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -2,6 +2,7 @@
* @copyright 2019 Christoph Wurst
*
* @author 2019 Christoph Wurst
+ * @author 2019, Merlin Mittelbach
*
* @license GNU AGPL version 3 or any later version
*
@@ -78,6 +79,22 @@ export const getters = {
getMessageByUid: state => uid => {
return state.messages[uid]
},
+ sieveAccountList: state => (accountID) => {
+ return state.accountList.map(function(accountID) {
+ if (state.accounts[accountID].sieveHost != undefined) {
+ return accountID
+ }
+ }).filter(accountID => accountID !== undefined)
+ },
+ getFilterSetByID: state => (accountID, filterSetID) => {
+ return state.sieveFilterSets[accountID].find(x => x.id === filterSetID)
+ },
+ getFilterSetByName: state => (accountID, name) => {
+ return state.sieveFilterSets[accountID].find(x => x.name === name)
+ },
+ getFilterByID: state => (accountID, filterSetID, filterID) => {
+ return state.sieveFilters[accountID][filterSetID].find(x => x.id === filterID)
+ },
}
export default new Vuex.Store({
@@ -112,6 +129,10 @@ export default new Vuex.Store({
envelopes: {},
messages: {},
autocompleteEntries: [],
+ sieveFilterSets: {},
+ sieveFilters: {},
+ sieveLoading: true,
+ sieveDelete: new Set(),
},
getters,
mutations,
diff --git a/src/store/mutations.js b/src/store/mutations.js
index beb9a7d18b..f40abeadaf 100644
--- a/src/store/mutations.js
+++ b/src/store/mutations.js
@@ -2,6 +2,7 @@
* @copyright 2019 Christoph Wurst
*
* @author 2019 Christoph Wurst
+ * @author 2019, Merlin Mittelbach
*
* @license GNU AGPL version 3 or any later version
*
@@ -21,11 +22,13 @@
import _ from 'lodash'
import Vue from 'vue'
+import getters from './index.js'
import {buildMailboxHierarchy} from '../imap/MailboxHierarchy'
import {havePrefix} from '../imap/MailboxPrefix'
import {sortMailboxes} from '../imap/MailboxSorter'
import {UNIFIED_ACCOUNT_ID} from './constants'
+import {ParseSieveError,parseSieveScript} from '../service/FiltersService'
const addFolderToState = (state, account) => folder => {
const id = account.id + '-' + folder.id
@@ -200,4 +203,116 @@ export default {
removeMessage(state, {accountId, folderId, id}) {
Vue.delete(state.messages, accountId + '-' + folderId + '-' + id)
},
+ newFilter(state, {accountID, filterSetID, filter}){
+ if (state.sieveFilters[accountID] === undefined) {
+ Vue.set(state.sieveFilters, accountID, {})
+ }
+ if (state.sieveFilters[accountID][filterSetID] === undefined) {
+ Vue.set(state.sieveFilters[accountID], filterSetID, [])
+ }
+ state.sieveFilters[accountID][filterSetID].push(filter)
+ state.sieveFilterSets[accountID][filterSetID].changed = true
+ },
+ newFilterSet(state, {accountID, name, raw, original_name, active = false, changed = true}) {
+ let newID = 0
+ if (state.sieveFilterSets[accountID] === undefined) {
+ Vue.set(state.sieveFilterSets, accountID, [])
+ }
+ if (state.sieveFilterSets[accountID] !== undefined){
+ while (state.sieveFilterSets[accountID].find(x => x.id === newID) !== undefined){
+ newID += 1
+ }
+ } else {
+ state.sieveFilterSets[accountID] = []
+ }
+ if (name === undefined) {
+ name = "FilterSet_"+newID;
+ let counter = 0;
+ if (state.sieveFilterSets[accountID].find(x => x.name === name) !== undefined){
+ while (state.sieveFilterSets[accountID].find(x => x.name === name+"_"+counter) !== undefined) {
+ counter += 1;
+ }
+ name = name+"_"+counter
+ }
+ }
+ try {
+ let filters = parseSieveScript(raw)
+ state.sieveFilterSets[accountID].push({
+ "id": newID,
+ "name": name,
+ "original_name": original_name,
+ "parsed": true,
+ "active": active,
+ "changed": changed,
+ })
+ if (state.sieveFilters[accountID] === undefined) {
+ Vue.set(state.sieveFilters, accountID, {})
+ }
+ if (state.sieveFilters[accountID][newID] === undefined) {
+ Vue.set(state.sieveFilters[accountID], newID, filters)
+ }
+ } catch (e) {
+ if (e instanceof ParseSieveError) {
+ state.sieveFilterSets[accountID].push({
+ "id": newID,
+ "name": name,
+ "parsed": false,
+ "raw": raw,
+ "active": active,
+ "parseError": e.message,
+ "changed": changed,
+ "original_name": original_name,
+ })
+ } else {
+ throw e
+ }
+ }
+ },
+ rmFilter(state, {accountID, filterSetID, filterID}){
+ state.sieveFilters[accountID][filterSetID] = state.sieveFilters[accountID][filterSetID].filter(x => x.id !== filterID)
+ state.sieveFilterSets[accountID][filterSetID].changed = true
+ },
+ rmFilters(state, {accountID, filterSetID}){
+ Vue.set(state.sieveFilters[accountID], filterSetID, [])
+ },
+ rmFilterSet(state, {accountID, filterSetID}){
+ state.sieveDelete.add(state.sieveFilterSets[accountID][state.sieveFilterSets[accountID].findIndex(x => x.id === filterSetID)].name)
+ state.sieveFilterSets[accountID] = state.sieveFilterSets[accountID].filter(x => x.id !== filterSetID)
+ Vue.delete(state.sieveFilters[accountID], filterSetID)
+ },
+ updateFilter(state, {accountID, filterSetID, filterID, filter}){
+ state.sieveFilters[accountID][filterSetID][state.sieveFilters[accountID][filterSetID].findIndex(x => x.id === filterID)] = filter
+ state.sieveFilterSets[accountID][filterSetID].changed = true
+ },
+ updateFilters(state, {accountID, filterSetID, value}){
+ state.sieveFilters[accountID][filterSetID] = value
+ state.sieveFilterSets[accountID][filterSetID].changed = true
+ },
+ updateFilterSet(state, {accountID, filterSetID, value}){
+ Vue.set(state.sieveFilterSets[accountID], state.sieveFilterSets[accountID].findIndex(x => x.id === filterSetID), value)
+ },
+ updateFilterSets(state, {accountID, value}){
+ state.sieveFilterSets[accountID] = value
+ },
+ updateFilterSetName(state, {accountID, filterSetID, name}){
+ state.sieveDelete.add(state.sieveFilterSets[accountID][state.sieveFilterSets[accountID].findIndex(x => x.id === filterSetID)].name)
+ state.sieveFilterSets[accountID][state.sieveFilterSets[accountID].findIndex(x => x.id === filterSetID)].name = name
+ state.sieveFilterSets[accountID][filterSetID].changed = true
+ },
+ updateRawSieveScript(state, {accountID, filterSetID, raw}) {
+ state.sieveFilterSets[accountID].find(x => x.id === filterSetID).raw = raw
+ state.sieveFilterSets[accountID][filterSetID].changed = true
+ },
+ resetFilterAccount(state, {accountID}) {
+ Vue.delete(state.sieveFilterSets, accountID)
+ Vue.delete(state.sieveFilters, accountID)
+ },
+ setSieveLoading(state, bool) {
+ state.sieveLoading = bool
+ state.sieveDelete = new Set()
+ },
+ toggleActiveSieve(state, {accountID, filterSetID}) {
+ state.sieveFilterSets[accountID][state.sieveFilterSets[accountID].findIndex(x => x.id === filterSetID)].active =
+ !state.sieveFilterSets[accountID][state.sieveFilterSets[accountID].findIndex(x => x.id === filterSetID)].active
+ },
}
diff --git a/src/views/AccountSettings.vue b/src/views/AccountSettings.vue
index 839d330e81..ee623865d2 100644
--- a/src/views/AccountSettings.vue
+++ b/src/views/AccountSettings.vue
@@ -2,6 +2,9 @@
+
+
+
{{ t('mail', 'Account Settings') }} - {{ email }}
{{ t('mail', 'Mail server') }}
@@ -24,6 +27,7 @@ import EditorSettings from '../components/EditorSettings'
import Logger from '../logger'
import Navigation from '../components/Navigation'
import SignatureSettings from '../components/SignatureSettings'
+import FilterSettings from '../components/FilterSettings'
export default {
name: 'AccountSettings',
@@ -34,6 +38,7 @@ export default {
EditorSettings,
Navigation,
SignatureSettings,
+ FilterSettings,
},
data() {
const account = this.$store.getters.getAccount(this.$route.params.accountId)
diff --git a/src/views/SieveView.vue b/src/views/SieveView.vue
new file mode 100644
index 0000000000..7c15844519
--- /dev/null
+++ b/src/views/SieveView.vue
@@ -0,0 +1,293 @@
+
+
+
+
+
+
+
{{ t("mail", "Filter Configuration") }}
+
+ {{ t("mail", "No Filter Account found.") }}
+
+
+
+
+
+
+ {{ $store.getters.getAccount(accountID).sieveUser }}
+ @ {{ tls(accountID) }}{{ $store.getters.getAccount(accountID).sieveHost }}
+
+
+
+ Reload
+
+
+ Save
+
+
+
+
+ Reload
+
+
+ Save
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Command/CreateAccountTest.php b/tests/Command/CreateAccountTest.php
index b2bb24008c..4cf5ab102e 100644
--- a/tests/Command/CreateAccountTest.php
+++ b/tests/Command/CreateAccountTest.php
@@ -29,7 +29,7 @@ class CreateAccountTest extends TestCase {
private $service;
private $crypto;
private $command;
- private $args = [
+ private $requiredArgs = [
'user-id',
'name',
'email',
@@ -44,6 +44,13 @@ class CreateAccountTest extends TestCase {
'smtp-user',
'smtp-password',
];
+ private $optionalArgs = [
+ 'sieve-host',
+ 'sieve-port',
+ 'sieve-ssl-mode',
+ 'sieve-user',
+ 'sieve-password',
+ ];
protected function setUp() {
parent::setUp();
@@ -68,8 +75,8 @@ public function testArguments() {
$actual = $this->command->getDefinition()->getArguments();
foreach ($actual as $actArg) {
- $this->assertTrue($actArg->isRequired());
- $this->assertTrue(in_array($actArg->getName(), $this->args));
+ $type = $actArg->isRequired() ? 'required' : 'optional';
+ $this->assertTrue(in_array($actArg->getName(), $this->{"{$type}Args"}));
}
}
diff --git a/tests/Controller/AccountsControllerTest.php b/tests/Controller/AccountsControllerTest.php
index b5ef64b6a7..e81ac77017 100644
--- a/tests/Controller/AccountsControllerTest.php
+++ b/tests/Controller/AccountsControllerTest.php
@@ -222,7 +222,7 @@ public function testCreateAutoDetectSuccess() {
->willReturn($account);
$response = $this->controller->create($accountName, $email, $password, null, null, null, null, null, null, null, null,
- null, null, true);
+ null, null, null, true);
$expectedResponse = new JSONResponse($account, Http::STATUS_CREATED);
@@ -240,7 +240,7 @@ public function testCreateAutoDetectFailure() {
$this->expectException(ClientException::class);
$this->controller->create($accountName, $email, $password, null, null, null, null, null, null, null, null,
- null, null, true);
+ null, null, null, true);
}
public function testUpdateAutoDetectSuccess() {
@@ -254,7 +254,7 @@ public function testUpdateAutoDetectSuccess() {
->willReturn($account);
$response = $this->controller->create($accountName, $email, $password, null, null, null, null, null, null, null, null,
- null, null, true);
+ null, null, null, true);
$expectedResponse = new JSONResponse($account, Http::STATUS_CREATED);
@@ -272,7 +272,7 @@ public function testUpdateAutoDetectFailure() {
$this->expectException(ClientException::class);
$this->controller->create($accountName, $email, $password, null, null, null, null, null, null, null, null,
- null, null, true);
+ null, null, null, true);
}
public function testCreateManualSuccess() {
@@ -290,13 +290,14 @@ public function testCreateManualSuccess() {
$smtpSslMode = 'none';
$smtpUser = 'user@domain.tld';
$smtpPassword = 'mypassword';
+ $sieveConfig = null;
$account = $this->createMock(Account::class);
$this->setupService->expects($this->once())
->method('createNewAccount')
- ->with($accountName, $email, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $this->userId)
+ ->with($accountName, $email, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $sieveConfig, $this->userId)
->willReturn($account);
- $response = $this->controller->create($accountName, $email, $password, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $autoDetect);
+ $response = $this->controller->create($accountName, $email, $password, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $sieveConfig, $autoDetect);
$expectedResponse = new JSONResponse($account, Http::STATUS_CREATED);
@@ -318,13 +319,14 @@ public function testCreateManualFailure() {
$smtpSslMode = 'none';
$smtpUser = 'user@domain.tld';
$smtpPassword = 'mypassword';
+ $sieveConfig = null;
$this->setupService->expects($this->once())
->method('createNewAccount')
- ->with($accountName, $email, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $this->userId)
+ ->with($accountName, $email, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $sieveConfig, $this->userId)
->willThrowException(new Exception());
$this->expectException(ClientException::class);
- $this->controller->create($accountName, $email, $password, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $autoDetect);
+ $this->controller->create($accountName, $email, $password, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $sieveConfig, $autoDetect);
}
public function testUpdateManualSuccess() {
@@ -343,10 +345,11 @@ public function testUpdateManualSuccess() {
$smtpSslMode = 'none';
$smtpUser = 'user@domain.tld';
$smtpPassword = 'mypassword';
+ $sieveConfig = null;
$account = $this->createMock(Account::class);
$this->setupService->expects($this->once())
->method('createNewAccount')
- ->with($accountName, $email, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $this->userId, $id)
+ ->with($accountName, $email, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $sieveConfig, $this->userId, $id)
->willReturn($account);
$response = $this->controller->update($id, $autoDetect, $accountName, $email, $password, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword);
@@ -372,9 +375,10 @@ public function testUpdateManualFailure() {
$smtpSslMode = 'none';
$smtpUser = 'user@domain.tld';
$smtpPassword = 'mypassword';
+ $sieveConfig = null;
$this->setupService->expects($this->once())
->method('createNewAccount')
- ->with($accountName, $email, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $this->userId, $id)
+ ->with($accountName, $email, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $sieveConfig, $this->userId, $id)
->willThrowException(new Exception());
$this->expectException(ClientException::class);
diff --git a/tests/Service/DefaultAccount/ManagerTest.php b/tests/Service/DefaultAccount/ManagerTest.php
index 96fd15c0b3..02de7af818 100644
--- a/tests/Service/DefaultAccount/ManagerTest.php
+++ b/tests/Service/DefaultAccount/ManagerTest.php
@@ -105,6 +105,9 @@ public function testGetDefaultAccount() {
'smtpHost' => 'smtp.domain.tld',
'smtpPort' => 465,
'smtpSslMode' => 'tls',
+ 'sieveHost' => null,
+ 'sievePort' => null,
+ 'sieveSslMode' => null,
]);
$credentials = $this->createMock(ICredentials::class);
$user = $this->createMock(IUser::class);
@@ -145,6 +148,11 @@ public function testGetDefaultAccount() {
$expected->setOutboundPort(465);
$expected->setOutboundSslMode('tls');
$expected->setOutboundPassword('encrypted');
+ $expected->setSieveUser(null);
+ $expected->setSieveHost(null);
+ $expected->setSievePort(null);
+ $expected->setSieveSslMode(null);
+ $expected->setSievePassword(null);
$account = $this->manager->getDefaultAccount();