← Back
Editing: MessagesController.php
<?php declare(strict_types=1); /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2014-2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Mail\Controller; use Exception; use OC\Security\CSP\ContentSecurityPolicyNonceManager; use OCA\Mail\Attachment; use OCA\Mail\Contracts\IDkimService; use OCA\Mail\Contracts\IMailManager; use OCA\Mail\Contracts\IMailSearch; use OCA\Mail\Contracts\IMailTransmission; use OCA\Mail\Contracts\ITrustedSenderService; use OCA\Mail\Contracts\IUserPreferences; use OCA\Mail\Db\Message; use OCA\Mail\Exception\ClientException; use OCA\Mail\Exception\ServiceException; use OCA\Mail\Http\AttachmentDownloadResponse; use OCA\Mail\Http\HtmlResponse; use OCA\Mail\Http\TrapError; use OCA\Mail\IMAP\IMAPClientFactory; use OCA\Mail\Model\SmimeData; use OCA\Mail\Service\AccountService; use OCA\Mail\Service\AiIntegrations\AiIntegrationsService; use OCA\Mail\Service\DelegationService; use OCA\Mail\Service\ItineraryService; use OCA\Mail\Service\SmimeService; use OCA\Mail\Service\SnoozeService; use OCP\AppFramework\Controller; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\OpenAPI; use OCP\AppFramework\Http\ContentSecurityPolicy; use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\ZipResponse; use OCP\Files\Folder; use OCP\Files\GenericFileException; use OCP\Files\IMimeTypeDetector; use OCP\Files\NotPermittedException; use OCP\ICache; use OCP\ICacheFactory; use OCP\IL10N; use OCP\IRequest; use OCP\IURLGenerator; use OCP\Lock\LockedException; use Psr\Log\LoggerInterface; use function array_map; #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] class MessagesController extends Controller { private AccountService $accountService; private IMailManager $mailManager; private IMailSearch $mailSearch; private ItineraryService $itineraryService; private ?string $currentUserId; private LoggerInterface $logger; private ?Folder $userFolder; private IMimeTypeDetector $mimeTypeDetector; private IL10N $l10n; private IURLGenerator $urlGenerator; private ContentSecurityPolicyNonceManager $nonceManager; private ITrustedSenderService $trustedSenderService; private IMailTransmission $mailTransmission; private SmimeService $smimeService; private IMAPClientFactory $clientFactory; private IDkimService $dkimService; private IUserPreferences $preferences; private SnoozeService $snoozeService; private AiIntegrationsService $aiIntegrationService; private DelegationService $delegationService; public function __construct( string $appName, IRequest $request, AccountService $accountService, IMailManager $mailManager, IMailSearch $mailSearch, ItineraryService $itineraryService, ?string $userId, $userFolder, LoggerInterface $logger, IL10N $l10n, IMimeTypeDetector $mimeTypeDetector, IURLGenerator $urlGenerator, ContentSecurityPolicyNonceManager $nonceManager, ITrustedSenderService $trustedSenderService, IMailTransmission $mailTransmission, SmimeService $smimeService, IMAPClientFactory $clientFactory, IDkimService $dkimService, IUserPreferences $preferences, SnoozeService $snoozeService, AiIntegrationsService $aiIntegrationService, private ICacheFactory $cacheFactory, DelegationService $delegationService, ) { parent::__construct($appName, $request); $this->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->delegationService = $delegationService; } /** * @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): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } $limit = min(100, max(1, $limit)); try { $effectiveUserId = $this->delegationService->resolveMailboxUserId($mailboxId, $this->currentUserId); $mailbox = $this->mailManager->getMailbox($effectiveUserId, $mailboxId); $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $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; $messages = $this->mailSearch->findMessages( $account, $mailbox, $sort, $filter === '' ? null : $filter, $cursor, $limit, $effectiveUserId, $view ); $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): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); $message = $this->mailManager->getMessage($effectiveUserId, $id); $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $this->logger->debug("loading message <$id>"); return new JSONResponse( $this->mailSearch->findMessage( $account, $mailbox, $message ) ); } /** * @NoAdminRequired * * @param int $id * * @return JSONResponse * * @throws ClientException * @throws ServiceException */ #[TrapError] public function getBody(int $id): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); $message = $this->mailManager->getMessage($effectiveUserId, $id); $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $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'] = $this->enrichAttachments($id, $json['attachments']); $json['inlineAttachments'] = $this->enrichAttachments($id, $json['inlineAttachments']); $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): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); $message = $this->mailManager->getMessage($effectiveUserId, $id); $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $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): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); $message = $this->mailManager->getMessage($effectiveUserId, $id); $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $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 { if ($this->currentUserId === null) { return false; } $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): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); $message = $this->mailManager->getMessage($effectiveUserId, $id); $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } if (empty($message->getThreadRootId())) { return new JSONResponse([], Http::STATUS_NOT_FOUND); } return new JSONResponse($this->mailManager->getThread($account, (string)$message->getThreadRootId())); } /** * @NoAdminRequired * * @param int $id * @param int $destFolderId * * @return JSONResponse * * @throws ClientException * @throws ServiceException */ #[TrapError] public function move(int $id, int $destFolderId): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); $message = $this->mailManager->getMessage($effectiveUserId, $id); $srcMailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); $dstMailbox = $this->mailManager->getMailbox($effectiveUserId, $destFolderId); $srcAccount = $this->accountService->find($effectiveUserId, $srcMailbox->getAccountId()); $dstAccount = $this->accountService->find($effectiveUserId, $dstMailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $this->mailManager->moveMessage( $srcAccount, $srcMailbox->getName(), $message->getUid(), $dstAccount, $dstMailbox->getName() ); $this->delegationService->logDelegatedAction($this->currentUserId, $effectiveUserId, "$this->currentUserId moved message <$id> to mailbox <$destFolderId> on behalf of $effectiveUserId"); 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): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); $message = $this->mailManager->getMessage($effectiveUserId, $id); $srcMailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); $dstMailbox = $this->mailManager->getMailbox($effectiveUserId, $destMailboxId); $srcAccount = $this->accountService->find($effectiveUserId, $srcMailbox->getAccountId()); $dstAccount = $this->accountService->find($effectiveUserId, $dstMailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $this->snoozeService->snoozeMessage($message, $unixTimestamp, $srcAccount, $srcMailbox, $dstAccount, $dstMailbox); $this->delegationService->logDelegatedAction($this->currentUserId, $effectiveUserId, "$this->currentUserId snoozed message <$id> to <$unixTimestamp> on behalf of $effectiveUserId"); return new JSONResponse(); } /** * @NoAdminRequired * * @param int $id * * @return JSONResponse * @throws ClientException * @throws ServiceException */ #[TrapError] public function unSnooze(int $id): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); $message = $this->mailManager->getMessage($effectiveUserId, $id); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $this->snoozeService->unSnoozeMessage($message, $effectiveUserId); $this->delegationService->logDelegatedAction($this->currentUserId, $effectiveUserId, "$this->currentUserId unsnoozed message <$id> on behalf of $effectiveUserId"); return new JSONResponse(); } /** * @NoAdminRequired * * @param int $id * * @return JSONResponse * * @throws ClientException * @throws ServiceException */ #[TrapError] public function mdn(int $id): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); $message = $this->mailManager->getMessage($effectiveUserId, $id); $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } 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): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); $message = $this->mailManager->getMessage($effectiveUserId, $id); $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $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): Response { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); $message = $this->mailManager->getMessage($effectiveUserId, $id); $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $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): Response { if ($this->currentUserId === null) { return new TemplateResponse( $this->appName, 'error', ['message' => 'Not authenticated'], TemplateResponse::RENDER_AS_BLANK, Http::STATUS_UNAUTHORIZED, ); } try { try { $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); $message = $this->mailManager->getMessage($effectiveUserId, $id); $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException) { return new TemplateResponse( $this->appName, 'error', ['message' => 'Not allowed'], TemplateResponse::RENDER_AS_BLANK, Http::STATUS_NOT_FOUND, ); } $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->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()], TemplateResponse::RENDER_AS_BLANK, Http::STATUS_INTERNAL_SERVER_ERROR ); } } /** * @NoAdminRequired * @NoCSRFRequired * * @param int $id * @param string $attachmentId * * @return Response * * @throws ClientException */ #[TrapError] public function downloadAttachment(int $id, string $attachmentId): Response { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); $message = $this->mailManager->getMessage($effectiveUserId, $id); $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $attachment = $this->mailManager->getMailAttachment( $account, $mailbox, $message, $attachmentId, ); // Body party and embedded messages do not have a name $attachmentName = $attachment->getName(); if ($attachmentName === null) { return new AttachmentDownloadResponse( $attachment->getContent(), $this->l10n->t('Embedded message %s', [ $attachmentId, ]) . '.eml', $attachment->getType() ); } return new AttachmentDownloadResponse( $attachment->getContent(), $attachmentName, $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): Response { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); $message = $this->mailManager->getMessage($effectiveUserId, $id); $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $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+'); if ($fh === false) { continue; } 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) { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } if ($this->userFolder === null) { return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR); } if (!$this->userFolder->nodeExists($targetPath)) { return new JSONResponse([], Http::STATUS_BAD_REQUEST); } if (!($this->userFolder->get($targetPath) instanceof Folder)) { return new JSONResponse([], Http::STATUS_BAD_REQUEST); } try { $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); $message = $this->mailManager->getMessage($effectiveUserId, $id); $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } /** @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): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); $message = $this->mailManager->getMessage($effectiveUserId, $id); $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $flagChanges = []; foreach ($flags as $flag => $value) { $value = filter_var($value, FILTER_VALIDATE_BOOLEAN); $this->mailManager->flagMessage($account, $mailbox->getName(), $message->getUid(), $flag, $value); $flagChanges[] = "$flag=" . ($value ? 'true' : 'false'); } $flagsSummary = implode(', ', $flagChanges); $this->delegationService->logDelegatedAction($this->currentUserId, $effectiveUserId, "$this->currentUserId updated flags on message <$id> with [$flagsSummary] on behalf of $effectiveUserId"); return new JSONResponse(); } /** * @NoAdminRequired * * @param int $id * @param string $imapLabel * * @return JSONResponse * * @throws ClientException * @throws ServiceException */ #[TrapError] public function setTag(int $id, string $imapLabel): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); $message = $this->mailManager->getMessage($effectiveUserId, $id); $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } 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); $this->delegationService->logDelegatedAction($this->currentUserId, $effectiveUserId, "$this->currentUserId added tag <$imapLabel> on message <$id> on behalf of $effectiveUserId"); 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): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); $message = $this->mailManager->getMessage($effectiveUserId, $id); $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } 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); $this->delegationService->logDelegatedAction($this->currentUserId, $effectiveUserId, "$this->currentUserId removed tag <$imapLabel> on message <$id> on behalf of $effectiveUserId"); return new JSONResponse($tag); } /** * @NoAdminRequired * * @param int $id * * @throws ClientException * @throws ServiceException */ #[TrapError] public function destroy(int $id): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); $message = $this->mailManager->getMessage($effectiveUserId, $id); $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $this->logger->debug("deleting message <$id>"); $this->mailManager->deleteMessage( $account, $mailbox->getName(), $message->getUid() ); $this->delegationService->logDelegatedAction($this->currentUserId, $effectiveUserId, "$this->currentUserId deleted message <$id> on behalf of $effectiveUserId"); return new JSONResponse(); } /** * @NoAdminRequired * * @param int $messageId * * @return JSONResponse */ #[TrapError] public function smartReply(int $messageId):JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { $effectiveUserId = $this->delegationService->resolveMessageUserId($messageId, $this->currentUserId); $message = $this->mailManager->getMessage($effectiveUserId, $messageId); $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } try { $replies = array_values($this->aiIntegrationService->getSmartReply($account, $mailbox, $message, $effectiveUserId) ?? []); } 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): JSONResponse { if ($this->currentUserId === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } try { $effectiveUserId = $this->delegationService->resolveMessageUserId($messageId, $this->currentUserId); $message = $this->mailManager->getMessage($effectiveUserId, $messageId); $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } 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, $effectiveUserId ); $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); } } private function enrichAttachments(int $id, array $attachments): array { return array_map( fn ($attachment) => $this->enrichAttachment($id, $attachment), $attachments ); } /** * @param int $id * @param array $attachment * * @return array */ private function enrichAttachment(int $id, array $attachment): array { $downloadUrl = $this->urlGenerator->linkToRouteAbsolute('mail.messages.downloadAttachment', [ 'id' => $id, 'attachmentId' => $attachment['id'], ]); $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"); } }
Save File
Cancel