diff --git a/appinfo/info.xml b/appinfo/info.xml index fb9237f058..3bca47814c 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -34,7 +34,7 @@ The rating depends on the installed text processing backend. See [the rating ove Learn more about the Nextcloud Ethical AI Rating [in our blog](https://nextcloud.com/blog/nextcloud-ethical-ai-rating/). ]]> - 4.1.0-alpha.0 + 4.1.1-alpha.0 agpl Christoph Wurst GretaD diff --git a/lib/Db/Message.php b/lib/Db/Message.php index 3e4a01f76e..229c26f395 100644 --- a/lib/Db/Message.php +++ b/lib/Db/Message.php @@ -68,6 +68,8 @@ * @method void setImipError(bool $imipError) * @method bool|null isEncrypted() * @method void setEncrypted(bool|null $encrypted) + * @method bool getMentionsMe() + * @method void setMentionsMe(bool $isMentionned) */ class Message extends Entity implements JsonSerializable { private const MUTABLE_FLAGS = [ @@ -109,6 +111,7 @@ class Message extends Entity implements JsonSerializable { protected $imipMessage = false; protected $imipProcessed = false; protected $imipError = false; + protected $mentionsMe = false; /** * @var bool|null @@ -323,6 +326,7 @@ static function (Tag $tag) { 'imipMessage' => $this->isImipMessage(), 'previewText' => $this->getPreviewText(), 'encrypted' => ($this->isEncrypted() === true), + 'mentionsMe' => $this->getMentionsMe(), ]; } } diff --git a/lib/Db/MessageMapper.php b/lib/Db/MessageMapper.php index 0084ecbf95..b9b63af0b0 100644 --- a/lib/Db/MessageMapper.php +++ b/lib/Db/MessageMapper.php @@ -563,6 +563,7 @@ public function updatePreviewDataBulk(Message ...$messages): array { ->set('updated_at', $query->createNamedParameter($this->timeFactory->getTime(), IQueryBuilder::PARAM_INT)) ->set('imip_message', $query->createParameter('imip_message')) ->set('encrypted', $query->createParameter('encrypted')) + ->set('mentions_me', $query->createParameter('mentions_me')) ->where($query->expr()->andX( $query->expr()->eq('uid', $query->createParameter('uid')), $query->expr()->eq('mailbox_id', $query->createParameter('mailbox_id')) @@ -593,6 +594,7 @@ public function updatePreviewDataBulk(Message ...$messages): array { ); $query->setParameter('imip_message', $message->isImipMessage(), IQueryBuilder::PARAM_BOOL); $query->setParameter('encrypted', $message->isEncrypted(), IQueryBuilder::PARAM_BOOL); + $query->setParameter('mentions_me', $message->getMentionsMe(), IQueryBuilder::PARAM_BOOL); $query->executeStatement(); } @@ -955,6 +957,12 @@ public function findIdsByQuery(Mailbox $mailbox, SearchQuery $query, string $sor ); } + if ($query->getMentionsMe()) { + $select->andWhere( + $qb->expr()->eq('m.mentions_me', $qb->createNamedParameter($query->getMentionsMe(), IQueryBuilder::PARAM_BOOL)) + ); + } + if ($query->getCursor() !== null && $sortOrder === IMailSearch::ORDER_NEWEST_FIRST) { $select->andWhere( $qb->expr()->lt('m.sent_at', $qb->createNamedParameter($query->getCursor(), IQueryBuilder::PARAM_INT)) diff --git a/lib/IMAP/MessageMapper.php b/lib/IMAP/MessageMapper.php index 6738f6d963..20f1b6b48f 100644 --- a/lib/IMAP/MessageMapper.php +++ b/lib/IMAP/MessageMapper.php @@ -52,7 +52,7 @@ class MessageMapper { public function __construct(LoggerInterface $logger, SmimeService $smimeService, - ImapMessageFetcherFactory $imapMessageFactory) { + ImapMessageFetcherFactory $imapMessageFactory, ) { $this->logger = $logger; $this->smimeService = $smimeService; $this->imapMessageFactory = $imapMessageFactory; @@ -858,7 +858,8 @@ private function buildAttachmentsPartsQuery(Horde_Mime_Part $structure, array $a */ public function getBodyStructureData(Horde_Imap_Client_Socket $client, string $mailbox, - array $uids): array { + array $uids, + string $emailAddress): array { $structureQuery = new Horde_Imap_Client_Fetch_Query(); $structureQuery->structure(); $structureQuery->headerText([ @@ -870,8 +871,7 @@ public function getBodyStructureData(Horde_Imap_Client_Socket $client, $structures = $client->fetch($mailbox, $structureQuery, [ 'ids' => new Horde_Imap_Client_Ids($uids), ]); - - return array_map(function (Horde_Imap_Client_Data_Fetch $fetchData) use ($mailbox, $client) { + return array_map(function (Horde_Imap_Client_Data_Fetch $fetchData) use ($mailbox, $client, $emailAddress) { $hasAttachments = false; $text = ''; $isImipMessage = false; @@ -901,7 +901,7 @@ public function getBodyStructureData(Horde_Imap_Client_Socket $client, $textBodyId = $structure->findBody() ?? $structure->findBody('text'); $htmlBodyId = $structure->findBody('html'); if ($textBodyId === null && $htmlBodyId === null) { - return new MessageStructureData($hasAttachments, $text, $isImipMessage, $isEncrypted); + return new MessageStructureData($hasAttachments, $text, $isImipMessage, $isEncrypted, false); } $partsQuery = new Horde_Imap_Client_Fetch_Query(); if ($htmlBodyId !== null) { @@ -927,7 +927,7 @@ public function getBodyStructureData(Horde_Imap_Client_Socket $client, $part = $parts[$fetchData->getUid()]; // This is sus - why does this even happen? A delete / move in the middle of this processing? if ($part === null) { - return new MessageStructureData($hasAttachments, $text, $isImipMessage, $isEncrypted); + return new MessageStructureData($hasAttachments, $text, $isImipMessage, $isEncrypted, false); } @@ -939,12 +939,14 @@ public function getBodyStructureData(Horde_Imap_Client_Socket $client, $structure->setContents($htmlBody); $htmlBody = $structure->getContents(); } + $mentionsUser = $this->checkLinks($htmlBody, $emailAddress); $html = new Html2Text($htmlBody, ['do_links' => 'none','alt_image' => 'hide']); return new MessageStructureData( $hasAttachments, trim($html->getText()), $isImipMessage, $isEncrypted, + $mentionsUser, ); } $textBody = $part->getBodyPart($textBodyId); @@ -961,9 +963,27 @@ public function getBodyStructureData(Horde_Imap_Client_Socket $client, $textBody, $isImipMessage, $isEncrypted, + false, ); } - return new MessageStructureData($hasAttachments, $text, $isImipMessage, $isEncrypted); + return new MessageStructureData($hasAttachments, $text, $isImipMessage, $isEncrypted, false); }, iterator_to_array($structures->getIterator())); } + private function checkLinks(string $body, string $mailAddress) : bool { + if (empty($body)) { + return false; + } + $dom = new \DOMDocument(); + libxml_use_internal_errors(true); + $dom->loadHTML($body); + libxml_use_internal_errors(); + $anchors = $dom->getElementsByTagName('a'); + foreach ($anchors as $anchor) { + $href = $anchor->getAttribute('href'); + if ($href === 'mailto:' . $mailAddress) { + return true; + } + } + return false; + } } diff --git a/lib/IMAP/MessageStructureData.php b/lib/IMAP/MessageStructureData.php index 3447831d2d..fc58ef9c84 100644 --- a/lib/IMAP/MessageStructureData.php +++ b/lib/IMAP/MessageStructureData.php @@ -20,15 +20,18 @@ class MessageStructureData { private $isImipMessage; private bool $isEncrypted; + private bool $mentionsMe; public function __construct(bool $hasAttachments, string $previewText, bool $isImipMessage, - bool $isEncrypted) { + bool $isEncrypted, + bool $mentionsMe) { $this->hasAttachments = $hasAttachments; $this->previewText = $previewText; $this->isImipMessage = $isImipMessage; $this->isEncrypted = $isEncrypted; + $this->mentionsMe = $mentionsMe; } public function hasAttachments(): bool { @@ -46,4 +49,8 @@ public function isImipMessage(): bool { public function isEncrypted(): bool { return $this->isEncrypted; } + + public function getMentionsMe(): bool { + return $this->mentionsMe; + } } diff --git a/lib/IMAP/PreviewEnhancer.php b/lib/IMAP/PreviewEnhancer.php index 99ce4b54e2..d9273bbfd3 100644 --- a/lib/IMAP/PreviewEnhancer.php +++ b/lib/IMAP/PreviewEnhancer.php @@ -69,7 +69,8 @@ public function process(Account $account, Mailbox $mailbox, array $messages): ar $data = $this->imapMapper->getBodyStructureData( $client, $mailbox->getName(), - $needAnalyze + $needAnalyze, + $account->getEMailAddress() ); } catch (Horde_Imap_Client_Exception $e) { // Ignore for now, but log @@ -94,6 +95,7 @@ public function process(Account $account, Mailbox $mailbox, array $messages): ar $message->setStructureAnalyzed(true); $message->setImipMessage($structureData->isImipMessage()); $message->setEncrypted($structureData->isEncrypted()); + $message->setMentionsMe($structureData->getMentionsMe()); return $message; }, $messages)); diff --git a/lib/Migration/Version4001Date20241009140707.php b/lib/Migration/Version4001Date20241009140707.php new file mode 100644 index 0000000000..155d6310f2 --- /dev/null +++ b/lib/Migration/Version4001Date20241009140707.php @@ -0,0 +1,43 @@ +getTable('mail_messages'); + if (!$messagesTable->hasColumn('mentions_me')) { + $messagesTable->addColumn('mentions_me', Types::BOOLEAN, [ + 'notnull' => false, + 'default' => false, + ]); + } + + return $schema; + } + +} diff --git a/lib/Service/Search/FilterStringParser.php b/lib/Service/Search/FilterStringParser.php index cadcf2d791..f2bd4f5470 100644 --- a/lib/Service/Search/FilterStringParser.php +++ b/lib/Service/Search/FilterStringParser.php @@ -106,6 +106,11 @@ private function parseFilterToken(SearchQuery $query, string $token): bool { case 'match': $query->setMatch($param); return true; + case 'mentions': + if ($param === 'true') { + $query->setMentionsMe(true); + } + return true; case 'flags': $flagArray = explode(',', $param); foreach ($flagArray as $flagItem) { diff --git a/lib/Service/Search/SearchQuery.php b/lib/Service/Search/SearchQuery.php index 4058e5d61a..d3731d1a20 100644 --- a/lib/Service/Search/SearchQuery.php +++ b/lib/Service/Search/SearchQuery.php @@ -52,6 +52,9 @@ class SearchQuery { /** @var bool */ private $hasAttachments = false; + /** @var bool */ + private $mentionsMe = false; + private string $match = 'allof'; /** @@ -230,4 +233,18 @@ public function getHasAttachments(): ?bool { public function setHasAttachments(bool $hasAttachments): void { $this->hasAttachments = $hasAttachments; } + + /** + * @return bool + */ + public function getMentionsMe(): bool { + return $this->mentionsMe; + } + + /** + * @param bool $hasAttachments + */ + public function setMentionsMe(bool $mentionsMe): void { + $this->mentionsMe = $mentionsMe; + } } diff --git a/src/components/SearchMessages.vue b/src/components/SearchMessages.vue index cbc759164e..aea61917de 100644 --- a/src/components/SearchMessages.vue +++ b/src/components/SearchMessages.vue @@ -252,6 +252,11 @@ {{ t('mail', 'Has attachments') }} + @@ -346,6 +351,7 @@ export default { searchInSubject: null, searchInMessageBody: null, searchFlags: [], + mentionsMe: false, hasAttachmentActive: false, hasLast7daysActive: false, hasFromMeActive: false, @@ -406,6 +412,7 @@ export default { body: this.searchInMessageBody !== null && this.searchInMessageBody.length > 1 ? this.searchInMessageBody : '', tags: this.selectedTags.length > 0 ? this.selectedTags.map(item => item.id) : '', flags: this.searchFlags.length > 0 ? this.searchFlags.map(item => item) : '', + mentions: this.mentionsMe, start: this.prepareStart(), end: this.prepareEnd(), } @@ -537,6 +544,7 @@ export default { this.searchFlags = [] this.startDate = null this.endDate = null + this.mentionsMe = false // Need if there is only tag filter or recipients filter if (prevQuery === '') { this.sendQueryEvent() @@ -661,9 +669,6 @@ export default { display: inline-block; width: 32%; - &:last-child { - width: 100%; - } } .range { display: flex;