From d74c401e6653be7d0713cb0c6d2ab358c9cf7516 Mon Sep 17 00:00:00 2001 From: Daniel Kesselberg Date: Thu, 10 Oct 2024 21:58:53 +0200 Subject: [PATCH] feat: mail filters Co-authored-by: Hamza Mahjoubi Signed-off-by: Daniel Kesselberg --- REUSE.toml | 6 + lib/Controller/FilterController.php | 59 +++++ lib/Exception/FilterParserException.php | 29 +++ lib/Service/AllowedRecipientsService.php | 34 +++ lib/Service/FilterService.php | 88 +++++++ lib/Service/MailFilter/FilterBuilder.php | 165 ++++++++++++ lib/Service/MailFilter/FilterParser.php | 67 +++++ lib/Service/MailFilter/FilterParserResult.php | 43 ++++ lib/Service/MailFilter/FilterState.php | 35 +++ lib/Service/MailManager.php | 6 +- lib/Service/OutOfOffice/OutOfOfficeParser.php | 10 +- lib/Service/OutOfOfficeService.php | 16 +- src/components/AccountSettings.vue | 13 +- src/components/SieveAccountForm.vue | 2 +- src/components/mailFilter/Action.vue | 98 +++++++ src/components/mailFilter/ActionAddflag.vue | 45 ++++ src/components/mailFilter/ActionFileinto.vue | 50 ++++ src/components/mailFilter/ActionStop.vue | 23 ++ src/components/mailFilter/DeleteModal.vue | 62 +++++ src/components/mailFilter/MailFilters.vue | 218 ++++++++++++++++ src/components/mailFilter/Operator.vue | 48 ++++ src/components/mailFilter/Test.vue | 120 +++++++++ src/components/mailFilter/UpdateModal.vue | 213 ++++++++++++++++ src/service/MailFilterService.js | 22 ++ src/store/mailFilterStore.js | 52 ++++ .../Service/AllowedRecipientsServiceTest.php | 54 ++++ tests/Unit/Service/FilterServiceTest.php | 239 ++++++++++++++++++ .../Service/MailFilter/FilterBuilderTest.php | 65 +++++ .../Service/MailFilter/FilterParserTest.php | 77 ++++++ tests/Unit/Service/OutOfOfficeServiceTest.php | 20 +- tests/data/mail-filter/builder1.json | 24 ++ tests/data/mail-filter/builder1.sieve | 11 + tests/data/mail-filter/builder2.json | 31 +++ tests/data/mail-filter/builder2.sieve | 11 + tests/data/mail-filter/builder3.json | 55 ++++ tests/data/mail-filter/builder3.sieve | 16 ++ tests/data/mail-filter/builder4.json | 15 ++ tests/data/mail-filter/builder4.sieve | 6 + tests/data/mail-filter/builder5.json | 27 ++ tests/data/mail-filter/builder5.sieve | 12 + tests/data/mail-filter/builder6.json | 36 +++ tests/data/mail-filter/builder6.sieve | 12 + tests/data/mail-filter/builder7.json | 1 + tests/data/mail-filter/builder7.sieve | 1 + tests/data/mail-filter/parser1.sieve | 11 + tests/data/mail-filter/parser2.sieve | 11 + tests/data/mail-filter/parser3.sieve | 21 ++ .../data/mail-filter/parser3_untouched.sieve | 21 ++ tests/data/mail-filter/parser4.sieve | 31 +++ .../data/mail-filter/parser4_untouched.sieve | 21 ++ tests/data/mail-filter/service1.json | 23 ++ tests/data/mail-filter/service1.sieve | 21 ++ tests/data/mail-filter/service1_new.sieve | 31 +++ tests/data/mail-filter/service2.json | 23 ++ tests/data/mail-filter/service2.sieve | 35 +++ tests/data/mail-filter/service2_new.sieve | 31 +++ 56 files changed, 2481 insertions(+), 36 deletions(-) create mode 100644 lib/Controller/FilterController.php create mode 100644 lib/Exception/FilterParserException.php create mode 100644 lib/Service/AllowedRecipientsService.php create mode 100644 lib/Service/FilterService.php create mode 100644 lib/Service/MailFilter/FilterBuilder.php create mode 100644 lib/Service/MailFilter/FilterParser.php create mode 100644 lib/Service/MailFilter/FilterParserResult.php create mode 100644 lib/Service/MailFilter/FilterState.php create mode 100644 src/components/mailFilter/Action.vue create mode 100644 src/components/mailFilter/ActionAddflag.vue create mode 100644 src/components/mailFilter/ActionFileinto.vue create mode 100644 src/components/mailFilter/ActionStop.vue create mode 100644 src/components/mailFilter/DeleteModal.vue create mode 100644 src/components/mailFilter/MailFilters.vue create mode 100644 src/components/mailFilter/Operator.vue create mode 100644 src/components/mailFilter/Test.vue create mode 100644 src/components/mailFilter/UpdateModal.vue create mode 100644 src/service/MailFilterService.js create mode 100644 src/store/mailFilterStore.js create mode 100644 tests/Unit/Service/AllowedRecipientsServiceTest.php create mode 100644 tests/Unit/Service/FilterServiceTest.php create mode 100644 tests/Unit/Service/MailFilter/FilterBuilderTest.php create mode 100644 tests/Unit/Service/MailFilter/FilterParserTest.php create mode 100644 tests/data/mail-filter/builder1.json create mode 100644 tests/data/mail-filter/builder1.sieve create mode 100644 tests/data/mail-filter/builder2.json create mode 100644 tests/data/mail-filter/builder2.sieve create mode 100644 tests/data/mail-filter/builder3.json create mode 100644 tests/data/mail-filter/builder3.sieve create mode 100644 tests/data/mail-filter/builder4.json create mode 100644 tests/data/mail-filter/builder4.sieve create mode 100644 tests/data/mail-filter/builder5.json create mode 100644 tests/data/mail-filter/builder5.sieve create mode 100644 tests/data/mail-filter/builder6.json create mode 100644 tests/data/mail-filter/builder6.sieve create mode 100644 tests/data/mail-filter/builder7.json create mode 100644 tests/data/mail-filter/builder7.sieve create mode 100644 tests/data/mail-filter/parser1.sieve create mode 100644 tests/data/mail-filter/parser2.sieve create mode 100644 tests/data/mail-filter/parser3.sieve create mode 100644 tests/data/mail-filter/parser3_untouched.sieve create mode 100644 tests/data/mail-filter/parser4.sieve create mode 100644 tests/data/mail-filter/parser4_untouched.sieve create mode 100644 tests/data/mail-filter/service1.json create mode 100644 tests/data/mail-filter/service1.sieve create mode 100644 tests/data/mail-filter/service1_new.sieve create mode 100644 tests/data/mail-filter/service2.json create mode 100644 tests/data/mail-filter/service2.sieve create mode 100644 tests/data/mail-filter/service2_new.sieve diff --git a/REUSE.toml b/REUSE.toml index df7fb28ef..1e2a9364c 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -101,6 +101,12 @@ precedence = "aggregate" SPDX-FileCopyrightText = "2024 Nextcloud GmbH and Nextcloud contributors" SPDX-License-Identifier = "AGPL-3.0-or-later" +[[annotations]] +path = ["tests/data/mail-filter/*"] +precedence = "aggregate" +SPDX-FileCopyrightText = "2024 Nextcloud GmbH and Nextcloud contributors" +SPDX-License-Identifier = "AGPL-3.0-or-later" + [[annotations]] path = ".github/CODEOWNERS" precedence = "aggregate" diff --git a/lib/Controller/FilterController.php b/lib/Controller/FilterController.php new file mode 100644 index 000000000..f2fc3104e --- /dev/null +++ b/lib/Controller/FilterController.php @@ -0,0 +1,59 @@ +currentUserId = $userId; + } + + #[Route(Route::TYPE_FRONTPAGE, verb: 'GET', url: '/api/filter/{accountId}', requirements: ['accountId' => '[\d]+'])] + public function getFilters(int $accountId) { + $account = $this->accountService->findById($accountId); + + if ($account->getUserId() !== $this->currentUserId) { + return new JSONResponse([], Http::STATUS_NOT_FOUND); + } + + $result = $this->mailFilterService->parse($account->getMailAccount()); + + return new JSONResponse($result->getFilters()); + } + + #[Route(Route::TYPE_FRONTPAGE, verb: 'PUT', url: '/api/filter/{accountId}', requirements: ['accountId' => '[\d]+'])] + public function updateFilters(int $accountId, array $filters) { + $account = $this->accountService->findById($accountId); + + if ($account->getUserId() !== $this->currentUserId) { + return new JSONResponse([], Http::STATUS_NOT_FOUND); + } + + $this->mailFilterService->update($account->getMailAccount(), $filters); + + return new JSONResponse([]); + } +} diff --git a/lib/Exception/FilterParserException.php b/lib/Exception/FilterParserException.php new file mode 100644 index 000000000..a6ab6d866 --- /dev/null +++ b/lib/Exception/FilterParserException.php @@ -0,0 +1,29 @@ +getMessage(), + 0, + $exception, + ); + } + + public static function invalidState(): FilterParserException { + return new self( + 'Reached an invalid state', + ); + } +} diff --git a/lib/Service/AllowedRecipientsService.php b/lib/Service/AllowedRecipientsService.php new file mode 100644 index 000000000..f97e03338 --- /dev/null +++ b/lib/Service/AllowedRecipientsService.php @@ -0,0 +1,34 @@ + $alias->getAlias(), + $this->aliasesService->findAll($mailAccount->getId(), $mailAccount->getUserId()) + ); + + return array_merge([$mailAccount->getEmail()], $aliases); + } +} diff --git a/lib/Service/FilterService.php b/lib/Service/FilterService.php new file mode 100644 index 000000000..1ddb5b27a --- /dev/null +++ b/lib/Service/FilterService.php @@ -0,0 +1,88 @@ +sieveService->getActiveScript($account->getUserId(), $account->getId()); + return $this->filterParser->parseFilterState($script->getScript()); + } + + /** + * @throws CouldNotConnectException + * @throws JsonException + * @throws ClientException + * @throws OutOfOfficeParserException + * @throws ManageSieveException + * @throws FilterParserException + */ + public function update(MailAccount $account, array $filters): void { + $script = $this->sieveService->getActiveScript($account->getUserId(), $account->getId()); + + $oooResult = $this->outOfOfficeParser->parseOutOfOfficeState($script->getScript()); + $filterResult = $this->filterParser->parseFilterState($oooResult->getUntouchedSieveScript()); + + $newScript = $this->filterBuilder->buildSieveScript( + $filters, + $filterResult->getUntouchedSieveScript() + ); + + $oooState = $oooResult->getState(); + + if ($oooState instanceof OutOfOfficeState) { + $newScript = $this->outOfOfficeParser->buildSieveScript( + $oooState, + $newScript, + $this->allowedRecipientsService->get($account), + ); + } + + try { + $this->sieveService->updateActiveScript($account->getUserId(), $account->getId(), $newScript); + } catch (ManageSieveException $e) { + $this->logger->error('Failed to save sieve script: ' . $e->getMessage(), [ + 'exception' => $e, + 'script' => $newScript, + ]); + throw $e; + } + } +} diff --git a/lib/Service/MailFilter/FilterBuilder.php b/lib/Service/MailFilter/FilterBuilder.php new file mode 100644 index 000000000..7c8e6cd8d --- /dev/null +++ b/lib/Service/MailFilter/FilterBuilder.php @@ -0,0 +1,165 @@ +sanitizeFlag($action['flag'])) + ); + } + if ($action['type'] === 'keep') { + $actions[] = 'keep;'; + } + if ($action['type'] === 'stop') { + $actions[] = 'stop;'; + } + } + + if (count($tests) > 1) { + $ifTest = sprintf('%s (%s)', $filter['operator'], implode(', ', $tests)); + } else { + $ifTest = $tests[0]; + } + + $actions = array_map( + static fn ($action) => "\t" . $action, + $actions + ); + + $ifBlock = sprintf( + "if %s {\r\n%s\r\n}", + $ifTest, + implode(self::SIEVE_NEWLINE, $actions) + ); + + $commands[] = $ifBlock; + } + + $lines = []; + + $extensions = array_unique($extensions); + if (count($extensions) > 0) { + $lines[] = self::SEPARATOR; + $lines[] = 'require ' . SieveUtils::stringList($extensions) . ';'; + $lines[] = self::SEPARATOR; + } + + /* + * Using implode("\r\n", $lines) may introduce an extra newline if the original script already ends with one. + * There may be a cleaner solution, but I couldn't find one that works seamlessly with Filter and Autoresponder. + * Feel free to give it a try! + */ + if (str_ends_with($untouchedScript, self::SIEVE_NEWLINE . self::SIEVE_NEWLINE)) { + $untouchedScript = substr($untouchedScript, 0, -2); + } + $lines[] = $untouchedScript; + + if (count($filters) > 0) { + $lines[] = self::SEPARATOR; + $lines[] = self::DATA_MARKER . json_encode($this->sanitizeDefinition($filters), JSON_THROW_ON_ERROR); + array_push($lines, ...$commands); + $lines[] = self::SEPARATOR; + } + + return implode(self::SIEVE_NEWLINE, $lines); + } + + private function sanitizeFlag(string $flag): string { + try { + return $this->imapFlag->create($flag); + } catch (ImapFlagEncodingException) { + return 'placeholder_for_invalid_label'; + } + } + + private function sanitizeDefinition(array $filters): array { + return array_map(static function ($filter) { + unset($filter['accountId'], $filter['id']); + $filter['tests'] = array_map(static function ($test) { + unset($test['id']); + return $test; + }, $filter['tests']); + $filter['actions'] = array_map(static function ($action) { + unset($action['id']); + return $action; + }, $filter['actions']); + $filter['priority'] = (int)$filter['priority']; + return $filter; + }, $filters); + } +} diff --git a/lib/Service/MailFilter/FilterParser.php b/lib/Service/MailFilter/FilterParser.php new file mode 100644 index 000000000..2f52d708d --- /dev/null +++ b/lib/Service/MailFilter/FilterParser.php @@ -0,0 +1,67 @@ +filters; + } + + public function getSieveScript(): string { + return $this->sieveScript; + } + + public function getUntouchedSieveScript(): string { + return $this->untouchedSieveScript; + } + + #[ReturnTypeWillChange] + public function jsonSerialize() { + return [ + 'filters' => $this->filters, + 'script' => $this->getSieveScript(), + 'untouchedScript' => $this->getUntouchedSieveScript(), + ]; + } +} diff --git a/lib/Service/MailFilter/FilterState.php b/lib/Service/MailFilter/FilterState.php new file mode 100644 index 000000000..e238da2c5 --- /dev/null +++ b/lib/Service/MailFilter/FilterState.php @@ -0,0 +1,35 @@ +filters; + } + + #[ReturnTypeWillChange] + public function jsonSerialize() { + return $this->filters; + } +} diff --git a/lib/Service/MailManager.php b/lib/Service/MailManager.php index 904f54786..7fd3cf782 100644 --- a/lib/Service/MailManager.php +++ b/lib/Service/MailManager.php @@ -94,7 +94,8 @@ class MailManager implements IMailManager { /** @var ThreadMapper */ private $threadMapper; - public function __construct(IMAPClientFactory $imapClientFactory, + public function __construct( + IMAPClientFactory $imapClientFactory, MailboxMapper $mailboxMapper, MailboxSync $mailboxSync, FolderMapper $folderMapper, @@ -105,7 +106,8 @@ class MailManager implements IMailManager { TagMapper $tagMapper, MessageTagsMapper $messageTagsMapper, ThreadMapper $threadMapper, - private ImapFlag $imapFlag) { + private ImapFlag $imapFlag, + ) { $this->imapClientFactory = $imapClientFactory; $this->mailboxMapper = $mailboxMapper; $this->mailboxSync = $mailboxSync; diff --git a/lib/Service/OutOfOffice/OutOfOfficeParser.php b/lib/Service/OutOfOffice/OutOfOfficeParser.php index 79a532514..28bb2abfd 100644 --- a/lib/Service/OutOfOffice/OutOfOfficeParser.php +++ b/lib/Service/OutOfOffice/OutOfOfficeParser.php @@ -13,6 +13,7 @@ use DateTimeImmutable; use DateTimeZone; use JsonException; use OCA\Mail\Exception\OutOfOfficeParserException; +use OCA\Mail\Sieve\SieveUtils; /** * Parses and builds out-of-office states from/to sieve scripts. @@ -119,7 +120,7 @@ class OutOfOfficeParser { $condition = "currentdate :value \"ge\" \"iso8601\" \"$formattedStart\""; } - $escapedSubject = $this->escapeStringForSieve($state->getSubject()); + $escapedSubject = SieveUtils::escapeString($state->getSubject()); $vacation = [ 'vacation', ':days 4', @@ -134,7 +135,7 @@ class OutOfOfficeParser { $vacation[] = ":addresses [$joinedRecipients]"; } - $escapedMessage = $this->escapeStringForSieve($state->getMessage()); + $escapedMessage = SieveUtils::escapeString($state->getMessage()); $vacation[] = "\"$escapedMessage\""; $vacationCommand = implode(' ', $vacation); @@ -183,9 +184,4 @@ class OutOfOfficeParser { private function formatDateForSieve(DateTimeImmutable $date): string { return $date->setTimezone($this->utc)->format('Y-m-d\TH:i:s\Z'); } - - private function escapeStringForSieve(string $subject): string { - $subject = preg_replace('/\\\\/', '\\\\\\\\', $subject); - return preg_replace('/"/', '\\"', $subject); - } } diff --git a/lib/Service/OutOfOfficeService.php b/lib/Service/OutOfOfficeService.php index d10f32373..6ff7e760a 100644 --- a/lib/Service/OutOfOfficeService.php +++ b/lib/Service/OutOfOfficeService.php @@ -13,7 +13,6 @@ use DateTimeImmutable; use Horde\ManageSieve\Exception as ManageSieveException; use InvalidArgumentException; use JsonException; -use OCA\Mail\Db\Alias; use OCA\Mail\Db\MailAccount; use OCA\Mail\Exception\ClientException; use OCA\Mail\Exception\CouldNotConnectException; @@ -33,8 +32,8 @@ class OutOfOfficeService { private OutOfOfficeParser $outOfOfficeParser, private SieveService $sieveService, private LoggerInterface $logger, - private AliasesService $aliasesService, private ITimeFactory $timeFactory, + private AllowedRecipientsService $allowedRecipientsService, private IAvailabilityCoordinator $availabilityCoordinator, ) { } @@ -63,7 +62,7 @@ class OutOfOfficeService { $newScript = $this->outOfOfficeParser->buildSieveScript( $state, $oldState->getUntouchedSieveScript(), - $this->buildAllowedRecipients($account), + $this->allowedRecipientsService->get($account), ); try { $this->sieveService->updateActiveScript($account->getUserId(), $account->getId(), $newScript); @@ -142,15 +141,4 @@ class OutOfOfficeService { $state->setEnabled(false); $this->update($account, $state); } - - /** - * @return string[] - */ - private function buildAllowedRecipients(MailAccount $mailAccount): array { - $aliases = $this->aliasesService->findAll($mailAccount->getId(), $mailAccount->getUserId()); - $formattedAliases = array_map(static function (Alias $alias) { - return $alias->getAlias(); - }, $aliases); - return array_merge([$mailAccount->getEmail()], $formattedAliases); - } } diff --git a/src/components/AccountSettings.vue b/src/components/AccountSettings.vue index 703ee899a..92f106a71 100644 --- a/src/components/AccountSettings.vue +++ b/src/components/AccountSettings.vue @@ -55,9 +55,16 @@ {{ t('mail', 'Please connect to a sieve server first.') }}

