feat: mail filters
Co-authored-by: Hamza Mahjoubi <hamzamahjoubi221@gmail.com> Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
This commit is contained in:
Родитель
9486987428
Коммит
d74c401e66
|
@ -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"
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Mail\Controller;
|
||||
|
||||
use OCA\Mail\AppInfo\Application;
|
||||
use OCA\Mail\Service\AccountService;
|
||||
use OCA\Mail\Service\FilterService;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\Attribute\Route;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\AppFramework\OCSController;
|
||||
use OCP\IRequest;
|
||||
|
||||
class FilterController extends OCSController {
|
||||
private string $currentUserId;
|
||||
|
||||
public function __construct(
|
||||
IRequest $request,
|
||||
string $userId,
|
||||
private FilterService $mailFilterService,
|
||||
private AccountService $accountService,
|
||||
) {
|
||||
parent::__construct(Application::APP_ID, $request);
|
||||
$this->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([]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Mail\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
class FilterParserException extends Exception {
|
||||
|
||||
public static function invalidJson(\Throwable $exception): FilterParserException {
|
||||
return new self(
|
||||
'Failed to parse filter state json: ' . $exception->getMessage(),
|
||||
0,
|
||||
$exception,
|
||||
);
|
||||
}
|
||||
|
||||
public static function invalidState(): FilterParserException {
|
||||
return new self(
|
||||
'Reached an invalid state',
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Mail\Service;
|
||||
|
||||
use OCA\Mail\Db\MailAccount;
|
||||
|
||||
class AllowedRecipientsService {
|
||||
|
||||
public function __construct(
|
||||
private AliasesService $aliasesService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of allowed recipients for a given mail account
|
||||
*
|
||||
* @return string[] email addresses
|
||||
*/
|
||||
public function get(MailAccount $mailAccount): array {
|
||||
$aliases = array_map(
|
||||
static fn ($alias) => $alias->getAlias(),
|
||||
$this->aliasesService->findAll($mailAccount->getId(), $mailAccount->getUserId())
|
||||
);
|
||||
|
||||
return array_merge([$mailAccount->getEmail()], $aliases);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Mail\Service;
|
||||
|
||||
use Horde\ManageSieve\Exception as ManageSieveException;
|
||||
use JsonException;
|
||||
use OCA\Mail\Db\MailAccount;
|
||||
use OCA\Mail\Exception\ClientException;
|
||||
use OCA\Mail\Exception\CouldNotConnectException;
|
||||
use OCA\Mail\Exception\FilterParserException;
|
||||
use OCA\Mail\Exception\OutOfOfficeParserException;
|
||||
use OCA\Mail\Service\MailFilter\FilterBuilder;
|
||||
use OCA\Mail\Service\MailFilter\FilterParser;
|
||||
use OCA\Mail\Service\MailFilter\FilterParserResult;
|
||||
use OCA\Mail\Service\OutOfOffice\OutOfOfficeParser;
|
||||
use OCA\Mail\Service\OutOfOffice\OutOfOfficeState;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class FilterService {
|
||||
|
||||
public function __construct(
|
||||
private AllowedRecipientsService $allowedRecipientsService,
|
||||
private OutOfOfficeParser $outOfOfficeParser,
|
||||
private FilterParser $filterParser,
|
||||
private FilterBuilder $filterBuilder,
|
||||
private SieveService $sieveService,
|
||||
private LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ClientException
|
||||
* @throws ManageSieveException
|
||||
* @throws CouldNotConnectException
|
||||
* @throws FilterParserException
|
||||
*/
|
||||
public function parse(MailAccount $account): FilterParserResult {
|
||||
$script = $this->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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Mail\Service\MailFilter;
|
||||
|
||||
use OCA\Mail\Exception\ImapFlagEncodingException;
|
||||
use OCA\Mail\IMAP\ImapFlag;
|
||||
use OCA\Mail\Sieve\SieveUtils;
|
||||
|
||||
class FilterBuilder {
|
||||
private const SEPARATOR = '### Nextcloud Mail: Filters ### DON\'T EDIT ###';
|
||||
private const DATA_MARKER = '# FILTER: ';
|
||||
private const SIEVE_NEWLINE = "\r\n";
|
||||
|
||||
public function __construct(
|
||||
private ImapFlag $imapFlag,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
public function buildSieveScript(array $filters, string $untouchedScript): string {
|
||||
$commands = [];
|
||||
$extensions = [];
|
||||
|
||||
foreach ($filters as $filter) {
|
||||
if ($filter['enable'] === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$commands[] = '# ' . $filter['name'];
|
||||
|
||||
$tests = [];
|
||||
foreach ($filter['tests'] as $test) {
|
||||
if ($test['field'] === 'subject') {
|
||||
$tests[] = sprintf(
|
||||
'header :%s "Subject" %s',
|
||||
$test['operator'],
|
||||
SieveUtils::stringList($test['values']),
|
||||
);
|
||||
}
|
||||
if ($test['field'] === 'to') {
|
||||
$tests[] = sprintf(
|
||||
'address :%s :all "To" %s',
|
||||
$test['operator'],
|
||||
SieveUtils::stringList($test['values']),
|
||||
);
|
||||
}
|
||||
if ($test['field'] === 'from') {
|
||||
$tests[] = sprintf(
|
||||
'address :%s :all "From" %s',
|
||||
$test['operator'],
|
||||
SieveUtils::stringList($test['values']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (count($tests) === 0) {
|
||||
// skip filter without tests
|
||||
$commands[] = '# No valid tests found';
|
||||
continue;
|
||||
}
|
||||
|
||||
$actions = [];
|
||||
foreach ($filter['actions'] as $action) {
|
||||
if ($action['type'] === 'fileinto') {
|
||||
$extensions[] = 'fileinto';
|
||||
$actions[] = sprintf(
|
||||
'fileinto "%s";',
|
||||
SieveUtils::escapeString($action['mailbox'])
|
||||
);
|
||||
}
|
||||
if ($action['type'] === 'addflag') {
|
||||
$extensions[] = 'imap4flags';
|
||||
$actions[] = sprintf(
|
||||
'addflag "%s";',
|
||||
SieveUtils::escapeString($this->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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Mail\Service\MailFilter;
|
||||
|
||||
use JsonException;
|
||||
use OCA\Mail\Exception\FilterParserException;
|
||||
|
||||
class FilterParser {
|
||||
private const SEPARATOR = '### Nextcloud Mail: Filters ### DON\'T EDIT ###';
|
||||
private const DATA_MARKER = '# FILTER: ';
|
||||
|
||||
private const STATE_COPY = 0;
|
||||
private const STATE_SKIP = 1;
|
||||
|
||||
/**
|
||||
* @throws FilterParserException
|
||||
*/
|
||||
public function parseFilterState(string $sieveScript): FilterParserResult {
|
||||
$filters = [];
|
||||
$scriptOut = [];
|
||||
|
||||
$state = self::STATE_COPY;
|
||||
$nextState = $state;
|
||||
|
||||
$lines = preg_split('/\r?\n/', $sieveScript);
|
||||
foreach ($lines as $line) {
|
||||
switch ($state) {
|
||||
case self::STATE_COPY:
|
||||
if (str_starts_with($line, self::SEPARATOR)) {
|
||||
$nextState = self::STATE_SKIP;
|
||||
} else {
|
||||
$scriptOut[] = $line;
|
||||
}
|
||||
break;
|
||||
case self::STATE_SKIP:
|
||||
if (str_starts_with($line, self::SEPARATOR)) {
|
||||
$nextState = self::STATE_COPY;
|
||||
} elseif (str_starts_with($line, self::DATA_MARKER)) {
|
||||
$json = substr($line, strlen(self::DATA_MARKER));
|
||||
|
||||
try {
|
||||
$data = json_decode($json, true, 10, JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException $e) {
|
||||
throw FilterParserException::invalidJson($e);
|
||||
}
|
||||
|
||||
if (is_array($data)) {
|
||||
array_push($filters, ...$data);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw FilterParserException::invalidState();
|
||||
}
|
||||
$state = $nextState;
|
||||
}
|
||||
|
||||
return new FilterParserResult($filters, $sieveScript, implode("\r\n", $scriptOut));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Mail\Service\MailFilter;
|
||||
|
||||
use JsonSerializable;
|
||||
use ReturnTypeWillChange;
|
||||
|
||||
class FilterParserResult implements JsonSerializable {
|
||||
public function __construct(
|
||||
private array $filters,
|
||||
private string $sieveScript,
|
||||
private string $untouchedSieveScript,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getFilters(): array {
|
||||
return $this->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(),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Mail\Service\MailFilter;
|
||||
|
||||
use JsonSerializable;
|
||||
use ReturnTypeWillChange;
|
||||
|
||||
class FilterState implements JsonSerializable {
|
||||
public const DEFAULT_VERSION = 1;
|
||||
|
||||
public function __construct(
|
||||
private array $filters,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromJson(array $data): self {
|
||||
return new self($data);
|
||||
}
|
||||
|
||||
public function getFilters(): array {
|
||||
return $this->filters;
|
||||
}
|
||||
|
||||
#[ReturnTypeWillChange]
|
||||
public function jsonSerialize() {
|
||||
return $this->filters;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,9 +55,16 @@
|
|||
{{ t('mail', 'Please connect to a sieve server first.') }}
|
||||
</p>
|
||||
</AppSettingsSection>
|
||||
<AppSettingsSection v-if="account && account.sieveEnabled"
|
||||
id="mail-filters"
|
||||
:name="t('mail', 'Filters')">
|
||||
<div id="mail-filters">
|
||||
<MailFilters :key="account.accountId" ref="mailFilters" :account="account" />
|
||||
</div>
|
||||
</AppSettingsSection>
|
||||
<AppSettingsSection v-if="account && account.sieveEnabled"
|
||||
id="sieve-filter"
|
||||
:name="t('mail', 'Sieve filter rules')">
|
||||
:name="t('mail', 'Sieve script editor')">
|
||||
<div id="sieve-filter">
|
||||
<SieveFilterForm :key="account.accountId"
|
||||
ref="sieveFilterForm"
|
||||
|
@ -77,7 +84,7 @@
|
|||
</AppSettingsSection>
|
||||
<AppSettingsSection v-if="account && !account.provisioningId"
|
||||
id="sieve-settings"
|
||||
:name="t('mail', 'Sieve filter server')">
|
||||
:name="t('mail', 'Sieve server')">
|
||||
<div id="sieve-settings">
|
||||
<SieveAccountForm :key="account.accountId"
|
||||
ref="sieveAccountForm"
|
||||
|
@ -104,6 +111,7 @@ import CertificateSettings from './CertificateSettings.vue'
|
|||
import SearchSettings from './SearchSettings.vue'
|
||||
import TrashRetentionSettings from './TrashRetentionSettings.vue'
|
||||
import logger from '../logger.js'
|
||||
import MailFilters from './mailFilter/MailFilters.vue'
|
||||
|
||||
export default {
|
||||
name: 'AccountSettings',
|
||||
|
@ -121,6 +129,7 @@ export default {
|
|||
CertificateSettings,
|
||||
TrashRetentionSettings,
|
||||
SearchSettings,
|
||||
MailFilters,
|
||||
},
|
||||
props: {
|
||||
account: {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<template>
|
||||
<form id="sieve-form">
|
||||
<p>
|
||||
{{ 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.') }}
|
||||
</p>
|
||||
<p>
|
||||
<NcCheckboxRadioSwitch :checked.sync="sieveConfig.sieveEnabled">
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<div class="mail-filter-row">
|
||||
<div class="mail-filter-column">
|
||||
<NcSelect :value="action.type"
|
||||
:required="true"
|
||||
:label-outside="true"
|
||||
:options="availableTypes"
|
||||
@input="updateAction({ type: $event })" />
|
||||
</div>
|
||||
<div class="mail-filter-column">
|
||||
<component :is="componentInstance"
|
||||
v-if="componentInstance"
|
||||
:action="action"
|
||||
:account="account"
|
||||
@update-action="updateAction" />
|
||||
</div>
|
||||
<div class="mail-filter-column">
|
||||
<NcButton aria-label="Delete action"
|
||||
type="tertiary-no-background"
|
||||
@click="deleteAction">
|
||||
{{ t('mail', 'Delete action') }}
|
||||
<template #icon>
|
||||
<DeleteIcon :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import ActionFileinto from './ActionFileinto.vue'
|
||||
import ActionAddflag from './ActionAddflag.vue'
|
||||
import ActionStop from './ActionStop.vue'
|
||||
import { NcButton, NcSelect, NcTextField } from '@nextcloud/vue'
|
||||
import DeleteIcon from 'vue-material-design-icons/Delete.vue'
|
||||
|
||||
export default {
|
||||
name: 'Action',
|
||||
components: {
|
||||
NcSelect,
|
||||
NcTextField,
|
||||
NcButton,
|
||||
ActionFileinto,
|
||||
ActionAddflag,
|
||||
ActionStop,
|
||||
DeleteIcon,
|
||||
},
|
||||
props: {
|
||||
action: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
account: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
availableTypes: [
|
||||
'addflag',
|
||||
'fileinto',
|
||||
'stop',
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
componentInstance() {
|
||||
if (this.action.type === 'fileinto') {
|
||||
return ActionFileinto
|
||||
} else if (this.action.type === 'addflag') {
|
||||
return ActionAddflag
|
||||
} else if (this.action.type === 'stop') {
|
||||
return ActionStop
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateAction(properties) {
|
||||
this.$emit('update-action', { ...this.action, ...properties })
|
||||
},
|
||||
deleteAction() {
|
||||
this.$emit('delete-action', this.action)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.mail-filter-row {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
align-items: baseline;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,45 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<NcTextField :required="true"
|
||||
:label="t('mail', 'Flag')"
|
||||
:value="flag"
|
||||
@update:value="onInput" />
|
||||
</template>
|
||||
<script>
|
||||
import { NcTextField } from '@nextcloud/vue'
|
||||
|
||||
export default {
|
||||
name: 'ActionAddflag',
|
||||
components: {
|
||||
NcTextField,
|
||||
},
|
||||
props: {
|
||||
action: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
account: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
flag() {
|
||||
return this.action.flag ?? ''
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onInput(value) {
|
||||
this.$emit('update-action', { flag: value })
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.input-field {
|
||||
min-width: 260px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,50 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<NcSelect ref="select"
|
||||
:value="mailbox"
|
||||
:options="mailboxes"
|
||||
:required="true"
|
||||
@input="onInput" />
|
||||
</template>
|
||||
<script>
|
||||
|
||||
import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
|
||||
import { mailboxHasRights } from '../../util/acl.js'
|
||||
export default {
|
||||
name: 'ActionFileinto',
|
||||
components: {
|
||||
NcSelect,
|
||||
},
|
||||
props: {
|
||||
action: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
account: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
mailbox() {
|
||||
return this.action.mailbox ?? undefined
|
||||
},
|
||||
mailboxes() {
|
||||
const mailboxes = this.$store.getters.getMailboxes(this.account.accountId)
|
||||
.filter(mailbox => mailboxHasRights(mailbox, 'i'))
|
||||
|
||||
return mailboxes.map((mailbox) => {
|
||||
return mailbox.displayName
|
||||
})
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onInput(value) {
|
||||
this.$emit('update-action', { mailbox: value })
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,23 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<p>{{ t('mail', 'The stop action ends all processing') }}</p>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'ActionStop',
|
||||
props: {
|
||||
action: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
account: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,62 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<NcDialog :name="t('mail', 'Delete mail filter {filterName}?', {filterName: filter.name})"
|
||||
:open="open"
|
||||
:message="t('mail', 'Are you sure to delete the mail filter?')"
|
||||
:buttons="buttons"
|
||||
@closing="closeModal()" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { NcDialog } from '@nextcloud/vue'
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import IconCancel from '@mdi/svg/svg/cancel.svg?raw'
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import IconCheck from '@mdi/svg/svg/check.svg?raw'
|
||||
|
||||
export default {
|
||||
name: 'DeleteModal',
|
||||
components: {
|
||||
NcDialog,
|
||||
},
|
||||
props: {
|
||||
filter: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
open: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
buttons: [
|
||||
{
|
||||
label: t('mail', 'Cancel'),
|
||||
icon: IconCancel,
|
||||
callback: () => { this.closeModal() },
|
||||
},
|
||||
{
|
||||
label: t('mail', 'Delete filter'),
|
||||
type: 'error',
|
||||
icon: IconCheck,
|
||||
callback: () => { this.deleteFilter() },
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
deleteFilter() {
|
||||
this.$emit('delete-filter', this.filter)
|
||||
},
|
||||
closeModal() {
|
||||
this.$emit('close')
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
</script>
|
|
@ -0,0 +1,218 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<div class="section">
|
||||
<p>{{ t('mail', 'Take control of your email chaos. Filters help you to prioritize what matters and eliminate clutter.') }}</p>
|
||||
<ul>
|
||||
<NcListItem v-for="filter in filters"
|
||||
:key="filter.id"
|
||||
:name="filter.name"
|
||||
:compact="true"
|
||||
@click="openUpdateModal(filter)">
|
||||
<template #subname>
|
||||
<span v-if="filter.enable">{{ t('mail', 'Filter is active') }}</span>
|
||||
<span v-else>{{ t('mail', 'Filter is not active') }}</span>
|
||||
</template>
|
||||
<template #actions>
|
||||
<NcActionButton @click="openDeleteModal(filter)">
|
||||
<template #icon>
|
||||
<DeleteIcon :size="20" />
|
||||
</template>
|
||||
{{ t('mail', 'Delete filter') }}
|
||||
</NcActionButton>
|
||||
</template>
|
||||
</NcListItem>
|
||||
</ul>
|
||||
<NcButton class="app-settings-button"
|
||||
type="primary"
|
||||
:aria-label="t('mail', 'New filter')"
|
||||
@click.prevent.stop="createFilter">
|
||||
{{ t('mail', 'New filter') }}
|
||||
</NcButton>
|
||||
<UpdateModal v-if="showUpdateModal && currentFilter"
|
||||
:filter="currentFilter"
|
||||
:account="account"
|
||||
:loading="loading"
|
||||
@update-filter="updateFilter"
|
||||
@close="closeModal" />
|
||||
<DeleteModal v-if="showDeleteModal && currentFilter"
|
||||
:filter="currentFilter"
|
||||
:open="showDeleteModal"
|
||||
:loading="loading"
|
||||
@delete-filter="deleteFilter"
|
||||
@close="closeModal" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { NcActionButton, NcListItem, NcButton } from '@nextcloud/vue'
|
||||
import UpdateModal from './UpdateModal.vue'
|
||||
import { randomId } from '../../util/randomId.js'
|
||||
import logger from '../../logger.js'
|
||||
import { mapStores } from 'pinia'
|
||||
import useMailFilterStore from '../../store/mailFilterStore.js'
|
||||
import DeleteIcon from 'vue-material-design-icons/Delete.vue'
|
||||
import DeleteModal from './DeleteModal.vue'
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
|
||||
export default {
|
||||
name: 'MailFilters',
|
||||
components: {
|
||||
NcButton,
|
||||
NcListItem,
|
||||
NcActionButton,
|
||||
UpdateModal,
|
||||
DeleteIcon,
|
||||
DeleteModal,
|
||||
|
||||
},
|
||||
props: {
|
||||
account: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showUpdateModal: false,
|
||||
showDeleteModal: false,
|
||||
script: '',
|
||||
loading: true,
|
||||
errorMessage: '',
|
||||
currentFilter: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useMailFilterStore),
|
||||
filters() {
|
||||
return this.mailFilterStore.filters
|
||||
},
|
||||
scriptData() {
|
||||
return this.$store.getters.getActiveSieveScript(this.account.id)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
scriptData: {
|
||||
immediate: true,
|
||||
handler(scriptData) {
|
||||
if (!scriptData) {
|
||||
return
|
||||
}
|
||||
|
||||
this.script = scriptData.script
|
||||
this.loading = false
|
||||
},
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
await this.mailFilterStore.fetch(this.account.id)
|
||||
},
|
||||
methods: {
|
||||
createFilter() {
|
||||
const priority = Math.max(0, ...this.filters.map((item) => item.priority ?? 0)) + 10
|
||||
|
||||
this.currentFilter = {
|
||||
id: randomId(),
|
||||
name: t('mail', 'New filter'),
|
||||
enable: true,
|
||||
operator: 'allof',
|
||||
tests: [],
|
||||
actions: [],
|
||||
priority,
|
||||
}
|
||||
this.showUpdateModal = true
|
||||
this.loading = false
|
||||
},
|
||||
openUpdateModal(filter) {
|
||||
this.currentFilter = filter
|
||||
this.showUpdateModal = true
|
||||
},
|
||||
openDeleteModal(filter) {
|
||||
this.currentFilter = filter
|
||||
this.showDeleteModal = true
|
||||
|
||||
},
|
||||
async updateFilter(filter) {
|
||||
this.loading = true
|
||||
this.mailFilterStore.$patch((state) => {
|
||||
const index = state.filters.findIndex((item) => item.id === filter.id)
|
||||
logger.debug('update filter', { filter, index })
|
||||
|
||||
if (index === -1) {
|
||||
state.filters.push(filter)
|
||||
} else {
|
||||
state.filters[index] = filter
|
||||
}
|
||||
|
||||
state.filters.sort((a, b) => a.priority - b.priority)
|
||||
})
|
||||
|
||||
try {
|
||||
await this.mailFilterStore.update(this.account.id).then(() => {
|
||||
showSuccess(t('mail', 'Filter saved'))
|
||||
})
|
||||
await this.$store.dispatch('fetchActiveSieveScript', { accountId: this.account.id })
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
showError(t('mail', 'Could not save filter'))
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async deleteFilter(filter) {
|
||||
this.loading = true
|
||||
|
||||
this.mailFilterStore.$patch((state) => {
|
||||
const index = state.filters.findIndex((item) => item.id === filter.id)
|
||||
logger.debug('delete filter', { filter, index })
|
||||
|
||||
if (index !== -1) {
|
||||
state.filters.splice(index, 1)
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
await this.mailFilterStore.update(this.account.id).then(() => {
|
||||
showSuccess(t('mail', 'Filter deleted'))
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
showError(t('mail', 'Could not delete filter'))
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
await this.$store.dispatch('fetchActiveSieveScript', { accountId: this.account.id })
|
||||
},
|
||||
closeModal() {
|
||||
this.currentFilter = null
|
||||
this.showUpdateModal = false
|
||||
this.showDeleteModal = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.section {
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin-bottom: 23px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.primary {
|
||||
padding-left: 26px;
|
||||
background-position: 6px;
|
||||
color: var(--color-main-background);
|
||||
|
||||
&:after {
|
||||
left: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,48 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<h6>{{ t('mail', 'Operator') }}</h6>
|
||||
</div>
|
||||
<div>
|
||||
<NcCheckboxRadioSwitch :checked="filter.operator"
|
||||
value="allof"
|
||||
name="sharing_permission_radio"
|
||||
type="radio"
|
||||
@update:checked="updateFilter('allof')">
|
||||
allof ({{ t('mail', 'If all tests pass, then the actions will be executed') }})
|
||||
</NcCheckboxRadioSwitch>
|
||||
<NcCheckboxRadioSwitch :checked="filter.operator"
|
||||
value="anyof"
|
||||
name="sharing_permission_radio"
|
||||
type="radio"
|
||||
@update:checked="updateFilter('anyof')">
|
||||
anyof ({{ t('mail', 'If one test pass, then the actions will be executed') }})
|
||||
</NcCheckboxRadioSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { NcCheckboxRadioSwitch } from '@nextcloud/vue'
|
||||
|
||||
export default {
|
||||
name: 'Operator',
|
||||
components: {
|
||||
NcCheckboxRadioSwitch,
|
||||
},
|
||||
props: {
|
||||
filter: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateFilter(operator) {
|
||||
this.$emit('update:operator', operator)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,120 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<div class="mail-filter-rows">
|
||||
<div class="mail-filter-rows__row">
|
||||
<div class="mail-filter-rows__row__column">
|
||||
<NcSelect input-label="field"
|
||||
:value="test.field"
|
||||
:required="true"
|
||||
:label-outside="true"
|
||||
:options="['subject', 'from', 'to']"
|
||||
@input="updateTest({ field: $event })" />
|
||||
</div>
|
||||
<div class="mail-filter-rows__row__column">
|
||||
<NcSelect input-label="operator"
|
||||
:value="test.operator"
|
||||
:required="true"
|
||||
:label-outside="true"
|
||||
:options="['is', 'contains', 'matches']"
|
||||
@input="updateTest({ operator: $event })" />
|
||||
</div>
|
||||
<div class="mail-filter-rows__row__column">
|
||||
<NcButton aria-label="Delete action"
|
||||
type="tertiary-no-background"
|
||||
@click="deleteTest">
|
||||
<template #icon>
|
||||
<DeleteIcon :size="20" />
|
||||
</template>
|
||||
{{ t('mail', 'Delete test') }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mail-filter-rows__row">
|
||||
<NcSelect v-model="localValues"
|
||||
input-label="value"
|
||||
class="mail-filter-rows__row__select"
|
||||
:multiple="true"
|
||||
:wrap="true"
|
||||
:close-on-select="false"
|
||||
:taggable="true"
|
||||
:required="true"
|
||||
@option:selected="updateTest({ values: localValues })"
|
||||
@option:deselected="updateTest({ values: localValues })" />
|
||||
</div>
|
||||
<hr class="solid">
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { NcButton, NcSelect } from '@nextcloud/vue'
|
||||
import DeleteIcon from 'vue-material-design-icons/Delete.vue'
|
||||
|
||||
export default {
|
||||
name: 'Test',
|
||||
components: {
|
||||
NcButton,
|
||||
NcSelect,
|
||||
DeleteIcon,
|
||||
},
|
||||
props: {
|
||||
test: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
inputValue: '',
|
||||
localValues: [],
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.localValues = [...this.test.values]
|
||||
},
|
||||
methods: {
|
||||
updateTest(properties) {
|
||||
this.$emit('update-test', { ...this.test, ...properties })
|
||||
},
|
||||
deleteTest() {
|
||||
this.$emit('delete-test', this.test)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.solid {
|
||||
margin: calc(var(--default-grid-baseline) * 4) 0 0 0;
|
||||
}
|
||||
.mail-filter-rows {
|
||||
margin-bottom: calc(var(--default-grid-baseline) * 4);
|
||||
&__row {
|
||||
display: flex;
|
||||
gap: var(--default-grid-baseline);
|
||||
align-items: baseline;
|
||||
&__column {
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
}
|
||||
&__column *{
|
||||
width: 100%;
|
||||
}
|
||||
&__select {
|
||||
max-width: 100% !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.values-list {
|
||||
display: flex;
|
||||
gap: var(--default-grid-baseline);
|
||||
flex-wrap: wrap;
|
||||
&__item {
|
||||
display: flex;
|
||||
gap: var(--default-grid-baseline);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,213 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<NcModal size="large"
|
||||
:close-on-click-outside="false"
|
||||
:name="t('mail','Update mail filter')"
|
||||
@close="closeModal">
|
||||
<form class="modal__content" @submit.prevent="updateFilter">
|
||||
<div class="filter-name">
|
||||
<NcTextField :value.sync="clone.name"
|
||||
:label="t('mail', 'Filter name')"
|
||||
:required="true" />
|
||||
</div>
|
||||
|
||||
<div class="filter-operator">
|
||||
<Operator :filter="clone" @update:operator="updateOperator" />
|
||||
</div>
|
||||
|
||||
<div class="filter-tests">
|
||||
<h6>{{ t('mail', 'Tests') }}</h6>
|
||||
|
||||
<div class="help-text">
|
||||
<p>
|
||||
{{ t('mail', 'Tests are applied to incoming emails on your mail server, targeting fields such as subject (the email\'s subject line), from (the sender), and to (the recipient). You can use the following operators to define conditions for these fields:') }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>is</strong>: {{ t('mail', 'An exact match. The field must be identical to the provided value.') }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>contains</strong>: {{ t('mail', 'A substring match. The field matches if the provided value is contained within it. For example, "report" would match "port".') }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>matches</strong>: {{ t('mail', 'A pattern match using wildcards. The "*" symbol represents any number of characters (including none), while "?" represents exactly one character. For example, "*report*" would match "Business report 2024".') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Test v-for="test in clone.tests"
|
||||
:key="test.id"
|
||||
:test="test"
|
||||
@update-test="updateTest"
|
||||
@delete-test="deleteTest" />
|
||||
<NcButton class="app-settings-button"
|
||||
type="secondary"
|
||||
:aria-label="t('mail', 'New test')"
|
||||
@click="createTest">
|
||||
{{ t('mail', 'New test') }}
|
||||
</NcButton>
|
||||
</div>
|
||||
|
||||
<div class="filter-actions">
|
||||
<h6>{{ t('mail', 'Actions') }}</h6>
|
||||
|
||||
<div class="help-text">
|
||||
<p>
|
||||
{{ t('mail', 'Actions are triggered when the specified tests are true. The following actions are available:') }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>fileinto</strong>: {{ t('mail', 'Moves the message into a specified folder.') }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>addflag</strong>: {{ t('mail', 'Adds a flag to the message.') }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>stop</strong>: {{ t('mail', 'Halts the execution of the filter script. No further filters with will be processed after this action.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Action v-for="action in clone.actions"
|
||||
:key="action.id"
|
||||
:action="action"
|
||||
:account="account"
|
||||
@update-action="updateAction"
|
||||
@delete-action="deleteAction" />
|
||||
<NcButton class="app-settings-button"
|
||||
type="secondary"
|
||||
:aria-label="t('mail', 'New action')"
|
||||
@click="createAction">
|
||||
{{ t('mail', 'New action') }}
|
||||
</NcButton>
|
||||
</div>
|
||||
|
||||
<NcTextField :value.sync="clone.priority"
|
||||
type="number"
|
||||
:label="t('mail', 'Priority')"
|
||||
:required="true" />
|
||||
|
||||
<NcCheckboxRadioSwitch :checked.sync="clone.enable" type="switch">
|
||||
{{ t('mail', 'Enable filter') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
|
||||
<NcButton type="primary"
|
||||
native-type="submit">
|
||||
<template #icon>
|
||||
<NcLoadingIcon v-if="loading" :size="16" />
|
||||
<IconCheck v-else :size="16" />
|
||||
</template>
|
||||
{{ t('mail', 'Save filter') }}
|
||||
</NcButton>
|
||||
</form>
|
||||
</NcModal>
|
||||
</template>
|
||||
<script>
|
||||
import { NcButton, NcCheckboxRadioSwitch, NcModal, NcTextField, NcLoadingIcon } from '@nextcloud/vue'
|
||||
import Test from './Test.vue'
|
||||
import Operator from './Operator.vue'
|
||||
import { randomId } from '../../util/randomId.js'
|
||||
import Action from './Action.vue'
|
||||
import IconCheck from 'vue-material-design-icons/Check.vue'
|
||||
|
||||
export default {
|
||||
name: 'UpdateModal',
|
||||
components: {
|
||||
IconCheck,
|
||||
Action,
|
||||
Operator,
|
||||
Test,
|
||||
NcButton,
|
||||
NcCheckboxRadioSwitch,
|
||||
NcLoadingIcon,
|
||||
NcModal,
|
||||
NcTextField,
|
||||
},
|
||||
props: {
|
||||
filter: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
account: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
clone: structuredClone(this.filter),
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
createTest() {
|
||||
this.clone.tests.push({ id: randomId(), operator: null, values: [] })
|
||||
},
|
||||
updateTest(test) {
|
||||
const index = this.clone.tests.findIndex((items) => items.id === test.id)
|
||||
this.$set(this.clone.tests, index, test)
|
||||
},
|
||||
deleteTest(test) {
|
||||
this.clone.tests = this.clone.tests.filter((item) => item.id !== test.id)
|
||||
},
|
||||
createAction() {
|
||||
this.clone.actions.push({ id: randomId(), type: null })
|
||||
},
|
||||
updateAction(action) {
|
||||
const index = this.clone.actions.findIndex((item) => item.id === action.id)
|
||||
this.$set(this.clone.actions, index, action)
|
||||
},
|
||||
updateOperator(operator) {
|
||||
this.clone.operator = operator
|
||||
},
|
||||
deleteAction(action) {
|
||||
this.clone.actions = this.clone.actions.filter((item) => item.id !== action.id)
|
||||
},
|
||||
updateFilter() {
|
||||
this.$emit('update-filter', structuredClone(this.clone))
|
||||
},
|
||||
closeModal() {
|
||||
this.$emit('close')
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.modal__content {
|
||||
margin: 50px;
|
||||
}
|
||||
|
||||
.modal__content h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.filter-name, .filter-operator, .filter-tests, .filter-actions {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin: calc(var(--default-grid-baseline) * 4) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.external-label {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.external-label label {
|
||||
padding-top: 7px;
|
||||
padding-right: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.help-text p {
|
||||
margin-bottom: 0.2em;
|
||||
}
|
||||
</style>
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
},
|
||||
},
|
||||
})
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace Unit\Service;
|
||||
|
||||
use ChristophWurst\Nextcloud\Testing\TestCase;
|
||||
use OCA\Mail\Db\Alias;
|
||||
use OCA\Mail\Db\MailAccount;
|
||||
use OCA\Mail\Service\AliasesService;
|
||||
use OCA\Mail\Service\AllowedRecipientsService;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
||||
class AllowedRecipientsServiceTest extends TestCase {
|
||||
|
||||
private AliasesService&MockObject $aliasesService;
|
||||
private AllowedRecipientsService $allowedRecipientsService;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->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]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,239 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace Unit\Service;
|
||||
|
||||
use ChristophWurst\Nextcloud\Testing\TestCase;
|
||||
use OCA\Mail\Db\MailAccount;
|
||||
use OCA\Mail\IMAP\ImapFlag;
|
||||
use OCA\Mail\Service\AllowedRecipientsService;
|
||||
use OCA\Mail\Service\FilterService;
|
||||
use OCA\Mail\Service\MailFilter\FilterBuilder;
|
||||
use OCA\Mail\Service\MailFilter\FilterParser;
|
||||
use OCA\Mail\Service\OutOfOffice\OutOfOfficeParser;
|
||||
use OCA\Mail\Service\SieveService;
|
||||
use OCA\Mail\Sieve\NamedSieveScript;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\Test\TestLogger;
|
||||
|
||||
class FilterServiceTest extends TestCase {
|
||||
|
||||
private string $testFolder;
|
||||
|
||||
private AllowedRecipientsService $allowedRecipientsService;
|
||||
private OutOfOfficeParser $outOfOfficeParser;
|
||||
private FilterParser $filterParser;
|
||||
private FilterBuilder $filterBuilder;
|
||||
private SieveService&MockObject $sieveService;
|
||||
private LoggerInterface $logger;
|
||||
private FilterService $filterService;
|
||||
|
||||
public function __construct(?string $name = null, array $data = [], $dataName = '') {
|
||||
parent::__construct($name, $data, $dataName);
|
||||
$this->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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Mail\Tests\Unit\Service\MailFilter;
|
||||
|
||||
use ChristophWurst\Nextcloud\Testing\TestCase;
|
||||
use OCA\Mail\IMAP\ImapFlag;
|
||||
use OCA\Mail\Service\MailFilter\FilterBuilder;
|
||||
|
||||
class FilterBuilderTest extends TestCase {
|
||||
private FilterBuilder $builder;
|
||||
private string $testFolder;
|
||||
|
||||
public function __construct(?string $name = null, array $data = [], $dataName = '') {
|
||||
parent::__construct($name, $data, $dataName);
|
||||
$this->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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace Unit\Service\MailFilter;
|
||||
|
||||
use ChristophWurst\Nextcloud\Testing\TestCase;
|
||||
use OCA\Mail\Service\MailFilter\FilterParser;
|
||||
|
||||
class FilterParserTest extends TestCase {
|
||||
private FilterParser $filterParser;
|
||||
|
||||
private string $testFolder;
|
||||
|
||||
public function __construct(?string $name = null, array $data = [], $dataName = '') {
|
||||
parent::__construct($name, $data, $dataName);
|
||||
$this->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']);
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
|
@ -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 ###
|
|
@ -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"
|
||||
}
|
||||
]
|
|
@ -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 ###
|
|
@ -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
|
||||
}
|
||||
]
|
|
@ -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 ###
|
|
@ -0,0 +1,15 @@
|
|||
[
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"flag": "Flag 123",
|
||||
"type": "addflag"
|
||||
}
|
||||
],
|
||||
"enable": true,
|
||||
"name": "Test 4",
|
||||
"operator": "allof",
|
||||
"priority": 60,
|
||||
"tests": []
|
||||
}
|
||||
]
|
|
@ -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 ###
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -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 ###
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -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 ###
|
|
@ -0,0 +1 @@
|
|||
[]
|
|
@ -0,0 +1 @@
|
|||
# Hello, this is a test
|
|
@ -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 ###
|
|
@ -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 ###
|
|
@ -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@test.org>", "Test Alias <alias@test.org>"] "I'm on vacation.";
|
||||
}
|
||||
### Nextcloud Mail: Vacation Responder ### DON'T EDIT ###
|
|
@ -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@test.org>", "Test Alias <alias@test.org>"] "I'm on vacation.";
|
||||
}
|
||||
### Nextcloud Mail: Vacation Responder ### DON'T EDIT ###
|
|
@ -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 ###
|
|
@ -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 ###
|
|
@ -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
|
||||
}
|
||||
]
|
|
@ -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 ###
|
|
@ -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 ###
|
|
@ -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
|
||||
}
|
||||
]
|
|
@ -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 ###
|
|
@ -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 ###
|
Загрузка…
Ссылка в новой задаче