accountService = $accountService; $this->mailManager = $mailManager; $this->mailSearch = $mailSearch; $this->itineraryService = $itineraryService; $this->currentUserId = $UserId; $this->userFolder = $userFolder; $this->logger = $logger; $this->l10n = $l10n; $this->mimeTypeDetector = $mimeTypeDetector; $this->urlGenerator = $urlGenerator; $this->nonceManager = $nonceManager; $this->trustedSenderService = $trustedSenderService; $this->mailTransmission = $mailTransmission; $this->smimeService = $smimeService; $this->clientFactory = $clientFactory; $this->dkimService = $dkimService; $this->preferences = $preferences; $this->snoozeService = $snoozeService; $this->aiIntegrationService = $aiIntegrationService; $this->messageEnvelopeEnricher = $messageEnvelopeEnricher; $this->mailboxShareService = $mailboxShareService; } /** * @NoAdminRequired * * @param int $mailboxId * @param int $cursor * @param string $filter * @param int|null $limit * @param string $view returns messages in requested view ('singleton' or 'threaded') * @param string|null $v Cache buster version to guarantee unique urls (will trigger HTTP caching if set) * * @return JSONResponse * * @throws ClientException * @throws ServiceException */ #[TrapError] public function index(int $mailboxId, ?int $cursor = null, ?string $filter = null, ?int $limit = null, ?string $view = null, ?string $v = null, ?int $shareId = null): JSONResponse { $limit = min(100, max(1, $limit)); if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $resolved = $this->mailboxShareService->resolveMailboxForAccess($this->currentUserId, $mailboxId, $shareId); if ($resolved === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $mailbox = $resolved['mailbox']; $account = $resolved['account']; $this->logger->debug("loading messages of mailbox <$mailboxId>"); $sort = $this->preferences->getPreference($this->currentUserId, 'sort-order', 'newest') === 'newest' ? IMailSearch::ORDER_NEWEST_FIRST : IMailSearch::ORDER_OLDEST_FIRST; $view = $view === 'singleton' ? IMailSearch::VIEW_SINGLETON : IMailSearch::VIEW_THREADED; try { $messages = $this->mailSearch->findMessages( $account, $mailbox, $sort, $filter === '' ? null : $filter, $cursor, $limit, $this->currentUserId, $view ); } catch (MailboxNotCachedException|MailboxLockedException $e) { $this->logger->debug('Mailbox not ready for message list, returning empty', [ 'mailboxId' => $mailboxId, 'message' => $e->getMessage(), ]); return new JSONResponse([]); } $response = new JSONResponse($messages); if ($v !== null && $v !== '') { $response->cacheFor(7 * 24 * 3600, false, true); } return $response; } /** * @NoAdminRequired * * @param int $id * * @throws ClientException * @throws ServiceException */ #[TrapError] public function show(int $id, ?int $shareId = null): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $resolved = $this->mailboxShareService->resolveMessageAccess($this->currentUserId, $id, $shareId); if ($resolved === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $message = $resolved['message']; $mailbox = $resolved['mailbox']; $account = $resolved['account']; $this->logger->debug("loading message <$id>"); $message = $this->mailSearch->findMessage( $account, $mailbox, $message ); $this->messageEnvelopeEnricher->enrichFromImapWhenEmpty($account, $mailbox, $message); return new JSONResponse($message); } /** * Return headers (subject, from, to) for the given message to prefill the "Create mail filter" form. * Used when the frontend opens the filter modal so it can show the list even if the message * wasn't passed from the view (e.g. thread context). Enriches from IMAP when DB has empty fields. * * @NoAdminRequired * * @param int $id Message database id * @return JSONResponse { headers: Array<{ field: string, label: string, values: string[] }> } * * @throws ClientException * @throws ServiceException */ #[TrapError] public function getFilterHeaders(int $id, ?int $shareId = null): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $resolved = $this->mailboxShareService->resolveMessageAccess($this->currentUserId, $id, $shareId); if ($resolved === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $message = $this->mailSearch->findMessage($resolved['account'], $resolved['mailbox'], $resolved['message']); $this->messageEnvelopeEnricher->enrichFromImapWhenEmpty($resolved['account'], $resolved['mailbox'], $message); $headers = []; $subject = trim((string)$message->getSubject()); if ($subject !== '') { $headers[] = [ 'field' => 'subject', 'label' => $this->l10n->t('Subject'), 'values' => [$subject], ]; } $fromEmails = array_values(array_filter(array_map( static fn ($a) => $a['email'] ?? null, $message->getFrom()->jsonSerialize() ))); if ($fromEmails !== []) { $headers[] = [ 'field' => 'from', 'label' => $this->l10n->t('From'), 'values' => $fromEmails, ]; } $toEmails = array_values(array_filter(array_map( static fn ($a) => $a['email'] ?? null, $message->getTo()->jsonSerialize() ))); if ($toEmails !== []) { $headers[] = [ 'field' => 'to', 'label' => $this->l10n->t('To'), 'values' => $toEmails, ]; } return new JSONResponse(['headers' => $headers]); } /** * @NoAdminRequired * * @param int $id * * @return JSONResponse * * @throws ClientException * @throws ServiceException */ #[TrapError] public function getBody(int $id, ?int $shareId = null): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $shareId = $shareId ?? ($this->request->getParam('shareId') !== null ? (int)$this->request->getParam('shareId') : null); $resolved = $this->mailboxShareService->resolveMessageAccess($this->currentUserId, $id, $shareId); if ($resolved === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $message = $resolved['message']; $mailbox = $resolved['mailbox']; $account = $resolved['account']; $cacheInstance = $this->getCacheForAccount($account->getId()); $imapMessageCacheKey = 'message_' . $id; $client = $this->clientFactory->getClient($account); try { $imapMessage = $this->mailManager->getImapMessage( $client, $account, $mailbox, $message->getUid(), true ); if ($imapMessage->hasHtmlMessage()) { $cacheInstance->set($imapMessageCacheKey, $imapMessage->getHtmlBody($id), 600); } $json = $imapMessage->getFullMessage($id); } finally { $client->logout(); } $itineraries = $this->itineraryService->getCached($account, $mailbox, $message->getUid()); if ($itineraries) { $json['itineraries'] = $itineraries; } $json['attachments'] = array_map(fn ($a) => $this->enrichDownloadUrl( $id, $a, $shareId ), $json['attachments']); $json['accountId'] = $account->getId(); $json['mailboxId'] = $mailbox->getId(); $json['databaseId'] = $message->getId(); $json['isSenderTrusted'] = $this->isSenderTrusted($message); $smimeData = new SmimeData(); $smimeData->setIsEncrypted($message->isEncrypted() || $imapMessage->isEncrypted()); if ($imapMessage->isSigned()) { $smimeData->setIsSigned(true); $smimeData->setSignatureIsValid($imapMessage->isSignatureValid()); } $json['smime'] = $smimeData; $dkimResult = $this->dkimService->getCached($account, $mailbox, $message->getUid()); if (is_bool($dkimResult)) { $json['dkimValid'] = $dkimResult; } $response = new JSONResponse($json); // Enable caching $response->cacheFor(60 * 60, false, true); return $response; } /** * @NoAdminRequired * * @param int $id * * @return JSONResponse * * @throws ClientException */ #[TrapError] public function getItineraries(int $id, ?int $shareId = null): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $resolved = $this->mailboxShareService->resolveMessageAccess($this->currentUserId, $id, $shareId); if ($resolved === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $message = $resolved['message']; $mailbox = $resolved['mailbox']; $account = $resolved['account']; $response = new JsonResponse($this->itineraryService->extract($account, $mailbox, $message->getUid())); $response->cacheFor(24 * 60 * 60, false, true); return $response; } /** * @NoAdminRequired * @param int $id * @return JSONResponse */ public function getDkim(int $id, ?int $shareId = null): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $resolved = $this->mailboxShareService->resolveMessageAccess($this->currentUserId, $id, $shareId); if ($resolved === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $message = $resolved['message']; $mailbox = $resolved['mailbox']; $account = $resolved['account']; $response = new JSONResponse(['valid' => $this->dkimService->validate($account, $mailbox, $message->getUid())]); $response->cacheFor(24 * 60 * 60, false, true); return $response; } private function isSenderTrusted(Message $message): bool { $from = $message->getFrom(); $first = $from->first(); if ($first === null) { return false; } $email = $first->getEmail(); if ($email === null) { return false; } return $this->trustedSenderService->isTrusted( $this->currentUserId, $email ); } /** * @NoAdminRequired * @NoCSRFRequired * * @param int $id * * @return JSONResponse * @throws ClientException */ #[TrapError] public function getThread(int $id, ?int $shareId = null): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $shareId = $shareId ?? ($this->request->getParam('shareId') !== null ? (int)$this->request->getParam('shareId') : null); $resolved = $this->mailboxShareService->resolveMessageAccess($this->currentUserId, $id, $shareId); if ($resolved === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $message = $resolved['message']; $account = $resolved['account']; if (empty($message->getThreadRootId())) { return new JSONResponse([], Http::STATUS_NOT_FOUND); } return new JSONResponse($this->mailManager->getThread($resolved['account'], $message->getThreadRootId())); } /** * @NoAdminRequired * * @param int $id * @param int $destFolderId * * @return JSONResponse * * @throws ClientException * @throws ServiceException */ #[TrapError] public function move(int $id, int $destFolderId, ?int $shareId = null): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $resolvedMsg = $this->mailboxShareService->resolveMessageAccess($this->currentUserId, $id, $shareId); $resolvedDst = $this->mailboxShareService->resolveMailboxForAccess($this->currentUserId, $destFolderId, $shareId); if ($resolvedMsg === null || $resolvedDst === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } if ($resolvedMsg['account']->getId() !== $resolvedDst['account']->getId()) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $this->mailManager->moveMessage( $resolvedMsg['account'], $resolvedMsg['mailbox']->getName(), $resolvedMsg['message']->getUid(), $resolvedDst['account'], $resolvedDst['mailbox']->getName() ); return new JSONResponse(); } /** * @NoAdminRequired * * @param int $id * @param int $unixTimestamp * @param int $destMailboxId * * @return JSONResponse * @throws ClientException * @throws ServiceException */ #[TrapError] public function snooze(int $id, int $unixTimestamp, int $destMailboxId, ?int $shareId = null): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $resolvedMsg = $this->mailboxShareService->resolveMessageAccess($this->currentUserId, $id, $shareId); $resolvedDst = $this->mailboxShareService->resolveMailboxForAccess($this->currentUserId, $destMailboxId, $shareId); if ($resolvedMsg === null || $resolvedDst === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } if ($resolvedMsg['account']->getId() !== $resolvedDst['account']->getId()) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $this->snoozeService->snoozeMessage($resolvedMsg['message'], $unixTimestamp, $resolvedMsg['account'], $resolvedMsg['mailbox'], $resolvedDst['account'], $resolvedDst['mailbox']); return new JSONResponse(); } /** * @NoAdminRequired * * @param int $id * * @return JSONResponse * @throws ClientException * @throws ServiceException */ #[TrapError] public function unSnooze(int $id, ?int $shareId = null): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $resolved = $this->mailboxShareService->resolveMessageAccess($this->currentUserId, $id, $shareId); if ($resolved === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $this->snoozeService->unSnoozeMessage($resolved['message'], $this->currentUserId); return new JSONResponse(); } /** * @NoAdminRequired * * @param int $id * * @return JSONResponse * * @throws ClientException * @throws ServiceException */ #[TrapError] public function mdn(int $id, ?int $shareId = null): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $resolved = $this->mailboxShareService->resolveMessageAccess($this->currentUserId, $id, $shareId); if ($resolved === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $message = $resolved['message']; $mailbox = $resolved['mailbox']; $account = $resolved['account']; if ($message->getFlagMdnsent()) { return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED); } try { $this->mailTransmission->sendMdn($account, $mailbox, $message); $this->mailManager->flagMessage($account, $mailbox->getName(), $message->getUid(), '$mdnsent', true); } catch (ServiceException $ex) { $this->logger->error('Sending mdn failed: ' . $ex->getMessage()); throw $ex; } return new JSONResponse(); } /** * @NoAdminRequired * @NoCSRFRequired * * @throws ServiceException */ #[TrapError] public function getSource(int $id, ?int $shareId = null): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $resolved = $this->mailboxShareService->resolveMessageAccess($this->currentUserId, $id, $shareId); if ($resolved === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $message = $resolved['message']; $mailbox = $resolved['mailbox']; $account = $resolved['account']; $client = $this->clientFactory->getClient($account); try { $response = new JSONResponse([ 'source' => $this->mailManager->getSource( $client, $account, $mailbox->getName(), $message->getUid() ) ]); } finally { $client->logout(); } // Enable caching $response->cacheFor(60 * 60, false, true); return $response; } /** * Export a whole message as an .eml file. * * @NoAdminRequired * @NoCSRFRequired * * @param int $id * @return Response * @throws ClientException * @throws ServiceException */ #[TrapError] public function export(int $id, ?int $shareId = null): Response { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $resolved = $this->mailboxShareService->resolveMessageAccess($this->currentUserId, $id, $shareId); if ($resolved === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $message = $resolved['message']; $mailbox = $resolved['mailbox']; $account = $resolved['account']; $client = $this->clientFactory->getClient($account); try { $source = $this->mailManager->getSource( $client, $account, $mailbox->getName(), $message->getUid() ); } finally { $client->logout(); } return new AttachmentDownloadResponse( $source, $message->getSubject() . '.eml', 'message/rfc822', ); } /** * @NoAdminRequired * @NoCSRFRequired * * @param int $id * @param bool $plain do not inject scripts if true (default=false) * * @return HtmlResponse|TemplateResponse * * @throws ClientException */ #[TrapError] public function getHtmlBody(int $id, bool $plain = false, ?int $shareId = null): Response { if ($this->currentUserId === null) { return new TemplateResponse($this->appName, 'error', ['message' => 'Not allowed'], 'none'); } $shareId = $shareId ?? ($this->request->getParam('shareId') !== null ? (int)$this->request->getParam('shareId') : null); $resolved = $this->mailboxShareService->resolveMessageAccess($this->currentUserId, $id, $shareId); if ($resolved === null) { return new TemplateResponse( $this->appName, 'error', ['message' => 'Not allowed'], 'none' ); } $message = $resolved['message']; $mailbox = $resolved['mailbox']; $account = $resolved['account']; try { $cacheInstance = $this->getCacheForAccount($account->getId()); $imapMessageCacheKey = 'message_' . $id; $html = $cacheInstance->get($imapMessageCacheKey); if ($html === null) { $client = $this->clientFactory->getClient($account); try { $html = $this->mailManager->getImapMessage( $client, $account, $mailbox, $message->getUid(), true )->getHtmlBody( $id ); } finally { $client->logout(); } } $htmlResponse = $plain ? HtmlResponse::plain($html) : HtmlResponse::withResizer( $html, $this->nonceManager->getNonce(), $this->urlGenerator->getAbsoluteURL( $this->urlGenerator->linkTo('mail', 'js/htmlresponse.js') ) ); // Harden the default security policy $policy = new ContentSecurityPolicy(); $policy->allowEvalScript(false); $policy->disallowScriptDomain('\'self\''); $policy->disallowConnectDomain('\'self\''); $policy->disallowFontDomain('\'self\''); $policy->disallowMediaDomain('\'self\''); $htmlResponse->setContentSecurityPolicy($policy); // Enable caching $htmlResponse->cacheFor(60 * 60, false, true); return $htmlResponse; } catch (Exception $ex) { return new TemplateResponse( $this->appName, 'error', ['message' => $ex->getMessage()], 'none' ); } } /** * @NoAdminRequired * @NoCSRFRequired * * @param int $id * @param string $attachmentId * * @return Response * * @throws ClientException */ #[TrapError] public function downloadAttachment(int $id, string $attachmentId, ?int $shareId = null): Response { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $shareId = $shareId ?? ($this->request->getParam('shareId') !== null ? (int)$this->request->getParam('shareId') : null); $resolved = $this->mailboxShareService->resolveMessageAccess($this->currentUserId, $id, $shareId); if ($resolved === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $message = $resolved['message']; $mailbox = $resolved['mailbox']; $account = $resolved['account']; $attachment = $this->mailManager->getMailAttachment( $account, $mailbox, $message, $attachmentId, ); // Body party and embedded messages do not have a name if ($attachment->getName() === null) { return new AttachmentDownloadResponse( $attachment->getContent(), $this->l10n->t('Embedded message %s', [ $attachmentId, ]) . '.eml', $attachment->getType() ); } return new AttachmentDownloadResponse( $attachment->getContent(), $attachment->getName(), $attachment->getType() ); } /** * @NoAdminRequired * @NoCSRFRequired * * @param int $id the message id * * @return ZipResponse|JSONResponse * * @throws ClientException * @throws ServiceException * @throws DoesNotExistException */ #[TrapError] public function downloadAttachments(int $id, ?int $shareId = null): Response { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $shareId = $shareId ?? ($this->request->getParam('shareId') !== null ? (int)$this->request->getParam('shareId') : null); $resolved = $this->mailboxShareService->resolveMessageAccess($this->currentUserId, $id, $shareId); if ($resolved === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $message = $resolved['message']; $mailbox = $resolved['mailbox']; $account = $resolved['account']; $attachments = $this->mailManager->getMailAttachments($account, $mailbox, $message); $zip = new ZipResponse($this->request, 'attachments'); foreach ($attachments as $attachment) { $fileName = $attachment->getName(); $fh = fopen('php://temp', 'r+'); fputs($fh, $attachment->getContent()); $size = $attachment->getSize(); rewind($fh); $zip->addResource($fh, $fileName, $size); } return $zip; } /** * @NoAdminRequired * * @param int $id * @param string $attachmentId * @param string $targetPath * * @return JSONResponse * * @throws ClientException * @throws GenericFileException * @throws NotPermittedException * @throws LockedException */ #[TrapError] public function saveAttachment(int $id, string $attachmentId, string $targetPath, ?int $shareId = null) { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $shareId = $shareId ?? ($this->request->getParam('shareId') !== null ? (int)$this->request->getParam('shareId') : null); $resolved = $this->mailboxShareService->resolveMessageAccess($this->currentUserId, $id, $shareId); if ($resolved === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $message = $resolved['message']; $mailbox = $resolved['mailbox']; $account = $resolved['account']; /** @var Attachment[] $attachments */ $attachments = []; if ($attachmentId === '0') { $attachments = $this->mailManager->getMailAttachments( $account, $mailbox, $message, ); } else { $attachments[] = $this->mailManager->getMailAttachment( $account, $mailbox, $message, $attachmentId, ); } foreach ($attachments as $attachment) { $fileName = $attachment->getName() ?? $this->l10n->t('Embedded message %s', [ $attachment->getId(), ]) . '.eml'; $fileParts = pathinfo($fileName); $fileName = $fileParts['filename']; $fileExtension = $fileParts['extension']; $fullPath = "$targetPath/$fileName.$fileExtension"; $counter = 2; while ($this->userFolder->nodeExists($fullPath)) { $fullPath = "$targetPath/$fileName ($counter).$fileExtension"; $counter++; } $newFile = $this->userFolder->newFile($fullPath); $newFile->putContent($attachment->getContent()); } return new JSONResponse(); } /** * @NoAdminRequired * * @param int $id * @param array $flags * * @return JSONResponse * * @throws ClientException * @throws ServiceException */ #[TrapError] public function setFlags(int $id, array $flags, ?int $shareId = null): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $resolved = $this->mailboxShareService->resolveMessageAccess($this->currentUserId, $id, $shareId); if ($resolved === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $message = $resolved['message']; $mailbox = $resolved['mailbox']; $account = $resolved['account']; foreach ($flags as $flag => $value) { $value = filter_var($value, FILTER_VALIDATE_BOOLEAN); $this->mailManager->flagMessage($account, $mailbox->getName(), $message->getUid(), $flag, $value); } return new JSONResponse(); } /** * @NoAdminRequired * * @param int $id * @param string $imapLabel * * @return JSONResponse * * @throws ClientException * @throws ServiceException */ #[TrapError] public function setTag(int $id, string $imapLabel, ?int $shareId = null): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $resolved = $this->mailboxShareService->resolveMessageAccess($this->currentUserId, $id, $shareId); if ($resolved === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $message = $resolved['message']; $mailbox = $resolved['mailbox']; $account = $resolved['account']; try { $tag = $this->mailManager->getTagByImapLabel($imapLabel, $this->currentUserId); } catch (ClientException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $this->mailManager->tagMessage($account, $mailbox->getName(), $message, $tag, true); return new JSONResponse($tag); } /** * @NoAdminRequired * * @param int $id * @param string $imapLabel * * @return JSONResponse * * @throws ClientException * @throws ServiceException */ #[TrapError] public function removeTag(int $id, string $imapLabel, ?int $shareId = null): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $resolved = $this->mailboxShareService->resolveMessageAccess($this->currentUserId, $id, $shareId); if ($resolved === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $message = $resolved['message']; $mailbox = $resolved['mailbox']; $account = $resolved['account']; try { $tag = $this->mailManager->getTagByImapLabel($imapLabel, $this->currentUserId); } catch (ClientException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $this->mailManager->tagMessage($account, $mailbox->getName(), $message, $tag, false); return new JSONResponse($tag); } /** * @NoAdminRequired * * @param int $id * * @throws ClientException * @throws ServiceException */ #[TrapError] public function destroy(int $id, ?int $shareId = null): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $resolved = $this->mailboxShareService->resolveMessageAccess($this->currentUserId, $id, $shareId); if ($resolved === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $message = $resolved['message']; $mailbox = $resolved['mailbox']; $account = $resolved['account']; $this->logger->debug("deleting message <$id>"); $this->mailManager->deleteMessage( $account, $mailbox->getName(), $message->getUid() ); return new JSONResponse(); } /** * @NoAdminRequired * * @param int $messageId * * @return JSONResponse */ #[TrapError] public function smartReply(int $messageId):JSONResponse { try { $message = $this->mailManager->getMessage($this->currentUserId, $messageId); $mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } try { $replies = array_values($this->aiIntegrationService->getSmartReply($account, $mailbox, $message, $this->currentUserId)); } catch (ServiceException $e) { $this->logger->error('Smart reply failed: ' . $e->getMessage(), [ 'exception' => $e, ]); return new JSONResponse([], Http::STATUS_NO_CONTENT); } return new JSONResponse($replies); } /** * @NoAdminRequired * * @param int $messageId * * @return JSONResponse */ #[TrapError] public function needsTranslation(int $messageId, ?int $shareId = null): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $resolved = $this->mailboxShareService->resolveMessageAccess($this->currentUserId, $messageId, $shareId); if ($resolved === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $message = $resolved['message']; $mailbox = $resolved['mailbox']; $account = $resolved['account']; if (!$this->aiIntegrationService->isLlmProcessingEnabled()) { $response = new JSONResponse([], Http::STATUS_NOT_IMPLEMENTED); $response->cacheFor(60 * 60 * 24, false, true); return $response; } try { $requiresTranslation = $this->aiIntegrationService->requiresTranslation( $account, $mailbox, $message, $this->currentUserId ); $response = new JSONResponse(['requiresTranslation' => $requiresTranslation === true]); $response->cacheFor(60 * 60 * 24, false, true); return $response; } catch (ServiceException $e) { $this->logger->error('Translation check failed: ' . $e->getMessage(), [ 'exception' => $e, ]); return new JSONResponse([], Http::STATUS_NO_CONTENT); } } /** * @param int $id * @param array $attachment * @param int|null $shareId for shared mailbox access * * @return array */ private function enrichDownloadUrl(int $id, array $attachment, ?int $shareId = null) { $params = [ 'id' => $id, 'attachmentId' => $attachment['id'], ]; if ($shareId !== null) { $params['shareId'] = $shareId; } $downloadUrl = $this->urlGenerator->linkToRoute('mail.messages.downloadAttachment', $params); $downloadUrl = $this->urlGenerator->getAbsoluteURL($downloadUrl); $attachment['downloadUrl'] = $downloadUrl; $attachment['mimeUrl'] = $this->mimeTypeDetector->mimeTypeIcon($attachment['mime']); $attachment['isImage'] = $this->attachmentIsImage($attachment); $attachment['isCalendarEvent'] = $this->attachmentIsCalendarEvent($attachment); return $attachment; } /** * Determines if the content of this attachment is an image * * @param array $attachment * * @return boolean */ private function attachmentIsImage(array $attachment): bool { return in_array( $attachment['mime'], [ 'image/jpeg', 'image/png', 'image/gif' ]); } /** * @param array $attachment * * @return boolean */ private function attachmentIsCalendarEvent(array $attachment): bool { return in_array($attachment['mime'], ['text/calendar', 'application/ics'], true); } private function getCacheForAccount(int $accountId): ICache { return $this->cacheFactory->createDistributed('mail_account_' . $accountId); } }