diff --git a/appinfo/info.xml b/appinfo/info.xml
index 6bb3d39258..7c8cf5da31 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.2.0-alpha.0
+ 4.2.1-alpha.0
agpl
Christoph Wurst
GretaD
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index 01bea26944..4d1cf88ceb 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -47,10 +47,12 @@
use OCA\Mail\Listener\MoveJunkListener;
use OCA\Mail\Listener\NewMessageClassificationListener;
use OCA\Mail\Listener\NewMessagesNotifier;
+use OCA\Mail\Listener\NewMessagesSummarizeListener;
use OCA\Mail\Listener\OauthTokenRefreshListener;
use OCA\Mail\Listener\OptionalIndicesListener;
use OCA\Mail\Listener\OutOfOfficeListener;
use OCA\Mail\Listener\SpamReportListener;
+use OCA\Mail\Listener\TaskProcessingListener;
use OCA\Mail\Listener\UserDeletedListener;
use OCA\Mail\Notification\Notifier;
use OCA\Mail\Provider\MailProvider;
@@ -72,6 +74,7 @@
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\DB\Events\AddMissingIndicesEvent;
use OCP\IServerContainer;
+use OCP\TaskProcessing\Events\TaskSuccessfulEvent;
use OCP\User\Events\OutOfOfficeChangedEvent;
use OCP\User\Events\OutOfOfficeClearedEvent;
use OCP\User\Events\OutOfOfficeEndedEvent;
@@ -133,6 +136,7 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(NewMessagesSynchronized::class, NewMessageClassificationListener::class);
$context->registerEventListener(NewMessagesSynchronized::class, MessageKnownSinceListener::class);
$context->registerEventListener(NewMessagesSynchronized::class, NewMessagesNotifier::class);
+ $context->registerEventListener(NewMessagesSynchronized::class, NewMessagesSummarizeListener::class);
$context->registerEventListener(SynchronizationEvent::class, AccountSynchronizedThreadUpdaterListener::class);
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
$context->registerEventListener(NewMessagesSynchronized::class, FollowUpClassifierListener::class);
@@ -141,6 +145,7 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(OutOfOfficeChangedEvent::class, OutOfOfficeListener::class);
$context->registerEventListener(OutOfOfficeClearedEvent::class, OutOfOfficeListener::class);
$context->registerEventListener(OutOfOfficeScheduledEvent::class, OutOfOfficeListener::class);
+ $context->registerEventListener(TaskSuccessfulEvent::class, TaskProcessingListener::class);
$context->registerMiddleWare(ErrorMiddleware::class);
$context->registerMiddleWare(ProvisioningMiddleware::class);
diff --git a/lib/Db/Message.php b/lib/Db/Message.php
index 229c26f395..3376f3742f 100644
--- a/lib/Db/Message.php
+++ b/lib/Db/Message.php
@@ -58,6 +58,8 @@
* @method bool|null getFlagMdnsent()
* @method void setPreviewText(?string $subject)
* @method null|string getPreviewText()
+ * @method void setSummary(?string $summary)
+ * @method null|string getSummary()
* @method void setUpdatedAt(int $time)
* @method int getUpdatedAt()
* @method bool isImipMessage()
@@ -108,6 +110,7 @@ class Message extends Entity implements JsonSerializable {
protected $flagImportant = false;
protected $flagMdnsent;
protected $previewText;
+ protected $summary;
protected $imipMessage = false;
protected $imipProcessed = false;
protected $imipError = false;
@@ -325,6 +328,7 @@ static function (Tag $tag) {
'threadRootId' => $this->getThreadRootId(),
'imipMessage' => $this->isImipMessage(),
'previewText' => $this->getPreviewText(),
+ 'summary' => $this->getSummary(),
'encrypted' => ($this->isEncrypted() === true),
'mentionsMe' => $this->getMentionsMe(),
];
diff --git a/lib/Listener/NewMessagesSummarizeListener.php b/lib/Listener/NewMessagesSummarizeListener.php
new file mode 100644
index 0000000000..e6f37e3bc8
--- /dev/null
+++ b/lib/Listener/NewMessagesSummarizeListener.php
@@ -0,0 +1,50 @@
+
+ */
+class NewMessagesSummarizeListener implements IEventListener {
+
+ public function __construct(
+ protected LoggerInterface $logger,
+ protected IMAPClientFactory $imapFactory,
+ protected AiIntegrationsService $aiService,
+ protected IMailManager $mailManager
+ ) { }
+
+ public function handle(Event $event): void {
+
+ if (!($event instanceof NewMessagesSynchronized)) {
+ return;
+ }
+
+ try {
+ $this->aiService->summarizeMessages(
+ $event->getAccount(),
+ $event->getMessages(),
+ );
+ } catch (ServiceException $e) {
+ $this->logger->error('Could not classify incoming message importance: ' . $e->getMessage(), [
+ 'exception' => $e,
+ ]);
+ }
+ }
+}
diff --git a/lib/Listener/TaskProcessingListener.php b/lib/Listener/TaskProcessingListener.php
new file mode 100644
index 0000000000..8aabe0acd3
--- /dev/null
+++ b/lib/Listener/TaskProcessingListener.php
@@ -0,0 +1,67 @@
+
+ */
+class TaskProcessingListener implements IEventListener {
+
+ public function __construct(
+ protected LoggerInterface $logger,
+ protected MessageMapper $messageStore,
+ ) { }
+
+ public function handle(Event $event): void {
+
+ if (!($event instanceof TaskSuccessfulEvent)) {
+ return;
+ }
+
+ $task = $event->getTask();
+
+ if ($task->getAppId() !== Application::APP_ID) {
+ return;
+ }
+
+ if ($task->getTaskTypeId() !== TextToTextSummary::ID) {
+ return;
+ }
+
+ list($type, $id) = explode(':', $task->getCustomId());
+ $userId = $task->getUserId();
+ $summary = $task->getOutput()['output'];
+
+ match ($type) {
+ 'message' => $this->handleMessageSummary($userId, (int)$id, $summary),
+ };
+
+ }
+
+ protected function handleMessageSummary(string $userId, int $id, string $summary) {
+ $messages = $this->messageStore->findByIds($userId, [$id], '');
+
+ if (count($messages) !== 1) {
+ return;
+ }
+
+ $message = $messages[0];
+ $message->setSummary($summary);
+ $this->messageStore->update($message);
+ }
+}
diff --git a/lib/Migration/Version4100Date20241209000000.php b/lib/Migration/Version4100Date20241209000000.php
new file mode 100644
index 0000000000..6161edaafc
--- /dev/null
+++ b/lib/Migration/Version4100Date20241209000000.php
@@ -0,0 +1,38 @@
+getTable('mail_messages');
+ if (!$outboxTable->hasColumn('summary')) {
+ $outboxTable->addColumn('summary', Types::STRING, [
+ 'length' => 1024,
+ 'notnull' => false,
+ ]);
+ }
+ return $schema;
+ }
+}
diff --git a/lib/Service/AiIntegrations/AiIntegrationsService.php b/lib/Service/AiIntegrations/AiIntegrationsService.php
index afd41fa19d..cdaa363862 100644
--- a/lib/Service/AiIntegrations/AiIntegrationsService.php
+++ b/lib/Service/AiIntegrations/AiIntegrationsService.php
@@ -9,6 +9,7 @@
namespace OCA\Mail\Service\AiIntegrations;
+use Horde_Imap_Client_Socket;
use JsonException;
use OCA\Mail\Account;
use OCA\Mail\AppInfo\Application;
@@ -20,6 +21,10 @@
use OCA\Mail\Model\EventData;
use OCA\Mail\Model\IMAPMessage;
use OCP\IConfig;
+use OCP\TaskProcessing\IManager as TaskProcessingManager;
+use OCP\TaskProcessing\Task as TaskProcessingTask;
+use OCP\TaskProcessing\TaskTypes\TextToTextSummary;
+use OCP\TaskProcessing\Exception\Exception as TaskProcessingException;
use OCP\TextProcessing\FreePromptTaskType;
use OCP\TextProcessing\IManager;
use OCP\TextProcessing\SummaryTaskType;
@@ -61,6 +66,74 @@ public function __construct(ContainerInterface $container, Cache $cache, IMAPCli
$this->mailManager = $mailManager;
$this->config = $config;
}
+
+ /**
+ * generates summary for each message
+ *
+ * @param Account $account
+ * @param array $messages
+ * @param Horde_Imap_Client_Socket $client
+ *
+ * @return null|string
+ *
+ * @throws ServiceException
+ */
+ public function summarizeMessages(Account $account, array $messages, Horde_Imap_Client_Socket $client = null): void {
+ try {
+ $manager = $this->container->get(TaskProcessingManager::class);
+ } catch (\Throwable $e) {
+ throw new ServiceException('Task processing is not available', 0, $e);
+ }
+ try {
+ $manager->getPreferredProvider(TextToTextSummary::ID);
+ } catch (TaskProcessingException $e) {
+ throw new ServiceException('No text summary provider available');
+ }
+ if (!$client) {
+ $client = $this->clientFactory->getClient($account);
+ }
+ try {
+ foreach ($messages as $entry) {
+
+ if (!empty($entry->getSummary())) {
+ continue;
+ }
+ // retrieve full message from server
+ $userId = $account->getUserId();
+ $mailboxId = $entry->getMailboxId();
+ $messageLocalId = $entry->getId();
+ $messageRemoteId = $entry->getUid();
+ $mailbox = $this->mailManager->getMailbox($userId, $mailboxId);
+ $message = $this->mailManager->getImapMessage(
+ $client,
+ $account,
+ $mailbox,
+ $messageRemoteId,
+ true
+ );
+ $messageBody = $message->getPlainBody();
+ // construct prompt and task
+ $prompt = "You are tasked with formulating a helpful summary of a email message. \r\n" .
+ "The summary should be less than 1024 characters. \r\n" .
+ "Here is the ***E-MAIL*** for which you must generate a helpful summary: \r\n" .
+ "***START_OF_E-MAIL***\r\n$messageBody\r\n***END_OF_E-MAIL***\r\n";
+ $task = new TaskProcessingTask(
+ TextToTextSummary::ID,
+ [
+ 'max_tokens' => 1024,
+ 'input' => $prompt,
+ ],
+ Application::APP_ID,
+ $userId,
+ 'message:' . (string)$messageLocalId
+ );
+ $manager->scheduleTask($task);
+ }
+ } finally {
+ $client->logout();
+ }
+ }
+
/**
* @param Account $account
* @param string $threadId