*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see
Hello
=> Hello\n\n with *Hello
=> Hello\n. * * @param \DOMDocument $doc * @param \DOMNode $node * @return string|null non-null, add this text to the output and skip further processing of the node. */ public function htmlToTextCallback(\DOMDocument $doc, \DOMNode $node) { if ($node instanceof \DOMElement && strtolower($node->tagName) === 'p') { return $node->textContent . "\n"; } return null; } /** * @param NewMessageData $message * @param Message|null $previousDraft * * @return array * * @throws ClientException * @throws ServiceException */ public function saveDraft(NewMessageData $message, Message $previousDraft = null): array { $perfLogger = $this->performanceLogger->start('save draft'); $this->eventDispatcher->dispatch( SaveDraftEvent::class, new SaveDraftEvent($message->getAccount(), $message, $previousDraft) ); $perfLogger->step('emit pre event'); $account = $message->getAccount(); $imapMessage = $account->newMessage(); $imapMessage->setTo($message->getTo()); $imapMessage->setSubject($message->getSubject()); $from = new AddressList([ Address::fromRaw($account->getName(), $account->getEMailAddress()), ]); $imapMessage->setFrom($from); $imapMessage->setCC($message->getCc()); $imapMessage->setBcc($message->getBcc()); $imapMessage->setContent($message->getBody()); // build mime body $headers = [ 'From' => $imapMessage->getFrom()->first()->toHorde(), 'To' => $imapMessage->getTo()->toHorde(), 'Cc' => $imapMessage->getCC()->toHorde(), 'Bcc' => $imapMessage->getBCC()->toHorde(), 'Subject' => $imapMessage->getSubject(), 'Date' => Horde_Mime_Headers_Date::create(), ]; $mail = new Horde_Mime_Mail(); $mail->addHeaders($headers); if ($message->isHtml()) { $mail->setHtmlBody($imapMessage->getContent()); } else { $mail->setBody($imapMessage->getContent()); } $mail->addHeaderOb(Horde_Mime_Headers_MessageId::create()); $perfLogger->step('build draft message'); // 'Send' the message $client = $this->imapClientFactory->getClient($account); try { $transport = new Horde_Mail_Transport_Null(); $mail->send($transport, false, false); $perfLogger->step('create IMAP message'); // save the message in the drafts folder $draftsMailboxId = $account->getMailAccount()->getDraftsMailboxId(); if ($draftsMailboxId === null) { throw new ClientException("No drafts mailbox configured"); } $draftsMailbox = $this->mailboxMapper->findById($draftsMailboxId); $newUid = $this->messageMapper->save( $client, $draftsMailbox, $mail, [Horde_Imap_Client::FLAG_DRAFT] ); $perfLogger->step('save message on IMAP'); } catch (DoesNotExistException $e) { throw new ServiceException('Drafts mailbox does not exist', 0, $e); } catch (Horde_Exception $e) { throw new ServiceException('Could not save draft message', 0, $e); } finally { $client->logout(); } $this->eventDispatcher->dispatch( DraftSavedEvent::class, new DraftSavedEvent($account, $message, $previousDraft) ); $perfLogger->step('emit post event'); $perfLogger->end(); return [$account, $draftsMailbox, $newUid]; } private function buildReplyMessage(Account $account, NewMessageData $messageData, string $repliedToMessageId): IMessage { // Reply $message = $account->newMessage(); $message->setSubject($messageData->getSubject()); $message->setTo($messageData->getTo()); $message->setInReplyTo($repliedToMessageId); return $message; } private function buildNewMessage(Account $account, NewMessageData $messageData): IMessage { // New message $message = $account->newMessage(); $message->setTo($messageData->getTo()); $message->setSubject($messageData->getSubject()); return $message; } /** * @param Account $account * @param NewMessageData $messageData * @param IMessage $message * * @return void */ private function handleAttachments(Account $account, NewMessageData $messageData, IMessage $message): void { foreach ($messageData->getAttachments() as $attachment) { if (isset($attachment['type']) && $attachment['type'] === 'local') { // Adds an uploaded attachment $this->handleLocalAttachment($account, $attachment, $message); } elseif (isset($attachment['type']) && $attachment['type'] === 'message') { // Adds another message as attachment $this->handleForwardedMessageAttachment($account, $attachment, $message); } elseif (isset($attachment['type']) && $attachment['type'] === 'message/rfc822') { // Adds another message as attachment with mime type 'message/rfc822 $this->handleEmbeddedMessageAttachments($account, $attachment, $message); } elseif (isset($attachment['type']) && $attachment['type'] === 'message-attachment') { // Adds an attachment from another email (use case is, eg., a mail forward) $this->handleForwardedAttachment($account, $attachment, $message); } else { // Adds an attachment from Files $this->handleCloudAttachment($attachment, $message); } } } /** * @param Account $account * @param array $attachment * @param IMessage $message * * @return int|null */ private function handleLocalAttachment(Account $account, array $attachment, IMessage $message) { if (!isset($attachment['id'])) { $this->logger->warning('ignoring local attachment because its id is unknown'); return null; } $id = (int)$attachment['id']; try { [$localAttachment, $file] = $this->attachmentService->getAttachment($account->getMailAccount()->getUserId(), $id); $message->addLocalAttachment($localAttachment, $file); } catch (AttachmentNotFoundException $ex) { $this->logger->warning('ignoring local attachment because it does not exist'); // TODO: rethrow? return null; } } /** * Adds an attachment that's coming from another message's attachment (typical use case: email forwarding) * * @param Account $account * @param mixed[] $attachment * @param IMessage $message */ private function handleForwardedMessageAttachment(Account $account, array $attachment, IMessage $message): void { // Gets original of other message $userId = $account->getMailAccount()->getUserId(); $attachmentMessage = $this->mailManager->getMessage($userId, (int)$attachment['id']); $mailbox = $this->mailManager->getMailbox($userId, $attachmentMessage->getMailboxId()); $client = $this->imapClientFactory->getClient($account); try { $fullText = $this->messageMapper->getFullText( $client, $mailbox->getName(), $attachmentMessage->getUid() ); } finally { $client->logout(); } $message->addRawAttachment( $attachment['displayName'] ?? $attachmentMessage->getSubject() . '.eml', $fullText ); } /** * Adds an email as attachment * * @param Account $account * @param mixed[] $attachment * @param IMessage $message */ private function handleEmbeddedMessageAttachments(Account $account, array $attachment, IMessage $message): void { // Gets original of other message $userId = $account->getMailAccount()->getUserId(); $attachmentMessage = $this->mailManager->getMessage($userId, (int)$attachment['id']); $mailbox = $this->mailManager->getMailbox($userId, $attachmentMessage->getMailboxId()); $client = $this->imapClientFactory->getClient($account); try { $fullText = $this->messageMapper->getFullText( $client, $mailbox->getName(), $attachmentMessage->getUid() ); } finally { $client->logout(); } $message->addEmbeddedMessageAttachment( $attachment['displayName'] ?? $attachmentMessage->getSubject() . '.eml', $fullText ); } /** * Adds an attachment that's coming from another message's attachment (typical use case: email forwarding) * * @param Account $account * @param mixed[] $attachment * @param IMessage $message */ private function handleForwardedAttachment(Account $account, array $attachment, IMessage $message): void { // Gets attachment from other message $userId = $account->getMailAccount()->getUserId(); $attachmentMessage = $this->mailManager->getMessage($userId, (int)$attachment['messageId']); $mailbox = $this->mailManager->getMailbox($userId, $attachmentMessage->getMailboxId()); $client = $this->imapClientFactory->getClient($account); try { $attachments = $this->messageMapper->getRawAttachments( $client, $mailbox->getName(), $attachmentMessage->getUid(), [ $attachment['id'] ] ); } finally { $client->logout(); } // Attaches attachment to new message $message->addRawAttachment($attachment['fileName'], $attachments[0]); } /** * @param array $attachment * @param IMessage $message * * @return File|null */ private function handleCloudAttachment(array $attachment, IMessage $message) { if (!isset($attachment['fileName'])) { $this->logger->warning('ignoring cloud attachment because its fileName is unknown'); return null; } $fileName = $attachment['fileName']; if (!$this->userFolder->nodeExists($fileName)) { $this->logger->warning('ignoring cloud attachment because the node does not exist'); return null; } $file = $this->userFolder->get($fileName); if (!$file instanceof File) { $this->logger->warning('ignoring cloud attachment because the node is not a file'); return null; } $message->addAttachmentFromFiles($file); } public function sendMdn(Account $account, Mailbox $mailbox, Message $message): void { $query = new Horde_Imap_Client_Fetch_Query(); $query->flags(); $query->uid(); $query->imapDate(); $query->headerText([ 'cache' => true, 'peek' => true, ]); $imapClient = $this->imapClientFactory->getClient($account); try { /** @var Horde_Imap_Client_Data_Fetch[] $fetchResults */ $fetchResults = iterator_to_array($imapClient->fetch($mailbox->getName(), $query, [ 'ids' => new Horde_Imap_Client_Ids([$message->getUid()]), ]), false); } finally { $imapClient->logout(); } if (count($fetchResults) < 1) { throw new ServiceException('Message "' .$message->getId() . '" not found.'); } /** @var Horde_Imap_Client_DateTime $imapDate */ $imapDate = $fetchResults[0]->getImapDate(); /** @var Horde_Mime_Headers $headers */ $mdnHeaders = $fetchResults[0]->getHeaderText('0', Horde_Imap_Client_Data_Fetch::HEADER_PARSE); /** @var Horde_Mime_Headers_Addresses|null $dispositionNotificationTo */ $dispositionNotificationTo = $mdnHeaders->getHeader('disposition-notification-to'); /** @var Horde_Mime_Headers_Addresses|null $originalRecipient */ $originalRecipient = $mdnHeaders->getHeader('original-recipient'); if ($dispositionNotificationTo === null) { throw new ServiceException('Message "' .$message->getId() . '" has no disposition-notification-to header.'); } $headers = new Horde_Mime_Headers(); $headers->addHeaderOb($dispositionNotificationTo); if ($originalRecipient instanceof Horde_Mime_Headers_Addresses) { $headers->addHeaderOb($originalRecipient); } $headers->addHeaderOb(new Horde_Mime_Headers_Subject(null, $message->getSubject())); $headers->addHeaderOb(new Horde_Mime_Headers_Addresses('From', $message->getFrom()->toHorde())); $headers->addHeaderOb(new Horde_Mime_Headers_Addresses('To', $message->getTo()->toHorde())); $headers->addHeaderOb(new Horde_Mime_Headers_MessageId(null, $message->getMessageId())); $headers->addHeaderOb(new Horde_Mime_Headers_Date(null, $imapDate->format('r'))); $smtpClient = $this->smtpClientFactory->create($account); $mdn = new Horde_Mime_Mdn($headers); try { $mdn->generate( true, true, 'displayed', $account->getMailAccount()->getOutboundHost(), $smtpClient, [ 'from_addr' => $account->getEMailAddress(), 'charset' => 'UTF-8', ] ); } catch (Horde_Mime_Exception $e) { throw new ServiceException('Unable to send mdn for message "' . $message->getId() . '" caused by: ' . $e->getMessage(), 0, $e); } } }