f7cloud_client/apps/mail/lib/Service/Attachment/AttachmentService.php
root 8b6a0139db f7cloud_client
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 22:59:26 +00:00

381 lines
11 KiB
PHP

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2017-2024 F7cloud GmbH and F7cloud contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\Mail\Service\Attachment;
use finfo;
use InvalidArgumentException;
use OCA\Files_Sharing\SharedStorage;
use OCA\Mail\Account;
use OCA\Mail\Contracts\IAttachmentService;
use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\LocalAttachment;
use OCA\Mail\Db\LocalAttachmentMapper;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Exception\AttachmentNotFoundException;
use OCA\Mail\Exception\UploadException;
use OCA\Mail\IMAP\MessageMapper;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use Psr\Log\LoggerInterface;
class AttachmentService implements IAttachmentService {
/** @var LocalAttachmentMapper */
private $mapper;
/** @var AttachmentStorage */
private $storage;
/**
* @var IMailManager
*/
private $mailManager;
/**
* @var MessageMapper
*/
private $messageMapper;
/**
* @var Folder
*/
private $userFolder;
/**
* @var LoggerInterface
*/
private $logger;
/**
* @param Folder $userFolder
*/
public function __construct($userFolder,
LocalAttachmentMapper $mapper,
AttachmentStorage $storage,
IMailManager $mailManager,
MessageMapper $imapMessageMapper,
LoggerInterface $logger) {
$this->mapper = $mapper;
$this->storage = $storage;
$this->mailManager = $mailManager;
$this->messageMapper = $imapMessageMapper;
$this->userFolder = $userFolder;
$this->logger = $logger;
}
/**
* @param string $userId
* @param UploadedFile $file
* @return LocalAttachment
*/
#[\Override]
public function addFile(string $userId, UploadedFile $file): LocalAttachment {
$attachment = new LocalAttachment();
$attachment->setUserId($userId);
$attachment->setFileName($file->getFileName());
$attachment->setMimeType($file->getMimeType());
$persisted = $this->mapper->insert($attachment);
try {
$this->storage->save($userId, $persisted->id, $file);
} catch (UploadException $ex) {
// Clean-up
$this->mapper->delete($persisted);
throw $ex;
}
return $attachment;
}
public function addFileFromString(string $userId, string $name, string $mime, string $fileContents): LocalAttachment {
$attachment = new LocalAttachment();
$attachment->setUserId($userId);
$attachment->setFileName($name);
$attachment->setMimeType($mime);
$persisted = $this->mapper->insert($attachment);
try {
$this->storage->saveContent($userId, $persisted->id, $fileContents);
} catch (NotFoundException|NotPermittedException $e) {
// Clean-up
$this->mapper->delete($persisted);
throw new UploadException($e->getMessage(), $e->getCode(), $e);
}
return $attachment;
}
/**
* @param string $userId
* @param int $id
*
* @return array of LocalAttachment and ISimpleFile
*
* @throws AttachmentNotFoundException
*/
#[\Override]
public function getAttachment(string $userId, int $id): array {
try {
$attachment = $this->mapper->find($userId, $id);
$file = $this->storage->retrieve($userId, $id);
return [$attachment, $file];
} catch (DoesNotExistException $ex) {
throw new AttachmentNotFoundException();
}
}
/**
* @param string $userId
* @param int $id
*
* @return void
*/
#[\Override]
public function deleteAttachment(string $userId, int $id) {
try {
$attachment = $this->mapper->find($userId, $id);
$this->mapper->delete($attachment);
} catch (DoesNotExistException $ex) {
// Nothing to do then
}
$this->storage->delete($userId, $id);
}
public function deleteLocalMessageAttachments(string $userId, int $localMessageId): void {
$attachments = $this->mapper->findByLocalMessageId($userId, $localMessageId);
// delete db entries
$this->mapper->deleteForLocalMessage($userId, $localMessageId);
// delete storage
foreach ($attachments as $attachment) {
$this->storage->delete($userId, $attachment->getId());
}
}
public function deleteLocalMessageAttachmentsById(string $userId, int $localMessageId, array $attachmentIds): void {
$attachments = $this->mapper->findByIds($userId, $attachmentIds);
// delete storage
foreach ($attachments as $attachment) {
$this->mapper->delete($attachment);
$this->storage->delete($userId, $attachment->getId());
}
}
/**
* @param int[] $attachmentIds
* @return LocalAttachment[]
*/
public function saveLocalMessageAttachments(string $userId, int $messageId, array $attachmentIds): array {
if ($attachmentIds === []) {
return [];
}
$this->mapper->saveLocalMessageAttachments($userId, $messageId, $attachmentIds);
return $this->mapper->findByLocalMessageId($userId, $messageId);
}
/**
* @return LocalAttachment[]
*/
public function updateLocalMessageAttachments(string $userId, LocalMessage $message, array $newAttachmentIds): array {
// no attachments any more. Delete any old ones and we're done
if ($newAttachmentIds === []) {
$this->deleteLocalMessageAttachments($userId, $message->getId());
return [];
}
// no need to diff, no old attachments
if (empty($message->getAttachments())) {
$this->mapper->saveLocalMessageAttachments($userId, $message->getId(), $newAttachmentIds);
return $this->mapper->findByLocalMessageId($userId, $message->getId());
}
$oldAttachmentIds = array_map(static fn ($attachment) => $attachment->getId(), $message->getAttachments());
$add = array_diff($newAttachmentIds, $oldAttachmentIds);
if ($add !== []) {
$this->mapper->saveLocalMessageAttachments($userId, $message->getId(), $add);
}
$delete = array_diff($oldAttachmentIds, $newAttachmentIds);
if ($delete !== []) {
$this->deleteLocalMessageAttachmentsById($userId, $message->getId(), $delete);
}
return $this->mapper->findByLocalMessageId($userId, $message->getId());
}
/**
* @param array $attachments
* @return int[]
*/
public function handleAttachments(Account $account, array $attachments, \Horde_Imap_Client_Socket $client): array {
$attachmentIds = [];
if ($attachments === []) {
return $attachmentIds;
}
foreach ($attachments as $attachment) {
if (!isset($attachment['type'])) {
throw new InvalidArgumentException('Attachment does not have a type');
}
if ($attachment['type'] === 'local' && isset($attachment['id'])) {
// attachment already exists, only return the id
$attachmentIds[] = (int)$attachment['id'];
continue;
}
if ($attachment['type'] === 'message' || $attachment['type'] === 'message/rfc822') {
// Adds another message as attachment
$attachmentIds[] = $this->handleForwardedMessageAttachment($account, $attachment, $client);
continue;
}
if ($attachment['type'] === 'message-attachment') {
// Adds an attachment from another email (use case is, eg., a mail forward)
$attachmentIds[] = $this->handleForwardedAttachment($account, $attachment, $client);
continue;
}
$attachmentIds[] = $this->handleCloudAttachment($account, $attachment);
}
return array_values(array_filter($attachmentIds));
}
/**
* Add a message as attachment
*
* @param Account $account
* @param mixed[] $attachment
* @param \Horde_Imap_Client_Socket $client
* @return int|null
*/
private function handleForwardedMessageAttachment(Account $account, array $attachment, \Horde_Imap_Client_Socket $client): ?int {
$attachmentMessage = $this->mailManager->getMessage($account->getUserId(), (int)$attachment['id']);
$mailbox = $this->mailManager->getMailbox($account->getUserId(), $attachmentMessage->getMailboxId());
$fullText = $this->messageMapper->getFullText(
$client,
$mailbox->getName(),
$attachmentMessage->getUid(),
$account->getUserId()
);
// detect mime type
$mime = 'application/octet-stream';
if (extension_loaded('fileinfo')) {
$finfo = new finfo(FILEINFO_MIME_TYPE);
$detectedMime = $finfo->buffer($fullText);
if ($detectedMime !== false) {
$mime = $detectedMime;
}
}
try {
$localAttachment = $this->addFileFromString($account->getUserId(), $attachment['fileName'] ?? $attachmentMessage->getSubject() . '.eml', $mime, $fullText);
} catch (UploadException $e) {
$this->logger->error('Could not create attachment', ['exception' => $e]);
return null;
}
return $localAttachment->getId();
}
/**
* Adds an emails attachments
*
* @param Account $account
* @param mixed[] $attachment
* @param \Horde_Imap_Client_Socket $client
* @return int
* @throws DoesNotExistException
*/
private function handleForwardedAttachment(Account $account, array $attachment, \Horde_Imap_Client_Socket $client): ?int {
$mailbox = $this->mailManager->getMailbox($account->getUserId(), $attachment['mailboxId']);
$attachments = $this->messageMapper->getRawAttachments(
$client,
$mailbox->getName(),
(int)$attachment['uid'],
$account->getUserId(),
[
$attachment['id'] ?? []
]
);
if ($attachments === []) {
return null;
}
// detect mime type
$mime = 'application/octet-stream';
if (extension_loaded('fileinfo')) {
$finfo = new finfo(FILEINFO_MIME_TYPE);
$detectedMime = $finfo->buffer($attachments[0]);
if ($detectedMime !== false) {
$mime = $detectedMime;
}
}
try {
$localAttachment = $this->addFileFromString($account->getUserId(), $attachment['fileName'], $mime, $attachments[0]);
} catch (UploadException $e) {
$this->logger->error('Could not create attachment', ['exception' => $e]);
return null;
}
return $localAttachment->getId();
}
private function hasDownloadPermissions(File $file, string $fileName): bool {
$storage = $file->getStorage();
if ($storage->instanceOfStorage(SharedStorage::class)) {
/** @var SharedStorage $storage */
$share = $storage->getShare();
$attributes = $share->getAttributes();
if ($attributes->getAttribute('permissions', 'download') === false) {
$this->logger->warning('Could not create attachment, no download permission for file: ' . $fileName);
return false;
}
}
return true;
}
/**
* @param Account $account
* @param array $attachment
* @return int|null
*/
private function handleCloudAttachment(Account $account, array $attachment): ?int {
if (!isset($attachment['fileName'])) {
return null;
}
$fileName = $attachment['fileName'];
if (!$this->userFolder->nodeExists($fileName)) {
return null;
}
$file = $this->userFolder->get($fileName);
if (!$file instanceof File) {
return null;
}
if (!$this->hasDownloadPermissions($file, $fileName)) {
return null;
}
try {
$localAttachment = $this->addFileFromString($account->getUserId(), $file->getName(), $file->getMimeType(), $file->getContent());
} catch (UploadException $e) {
$this->logger->error('Could not create attachment', ['exception' => $e]);
return null;
}
return $localAttachment->getId();
}
}