Skip to content

Commit

Permalink
Merge pull request #8627 from nextcloud/feat/trash-retention
Browse files Browse the repository at this point in the history
feat: implement trash retention
  • Loading branch information
ChristophWurst authored Aug 7, 2023
2 parents dd73bf9 + de09050 commit 2507be5
Show file tree
Hide file tree
Showing 22 changed files with 997 additions and 52 deletions.
3 changes: 2 additions & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Positive:
Learn more about the Nextcloud Ethical AI Rating [in our blog](https://nextcloud.com/blog/nextcloud-ethical-ai-rating/).
]]></description>
<version>3.4.0-alpha.1</version>
<version>3.4.0-alpha.2</version>
<licence>agpl</licence>
<author>Greta Doçi</author>
<author homepage="https://github.com/nextcloud/groupware">Nextcloud Groupware Team</author>
Expand All @@ -47,6 +47,7 @@ Learn more about the Nextcloud Ethical AI Rating [in our blog](https://nextcloud
<job>OCA\Mail\BackgroundJob\OutboxWorkerJob</job>
<job>OCA\Mail\BackgroundJob\IMipMessageJob</job>
<job>OCA\Mail\BackgroundJob\DraftsJob</job>
<job>OCA\Mail\BackgroundJob\TrashRetentionJob</job>
</background-jobs>
<repair-steps>
<post-migration>
Expand Down
2 changes: 2 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
use OCA\Mail\Listener\InteractionListener;
use OCA\Mail\Listener\MailboxesSynchronizedSpecialMailboxesUpdater;
use OCA\Mail\Listener\MessageCacheUpdaterListener;
use OCA\Mail\Listener\MessageKnownSinceListener;
use OCA\Mail\Listener\NewMessageClassificationListener;
use OCA\Mail\Listener\OauthTokenRefreshListener;
use OCA\Mail\Listener\SaveSentMessageListener;
Expand Down Expand Up @@ -128,6 +129,7 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(MessageSentEvent::class, InteractionListener::class);
$context->registerEventListener(MessageSentEvent::class, SaveSentMessageListener::class);
$context->registerEventListener(NewMessagesSynchronized::class, NewMessageClassificationListener::class);
$context->registerEventListener(NewMessagesSynchronized::class, MessageKnownSinceListener::class);
$context->registerEventListener(SynchronizationEvent::class, AccountSynchronizedThreadUpdaterListener::class);
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);

Expand Down
137 changes: 137 additions & 0 deletions lib/BackgroundJob/TrashRetentionJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2023 Richard Steinmetz <[email protected]>
*
* @author Richard Steinmetz <[email protected]>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OCA\Mail\BackgroundJob;

use OCA\Mail\Account;
use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\MailAccountMapper;
use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Db\MessageMapper;
use OCA\Mail\Db\MessageRetentionMapper;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\IMAP\IMAPClientFactory;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use Psr\Log\LoggerInterface;

