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 Jan-Christoph Borchardt <hey@jancborchardt.net>
10 * @author Robin McCorkell <rmccorkell@karoshi.org.uk>
11 * @author Thomas Mueller <thomas.mueller@tmit.eu>
12 * @author Thomas Müller <thomas.mueller@tmit.eu>
13 *
14 * Mail
15 *
16 * This code is free software: you can redistribute it and/or modify
17 * it under the terms of the GNU Affero General Public License, version 3,
18 * as published by the Free Software Foundation.
19 *
20 * This program is distributed in the hope that it will be useful,
21 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 * GNU Affero General Public License for more details.
24 *
25 * You should have received a copy of the GNU Affero General Public License, version 3,
26 * along with this program.  If not, see <http://www.gnu.org/licenses/>
27 *
28 */
29
30namespace OCA\Mail\Model;
31
32use Exception;
33use Horde_Imap_Client;
34use Horde_Imap_Client_Data_Envelope;
35use Horde_Imap_Client_Data_Fetch;
36use Horde_Imap_Client_DateTime;
37use Horde_Imap_Client_Fetch_Query;
38use Horde_Imap_Client_Ids;
39use Horde_Imap_Client_Mailbox;
40use Horde_Imap_Client_Socket;
41use Horde_Mime_Headers;
42use Horde_Mime_Headers_MessageId;
43use Horde_Mime_Part;
44use JsonSerializable;
45use OC;
46use OCA\Mail\AddressList;
47use OCA\Mail\Db\LocalAttachment;
48use OCA\Mail\Db\MailAccount;
49use OCA\Mail\Db\Message;
50use OCA\Mail\Db\Tag;
51use OCA\Mail\Service\Html;
52use OCP\AppFramework\Db\DoesNotExistException;
53use OCP\Files\File;
54use OCP\Files\SimpleFS\ISimpleFile;
55use function in_array;
56use function mb_convert_encoding;
57use function mb_strcut;
58use function trim;
59
60class IMAPMessage implements IMessage, JsonSerializable {
61	use ConvertAddresses;
62
63	/**
64	 * @var string[]
65	 */
66	private $attachmentsToIgnore = ['signature.asc', 'smime.p7s'];
67
68	/** @var Html|null */
69	private $htmlService;
70
71	/**
72	 * @param Horde_Imap_Client_Socket|null $conn
73	 * @param Horde_Imap_Client_Mailbox|string $mailBox
74	 * @param int $messageId
75	 * @param Horde_Imap_Client_Data_Fetch|null $fetch
76	 * @param bool $loadHtmlMessage
77	 * @param Html|null $htmlService
78	 *
79	 * @throws DoesNotExistException
80	 */
81	public function __construct($conn,
82								$mailBox,
83								int $messageId,
84								Horde_Imap_Client_Data_Fetch $fetch = null,
85								bool $loadHtmlMessage = false,
86								Html $htmlService = null) {
87		$this->conn = $conn;
88		$this->mailBox = $mailBox;
89		$this->messageId = $messageId;
90		$this->loadHtmlMessage = $loadHtmlMessage;
91
92		$this->htmlService = $htmlService;
93		if (is_null($htmlService)) {
94			$urlGenerator = OC::$server->getURLGenerator();
95			$request = OC::$server->getRequest();
96			$this->htmlService = new Html($urlGenerator, $request);
97		}
98
99		if ($fetch === null) {
100			$this->loadMessageBodies();
101		} else {
102			$this->fetch = $fetch;
103		}
104	}
105
106	// output all the following:
107	public $header = null;
108	public $htmlMessage = '';
109	public $plainMessage = '';
110	public $attachments = [];
111	public $inlineAttachments = [];
112	private $loadHtmlMessage = false;
113	private $hasHtmlMessage = false;
114
115	/**
116	 * @var Horde_Imap_Client_Socket
117	 */
118	private $conn;
119
120	/**
121	 * @var Horde_Imap_Client_Mailbox
122	 */
123	private $mailBox;
124	private $messageId;
125
126	/**
127	 * @var Horde_Imap_Client_Data_Fetch
128	 */
129	private $fetch;
130
131	public static function generateMessageId(): string {
132		return Horde_Mime_Headers_MessageId::create('nextcloud-mail-generated')->value;
133	}
134
135	/**
136	 * @return int
137	 */
138	public function getUid(): int {
139		return $this->fetch->getUid();
140	}
141
142	/**
143	 * @deprecated  Seems unused
144	 * @return array
145	 */
146	public function getFlags(): array {
147		$flags = $this->fetch->getFlags();
148		return [
149			'seen' => in_array(Horde_Imap_Client::FLAG_SEEN, $flags),
150			'flagged' => in_array(Horde_Imap_Client::FLAG_FLAGGED, $flags),
151			'answered' => in_array(Horde_Imap_Client::FLAG_ANSWERED, $flags),
152			'deleted' => in_array(Horde_Imap_Client::FLAG_DELETED, $flags),
153			'draft' => in_array(Horde_Imap_Client::FLAG_DRAFT, $flags),
154			'forwarded' => in_array(Horde_Imap_Client::FLAG_FORWARDED, $flags),
155			'hasAttachments' => $this->hasAttachments($this->fetch->getStructure()),
156			'mdnsent' => in_array(Horde_Imap_Client::FLAG_MDNSENT, $flags, true),
157			'important' => in_array(Tag::LABEL_IMPORTANT, $flags, true)
158		];
159	}
160
161	/**
162	 * @deprecated  Seems unused
163	 * @param string[] $flags
164	 *
165	 * @throws Exception
166	 *
167	 * @return void
168	 */
169	public function setFlags(array $flags) {
170		// TODO: implement
171		throw new Exception('Not implemented');
172	}
173
174	/**
175	 * @return Horde_Imap_Client_Data_Envelope
176	 */
177	public function getEnvelope() {
178		return $this->fetch->getEnvelope();
179	}
180
181	private function getRawReferences(): string {
182		/** @var Horde_Mime_Headers $headers */
183		$headers = $this->fetch->getHeaderText('0', Horde_Imap_Client_Data_Fetch::HEADER_PARSE);
184		$header = $headers->getHeader('references');
185		if ($header === null) {
186			return '';
187		}
188		return $header->value_single;
189	}
190
191	private function getRawInReplyTo(): string {
192		return $this->fetch->getEnvelope()->in_reply_to;
193	}
194
195	public function getDispositionNotificationTo(): string {
196		/** @var Horde_Mime_Headers $headers */
197		$headers = $this->fetch->getHeaderText('0', Horde_Imap_Client_Data_Fetch::HEADER_PARSE);
198		$header = $headers->getHeader('disposition-notification-to');
199		if ($header === null) {
200			return '';
201		}
202		return $header->value_single;
203	}
204
205	/**
206	 * @return AddressList
207	 */
208	public function getFrom(): AddressList {
209		return AddressList::fromHorde($this->getEnvelope()->from);
210	}
211
212	/**
213	 * @param AddressList $from
214	 *
215	 * @throws Exception
216	 *
217	 * @return void
218	 */
219	public function setFrom(AddressList $from) {
220		throw new Exception('IMAP message is immutable');
221	}
222
223	/**
224	 * @return AddressList
225	 */
226	public function getTo(): AddressList {
227		return AddressList::fromHorde($this->getEnvelope()->to);
228	}
229
230	/**
231	 * @param AddressList $to
232	 *
233	 * @throws Exception
234	 *
235	 * @return void
236	 */
237	public function setTo(AddressList $to) {
238		throw new Exception('IMAP message is immutable');
239	}
240
241	/**
242	 * @return AddressList
243	 */
244	public function getCC(): AddressList {
245		return AddressList::fromHorde($this->getEnvelope()->cc);
246	}
247
248	/**
249	 * @param AddressList $cc
250	 *
251	 * @throws Exception
252	 *
253	 * @return void
254	 */
255	public function setCC(AddressList $cc) {
256		throw new Exception('IMAP message is immutable');
257	}
258
259	/**
260	 * @return AddressList
261	 */
262	public function getBCC(): AddressList {
263		return AddressList::fromHorde($this->getEnvelope()->bcc);
264	}
265
266	/**
267	 * @param AddressList $bcc
268	 *
269	 * @throws Exception
270	 *
271	 * @return void
272	 */
273	public function setBcc(AddressList $bcc) {
274		throw new Exception('IMAP message is immutable');
275	}
276
277	/**
278	 * Get the ID if available
279	 *
280	 * @return string
281	 */
282	public function getMessageId(): string {
283		return $this->getEnvelope()->message_id;
284	}
285
286	/**
287	 * @return string
288	 */
289	public function getSubject(): string {
290		// Try a soft conversion first (some installations, eg: Alpine linux,
291		// have issues with the '//IGNORE' option)
292		$subject = $this->getEnvelope()->subject;
293		$utf8 = iconv('UTF-8', 'UTF-8', $subject);
294		if ($utf8 !== false) {
295			return $utf8;
296		}
297		return iconv("UTF-8", "UTF-8//IGNORE", $subject);
298	}
299
300	/**
301	 * @param string $subject
302	 *
303	 * @throws Exception
304	 *
305	 * @return void
306	 */
307	public function setSubject(string $subject) {
308		throw new Exception('IMAP message is immutable');
309	}
310
311	/**
312	 * @return Horde_Imap_Client_DateTime
313	 */
314	public function getSentDate(): Horde_Imap_Client_DateTime {
315		return $this->fetch->getImapDate();
316	}
317
318	/**
319	 * @return int
320	 */
321	public function getSize(): int {
322		return $this->fetch->getSize();
323	}
324
325	/**
326	 * @param Horde_Mime_Part $part
327	 *
328	 * @return bool
329	 */
330	private function hasAttachments($part) {
331		foreach ($part->getParts() as $p) {
332			if ($p->isAttachment() || $p->getType() === 'message/rfc822') {
333				return true;
334			}
335			if ($this->hasAttachments($p)) {
336				return true;
337			}
338		}
339
340		return false;
341	}
342
343	private function loadMessageBodies(): void {
344		$fetch_query = new Horde_Imap_Client_Fetch_Query();
345		$fetch_query->envelope();
346		$fetch_query->structure();
347		$fetch_query->flags();
348		$fetch_query->size();
349		$fetch_query->imapDate();
350		$fetch_query->headerText([
351			'cache' => true,
352			'peek' => true,
353		]);
354
355		// $list is an array of Horde_Imap_Client_Data_Fetch objects.
356		$ids = new Horde_Imap_Client_Ids($this->messageId);
357		$headers = $this->conn->fetch($this->mailBox, $fetch_query, ['ids' => $ids]);
358		/** @var Horde_Imap_Client_Data_Fetch $fetch */
359		$fetch = $headers[$this->messageId];
360		if (is_null($fetch)) {
361			throw new DoesNotExistException("This email ($this->messageId) can't be found. Probably it was deleted from the server recently. Please reload.");
362		}
363
364		// set $this->fetch to get to, from ...
365		$this->fetch = $fetch;
366
367		// analyse the body part
368		$structure = $fetch->getStructure();
369
370		// debugging below
371		$structure_type = $structure->getPrimaryType();
372		if ($structure_type === 'multipart') {
373			$i = 1;
374			foreach ($structure->getParts() as $p) {
375				$this->getPart($p, $i++);
376			}
377		} else {
378			if (!is_null($structure->findBody())) {
379				// get the body from the server
380				$partId = (int)$structure->findBody();
381				$this->getPart($structure->getPart($partId), $partId);
382			}
383		}
384	}
385
386	/**
387	 * @param Horde_Mime_Part $p
388	 * @param mixed $partNo
389	 *
390	 * @throws DoesNotExistException
391	 *
392	 * @return void
393	 */
394	private function getPart(Horde_Mime_Part $p, $partNo): void {
395		// Regular attachments
396		if ($p->isAttachment() || $p->getType() === 'message/rfc822') {
397			$this->attachments[] = [
398				'id' => $p->getMimeId(),
399				'messageId' => $this->messageId,
400				'fileName' => $p->getName(),
401				'mime' => $p->getType(),
402				'size' => $p->getBytes(),
403				'cid' => $p->getContentId(),
404				'disposition' => $p->getDisposition()
405			];
406			return;
407		}
408
409		// Inline attachments
410		// Horde doesn't consider parts with content-disposition set to inline as
411		// attachment so we need to use another way to get them.
412		// We use these inline attachments to render a message's html body in $this->getHtmlBody()
413		$filename = $p->getName();
414		if ($p->getType() === 'message/rfc822' || isset($filename)) {
415			if (in_array($filename, $this->attachmentsToIgnore)) {
416				return;
417			}
418			$this->inlineAttachments[] = [
419				'id' => $p->getMimeId(),
420				'messageId' => $this->messageId,
421				'fileName' => $filename,
422				'mime' => $p->getType(),
423				'size' => $p->getBytes(),
424				'cid' => $p->getContentId()
425			];
426			return;
427		}
428
429		if ($p->getPrimaryType() === 'multipart') {
430			$this->handleMultiPartMessage($p, $partNo);
431			return;
432		}
433
434		if ($p->getType() === 'text/plain') {
435			$this->handleTextMessage($p, $partNo);
436			return;
437		}
438
439		if ($p->getType() === 'text/calendar') {
440			$this->attachments[] = [
441				'id' => $p->getMimeId(),
442				'messageId' => $this->messageId,
443				'fileName' => $p->getName() ?? 'calendar.ics',
444				'mime' => $p->getType(),
445				'size' => $p->getBytes(),
446				'cid' => $p->getContentId(),
447				'disposition' => $p->getDisposition()
448			];
449			return;
450		}
451
452		if ($p->getType() === 'text/html') {
453			$this->handleHtmlMessage($p, $partNo);
454			return;
455		}
456
457		// EMBEDDED MESSAGE
458		// Many bounce notifications embed the original message as type 2,
459		// but AOL uses type 1 (multipart), which is not handled here.
460		// There are no PHP functions to parse embedded messages,
461		// so this just appends the raw source to the main message.
462		if ($p[0] === 'message') {
463			$data = $this->loadBodyData($p, $partNo);
464			$this->plainMessage .= trim($data) . "\n\n";
465		}
466	}
467
468	/**
469	 * @param int $id
470	 *
471	 * @return array
472	 */
473	public function getFullMessage(int $id): array {
474		$mailBody = $this->plainMessage;
475
476		$data = $this->jsonSerialize();
477		if ($this->hasHtmlMessage) {
478			$data['hasHtmlBody'] = true;
479			$data['body'] = $this->getHtmlBody($id);
480			$data['attachments'] = $this->attachments;
481		} else {
482			$mailBody = $this->htmlService->convertLinks($mailBody);
483			[$mailBody, $signature] = $this->htmlService->parseMailBody($mailBody);
484			$data['body'] = $mailBody;
485			$data['signature'] = $signature;
486			$data['attachments'] = array_merge($this->attachments, $this->inlineAttachments);
487		}
488
489		return $data;
490	}
491
492	/**
493	 * @return array
494	 */
495	public function jsonSerialize(): array {
496		return [
497			'uid' => $this->getUid(),
498			'messageId' => $this->getMessageId(),
499			'from' => $this->getFrom()->jsonSerialize(),
500			'to' => $this->getTo()->jsonSerialize(),
501			'cc' => $this->getCC()->jsonSerialize(),
502			'bcc' => $this->getBCC()->jsonSerialize(),
503			'subject' => $this->getSubject(),
504			'dateInt' => $this->getSentDate()->getTimestamp(),
505			'flags' => $this->getFlags(),
506			'hasHtmlBody' => $this->hasHtmlMessage,
507			'dispositionNotificationTo' => $this->getDispositionNotificationTo(),
508		];
509	}
510
511	/**
512	 * @param int $id
513	 *
514	 * @return string
515	 */
516	public function getHtmlBody(int $id): string {
517		return $this->htmlService->sanitizeHtmlMailBody($this->htmlMessage, [
518			'id' => $id,
519		], function ($cid) {
520			$match = array_filter($this->inlineAttachments,
521				function ($a) use ($cid) {
522					return $a['cid'] === $cid;
523				});
524			$match = array_shift($match);
525			if ($match === null) {
526				return null;
527			}
528			return $match['id'];
529		});
530	}
531
532	/**
533	 * @return string
534	 */
535	public function getPlainBody(): string {
536		return $this->plainMessage;
537	}
538
539	/**
540	 * @param Horde_Mime_Part $part
541	 * @param mixed $partNo
542	 *
543	 * @throws DoesNotExistException
544	 *
545	 * @return void
546	 */
547	private function handleMultiPartMessage(Horde_Mime_Part $part, $partNo): void {
548		$i = 1;
549		foreach ($part->getParts() as $p) {
550			$this->getPart($p, "$partNo.$i");
551			$i++;
552		}
553	}
554
555	/**
556	 * @param Horde_Mime_Part $p
557	 * @param mixed $partNo
558	 *
559	 * @throws DoesNotExistException
560	 *
561	 * @return void
562	 */
563	private function handleTextMessage(Horde_Mime_Part $p, $partNo): void {
564		$data = $this->loadBodyData($p, $partNo);
565		$this->plainMessage .= trim($data) . "\n\n";
566	}
567
568	/**
569	 * @param Horde_Mime_Part $p
570	 * @param mixed $partNo
571	 *
572	 * @throws DoesNotExistException
573	 *
574	 * @return void
575	 */
576	private function handleHtmlMessage(Horde_Mime_Part $p, $partNo): void {
577		$this->hasHtmlMessage = true;
578		if ($this->loadHtmlMessage) {
579			$data = $this->loadBodyData($p, $partNo);
580			$this->htmlMessage .= $data . "<br><br>";
581		}
582	}
583
584	/**
585	 * @param Horde_Mime_Part $p
586	 * @param mixed $partNo
587	 *
588	 * @return string
589	 * @throws DoesNotExistException
590	 * @throws Exception
591	 */
592	private function loadBodyData(Horde_Mime_Part $p, $partNo): string {
593		// DECODE DATA
594		$fetch_query = new Horde_Imap_Client_Fetch_Query();
595		$ids = new Horde_Imap_Client_Ids($this->messageId);
596
597		$fetch_query->bodyPart($partNo, [
598			'peek' => true
599		]);
600		$fetch_query->bodyPartSize($partNo);
601		$fetch_query->mimeHeader($partNo, [
602			'peek' => true
603		]);
604
605		$headers = $this->conn->fetch($this->mailBox, $fetch_query, ['ids' => $ids]);
606		/* @var $fetch Horde_Imap_Client_Data_Fetch */
607		$fetch = $headers[$this->messageId];
608		if (is_null($fetch)) {
609			throw new DoesNotExistException("Mail body for this mail($this->messageId) could not be loaded");
610		}
611
612		$mimeHeaders = $fetch->getMimeHeader($partNo, Horde_Imap_Client_Data_Fetch::HEADER_PARSE);
613		if ($enc = $mimeHeaders->getValue('content-transfer-encoding')) {
614			$p->setTransferEncoding($enc);
615		}
616
617		$data = $fetch->getBodyPart($partNo);
618
619		$p->setContents($data);
620		$data = $p->getContents();
621
622		$data = mb_convert_encoding($data, 'UTF-8', $p->getCharset());
623		return $data;
624	}
625
626	public function getContent(): string {
627		return $this->getPlainBody();
628	}
629
630	/**
631	 * @return void
632	 */
633	public function setContent(string $content) {
634		throw new Exception('IMAP message is immutable');
635	}
636
637	/**
638	 * @return Horde_Mime_Part[]
639	 */
640	public function getAttachments(): array {
641		throw new Exception('not implemented');
642	}
643
644	/**
645	 * @param string $name
646	 * @param string $content
647	 *
648	 * @return void
649	 */
650	public function addRawAttachment(string $name, string $content): void {
651		throw new Exception('IMAP message is immutable');
652	}
653
654	/**
655	 * @param string $name
656	 * @param string $content
657	 *
658	 * @return void
659	 */
660	public function addEmbeddedMessageAttachment(string $name, string $content): void {
661		throw new Exception('IMAP message is immutable');
662	}
663
664	/**
665	 * @param File $file
666	 *
667	 * @return void
668	 */
669	public function addAttachmentFromFiles(File $file) {
670		throw new Exception('IMAP message is immutable');
671	}
672
673	/**
674	 * @param LocalAttachment $attachment
675	 * @param ISimpleFile $file
676	 *
677	 * @return void
678	 */
679	public function addLocalAttachment(LocalAttachment $attachment, ISimpleFile $file) {
680		throw new Exception('IMAP message is immutable');
681	}
682
683	/**
684	 * @return string|null
685	 */
686	public function getInReplyTo() {
687		throw new Exception('not implemented');
688	}
689
690	/**
691	 * @param string $id
692	 *
693	 * @return void
694	 */
695	public function setInReplyTo(string $id) {
696		throw new Exception('not implemented');
697	}
698
699	/**
700	 * Cast all values from an IMAP message into the correct DB format
701	 *
702	 * @param integer $mailboxId
703	 * @return Message
704	 */
705	public function toDbMessage(int $mailboxId, MailAccount $account): Message {
706		$msg = new Message();
707
708		$messageId = $this->getMessageId();
709		$msg->setMessageId($messageId);
710
711		// Sometimes the message ID is missing or invalid and therefore not set.
712		// Then we create one and set it.
713		if ($msg->getMessageId() === null || trim($msg->getMessageId()) === '') {
714			$messageId = self::generateMessageId();
715			$msg->setMessageId($messageId);
716		}
717
718		$msg->setUid($this->getUid());
719		$msg->setRawReferences($this->getRawReferences());
720		$msg->setThreadRootId($messageId);
721		$msg->setInReplyTo($this->getRawInReplyTo());
722		$msg->setMailboxId($mailboxId);
723		$msg->setFrom($this->getFrom());
724		$msg->setTo($this->getTo());
725		$msg->setCc($this->getCc());
726		$msg->setBcc($this->getBcc());
727		$msg->setSubject(mb_strcut($this->getSubject(), 0, 255));
728		$msg->setSentAt($this->getSentDate()->getTimestamp());
729
730		$flags = $this->fetch->getFlags();
731		$msg->setFlagAnswered(in_array(Horde_Imap_Client::FLAG_ANSWERED, $flags, true));
732		$msg->setFlagDeleted(in_array(Horde_Imap_Client::FLAG_DELETED, $flags, true));
733		$msg->setFlagDraft(in_array(Horde_Imap_Client::FLAG_DRAFT, $flags, true));
734		$msg->setFlagFlagged(in_array(Horde_Imap_Client::FLAG_FLAGGED, $flags, true));
735		$msg->setFlagSeen(in_array(Horde_Imap_Client::FLAG_SEEN, $flags, true));
736		$msg->setFlagForwarded(in_array(Horde_Imap_Client::FLAG_FORWARDED, $flags, true));
737		$msg->setFlagJunk(
738			in_array(Horde_Imap_Client::FLAG_JUNK, $flags, true) ||
739			in_array('junk', $flags, true)
740		);
741		$msg->setFlagNotjunk(in_array(Horde_Imap_Client::FLAG_NOTJUNK, $flags, true) || in_array('nonjunk', $flags, true));// While this is not a standard IMAP Flag, Thunderbird uses it to mark "not junk"
742		// @todo remove this as soon as possible @link https://github.com/nextcloud/mail/issues/25
743		$msg->setFlagImportant(in_array('$important', $flags, true) || in_array('$labelimportant', $flags, true) || in_array(Tag::LABEL_IMPORTANT, $flags, true));
744		$msg->setFlagAttachments(false);
745		$msg->setFlagMdnsent(in_array(Horde_Imap_Client::FLAG_MDNSENT, $flags, true));
746
747		$allowed = [
748			Horde_Imap_Client::FLAG_ANSWERED,
749			Horde_Imap_Client::FLAG_FLAGGED,
750			Horde_Imap_Client::FLAG_FORWARDED,
751			Horde_Imap_Client::FLAG_DELETED,
752			Horde_Imap_Client::FLAG_DRAFT,
753			Horde_Imap_Client::FLAG_JUNK,
754			Horde_Imap_Client::FLAG_NOTJUNK,
755			'nonjunk', // While this is not a standard IMAP Flag, Thunderbird uses it to mark "not junk"
756			Horde_Imap_Client::FLAG_MDNSENT,
757			Horde_Imap_Client::FLAG_RECENT,
758			Horde_Imap_Client::FLAG_SEEN,
759		];
760
761		// remove all standard IMAP flags from $filters
762		$tags = array_filter($flags, function ($flag) use ($allowed) {
763			return in_array($flag, $allowed, true) === false;
764		});
765
766		if (empty($tags) === true) {
767			return $msg;
768		}
769		// cast all leftover $flags to be used as tags
770		$msg->setTags($this->generateTagEntites($tags, $account->getUserId()));
771		return $msg;
772	}
773
774	/**
775	 * Build tag entities from keywords sent by IMAP
776	 *
777	 * Will use IMAP keyword '$xxx' to create a value for
778	 * display_name like 'xxx'
779	 *
780	 * @link https://github.com/nextcloud/mail/issues/25
781	 * @link https://github.com/nextcloud/mail/issues/5150
782	 *
783	 * @param string[] $tags
784	 * @return Tag[]
785	 */
786	private function generateTagEntites(array $tags, string $userId): array {
787		$t = [];
788		foreach ($tags as $keyword) {
789			if ($keyword === '$important' || $keyword === 'important' || $keyword === '$labelimportant') {
790				$keyword = Tag::LABEL_IMPORTANT;
791			}
792			if ($keyword === '$labelwork') {
793				$keyword = Tag::LABEL_WORK;
794			}
795			if ($keyword === '$labelpersonal') {
796				$keyword = Tag::LABEL_PERSONAL;
797			}
798			if ($keyword === '$labeltodo') {
799				$keyword = Tag::LABEL_TODO;
800			}
801			if ($keyword === '$labellater') {
802				$keyword = Tag::LABEL_LATER;
803			}
804
805			$displayName = str_replace(['_', '$'], [' ', ''], $keyword);
806			$displayName = strtoupper($displayName);
807			$displayName = mb_convert_encoding($displayName, 'UTF-8', 'UTF7-IMAP');
808			$displayName = strtolower($displayName);
809			$displayName = ucwords($displayName);
810
811			$keyword = mb_strcut($keyword, 0, 64);
812			$displayName = mb_strcut($displayName, 0, 128);
813
814			$tag = new Tag();
815			$tag->setImapLabel($keyword);
816			$tag->setDisplayName($displayName);
817			$tag->setUserId($userId);
818			$t[] = $tag;
819		}
820		return $t;
821	}
822}
823