diff --git a/css/mail.scss b/css/mail.scss index bdd23ac5ca..e66fac7b5a 100755 --- a/css/mail.scss +++ b/css/mail.scss @@ -331,9 +331,6 @@ .icon-drafts { @include icon-color('drafts', 'mail', $color-black); } -.icon-important { - @include icon-color('important', 'mail', $color-black); -} .icon-sent { @include icon-color('sent', 'mail', $color-black); } diff --git a/img/important.svg b/img/important.svg index 710dfffbd5..026d5dfb7e 100644 --- a/img/important.svg +++ b/img/important.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/lib/AppInfo/BootstrapSingleton.php b/lib/AppInfo/BootstrapSingleton.php index 1e605b7942..9d074e3c4e 100644 --- a/lib/AppInfo/BootstrapSingleton.php +++ b/lib/AppInfo/BootstrapSingleton.php @@ -36,6 +36,7 @@ use OCA\Mail\Events\MessageDeletedEvent; use OCA\Mail\Events\MessageFlaggedEvent; use OCA\Mail\Events\MessageSentEvent; +use OCA\Mail\Events\NewMessagesSynchronized; use OCA\Mail\Events\SaveDraftEvent; use OCA\Mail\Http\Middleware\ErrorMiddleware; use OCA\Mail\Http\Middleware\ProvisioningMiddleware; @@ -45,6 +46,7 @@ use OCA\Mail\Listener\FlagRepliedMessageListener; use OCA\Mail\Listener\InteractionListener; use OCA\Mail\Listener\MessageCacheUpdaterListener; +use OCA\Mail\Listener\NewMessageClassificationListener; use OCA\Mail\Listener\SaveSentMessageListener; use OCA\Mail\Listener\TrashMailboxCreatorListener; use OCA\Mail\Service\Attachment\AttachmentService; @@ -135,6 +137,7 @@ private function registerEvents(IAppContainer $container): void { $dispatcher->addServiceListener(MessageSentEvent::class, FlagRepliedMessageListener::class); $dispatcher->addServiceListener(MessageSentEvent::class, InteractionListener::class); $dispatcher->addServiceListener(MessageSentEvent::class, SaveSentMessageListener::class); + $dispatcher->addServiceListener(NewMessagesSynchronized::class, NewMessageClassificationListener::class); $dispatcher->addServiceListener(SaveDraftEvent::class, DraftMailboxCreatorListener::class); } } diff --git a/lib/Db/Message.php b/lib/Db/Message.php index c909b73480..55e0963154 100644 --- a/lib/Db/Message.php +++ b/lib/Db/Message.php @@ -97,7 +97,7 @@ class Message extends Entity implements JsonSerializable { protected $updatedAt; protected $structureAnalyzed; protected $flagAttachments; - protected $flagImportant; + protected $flagImportant = false; protected $previewText; /** @var AddressList */ diff --git a/lib/Db/MessageMapper.php b/lib/Db/MessageMapper.php index 1e0bde2adf..f45ee0d783 100644 --- a/lib/Db/MessageMapper.php +++ b/lib/Db/MessageMapper.php @@ -104,6 +104,7 @@ public function insertBulk(Message ...$messages): void { $qb1->setValue('flag_forwarded', $qb1->createParameter('flag_forwarded')); $qb1->setValue('flag_junk', $qb1->createParameter('flag_junk')); $qb1->setValue('flag_notjunk', $qb1->createParameter('flag_notjunk')); + $qb1->setValue('flag_important', $qb1->createParameter('flag_important')); $qb2 = $this->db->getQueryBuilder(); $qb2->insert('mail_recipients') @@ -126,6 +127,7 @@ public function insertBulk(Message ...$messages): void { $qb1->setParameter('flag_forwarded', $message->getFlagForwarded(), IQueryBuilder::PARAM_BOOL); $qb1->setParameter('flag_junk', $message->getFlagJunk(), IQueryBuilder::PARAM_BOOL); $qb1->setParameter('flag_notjunk', $message->getFlagNotjunk(), IQueryBuilder::PARAM_BOOL); + $qb1->setParameter('flag_important', $message->getFlagImportant(), IQueryBuilder::PARAM_BOOL); $qb1->execute(); diff --git a/lib/Events/NewMessagesSynchronized.php b/lib/Events/NewMessagesSynchronized.php new file mode 100644 index 0000000000..a2ddc9c6ec --- /dev/null +++ b/lib/Events/NewMessagesSynchronized.php @@ -0,0 +1,73 @@ + + * + * @author 2020 Christoph Wurst + * + * @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 . + */ + +namespace OCA\Mail\Events; + +use OCA\Mail\Account; +use OCA\Mail\Db\Mailbox; +use OCA\Mail\Db\Message; +use OCP\EventDispatcher\Event; + +class NewMessagesSynchronized extends Event { + + /** @var Account */ + private $account; + + /** @var Mailbox */ + private $mailbox; + + /** @var array|Message[] */ + private $messages; + + /** + * @param Account $account + * @param Mailbox $mailbox + * @param Message[] $messages + */ + public function __construct(Account $account, + Mailbox $mailbox, + array $messages) { + parent::__construct(); + $this->account = $account; + $this->mailbox = $mailbox; + $this->messages = $messages; + } + + public function getAccount(): Account { + return $this->account; + } + + public function getMailbox(): Mailbox { + return $this->mailbox; + } + + /** + * @return Message[] + */ + public function getMessages() { + return $this->messages; + } + +} diff --git a/lib/Listener/NewMessageClassificationListener.php b/lib/Listener/NewMessageClassificationListener.php new file mode 100644 index 0000000000..cbdaa4e150 --- /dev/null +++ b/lib/Listener/NewMessageClassificationListener.php @@ -0,0 +1,54 @@ + + * + * @author 2020 Christoph Wurst + * + * @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 . + */ + +namespace OCA\Mail\Listener; + +use OCA\Mail\Events\NewMessagesSynchronized; +use OCA\Mail\Service\Classification\MessageClassifier; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; + +class NewMessageClassificationListener implements IEventListener { + + /** @var MessageClassifier */ + private $classifier; + + public function __construct(MessageClassifier $classifier) { + $this->classifier = $classifier; + } + + public function handle(Event $event): void { + if (!($event instanceof NewMessagesSynchronized)) { + return; + } + + foreach ($event->getMessages() as $message) { + if ($this->classifier->isImportant($event->getAccount(), $event->getMailbox(), $message)) { + $message->setFlagImportant(true); + } + } + } + +} diff --git a/lib/Service/Classification/AClassifier.php b/lib/Service/Classification/AClassifier.php new file mode 100644 index 0000000000..b738a8e3ba --- /dev/null +++ b/lib/Service/Classification/AClassifier.php @@ -0,0 +1,55 @@ + + * + * @author 2020 Christoph Wurst + * + * @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 . + */ + +namespace OCA\Mail\Service\Classification; + +use OCA\Mail\Account; +use OCA\Mail\Db\Mailbox; +use OCA\Mail\Db\Message; + +abstract class AClassifier { + + public abstract function isImportant(Account $account, Mailbox $mailbox, Message $message): bool; + + public final function or(AClassifier $next): AClassifier { + return new class($this, $next) extends AClassifier { + /** @var AClassifier */ + private $outer; + + /** @var AClassifier */ + private $next; + + public function __construct(AClassifier $outer, AClassifier $next) { + $this->outer = $outer; + $this->next = $next; + } + + public function isImportant(Account $account, Mailbox $mailbox, Message $message): bool { + return $this->outer->isImportant($account, $mailbox, $message) || $this->next->isImportant($account, $mailbox, $message); + } + }; + } + +} diff --git a/lib/Service/Classification/MessageClassifier.php b/lib/Service/Classification/MessageClassifier.php new file mode 100644 index 0000000000..c4a0fc099f --- /dev/null +++ b/lib/Service/Classification/MessageClassifier.php @@ -0,0 +1,66 @@ + + * + * @author 2020 Christoph Wurst + * + * @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 . + */ + +namespace OCA\Mail\Service\Classification; + +use OCA\Mail\Account; +use OCA\Mail\Db\Mailbox; +use OCA\Mail\Db\Message; + +class MessageClassifier { + + /** @var AClassifier */ + private $oftenImportantSenderClassifier; + + /** @var AClassifier */ + private $oftenContactedSenderClassifier; + + /** @var AClassifier */ + private $oftenReadSenderClassifier; + + /** @var AClassifier */ + private $oftenRepliedSenderClassifier; + + public function __construct(OftenImportantSenderClassifier $oftenImportantSenderClassifier, + OftenContactedSenderClassifier $oftenContactedSenderClassifier, + OftenReadSenderClassifier $oftenReadSenderClassifier, + OftenRepliedSenderClassifier $oftenRepliedSenderClassifier) { + $this->oftenImportantSenderClassifier = $oftenImportantSenderClassifier; + $this->oftenContactedSenderClassifier = $oftenContactedSenderClassifier; + $this->oftenReadSenderClassifier = $oftenReadSenderClassifier; + $this->oftenRepliedSenderClassifier = $oftenRepliedSenderClassifier; + } + + public function isImportant(Account $account, + Mailbox $mailbox, + Message $message): bool { + return $this->oftenImportantSenderClassifier + ->or($this->oftenContactedSenderClassifier) + ->or($this->oftenReadSenderClassifier) + ->or($this->oftenRepliedSenderClassifier) + ->isImportant($account, $mailbox, $message); + } + +} diff --git a/lib/Service/Classification/OftenContactedSenderClassifier.php b/lib/Service/Classification/OftenContactedSenderClassifier.php new file mode 100644 index 0000000000..6dd9772672 --- /dev/null +++ b/lib/Service/Classification/OftenContactedSenderClassifier.php @@ -0,0 +1,104 @@ + + * + * @author 2020 Christoph Wurst + * + * @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 . + */ + +namespace OCA\Mail\Service\Classification; + +use OCA\Mail\Account; +use OCA\Mail\Address; +use OCA\Mail\Db\Mailbox; +use OCA\Mail\Db\MailboxMapper; +use OCA\Mail\Db\Message; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +class OftenContactedSenderClassifier extends AClassifier { + + use SafeRatio; + + /** @var MailboxMapper */ + private $mailboxMapper; + + /** @var IDBConnection */ + private $db; + + public function __construct(MailboxMapper $mailboxMapper, + IDBConnection $db) { + $this->mailboxMapper = $mailboxMapper; + $this->db = $db; + } + + public function isImportant(Account $account, Mailbox $mailbox, Message $message): bool { + $sender = $message->getTo()->first(); + if ($sender === null) { + return false; + } + + try { + $mb = $this->mailboxMapper->findSpecial($account, 'sent'); + } catch (DoesNotExistException $e) { + return false; + } + + return $this->greater( + $this->getMessagesSentTo($mb, $sender->getEmail()), + $this->getMessagesSentTotal($mb), + 0.1, + true // The very first message is important + ); + } + + private function getMessagesSentTotal(Mailbox $mb): int { + $qb = $this->db->getQueryBuilder(); + + $select = $qb->select($qb->func()->count('*')) + ->from('mail_recipients', 'r') + ->join('r', 'mail_messages', 'm', $qb->expr()->eq('m.id', 'r.message_id')) + ->join('r', 'mail_mailboxes', 'mb', $qb->expr()->eq('mb.id', 'm.mailbox_id')) + ->where($qb->expr()->eq('r.id', $qb->createNamedParameter(Address::TYPE_FROM), IQueryBuilder::PARAM_INT)) + ->andWhere($qb->expr()->eq('mb.id', $qb->createNamedParameter($mb->getId(), IQueryBuilder::PARAM_INT))); + $result = $select->execute(); + $cnt = $result->fetchColumn(); + $result->closeCursor(); + return (int)$cnt; + } + + private function getMessagesSentTo(Mailbox $mb, string $email): int { + $qb = $this->db->getQueryBuilder(); + + $select = $qb->select($qb->func()->count('*')) + ->from('mail_recipients', 'r') + ->join('r', 'mail_messages', 'm', $qb->expr()->eq('m.id', 'r.message_id')) + ->join('r', 'mail_mailboxes', 'mb', $qb->expr()->eq('mb.id', 'm.mailbox_id')) + ->where($qb->expr()->eq('r.id', $qb->createNamedParameter(Address::TYPE_FROM), IQueryBuilder::PARAM_INT)) + ->andWhere($qb->expr()->eq('r.email', $qb->createNamedParameter($email))) + ->andWhere($qb->expr()->eq('mb.id', $qb->createNamedParameter($mb->getId(), IQueryBuilder::PARAM_INT))); + $result = $select->execute(); + $cnt = $result->fetchColumn(); + $result->closeCursor(); + return (int)$cnt; + } + +} diff --git a/lib/Service/Classification/OftenImportantSenderClassifier.php b/lib/Service/Classification/OftenImportantSenderClassifier.php new file mode 100644 index 0000000000..d5e0d60b68 --- /dev/null +++ b/lib/Service/Classification/OftenImportantSenderClassifier.php @@ -0,0 +1,105 @@ + + * + * @author 2020 Christoph Wurst + * + * @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 . + */ + +namespace OCA\Mail\Service\Classification; + +use OCA\Mail\Account; +use OCA\Mail\Address; +use OCA\Mail\Db\Mailbox; +use OCA\Mail\Db\MailboxMapper; +use OCA\Mail\Db\Message; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +class OftenImportantSenderClassifier extends AClassifier { + + use SafeRatio; + + /** @var MailboxMapper */ + private $mailboxMapper; + + /** @var IDBConnection */ + private $db; + + public function __construct(MailboxMapper $mailboxMapper, + IDBConnection $db) { + $this->mailboxMapper = $mailboxMapper; + $this->db = $db; + } + + public function isImportant(Account $account, Mailbox $mailbox, Message $message): bool { + $sender = $message->getTo()->first(); + if ($sender === null) { + return false; + } + + try { + $mb = $this->mailboxMapper->findSpecial($account, 'inbox'); + } catch (DoesNotExistException $e) { + return false; + } + + return $this->greater( + $this->getNrOfImportantMessages($mb, $sender->getEmail()), + $this->getNumberOfMessages($mb, $sender->getEmail()), + 0.3 + ); + } + + private function getNrOfImportantMessages(Mailbox $mb, string $email): int { + $qb = $this->db->getQueryBuilder(); + + $select = $qb->select($qb->func()->count('*')) + ->from('mail_recipients', 'r') + ->join('r', 'mail_messages', 'm', $qb->expr()->eq('m.id', 'r.message_id')) + ->join('r', 'mail_mailboxes', 'mb', $qb->expr()->eq('mb.id', 'm.mailbox_id')) + ->where($qb->expr()->eq('r.type', $qb->createNamedParameter(Address::TYPE_FROM), IQueryBuilder::PARAM_INT)) + ->andWhere($qb->expr()->eq('mb.id', $qb->createNamedParameter($mb->getId(), IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('m.flag_important', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))) + ->andWhere($qb->expr()->eq('r.email', $qb->createNamedParameter($email))); + $result = $select->execute(); + $cnt = $result->fetchColumn(); + $result->closeCursor(); + return (int)$cnt; + } + + private function getNumberOfMessages(Mailbox $mb, string $email): int { + $qb = $this->db->getQueryBuilder(); + + $select = $qb->select($qb->func()->count('*')) + ->from('mail_recipients', 'r') + ->join('r', 'mail_messages', 'm', $qb->expr()->eq('m.id', 'r.message_id')) + ->join('r', 'mail_mailboxes', 'mb', $qb->expr()->eq('mb.id', 'm.mailbox_id')) + ->where($qb->expr()->eq('r.type', $qb->createNamedParameter(Address::TYPE_FROM), IQueryBuilder::PARAM_INT)) + ->andWhere($qb->expr()->eq('mb.id', $qb->createNamedParameter($mb->getId(), IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('r.email', $qb->createNamedParameter($email))); + $result = $select->execute(); + $cnt = $result->fetchColumn(); + $result->closeCursor(); + return (int)$cnt; + } + +} diff --git a/lib/Service/Classification/OftenReadSenderClassifier.php b/lib/Service/Classification/OftenReadSenderClassifier.php new file mode 100644 index 0000000000..b4e6be810b --- /dev/null +++ b/lib/Service/Classification/OftenReadSenderClassifier.php @@ -0,0 +1,105 @@ + + * + * @author 2020 Christoph Wurst + * + * @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 . + */ + +namespace OCA\Mail\Service\Classification; + +use OCA\Mail\Account; +use OCA\Mail\Address; +use OCA\Mail\Db\Mailbox; +use OCA\Mail\Db\MailboxMapper; +use OCA\Mail\Db\Message; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +class OftenReadSenderClassifier extends AClassifier { + + use SafeRatio; + + /** @var MailboxMapper */ + private $mailboxMapper; + + /** @var IDBConnection */ + private $db; + + public function __construct(MailboxMapper $mailboxMapper, + IDBConnection $db) { + $this->mailboxMapper = $mailboxMapper; + $this->db = $db; + } + + public function isImportant(Account $account, Mailbox $mailbox, Message $message): bool { + $sender = $message->getTo()->first(); + if ($sender === null) { + return false; + } + + try { + $mb = $this->mailboxMapper->findSpecial($account, 'inbox'); + } catch (DoesNotExistException $e) { + return false; + } + + return $this->greater( + $this->getNrOfReadMessages($mb, $sender->getEmail()), + $this->getNumberOfMessages($mb, $sender->getEmail()), + 0.9 + ); + } + + private function getNrOfReadMessages(Mailbox $mb, string $email): int { + $qb = $this->db->getQueryBuilder(); + + $select = $qb->select($qb->func()->count('*')) + ->from('mail_recipients', 'r') + ->join('r', 'mail_messages', 'm', $qb->expr()->eq('m.id', 'r.message_id')) + ->join('r', 'mail_mailboxes', 'mb', $qb->expr()->eq('mb.id', 'm.mailbox_id')) + ->where($qb->expr()->eq('r.type', $qb->createNamedParameter(Address::TYPE_FROM), IQueryBuilder::PARAM_INT)) + ->andWhere($qb->expr()->eq('mb.id', $qb->createNamedParameter($mb->getId(), IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('m.flag_seen', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))) + ->andWhere($qb->expr()->eq('r.email', $qb->createNamedParameter($email))); + $result = $select->execute(); + $cnt = $result->fetchColumn(); + $result->closeCursor(); + return (int)$cnt; + } + + private function getNumberOfMessages(Mailbox $mb, string $email): int { + $qb = $this->db->getQueryBuilder(); + + $select = $qb->select($qb->func()->count('*')) + ->from('mail_recipients', 'r') + ->join('r', 'mail_messages', 'm', $qb->expr()->eq('m.id', 'r.message_id')) + ->join('r', 'mail_mailboxes', 'mb', $qb->expr()->eq('mb.id', 'm.mailbox_id')) + ->where($qb->expr()->eq('r.type', $qb->createNamedParameter(Address::TYPE_FROM), IQueryBuilder::PARAM_INT)) + ->andWhere($qb->expr()->eq('mb.id', $qb->createNamedParameter($mb->getId(), IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('r.email', $qb->createNamedParameter($email))); + $result = $select->execute(); + $cnt = $result->fetchColumn(); + $result->closeCursor(); + return (int)$cnt; + } + +} diff --git a/lib/Service/Classification/OftenRepliedSenderClassifier.php b/lib/Service/Classification/OftenRepliedSenderClassifier.php new file mode 100644 index 0000000000..f62da2fe38 --- /dev/null +++ b/lib/Service/Classification/OftenRepliedSenderClassifier.php @@ -0,0 +1,105 @@ + + * + * @author 2020 Christoph Wurst + * + * @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 . + */ + +namespace OCA\Mail\Service\Classification; + +use OCA\Mail\Account; +use OCA\Mail\Address; +use OCA\Mail\Db\Mailbox; +use OCA\Mail\Db\MailboxMapper; +use OCA\Mail\Db\Message; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +class OftenRepliedSenderClassifier extends AClassifier { + + use SafeRatio; + + /** @var MailboxMapper */ + private $mailboxMapper; + + /** @var IDBConnection */ + private $db; + + public function __construct(MailboxMapper $mailboxMapper, + IDBConnection $db) { + $this->mailboxMapper = $mailboxMapper; + $this->db = $db; + } + + public function isImportant(Account $account, Mailbox $mailbox, Message $message): bool { + $sender = $message->getTo()->first(); + if ($sender === null) { + return false; + } + + try { + $mb = $this->mailboxMapper->findSpecial($account, 'inbox'); + } catch (DoesNotExistException $e) { + return false; + } + + return $this->greater( + $this->getNrOfRepliedMessages($mb, $sender->getEmail()), + $this->getNumberOfMessages($mb, $sender->getEmail()), + 0.1 + ); + } + + private function getNrOfRepliedMessages(Mailbox $mb, string $email): int { + $qb = $this->db->getQueryBuilder(); + + $select = $qb->select($qb->func()->count('*')) + ->from('mail_recipients', 'r') + ->join('r', 'mail_messages', 'm', $qb->expr()->eq('m.id', 'r.message_id')) + ->join('r', 'mail_mailboxes', 'mb', $qb->expr()->eq('mb.id', 'm.mailbox_id')) + ->where($qb->expr()->eq('r.type', $qb->createNamedParameter(Address::TYPE_FROM), IQueryBuilder::PARAM_INT)) + ->andWhere($qb->expr()->eq('mb.id', $qb->createNamedParameter($mb->getId(), IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('m.flag_answered', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))) + ->andWhere($qb->expr()->eq('r.email', $qb->createNamedParameter($email))); + $result = $select->execute(); + $cnt = $result->fetchColumn(); + $result->closeCursor(); + return (int)$cnt; + } + + private function getNumberOfMessages(Mailbox $mb, string $email): int { + $qb = $this->db->getQueryBuilder(); + + $select = $qb->select($qb->func()->count('*')) + ->from('mail_recipients', 'r') + ->join('r', 'mail_messages', 'm', $qb->expr()->eq('m.id', 'r.message_id')) + ->join('r', 'mail_mailboxes', 'mb', $qb->expr()->eq('mb.id', 'm.mailbox_id')) + ->where($qb->expr()->eq('r.type', $qb->createNamedParameter(Address::TYPE_FROM), IQueryBuilder::PARAM_INT)) + ->andWhere($qb->expr()->eq('mb.id', $qb->createNamedParameter($mb->getId(), IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('r.email', $qb->createNamedParameter($email))); + $result = $select->execute(); + $cnt = $result->fetchColumn(); + $result->closeCursor(); + return (int)$cnt; + } + +} diff --git a/lib/Service/Classification/SafeRatio.php b/lib/Service/Classification/SafeRatio.php new file mode 100644 index 0000000000..43f56df01f --- /dev/null +++ b/lib/Service/Classification/SafeRatio.php @@ -0,0 +1,40 @@ + + * + * @author 2020 Christoph Wurst + * + * @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 . + */ + +namespace OCA\Mail\Service\Classification; + +trait SafeRatio { + + protected function greater(int $num, + int $of, + float $threshold, + bool $default = false): bool { + if ($of === 0) { + return $default; + } + return $num / $of > $threshold; + } + +} diff --git a/lib/Service/Sync/ImapToDbSynchronizer.php b/lib/Service/Sync/ImapToDbSynchronizer.php index c0f19e96a8..4378270c6e 100644 --- a/lib/Service/Sync/ImapToDbSynchronizer.php +++ b/lib/Service/Sync/ImapToDbSynchronizer.php @@ -31,6 +31,7 @@ use OCA\Mail\Db\Mailbox; use OCA\Mail\Db\MailboxMapper; use OCA\Mail\Db\MessageMapper as DatabaseMessageMapper; +use OCA\Mail\Events\NewMessagesSynchronized; use OCA\Mail\Exception\ClientException; use OCA\Mail\Exception\IncompleteSyncException; use OCA\Mail\Exception\MailboxLockedException; @@ -43,6 +44,7 @@ use OCA\mail\lib\Exception\UidValidityChangedException; use OCA\Mail\Model\IMAPMessage; use OCA\Mail\Support\PerformanceLogger; +use OCP\EventDispatcher\IEventDispatcher; use OCP\ILogger; use Throwable; use function array_chunk; @@ -71,6 +73,9 @@ class ImapToDbSynchronizer { /** @var Synchronizer */ private $synchronizer; + /** @var IEventDispatcher */ + private $dispatcher; + /** @var PerformanceLogger */ private $performanceLogger; @@ -83,6 +88,7 @@ public function __construct(DatabaseMessageMapper $dbMapper, MailboxMapper $mailboxMapper, DatabaseMessageMapper $messageMapper, Synchronizer $synchronizer, + IEventDispatcher $dispatcher, PerformanceLogger $performanceLogger, ILogger $logger) { $this->dbMapper = $dbMapper; @@ -91,6 +97,7 @@ public function __construct(DatabaseMessageMapper $dbMapper, $this->mailboxMapper = $mailboxMapper; $this->messageMapper = $messageMapper; $this->synchronizer = $synchronizer; + $this->dispatcher = $dispatcher; $this->performanceLogger = $performanceLogger; $this->logger = $logger; } @@ -275,9 +282,17 @@ private function runPartialSync(Account $account, $perf->step('get new messages via Horde'); foreach (array_chunk($response->getNewMessages(), 500) as $chunk) { - $this->dbMapper->insertBulk(...array_map(function (IMAPMessage $imapMessage) use ($mailbox) { + $dbMessages = array_map(function (IMAPMessage $imapMessage) use ($mailbox) { return $imapMessage->toDbMessage($mailbox->getId()); - }, $chunk)); + }, $chunk); + + $this->dispatcher->dispatch( + NewMessagesSynchronized::class, + new NewMessagesSynchronized($account, $mailbox, $dbMessages) + ); + $perf->step('classified a chunk of new messages'); + + $this->dbMapper->insertBulk(...$dbMessages); } $perf->step('persist new messages'); diff --git a/package-lock.json b/package-lock.json index 6b32d9bcc8..ade7787f67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11360,6 +11360,12 @@ "simple-concat": "^1.0.0" } }, + "simple-html-tokenizer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/simple-html-tokenizer/-/simple-html-tokenizer-0.1.1.tgz", + "integrity": "sha1-BcLuxXn//+FFoDCsJs/qYbmA+r4=", + "dev": true + }, "simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -11865,6 +11871,17 @@ "has-flag": "^3.0.0" } }, + "svg-inline-loader": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/svg-inline-loader/-/svg-inline-loader-0.8.2.tgz", + "integrity": "sha512-kbrcEh5n5JkypaSC152eGfGcnT4lkR0eSfvefaUJkLqgGjRQJyKDvvEE/CCv5aTSdfXuc+N98w16iAojhShI3g==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "object-assign": "^4.0.1", + "simple-html-tokenizer": "^0.1.1" + } + }, "svg-tags": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", diff --git a/package.json b/package.json index caf30f0061..adc59a250e 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "sass-loader": "^8.0.2", "sinon": "^9.0.2", "sinon-chai": "^3.4.0", + "svg-inline-loader": "^0.8.2", "url-loader": "^4.1.0", "vue-loader": "^15.9.1", "vue-server-renderer": "^2.6.11", diff --git a/src/components/Envelope.vue b/src/components/Envelope.vue index 5f705011eb..4631da8fd6 100644 --- a/src/components/Envelope.vue +++ b/src/components/Envelope.vue @@ -1,9 +1,5 @@