From 0ba8a1c39dbd04abd061265db040d5eed1fced49 Mon Sep 17 00:00:00 2001 From: Roberto Guido Date: Mon, 6 Jul 2026 18:42:47 +0200 Subject: [PATCH] feat: save email as .eml file in Files Signed-off-by: Roberto Guido --- appinfo/routes.php | 5 ++ lib/Controller/MessagesController.php | 68 +++++++++++++++++++ src/components/Envelope.vue | 52 ++++++++++++++ src/components/MenuEnvelope.vue | 52 ++++++++++++++ src/service/MessageService.js | 10 +++ .../Controller/MessagesControllerTest.php | 68 +++++++++++++++++++ 6 files changed, 255 insertions(+) diff --git a/appinfo/routes.php b/appinfo/routes.php index 26e7e17776..0e116e1349 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -210,6 +210,11 @@ 'url' => '/api/messages/{id}/attachment/{attachmentId}', 'verb' => 'POST' ], + [ + 'name' => 'messages#saveFile', + 'url' => '/api/messages/{id}/file', + 'verb' => 'POST' + ], [ 'name' => 'messages#getBody', 'url' => '/api/messages/{id}/body', diff --git a/lib/Controller/MessagesController.php b/lib/Controller/MessagesController.php index eb076c9f24..e015113c69 100755 --- a/lib/Controller/MessagesController.php +++ b/lib/Controller/MessagesController.php @@ -44,6 +44,7 @@ use OCP\AppFramework\Http\ZipResponse; use OCP\Files\Folder; use OCP\Files\GenericFileException; +use OCP\Files\IFilenameValidator; use OCP\Files\IMimeTypeDetector; use OCP\Files\NotPermittedException; use OCP\ICache; @@ -71,6 +72,7 @@ public function __construct( private ItineraryService $itineraryService, private ?string $userId, private ?Folder $userFolder, + private IFilenameValidator $filenameValidator, private LoggerInterface $logger, IL10N $l10n, IMimeTypeDetector $mimeTypeDetector, @@ -585,6 +587,72 @@ public function export(int $id): Response { ); } + /** + * Save a whole message as an .eml file in the local storage + * + * @NoAdminRequired + * + * @param int $id + * @param string $targetPath + * + * @return Response + * + * @throws ClientException + * @throws GenericFileException + * @throws NotPermittedException + * @throws LockedException + * @throws ServiceException + */ + #[TrapError] + public function saveFile(int $id, string $targetPath): Response { + if ($this->userId === 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->userId); + $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(); + } + + $fileName = $this->filenameValidator->sanitizeFilename($message->getSubject()); + $fileExtension = 'eml'; + $fullPath = "$targetPath/$fileName.$fileExtension"; + $counter = 2; + while ($this->userFolder->nodeExists($fullPath)) { + $fullPath = "$targetPath/$fileName ($counter).$fileExtension"; + $counter++; + } + + $newFile = $this->userFolder->newFile($fullPath); + $newFile->putContent($source ?? ''); + + return new JSONResponse(); + } + /** * @NoAdminRequired * @NoCSRFRequired diff --git a/src/components/Envelope.vue b/src/components/Envelope.vue index a61d100d6b..d4b39ee410 100644 --- a/src/components/Envelope.vue +++ b/src/components/Envelope.vue @@ -189,6 +189,14 @@ fill-color="var(--color-primary-element)" />