Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/mail.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnna Larch <anna@nextcloud.com>2022-06-30 15:13:54 +0300
committerAnna Larch <anna@nextcloud.com>2022-09-09 14:16:23 +0300
commit2dcf8b9576326c2bf6e42d63894e2c605803c265 (patch)
tree0fad8afa3fc7d083e2a83601b95720f21dd6f2be
parent85c593e9dc6040c9a4af2161aae413e7aa809ec4 (diff)
Add imip processing
Signed-off-by: Anna Larch <anna@nextcloud.com>
-rw-r--r--appinfo/info.xml1
-rw-r--r--lib/BackgroundJob/IMipMessageJob.php47
-rw-r--r--lib/BackgroundJob/PreviewEnhancementProcessingJob.php96
-rw-r--r--lib/Db/MailboxMapper.php18
-rw-r--r--lib/Db/Message.php13
-rw-r--r--lib/Db/MessageMapper.php88
-rw-r--r--lib/IMAP/MessageMapper.php29
-rw-r--r--lib/IMAP/MessageStructureData.php11
-rw-r--r--lib/IMAP/PreviewEnhancer.php1
-rw-r--r--lib/Migration/FixBackgroundJobs.php2
-rw-r--r--lib/Migration/Version2000Date20220908130842.php66
-rw-r--r--lib/Model/IMAPMessage.php19
-rw-r--r--lib/Service/IMipService.php169
-rw-r--r--lib/Service/MailManager.php30
-rw-r--r--lib/Service/PreprocessingService.php85
-rw-r--r--tests/Unit/Job/PreviewEnhancementProcessingJobTest.php172
-rw-r--r--tests/Unit/Service/IMipServiceTest.php458
-rw-r--r--tests/Unit/Service/PreprocessingServiceTest.php130
18 files changed, 1425 insertions, 10 deletions
diff --git a/appinfo/info.xml b/appinfo/info.xml
index 6becb7c3e..96e009bac 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -35,6 +35,7 @@
<background-jobs>
<job>OCA\Mail\BackgroundJob\CleanupJob</job>
<job>OCA\Mail\BackgroundJob\OutboxWorkerJob</job>
+ <job>OCA\Mail\BackgroundJob\IMipMessageJob</job>
</background-jobs>
<repair-steps>
<post-migration>
diff --git a/lib/BackgroundJob/IMipMessageJob.php b/lib/BackgroundJob/IMipMessageJob.php
new file mode 100644
index 000000000..371645c91
--- /dev/null
+++ b/lib/BackgroundJob/IMipMessageJob.php
@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * @copyright 2022 Anna Larch <anna.larch@gmx.net>
+ *
+ * @author 2022 Anna Larch <anna.larch@gmx.net>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\Mail\BackgroundJob;
+
+use OCA\Mail\Service\IMipService;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\TimedJob;
+
+class IMipMessageJob extends TimedJob {
+ private IMipService $iMipService;
+
+ public function __construct(ITimeFactory $time,
+ IMipService $iMipService) {
+ parent::__construct($time);
+
+ // Run once per hour
+ $this->setInterval(60 * 60);
+ $this->iMipService = $iMipService;
+ }
+
+ protected function run($argument): void {
+ $this->iMipService->process();
+ }
+}
diff --git a/lib/BackgroundJob/PreviewEnhancementProcessingJob.php b/lib/BackgroundJob/PreviewEnhancementProcessingJob.php
new file mode 100644
index 000000000..6aaecc1f1
--- /dev/null
+++ b/lib/BackgroundJob/PreviewEnhancementProcessingJob.php
@@ -0,0 +1,96 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * @copyright Anna Larch <anna.larch@gmx.net>
+ *
+ * @author Anna Larch <anna.larch@gmx.net>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\Mail\BackgroundJob;
+
+use OCA\Mail\Service\AccountService;
+use OCA\Mail\Service\PreprocessingService;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\IJobList;
+use OCP\BackgroundJob\TimedJob;
+use OCP\IUserManager;
+use Psr\Log\LoggerInterface;
+use function sprintf;
+
+class PreviewEnhancementProcessingJob extends TimedJob {
+ private IUserManager $userManager;
+ private AccountService $accountService;
+ private LoggerInterface $logger;
+ private IJobList $jobList;
+ private PreprocessingService $preprocessingService;
+
+ public function __construct(ITimeFactory $time,
+ IUserManager $userManager,
+ AccountService $accountService,
+ PreprocessingService $preprocessingService,
+ LoggerInterface $logger,
+ IJobList $jobList) {
+ parent::__construct($time);
+
+ $this->userManager = $userManager;
+ $this->accountService = $accountService;
+ $this->logger = $logger;
+ $this->jobList = $jobList;
+ $this->preprocessingService = $preprocessingService;
+
+ $this->setInterval(3600);
+ $this->setTimeSensitivity(self::TIME_SENSITIVE);
+ }
+
+ /**
+ * @return void
+ */
+ public function run($argument) {
+ $accountId = (int)$argument['accountId'];
+
+ try {
+ $account = $this->accountService->findById($accountId);
+ } catch (DoesNotExistException $e) {
+ $this->logger->debug('Could not find account <' . $accountId . '> removing from jobs');
+ $this->jobList->remove(self::class, $argument);
+ return;
+ }
+
+ $user = $this->userManager->get($account->getUserId());
+ if ($user === null || !$user->isEnabled()) {
+ $this->logger->debug(sprintf(
+ 'Account %d of user %s could not be found or was disabled, skipping preprocessing of messages',
+ $account->getId(),
+ $account->getUserId()
+ ));
+ return;
+ }
+
+ $dbAccount = $account->getMailAccount();
+ if (!is_null($dbAccount->getProvisioningId()) && $dbAccount->getInboundPassword() === null) {
+ $this->logger->info("Ignoring preprocessing job for provisioned account that has no password set yet");
+ return;
+ }
+
+ $limitTimestamp = $this->time->getTime() - (60 * 60 * 24 * 14); // Two weeks into the past
+ $this->preprocessingService->process($limitTimestamp, $account);
+ }
+}
diff --git a/lib/Db/MailboxMapper.php b/lib/Db/MailboxMapper.php
index 52e0b814c..d8c2ce895 100644
--- a/lib/Db/MailboxMapper.php
+++ b/lib/Db/MailboxMapper.php
@@ -35,6 +35,7 @@ use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\AppFramework\Db\QBMapper;
use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\DB\QueryBuilder\IQueryFunction;
use OCP\IDBConnection;
@@ -129,6 +130,23 @@ class MailboxMapper extends QBMapper {
}
/**
+ * @return Mailbox[]
+ *
+ * @throws Exception
+ */
+ public function findByIds(array $ids): array {
+ $qb = $this->db->getQueryBuilder();
+
+ $select = $qb->select('*')
+ ->from($this->getTableName())
+ ->where(
+ $qb->expr()->in('id', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY), IQueryBuilder::PARAM_INT_ARRAY)
+ );
+ return $this->findEntities($select);
+ }
+
+
+ /**
* @param int $id
* @param string $uid
*
diff --git a/lib/Db/Message.php b/lib/Db/Message.php
index c01f7650b..d72ad5403 100644
--- a/lib/Db/Message.php
+++ b/lib/Db/Message.php
@@ -76,6 +76,12 @@ use function json_encode;
* @method null|string getPreviewText()
* @method void setUpdatedAt(int $time)
* @method int getUpdatedAt()
+ * @method bool isImipMessage()
+ * @method void setImipMessage(bool $imipMessage)
+ * @method bool isImipProcessed()
+ * @method void setImipProcessed(bool $imipProcessed)
+ * @method bool isImipError()
+ * @method void setImipError(bool $imipError)
*/
class Message extends Entity implements JsonSerializable {
private const MUTABLE_FLAGS = [
@@ -114,6 +120,9 @@ class Message extends Entity implements JsonSerializable {
protected $flagImportant = false;
protected $flagMdnsent;
protected $previewText;
+ protected $imipMessage = false;
+ protected $imipProcessed = false;
+ protected $imipError = false;
/** @var AddressList */
private $from;
@@ -152,6 +161,9 @@ class Message extends Entity implements JsonSerializable {
$this->addType('flagImportant', 'boolean');
$this->addType('flagMdnsent', 'boolean');
$this->addType('updatedAt', 'integer');
+ $this->addType('imipMessage', 'boolean');
+ $this->addType('imipProcessed', 'boolean');
+ $this->addType('imipError', 'boolean');
}
/**
@@ -316,6 +328,7 @@ class Message extends Entity implements JsonSerializable {
'inReplyTo' => $this->getInReplyTo(),
'references' => empty($this->getReferences()) ? null: json_decode($this->getReferences(), true),
'threadRootId' => $this->getThreadRootId(),
+ 'imipMessage' => $this->isImipMessage(),
'previewText' => $this->getPreviewText(),
];
}
diff --git a/lib/Db/MessageMapper.php b/lib/Db/MessageMapper.php
index 6c6de1b20..5d62e9093 100644
--- a/lib/Db/MessageMapper.php
+++ b/lib/Db/MessageMapper.php
@@ -312,7 +312,6 @@ class MessageMapper extends QBMapper {
$qb1->setParameter('flag_notjunk', $message->getFlagNotjunk(), IQueryBuilder::PARAM_BOOL);
$qb1->setParameter('flag_important', $message->getFlagImportant(), IQueryBuilder::PARAM_BOOL);
$qb1->setParameter('flag_mdnsent', $message->getFlagMdnsent(), IQueryBuilder::PARAM_BOOL);
-
$qb1->execute();
$messageId = $qb1->getLastInsertId();
@@ -482,6 +481,7 @@ class MessageMapper extends QBMapper {
->set('preview_text', $query->createParameter('preview_text'))
->set('structure_analyzed', $query->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))
->set('updated_at', $query->createNamedParameter($this->timeFactory->getTime(), IQueryBuilder::PARAM_INT))
+ ->set('imip_message', $query->createParameter('imip_message'))
->where($query->expr()->andX(
$query->expr()->eq('uid', $query->createParameter('uid')),
$query->expr()->eq('mailbox_id', $query->createParameter('mailbox_id'))
@@ -505,6 +505,7 @@ class MessageMapper extends QBMapper {
$previewText,
$previewText === null ? IQueryBuilder::PARAM_NULL : IQueryBuilder::PARAM_STR
);
+ $query->setParameter('imip_message', $message->isImipMessage(), IQueryBuilder::PARAM_BOOL);
$query->execute();
}
@@ -520,6 +521,50 @@ class MessageMapper extends QBMapper {
return $messages;
}
+ /**
+ * @param Message ...$messages
+ *
+ * @return Message[]
+ */
+ public function updateImipData(Message ...$messages): array {
+ $this->db->beginTransaction();
+
+ try {
+ $query = $this->db->getQueryBuilder();
+ $query->update($this->getTableName())
+ ->set('imip_message', $query->createParameter('imip_message'))
+ ->set('imip_error', $query->createParameter('imip_error'))
+ ->set('imip_processed', $query->createParameter('imip_processed'))
+ ->where($query->expr()->andX(
+ $query->expr()->eq('uid', $query->createParameter('uid')),
+ $query->expr()->eq('mailbox_id', $query->createParameter('mailbox_id'))
+ ));
+
+ foreach ($messages as $message) {
+ if (empty($message->getUpdatedFields())) {
+ // Micro optimization
+ continue;
+ }
+
+ $query->setParameter('uid', $message->getUid(), IQueryBuilder::PARAM_INT);
+ $query->setParameter('mailbox_id', $message->getMailboxId(), IQueryBuilder::PARAM_INT);
+ $query->setParameter('imip_message', $message->isImipMessage(), IQueryBuilder::PARAM_BOOL);
+ $query->setParameter('imip_error', $message->isImipError(), IQueryBuilder::PARAM_BOOL);
+ $query->setParameter('imip_processed', $message->isImipProcessed(), IQueryBuilder::PARAM_BOOL);
+ $query->execute();
+ }
+
+ $this->db->commit();
+ } catch (Throwable $e) {
+ // Make sure to always roll back, otherwise the outer code runs in a failed transaction
+ $this->db->rollBack();
+
+ throw $e;
+ }
+
+ return $messages;
+ }
+
public function resetPreviewDataFlag(): void {
$qb = $this->db->getQueryBuilder();
$update = $qb->update($this->getTableName())
@@ -1232,4 +1277,45 @@ class MessageMapper extends QBMapper {
);
return $update->execute();
}
+
+ /**
+ * Get all iMIP messages from the last two weeks
+ * that haven't been processed yet
+ * @return Message[]
+ */
+ public function findIMipMessagesAscending(): array {
+ $time = $this->timeFactory->getTime() - 60 * 60 * 24 * 14;
+ $qb = $this->db->getQueryBuilder();
+
+ $select = $qb->select('*')
+ ->from($this->getTableName())
+ ->where(
+ $qb->expr()->eq('imip_message', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL), IQueryBuilder::PARAM_BOOL),
+ $qb->expr()->eq('imip_processed', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL), IQueryBuilder::PARAM_BOOL),
+ $qb->expr()->eq('imip_error', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL), IQueryBuilder::PARAM_BOOL),
+ $qb->expr()->eq('flag_junk', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL), IQueryBuilder::PARAM_BOOL),
+ $qb->expr()->gt('sent_at', $qb->createNamedParameter($time, IQueryBuilder::PARAM_INT)),
+ )->orderBy('sent_at', 'ASC'); // make sure we don't process newer messages first
+
+ return $this->findEntities($select);
+ }
+
+ /**
+ * @return Message[]
+ *
+ * @throws \OCP\DB\Exception
+ */
+ public function getUnanalyzed(int $lastRun, array $mailboxIds): array {
+ $qb = $this->db->getQueryBuilder();
+
+ $select = $qb->select('*')
+ ->from($this->getTableName())
+ ->where(
+ $qb->expr()->lte('sent_at', $qb->createNamedParameter($lastRun. IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
+ $qb->expr()->eq('structure_analyzed', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL), IQueryBuilder::PARAM_BOOL),
+ $qb->expr()->in('mailbox_id', $qb->createNamedParameter($mailboxIds, IQueryBuilder::PARAM_INT_ARRAY), IQueryBuilder::PARAM_INT_ARRAY),
+ )->orderBy('sent_at', 'ASC');
+
+ return $this->findEntities($select);
+ }
}
diff --git a/lib/IMAP/MessageMapper.php b/lib/IMAP/MessageMapper.php
index d70e6036d..b3a10bd57 100644
--- a/lib/IMAP/MessageMapper.php
+++ b/lib/IMAP/MessageMapper.php
@@ -665,6 +665,10 @@ class MessageMapper {
array $uids): array {
$structureQuery = new Horde_Imap_Client_Fetch_Query();
$structureQuery->structure();
+ $structureQuery->headerText([
+ 'cache' => true,
+ 'peek' => true,
+ ]);
$structures = $client->fetch($mailbox, $structureQuery, [
'ids' => new Horde_Imap_Client_Ids($uids),
@@ -673,19 +677,28 @@ class MessageMapper {
return array_map(function (Horde_Imap_Client_Data_Fetch $fetchData) use ($mailbox, $client) {
$hasAttachments = false;
$text = '';
+ $isImipMessage = false;
$structure = $fetchData->getStructure();
- foreach ($structure as $part) {
- if ($part instanceof Horde_Mime_Part && $part->isAttachment()) {
+ /** @var Horde_Mime_Part $part */
+ foreach ($structure->getParts() as $part) {
+ if ($part->isAttachment()) {
$hasAttachments = true;
- break;
+ }
+ $bodyParts = $part->getParts();
+ /** @var Horde_Mime_Part $bodyPart */
+ foreach ($bodyParts as $bodyPart) {
+ $contentParameters = $bodyPart->getAllContentTypeParameters();
+ if ($bodyPart->getType() === 'text/calendar' && isset($contentParameters['method'])) {
+ $isImipMessage = true;
+ }
}
}
$textBodyId = $structure->findBody() ?? $structure->findBody('text');
$htmlBodyId = $structure->findBody('html');
if ($textBodyId === null && $htmlBodyId === null) {
- return new MessageStructureData($hasAttachments, $text);
+ return new MessageStructureData($hasAttachments, $text, $isImipMessage);
}
if ($htmlBodyId !== null) {
$partsQuery = new Horde_Imap_Client_Fetch_Query();
@@ -720,7 +733,7 @@ class MessageMapper {
}
$structure->setContents($htmlBody);
$html = new Html2Text($structure->getContents());
- return new MessageStructureData($hasAttachments, trim($html->getText()));
+ return new MessageStructureData($hasAttachments, trim($html->getText()), $isImipMessage);
}
$textBody = $part->getBodyPart($textBodyId);
if (!empty($textBody)) {
@@ -728,12 +741,12 @@ class MessageMapper {
if ($enc = $mimeHeaders->getValue('content-transfer-encoding')) {
$structure->setTransferEncoding($enc);
$structure->setContents($textBody);
- return new MessageStructureData($hasAttachments, $structure->getContents());
+ return new MessageStructureData($hasAttachments, $structure->getContents(), $isImipMessage);
}
- return new MessageStructureData($hasAttachments, $textBody);
+ return new MessageStructureData($hasAttachments, $textBody, $isImipMessage);
}
- return new MessageStructureData($hasAttachments, $text);
+ return new MessageStructureData($hasAttachments, $text, $isImipMessage);
}, iterator_to_array($structures->getIterator()));
}
}
diff --git a/lib/IMAP/MessageStructureData.php b/lib/IMAP/MessageStructureData.php
index 946649703..3c092a237 100644
--- a/lib/IMAP/MessageStructureData.php
+++ b/lib/IMAP/MessageStructureData.php
@@ -32,10 +32,15 @@ class MessageStructureData {
/** @var string */
private $previewText;
+ /** @var bool */
+ private $isImipMessage;
+
public function __construct(bool $hasAttachments,
- string $previewText) {
+ string $previewText,
+ bool $isImipMessage) {
$this->hasAttachments = $hasAttachments;
$this->previewText = $previewText;
+ $this->isImipMessage = $isImipMessage;
}
public function hasAttachments(): bool {
@@ -45,4 +50,8 @@ class MessageStructureData {
public function getPreviewText(): string {
return $this->previewText;
}
+
+ public function isImipMessage(): bool {
+ return $this->isImipMessage;
+ }
}
diff --git a/lib/IMAP/PreviewEnhancer.php b/lib/IMAP/PreviewEnhancer.php
index 23b0ddbef..a8c2cd531 100644
--- a/lib/IMAP/PreviewEnhancer.php
+++ b/lib/IMAP/PreviewEnhancer.php
@@ -109,6 +109,7 @@ class PreviewEnhancer {
$message->setFlagAttachments($structureData->hasAttachments());
$message->setPreviewText($structureData->getPreviewText());
$message->setStructureAnalyzed(true);
+ $message->setImipMessage($structureData->isImipMessage());
return $message;
}, $messages));
diff --git a/lib/Migration/FixBackgroundJobs.php b/lib/Migration/FixBackgroundJobs.php
index 121fe601d..403b8afe1 100644
--- a/lib/Migration/FixBackgroundJobs.php
+++ b/lib/Migration/FixBackgroundJobs.php
@@ -25,6 +25,7 @@ declare(strict_types=1);
namespace OCA\Mail\Migration;
+use OCA\Mail\BackgroundJob\PreviewEnhancementProcessingJob;
use OCA\Mail\BackgroundJob\SyncJob;
use OCA\Mail\BackgroundJob\TrainImportanceClassifierJob;
use OCA\Mail\Db\MailAccount;
@@ -59,6 +60,7 @@ class FixBackgroundJobs implements IRepairStep {
foreach ($accounts as $account) {
$this->jobList->add(SyncJob::class, ['accountId' => $account->getId()]);
$this->jobList->add(TrainImportanceClassifierJob::class, ['accountId' => $account->getId()]);
+ $this->jobList->add(PreviewEnhancementProcessingJob::class, ['accountId' => $account->getId()]);
$output->advance();
}
$output->finishProgress();
diff --git a/lib/Migration/Version2000Date20220908130842.php b/lib/Migration/Version2000Date20220908130842.php
new file mode 100644
index 000000000..133f2074b
--- /dev/null
+++ b/lib/Migration/Version2000Date20220908130842.php
@@ -0,0 +1,66 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2022 Anna Larch <anna.larch@gmx.net>
+ *
+ * @author Anna Larch <anna.larch@gmx.net>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\Mail\Migration;
+
+use Closure;
+use OCP\DB\ISchemaWrapper;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+class Version2000Date20220908130842 extends SimpleMigrationStep {
+ /**
+ * @param IOutput $output
+ * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
+ * @param array $options
+ * @return null|ISchemaWrapper
+ */
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
+ /** @var ISchemaWrapper $schema */
+ $schema = $schemaClosure();
+
+ $messagesTable = $schema->getTable('mail_messages');
+ if (!$messagesTable->hasColumn('imip_message')) {
+ $messagesTable->addColumn('imip_message', 'boolean', [
+ 'notnull' => false,
+ 'default' => false,
+ ]);
+ }
+ if (!$messagesTable->hasColumn('imip_processed')) {
+ $messagesTable->addColumn('imip_processed', 'boolean', [
+ 'notnull' => false,
+ 'default' => false,
+ ]);
+ }
+ if (!$messagesTable->hasColumn('imip_error')) {
+ $messagesTable->addColumn('imip_error', 'boolean', [
+ 'notnull' => false,
+ 'default' => false,
+ ]);
+ }
+ return $schema;
+ }
+}
diff --git a/lib/Model/IMAPMessage.php b/lib/Model/IMAPMessage.php
index 95ac594c8..c639bc8f1 100644
--- a/lib/Model/IMAPMessage.php
+++ b/lib/Model/IMAPMessage.php
@@ -733,6 +733,22 @@ class IMAPMessage implements IMessage, JsonSerializable {
}
/**
+ * @return AddressList
+ */
+ public function getReplyTo() {
+ return AddressList::fromHorde($this->getEnvelope()->reply_to);
+ }
+
+ /**
+ * @param string $id
+ *
+ * @return void
+ */
+ public function setReplyTo(string $id) {
+ throw new Exception('not implemented');
+ }
+
+ /**
* Cast all values from an IMAP message into the correct DB format
*
* @param integer $mailboxId
@@ -779,6 +795,9 @@ class IMAPMessage implements IMessage, JsonSerializable {
$msg->setFlagImportant(in_array('$important', $flags, true) || in_array('$labelimportant', $flags, true) || in_array(Tag::LABEL_IMPORTANT, $flags, true));
$msg->setFlagAttachments(false);
$msg->setFlagMdnsent(in_array(Horde_Imap_Client::FLAG_MDNSENT, $flags, true));
+ if (!empty($this->scheduling)) {
+ $msg->setImipMessage(true);
+ }
$allowed = [
Horde_Imap_Client::FLAG_ANSWERED,
diff --git a/lib/Service/IMipService.php b/lib/Service/IMipService.php
new file mode 100644
index 000000000..100547513
--- /dev/null
+++ b/lib/Service/IMipService.php
@@ -0,0 +1,169 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * Mail App
+ *
+ * @copyright 2022 Anna Larch <anna.larch@gmx.net>
+ *
+ * @author Anna Larch <anna.larch@gmx.net>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\Mail\Service;
+
+use OCA\Mail\Account;
+use OCA\Mail\Db\Mailbox;
+use OCA\Mail\Db\MailboxMapper;
+use OCA\Mail\Db\Message;
+use OCA\Mail\Db\MessageMapper;
+use OCA\Mail\Exception\ServiceException;
+use OCA\Mail\Model\IMAPMessage;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\Calendar\IManager;
+use Psr\Log\LoggerInterface;
+
+class IMipService {
+ private AccountService $accountService;
+ private IManager $calendarManager;
+ private LoggerInterface $logger;
+ private MailboxMapper $mailboxMapper;
+ private MailManager $mailManager;
+ private MessageMapper $messageMapper;
+
+ public function __construct(
+ AccountService $accountService,
+ IManager $manager,
+ LoggerInterface $logger,
+ MailboxMapper $mailboxMapper,
+ MailManager $mailManager,
+ MessageMapper $messageMapper
+ ) {
+ $this->accountService = $accountService;
+ $this->calendarManager = $manager;
+ $this->logger = $logger;
+ $this->mailboxMapper = $mailboxMapper;
+ $this->mailManager = $mailManager;
+ $this->messageMapper = $messageMapper;
+ }
+
+ public function process(): void {
+ $messages = $this->messageMapper->findIMipMessagesAscending();
+ if (empty($messages)) {
+ $this->logger->info('No iMIP messages to process.');
+ return;
+ }
+
+ // Collect all mailboxes in memory
+ // possible perf improvement - make this one IN query
+ // and JOIN with accounts table
+ // although this might not make much of a difference
+ // since there are very few messages to process
+ $mailboxIds = array_unique(array_map(function (Message $message) {
+ return $message->getMailboxId();
+ }, $messages));
+
+ $mailboxes = array_map(function (int $mailboxId) {
+ try {
+ return $this->mailboxMapper->findById($mailboxId);
+ } catch (DoesNotExistException | ServiceException $e) {
+ return null;
+ }
+ }, $mailboxIds);
+
+ // Collect all accounts in memory
+ $accountIds = array_unique(array_map(function (Mailbox $mailbox) {
+ return $mailbox->getAccountId();
+ }, $mailboxes));
+
+ $accounts = array_combine($accountIds, array_map(function (int $accountId) {
+ try {
+ return $this->accountService->findById($accountId);
+ } catch (DoesNotExistException $e) {
+ return null;
+ }
+ }, $accountIds));
+
+ /** @var Mailbox $mailbox */
+ foreach ($mailboxes as $mailbox) {
+ /** @var Account $account */
+ $account = $accounts[$mailbox->getAccountId()];
+ $filteredMessages = array_filter($messages, function ($message) use ($mailbox) {
+ return $message->getMailboxId() === $mailbox->getId();
+ });
+
+ if (empty($filteredMessages)) {
+ continue;
+ }
+
+ // Check for accounts or mailboxes that no longer exist,
+ // no processing for drafts, sent items, junk or archive
+ if ($account === null
+ || $account->getMailAccount()->getTrashMailboxId() === $mailbox->getId()
+ || $account->getMailAccount()->getSentMailboxId() === $mailbox->getId()
+ || $account->getMailAccount()->getDraftsMailboxId() === $mailbox->getId()
+ || $mailbox->isSpecialUse(\Horde_Imap_Client::SPECIALUSE_ARCHIVE)
+ ) {
+ $processedMessages = array_map(function (Message $message) {
+ $message->setImipProcessed(true);
+ return $message;
+ }, $filteredMessages); // Silently drop from passing to DAV and mark as processed, so we won't run into these messages again.
+ $this->messageMapper->updateImipData(...$processedMessages);
+ continue;
+ }
+
+ try {
+ $imapMessages = $this->mailManager->getImapMessagesForScheduleProcessing($account, $mailbox, array_map(function ($message) {
+ return $message->getUid();
+ }, $filteredMessages));
+ } catch (ServiceException $e) {
+ $this->logger->error('Could not get IMAP messages form IMAP server', ['exception' => $e]);
+ continue;
+ }
+
+ foreach ($filteredMessages as $message) {
+ /** @var IMAPMessage $imapMessage */
+ $imapMessage = current(array_filter($imapMessages, function (IMAPMessage $imapMessage) use ($message) {
+ return $message->getUid() === $imapMessage->getUid();
+ }));
+ if (empty($imapMessage->scheduling)) {
+ // No scheduling info, maybe the DB is wrong
+ $message->setImipError(true);
+ continue;
+ }
+
+ $principalUri = 'principals/users/' . $account->getUserId();
+ $sender = $imapMessage->getFrom()->first()->getEmail();
+ $recipient = $account->getEmail();
+ foreach ($imapMessage->scheduling as $schedulingInfo) { // an IMAP message could contain more than one iMIP object
+ if ($schedulingInfo['method'] === 'REPLY') {
+ $processed = $this->calendarManager->handleIMipReply($principalUri, $sender, $recipient, $schedulingInfo['contents']);
+ $message->setImipProcessed($processed);
+ $message->setImipError(!$processed);
+ } elseif ($schedulingInfo['method'] === 'CANCEL') {
+ $replyTo = $imapMessage->getReplyTo()->first();
+ $replyTo = !empty($replyTo) ? $replyTo->getEmail() : null;
+ $processed = $this->calendarManager->handleIMipCancel($principalUri, $sender, $replyTo, $recipient, $schedulingInfo['contents']);
+ $message->setImipProcessed($processed);
+ $message->setImipError(!$processed);
+ }
+ }
+ }
+ $this->messageMapper->updateImipData(...$filteredMessages);
+ }
+ }
+}
diff --git a/lib/Service/MailManager.php b/lib/Service/MailManager.php
index 883d98554..cbe5229d4 100644
--- a/lib/Service/MailManager.php
+++ b/lib/Service/MailManager.php
@@ -26,6 +26,7 @@ namespace OCA\Mail\Service;
use Horde_Imap_Client;
use Horde_Imap_Client_Exception;
use Horde_Imap_Client_Exception_NoSupportExtension;
+use Horde_Imap_Client_Ids;
use Horde_Imap_Client_Socket;
use OCA\Mail\Account;
use OCA\Mail\Contracts\IMailManager;
@@ -193,6 +194,35 @@ class MailManager implements IMailManager {
}
}
+ /**
+ * @param Account $account
+ * @param Mailbox $mailbox
+ * @param int[] $uids
+ * @return IMAPMessage[]
+ * @throws ServiceException
+ */
+ public function getImapMessagesForScheduleProcessing(Account $account,
+ Mailbox $mailbox,
+ array $uids): array {
+ $client = $this->imapClientFactory->getClient($account);
+ try {
+ return $this->imapMessageMapper->findByIds(
+ $client,
+ $mailbox->getName(),
+ new Horde_Imap_Client_Ids($uids),
+ true
+ );
+ } catch (Horde_Imap_Client_Exception $e) {
+ throw new ServiceException(
+ 'Could not load messages: ' . $e->getMessage(),
+ (int)$e->getCode(),
+ $e
+ );
+ } finally {
+ $client->logout();
+ }
+ }
+
public function getThread(Account $account, string $threadRootId): array {
return $this->dbMessageMapper->findThread($account, $threadRootId);
}
diff --git a/lib/Service/PreprocessingService.php b/lib/Service/PreprocessingService.php
new file mode 100644
index 000000000..e37a51ad2
--- /dev/null
+++ b/lib/Service/PreprocessingService.php
@@ -0,0 +1,85 @@
+<?php
+
+declare(strict_types=1);
+/*
+ * *
+ * * {$app} App
+ * *
+ * * @copyright 2022 Anna Larch <anna.larch@gmx.net>
+ * *
+ * * @author Anna Larch <anna.larch@gmx.net>
+ * *
+ * * This library is free software; you can redistribute it and/or
+ * * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * * License as published by the Free Software Foundation; either
+ * * version 3 of the License, or any later version.
+ * *
+ * * This library is distributed in the hope that it will be useful,
+ * * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ * *
+ * * You should have received a copy of the GNU Affero General Public
+ * * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ * *
+ *
+ */
+
+namespace OCA\Mail\Service;
+
+use OCA\Mail\Account;
+use OCA\Mail\Db\Mailbox;
+use OCA\Mail\Db\MailboxMapper;
+use OCA\Mail\Db\MessageMapper;
+use OCA\Mail\IMAP\PreviewEnhancer;
+use Psr\Log\LoggerInterface;
+
+class PreprocessingService {
+ private MailboxMapper $mailboxMapper;
+ private MessageMapper $messageMapper;
+ private LoggerInterface $logger;
+ private PreviewEnhancer $previewEnhancer;
+
+ public function __construct(
+ MessageMapper $messageMapper,
+ LoggerInterface $logger,
+ MailboxMapper $mailboxMapper,
+ PreviewEnhancer $previewEnhancer
+ ) {
+ $this->messageMapper = $messageMapper;
+ $this->logger = $logger;
+ $this->mailboxMapper = $mailboxMapper;
+ $this->previewEnhancer = $previewEnhancer;
+ }
+
+ public function process(int $limitTimestamp, Account $account): void {
+ $mailboxes = $this->mailboxMapper->findAll($account);
+ if (empty($mailboxes)) {
+ $this->logger->debug('No mailboxes found.');
+ return;
+ }
+ $mailboxIds = array_unique(array_map(function (Mailbox $mailbox) {
+ return $mailbox->getId();
+ }, $mailboxes));
+
+
+ $messages = $this->messageMapper->getUnanalyzed($limitTimestamp, $mailboxIds);
+ if (empty($messages)) {
+ $this->logger->debug('No structure data to analyse.');
+ return;
+ }
+
+ foreach ($mailboxes as $mailbox) {
+ $filteredMessages = array_filter($messages, function ($message) use ($mailbox) {
+ return $message->getMailboxId() === $mailbox->getId();
+ });
+
+ if (empty($filteredMessages)) {
+ continue;
+ }
+
+ $processedMessages = $this->previewEnhancer->process($account, $mailbox, $filteredMessages);
+ $this->logger->debug('Processed ' . count($processedMessages) . ' messages for structure data for mailbox ' . $mailbox->getId());
+ }
+ }
+}
diff --git a/tests/Unit/Job/PreviewEnhancementProcessingJobTest.php b/tests/Unit/Job/PreviewEnhancementProcessingJobTest.php
new file mode 100644
index 000000000..e9dfd5755
--- /dev/null
+++ b/tests/Unit/Job/PreviewEnhancementProcessingJobTest.php
@@ -0,0 +1,172 @@
+<?php
+/*
+ * *
+ * * Mail App
+ * *
+ * * @copyright 2022 Anna Larch <anna.larch@gmx.net>
+ * *
+ * * @author Anna Larch <anna.larch@gmx.net>
+ * *
+ * * This library is free software; you can redistribute it and/or
+ * * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * * License as published by the Free Software Foundation; either
+ * * version 3 of the License, or any later version.
+ * *
+ * * This library is distributed in the hope that it will be useful,
+ * * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ * *
+ * * You should have received a copy of the GNU Affero General Public
+ * * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ * *
+ *
+ */
+
+namespace OCA\Mail\Tests\Unit\Job;
+
+use OCA\Mail\Account;
+use OCA\Mail\BackgroundJob\PreviewEnhancementProcessingJob;
+use OCA\Mail\Db\MailAccount;
+use OCA\Mail\Service\AccountService;
+use OCA\Mail\Service\PreprocessingService;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\IJobList;
+use OCP\IUser;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use ChristophWurst\Nextcloud\Testing\TestCase;
+use Psr\Log\LoggerInterface;
+
+class PreviewEnhancementProcessingJobTest extends TestCase {
+ /** @var ITimeFactory|ITimeFactory&MockObject|MockObject */
+ private $time;
+
+ /** @var IUserManager|IUserManager&MockObject|MockObject */
+ private $manager;
+
+ /** @var AccountService|AccountService&MockObject|MockObject */
+ private $accountService;
+
+ /** @var PreprocessingService|PreprocessingService&MockObject|MockObject */
+ private $preprocessingService;
+
+ /** @var MockObject|LoggerInterface|LoggerInterface&MockObject */
+ private $logger;
+
+ /** @var IJobList|IJobList&MockObject|MockObject */
+ private $jobList;
+
+ /** @var int[] */
+ private static $argument;
+
+ public function setUp(): void {
+ parent::setUp();
+ $this->time = $this->createMock(ITimeFactory::class);
+ $this->manager = $this->createMock(IUserManager::class);
+ $this->accountService = $this->createMock(AccountService::class);
+ $this->preprocessingService = $this->createMock(PreprocessingService::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->jobList = $this->createMock(IJobList::class);
+ $this->job = new PreviewEnhancementProcessingJob(
+ $this->time,
+ $this->manager,
+ $this->accountService,
+ $this->preprocessingService,
+ $this->logger,
+ $this->jobList
+ );
+
+ self::$argument = ['accountId' => 1];
+ }
+
+ public function testNoAccount(): void {
+ $this->accountService->expects(self::once())
+ ->method('findById')
+ ->with(self::$argument['accountId'])
+ ->willThrowException(new DoesNotExistException('Account does not exist'));
+ $this->logger->expects(self::once())
+ ->method('debug');
+ $this->jobList->expects(self::once())
+ ->method('remove');
+
+ $this->job->run(self::$argument);
+ }
+
+ public function testNoUser(): void {
+ $mailAccount = new MailAccount();
+ $mailAccount->setUserId(1);
+ $account = new Account($mailAccount);
+
+ $this->accountService->expects(self::once())
+ ->method('findById')
+ ->with(self::$argument['accountId'])
+ ->willReturn($account);
+ $this->manager->expects(self::once())
+ ->method('get')
+ ->with($account->getUserId())
+ ->willReturn(null);
+ $this->logger->expects(self::once())
+ ->method('debug');
+
+ $this->job->run(self::$argument);
+ }
+
+ public function testProvisionedNoPassword(): void {
+ $mailAccount = new MailAccount();
+ $mailAccount->setUserId(1);
+ $mailAccount->setProvisioningId(1);
+ $mailAccount->setInboundPassword(null);
+ $account = new Account($mailAccount);
+ $user = $this->createMock(IUser::class);
+ $user->setEnabled();
+
+ $this->accountService->expects(self::once())
+ ->method('findById')
+ ->with(self::$argument['accountId'])
+ ->willReturn($account);
+ $this->manager->expects(self::once())
+ ->method('get')
+ ->with($account->getUserId())
+ ->willReturn($user);
+ $user->expects(self::once())
+ ->method('isEnabled')
+ ->willReturn(true);
+ $this->logger->expects(self::once())
+ ->method('info');
+
+ $this->job->run(self::$argument);
+ }
+
+ public function testProcessing(): void {
+ $mailAccount = new MailAccount();
+ $mailAccount->setUserId(1);
+ $account = new Account($mailAccount);
+ $time = time();
+ $user = $this->createMock(IUser::class);
+ $user->setEnabled();
+
+ $this->accountService->expects(self::once())
+ ->method('findById')
+ ->with(self::$argument['accountId'])
+ ->willReturn($account);
+ $this->manager->expects(self::once())
+ ->method('get')
+ ->with($account->getUserId())
+ ->willReturn($user);
+ $user->expects(self::once())
+ ->method('isEnabled')
+ ->willReturn(true);
+ $this->time->expects(self::once())
+ ->method('getTime')
+ ->willReturn($time);
+ $this->preprocessingService->expects(self::once())
+ ->method('process')
+ ->with(($time - (60 * 60 * 24 * 14)), $account);
+ $this->logger->expects(self::never())
+ ->method('error');
+
+ $this->job->run(self::$argument);
+ }
+}
diff --git a/tests/Unit/Service/IMipServiceTest.php b/tests/Unit/Service/IMipServiceTest.php
new file mode 100644
index 000000000..2f39b5b2c
--- /dev/null
+++ b/tests/Unit/Service/IMipServiceTest.php
@@ -0,0 +1,458 @@
+<?php
+/*
+ * *
+ * * {$app} App
+ * *
+ * * @copyright 2022 Anna Larch <anna.larch@gmx.net>
+ * *
+ * * @author Anna Larch <anna.larch@gmx.net>
+ * *
+ * * This library is free software; you can redistribute it and/or
+ * * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * * License as published by the Free Software Foundation; either
+ * * version 3 of the License, or any later version.
+ * *
+ * * This library is distributed in the hope that it will be useful,
+ * * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ * *
+ * * You should have received a copy of the GNU Affero General Public
+ * * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ * *
+ *
+ */
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2022 Anna Larch <anna.larch@gm.net>
+ *
+ * @author 2022 Anna Larch <anna.larch@gm.net>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\Mail\Tests\Service;
+
+use ChristophWurst\Nextcloud\Testing\TestCase;
+use OCA\Mail\Account;
+use OCA\Mail\Address;
+use OCA\Mail\AddressList;
+use OCA\Mail\Db\MailAccount;
+use OCA\Mail\Db\MailboxMapper;
+use OCA\Mail\Db\Message;
+use OCA\Mail\Db\MessageMapper;
+use OCA\Mail\Db\Mailbox;
+use OCA\Mail\Exception\ServiceException;
+use OCA\Mail\Model\IMAPMessage;
+use OCA\Mail\Service\AccountService;
+use OCA\Mail\Service\IMipService;
+use OCA\Mail\Service\MailManager;
+use OCP\Calendar\IManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+
+class IMipServiceTest extends TestCase {
+ /** @var MailboxMapper|MockObject */
+ private $mailboxMapper;
+
+ /** @var MessageMapper|MockObject */
+ private $messageMapper;
+
+ /** @var AccountService|MockObject */
+ private $accountService;
+
+ /** @var MailManager|MockObject */
+ private $mailManager;
+
+ /** @var MockObject|LoggerInterface */
+ private $logger;
+
+ private IMipService $service;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ // iMIP is NC25+
+ if (!method_exists(IManager::class, 'handleImipReply')) {
+ self::markTestIncomplete();
+ }
+
+
+ $this->accountService = $this->createMock(AccountService::class);
+ $this->calendarManager = $this->createMock(IManager::class);
+ $this->mailboxMapper = $this->createMock(MailboxMapper::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->mailManager = $this->createMock(MailManager::class);
+ $this->messageMapper = $this->createMock(MessageMapper::class);
+
+ $this->service = new IMipService(
+ $this->accountService,
+ $this->calendarManager,
+ $this->logger,
+ $this->mailboxMapper,
+ $this->mailManager,
+ $this->messageMapper
+ );
+ }
+
+ public function testNoSchedulingInformation(): void {
+ $this->messageMapper->expects(self::once())
+ ->method('findIMipMessagesAscending')
+ ->willReturn([]);
+ $this->logger->expects(self::once())
+ ->method('info');
+ $this->mailboxMapper->expects(self::never())
+ ->method('findById');
+ $this->accountService->expects(self::never())
+ ->method('findById');
+ $this->calendarManager->expects(self::never())
+ ->method('handleIMipReply');
+ $this->calendarManager->expects(self::never())
+ ->method('handleIMipCancel');
+ $this->messageMapper->expects(self::never())
+ ->method('updateImipData');
+
+ $this->service->process();
+ }
+
+ public function testIsSpecialUse(): void {
+ $message = new Message();
+ $message->setImipMessage(true);
+ $message->setUid(1);
+ $message->setMailboxId(100);
+ $mailbox = new Mailbox();
+ $mailbox->setId(100);
+ $mailbox->setAccountId(200);
+ $mailbox->setSpecialUse('["sent"]');
+ $mailAccount = new MailAccount();
+ $mailAccount->setDraftsMailboxId(100);
+ $account = new Account($mailAccount);
+
+ $this->messageMapper->expects(self::once())
+ ->method('findIMipMessagesAscending')
+ ->willReturn([$message]);
+ $this->mailboxMapper->expects(self::once())
+ ->method('findById')
+ ->with($message->getMailboxId())
+ ->willReturn($mailbox);
+ $this->accountService->expects(self::once())
+ ->method('findById')
+ ->with($mailbox->getAccountId())
+ ->willReturn($account);
+ $this->messageMapper->expects(self::once())
+ ->method('updateImipData');
+ $this->calendarManager->expects(self::never())
+ ->method('handleIMipReply');
+ $this->calendarManager->expects(self::never())
+ ->method('handleIMipCancel');
+
+ $this->service->process();
+ }
+
+ public function testIsArchive(): void {
+ $message = new Message();
+ $message->setImipMessage(true);
+ $message->setUid(1);
+ $message->setMailboxId(100);
+ $mailbox = new Mailbox();
+ $mailbox->setId(100);
+ $mailbox->setAccountId(200);
+ $mailbox->setSpecialUse('["archive"]');
+ $mailAccount = new MailAccount();
+ $account = new Account($mailAccount);
+
+ $this->messageMapper->expects(self::once())
+ ->method('findIMipMessagesAscending')
+ ->willReturn([$message]);
+ $this->mailboxMapper->expects(self::once())
+ ->method('findById')
+ ->with($message->getMailboxId())
+ ->willReturn($mailbox);
+ $this->accountService->expects(self::once())
+ ->method('findById')
+ ->with($mailbox->getAccountId())
+ ->willReturn($account);
+ $this->messageMapper->expects(self::once())
+ ->method('updateImipData');
+ $this->calendarManager->expects(self::never())
+ ->method('handleIMipReply');
+ $this->calendarManager->expects(self::never())
+ ->method('handleIMipCancel');
+
+ $this->service->process();
+ }
+
+ public function testNoSchedulingInfo(): void {
+ $message = new Message();
+ $message->setImipMessage(true);
+ $message->setUid(1);
+ $message->setMailboxId(100);
+ $mailbox = new Mailbox();
+ $mailbox->setId(100);
+ $mailbox->setAccountId(200);
+ $account = $this->createConfiguredMock(Account::class, [
+ 'getId' => 200,
+ 'getEmail' => 'dimitrius@stardew-science.com'
+ ]);
+ $imapMessage = $this->createConfiguredMock(IMAPMessage::class, [
+ 'getUid' => 1
+ ]);
+ $imapMessage->scheduling = [];
+
+ $this->messageMapper->expects(self::once())
+ ->method('findIMipMessagesAscending')
+ ->willReturn([$message]);
+ $this->mailboxMapper->expects(self::once())
+ ->method('findById')
+ ->willReturn($mailbox);
+ $this->accountService->expects(self::once())
+ ->method('findById')
+ ->willReturn($account);
+ $this->mailManager->expects(self::once())
+ ->method('getImapMessagesForScheduleProcessing')
+ ->with($account, $mailbox, [$message->getUid()])
+ ->willReturn([$imapMessage]);
+ $this->calendarManager->expects(self::never())
+ ->method('handleIMipReply');
+ $this->calendarManager->expects(self::never())
+ ->method('handleIMipCancel');
+ $this->messageMapper->expects(self::once())
+ ->method('updateImipData')
+ ->with($message);
+
+ $this->service->process();
+ }
+
+ public function testImapConnectionServiceException(): void {
+ $message = new Message();
+ $message->setImipMessage(true);
+ $message->setUid(1);
+ $message->setMailboxId(100);
+ $mailbox = new Mailbox();
+ $mailbox->setId(100);
+ $mailbox->setAccountId(200);
+ $account = $this->createConfiguredMock(Account::class, [
+ 'getId' => 200,
+ 'getEmail' => 'dimitrius@stardew-science.com'
+ ]);
+ $imapMessage = $this->createConfiguredMock(IMAPMessage::class, [
+ 'getUid' => 1
+ ]);
+ $imapMessage->scheduling = [];
+
+ $this->messageMapper->expects(self::once())
+ ->method('findIMipMessagesAscending')
+ ->willReturn([$message]);
+ $this->mailboxMapper->expects(self::once())
+ ->method('findById')
+ ->willReturn($mailbox);
+ $this->accountService->expects(self::once())
+ ->method('findById')
+ ->willReturn($account);
+ $this->mailManager->expects(self::once())
+ ->method('getImapMessagesForScheduleProcessing')
+ ->willThrowException(new ServiceException());
+ $this->logger->expects(self::once())
+ ->method('error');
+ $this->calendarManager->expects(self::never())
+ ->method('handleIMipReply');
+ $this->calendarManager->expects(self::never())
+ ->method('handleIMipCancel');
+ $this->messageMapper->expects(self::never())
+ ->method('updateImipData');
+
+ $this->service->process();
+ }
+
+ public function testIsRequest(): void {
+ $message = new Message();
+ $message->setImipMessage(true);
+ $message->setUid(1);
+ $message->setMailboxId(100);
+ $mailbox = new Mailbox();
+ $mailbox->setId(100);
+ $mailbox->setAccountId(200);
+ $mailAccount = new MailAccount();
+ $mailAccount->setId(200);
+ $account = new Account($mailAccount);
+ $imapMessage = $this->createMock(IMAPMessage::class);
+ $imapMessage->scheduling[] = ['method' => 'REQUEST'];
+ $addressList = $this->createMock(AddressList::class);
+ $address = $this->createMock(Address::class);
+
+ $this->messageMapper->expects(self::once())
+ ->method('findIMipMessagesAscending')
+ ->willReturn([$message]);
+ $this->mailboxMapper->expects(self::once())
+ ->method('findById')
+ ->willReturn($mailbox);
+ $this->accountService->expects(self::once())
+ ->method('findById')
+ ->willReturn($account);
+ $this->mailManager->expects(self::once())
+ ->method('getImapMessagesForScheduleProcessing')
+ ->with($account, $mailbox, [$message->getUid()])
+ ->willReturn([$imapMessage]);
+ $imapMessage->expects(self::once())
+ ->method('getUid')
+ ->willReturn(1);
+ $imapMessage->expects(self::once())
+ ->method('getFrom')
+ ->willReturn($addressList);
+ $addressList->expects(self::once())
+ ->method('first')
+ ->willReturn($address);
+ $address->expects(self::once())
+ ->method('getEmail')
+ ->willReturn('pam@stardew-bus-company.com');
+ $this->logger->expects(self::never())
+ ->method('info');
+ $this->calendarManager->expects(self::never())
+ ->method('handleIMipReply');
+ $this->calendarManager->expects(self::never())
+ ->method('handleIMipCancel');
+ $this->messageMapper->expects(self::never())
+ ->method('updateBulk');
+
+ $this->service->process();
+ }
+
+ public function testIsReply(): void {
+ $message = new Message();
+ $message->setImipMessage(true);
+ $message->setUid(1);
+ $message->setMailboxId(100);
+ $mailbox = new Mailbox();
+ $mailbox->setId(100);
+ $mailbox->setAccountId(200);
+ $mailAccount = new MailAccount();
+ $mailAccount->setId(200);
+ $mailAccount->setEmail('vincent@stardew-valley.edu');
+ $mailAccount->setUserId('vincent');
+ $account = new Account($mailAccount);
+ $imapMessage = $this->createMock(IMAPMessage::class);
+ $imapMessage->scheduling[] = ['method' => 'REPLY', 'contents' => 'VCARD'];
+ $addressList = $this->createMock(AddressList::class);
+ $address = $this->createMock(Address::class);
+
+ $this->messageMapper->expects(self::once())
+ ->method('findIMipMessagesAscending')
+ ->willReturn([$message]);
+ $this->mailboxMapper->expects(self::once())
+ ->method('findById')
+ ->willReturn($mailbox);
+ $this->accountService->expects(self::once())
+ ->method('findById')
+ ->willReturn($account);
+ $this->mailManager->expects(self::once())
+ ->method('getImapMessagesForScheduleProcessing')
+ ->with($account, $mailbox, [$message->getUid()])
+ ->willReturn([$imapMessage]);
+ $imapMessage->expects(self::once())
+ ->method('getUid')
+ ->willReturn(1);
+ $this->logger->expects(self::never())
+ ->method('info');
+ $imapMessage->expects(self::once())
+ ->method('getFrom')
+ ->willReturn($addressList);
+ $addressList->expects(self::once())
+ ->method('first')
+ ->willReturn($address);
+ $address->expects(self::once())
+ ->method('getEmail')
+ ->willReturn('pam@stardew-bus-service.com');
+ $imapMessage->expects(self::never())
+ ->method('getInReplyTo')
+ ->willReturn($addressList);
+ $this->calendarManager->expects(self::once())
+ ->method('handleIMipReply')
+ ->with('principals/users/vincent',
+ 'pam@stardew-bus-service.com',
+ $account->getEmail(),
+ $imapMessage->scheduling[0]['contents']);
+ $this->calendarManager->expects(self::never())
+ ->method('handleIMipCancel');
+ $this->messageMapper->expects(self::once())
+ ->method('updateImipData');
+
+ $this->service->process();
+ }
+
+ public function testIsCancel(): void {
+ $message = new Message();
+ $message->setImipMessage(true);
+ $message->setUid(1);
+ $message->setMailboxId(100);
+ $mailbox = new Mailbox();
+ $mailbox->setId(100);
+ $mailbox->setAccountId(200);
+ $mailAccount = new MailAccount();
+ $mailAccount->setId(200);
+ $mailAccount->setEmail('vincent@stardew-valley.edu');
+ $mailAccount->setUserId('vincent');
+ $account = new Account($mailAccount);
+ $imapMessage = $this->createMock(IMAPMessage::class);
+ $imapMessage->scheduling[] = ['method' => 'CANCEL', 'contents' => 'VCARD'];
+ $addressList = $this->createMock(AddressList::class);
+ $address = $this->createMock(Address::class);
+
+ $this->messageMapper->expects(self::once())
+ ->method('findIMipMessagesAscending')
+ ->willReturn([$message]);
+ $this->mailboxMapper->expects(self::once())
+ ->method('findById')
+ ->willReturn($mailbox);
+ $this->accountService->expects(self::once())
+ ->method('findById')
+ ->willReturn($account);
+ $this->mailManager->expects(self::once())
+ ->method('getImapMessagesForScheduleProcessing')
+ ->with($account, $mailbox, [$message->getUid()])
+ ->willReturn([$imapMessage]);
+ $imapMessage->expects(self::once())
+ ->method('getUid')
+ ->willReturn(1);
+ $this->logger->expects(self::never())
+ ->method('info');
+ $imapMessage->expects(self::once())
+ ->method('getFrom')
+ ->willReturn($addressList);
+ $addressList->expects(self::once())
+ ->method('first')
+ ->willReturn($address);
+ $address->expects(self::once())
+ ->method('getEmail')
+ ->willReturn('pam@stardew-bus-service.com');
+ $imapMessage->expects(self::once())
+ ->method('getReplyTo')
+ ->willReturn(new AddressList([]));
+ $this->calendarManager->expects(self::once())
+ ->method('handleIMipCancel')
+ ->with('principals/users/vincent',
+ 'pam@stardew-bus-service.com',
+ null,
+ $account->getEmail(),
+ $imapMessage->scheduling[0]['contents']
+ );
+ $this->messageMapper->expects(self::once())
+ ->method('updateImipData');
+
+ $this->service->process();
+ }
+}
diff --git a/tests/Unit/Service/PreprocessingServiceTest.php b/tests/Unit/Service/PreprocessingServiceTest.php
new file mode 100644
index 000000000..e181d9e0b
--- /dev/null
+++ b/tests/Unit/Service/PreprocessingServiceTest.php
@@ -0,0 +1,130 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2022 Anna Larch <anna.larch@gm.net>
+ *
+ * @author 2022 Anna Larch <anna.larch@gm.net>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\Mail\Tests\Service;
+
+use ChristophWurst\Nextcloud\Testing\TestCase;
+use OCA\Mail\Account;
+use OCA\Mail\Db\MailAccount;
+use OCA\Mail\Db\MailboxMapper;
+use OCA\Mail\Db\Message;
+use OCA\Mail\Db\MessageMapper;
+use OCA\Mail\Db\Mailbox;
+use OCA\Mail\IMAP\PreviewEnhancer;
+use OCA\Mail\Service\PreprocessingService;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+
+class PreprocessingServiceTest extends TestCase {
+ /** @var MailboxMapper|MockObject */
+ private $mailboxMapper;
+
+ /** @var MessageMapper|MockObject */
+ private $messageMapper;
+
+ /** @var MockObject|LoggerInterface */
+ private $logger;
+
+ /** @var MockObject|PreviewEnhancer */
+ private $previewEnhancer;
+
+ private PreprocessingService $service;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->mailboxMapper = $this->createMock(MailboxMapper::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->messageMapper = $this->createMock(MessageMapper::class);
+ $this->previewEnhancer = $this->createMock(PreviewEnhancer::class);
+
+ $this->service = new PreprocessingService(
+ $this->messageMapper,
+ $this->logger,
+ $this->mailboxMapper,
+ $this->previewEnhancer
+ );
+ }
+
+ public function testNoMailboxes(): void {
+ $account = new Account(new MailAccount());
+ $timestamp = 0;
+
+ $this->mailboxMapper->expects(self::once())
+ ->method('findAll')
+ ->with($account)
+ ->willReturn([]);
+ $this->messageMapper->expects(self::never())
+ ->method('getUnanalyzed');
+ $this->previewEnhancer->expects(self::never())
+ ->method('process');
+
+ $this->service->process($timestamp, $account);
+ }
+
+ public function testNoUnanalysed(): void {
+ $account = new Account(new MailAccount());
+ $timestamp = 0;
+ $mailbox = new Mailbox();
+ $mailbox->setId(1);
+
+ $this->mailboxMapper->expects(self::once())
+ ->method('findAll')
+ ->with($account)
+ ->willReturn([$mailbox]);
+ $this->messageMapper->expects(self::once())
+ ->method('getUnanalyzed')
+ ->with($timestamp, [$mailbox->getId()])
+ ->willReturn([]);
+ $this->previewEnhancer->expects(self::never())
+ ->method('process');
+
+ $this->service->process($timestamp, $account);
+ }
+
+ public function testProcessing(): void {
+ $account = new Account(new MailAccount());
+ $timestamp = 0;
+ $mailbox = new Mailbox();
+ $mailbox->setId(1);
+ $message = new Message();
+ $message->setMailboxId($mailbox->getId());
+
+ $this->mailboxMapper->expects(self::once())
+ ->method('findAll')
+ ->with($account)
+ ->willReturn([$mailbox]);
+ $this->messageMapper->expects(self::once())
+ ->method('getUnanalyzed')
+ ->with($timestamp, [$mailbox->getId()])
+ ->willReturn([$message]);
+ $this->previewEnhancer->expects(self::once())
+ ->method('process')
+ ->with($account, $mailbox, [$message])
+ ->willReturn([$message]);
+
+ $this->service->process($timestamp, $account);
+ }
+}