class TrashRetentionJob extends TimedJob {
public function __construct(
ITimeFactory $time,
private LoggerInterface $logger,
private IMAPClientFactory $clientFactory,
private MessageMapper $messageMapper,
private MessageRetentionMapper $messageRetentionMapper,
private MailAccountMapper $accountMapper,
private MailboxMapper $mailboxMapper,
private IMailManager $mailManager,
) {
parent::__construct($time);

$this->setInterval(24 * 3600);
$this->setTimeSensitivity(self::TIME_INSENSITIVE);
}

/**
* @inheritDoc
*/
public function run($argument) {
$accounts = $this->accountMapper->getAllAccounts();
foreach ($accounts as $account) {
$account = new Account($account);

$retentionDays = $account->getMailAccount()->getTrashRetentionDays();
if ($retentionDays === null || $retentionDays <= 0) {
continue;
}

$retentionSeconds = $retentionDays * 24 * 3600;

try {
$this->cleanTrash($account, $retentionSeconds);
} catch (ServiceException|ClientException $e) {
$this->logger->error('Could not clean trash mailbox', [
'exception' => $e,
'userId' => $account->getUserId(),
'accountId' => $account->getId(),
'trashMailboxId' => $account->getMailAccount()->getTrashMailboxId(),
]);
}
}

}

/**
* @throws ClientException
* @throws ServiceException
*/
private function cleanTrash(Account $account, int $retentionSeconds): void {
$trashMailboxId = $account->getMailAccount()->getTrashMailboxId();
if ($trashMailboxId === null) {
return;
}

try {
$trashMailbox = $this->mailboxMapper->findById($trashMailboxId);
} catch (DoesNotExistException $e) {
return;
}

$now = $this->time->getTime();
$messages = $this->messageMapper->findMessagesKnownSinceBefore(
$trashMailboxId,
$now - $retentionSeconds,
);

if (count($messages) === 0) {
return;
}

$processedMessageIds = [];
$client = $this->clientFactory->getClient($account);
try {
foreach ($messages as $message) {
$this->mailManager->deleteMessageWithClient(
$account,
$trashMailbox,
$message->getUid(),
$client,
);

$messageId = $message->getMessageId();
if ($messageId !== null) {
$processedMessageIds[] = $messageId;
}
}
} finally {
$client->logout();
}

$this->messageRetentionMapper->deleteByMessageIds($processedMessageIds);
}
}
22 changes: 20 additions & 2 deletions lib/Contracts/IMailManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use OCA\Mail\Db\Tag;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Exception\TrashMailboxNotSetException;
use OCA\Mail\Model\IMAPMessage;
use OCA\Mail\Service\Quota;
use OCP\AppFramework\Db\DoesNotExistException;
Expand Down Expand Up @@ -142,12 +143,29 @@ public function moveMessage(Account $sourceAccount,
/**
* @param Account $account
* @param string $mailboxId
* @param int $messageId
* @param int $messageUid
*
* @throws ClientException
* @throws ServiceException
*/
public function deleteMessage(Account $account, string $mailboxId, int $messageId): void;
public function deleteMessage(Account $account, string $mailboxId, int $messageUid): void;

/**
* @param Account $account
* @param Mailbox $mailbox
* @param int $messageUid
* @param Horde_Imap_Client_Socket $client The caller is responsible to close the client.
*
* @throws ServiceException
* @throws ClientException
* @throws TrashMailboxNotSetException If no trash folder is configured for the given account.
*/
public function deleteMessageWithClient(
Account $account,
Mailbox $mailbox,
int $messageUid,
Horde_Imap_Client_Socket $client,
): void;

/**
* Mark all messages of a folder as read
Expand Down
7 changes: 6 additions & 1 deletion lib/Controller/AccountsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,8 @@ public function patchAccount(int $id,
int $sentMailboxId = null,
int $trashMailboxId = null,
int $archiveMailboxId = null,
bool $signatureAboveQuote = null): JSONResponse {
bool $signatureAboveQuote = null,
int $trashRetentionDays = null): JSONResponse {
$account = $this->accountService->find($this->currentUserId, $id);

$dbAccount = $account->getMailAccount();
Expand Down Expand Up @@ -274,6 +275,10 @@ public function patchAccount(int $id,
if ($signatureAboveQuote !== null) {
$dbAccount->setSignatureAboveQuote($signatureAboveQuote);
}
if ($trashRetentionDays !== null) {
// Passing 0 (or lower) disables retention
$dbAccount->setTrashRetentionDays($trashRetentionDays <= 0 ? null : $trashRetentionDays);
}
return new JSONResponse(
$this->accountService->save($dbAccount)
);
Expand Down
10 changes: 10 additions & 0 deletions lib/Db/MailAccount.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@
* @method void setSmimeCertificateId(int|null $smimeCertificateId)
* @method int|null getQuotaPercentage()
* @method void setQuotaPercentage(int $quota);
* @method int|null getTrashRetentionDays()
* @method void setTrashRetentionDays(int|null $trashRetentionDays)
*/
class MailAccount extends Entity {
public const SIGNATURE_MODE_PLAIN = 0;
Expand Down Expand Up @@ -176,6 +178,9 @@ class MailAccount extends Entity {
/** @var int|null */
protected $quotaPercentage;

/** @var int|null */
protected $trashRetentionDays;

/**
* @param array $params
*/
Expand Down Expand Up @@ -227,6 +232,9 @@ public function __construct(array $params = []) {
if (isset($params['signatureAboveQuote'])) {
$this->setSignatureAboveQuote($params['signatureAboveQuote']);
}
if (isset($params['trashRetentionDays'])) {
$this->setTrashRetentionDays($params['trashRetentionDays']);
}

$this->addType('inboundPort', 'integer');
$this->addType('outboundPort', 'integer');
Expand All @@ -245,6 +253,7 @@ public function __construct(array $params = []) {
$this->addType('signatureMode', 'int');
$this->addType('smimeCertificateId', 'integer');
$this->addType('quotaPercentage', 'integer');
$this->addType('trashRetentionDays', 'integer');
}

/**
Expand Down Expand Up @@ -275,6 +284,7 @@ public function toJson() {
'signatureMode' => $this->getSignatureMode(),
'smimeCertificateId' => $this->getSmimeCertificateId(),
'quotaPercentage' => $this->getQuotaPercentage(),
'trashRetentionDays' => $this->getTrashRetentionDays(),
];

if (!is_null($this->getOutboundHost())) {
Expand Down
3 changes: 3 additions & 0 deletions lib/Db/MailAccountMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ public function deleteProvisionedAccountsByUid(string $uid): void {
$delete->executeStatement();
}

/**
* @return MailAccount[]
*/
public function getAllAccounts(): array {
$qb = $this->db->getQueryBuilder();
$query = $qb
Expand Down
33 changes: 33 additions & 0 deletions lib/Db/MessageMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* @copyright 2019 Christoph Wurst <[email protected]>
*
* @author 2019 Christoph Wurst <[email protected]>
* @author 2023 Richard Steinmetz <[email protected]>
*
* @license GNU AGPL version 3 or any later version
*
Expand Down Expand Up @@ -1414,4 +1415,36 @@ public function getUnanalyzed(int $lastRun, array $mailboxIds): array {

return $this->findEntities($select);
}

/**
* @param int $mailboxId
* @param int $before UNIX timestamp (seconds)
*
* @return Message[]
*/
public function findMessagesKnownSinceBefore(int $mailboxId, int $before): array {
$qb = $this->db->getQueryBuilder();

$select = $qb->select('m.*')
->from($this->getTableName(), 'm')
->join('m', 'mail_messages_retention', 'mr', $qb->expr()->eq(
'm.message_id',
'mr.message_id',
IQueryBuilder::PARAM_STR,
))
->where(
$qb->expr()->eq(
'm.mailbox_id',
$qb->createNamedParameter($mailboxId, IQueryBuilder::PARAM_INT),
IQueryBuilder::PARAM_INT,
),
$qb->expr()->lt(
'mr.known_since',
$qb->createNamedParameter($before, IQueryBuilder::PARAM_INT),
IQueryBuilder::PARAM_INT,
),
);

return $this->findEntities($select);
}
}
49 changes: 49 additions & 0 deletions lib/Db/MessageRetention.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2023 Richard Steinmetz <[email protected]>
*
* @author Richard Steinmetz <[email protected]>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OCA\Mail\Db;

use OCP\AppFramework\Db\Entity;

/**
* @method void setMessageId(string $messageId)
* @method string getMessageId()
* @method void setKnownSince(int $knownSince)
* @method int getKnownSince()
*/
class MessageRetention extends Entity {

/** @var string */
protected $messageId;

/** @var int */
protected $knownSince;

public function __construct() {
$this->addType('messageId', 'string');
$this->addType('knownSince', 'integer');
}
}
Loading

0 comments on commit 2507be5

Please sign in to comment.