+ +
+ +
+
+ :name="t('mail', 'Sieve script editor')">
+ :name="t('mail', 'Sieve server')">

- {{ t('mail', 'Sieve is a powerful language for writing filters for your mailbox. You can manage the sieve scripts in Mail if your email service supports it.') }} + {{ t('mail', 'Sieve is a powerful language for writing filters for your mailbox. You can manage the sieve scripts in Mail if your email service supports it. Sieve is also required to use Autoresponder and Filters.') }}

diff --git a/src/components/mailFilter/Action.vue b/src/components/mailFilter/Action.vue new file mode 100644 index 000000000..7f5754d78 --- /dev/null +++ b/src/components/mailFilter/Action.vue @@ -0,0 +1,98 @@ + + + + diff --git a/src/components/mailFilter/ActionAddflag.vue b/src/components/mailFilter/ActionAddflag.vue new file mode 100644 index 000000000..961f512cf --- /dev/null +++ b/src/components/mailFilter/ActionAddflag.vue @@ -0,0 +1,45 @@ + + + + diff --git a/src/components/mailFilter/ActionFileinto.vue b/src/components/mailFilter/ActionFileinto.vue new file mode 100644 index 000000000..4ea61da39 --- /dev/null +++ b/src/components/mailFilter/ActionFileinto.vue @@ -0,0 +1,50 @@ + + + diff --git a/src/components/mailFilter/ActionStop.vue b/src/components/mailFilter/ActionStop.vue new file mode 100644 index 000000000..6d7184fd6 --- /dev/null +++ b/src/components/mailFilter/ActionStop.vue @@ -0,0 +1,23 @@ + + + diff --git a/src/components/mailFilter/DeleteModal.vue b/src/components/mailFilter/DeleteModal.vue new file mode 100644 index 000000000..72f6dd98c --- /dev/null +++ b/src/components/mailFilter/DeleteModal.vue @@ -0,0 +1,62 @@ + + + + diff --git a/src/components/mailFilter/MailFilters.vue b/src/components/mailFilter/MailFilters.vue new file mode 100644 index 000000000..8a1a3a662 --- /dev/null +++ b/src/components/mailFilter/MailFilters.vue @@ -0,0 +1,218 @@ + + + + + + diff --git a/src/components/mailFilter/Operator.vue b/src/components/mailFilter/Operator.vue new file mode 100644 index 000000000..16ba5639f --- /dev/null +++ b/src/components/mailFilter/Operator.vue @@ -0,0 +1,48 @@ + + + diff --git a/src/components/mailFilter/Test.vue b/src/components/mailFilter/Test.vue new file mode 100644 index 000000000..2bc4b05e8 --- /dev/null +++ b/src/components/mailFilter/Test.vue @@ -0,0 +1,120 @@ + + + + diff --git a/src/components/mailFilter/UpdateModal.vue b/src/components/mailFilter/UpdateModal.vue new file mode 100644 index 000000000..e14e56a29 --- /dev/null +++ b/src/components/mailFilter/UpdateModal.vue @@ -0,0 +1,213 @@ + + + + diff --git a/src/service/MailFilterService.js b/src/service/MailFilterService.js new file mode 100644 index 000000000..4c500295a --- /dev/null +++ b/src/service/MailFilterService.js @@ -0,0 +1,22 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { generateUrl } from '@nextcloud/router' +import axios from '@nextcloud/axios' + +export async function getFilters(accountId) { + const url = generateUrl('/apps/mail/api/filter/{accountId}', { accountId }) + + const { data } = await axios.get(url) + + return data +} + +export async function updateFilters(accountId, filters) { + const url = generateUrl('/apps/mail/api/filter/{accountId}', { accountId }) + + const { data } = await axios.put(url, { filters }) + + return data +} diff --git a/src/store/mailFilterStore.js b/src/store/mailFilterStore.js new file mode 100644 index 000000000..da6d1d6fc --- /dev/null +++ b/src/store/mailFilterStore.js @@ -0,0 +1,52 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { defineStore } from 'pinia' +import * as MailFilterService from '../service/MailFilterService.js' +import { randomId } from '../util/randomId.js' + +export default defineStore('mailFilter', { + state: () => { + return { + filters: [], + } + }, + actions: { + async fetch(accountId) { + await this.$patch(async (state) => { + const filters = await MailFilterService.getFilters(accountId) + state.filters = filters.map((filter) => { + filter.id = randomId() + filter.tests.map((test) => { + test.id = randomId() + return test + }) + filter.actions.map((action) => { + action.id = randomId() + return action + }) + return filter + }) + }) + }, + async update(accountId) { + let filters = structuredClone(this.filters) + filters = filters.map((filter) => { + delete filter.id + filter.tests.map((test) => { + delete test.id + return test + }) + filter.actions.map((action) => { + delete action.id + return action + }) + return filter + }) + + await MailFilterService.updateFilters(accountId, filters) + }, + }, +}) diff --git a/tests/Unit/Service/AllowedRecipientsServiceTest.php b/tests/Unit/Service/AllowedRecipientsServiceTest.php new file mode 100644 index 000000000..ad064d52d --- /dev/null +++ b/tests/Unit/Service/AllowedRecipientsServiceTest.php @@ -0,0 +1,54 @@ +aliasesService = $this->createMock(AliasesService::class); + $this->allowedRecipientsService = new AllowedRecipientsService($this->aliasesService); + } + + public function testGet(): void { + $alias1 = new Alias(); + $alias1->setAlias('alias1@example.org'); + + $alias2 = new Alias(); + $alias2->setAlias('alias2@example.org'); + + $this->aliasesService->expects(self::once()) + ->method('findAll') + ->willReturn([$alias1, $alias2]); + + $mailAccount = new MailAccount(); + $mailAccount->setId(1); + $mailAccount->setUserId('user'); + $mailAccount->setEmail('user@example.org'); + + $recipients = $this->allowedRecipientsService->get($mailAccount); + + $this->assertCount(3, $recipients); + $this->assertEquals('user@example.org', $recipients[0]); + $this->assertEquals('alias1@example.org', $recipients[1]); + $this->assertEquals('alias2@example.org', $recipients[2]); + } +} diff --git a/tests/Unit/Service/FilterServiceTest.php b/tests/Unit/Service/FilterServiceTest.php new file mode 100644 index 000000000..783666ff0 --- /dev/null +++ b/tests/Unit/Service/FilterServiceTest.php @@ -0,0 +1,239 @@ +testFolder = __DIR__ . '/../../data/mail-filter/'; + } + + protected function setUp(): void { + parent::setUp(); + + $this->allowedRecipientsService = $this->createMock(AllowedRecipientsService::class); + $this->outOfOfficeParser = new OutOfOfficeParser(); + $this->filterParser = new FilterParser(); + $this->filterBuilder = new FilterBuilder(new ImapFlag()); + $this->sieveService = $this->createMock(SieveService::class); + $this->logger = new TestLogger(); + + $this->filterService = new FilterService( + $this->allowedRecipientsService, + $this->outOfOfficeParser, + $this->filterParser, + $this->filterBuilder, + $this->sieveService, + $this->logger + ); + } + + public function testParse1(): void { + $mailAccount = new MailAccount(); + $mailAccount->setId(1); + $mailAccount->setUserId('alice'); + $mailAccount->setEmail('alice@mail.internal'); + + $script = new NamedSieveScript( + 'test.sieve', + file_get_contents($this->testFolder . 'parser1.sieve'), + ); + + $this->sieveService->method('getActiveScript') + ->willReturn($script); + + $result = $this->filterService->parse($mailAccount); + + // Not checking the filters because FilterParserTest.testParser1 uses the same script. + $this->assertCount(1, $result->getFilters()); + + $this->assertEquals("# Hello, this is a test\r\n", $result->getUntouchedSieveScript()); + } + + public function testParse2(): void { + $mailAccount = new MailAccount(); + $mailAccount->setId(1); + $mailAccount->setUserId('alice'); + $mailAccount->setEmail('alice@mail.internal'); + + $script = new NamedSieveScript( + 'test.sieve', + file_get_contents($this->testFolder . 'parser2.sieve'), + ); + + $this->sieveService->method('getActiveScript') + ->willReturn($script); + + $result = $this->filterService->parse($mailAccount); + + // Not checking the filters because FilterParserTest.testParser2 uses the same script. + $this->assertCount(1, $result->getFilters()); + + $this->assertEquals("# Hello, this is a test\r\n", $result->getUntouchedSieveScript()); + } + + public function testParse3(): void { + $mailAccount = new MailAccount(); + $mailAccount->setId(1); + $mailAccount->setUserId('alice'); + $mailAccount->setEmail('alice@mail.internal'); + + $script = new NamedSieveScript( + 'test.sieve', + file_get_contents($this->testFolder . 'parser3.sieve'), + ); + + $untouchedScript = file_get_contents($this->testFolder . 'parser3_untouched.sieve'); + + $this->sieveService->method('getActiveScript') + ->willReturn($script); + + $result = $this->filterService->parse($mailAccount); + + $this->assertCount(0, $result->getFilters()); + + $this->assertEquals($untouchedScript, $result->getUntouchedSieveScript()); + } + + public function testParse4(): void { + $mailAccount = new MailAccount(); + $mailAccount->setId(1); + $mailAccount->setUserId('alice'); + $mailAccount->setEmail('alice@mail.internal'); + + $script = new NamedSieveScript( + 'test.sieve', + file_get_contents($this->testFolder . 'parser4.sieve'), + ); + + $untouchedScript = file_get_contents($this->testFolder . 'parser4_untouched.sieve'); + + $this->sieveService->method('getActiveScript') + ->willReturn($script); + + $result = $this->filterService->parse($mailAccount); + + $filters = $result->getFilters(); + + $this->assertCount(1, $filters); + $this->assertSame('Marketing', $filters[0]['name']); + $this->assertTrue($filters[0]['enable']); + $this->assertSame('allof', $filters[0]['operator']); + $this->assertSame(10, $filters[0]['priority']); + + $this->assertCount(1, $filters[0]['tests']); + $this->assertSame('from', $filters[0]['tests'][0]['field']); + $this->assertSame('is', $filters[0]['tests'][0]['operator']); + $this->assertEquals(['marketing@mail.internal'], $filters[0]['tests'][0]['values']); + + $this->assertCount(1, $filters[0]['actions']); + $this->assertSame('fileinto', $filters[0]['actions'][0]['type']); + $this->assertSame('Marketing', $filters[0]['actions'][0]['mailbox']); + + $this->assertEquals($untouchedScript, $result->getUntouchedSieveScript()); + } + + /** + * Test case: Add a filter set to a sieve script with autoresponder. + */ + public function testUpdate1(): void { + $mailAccount = new MailAccount(); + $mailAccount->setId(1); + $mailAccount->setUserId('alice'); + $mailAccount->setEmail('alice@mail.internal'); + + $script = new NamedSieveScript( + 'test.sieve', + file_get_contents($this->testFolder . 'service1.sieve'), + ); + + $filters = json_decode( + file_get_contents($this->testFolder . 'service1.json'), + true + ); + + $this->sieveService->method('getActiveScript') + ->willReturn($script); + + $this->sieveService->method('updateActiveScript') + ->willReturnCallback(function (string $userId, int $accountId, string $script) { + // the .sieve files have \r\n line endings + $script .= "\r\n"; + + $this->assertStringEqualsFile($this->testFolder . 'service1_new.sieve', $script); + }); + + $this->allowedRecipientsService->method('get') + ->willReturn(['alice@mail.internal']); + + $this->filterService->update($mailAccount, $filters); + } + + /** + * Test case: Delete a filter rule from a set. + */ + public function testUpdate2(): void { + $mailAccount = new MailAccount(); + $mailAccount->setId(1); + $mailAccount->setUserId('alice'); + $mailAccount->setEmail('alice@mail.internal'); + + $script = new NamedSieveScript( + 'test.sieve', + file_get_contents($this->testFolder . 'service2.sieve'), + ); + + $filters = json_decode( + file_get_contents($this->testFolder . 'service2.json'), + true + ); + + $this->sieveService->method('getActiveScript') + ->willReturn($script); + + $this->sieveService->method('updateActiveScript') + ->willReturnCallback(function (string $userId, int $accountId, string $script) { + // the .sieve files have \r\n line endings + $script .= "\r\n"; + + $this->assertStringEqualsFile($this->testFolder . 'service2_new.sieve', $script); + }); + + $this->allowedRecipientsService->method('get') + ->willReturn(['alice@mail.internal']); + + $this->filterService->update($mailAccount, $filters); + } +} diff --git a/tests/Unit/Service/MailFilter/FilterBuilderTest.php b/tests/Unit/Service/MailFilter/FilterBuilderTest.php new file mode 100644 index 000000000..c9368be7a --- /dev/null +++ b/tests/Unit/Service/MailFilter/FilterBuilderTest.php @@ -0,0 +1,65 @@ +testFolder = __DIR__ . '/../../../data/mail-filter/'; + } + + public function setUp(): void { + parent::setUp(); + $this->builder = new FilterBuilder(new ImapFlag()); + } + + /** + * @dataProvider dataBuild + */ + public function testBuild(string $testName): void { + $untouchedScript = '# Hello, this is a test'; + + $filters = json_decode( + file_get_contents($this->testFolder . $testName . '.json'), + true, + 512, + JSON_THROW_ON_ERROR + ); + + $script = $this->builder->buildSieveScript($filters, $untouchedScript); + + // the .sieve files have \r\n line endings + $script .= "\r\n"; + + $this->assertStringEqualsFile( + $this->testFolder . $testName . '.sieve', + $script + ); + } + + public function dataBuild(): array { + $files = glob($this->testFolder . 'builder*.json'); + $tests = []; + + foreach ($files as $file) { + $filename = pathinfo($file, PATHINFO_FILENAME); + $tests[$filename] = [$filename]; + } + + return $tests; + } +} diff --git a/tests/Unit/Service/MailFilter/FilterParserTest.php b/tests/Unit/Service/MailFilter/FilterParserTest.php new file mode 100644 index 000000000..83e775df6 --- /dev/null +++ b/tests/Unit/Service/MailFilter/FilterParserTest.php @@ -0,0 +1,77 @@ +testFolder = __DIR__ . '/../../../data/mail-filter/'; + } + + protected function setUp(): void { + parent::setUp(); + + $this->filterParser = new FilterParser(); + } + + public function testParse1(): void { + $script = file_get_contents($this->testFolder . 'parser1.sieve'); + + $state = $this->filterParser->parseFilterState($script); + $filters = $state->getFilters(); + + $this->assertCount(1, $filters); + $this->assertSame('Test 1', $filters[0]['name']); + $this->assertTrue($filters[0]['enable']); + $this->assertSame('allof', $filters[0]['operator']); + $this->assertSame(10, $filters[0]['priority']); + + $this->assertCount(1, $filters[0]['tests']); + $this->assertSame('from', $filters[0]['tests'][0]['field']); + $this->assertSame('is', $filters[0]['tests'][0]['operator']); + $this->assertEquals(['alice@example.org', 'bob@example.org'], $filters[0]['tests'][0]['values']); + + $this->assertCount(1, $filters[0]['actions']); + $this->assertSame('addflag', $filters[0]['actions'][0]['type']); + $this->assertSame('Alice and Bob', $filters[0]['actions'][0]['flag']); + } + + public function testParse2(): void { + $script = file_get_contents($this->testFolder . 'parser2.sieve'); + + $state = $this->filterParser->parseFilterState($script); + $filters = $state->getFilters(); + + $this->assertCount(1, $filters); + $this->assertSame('Test 2', $filters[0]['name']); + $this->assertTrue($filters[0]['enable']); + $this->assertSame('anyof', $filters[0]['operator']); + $this->assertSame(20, $filters[0]['priority']); + + $this->assertCount(2, $filters[0]['tests']); + $this->assertSame('subject', $filters[0]['tests'][0]['field']); + $this->assertSame('contains', $filters[0]['tests'][0]['operator']); + $this->assertEquals(['Project-A', 'Project-B'], $filters[0]['tests'][0]['values']); + $this->assertSame('from', $filters[0]['tests'][1]['field']); + $this->assertSame('is', $filters[0]['tests'][1]['operator']); + $this->assertEquals(['john@example.org'], $filters[0]['tests'][1]['values']); + + $this->assertCount(1, $filters[0]['actions']); + $this->assertSame('fileinto', $filters[0]['actions'][0]['type']); + $this->assertSame('Test Data', $filters[0]['actions'][0]['mailbox']); + } +} diff --git a/tests/Unit/Service/OutOfOfficeServiceTest.php b/tests/Unit/Service/OutOfOfficeServiceTest.php index 8057bf07f..522b11107 100644 --- a/tests/Unit/Service/OutOfOfficeServiceTest.php +++ b/tests/Unit/Service/OutOfOfficeServiceTest.php @@ -124,11 +124,11 @@ class OutOfOfficeServiceTest extends TestCase { ['email@domain.com'], ) ->willReturn('# new sieve script'); - $aliasesService = $this->serviceMock->getParameter('aliasesService'); - $aliasesService->expects(self::once()) - ->method('findAll') - ->with(1, 'user') - ->willReturn([]); + $allowedRecipientsService = $this->serviceMock->getParameter('allowedRecipientsService'); + $allowedRecipientsService->expects(self::once()) + ->method('get') + ->with($mailAccount) + ->willReturn(['email@domain.com']); $sieveService->expects(self::once()) ->method('updateActiveScript') ->with('user', 1, '# new sieve script'); @@ -203,11 +203,11 @@ class OutOfOfficeServiceTest extends TestCase { ['email@domain.com'], ) ->willReturn('# new sieve script'); - $aliasesService = $this->serviceMock->getParameter('aliasesService'); - $aliasesService->expects(self::once()) - ->method('findAll') - ->with(1, 'user') - ->willReturn([]); + $allowedRecipientsService = $this->serviceMock->getParameter('allowedRecipientsService'); + $allowedRecipientsService->expects(self::once()) + ->method('get') + ->with($mailAccount) + ->willReturn(['email@domain.com']); $sieveService->expects(self::once()) ->method('updateActiveScript') ->with('user', 1, '# new sieve script'); diff --git a/tests/data/mail-filter/builder1.json b/tests/data/mail-filter/builder1.json new file mode 100644 index 000000000..35ac4a670 --- /dev/null +++ b/tests/data/mail-filter/builder1.json @@ -0,0 +1,24 @@ +[ + { + "name": "Test 1", + "enable": true, + "operator": "allof", + "tests": [ + { + "operator": "is", + "values": [ + "alice@example.org", + "bob@example.org" + ], + "field": "from" + } + ], + "actions": [ + { + "type": "addflag", + "flag": "Alice and Bob" + } + ], + "priority": 10 + } +] diff --git a/tests/data/mail-filter/builder1.sieve b/tests/data/mail-filter/builder1.sieve new file mode 100644 index 000000000..7f96bd089 --- /dev/null +++ b/tests/data/mail-filter/builder1.sieve @@ -0,0 +1,11 @@ +### Nextcloud Mail: Filters ### DON'T EDIT ### +require ["imap4flags"]; +### Nextcloud Mail: Filters ### DON'T EDIT ### +# Hello, this is a test +### Nextcloud Mail: Filters ### DON'T EDIT ### +# FILTER: [{"name":"Test 1","enable":true,"operator":"allof","tests":[{"operator":"is","values":["alice@example.org","bob@example.org"],"field":"from"}],"actions":[{"type":"addflag","flag":"Alice and Bob"}],"priority":10}] +# Test 1 +if address :is :all "From" ["alice@example.org", "bob@example.org"] { + addflag "$alice_and_bob"; +} +### Nextcloud Mail: Filters ### DON'T EDIT ### diff --git a/tests/data/mail-filter/builder2.json b/tests/data/mail-filter/builder2.json new file mode 100644 index 000000000..d431502ed --- /dev/null +++ b/tests/data/mail-filter/builder2.json @@ -0,0 +1,31 @@ +[ + { + "name": "Test 2", + "enable": true, + "operator": "anyof", + "tests": [ + { + "operator": "contains", + "values": [ + "Project-A", + "Project-B" + ], + "field": "subject" + }, + { + "operator": "is", + "values": [ + "john@example.org" + ], + "field": "from" + } + ], + "actions": [ + { + "type": "fileinto", + "mailbox": "Test Data" + } + ], + "priority": "20" + } +] diff --git a/tests/data/mail-filter/builder2.sieve b/tests/data/mail-filter/builder2.sieve new file mode 100644 index 000000000..193af1daa --- /dev/null +++ b/tests/data/mail-filter/builder2.sieve @@ -0,0 +1,11 @@ +### Nextcloud Mail: Filters ### DON'T EDIT ### +require ["fileinto"]; +### Nextcloud Mail: Filters ### DON'T EDIT ### +# Hello, this is a test +### Nextcloud Mail: Filters ### DON'T EDIT ### +# FILTER: [{"name":"Test 2","enable":true,"operator":"anyof","tests":[{"operator":"contains","values":["Project-A","Project-B"],"field":"subject"},{"operator":"is","values":["john@example.org"],"field":"from"}],"actions":[{"type":"fileinto","mailbox":"Test Data"}],"priority":20}] +# Test 2 +if anyof (header :contains "Subject" ["Project-A", "Project-B"], address :is :all "From" ["john@example.org"]) { + fileinto "Test Data"; +} +### Nextcloud Mail: Filters ### DON'T EDIT ### diff --git a/tests/data/mail-filter/builder3.json b/tests/data/mail-filter/builder3.json new file mode 100644 index 000000000..dd7b4583d --- /dev/null +++ b/tests/data/mail-filter/builder3.json @@ -0,0 +1,55 @@ +[ + { + "name": "Test 3.1", + "enable": true, + "operator": "anyof", + "tests": [ + { + "operator": "contains", + "values": [ + "Project-A", + "Project-B" + ], + "field": "subject" + }, + { + "operator": "is", + "values": [ + "john@example.org" + ], + "field": "from" + } + ], + "actions": [ + { + "type": "fileinto", + "mailbox": "Test Data" + }, + { + "type": "stop" + } + ], + "priority": "20" + }, + { + "name": "Test 3.2", + "enable": true, + "operator": "allof", + "tests": [ + { + "operator": "contains", + "values": [ + "@example.org" + ], + "field": "to" + } + ], + "actions": [ + { + "type": "addflag", + "flag": "Test A" + } + ], + "priority": 30 + } +] diff --git a/tests/data/mail-filter/builder3.sieve b/tests/data/mail-filter/builder3.sieve new file mode 100644 index 000000000..888d3aa82 --- /dev/null +++ b/tests/data/mail-filter/builder3.sieve @@ -0,0 +1,16 @@ +### Nextcloud Mail: Filters ### DON'T EDIT ### +require ["fileinto", "imap4flags"]; +### Nextcloud Mail: Filters ### DON'T EDIT ### +# Hello, this is a test +### Nextcloud Mail: Filters ### DON'T EDIT ### +# FILTER: [{"name":"Test 3.1","enable":true,"operator":"anyof","tests":[{"operator":"contains","values":["Project-A","Project-B"],"field":"subject"},{"operator":"is","values":["john@example.org"],"field":"from"}],"actions":[{"type":"fileinto","mailbox":"Test Data"},{"type":"stop"}],"priority":20},{"name":"Test 3.2","enable":true,"operator":"allof","tests":[{"operator":"contains","values":["@example.org"],"field":"to"}],"actions":[{"type":"addflag","flag":"Test A"}],"priority":30}] +# Test 3.1 +if anyof (header :contains "Subject" ["Project-A", "Project-B"], address :is :all "From" ["john@example.org"]) { + fileinto "Test Data"; + stop; +} +# Test 3.2 +if address :contains :all "To" ["@example.org"] { + addflag "$test_a"; +} +### Nextcloud Mail: Filters ### DON'T EDIT ### diff --git a/tests/data/mail-filter/builder4.json b/tests/data/mail-filter/builder4.json new file mode 100644 index 000000000..c02d4c469 --- /dev/null +++ b/tests/data/mail-filter/builder4.json @@ -0,0 +1,15 @@ +[ + { + "actions": [ + { + "flag": "Flag 123", + "type": "addflag" + } + ], + "enable": true, + "name": "Test 4", + "operator": "allof", + "priority": 60, + "tests": [] + } +] diff --git a/tests/data/mail-filter/builder4.sieve b/tests/data/mail-filter/builder4.sieve new file mode 100644 index 000000000..2877d7de0 --- /dev/null +++ b/tests/data/mail-filter/builder4.sieve @@ -0,0 +1,6 @@ +# Hello, this is a test +### Nextcloud Mail: Filters ### DON'T EDIT ### +# FILTER: [{"actions":[{"flag":"Flag 123","type":"addflag"}],"enable":true,"name":"Test 4","operator":"allof","priority":60,"tests":[]}] +# Test 4 +# No valid tests found +### Nextcloud Mail: Filters ### DON'T EDIT ### diff --git a/tests/data/mail-filter/builder5.json b/tests/data/mail-filter/builder5.json new file mode 100644 index 000000000..9cc8c89f4 --- /dev/null +++ b/tests/data/mail-filter/builder5.json @@ -0,0 +1,27 @@ +[ + { + "actions": [ + { + "flag": "Report", + "type": "addflag" + }, + { + "flag": "To read", + "type": "addflag" + } + ], + "enable": true, + "name": "Test 5", + "operator": "allof", + "priority": 10, + "tests": [ + { + "field": "subject", + "operator": "matches", + "values": [ + "work*report" + ] + } + ] + } +] diff --git a/tests/data/mail-filter/builder5.sieve b/tests/data/mail-filter/builder5.sieve new file mode 100644 index 000000000..945164f90 --- /dev/null +++ b/tests/data/mail-filter/builder5.sieve @@ -0,0 +1,12 @@ +### Nextcloud Mail: Filters ### DON'T EDIT ### +require ["imap4flags"]; +### Nextcloud Mail: Filters ### DON'T EDIT ### +# Hello, this is a test +### Nextcloud Mail: Filters ### DON'T EDIT ### +# FILTER: [{"actions":[{"flag":"Report","type":"addflag"},{"flag":"To read","type":"addflag"}],"enable":true,"name":"Test 5","operator":"allof","priority":10,"tests":[{"field":"subject","operator":"matches","values":["work*report"]}]}] +# Test 5 +if header :matches "Subject" ["work*report"] { + addflag "$report"; + addflag "$to_read"; +} +### Nextcloud Mail: Filters ### DON'T EDIT ### diff --git a/tests/data/mail-filter/builder6.json b/tests/data/mail-filter/builder6.json new file mode 100644 index 000000000..82c8131c1 --- /dev/null +++ b/tests/data/mail-filter/builder6.json @@ -0,0 +1,36 @@ +[ + { + "actions": [ + { + "mailbox": "Test Data", + "type": "fileinto" + }, + { + "flag": "Projects\\Reporting", + "type": "addflag" + } + ], + "enable": true, + "name": "Test 6", + "operator": "anyof", + "priority": 10, + "tests": [ + { + "field": "subject", + "operator": "is", + "values": [ + "\"Project-A\"", + "Project\\A" + ] + }, + { + "field": "subject", + "operator": "is", + "values": [ + "\"Project-B\"", + "Project\\B" + ] + } + ] + } +] diff --git a/tests/data/mail-filter/builder6.sieve b/tests/data/mail-filter/builder6.sieve new file mode 100644 index 000000000..8b6db962a --- /dev/null +++ b/tests/data/mail-filter/builder6.sieve @@ -0,0 +1,12 @@ +### Nextcloud Mail: Filters ### DON'T EDIT ### +require ["fileinto", "imap4flags"]; +### Nextcloud Mail: Filters ### DON'T EDIT ### +# Hello, this is a test +### Nextcloud Mail: Filters ### DON'T EDIT ### +# FILTER: [{"actions":[{"mailbox":"Test Data","type":"fileinto"},{"flag":"Projects\\Reporting","type":"addflag"}],"enable":true,"name":"Test 6","operator":"anyof","priority":10,"tests":[{"field":"subject","operator":"is","values":["\"Project-A\"","Project\\A"]},{"field":"subject","operator":"is","values":["\"Project-B\"","Project\\B"]}]}] +# Test 6 +if anyof (header :is "Subject" ["\"Project-A\"", "Project\\A"], header :is "Subject" ["\"Project-B\"", "Project\\B"]) { + fileinto "Test Data"; + addflag "$projects\\reporting"; +} +### Nextcloud Mail: Filters ### DON'T EDIT ### diff --git a/tests/data/mail-filter/builder7.json b/tests/data/mail-filter/builder7.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/tests/data/mail-filter/builder7.json @@ -0,0 +1 @@ +[] diff --git a/tests/data/mail-filter/builder7.sieve b/tests/data/mail-filter/builder7.sieve new file mode 100644 index 000000000..6c2dbd662 --- /dev/null +++ b/tests/data/mail-filter/builder7.sieve @@ -0,0 +1 @@ +# Hello, this is a test diff --git a/tests/data/mail-filter/parser1.sieve b/tests/data/mail-filter/parser1.sieve new file mode 100644 index 000000000..d20750752 --- /dev/null +++ b/tests/data/mail-filter/parser1.sieve @@ -0,0 +1,11 @@ +### Nextcloud Mail: Filters ### DON'T EDIT ### +require ["imap4flags"]; +### Nextcloud Mail: Filters ### DON'T EDIT ### +# Hello, this is a test +### Nextcloud Mail: Filters ### DON'T EDIT ### +# FILTER: [{"name":"Test 1","enable":true,"operator":"allof","tests":[{"operator":"is","values":["alice@example.org","bob@example.org"],"field":"from"}],"actions":[{"type":"addflag","flag":"Alice and Bob"}],"priority":10}] +# Test 1 +if address :is :all "From" ["alice@example.org", "bob@example.org"] { +addflag "$alice_and_bob"; +} +### Nextcloud Mail: Filters ### DON'T EDIT ### diff --git a/tests/data/mail-filter/parser2.sieve b/tests/data/mail-filter/parser2.sieve new file mode 100644 index 000000000..10e91a254 --- /dev/null +++ b/tests/data/mail-filter/parser2.sieve @@ -0,0 +1,11 @@ +### Nextcloud Mail: Filters ### DON'T EDIT ### +require ["fileinto"]; +### Nextcloud Mail: Filters ### DON'T EDIT ### +# Hello, this is a test +### Nextcloud Mail: Filters ### DON'T EDIT ### +# FILTER: [{"name":"Test 2","enable":true,"operator":"anyof","tests":[{"operator":"contains","values":["Project-A","Project-B"],"field":"subject"},{"operator":"is","values":["john@example.org"],"field":"from"}],"actions":[{"type":"fileinto","flag":"","mailbox":"Test Data"}],"priority":20}] +# Test 2 +if anyof (header :contains "Subject" ["Project-A", "Project-B"], address :is :all "From" ["john@example.org"]) { +fileinto "Test Data"; +} +### Nextcloud Mail: Filters ### DON'T EDIT ### diff --git a/tests/data/mail-filter/parser3.sieve b/tests/data/mail-filter/parser3.sieve new file mode 100644 index 000000000..2d4aa8cfc --- /dev/null +++ b/tests/data/mail-filter/parser3.sieve @@ -0,0 +1,21 @@ +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### +require "date"; +require "relational"; +require "vacation"; +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### + +require "fileinto"; + +if allof( + address "From" "noreply@help.nextcloud.org", + header :contains "Subject" "[Nextcloud community]" +){ + fileinto "Community"; +} + +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### +# DATA: {"version":1,"enabled":true,"start":"2022-09-02T00:00:00+01:00","end":"2022-09-08T23:59:00+01:00","subject":"On vacation","message":"I'm on vacation."} +if allof(currentdate :value "ge" "iso8601" "2022-09-01T23:00:00Z", currentdate :value "le" "iso8601" "2022-09-08T22:59:00Z") { + vacation :days 4 :subject "On vacation" :addresses ["Test Test ", "Test Alias "] "I'm on vacation."; +} +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### diff --git a/tests/data/mail-filter/parser3_untouched.sieve b/tests/data/mail-filter/parser3_untouched.sieve new file mode 100644 index 000000000..2d4aa8cfc --- /dev/null +++ b/tests/data/mail-filter/parser3_untouched.sieve @@ -0,0 +1,21 @@ +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### +require "date"; +require "relational"; +require "vacation"; +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### + +require "fileinto"; + +if allof( + address "From" "noreply@help.nextcloud.org", + header :contains "Subject" "[Nextcloud community]" +){ + fileinto "Community"; +} + +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### +# DATA: {"version":1,"enabled":true,"start":"2022-09-02T00:00:00+01:00","end":"2022-09-08T23:59:00+01:00","subject":"On vacation","message":"I'm on vacation."} +if allof(currentdate :value "ge" "iso8601" "2022-09-01T23:00:00Z", currentdate :value "le" "iso8601" "2022-09-08T22:59:00Z") { + vacation :days 4 :subject "On vacation" :addresses ["Test Test ", "Test Alias "] "I'm on vacation."; +} +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### diff --git a/tests/data/mail-filter/parser4.sieve b/tests/data/mail-filter/parser4.sieve new file mode 100644 index 000000000..9a55e7cdd --- /dev/null +++ b/tests/data/mail-filter/parser4.sieve @@ -0,0 +1,31 @@ +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### +require "date"; +require "relational"; +require "vacation"; +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### +### Nextcloud Mail: Filters ### DON'T EDIT ### +require ["fileinto"]; +### Nextcloud Mail: Filters ### DON'T EDIT ### + +require "fileinto"; + +if allof( + address "From" "noreply@help.nextcloud.org", + header :contains "Subject" "[Nextcloud community]" +){ + fileinto "Community"; +} + +### Nextcloud Mail: Filters ### DON'T EDIT ### +# FILTER: [{"name":"Marketing","enable":true,"operator":"allof","tests":[{"operator":"is","values":["marketing@mail.internal"],"field":"from"}],"actions":[{"type":"fileinto","mailbox":"Marketing"}],"priority":10}] +# Marketing +if address :is :all "From" ["marketing@mail.internal"] { + fileinto "Marketing"; +} +### Nextcloud Mail: Filters ### DON'T EDIT ### +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### +# DATA: {"version":1,"enabled":true,"start":"2024-10-08T22:00:00+00:00","subject":"Thanks for your message!","message":"I'm not here, please try again later.\u00a0"} +if currentdate :value "ge" "iso8601" "2024-10-08T22:00:00Z" { + vacation :days 4 :subject "Thanks for your message!" :addresses ["alice@mail.internal"] "I'm not here, please try again later. "; +} +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### diff --git a/tests/data/mail-filter/parser4_untouched.sieve b/tests/data/mail-filter/parser4_untouched.sieve new file mode 100644 index 000000000..7bb0f893c --- /dev/null +++ b/tests/data/mail-filter/parser4_untouched.sieve @@ -0,0 +1,21 @@ +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### +require "date"; +require "relational"; +require "vacation"; +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### + +require "fileinto"; + +if allof( + address "From" "noreply@help.nextcloud.org", + header :contains "Subject" "[Nextcloud community]" +){ + fileinto "Community"; +} + +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### +# DATA: {"version":1,"enabled":true,"start":"2024-10-08T22:00:00+00:00","subject":"Thanks for your message!","message":"I'm not here, please try again later.\u00a0"} +if currentdate :value "ge" "iso8601" "2024-10-08T22:00:00Z" { + vacation :days 4 :subject "Thanks for your message!" :addresses ["alice@mail.internal"] "I'm not here, please try again later. "; +} +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### diff --git a/tests/data/mail-filter/service1.json b/tests/data/mail-filter/service1.json new file mode 100644 index 000000000..fef65236f --- /dev/null +++ b/tests/data/mail-filter/service1.json @@ -0,0 +1,23 @@ +[ + { + "name": "Marketing", + "enable": true, + "operator": "allof", + "tests": [ + { + "operator": "is", + "values": [ + "marketing@mail.internal" + ], + "field": "from" + } + ], + "actions": [ + { + "type": "fileinto", + "mailbox": "Marketing" + } + ], + "priority": 10 + } +] diff --git a/tests/data/mail-filter/service1.sieve b/tests/data/mail-filter/service1.sieve new file mode 100644 index 000000000..7bb0f893c --- /dev/null +++ b/tests/data/mail-filter/service1.sieve @@ -0,0 +1,21 @@ +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### +require "date"; +require "relational"; +require "vacation"; +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### + +require "fileinto"; + +if allof( + address "From" "noreply@help.nextcloud.org", + header :contains "Subject" "[Nextcloud community]" +){ + fileinto "Community"; +} + +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### +# DATA: {"version":1,"enabled":true,"start":"2024-10-08T22:00:00+00:00","subject":"Thanks for your message!","message":"I'm not here, please try again later.\u00a0"} +if currentdate :value "ge" "iso8601" "2024-10-08T22:00:00Z" { + vacation :days 4 :subject "Thanks for your message!" :addresses ["alice@mail.internal"] "I'm not here, please try again later. "; +} +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### diff --git a/tests/data/mail-filter/service1_new.sieve b/tests/data/mail-filter/service1_new.sieve new file mode 100644 index 000000000..9a55e7cdd --- /dev/null +++ b/tests/data/mail-filter/service1_new.sieve @@ -0,0 +1,31 @@ +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### +require "date"; +require "relational"; +require "vacation"; +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### +### Nextcloud Mail: Filters ### DON'T EDIT ### +require ["fileinto"]; +### Nextcloud Mail: Filters ### DON'T EDIT ### + +require "fileinto"; + +if allof( + address "From" "noreply@help.nextcloud.org", + header :contains "Subject" "[Nextcloud community]" +){ + fileinto "Community"; +} + +### Nextcloud Mail: Filters ### DON'T EDIT ### +# FILTER: [{"name":"Marketing","enable":true,"operator":"allof","tests":[{"operator":"is","values":["marketing@mail.internal"],"field":"from"}],"actions":[{"type":"fileinto","mailbox":"Marketing"}],"priority":10}] +# Marketing +if address :is :all "From" ["marketing@mail.internal"] { + fileinto "Marketing"; +} +### Nextcloud Mail: Filters ### DON'T EDIT ### +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### +# DATA: {"version":1,"enabled":true,"start":"2024-10-08T22:00:00+00:00","subject":"Thanks for your message!","message":"I'm not here, please try again later.\u00a0"} +if currentdate :value "ge" "iso8601" "2024-10-08T22:00:00Z" { + vacation :days 4 :subject "Thanks for your message!" :addresses ["alice@mail.internal"] "I'm not here, please try again later. "; +} +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### diff --git a/tests/data/mail-filter/service2.json b/tests/data/mail-filter/service2.json new file mode 100644 index 000000000..6783e49cf --- /dev/null +++ b/tests/data/mail-filter/service2.json @@ -0,0 +1,23 @@ +[ + { + "name": "Add flag for emails with subject Hello", + "enable": true, + "operator": "allof", + "tests": [ + { + "operator": "contains", + "values": [ + "Hello" + ], + "field": "subject" + } + ], + "actions": [ + { + "type": "addflag", + "flag": "Test 123" + } + ], + "priority": 10 + } +] diff --git a/tests/data/mail-filter/service2.sieve b/tests/data/mail-filter/service2.sieve new file mode 100644 index 000000000..d605d1e4e --- /dev/null +++ b/tests/data/mail-filter/service2.sieve @@ -0,0 +1,35 @@ +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### +require "date"; +require "relational"; +require "vacation"; +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### +### Nextcloud Mail: Filters ### DON'T EDIT ### +require ["imap4flags"]; +### Nextcloud Mail: Filters ### DON'T EDIT ### + +require "fileinto"; + +if allof( + address "From" "noreply@help.nextcloud.org", + header :contains "Subject" "[Nextcloud community]" +){ + fileinto "Community"; +} + +### Nextcloud Mail: Filters ### DON'T EDIT ### +# FILTER: [{"name":"Add flag for emails with subject Hello","enable":true,"operator":"allof","tests":[{"operator":"contains","values":["Hello"],"field":"subject"}],"actions":[{"type":"addflag","flag":"Test 123"}],"priority":10},{"name":"Add flag for emails with subject World","enable":true,"operator":"allof","tests":[{"operator":"contains","values":["World"],"field":"subject"}],"actions":[{"type":"addflag","flag":"Test 456"}],"priority":20}] +# Add flag for emails with subject Hello +if header :contains "Subject" ["Hello"] { + addflag "$test_123"; +} +# Add flag for emails with subject World +if header :contains "Subject" ["World"] { + addflag "$test_456"; +} +### Nextcloud Mail: Filters ### DON'T EDIT ### +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### +# DATA: {"version":1,"enabled":true,"start":"2024-10-08T22:00:00+00:00","subject":"Thanks for your message!","message":"I'm not here, please try again later.\u00a0"} +if currentdate :value "ge" "iso8601" "2024-10-08T22:00:00Z" { + vacation :days 4 :subject "Thanks for your message!" :addresses ["alice@mail.internal"] "I'm not here, please try again later. "; +} +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### diff --git a/tests/data/mail-filter/service2_new.sieve b/tests/data/mail-filter/service2_new.sieve new file mode 100644 index 000000000..3b9e21eee --- /dev/null +++ b/tests/data/mail-filter/service2_new.sieve @@ -0,0 +1,31 @@ +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### +require "date"; +require "relational"; +require "vacation"; +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### +### Nextcloud Mail: Filters ### DON'T EDIT ### +require ["imap4flags"]; +### Nextcloud Mail: Filters ### DON'T EDIT ### + +require "fileinto"; + +if allof( + address "From" "noreply@help.nextcloud.org", + header :contains "Subject" "[Nextcloud community]" +){ + fileinto "Community"; +} + +### Nextcloud Mail: Filters ### DON'T EDIT ### +# FILTER: [{"name":"Add flag for emails with subject Hello","enable":true,"operator":"allof","tests":[{"operator":"contains","values":["Hello"],"field":"subject"}],"actions":[{"type":"addflag","flag":"Test 123"}],"priority":10}] +# Add flag for emails with subject Hello +if header :contains "Subject" ["Hello"] { + addflag "$test_123"; +} +### Nextcloud Mail: Filters ### DON'T EDIT ### +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ### +# DATA: {"version":1,"enabled":true,"start":"2024-10-08T22:00:00+00:00","subject":"Thanks for your message!","message":"I'm not here, please try again later.\u00a0"} +if currentdate :value "ge" "iso8601" "2024-10-08T22:00:00Z" { + vacation :days 4 :subject "Thanks for your message!" :addresses ["alice@mail.internal"] "I'm not here, please try again later. "; +} +### Nextcloud Mail: Vacation Responder ### DON'T EDIT ###