From 5058884cbb4dc2c703086b84a3ca8304eea4bfd5 Mon Sep 17 00:00:00 2001 From: Holger Dehnhardt Date: Mon, 12 Oct 2020 18:57:22 +0200 Subject: [PATCH 1/6] redo sieve changes Signed-off-by: Holger Dehnhardt --- img/filter.svg | 69 +++ img/filterset.svg | 70 +++ lib/Contracts/ISieveParser.php | 55 ++ lib/Controller/SieveController.php | 169 ++++++ .../Version1040Date20200526162604.php | 59 ++ lib/Service/SieveService.php | 168 +++++ lib/Sieve/SieveAction.php | 35 ++ lib/Sieve/SieveAddressPart.php | 35 ++ lib/Sieve/SieveClientFactory.php | 104 ++++ lib/Sieve/SieveMatchType.php | 35 ++ lib/Sieve/SieveParser.php | 574 ++++++++++++++++++ lib/Sieve/SieveStructure.php | 162 +++++ lib/Sieve/SieveSyntaxItem.php | 46 ++ lib/Sieve/SieveTestSubject.php | 39 ++ src/components/SieveAccountForm.vue | 240 ++++++++ src/components/SieveFilterAction.vue | 128 ++++ src/components/SieveFilterNavigation.vue | 112 ++++ src/components/SieveFilterTest.vue | 288 +++++++++ src/service/SieveService.js | 60 ++ src/store/sieve.js | 111 ++++ src/views/FilterSettings.vue | 375 ++++++++++++ src/views/SieveFilterRules.vue | 184 ++++++ 22 files changed, 3118 insertions(+) create mode 100644 img/filter.svg create mode 100644 img/filterset.svg create mode 100644 lib/Contracts/ISieveParser.php create mode 100644 lib/Controller/SieveController.php create mode 100644 lib/Migration/Version1040Date20200526162604.php create mode 100644 lib/Service/SieveService.php create mode 100644 lib/Sieve/SieveAction.php create mode 100644 lib/Sieve/SieveAddressPart.php create mode 100644 lib/Sieve/SieveClientFactory.php create mode 100644 lib/Sieve/SieveMatchType.php create mode 100644 lib/Sieve/SieveParser.php create mode 100644 lib/Sieve/SieveStructure.php create mode 100644 lib/Sieve/SieveSyntaxItem.php create mode 100644 lib/Sieve/SieveTestSubject.php create mode 100644 src/components/SieveAccountForm.vue create mode 100644 src/components/SieveFilterAction.vue create mode 100644 src/components/SieveFilterNavigation.vue create mode 100644 src/components/SieveFilterTest.vue create mode 100644 src/service/SieveService.js create mode 100644 src/store/sieve.js create mode 100644 src/views/FilterSettings.vue create mode 100644 src/views/SieveFilterRules.vue diff --git a/img/filter.svg b/img/filter.svg new file mode 100644 index 0000000000..b15c78213d --- /dev/null +++ b/img/filter.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/img/filterset.svg b/img/filterset.svg new file mode 100644 index 0000000000..617f1f2e5c --- /dev/null +++ b/img/filterset.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/Contracts/ISieveParser.php b/lib/Contracts/ISieveParser.php new file mode 100644 index 0000000000..f6885b6fac --- /dev/null +++ b/lib/Contracts/ISieveParser.php @@ -0,0 +1,55 @@ + + * + * @author 2020 Holger Dehnhardt + * + * @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\Contracts; + +interface ISieveParser { + + /** + * + * @param array $sieveExtensions + * + * @return array; + */ + public function getSupportedSieveStructure(array $sieveExtensions); + + /** + * + * @param string $script + * + * @return array; + */ + + public function parse(string $script); + + /** + * + * @param array $scriptContent + * + * @return string; + */ + public function merge(array $scriptContent); +} diff --git a/lib/Controller/SieveController.php b/lib/Controller/SieveController.php new file mode 100644 index 0000000000..a01fcde228 --- /dev/null +++ b/lib/Controller/SieveController.php @@ -0,0 +1,169 @@ + + * + * 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\Exception\ServiceException; +use OCA\Mail\Exception\ClientException; +use OCA\Mail\Service\AccountService; +use OCA\Mail\Service\SieveService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; +use OCP\ILogger; + +class SieveController extends Controller { + + /** @var AccountService */ + private $accountService; + /** @var UserId */ + private $currentUserId; + /** @var SieveService */ + private $sieveService; + /** @var ILogger */ + private $logger; + + /** + * @param string $appName + * @param IRequest $request + * @param AccountService $accountService + * @param string $UserId + * @param SieveService $sieveService + * @param ILogger $logger + */ + + public function __construct(string $appName, + IRequest $request, + AccountService $accountService, + $UserId, + SieveService $sieveService, + ILogger $logger) { + parent::__construct($appName, $request); + + $this->accountService = $accountService; + $this->currentUserId = $UserId; + $this->sieveService = $sieveService; + $this->logger = $logger; + } + + /** + * @NoAdminRequired + * @TrapError + * + * @param int $accountId + * @param bool $sieveEnabled + * @param string $sieveHost + * @param int $sievePort + * @param string $sieveUSer + * @param string $sieveSslMode + * @param string $sievePassword + * + * @return JSONResponse + * @throws ClientException + */ + public function updateSieveAccount(int $accountId, bool $sieveEnabled, string $sieveHost, int $sievePort, string $sieveUser, string $sieveSslMode, string $sievePassword): JSONResponse { + $this->logger->info("update account (from SieveController"); + $account = $this->accountService->find($this->currentUserId, $accountId); + + $params = [ + 'host' => $sieveHost, + 'port' => $sievePort, + 'user' => $sieveUser, + 'password' => $sievePassword, + 'secure' => $sieveSslMode, + ]; + + try { + $ret = $this->sieveService->updateSieveAccount($account, $params); + $message = "account modified successfully"; + } catch (ServiceException $e) { + $ret = false; + $message = $e->getMessage(); + } catch (\Throwable $e) { + throw new ServiceException($e->getMessage(), 0); + } + + return new JSONResponse( + ['sieveEnabled' => $ret, + 'message' => $message] + ); + } + + /** + * @NoAdminRequired + * @TrapError + * + * @param int $accountId + * + * @return JSONResponse + * @throws ClientException + */ + public function listScripts(int $accountId) { + try { + $account = $this->accountService->find($this->currentUserId, $accountId); + $scripts = $this->sieveService->listScripts($account); + } catch (ServiceException $e) { + $message = $e->getMessage(); + } + return new JSONResponse($scripts); + } + + /** + * @NoAdminRequired + * @TrapError + * + * @param int $accountId + * @param string $scriptName + * + * @return JSONResponse + * @throws ClientException + */ + public function getScriptContent(int $accountId, string $scriptName) { + try { + $account = $this->accountService->find($this->currentUserId, $accountId); + $scriptContent = $this->sieveService->getScriptContent($account, $scriptName); + } catch (ServiceException $e) { + $message = $e->getMessage(); + } + return new JSONResponse(['scriptContent' => $scriptContent]); + } + + /** + * @NoAdminRequired + * @TrapError + * + * @param int $accountId + * @param string $scriptName + * @param bool $install + * @param array $scriptContent + * + * @return JSONResponse + * @throws ClientException + */ + public function setScriptContent(int $accountId, string $scriptName, bool $install, array $scriptContent) { + $this->logger->debug("SieveController: setScriptContent"); + $account = $this->accountService->find($this->currentUserId, $accountId); + $this->sieveService->setScriptContent($account, $scriptName, $install, $scriptContent); + return new JSONResponse(); + } +} diff --git a/lib/Migration/Version1040Date20200526162604.php b/lib/Migration/Version1040Date20200526162604.php new file mode 100644 index 0000000000..e55a94442b --- /dev/null +++ b/lib/Migration/Version1040Date20200526162604.php @@ -0,0 +1,59 @@ +hasTable('mail_accounts')) { + $table = $schema->getTable('mail_accounts'); + + $table->addColumn('sieve_enabled', Type::BOOLEAN, [ + 'notnull' => true, + 'default' => false, + ]); + + $table->addColumn('sieve_host', Type::STRING, [ + 'notnull' => false, + 'length' => 64, + ]); + + $table->addColumn('sieve_port', Type::INTEGER, [ + 'notnull' => false, + ]); + + $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/SieveService.php b/lib/Service/SieveService.php new file mode 100644 index 0000000000..2122e1f030 --- /dev/null +++ b/lib/Service/SieveService.php @@ -0,0 +1,168 @@ + + * + * 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 OCA\Mail\Account; +use OCA\Mail\Contracts\ISieveParser; +use OCA\Mail\Db\MailAccountMapper; +use OCA\Mail\Exception\ServiceException; +use OCA\Mail\Sieve\SieveClientFactory; +use OCP\ILogger; +use OCP\Security\ICrypto; + +class SieveService { + + /** @var SieveClientFactory */ + private $sieveClientFactory; + + /** @var MailAccountMapper */ + private $mapper; + + /** @var ICrypto */ + private $crypto; + + /** @var ISieveParser */ + private $sieveParser; + + /** @var ILogger */ + private $logger; + + /** + * @param SieveClientFactory $sieveClientFactory + * @param MailAccountMapper $mailAccountMapper + * @param ISieveParser $sieveParser + * @param ICrypto $crypto + * @param ILogger $logger + */ + + public function __construct(SieveClientFactory $sieveClientFactory, MailAccountMapper $mailAccountMapper, ISieveParser $sieveParser, ICrypto $crypto, ILogger $logger) { + $this->sieveClientFactory = $sieveClientFactory; + $this->mapper = $mailAccountMapper; + $this->crypto = $crypto; + $this->sieveParser = $sieveParser; + $this->logger = $logger; + } + + /** + * @param Account $account + * @param array $sieveParams + * + * @return bool + * @throws ServiceException + */ + public function updateSieveAccount(Account $account, array $sieveParams): bool { + $this->logger->info('updateSieveAccount from SieveService'); + /* Try to establish a sieve connection and save the entered values only if successful */ + try { + $sieveClient = $this->sieveClientFactory->getSieveClient($account, $sieveParams); + } catch (ServiceException $e) { + // Disable sieve account if an error occurs + $this->disableSieveAccount($account); + throw $e; + } catch (\Throwable $throwable) { + // Disable sieve account if an error occurs + $this->disableSieveAccount($account); + throw new ServiceException($throwable->getMessage()); + } + $mailAccount = $account->getMailAccount(); + $mailAccount->setSieveUser($sieveParams['user']); + $mailAccount->setSievePassword($this->crypto->encrypt($sieveParams['password'])); + $mailAccount->setSieveHost($sieveParams['host']); + $mailAccount->setSievePort($sieveParams['port']); + $mailAccount->setSieveSslMode($sieveParams['secure']); + $mailAccount->setSieveEnabled(true); + try { + $this->mapper->save($mailAccount); + } catch (\Throwable $throwable) { + throw new ServiceException($throwable->getMessage()); + } + return true; + } + + /** + * @param Account $account + * + * @return array + */ + public function listScripts(Account $account) : array { + $sieveClient = $this->sieveClientFactory->getSieveClient($account); + $sieveExtensions = $sieveClient->getExtensions(); + $scripts = $sieveClient->listScripts(); + $activeScript = $sieveClient->getActive(); + if( sizeof($scripts) > 0 && $activeScript ){ + $scriptContent = $this->getScriptContent($account, $activeScript); + } + $supportedSieveStructure = $this->sieveParser->getSupportedSieveStructure($sieveExtensions); + return [ + "scripts" => $scripts, + "activeScript" => $activeScript, + "scriptContent" => $scriptContent, + "sieveExtensions" => $sieveExtensions, + "supportedSieveStructure" => $supportedSieveStructure]; + } + + /** + * @param Account $account + * @param string $scriptName + * + * @return array + */ + public function getScriptContent(Account $account, string $scriptName) : array { + $sieveClient = $this->sieveClientFactory->getSieveClient($account); + $scriptContent = $sieveClient->getScript($scriptName); + $parsedScript = $this->sieveParser->parse($scriptContent); + return $parsedScript; + } + + /** + * @param Account $account + * @param string $scriptName + * @param bool $install + * @param array $scriptContent + * + * @return bool + * @throws ServiceException + */ + public function setScriptContent(Account $account, string $scriptName, bool $install, array $scriptContent) : bool { + $this->logger->debug("SieveService: setScriptContent"); + $script = $this->sieveParser->merge($scriptContent); + $sieveClient = $this->sieveClientFactory->getSieveClient($account); + try { + $sieveClient->installScript($scriptName, $script, $install); + } catch (\Horde\ManageSieve\Exception $e) { + $this->logger->error($e->getMessage()); + } + return true; + } + + /** + * @param Account $account + * + */ + private function disableSieveAccount(Account $account) { + $mailAccount = $account->getMailAccount(); + $mailAccount->setSieveEnabled(false); + $updated=$this->mapper->save($mailAccount); + } +} diff --git a/lib/Sieve/SieveAction.php b/lib/Sieve/SieveAction.php new file mode 100644 index 0000000000..389cbfe6aa --- /dev/null +++ b/lib/Sieve/SieveAction.php @@ -0,0 +1,35 @@ + + * + * 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 + * + */ + + class SieveAction extends SieveSyntaxItem { + + /** @var $parameters */ + public $parameters; + + public function __construct(String $name, String $extension = '', String $parameters = '') { + parent::__construct($name, $extension); + $this->parameters = $parameters; + } + } diff --git a/lib/Sieve/SieveAddressPart.php b/lib/Sieve/SieveAddressPart.php new file mode 100644 index 0000000000..942e2d3ac6 --- /dev/null +++ b/lib/Sieve/SieveAddressPart.php @@ -0,0 +1,35 @@ + + * + * 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 + * + */ + + class SieveAddressPart extends SieveSyntaxItem { + + /** @var $usages */ + public $usages; + + public function __construct(String $name, String $extension = '', array $usages) { + parent::__construct($name, $extension); + $this->usages = $usages; + } + } diff --git a/lib/Sieve/SieveClientFactory.php b/lib/Sieve/SieveClientFactory.php new file mode 100644 index 0000000000..c8b035177e --- /dev/null +++ b/lib/Sieve/SieveClientFactory.php @@ -0,0 +1,104 @@ + + * + * 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 Horde\ManageSieve\Exception; +use Horde\ManageSieve\Exception\ConnectionFailed; +use Horde\Socket\Client\Exception as SocketException; +use OCA\Mail\Account; +use OCA\Mail\Exception\ServiceException; +use OCA\Mail\IMAP\IMAPClientFactory; +use OCP\IConfig; +use OCP\ILogger; +use OCP\Security\ICrypto; + +class SieveClientFactory { + + /** @var ICrypto */ + private $crypto; + + /** @var IConfig */ + private $config; + + /** @var IMAPClientFactory */ + private $imapClientFactory; + + /** @var ILogger */ + private $logger; + + private $cache = []; + + /** + * @param ICrypto $crypto + * @param IConfig $config + * @param IMAPClientFactory $imapClientFactory + */ + public function __construct(ICrypto $crypto , IConfig $config, IMAPClientFactory $imapClientFactory) { + $this->crypto = $crypto; + $this->config = $config; + $this->imapClientFactory = $imapClientFactory; + } + + /** + * @param Account $account + * @param array $params + * @return ManageSieve + * @throws ServiceException + */ + public function getSieveClient(Account $account, array $params = null): ManageSieve { + if (!isset($this->cache[$account->getId()])) { + if (!isset($params)) { + $imapClient = $this->imapClientFactory->getClient($account); + $mailAccount = $account->getMailAccount(); + $host = $mailAccount->getSieveHost(); + $user = $mailAccount->getSieveUser(); + $password = $mailAccount->getSievePassword(); + $password = $this->crypto->decrypt($password); + $port = $mailAccount->getSievePort(); + $ssl_mode = $account->convertSslMode($mailAccount->getSieveSslMode()); + + $params = [ + 'host' => $host, + 'port' => $port, + 'user' => $user, + 'password' => $password, + 'secure' => $ssl_mode, + ]; + } + try { + $manageSieve = new ManageSieve($params); + $manageSieve->setLogger($this->logger); + $this->cache[$account->getId()] = $manageSieve; + } catch (SocketException $se) { + throw new ServiceException($se->getMessage(), 10); + } catch (ConnectionFailed $e) { + throw new ServiceException($e->getMessage(), 11); + } catch (Exception $e) { + throw new ServiceException($e->getMessage(), 12); + } + } + return $this->cache[$account->getId()]; + } +} diff --git a/lib/Sieve/SieveMatchType.php b/lib/Sieve/SieveMatchType.php new file mode 100644 index 0000000000..1f27c68359 --- /dev/null +++ b/lib/Sieve/SieveMatchType.php @@ -0,0 +1,35 @@ + + * + * 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 + * + */ + + class SieveMatchType extends SieveSyntaxItem { + + /** @var $usages */ + public $usages; + + public function __construct(String $name, String $extension = '', array $usages) { + parent::__construct($name, $extension); + $this->usages = $usages; + } + } diff --git a/lib/Sieve/SieveParser.php b/lib/Sieve/SieveParser.php new file mode 100644 index 0000000000..b6ea93f0ac --- /dev/null +++ b/lib/Sieve/SieveParser.php @@ -0,0 +1,574 @@ + + * + * 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 OCA\Mail\Contracts\ISieveParser; + +class SieveParser implements ISieveParser { + + /** @var $splitQuotedStringByComma */ + private $splitQuotedStringByComma="/,(?=(?:[^\"\[\]]*[\"\[][^\"\]]*[\"\]])*[^\"\]]*$)/"; + + /** @var $splitQuotedStringByBlank */ + private $splitQuotedStringByBlank="/ (?=(?:[^\"\[\]]*[\"\[][^\"\]]*[\"\]])*[^\"\]]*$)/"; + + /** @var $installedExtensions */ + private $installedExtensions = []; + + /** @var $sieveStructure array */ + private $sieveStructure; + + /** @var $parsedTree */ + private $parsedTree = []; + + /** @var $ruleNumber */ + private $ruleNumber = 0; + + /** @var $headerRule */ + private $headerRule = []; + + /** @var $requirements */ + private $requirements = []; + + /** @var $generatorName */ + const GENERATOR_NAME = "Nextcloud Mail"; + + /** @var $origin */ + private $native = false; + + /** + * + * @param SieveStructure $sieveStructure + * + * @return array; + */ + public function __construct(SieveStructure $sieveStructure) { + $this->sieveStructure = $sieveStructure; + } + + /** + * + * @param array $sieveExtensions + * + * @return array; + */ + public function getSupportedSieveStructure(array $sieveExtensions) : array { + $this->installedExtensions = $sieveExtensions; + return $this->sieveStructure->getSupportedStructure($sieveExtensions); + } + + /** + * + * @param string $sieveClientFactory + * + * @return array; + */ + public function parse(string $script) : array { + $this->headerRule['scriptOrigin'] = 'unknown'; + $this->headerRule['type'] = 'header'; + $this->parseMultilineComments($script); + array_splice($this->parsedTree, 0, 0, [$this->headerRule]); + return $this->parsedTree; + } + + private function parseMultilineComments(string $script) { + //separate multiline comments from singleline comments and instructions + $tokens = preg_split("/(\/\*[^*]*\*\/)/s", $script, -1, PREG_SPLIT_DELIM_CAPTURE); + foreach ($tokens as $token) { + $token = trim($token); + if (stripos($token, "/*") === 0) { + $this->parsedTree[] = ["type" => "comment", "value" => $token]; + } else { + $this->parseRules($token); + } + } + } + + private function guessSpecialMeanings($token) : bool { + $i = preg_match("/#.*generated by ([^#]*)#/si", $token, $matches); + $ret = false; + if ($i > 0) { + $this->headerRule['scriptOrigin']=trim($matches[1]); + $this->native = ($this->headerRule['scriptOrigin'] === SELF::GENERATOR_NAME); + $ret = true; + } elseif (stripos($token, "RAINLOOP:SIEVE") !== false) { + $this->headerRule['scriptOrigin']="Rainloop"; + $ret = true; + } + return $ret; + } + + private function parseRules(string $script) { + $tokencount=0; + $rules=0; + // For scripts with rule naming convention "# rule:[" which is used by several sieve clients + $tokens = preg_split("/(# rule:[^\{]*\{(?:[^{}]++|(?R))*\})/", $script, -1, PREG_SPLIT_DELIM_CAPTURE); + if (sizeof($tokens) > 1 && stripos($tokens[0], "# rule:[") === false) { + $header = preg_split("/\r\n/", $tokens[0]); + } + // if no rule naming convention found, split by topmost 'if' statements: + // Everything above the first if statement is some kind of header + if (sizeof($tokens) === 1) { + $tokens = preg_split("/(if[^\{]*\{(?:[^{}]++|(?R))*\})/", $script, -1, PREG_SPLIT_DELIM_CAPTURE); + if (sizeof($tokens) > 0 && stripos($tokens[0], "if") === false) { + // Split header by line + $header = preg_split("/\r\n/", $tokens[0]); + } + } + // If header consists of more than one line, remove the unsplit header + // and replace with 'line by line' header + if (sizeof($header) > 1) { + unset($tokens[0]); + $tokens = array_merge($header, $tokens); + } + // than iterate throug all tokens + foreach ($tokens as $token) { + $token = trim($token); + if (stripos($token, "require") === 0) { + // requires found, although useless for us, + // because we have to recollect the requires when rules change + $this->parsedTree[] = ["type" => "require", "value" => $token]; + } elseif (stripos($token, "# rule:[") === 0 || stripos($token, "if") === 0) { + // we have found comments with rulenames or the start of a rule (if) + $this->ruleNumber++; + $this->parsedTree[]= $this->parseRule($token); + ; + } elseif (stripos($token, "#") === 0) { + // single line comment found + // if we have found a hint for an originator if the + // file, we will store it and will not store this token + if (!$this->guessSpecialMeanings($token)) { + // otherwise siply store the comment + $this->parsedTree[] = ["type" => "comment", "value" => $token]; + } + } + $tokencount++; + } + } + + private function parseRule(string $rule) : array { + // we will handle the rule here + $level_1 = ["type" => "rule"]; + if ($this->native) { + $level_1['comment'] = ""; + } + $namefound = false; + if (stripos($rule, "# rule:[") === 0) { + // here we have a rule with naming convention + // separate name from rule + $i = preg_match("/[^\[]*\[([^\]]*)\](.*)/s", $rule, $matches); + if ($i > 0) { + // if we can identify a rulename -> store and remember it is the original name + $level_1['name'] = $matches[1]; + $level_1['rule'] = $matches[2]; + $namefound = true; + if (stripos($rule, "# comment:[")) { + $i = preg_match("/# comment:[^\[]*\[([^\]]*)\](.*)/s", $rule, $matches); + if ($i > 0) { + $level_1['comment'] = $matches[1]; + } + } + $level_1['parsedrule'] = $this->parseRuleBody($matches[2]); + } else { + // for some reason we can not finde the name (fallback) + // we assign a name pattern and make it read only + // simply parse the rule + $level_1['name'] = "Rule " . $this->ruleNumber; + $level_1['rule'] = $rule; + $level_1['parsedrule'] = $this->parseRuleBody($rule); + } + } else { + // no naming convention found - simply parse the rule + // we assign a name pattern and make it read only + $level_1['name'] = "Rule " . $this->ruleNumber; + $level_1['rule'] = $rule; + $level_1['parsedrule'] = $this->parseRuleBody($rule); + } + $level_1['origname'] = $namefound; + return $level_1; + } + + private function parseRuleBody($ruleBody) : array { + $level_2 = []; + $ruleBody = trim($ruleBody); + // search for everything between if/else/elsif and a closing bracket '}' + // to find the conditions and actions + $matched = preg_match_all("/((if|else|elsif)[^}]*})/m", $ruleBody, $matches); + if ($matched > 0) { + foreach ($matches[1] as $match) { + $level_2 = $this->parseSingleRuleBody($match); + } + } + return $level_2; + } + + private function parseSingleRuleBody($ruleBody) : array { + $level_3 = []; + // search for everything between if/else/elsif and an opening bracket '{' + // to find only the conditons + $matched = preg_match_all("/((if|else|elsif)[^{]*)/m", $ruleBody, $matches); + if ($matched > 0) { + foreach ($matches[1] as $match) { + $level_3["conditions"] = $this->parseCondition($match); + } + } + // match anything after an openig bracket until the last semikolon + // to find only the actions + $matched = preg_match_all("/([^;{]*;)/", $ruleBody, $matches); + if ($matched > 0) { + foreach ($matches[1] as $match) { + $level_3["actions"][] = $this->parseAction(trim($match, " ;,%*?\t\n\r\x0B()")); + } + } + return $level_3; + } + + private function parseCondition($condition) : array { + $level_4 = []; + $verbs = preg_split("/ +/", $condition, -1, PREG_SPLIT_DELIM_CAPTURE); + $level_4['condition-verb'] = trim($verbs[0]," \t\n\r\0\x0B()"); + if (in_array(trim($verbs[1], " \t\n\r\0\x0B()"), $this->sieveStructure->sieveListOperators)) { + $level_4['testlist']['sieveListOperator']=trim($verbs[1]," \t\n\r\0\x0B()"); + $matched=preg_match("/\(([^\)]*)\)/", $condition, $matches); + if ($matched > 0) { + $level_4['testlist']['tests'] = $this->parseConditionComparisonsList(trim($matches[1])); + } + } else { + unset($verbs[0]); + $level_4['testlist']['tests'][] = $this->parseConditionComparisons(trim(implode(" ", $verbs))); + } + return $level_4; + } + + private function parseConditionComparisonsList($condition) : array { + $level_5 = []; + $conditions = preg_split($this->splitQuotedStringByComma, $condition); + foreach ($conditions as $condition) { + $level_5[] = $this->parseConditionComparisons(trim($condition)); + } + return $level_5; + } + + private function parseConditionComparisons($condition) : array { + $level_6 = []; + $tokens = preg_split($this->splitQuotedStringByBlank, $condition); + for ($i = 0; $i < count($tokens); ++$i) { + $token = $tokens[$i]; + if (array_key_exists($token, $this->sieveStructure->sieveTestSubjects)) { + $level_6['testSubject'] = $token; + $params = $this->sieveStructure->sieveTestSubjects[$token]->parameters; + $conditionParameters = $this->parseConditionParameters($tokens, $params, $i); + $level_6['parameters'] = $conditionParameters; + continue; + } + if (in_array($token, $this->sieveStructure->sieveMatchTypes)) { + $level_6['matchtype'] = $token; + continue; + } + $level_6[] = $token; + } + return $level_6; + } + + private function parseConditionParameters($tokens, $paramlist, &$offset) : array { + $params = explode(' ', $paramlist); + $level_6 = []; + ++$offset; + + for ($i = 0; $i < count($params); ++$i) { + $param = $params[$i]; + if (stripos($param, "%?comparator") === 0) { + // currently ignored + continue; + } + + if (stripos($tokens[$offset], "[") === 0) { + $token_array = explode(' ', $tokens[$offset]); + array_walk($token_array, [$this, "trimArray"]); + $level_6[trim($param, " %*?\"")] = $token_array; + } else { + $level_6[trim($param, " %*?\"")][] = trim($tokens[$offset], " %*?\""); + } + ++$offset; + } + return $level_6; + } + + private function parseAction(string $actionString) { + $level_4 = []; + $offset = 0; + $tokenArray = preg_split($this->splitQuotedStringByBlank, $actionString); + array_walk($tokenArray, [$this, "trimArray"]); + $action = $tokenArray[0]; + $level_4['action'] = $action; + $paramlist = $this->sieveStructure->sieveActions[$action]->parameters; + if (sizeof($tokenArray)>1 && $paramlist !== "") { + $level_4['parameters'] = $this->parseActionParameters($tokenArray, $paramlist, $offset); + } + return $level_4; + } + + private function parseActionParameters($tokens, $paramlist, &$offset) : array { + $params = explode(' ', $paramlist); + $level_5 = []; + ++$offset; + + for ($i = 0; $i < count($params); ++$i) { + $param = $params[$i]; + if (stripos($tokens[$offset], "[") === 0) { + $token_array = explode(' ', $tokens[$offset]); + array_walk($token_array, [$this, "trimArray"]); + $level_5[trim($param, " %*?\"")] = $token_array; + } else { + $level_5[trim($param, " %*?\"")][] = $tokens[$offset]; + } + ++$offset; + } + return $level_5; + } + + public function merge(array $scriptContent) { + $script = ""; + foreach ($scriptContent as $contentItem) { + switch ($contentItem['type']) { + case 'header': $script .= $this->mergeHeader($contentItem) . "\r\n"; + break; + case 'require': break; + case 'comment': $script .= $this->mergeComment($contentItem) . "\r\n"; + break; + case 'rule': $script .= $this->mergeRule($contentItem) . "\r\n"; + break; + default: break; + } + } + return $this->mergeRequire() . $script; + } + private function mergeRequire() { + $require = ""; + if (sizeof($this->requirements) > 0) { + $require .= "require ["; + $i = 0; + foreach ($this->requirements as $requirement) { + if ($i > 0) { + $require .= ", "; + } + $require .= '"' . $requirement . '"'; + } + $require .= "];\r\n"; + } + return $require; + } + private function mergeHeader(array $token) { + $header = ""; + if ($token['scriptOrigin'] !== 'unknown') { + $header = "## Generated by " . $token['scriptOrigin'] . " ##"; + } + return $header; + } + + private function mergeComment(array $token) { + $comment = $token['value']; + return $comment; + } + + private function mergeRule(array $token) { + $rule = ""; + if ($token['name'] !== "" && $token['origname']) { + $rule .= "# rule:[" . $token['name'] . "]\r\n"; + } + $rule .= $this->mergeParsedRule($token['parsedrule']); + return $rule; + } + + private function mergeParsedRule(array $token) { + $parsedRule = ""; + $parsedRule .= $this->mergeConditions($token['conditions']); + $parsedRule .= $this->mergeActions($token['actions']); + return $parsedRule; + } + + private function mergeActions(array $token) { + $actions = "{"; + foreach ($token as $action) { + $actions .= "\r\n\t" . $this->mergeAction($action) . ";"; + } + $actions .= "\r\n}"; + return $actions; + } + + private function mergeAction(array $token) { + $actionname = $token['action']; + $action = $actionname . " "; + $extension =$this->sieveStructure->sieveActions[$actionname]->extension; + if ($extension !== '' && array_search($extension, $this->requirements) === false) { + $this->requirements[] = $extension; + } + if ($token['parameters']) { + $action .= $this->mergeActionParameters($actionname, $token['parameters']); + } + return $action; + } + + private function mergeActionParameters(string $action, array $parameters) { + $actionparameters = ""; + $expectedParameters = $this->sieveStructure->sieveActions[$action]->parameters; + if (trim($expectedParameters) === "") { + return $actionparameters; + } + $expectedParamArray=explode(" ", $expectedParameters); + foreach ($expectedParamArray as $expectedParam) { + $multiple = false; + $optional = false; + $expectedParam = substr($expectedParam, 1); + if (stripos($expectedParam, '*') === 0) { + $multiple = true; + $expectedParam = substr($expectedParam, 1); + } + if (stripos($expectedParam, '?') === 0) { + $optional = true; + $expectedParam = substr($expectedParam, 1); + } + if ($parameters[$expectedParam]) { + $values = array_filter($parameters[$expectedParam], [$this, "filterEmpty"]); + if ($values) { + if (!$multiple && sizeof($values) > 1) { + $this->logger->error("To much values for parameter"); + } elseif (!$optional && sizeof($values) === 0) { + $this->logger("Error"); + } else { + $isString = $this->sieveStructure->parameterTypes[$expectedParam] === "String"; + $actionparameters .= $this->mergeParameterArray($values, $isString) . " "; + } + } + } + } + return $actionparameters; + } + + private function mergeConditions(array $token) { + $conditions = $token['condition-verb'] . " "; + $conditions .= $this->mergeTestList($token['testlist']); + return $conditions; + } + + private function mergeTestList(array $token) { + $testlist = ""; + $testcount = sizeof($token['tests']); + if ($testcount > 1 && $token['sieveListOperator']) { + $testlist .= $token['sieveListOperator'] . " "; + } + if ($testcount > 1) { + $testlist .= "(\r\n"; + } + $testlist .= $this->mergeTests($token['tests']); + if ($testcount > 1) { + $testlist .= "\r\n)"; + } + return $testlist; + } + + private function mergeTests(array $token) { + $tests = ""; + $i = 0; + foreach ($token as $test) { + if ($i > 0) { + $tests .= ",\r\n"; + } + $tests .= "\t" . $this->mergeTest($test); + ++$i; + } + return $tests; + } + + private function mergeTest(array $test) { + $testsubject = $test['testSubject']; + $testval = $testsubject . " "; + $extension =$this->sieveStructure->sieveTestSubjects[$testsubject]->extension; + if ($extension !== '' && array_search($testsubject, $this->requirements) === false) { + $this->requirements[] = $extension; + } + $testval .= $this->mergeTestParameters($test['testSubject'], $test['parameters']); + return $testval; + } + + private function mergeTestParameters(string $testsubject, array $parameters) { + $testparameters = ""; + $expectedParameters = $this->sieveStructure->sieveTestSubjects[$testsubject]->parameters; + $expectedParamArray=explode(" ", $expectedParameters); + foreach ($expectedParamArray as $expectedParam) { + $multiple = false; + $optional = false; + $expectedParam = substr($expectedParam, 1); + if (stripos($expectedParam, '*') === 0) { + $multiple = true; + $expectedParam = substr($expectedParam, 1); + } + if (stripos($expectedParam, '?') === 0) { + $optional = true; + $expectedParam = substr($expectedParam, 1); + } + if ($parameters[$expectedParam]) { + $values = array_filter($parameters[$expectedParam], [$this, "filterEmpty"]); + if ($values) { + if (!$multiple && sizeof($values) > 1) { + $this->logger->error("To much values for parameter"); + } elseif (!$optional && sizeof($values) === 0) { + $this->logger("Error"); + } else { + $isString = $this->sieveStructure->parameterTypes[$expectedParam] === "String"; + $testparameters .= $this->mergeParameterArray($values, $isString) . " "; + } + } + } + } + return $testparameters; + } + + private function mergeParameterArray(array $values, bool $isString) { + $params = ""; + $i = 0; + if (sizeof($values) > 1) { + $params .= "["; + } + foreach ($values as $value) { + if ($i > 0) { + $params .= ", "; + } + $params .= ($isString?'"':'') . $value . ($isString?'"':''); + $i++; + } + if (sizeof($values) > 1) { + $params .= "]"; + } + return $params; + } + + private function trimArray(&$item, $key) { + $item = trim($item, " [],<>\""); + } + + private function filterEmpty($var) { + return $var !== ""; + } +} diff --git a/lib/Sieve/SieveStructure.php b/lib/Sieve/SieveStructure.php new file mode 100644 index 0000000000..65c0e6d8d0 --- /dev/null +++ b/lib/Sieve/SieveStructure.php @@ -0,0 +1,162 @@ + + * + * 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 SieveStructure { + + /** @var $installedExtensions */ + public $installedExtensions = []; + + /** @var $sieveActions */ + public $sieveActions = []; + + /** @var $sieveTestSubjects */ + public $sieveTestSubjects = []; + + /** @var $sieveOperators */ + public $sieveListOperators = ['allof', 'anyof']; + + /** @var $sieveAddressPart */ + public $sieveAddressParts = []; + + /** @var $sieveControls */ + public $sieveControls = [ + 'require', 'if', 'else', 'elseif' + ]; + + /** @var $envelopePart */ + public $sieveEnvelopeParts = [ + 'from', 'to' + ]; + + /** @var $sieveMatchTypes */ + public $sieveMatchTypes = []; + + /** @var $headers */ + public $headers = [ + 'From', + 'To', + 'CC', + 'BCC', + 'Envelope-to', + 'Date', + 'Reply-To', + 'List-ID', + 'Subject', + ]; + + /** @var $parameterTypes */ + public $parameterTypes = [ + 'headers' => 'String', + 'keylist' => 'String', + 'envelopepart' => 'String', + 'mailbox' => 'String', + 'address' => 'String', + ]; + + + public function __construct() { + $this->createSieveTestSubjects(); + $this->createSieveActions(); + $this->createSieveMatchTypes(); + $this->createSieveAddressParts(); + } + + /** + * + * @param array $sieveExtensions + * + * @return array; + */ + public function getSupportedStructure(array $sieveExtensions) : array { + $this->installedExtensions = $sieveExtensions; + $supportedStructure = []; + $supportedActions = []; + $supportedActions = array_filter($this->sieveActions, [ $this, "filterByExtension"]); + $supportedAddressParts = array_filter($this->sieveAddressParts, [$this, "filterByExtension"]); + $supportedMatchTypes = array_filter($this->sieveMatchTypes, [$this, "filterByExtension"]); + $supportedTestSubjects = array_filter($this->sieveTestSubjects, [$this, "filterByExtension"]); + $supportedStructure['sieveListOperators'] = $this->sieveListOperators; + $supportedStructure['supportedAction'] = $supportedActions; + $supportedStructure['supportedAddressParts'] = $supportedAddressParts; + $supportedStructure['supportedMatchTypes'] = $supportedMatchTypes; + $supportedStructure['supportedTestSubjects'] = $supportedTestSubjects; + $supportedStructure['envelopeParts'] = $this->sieveEnvelopeParts; + $supportedStructure['headers'] = $this->headers; + return $supportedStructure; + } + + /** + * + * @param object $sieveVerbObject + * + */ + public function filterByExtension($var) { + return $var->extension === "" || in_array(strtoupper($var->extension), $this->installedExtensions); + } + + private function createSieveTestSubjects() { + $this->sieveTestSubjects = [ + 'address' => new SieveTestSubject('address', '', '%?comparator %?addresspart %matchtype %*headers %*keylist'), + 'envelope' => new SieveTestSubject('envelope', 'envelope', '%?comparator %?addresspart %matchtype %*envelopepart %*keylist'), + 'exists' => new SieveTestSubject('exists', '', '%*headers'), + 'header' => new SieveTestSubject('header', '', '%?comparator %matchtype %*headers %*keylist'), + 'size' => new SieveTestSubject('size', '', '%matchtype %size'), + ]; + } + + private function createSieveAddressParts() { + $this->sieveAddressParts = [ + ':localpart' => new SieveAddressPart(':localpart', '', ['address', 'envelope']), + ':domain' => new SieveAddressPart(':domain', '', ['address', 'envelope']), + ':all' => new SieveAddressPart(':all', '', ['address', 'envelope']), + ]; + } + + private function createSieveMatchTypes() { + $this->sieveMatchTypes = [ + ':contains' => new SieveMatchType(':contains', '', ['address', 'envelope', 'header']), + ':is' => new SieveMatchType(':is', 'envelope', ['address', 'envelope', 'header']), + ':matches' => new SieveMatchType(':matches', '', ['address', 'envelope', 'header']), + ':over' => new SieveMatchType(':over', '', ['size']), + ':under' => new SieveMatchType(':under', '', ['size']), + ]; + } + + private function createSieveActions() { + $this->sieveActions =[ + 'keep' => new SieveAction('keep'), + 'fileinto' => new SieveAction('fileinto', 'fileinto', '%mailbox'), + 'redirect' => new SieveAction('redirect', '', '%address'), + 'discard' => new SieveAction('discard', '', ''), + 'stop' => new SieveAction('stop', '', ''), + //'notify' => new SieveAction('notify', 'notify', '%folder'), + //'addheader' => new SieveAction('addheader', 'editheader', '%header'), + //'deleteheader' => new SieveAction('deleteheader', 'editheader', '%header'), + //'setflag' => new SieveAction('setflag', '', '%folder'), + //'deleteflag' => new SieveAction('deleteflag', '', '%folder'), + //'removeflag' => new SieveAction('removeflag', '', '%folder') + ]; + } +} diff --git a/lib/Sieve/SieveSyntaxItem.php b/lib/Sieve/SieveSyntaxItem.php new file mode 100644 index 0000000000..8b4ab8f3ae --- /dev/null +++ b/lib/Sieve/SieveSyntaxItem.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 + * + */ + + class SieveSyntaxItem { + /** @var $name */ + public $name; + + /** @var $extension */ + public $extension; + + /** + * @param String $name + * @param String $extension + */ + + public function __construct(String $name, String $extension = '') { + $this->name = $name; + $this->extension = $extension; + } + + public function __toString() { + return $this->name . "(" . $this->extension . ")"; + } + } diff --git a/lib/Sieve/SieveTestSubject.php b/lib/Sieve/SieveTestSubject.php new file mode 100644 index 0000000000..71ba78e237 --- /dev/null +++ b/lib/Sieve/SieveTestSubject.php @@ -0,0 +1,39 @@ + + * + * 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 + * + */ + + class SieveTestSubject extends SieveSyntaxItem { + + /** @var $parameters */ + public $parameters; + + public function __construct(String $name, String $extension = '', String $parameters) { + parent::__construct($name, $extension); + $this->parameters = $parameters; + } + + public function __toString() { + return $this->name . "(" . $this->extension . ")"; + } + } diff --git a/src/components/SieveAccountForm.vue b/src/components/SieveAccountForm.vue new file mode 100644 index 0000000000..fb9b541a16 --- /dev/null +++ b/src/components/SieveAccountForm.vue @@ -0,0 +1,240 @@ + + + + + + + diff --git a/src/components/SieveFilterAction.vue b/src/components/SieveFilterAction.vue new file mode 100644 index 0000000000..4f69988c25 --- /dev/null +++ b/src/components/SieveFilterAction.vue @@ -0,0 +1,128 @@ + + + + diff --git a/src/components/SieveFilterNavigation.vue b/src/components/SieveFilterNavigation.vue new file mode 100644 index 0000000000..b1bc18c99e --- /dev/null +++ b/src/components/SieveFilterNavigation.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/src/components/SieveFilterTest.vue b/src/components/SieveFilterTest.vue new file mode 100644 index 0000000000..2d95924d2c --- /dev/null +++ b/src/components/SieveFilterTest.vue @@ -0,0 +1,288 @@ + + + + diff --git a/src/service/SieveService.js b/src/service/SieveService.js new file mode 100644 index 0000000000..e07775881b --- /dev/null +++ b/src/service/SieveService.js @@ -0,0 +1,60 @@ +/* + * @copyright 2020 Christoph Wurst + * + * @author 2020 Holger Dehnhardt + * + * @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 Axios from '@nextcloud/axios' + +export const updateSieveAccount = (data) => { + const url = generateUrl('/apps/mail/api/sieve/{id}/account', { + id: data.accountId, + }) + + return Axios.put(url, data).then((resp) => resp.data) +} + +export const listScripts = (accountId) => { + const url = generateUrl('/apps/mail/api/sieve/{id}/account', { + id: accountId, + }) + + return Axios.get(url).then((resp) => resp.data) +} + +export const getScriptContent = (accountId, scriptName) => { + const url = generateUrl('/apps/mail/api/sieve/{id}/script/{scriptName}', { + id: accountId, + scriptName, + }) + + return Axios.get(url).then((resp) => resp.data) +} + +export const putScriptContent = (accountId, scriptName, install, scriptContent) => { + const url = generateUrl('/apps/mail/api/sieve/{id}/script/{scriptName}', { + id: accountId, + scriptName, + }) + const data = { + install, + scriptContent, + } + return Axios.put(url, data).then((resp) => resp.data) +} diff --git a/src/store/sieve.js b/src/store/sieve.js new file mode 100644 index 0000000000..140a190241 --- /dev/null +++ b/src/store/sieve.js @@ -0,0 +1,111 @@ +import { + // updateSieveAccount, + listScripts, + // getScriptContent as getSieveScriptContent, + // putScriptContent as putSieveScriptContent, +} from '../service/SieveService' + +export default { + namespaced: true, + state() { + return { + activeFilterset: '', + selectedFilterset: '', + filtersetNames: [], + filterRules: [], + filtersetOrigin: '', + filtersets: Object(), + supportedSieveStructure: Object(), + } + }, + mutations: { + addFilterset(state, data) { + state.activeFilterset = data.activeScript + state.selectedFilterset = data.activeScript + state.filtersetNames = data.scripts + state.supportedSieveStructure = data.supportedSieveStructure + state.filtersets[state.selectedFilterset] = data.scriptContent + }, + extractFilterRules(state, scriptName) { + let i = 0 + state.filterRules = [] + state.filtersets[scriptName].forEach((rule, index) => { + if (rule.type === 'header') { + state.filtersetOrigin = rule.scriptOrigin + } else if (rule.type === 'rule') { + rule.index = index + state.filterRules[i] = rule + i++ + } + }) + }, + }, + actions: { + /* updateSieveAccount({commit}, account) { + return updateSieveAccount(account) + .then((data) => { + console.info('UpdateSieveAccount returned') + commit('setSieveStatus', {account, sieveEnabled: data.sieveEnabled}) + return data + }) + .catch((err) => { + console.info('UpdateSieveAccount errored') + commit('setSieveStatus', {account, sieveEnabled: false}) + throw err + }) + }, */ + listFiltersets({ commit }, accountId) { + return listScripts(accountId) + .then((data) => { + console.info('sieve/listFiltersets returned') + commit('addFilterset', data) + commit('extractFilterRules', data.activeScript) + return data + }) + .catch((err) => { + console.info('sieve/listFiltersets errored') + throw err + }) + }, + /* getSieveScriptContent({commit}, {accountId, scriptName}) { + return getSieveScriptContent(accountId, scriptName) + .then((data) => { + console.info('getSieveScriptContent returned') + commit('addSieveScript', {scriptName, data}) + return data + }) + .catch((err) => { + console.info('getSieveScriptContent errored') + throw err + }) + }, + putSieveScriptContent({commit}, {accountId, scriptName, install, scriptContent}) { + return putSieveScriptContent(accountId, scriptName, install, scriptContent) + .then((data) => { + console.info('putSieveScriptContent returned') + return data + }) + .catch((err) => { + console.info('putSieveScriptContent errored') + throw err + }) + }, */ + }, + getters: { + getActiveFilterset(state) { + return state.activeFilterset + }, + getFiltersets(state) { + return state.filtersetNames + }, + getSelectedFilterset(state) { + return state.selectedFilterset + }, + getFiltersetOrigin(state) { + return state.filtersetOrigin + }, + isActiveFilterset(state) { + return state.activeFilterset === state.selectedFilterset + } + }, +} diff --git a/src/views/FilterSettings.vue b/src/views/FilterSettings.vue new file mode 100644 index 0000000000..6ca0aec658 --- /dev/null +++ b/src/views/FilterSettings.vue @@ -0,0 +1,375 @@ + + + + + + diff --git a/src/views/SieveFilterRules.vue b/src/views/SieveFilterRules.vue new file mode 100644 index 0000000000..3fa9c26680 --- /dev/null +++ b/src/views/SieveFilterRules.vue @@ -0,0 +1,184 @@ + + + + + + + From 3979809839b5fcd5294603643a3b41b06db384fe Mon Sep 17 00:00:00 2001 From: Holger Dehnhardt Date: Mon, 12 Oct 2020 18:58:20 +0200 Subject: [PATCH 2/6] redo sive changes Signed-off-by: Holger Dehnhardt --- appinfo/routes.php | 21 +++++++++++++++ composer.json | 1 + lib/Account.php | 7 +++++ lib/AppInfo/Application.php | 3 +++ src/components/NavigationAccount.vue | 16 +++++++++++ src/store/actions.js | 40 ++++++++++++++++++++++++++++ src/store/mutations.js | 3 +++ src/views/AccountSettings.vue | 5 ++++ 8 files changed, 96 insertions(+) diff --git a/appinfo/routes.php b/appinfo/routes.php index 4f669ad09a..c08e1454ed 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -179,6 +179,26 @@ 'url' => '/api/settings/provisioning', 'verb' => 'DELETE' ], + [ + 'name' => 'sieve#updateSieveAccount', + 'url' => '/api/sieve/{accountId}/account', + 'verb' => 'PUT' + ], + [ + 'name' => 'sieve#listScripts', + 'url' => '/api/sieve/{accountId}/account', + 'verb' => 'GET' + ], + [ + 'name' => 'sieve#getScriptContent', + 'url' => '/api/sieve/{accountId}/script/{scriptName}', + 'verb' => 'GET' + ], + [ + 'name' => 'sieve#setScriptContent', + 'url' => '/api/sieve/{accountId}/script/{scriptName}', + 'verb' => 'PUT' + ], ], 'resources' => [ 'accounts' => ['url' => '/api/accounts'], @@ -188,5 +208,6 @@ 'mailboxes' => ['url' => '/api/mailboxes'], 'messages' => ['url' => '/api/messages'], 'preferences' => ['url' => '/api/preferences'], + 'sieve' => ['url' => '/api/sieve'], ] ]; diff --git a/composer.json b/composer.json index 5ae9dc7be5..92e401afdc 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,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.2@stable", "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_stream": "^1.6.3@stable", diff --git a/lib/Account.php b/lib/Account.php index 871304c588..b5f7a2fe48 100644 --- a/lib/Account.php +++ b/lib/Account.php @@ -114,6 +114,13 @@ public function getEMailAddress() { return $this->account->getEmail(); } + /** + * @return boolean + */ + public function getSieveEnabled() { + return $this->account->getSieveEnabled(); + } + /** * @deprecated use \OCA\Mail\IMAP\IMAPClientFactory instead * @return Horde_Imap_Client_Socket diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index a9eca0249f..d7339a3e24 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -31,6 +31,7 @@ use OCA\Mail\Contracts\IMailManager; use OCA\Mail\Contracts\IMailSearch; use OCA\Mail\Contracts\IMailTransmission; +use OCA\Mail\Contracts\ISieveParser; use OCA\Mail\Contracts\IUserPreferences; use OCA\Mail\Dashboard\MailWidget; use OCA\Mail\Events\BeforeMessageDeletedEvent; @@ -62,6 +63,7 @@ use OCA\Mail\Service\MailTransmission; use OCA\Mail\Service\Search\MailSearch; use OCA\Mail\Service\UserPreferenceSevice; +use OCA\Mail\Sieve\SieveParser; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; @@ -97,6 +99,7 @@ public function register(IRegistrationContext $context): void { $context->registerServiceAlias(IMailManager::class, MailManager::class); $context->registerServiceAlias(IMailSearch::class, MailSearch::class); $context->registerServiceAlias(IMailTransmission::class, MailTransmission::class); + $context->registerServiceAlias(ISieveParser::class, SieveParser::class); $context->registerServiceAlias(IUserPreferences::class, UserPreferenceSevice::class); $context->registerEventListener(BeforeMessageDeletedEvent::class, TrashMailboxCreatorListener::class); diff --git a/src/components/NavigationAccount.vue b/src/components/NavigationAccount.vue index 20d1e56a96..1dc0f49bea 100644 --- a/src/components/NavigationAccount.vue +++ b/src/components/NavigationAccount.vue @@ -41,6 +41,14 @@ {{ t('mail', 'Account settings') }} + + + {{ t('mail', 'Edit filter settings') }} + { + console.info('UpdateSieveAccount returned') + commit('setSieveStatus', { account, sieveEnabled: data.sieveEnabled }) + return data + }) + .catch((err) => { + console.info('UpdateSieveAccount errored') + commit('setSieveStatus', { account, sieveEnabled: false }) + throw err + }) + }, + getSieveScriptContent({ commit }, { accountId, scriptName }) { + return getSieveScriptContent(accountId, scriptName) + .then((data) => { + console.info('getSieveScriptContent returned') + return data + }) + .catch((err) => { + console.info('getSieveScriptContent errored') + throw err + }) + }, + putSieveScriptContent({ commit }, { accountId, scriptName, install, scriptContent }) { + return putSieveScriptContent(accountId, scriptName, install, scriptContent) + .then((data) => { + console.info('putSieveScriptContent returned') + return data + }) + .catch((err) => { + console.info('putSieveScriptContent errored') + throw err + }) + }, moveAccount({ commit, getters }, { account, up }) { const accounts = getters.accounts const index = accounts.indexOf(account) diff --git a/src/store/mutations.js b/src/store/mutations.js index 591366e166..d1db62eb18 100644 --- a/src/store/mutations.js +++ b/src/store/mutations.js @@ -134,6 +134,9 @@ export default { } removeRec(account) }, + setSieveStatus(state, { account, sieveEnabled }) { + Vue.set(state.accounts[account.accountId], 'sieveEnabled', sieveEnabled) + }, addEnvelope(state, { query, envelope }) { const mailbox = state.mailboxes[envelope.mailboxId] Vue.set(state.envelopes, envelope.databaseId, Object.assign({}, state.envelopes[envelope.databaseId] || {}, envelope)) diff --git a/src/views/AccountSettings.vue b/src/views/AccountSettings.vue index b51a31bc6f..4a5bb3b057 100644 --- a/src/views/AccountSettings.vue +++ b/src/views/AccountSettings.vue @@ -27,6 +27,9 @@ :account="account" /> +
+ +
@@ -36,6 +39,7 @@ import AppContent from '@nextcloud/vue/dist/Components/AppContent' import Content from '@nextcloud/vue/dist/Components/Content' import AccountForm from '../components/AccountForm' +import SieveAccountForm from '../components/SieveAccountForm' import EditorSettings from '../components/EditorSettings' import Logger from '../logger' import Navigation from '../components/Navigation' @@ -47,6 +51,7 @@ export default { components: { AccountForm, AliasSettings, + SieveAccountForm, AppContent, Content, EditorSettings, From f72ac5f7575b1d38c4546766e387aac8810025f5 Mon Sep 17 00:00:00 2001 From: Holger Dehnhardt Date: Tue, 13 Oct 2020 22:13:21 +0200 Subject: [PATCH 3/6] Fix coding style Signed-off-by: Holger Dehnhardt --- composer.lock | 465 +++++++++---------- lib/Account.php | 2 +- lib/Controller/SieveController.php | 4 +- lib/Db/MailAccount.php | 45 ++ lib/Service/SieveService.php | 2 +- lib/Sieve/SieveParser.php | 2 +- package-lock.json | 2 +- src/components/AppContentListEntry.vue | 540 +++++++++++++++++++++++ src/components/SieveAccountForm.vue | 54 +-- src/components/SieveFilterAction.vue | 23 +- src/components/SieveFilterNavigation.vue | 9 +- src/components/SieveFilterTest.vue | 67 ++- src/router.js | 12 + src/service/SieveService.js | 8 +- src/store/sieve.js | 6 +- src/views/SieveFilterRules.vue | 52 +-- webpack.dev.js | 2 +- 17 files changed, 938 insertions(+), 357 deletions(-) create mode 100644 src/components/AppContentListEntry.vue diff --git a/composer.lock b/composer.lock index 8ea00e48bb..e584c736c6 100644 --- a/composer.lock +++ b/composer.lock @@ -82,12 +82,6 @@ "non-blocking", "promise" ], - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], "time": "2020-07-14T21:47:18+00:00" }, { @@ -731,12 +725,6 @@ } ], "description": "Composes all dependencies as a package inside a WordPress plugin", - "funding": [ - { - "url": "https://github.com/coenjacobs", - "type": "github" - } - ], "time": "2020-05-23T13:12:01+00:00" }, { @@ -978,12 +966,6 @@ "sftp", "storage" ], - "funding": [ - { - "url": "https://offset.earth/frankdejonge", - "type": "other" - } - ], "time": "2020-08-23T07:39:11+00:00" }, { @@ -1254,6 +1236,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.1", @@ -2141,18 +2153,120 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "funding": [ + "time": "2020-09-02T16:23:27+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.18.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", + "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" }, { - "url": "https://github.com/fabpot", - "type": "github" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2020-07-14T12:35:20+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.18.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b740103edbdcc39602239ee8860f0f45a8eb9aa5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b740103edbdcc39602239ee8860f0f45a8eb9aa5", + "reference": "b740103edbdcc39602239ee8860f0f45a8eb9aa5", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], "time": "2020-09-02T16:23:27+00:00" @@ -2453,20 +2567,6 @@ "portable", "shim" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-07-14T12:35:20+00:00" }, { @@ -2529,20 +2629,6 @@ "portable", "shim" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-07-14T12:35:20+00:00" }, { @@ -2609,20 +2695,6 @@ "portable", "shim" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-07-14T12:35:20+00:00" }, { @@ -2685,18 +2757,65 @@ "interoperability", "standards" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" + "time": "2020-09-07T11:33:47+00:00" + }, + { + "name": "symfony/string", + "version": "v5.1.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "4a9afe9d07bac506f75bcee8ed3ce76da5a9343e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/4a9afe9d07bac506f75bcee8ed3ce76da5a9343e", + "reference": "4a9afe9d07bac506f75bcee8ed3ce76da5a9343e", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "~1.15" + }, + "require-dev": { + "symfony/error-handler": "^4.4|^5.0", + "symfony/http-client": "^4.4|^5.0", + "symfony/translation-contracts": "^1.1|^2", + "symfony/var-exporter": "^4.4|^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\String\\": "" }, + "files": [ + "Resources/functions.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ { - "url": "https://github.com/fabpot", - "type": "github" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], "time": "2020-09-07T11:33:47+00:00" @@ -2917,20 +3036,6 @@ } ], "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], "time": "2020-08-25T05:50:16+00:00" }, { @@ -2992,20 +3097,6 @@ "validation", "versioning" ], - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], "time": "2020-09-27T13:13:07+00:00" }, { @@ -3050,20 +3141,6 @@ "Xdebug", "performance" ], - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], "time": "2020-08-19T10:27:58+00:00" }, { @@ -3223,20 +3300,6 @@ "constructor", "instantiate" ], - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", - "type": "tidelift" - } - ], "time": "2020-05-29T17:27:14+00:00" }, { @@ -3299,20 +3362,6 @@ "parser", "php" ], - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", - "type": "tidelift" - } - ], "time": "2020-05-25T17:44:05+00:00" }, { @@ -3492,12 +3541,6 @@ } ], "description": "A tool to automatically fix PHP code style", - "funding": [ - { - "url": "https://github.com/keradus", - "type": "github" - } - ], "time": "2020-06-27T23:57:46+00:00" }, { @@ -3587,12 +3630,6 @@ "object", "object graph" ], - "funding": [ - { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" - } - ], "time": "2020-06-29T13:22:24+00:00" }, { @@ -4764,17 +4801,53 @@ "testing", "xunit" ], - "funding": [ - { - "url": "https://phpunit.de/donate.html", - "type": "custom" - }, + "time": "2020-06-22T07:06:58+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" } ], - "time": "2020-07-13T17:55:55+00:00" + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "time": "2019-01-08T18:20:26+00:00" }, { "name": "psr/event-dispatcher", @@ -5119,16 +5192,6 @@ } ], "description": "Prevents installation of composer packages with known security vulnerabilities: no API, simply require it", - "funding": [ - { - "url": "https://github.com/Ocramius", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/roave/security-advisories", - "type": "tidelift" - } - ], "time": "2020-10-08T21:02:27+00:00" }, { @@ -6350,20 +6413,6 @@ "portable", "shim" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-07-14T12:35:20+00:00" }, { @@ -6423,20 +6472,6 @@ "portable", "shim" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-07-14T12:35:20+00:00" }, { @@ -6487,20 +6522,6 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-09-02T16:23:27+00:00" }, { @@ -6605,12 +6626,6 @@ } ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "funding": [ - { - "url": "https://github.com/theseer", - "type": "github" - } - ], "time": "2020-07-12T23:59:07+00:00" }, { @@ -6865,6 +6880,7 @@ "pear-pear.horde.org/horde_exception": 0, "pear-pear.horde.org/horde_imap_client": 0, "pear-pear.horde.org/horde_mail": 0, + "pear-pear.horde.org/horde_managesieve": 0, "pear-pear.horde.org/horde_mime": 0, "pear-pear.horde.org/horde_nls": 0, "pear-pear.horde.org/horde_stream": 0, @@ -6883,6 +6899,5 @@ "platform-dev": [], "platform-overrides": { "php": "7.3" - }, - "plugin-api-version": "1.1.0" + } } diff --git a/lib/Account.php b/lib/Account.php index b5f7a2fe48..8e1f9cf8c8 100644 --- a/lib/Account.php +++ b/lib/Account.php @@ -204,7 +204,7 @@ public function jsonSerialize() { * @param string $sslMode * @return false|string */ - protected function convertSslMode($sslMode) { + public function convertSslMode($sslMode) { switch ($sslMode) { case 'none': return false; diff --git a/lib/Controller/SieveController.php b/lib/Controller/SieveController.php index a01fcde228..22e7086561 100644 --- a/lib/Controller/SieveController.php +++ b/lib/Controller/SieveController.php @@ -47,7 +47,7 @@ class SieveController extends Controller { * @param string $appName * @param IRequest $request * @param AccountService $accountService - * @param string $UserId + * @param String $UserId * @param SieveService $sieveService * @param ILogger $logger */ @@ -55,7 +55,7 @@ class SieveController extends Controller { public function __construct(string $appName, IRequest $request, AccountService $accountService, - $UserId, + String $UserId, SieveService $sieveService, ILogger $logger) { parent::__construct($appName, $request); diff --git a/lib/Db/MailAccount.php b/lib/Db/MailAccount.php index 80b554c46b..12813e108a 100644 --- a/lib/Db/MailAccount.php +++ b/lib/Db/MailAccount.php @@ -57,6 +57,16 @@ * @method void setOutboundUser(string $outboundUser) * @method string|null getOutboundPassword() * @method void setOutboundPassword(string $outboundPassword) + * @method string|null getSieveHost() + * @method void setSieveHost(string|null $sieveHost) + * @method integer|null getSievePort() + * @method void setSievePort(integer|null $sievePort) + * @method string|null getSieveSslMode() + * @method void setSieveSslMode(string|null $sieveSslMode) + * @method string|null getSieveUser() + * @method void setSieveUser(string|null $sieveUser) + * @method string|null getSievePassword() + * @method void setSievePassword(string|null $sievePassword) * @method string|null getSignature() * @method void setSignature(string|null $signature) * @method int getLastMailboxSync() @@ -86,6 +96,12 @@ class MailAccount extends Entity { protected $outboundSslMode; protected $outboundUser; protected $outboundPassword; + protected $sieveEnabled; + protected $sieveHost; + protected $sievePort; + protected $sieveSslMode; + protected $sieveUser; + protected $sievePassword; protected $signature; protected $lastMailboxSync; protected $editorMode; @@ -139,6 +155,25 @@ public function __construct(array $params=[]) { if (isset($params['smtpPassword'])) { $this->setOutboundPassword($params['smtpPassword']); } + if (isset($params['sieveEnabled'])) { + $this->sieveEnabled($params['sieveEnabled']); + } + if (isset($params['sieveHost'])) { + $this->setSieveHost($params['sieveHost']); + } + if (isset($params['sievePort'])) { + $this->setSievePort(intval($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']); + } + if (isset($params['showSubscribedOnly'])) { $this->setShowSubscribedOnly($params['showSubscribedOnly']); } @@ -148,6 +183,8 @@ public function __construct(array $params=[]) { $this->addType('lastMailboxSync', 'integer'); $this->addType('provisioned', 'bool'); $this->addType('order', 'integer'); + $this->addType('sieveEnabled', 'boolean'); + $this->addType('sievePort', 'integer'); $this->addType('showSubscribedOnly', 'boolean'); $this->addType('personalNamespace', 'string'); } @@ -166,6 +203,7 @@ public function toJson() { 'imapPort' => $this->getInboundPort(), 'imapUser' => $this->getInboundUser(), 'imapSslMode' => $this->getInboundSslMode(), + 'sieveEnabled' => $this->getSieveEnabled(), 'signature' => $this->getSignature(), 'editorMode' => $this->getEditorMode(), 'provisioned' => $this->getProvisioned(), @@ -180,6 +218,13 @@ public function toJson() { $result['smtpSslMode'] = $this->getOutboundSslMode(); } + if ($this->getSieveEnabled() !== null) { + $result['sieveHost'] = $this->getSieveHost(); + $result['sievePort'] = $this->getSievePort(); + $result['sieveUser'] = $this->getSieveUser(); + $result['sieveSslMode'] = $this->getSieveSslMode(); + } + return $result; } } diff --git a/lib/Service/SieveService.php b/lib/Service/SieveService.php index 2122e1f030..099a68a7a1 100644 --- a/lib/Service/SieveService.php +++ b/lib/Service/SieveService.php @@ -110,7 +110,7 @@ public function listScripts(Account $account) : array { $sieveExtensions = $sieveClient->getExtensions(); $scripts = $sieveClient->listScripts(); $activeScript = $sieveClient->getActive(); - if( sizeof($scripts) > 0 && $activeScript ){ + if (sizeof($scripts) > 0 && $activeScript) { $scriptContent = $this->getScriptContent($account, $activeScript); } $supportedSieveStructure = $this->sieveParser->getSupportedSieveStructure($sieveExtensions); diff --git a/lib/Sieve/SieveParser.php b/lib/Sieve/SieveParser.php index b6ea93f0ac..23fd37def2 100644 --- a/lib/Sieve/SieveParser.php +++ b/lib/Sieve/SieveParser.php @@ -52,7 +52,7 @@ class SieveParser implements ISieveParser { private $requirements = []; /** @var $generatorName */ - const GENERATOR_NAME = "Nextcloud Mail"; + public const GENERATOR_NAME = "Nextcloud Mail"; /** @var $origin */ private $native = false; diff --git a/package-lock.json b/package-lock.json index 882c9c046e..a0ce59cba7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6018,7 +6018,7 @@ }, "domelementtype": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "resolved": "http://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" }, "domexception": { diff --git a/src/components/AppContentListEntry.vue b/src/components/AppContentListEntry.vue new file mode 100644 index 0000000000..09affdd30d --- /dev/null +++ b/src/components/AppContentListEntry.vue @@ -0,0 +1,540 @@ + + + + + diff --git a/src/components/SieveAccountForm.vue b/src/components/SieveAccountForm.vue index fb9b541a16..f54ab477c3 100644 --- a/src/components/SieveAccountForm.vue +++ b/src/components/SieveAccountForm.vue @@ -10,8 +10,7 @@ type="radio" name="sieve-active" :disabled="loading" - value="0" - /> + value="0"> @@ -21,8 +20,7 @@ type="radio" name="sieve-active" :disabled="loading" - value="1" - /> + value="1"> @@ -34,8 +32,7 @@ v-model="sieveAccount.sieveHost" type="text" :disabled="controlsDisabled" - required - /> + required>

{{ t('mail', 'Sieve Security') }}

+ @change="onSieveSslModeChange"> + :class="{primary: sieveAccount.sieveSslMode === 'none'}">{{ t('mail', 'None') }} + value="ssl"> + @change="onSieveSslModeChange">
@@ -94,32 +84,28 @@ v-model="sieveAccount.sievePort" type="text" :disabled="controlsDisabled" - required - /> + required> + required> - + required> + + @click.prevent="onSubmit"> @@ -128,7 +114,7 @@ + diff --git a/src/components/SieveFilterNavigation.vue b/src/components/SieveFilterNavigation.vue index d055f3ad61..a4ce68f14a 100644 --- a/src/components/SieveFilterNavigation.vue +++ b/src/components/SieveFilterNavigation.vue @@ -21,7 +21,7 @@ icon="icon-filter">