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