diff --git a/lib/Contracts/IAttachmentService.php b/lib/Contracts/IAttachmentService.php index 38d6ee9a4f..243cdd186a 100644 --- a/lib/Contracts/IAttachmentService.php +++ b/lib/Contracts/IAttachmentService.php @@ -34,4 +34,5 @@ public function getAttachment(string $userId, int $id): array; * @param int $id */ public function deleteAttachment(string $userId, int $id); + } diff --git a/lib/Db/Message.php b/lib/Db/Message.php index 746021fe4c..eda357fe43 100644 --- a/lib/Db/Message.php +++ b/lib/Db/Message.php @@ -143,6 +143,8 @@ class Message extends Entity implements JsonSerializable { /** @var bool */ private $fetchAvatarFromClient = false; + /** @var array */ + private $attachments = []; public function __construct() { $this->from = new AddressList([]); @@ -313,6 +315,14 @@ public function getAvatar(): ?Avatar { return $this->avatar; } + public function setAttachments(array $attachments): void { + $this->attachments = $attachments; + } + + public function getAttachments(): array { + return $this->attachments; + } + #[\Override] #[ReturnTypeWillChange] public function jsonSerialize() { @@ -358,6 +368,7 @@ public function jsonSerialize() { 'mentionsMe' => $this->getMentionsMe(), 'avatar' => $this->avatar?->jsonSerialize(), 'fetchAvatarFromClient' => $this->fetchAvatarFromClient, + 'attachments' => $this->getAttachments(), ]; } } diff --git a/lib/IMAP/PreviewEnhancer.php b/lib/IMAP/PreviewEnhancer.php index eadc094cac..36a1cc2d80 100644 --- a/lib/IMAP/PreviewEnhancer.php +++ b/lib/IMAP/PreviewEnhancer.php @@ -15,6 +15,7 @@ use OCA\Mail\Db\Message; use OCA\Mail\Db\MessageMapper as DbMapper; use OCA\Mail\IMAP\MessageMapper as ImapMapper; +use OCA\Mail\Service\Attachment\AttachmentService; use OCA\Mail\Service\Avatar\Avatar; use OCA\Mail\Service\AvatarService; use Psr\Log\LoggerInterface; @@ -39,11 +40,14 @@ class PreviewEnhancer { /** @var AvatarService */ private $avatarService; - public function __construct(IMAPClientFactory $clientFactory, + public function __construct( + IMAPClientFactory $clientFactory, ImapMapper $imapMapper, DbMapper $dbMapper, LoggerInterface $logger, - AvatarService $avatarService) { + AvatarService $avatarService, + private AttachmentService $attachmentService, + ) { $this->clientFactory = $clientFactory; $this->imapMapper = $imapMapper; $this->mapper = $dbMapper; @@ -65,6 +69,12 @@ public function process(Account $account, Mailbox $mailbox, array $messages, boo return array_merge($carry, [$message->getUid()]); }, []); + $client = $this->clientFactory->getClient($account); + + foreach ($messages as $message) { + $attachments = $this->attachmentService->getAttachmentNames($account, $mailbox, $message, $client); + $message->setAttachments($attachments); + } if ($preLoadAvatars) { foreach ($messages as $message) { @@ -87,7 +97,7 @@ public function process(Account $account, Mailbox $mailbox, array $messages, boo return $messages; } - $client = $this->clientFactory->getClient($account); + try { $data = $this->imapMapper->getBodyStructureData( $client, diff --git a/lib/Model/IMAPMessage.php b/lib/Model/IMAPMessage.php index 2842b49a8b..a8bbec036a 100644 --- a/lib/Model/IMAPMessage.php +++ b/lib/Model/IMAPMessage.php @@ -287,6 +287,7 @@ public function getSentDate(): Horde_Imap_Client_DateTime { return $this->imapDate; } + /** * @param int $id * @@ -386,7 +387,7 @@ public function setContent(string $content) { */ #[\Override] public function getAttachments(): array { - throw new Exception('not implemented'); + return $this->attachments; } /** diff --git a/lib/Service/Attachment/AttachmentService.php b/lib/Service/Attachment/AttachmentService.php index 6bc208de3e..45aaa3f0ba 100644 --- a/lib/Service/Attachment/AttachmentService.php +++ b/lib/Service/Attachment/AttachmentService.php @@ -18,7 +18,10 @@ use OCA\Mail\Db\LocalAttachment; use OCA\Mail\Db\LocalAttachmentMapper; use OCA\Mail\Db\LocalMessage; +use OCA\Mail\Db\Mailbox; +use OCA\Mail\Db\Message; use OCA\Mail\Exception\AttachmentNotFoundException; +use OCA\Mail\Exception\ServiceException; use OCA\Mail\Exception\UploadException; use OCA\Mail\IMAP\MessageMapper; use OCP\AppFramework\Db\DoesNotExistException; @@ -26,6 +29,9 @@ use OCP\Files\Folder; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\IURLGenerator; use Psr\Log\LoggerInterface; class AttachmentService implements IAttachmentService { @@ -51,22 +57,30 @@ class AttachmentService implements IAttachmentService { * @var LoggerInterface */ private $logger; - + /** + * @var ICache + */ + private $cache; /** * @param Folder $userFolder */ - public function __construct($userFolder, + public function __construct( + $userFolder, LocalAttachmentMapper $mapper, AttachmentStorage $storage, IMailManager $mailManager, MessageMapper $imapMessageMapper, - LoggerInterface $logger) { + ICacheFactory $cacheFactory, + private IURLGenerator $urlGenerator, + LoggerInterface $logger, + ) { $this->mapper = $mapper; $this->storage = $storage; $this->mailManager = $mailManager; $this->messageMapper = $imapMessageMapper; $this->userFolder = $userFolder; $this->logger = $logger; + $this->cache = $cacheFactory->createLocal('mail.attachment_names'); } /** @@ -247,6 +261,39 @@ public function handleAttachments(Account $account, array $attachments, \Horde_I return array_values(array_filter($attachmentIds)); } + public function getAttachmentNames(Account $account, Mailbox $mailbox, Message $message, \Horde_Imap_Client_Socket $client): array { + $attachments = []; + $uniqueCacheId = $account->getUserId() . $account->getId() . $mailbox->getId() . $message->getUid(); + $cached = $this->cache->get($uniqueCacheId); + if ($cached) { + return $cached; + } + try { + $imapMessage = $this->mailManager->getImapMessage( + $client, + $account, + $mailbox, + $message->getUid(), + true + ); + $attachments = $imapMessage->getAttachments(); + } catch (ServiceException $e) { + $this->logger->error('Could not get attachment names', ['exception' => $e, 'messageId' => $message->getUid()]); + } + + $result = array_map(function ($attachment) use ($message) { + $downloadUrl = $this->urlGenerator->linkToRoute('mail.messages.downloadAttachment', + [ + 'id' => $message->getId(), + 'attachmentId' => $attachment['id'], + ]); + $downloadUrl = $this->urlGenerator->getAbsoluteURL($downloadUrl); + return ['id' => $attachment['id'] , 'fileName' => $attachment['fileName'], 'mime' => $attachment['mime'], 'downloadUrl' => $downloadUrl]; + }, $attachments); + $this->cache->set($uniqueCacheId, $result); + return $result; + } + /** * Add a message as attachment * diff --git a/tests/Integration/Service/DraftServiceIntegrationTest.php b/tests/Integration/Service/DraftServiceIntegrationTest.php index 7453d2c633..ec762efb66 100644 --- a/tests/Integration/Service/DraftServiceIntegrationTest.php +++ b/tests/Integration/Service/DraftServiceIntegrationTest.php @@ -32,8 +32,10 @@ use OCP\AppFramework\Utility\ITimeFactory; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Folder; +use OCP\ICacheFactory; use OCP\IDBConnection; use OCP\IServerContainer; +use OCP\IURLGenerator; use OCP\IUser; use OCP\Server; use Psr\Container\ContainerInterface; @@ -96,6 +98,8 @@ protected function setUp(): void { Server::get(AttachmentStorage::class), $mailManager, Server::get(MessageMapper::class), + Server::get(ICacheFactory::class), + Server::get(IURLGenerator::class), new NullLogger() ); $this->client = $this->getClient($this->account); diff --git a/tests/Integration/Service/OutboxServiceIntegrationTest.php b/tests/Integration/Service/OutboxServiceIntegrationTest.php index a55eed5d49..04e2319fd9 100644 --- a/tests/Integration/Service/OutboxServiceIntegrationTest.php +++ b/tests/Integration/Service/OutboxServiceIntegrationTest.php @@ -34,8 +34,10 @@ use OCP\AppFramework\Utility\ITimeFactory; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Folder; +use OCP\ICacheFactory; use OCP\IDBConnection; use OCP\IServerContainer; +use OCP\IURLGenerator; use OCP\IUser; use OCP\Server; use Psr\Container\ContainerInterface; @@ -97,6 +99,8 @@ protected function setUp(): void { Server::get(AttachmentStorage::class), $mailManager, Server::get(\OCA\Mail\IMAP\MessageMapper::class), + Server::get(ICacheFactory::class), + Server::get(IURLGenerator::class), new NullLogger() ); $this->client = $this->getClient($this->account); diff --git a/tests/Unit/IMAP/PreviewEnhancerTest.php b/tests/Unit/IMAP/PreviewEnhancerTest.php index a234592d92..8f848e1d6b 100644 --- a/tests/Unit/IMAP/PreviewEnhancerTest.php +++ b/tests/Unit/IMAP/PreviewEnhancerTest.php @@ -10,6 +10,7 @@ namespace Unit\IMAP; use ChristophWurst\Nextcloud\Testing\TestCase; +use Horde_Imap_Client_Socket; use OCA\Mail\Address; use OCA\Mail\AddressList; use OCA\Mail\Db\Message; @@ -17,6 +18,7 @@ use OCA\Mail\IMAP\IMAPClientFactory; use OCA\Mail\IMAP\MessageMapper as ImapMapper; use OCA\Mail\IMAP\PreviewEnhancer; +use OCA\Mail\Service\Attachment\AttachmentService; use OCA\Mail\Service\Avatar\Avatar; use OCA\Mail\Service\AvatarService; use PHPUnit\Framework\MockObject\MockObject; @@ -37,6 +39,8 @@ class PreviewEnhancerTest extends TestCase { private $avatarService; /** @var PreviewEnhancer */ private $previewEnhancer; + /** @var AttachmentService|MockObject */ + private $attachmentService; protected function setUp(): void { parent::setUp(); @@ -46,16 +50,21 @@ protected function setUp(): void { $this->dbMapper = $this->createMock(DbMapper::class); $this->logger = $this->createMock(LoggerInterface::class); $this->avatarService = $this->createMock(AvatarService::class); + $this->attachmentService = $this->createMock(AttachmentService::class); - $this->previewEnhancer = new previewEnhancer($this->imapClientFactory, + $this->previewEnhancer = new PreviewEnhancer( + $this->imapClientFactory, $this->imapMapper, $this->dbMapper, $this->logger, - $this->avatarService); + $this->avatarService, + $this->attachmentService + ); } public function testAvatars(): void { - + $account = $this->createMock(\OCA\Mail\Account::class); + $mailbox = $this->createMock(\OCA\Mail\Db\Mailbox::class); $message1 = new Message(); $message1->setId(1); $message1->setStructureAnalyzed(true); @@ -66,6 +75,20 @@ public function testAvatars(): void { $message2->setFrom(new AddressList([Address::fromRaw('Bob', 'bob@example.com')])); $messages = [$message1, $message2]; $message2Avatar = new Avatar('example.com', 'image/png', true); + $client = $this->createMock(Horde_Imap_Client_Socket::class); + $this->imapClientFactory->expects($this->once()) + ->method('getClient') + ->with($account) + ->willReturn($client); + $this->attachmentService->expects($this->exactly(2)) + ->method('getAttachmentNames') + ->withConsecutive( + [$account, $mailbox, $message1, $client], + [$account, $mailbox, $message2, $client], + ) + ->willReturnOnConsecutiveCalls( + [], [] + ); $this->avatarService->expects($this->exactly(2)) ->method('getCachedAvatar') ->withConsecutive( @@ -77,8 +100,8 @@ public function testAvatars(): void { $message2Avatar ); $this->previewEnhancer->process( - $this->createMock(\OCA\Mail\Account::class), - $this->createMock(\OCA\Mail\Db\Mailbox::class), + $account, + $mailbox, $messages, true, 'testuser' diff --git a/tests/Unit/Service/Attachment/AttachmentServiceTest.php b/tests/Unit/Service/Attachment/AttachmentServiceTest.php index 9689fd420a..1e2d98f165 100644 --- a/tests/Unit/Service/Attachment/AttachmentServiceTest.php +++ b/tests/Unit/Service/Attachment/AttachmentServiceTest.php @@ -26,6 +26,8 @@ use OCP\AppFramework\Db\DoesNotExistException; use OCP\Files\Folder; use OCP\Files\NotPermittedException; +use OCP\ICacheFactory; +use OCP\IURLGenerator; use OCP\Share\IAttributes; use OCP\Share\IShare; use PHPUnit\Framework\MockObject\MockObject; @@ -50,9 +52,15 @@ class AttachmentServiceTest extends TestCase { /** @var MessageMapper|MockObject */ private $messageMapper; + /** @var ICacheFactory|MockObject */ + private $cacheFactory; + /** @var MockObject|LoggerInterface */ private $logger; + /** @var MockObject|IURLGenerator */ + private $urlGenerator; + protected function setUp(): void { parent::setUp(); @@ -61,6 +69,8 @@ protected function setUp(): void { $this->mailManager = $this->createMock(IMailManager::class); $this->messageMapper = $this->createMock(MessageMapper::class); $this->userFolder = $this->createMock(Folder::class); + $this->cacheFactory = $this->createMock(ICacheFactory::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); $this->logger = $this->createMock(LoggerInterface::class); $this->service = new AttachmentService( @@ -69,6 +79,8 @@ protected function setUp(): void { $this->storage, $this->mailManager, $this->messageMapper, + $this->cacheFactory, + $this->urlGenerator, $this->logger ); }