1<?php
2
3declare(strict_types=1);
4
5/**
6 * @author Alexander Weidinger <alexwegoo@gmail.com>
7 * @author Christoph Wurst <christoph@winzerhof-wurst.at>
8 * @author Christoph Wurst <wurst.christoph@gmail.com>
9 * @author Jakob Sack <jakob@owncloud.org>
10 * @author Jan-Christoph Borchardt <hey@jancborchardt.net>
11 * @author Lukas Reschke <lukas@owncloud.com>
12 * @author Thomas Imbreckx <zinks@iozero.be>
13 * @author Thomas Müller <thomas.mueller@tmit.eu>
14 *
15 * Mail
16 *
17 * This code is free software: you can redistribute it and/or modify
18 * it under the terms of the GNU Affero General Public License, version 3,
19 * as published by the Free Software Foundation.
20 *
21 * This program is distributed in the hope that it will be useful,
22 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 * GNU Affero General Public License for more details.
25 *
26 * You should have received a copy of the GNU Affero General Public License, version 3,
27 * along with this program.  If not, see <http://www.gnu.org/licenses/>
28 *
29 */
30
31namespace OCA\Mail\Controller;
32
33use Exception;
34use OC\Security\CSP\ContentSecurityPolicyNonceManager;
35use OCA\Mail\Contracts\IMailManager;
36use OCA\Mail\Contracts\IMailSearch;
37use OCA\Mail\Contracts\IMailTransmission;
38use OCA\Mail\Contracts\ITrustedSenderService;
39use OCA\Mail\Db\Message;
40use OCA\Mail\Exception\ClientException;
41use OCA\Mail\Exception\ServiceException;
42use OCA\Mail\Http\AttachmentDownloadResponse;
43use OCA\Mail\Http\HtmlResponse;
44use OCA\Mail\Service\AccountService;
45use OCA\Mail\Service\ItineraryService;
46use OCP\AppFramework\Controller;
47use OCP\AppFramework\Db\DoesNotExistException;
48use OCP\AppFramework\Http;
49use OCP\AppFramework\Http\ContentSecurityPolicy;
50use OCP\AppFramework\Http\JSONResponse;
51use OCP\AppFramework\Http\Response;
52use OCP\AppFramework\Http\TemplateResponse;
53use OCP\AppFramework\Http\ZipResponse;
54use OCP\Files\Folder;
55use OCP\Files\IMimeTypeDetector;
56use OCP\IL10N;
57use OCP\IRequest;
58use OCP\IURLGenerator;
59use Psr\Log\LoggerInterface;
60use function array_map;
61
62class MessagesController extends Controller {
63
64	/** @var AccountService */
65	private $accountService;
66
67	/** @var IMailManager */
68	private $mailManager;
69
70	/** @var IMailSearch */
71	private $mailSearch;
72
73	/** @var ItineraryService */
74	private $itineraryService;
75
76	/** @var string */
77	private $currentUserId;
78
79	/** @var LoggerInterface */
80	private $logger;
81
82	/** @var Folder */
83	private $userFolder;
84
85	/** @var IMimeTypeDetector */
86	private $mimeTypeDetector;
87
88	/** @var IL10N */
89	private $l10n;
90
91	/** @var IURLGenerator */
92	private $urlGenerator;
93
94	/** @var ContentSecurityPolicyNonceManager */
95	private $nonceManager;
96
97	/** @var ITrustedSenderService */
98	private $trustedSenderService;
99
100	/** @var IMailTransmission */
101	private $mailTransmission;
102
103	/**
104	 * @param string $appName
105	 * @param IRequest $request
106	 * @param AccountService $accountService
107	 * @param IMailManager $mailManager
108	 * @param IMailSearch $mailSearch
109	 * @param ItineraryService $itineraryService
110	 * @param string $UserId
111	 * @param $userFolder
112	 * @param LoggerInterface $logger
113	 * @param IL10N $l10n
114	 * @param IMimeTypeDetector $mimeTypeDetector
115	 * @param IURLGenerator $urlGenerator
116	 * @param ContentSecurityPolicyNonceManager $nonceManager
117	 * @param ITrustedSenderService $trustedSenderService
118	 * @param IMailTransmission $mailTransmission
119	 */
120	public function __construct(string $appName,
121								IRequest $request,
122								AccountService $accountService,
123								IMailManager $mailManager,
124								IMailSearch $mailSearch,
125								ItineraryService $itineraryService,
126								string $UserId,
127								$userFolder,
128								LoggerInterface $logger,
129								IL10N $l10n,
130								IMimeTypeDetector $mimeTypeDetector,
131								IURLGenerator $urlGenerator,
132								ContentSecurityPolicyNonceManager $nonceManager,
133								ITrustedSenderService $trustedSenderService,
134								IMailTransmission $mailTransmission) {
135		parent::__construct($appName, $request);
136
137		$this->accountService = $accountService;
138		$this->mailManager = $mailManager;
139		$this->mailSearch = $mailSearch;
140		$this->itineraryService = $itineraryService;
141		$this->currentUserId = $UserId;
142		$this->userFolder = $userFolder;
143		$this->logger = $logger;
144		$this->l10n = $l10n;
145		$this->mimeTypeDetector = $mimeTypeDetector;
146		$this->urlGenerator = $urlGenerator;
147		$this->mailManager = $mailManager;
148		$this->nonceManager = $nonceManager;
149		$this->trustedSenderService = $trustedSenderService;
150		$this->mailTransmission = $mailTransmission;
151	}
152
153	/**
154	 * @NoAdminRequired
155	 * @TrapError
156	 *
157	 * @param int $mailboxId
158	 * @param int $cursor
159	 * @param string $filter
160	 * @param int|null $limit
161	 *
162	 * @return JSONResponse
163	 *
164	 * @throws ClientException
165	 * @throws ServiceException
166	 */
167
168	public function index(int $mailboxId,
169						  int $cursor = null,
170						  string $filter = null,
171						  int $limit = null): JSONResponse {
172		try {
173			$mailbox = $this->mailManager->getMailbox($this->currentUserId, $mailboxId);
174			$account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId());
175		} catch (DoesNotExistException $e) {
176			return new JSONResponse([], Http::STATUS_FORBIDDEN);
177		}
178
179		$this->logger->debug("loading messages of folder <$mailboxId>");
180
181		return new JSONResponse(
182			$this->mailSearch->findMessages(
183				$account,
184				$mailbox,
185				$filter === '' ? null : $filter,
186				$cursor,
187				$limit
188			)
189		);
190	}
191
192	/**
193	 * @NoAdminRequired
194	 * @TrapError
195	 *
196	 * @param int $accountId
197	 * @param string $folderId
198	 * @param int $id
199	 *
200	 * @return JSONResponse
201	 *
202	 * @throws ClientException
203	 * @throws ServiceException
204	 */
205	public function show(int $id): JSONResponse {
206		try {
207			$message = $this->mailManager->getMessage($this->currentUserId, $id);
208			$mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId());
209			$account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId());
210		} catch (DoesNotExistException $e) {
211			return new JSONResponse([], Http::STATUS_FORBIDDEN);
212		}
213
214		$this->logger->debug("loading message <$id>");
215
216		return new JSONResponse(
217			$this->mailSearch->findMessage(
218				$account,
219				$mailbox,
220				$message
221			)
222		);
223	}
224
225	/**
226	 * @NoAdminRequired
227	 * @TrapError
228	 *
229	 * @param int $id
230	 *
231	 * @return JSONResponse
232	 *
233	 * @throws ClientException
234	 * @throws ServiceException
235	 */
236	public function getBody(int $id): JSONResponse {
237		try {
238			$message = $this->mailManager->getMessage($this->currentUserId, $id);
239			$mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId());
240			$account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId());
241		} catch (DoesNotExistException $e) {
242			return new JSONResponse([], Http::STATUS_FORBIDDEN);
243		}
244
245		$json = $this->mailManager->getImapMessage(
246			$account,
247			$mailbox,
248			$message->getUid(),
249			true
250		)->getFullMessage($id);
251		$json['itineraries'] = $this->itineraryService->extract(
252			$account,
253			$mailbox->getName(),
254			$message->getUid()
255		);
256		$json['attachments'] = array_map(function ($a) use ($id) {
257			return $this->enrichDownloadUrl(
258				$id,
259				$a
260			);
261		}, $json['attachments']);
262		$json['accountId'] = $account->getId();
263		$json['mailboxId'] = $mailbox->getId();
264		$json['databaseId'] = $message->getId();
265		$json['isSenderTrusted'] = $this->isSenderTrusted($message);
266
267		$response = new JSONResponse($json);
268
269		// Enable caching
270		$response->cacheFor(60 * 60);
271
272		return $response;
273	}
274
275	private function isSenderTrusted(Message $message): bool {
276		$from = $message->getFrom();
277		$first = $from->first();
278		if ($first === null) {
279			return false;
280		}
281		$email = $first->getEmail();
282		if ($email === null) {
283			return false;
284		}
285		return $this->trustedSenderService->isTrusted(
286			$this->currentUserId,
287			$email
288		);
289	}
290
291	/**
292	 * @NoAdminRequired
293	 * @NoCSRFRequired
294	 * @TrapError
295	 *
296	 * @param int $id
297	 *
298	 * @return JSONResponse
299	 * @throws ClientException
300	 */
301	public function getThread(int $id): JSONResponse {
302		try {
303			$message = $this->mailManager->getMessage($this->currentUserId, $id);
304			$mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId());
305			$account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId());
306		} catch (DoesNotExistException $e) {
307			return new JSONResponse([], Http::STATUS_FORBIDDEN);
308		}
309
310		return new JSONResponse($this->mailManager->getThread($account, $message->getThreadRootId()));
311	}
312
313	/**
314	 * @NoAdminRequired
315	 * @TrapError
316	 *
317	 * @param int $id
318	 * @param int $destFolderId
319	 *
320	 * @return JSONResponse
321	 *
322	 * @throws ClientException
323	 * @throws ServiceException
324	 */
325	public function move(int $id, int $destFolderId): JSONResponse {
326		try {
327			$message = $this->mailManager->getMessage($this->currentUserId, $id);
328			$srcMailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId());
329			$dstMailbox = $this->mailManager->getMailbox($this->currentUserId, $destFolderId);
330			$srcAccount = $this->accountService->find($this->currentUserId, $srcMailbox->getAccountId());
331			$dstAccount = $this->accountService->find($this->currentUserId, $dstMailbox->getAccountId());
332		} catch (DoesNotExistException $e) {
333			return new JSONResponse([], Http::STATUS_FORBIDDEN);
334		}
335
336		$this->mailManager->moveMessage(
337			$srcAccount,
338			$srcMailbox->getName(),
339			$message->getUid(),
340			$dstAccount,
341			$dstMailbox->getName()
342		);
343		return new JSONResponse();
344	}
345
346	/**
347	 * @NoAdminRequired
348	 * @TrapError
349	 *
350	 * @param int $id
351	 *
352	 * @return JSONResponse
353	 *
354	 * @throws ClientException
355	 * @throws ServiceException
356	 */
357	public function mdn(int $id): JSONResponse {
358		try {
359			$message = $this->mailManager->getMessage($this->currentUserId, $id);
360			$mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId());
361			$account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId());
362		} catch (DoesNotExistException $e) {
363			return new JSONResponse([], Http::STATUS_FORBIDDEN);
364		}
365
366		if ($message->getFlagMdnsent()) {
367			return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
368		}
369
370		try {
371			$this->mailTransmission->sendMdn($account, $mailbox, $message);
372			$this->mailManager->flagMessage($account, $mailbox->getName(), $message->getUid(), 'mdnsent', true);
373		} catch (ServiceException $ex) {
374			$this->logger->error('Sending mdn failed: ' . $ex->getMessage());
375			throw $ex;
376		}
377
378		return new JSONResponse();
379	}
380
381	/**
382	 * @NoAdminRequired
383	 * @NoCSRFRequired
384	 * @TrapError
385	 *
386	 * @param int $accountId
387	 * @param string $folderId
388	 * @param int $messageId
389	 *
390	 * @return JSONResponse
391	 * @throws ServiceException
392	 */
393	public function getSource(int $id): JSONResponse {
394		try {
395			$message = $this->mailManager->getMessage($this->currentUserId, $id);
396			$mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId());
397			$account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId());
398		} catch (DoesNotExistException $e) {
399			return new JSONResponse([], Http::STATUS_FORBIDDEN);
400		}
401
402		$response = new JSONResponse([
403			'source' => $this->mailManager->getSource(
404				$account,
405				$mailbox->getName(),
406				$message->getUid()
407			)
408		]);
409
410		// Enable caching
411		$response->cacheFor(60 * 60);
412
413		return $response;
414	}
415
416	/**
417	 * @NoAdminRequired
418	 * @NoCSRFRequired
419	 * @TrapError
420	 *
421	 * @param int $id
422	 * @param bool $plain do not inject scripts if true (default=false)
423	 *
424	 * @return HtmlResponse|TemplateResponse
425	 *
426	 * @throws ClientException
427	 */
428	public function getHtmlBody(int $id, bool $plain = false): Response {
429		try {
430			try {
431				$message = $this->mailManager->getMessage($this->currentUserId, $id);
432				$mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId());
433				$account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId());
434			} catch (DoesNotExistException $e) {
435				return new TemplateResponse(
436					$this->appName,
437					'error',
438					['message' => 'Not allowed'],
439					'none'
440				);
441			}
442
443			$html = $this->mailManager->getImapMessage(
444				$account,
445				$mailbox,
446				$message->getUid(),
447				true
448			)->getHtmlBody(
449				$id
450			);
451			$htmlResponse = $plain ?
452				HtmlResponse::plain($html) :
453				HtmlResponse::withResizer(
454					$html,
455					$this->nonceManager->getNonce(),
456					$this->urlGenerator->getAbsoluteURL(
457						$this->urlGenerator->linkTo('mail', 'js/htmlresponse.js')
458					)
459				);
460
461			// Harden the default security policy
462			$policy = new ContentSecurityPolicy();
463			$policy->allowEvalScript(false);
464			$policy->disallowScriptDomain('\'self\'');
465			$policy->disallowConnectDomain('\'self\'');
466			$policy->disallowFontDomain('\'self\'');
467			$policy->disallowMediaDomain('\'self\'');
468			$htmlResponse->setContentSecurityPolicy($policy);
469
470			// Enable caching
471			$htmlResponse->cacheFor(60 * 60);
472
473			return $htmlResponse;
474		} catch (Exception $ex) {
475			return new TemplateResponse(
476				$this->appName,
477				'error',
478				['message' => $ex->getMessage()],
479				'none'
480			);
481		}
482	}
483
484	/**
485	 * @NoAdminRequired
486	 * @NoCSRFRequired
487	 * @TrapError
488	 *
489	 * @param int $accountId
490	 * @param string $folderId
491	 * @param int $id
492	 * @param string $attachmentId
493	 *
494	 * @return Response
495	 *
496	 * @throws ClientException
497	 * @throws ServiceException
498	 */
499	public function downloadAttachment(int $id,
500									   string $attachmentId): Response {
501		try {
502			$message = $this->mailManager->getMessage($this->currentUserId, $id);
503			$mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId());
504			$account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId());
505		} catch (DoesNotExistException $e) {
506			return new JSONResponse([], Http::STATUS_FORBIDDEN);
507		}
508		$folder = $account->getMailbox($mailbox->getName());
509		$attachment = $folder->getAttachment($message->getUid(), $attachmentId);
510
511		// Body party and embedded messages do not have a name
512		if ($attachment->getName() === null) {
513			return new AttachmentDownloadResponse(
514				$attachment->getContents(),
515				$this->l10n->t('Embedded message %s', [
516					$attachmentId,
517				]) . '.eml',
518				$attachment->getType()
519			);
520		}
521		return new AttachmentDownloadResponse(
522			$attachment->getContents(),
523			$attachment->getName(),
524			$attachment->getType()
525		);
526	}
527
528	/**
529	 * @NoAdminRequired
530	 * @NoCSRFRequired
531	 * @TrapError
532	 *
533	 * @param int $id the message id
534	 * @param string $attachmentId
535	 *
536	 * @return ZipResponse|JSONResponse
537	 *
538	 * @throws ClientException
539	 * @throws ServiceException
540	 * @throws DoesNotExistException
541	 */
542	public function downloadAttachments(int $id): Response {
543		try {
544			$message = $this->mailManager->getMessage($this->currentUserId, $id);
545			$mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId());
546			$account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId());
547		} catch (DoesNotExistException $e) {
548			return new JSONResponse([], Http::STATUS_FORBIDDEN);
549		}
550
551		$attachments = $this->mailManager->getMailAttachments($account, $mailbox, $message);
552		$zip = new ZipResponse($this->request, 'attachments');
553
554		foreach ($attachments as $attachment) {
555			$fileName = $attachment['name'];
556			$fh = fopen("php://temp", 'r+');
557			fputs($fh, $attachment['content']);
558			$size = (int)$attachment['size'];
559			rewind($fh);
560			$zip->addResource($fh, $fileName, $size);
561		}
562		return $zip;
563	}
564
565	/**
566	 * @NoAdminRequired
567	 * @TrapError
568	 *
569	 * @param int $id
570	 * @param string $attachmentId
571	 * @param string $targetPath
572	 *
573	 * @return JSONResponse
574	 *
575	 * @throws ClientException
576	 * @throws ServiceException
577	 */
578	public function saveAttachment(int $id,
579								   string $attachmentId,
580								   string $targetPath) {
581		try {
582			$message = $this->mailManager->getMessage($this->currentUserId, $id);
583			$mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId());
584			$account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId());
585		} catch (DoesNotExistException $e) {
586			return new JSONResponse([], Http::STATUS_FORBIDDEN);
587		}
588		$folder = $account->getMailbox($mailbox->getName());
589
590		if ($attachmentId === '0') {
591			// Save all attachments
592			/* @var $m IMAPMessage */
593			$m = $folder->getMessage($message->getUid());
594			$attachmentIds = array_map(function ($a) {
595				return $a['id'];
596			}, $m->attachments);
597		} else {
598			$attachmentIds = [$attachmentId];
599		}
600
601		foreach ($attachmentIds as $aid) {
602			$attachment = $folder->getAttachment($message->getUid(), $aid);
603
604			$fileName = $attachment->getName() ?? $this->l10n->t('Embedded message %s', [
605				$aid,
606			]) . '.eml';
607			$fileParts = pathinfo($fileName);
608			$fileName = $fileParts['filename'];
609			$fileExtension = $fileParts['extension'];
610			$fullPath = "$targetPath/$fileName.$fileExtension";
611			$counter = 2;
612			while ($this->userFolder->nodeExists($fullPath)) {
613				$fullPath = "$targetPath/$fileName ($counter).$fileExtension";
614				$counter++;
615			}
616
617			$newFile = $this->userFolder->newFile($fullPath);
618			$newFile->putContent($attachment->getContents());
619		}
620		return new JSONResponse();
621	}
622
623	/**
624	 * @NoAdminRequired
625	 * @TrapError
626	 *
627	 * @param int $id
628	 * @param array $flags
629	 *
630	 * @return JSONResponse
631	 *
632	 * @throws ClientException
633	 * @throws ServiceException
634	 */
635	public function setFlags(int $id, array $flags): JSONResponse {
636		try {
637			$message = $this->mailManager->getMessage($this->currentUserId, $id);
638			$mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId());
639			$account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId());
640		} catch (DoesNotExistException $e) {
641			return new JSONResponse([], Http::STATUS_FORBIDDEN);
642		}
643
644		foreach ($flags as $flag => $value) {
645			$value = filter_var($value, FILTER_VALIDATE_BOOLEAN);
646			$this->mailManager->flagMessage($account, $mailbox->getName(), $message->getUid(), $flag, $value);
647		}
648		return new JSONResponse();
649	}
650
651	/**
652	 * @NoAdminRequired
653	 * @TrapError
654	 *
655	 * @param int $id
656	 * @param string $imapLabel
657	 *
658	 * @return JSONResponse
659	 *
660	 * @throws ClientException
661	 * @throws ServiceException
662	 */
663	public function setTag(int $id, string $imapLabel): JSONResponse {
664		try {
665			$message = $this->mailManager->getMessage($this->currentUserId, $id);
666			$mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId());
667			$account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId());
668		} catch (DoesNotExistException $e) {
669			return new JSONResponse([], Http::STATUS_FORBIDDEN);
670		}
671
672		try {
673			$tag = $this->mailManager->getTagByImapLabel($imapLabel, $this->currentUserId);
674		} catch (ClientException $e) {
675			return new JSONResponse([], Http::STATUS_FORBIDDEN);
676		}
677
678		$this->mailManager->tagMessage($account, $mailbox->getName(), $message, $tag, true);
679		return new JSONResponse($tag);
680	}
681
682	/**
683	 * @NoAdminRequired
684	 * @TrapError
685	 *
686	 * @param int $id
687	 * @param string $imapLabel
688	 *
689	 * @return JSONResponse
690	 *
691	 * @throws ClientException
692	 * @throws ServiceException
693	 */
694	public function removeTag(int $id, string $imapLabel): JSONResponse {
695		try {
696			$message = $this->mailManager->getMessage($this->currentUserId, $id);
697			$mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId());
698			$account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId());
699		} catch (DoesNotExistException $e) {
700			return new JSONResponse([], Http::STATUS_FORBIDDEN);
701		}
702
703		try {
704			$tag = $this->mailManager->getTagByImapLabel($imapLabel, $this->currentUserId);
705		} catch (ClientException $e) {
706			return new JSONResponse([], Http::STATUS_FORBIDDEN);
707		}
708
709		$this->mailManager->tagMessage($account, $mailbox->getName(), $message, $tag, false);
710		return new JSONResponse($tag);
711	}
712
713	/**
714	 * @NoAdminRequired
715	 * @TrapError
716	 *
717	 * @param int $accountId
718	 * @param string $folderId
719	 * @param int $id
720	 *
721	 * @return JSONResponse
722	 *
723	 * @throws ClientException
724	 * @throws ServiceException
725	 */
726	public function destroy(int $id): JSONResponse {
727		try {
728			$message = $this->mailManager->getMessage($this->currentUserId, $id);
729			$mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId());
730			$account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId());
731		} catch (DoesNotExistException $e) {
732			return new JSONResponse([], Http::STATUS_FORBIDDEN);
733		}
734
735		$this->logger->debug("deleting message <$id>");
736
737		$this->mailManager->deleteMessage(
738			$account,
739			$mailbox->getName(),
740			$message->getUid()
741		);
742		return new JSONResponse();
743	}
744
745	/**
746	 * @param int $id
747	 * @param array $attachment
748	 *
749	 * @return array
750	 */
751	private function enrichDownloadUrl(int $id,
752									   array $attachment) {
753		$downloadUrl = $this->urlGenerator->linkToRoute('mail.messages.downloadAttachment',
754			[
755				'id' => $id,
756				'attachmentId' => $attachment['id'],
757			]);
758		$downloadUrl = $this->urlGenerator->getAbsoluteURL($downloadUrl);
759		$attachment['downloadUrl'] = $downloadUrl;
760		$attachment['mimeUrl'] = $this->mimeTypeDetector->mimeTypeIcon($attachment['mime']);
761
762		$attachment['isImage'] = $this->attachmentIsImage($attachment);
763		$attachment['isCalendarEvent'] = $this->attachmentIsCalendarEvent($attachment);
764
765		return $attachment;
766	}
767
768	/**
769	 * Determines if the content of this attachment is an image
770	 *
771	 * @param array $attachment
772	 *
773	 * @return boolean
774	 */
775	private function attachmentIsImage(array $attachment): bool {
776		return in_array(
777			$attachment['mime'], [
778				'image/jpeg',
779				'image/png',
780				'image/gif'
781			]);
782	}
783
784	/**
785	 * @param array $attachment
786	 *
787	 * @return boolean
788	 */
789	private function attachmentIsCalendarEvent(array $attachment): bool {
790		return in_array($attachment['mime'], ['text/calendar', 'application/ics'], true);
791	}
792}
793