transmissionService->getAddressList($localMessage, Recipient::TYPE_TO); $cc = $this->transmissionService->getAddressList($localMessage, Recipient::TYPE_CC); $bcc = $this->transmissionService->getAddressList($localMessage, Recipient::TYPE_BCC); $attachments = $this->transmissionService->getAttachments($localMessage); $name = $account->getName(); $emailAddress = $account->getEMailAddress(); if ($localMessage->getAliasId() !== null) { try { $alias = $this->aliasesService->find($localMessage->getAliasId(), $account->getUserId()); $name = ($alias->getName() ?? $name); $emailAddress = $alias->getAlias(); } catch (DoesNotExistException) { $this->logger->debug('The assigned alias no longer exists. Falling back to the default name and email address. It is likely that the alias was deleted or deprovisioned in the meantime.', [ 'aliasId' => $localMessage->getAliasId(), 'accountId' => $account->getId(), ]); } } $from = Address::fromRaw($name, $emailAddress); $attachmentParts = []; foreach ($attachments as $attachment) { $part = $this->transmissionService->handleAttachment($account, $attachment); if ($part !== null) { $attachmentParts[] = $part; } } $transport = $this->smtpClientFactory->create($account); // build mime body $headers = [ 'From' => $from->toHorde(), 'To' => $to->toHorde(), 'Cc' => $cc->toHorde(), 'Bcc' => $bcc->toHorde(), 'Subject' => $localMessage->getSubject(), ]; // The table (oc_local_messages) currently only allows for a single reply to message id // but we already set the 'references' header for an email so we could support multiple references // Get the previous message and then concatenate all its "References" message ids with this one if (($inReplyTo = $localMessage->getInReplyToMessageId()) !== null) { $headers['References'] = $inReplyTo; $headers['In-Reply-To'] = $inReplyTo; } if ($localMessage->getRequestMdn()) { $headers[Horde_Mime_Mdn::MDN_HEADER] = $from->toHorde(); } $mail = new Horde_Mime_Mail(); $mail->addHeaders($headers); $mimeMessage = new MimeMessage( new DataUriParser() ); $mimePart = $mimeMessage->build( $localMessage->getBodyPlain(), $localMessage->getBodyHtml(), $attachmentParts, $localMessage->isPgpMime() === true ); // TODO: add smimeEncrypt check if implemented try { $mimePart = $this->transmissionService->getSignMimePart($localMessage, $account, $mimePart); $mimePart = $this->transmissionService->getEncryptMimePart($localMessage, $to, $cc, $bcc, $account, $mimePart); } catch (ServiceException $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); return; } $mail->setBasePart($mimePart); // Send the message try { $mail->send($transport, false, false); $localMessage->setRaw($mail->getRaw(false)); $localMessage->setStatus(LocalMessage::STATUS_RAW); } catch (Horde_Mime_Exception $e) { if ($e->getPrevious() instanceof Horde_Smtp_Exception) { /** @var Horde_Smtp_Exception $previousException */ $previousException = $e->getPrevious(); $this->logger->error('SMTP error: ' . $e->getMessage(), [ 'exception' => $e, 'smtpErrorCode' => $previousException->getSmtpCode(), ]); } else { $this->logger->error($e->getMessage(), ['exception' => $e]); } if (in_array($e->getCode(), self::RETRIABLE_CODES, true)) { $localMessage->setStatus(LocalMessage::STATUS_SMPT_SEND_FAIL); return; } $localMessage->setStatus(LocalMessage::STATUS_ERROR); return; } finally { if ($transport instanceof Horde_Mail_Transport_Smtphorde) { try { $transport->getSMTPObject()->logout(); } catch (\Throwable $e) { // Handle silently as this is a resource usage optimization } } } $this->eventDispatcher->dispatchTyped( new MessageSentEvent($account, $localMessage) ); } #[\Override] public function saveLocalDraft(Account $account, LocalMessage $message): void { $to = $this->transmissionService->getAddressList($message, Recipient::TYPE_TO); $cc = $this->transmissionService->getAddressList($message, Recipient::TYPE_CC); $bcc = $this->transmissionService->getAddressList($message, Recipient::TYPE_BCC); $attachments = $this->transmissionService->getAttachments($message); $perfLogger = $this->performanceLogger->start('save local draft'); $imapMessage = new ModelMessage(); $imapMessage->setTo($to); $imapMessage->setSubject($message->getSubject()); $from = new AddressList([ Address::fromRaw($account->getName(), $account->getEMailAddress()), ]); $imapMessage->setFrom($from); $imapMessage->setCC($cc); $imapMessage->setBcc($bcc); if ($message->isHtml() === true) { $imapMessage->setContent($message->getBodyHtml()); } else { $imapMessage->setContent($message->getBodyPlain()); } foreach ($attachments as $attachment) { $this->transmissionService->handleAttachment($account, $attachment); } // 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 local 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 draft message'); $draftsMailbox = $this->findOrCreateDraftsMailbox($account); $this->messageMapper->save( $client, $draftsMailbox, $mail->getRaw(false), [Horde_Imap_Client::FLAG_DRAFT] ); $perfLogger->step('save local draft 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->dispatchTyped(new DraftSavedEvent($account, null)); $perfLogger->step('emit post local draft save event'); $perfLogger->end(); } private function findOrCreateDraftsMailbox(Account $account): Mailbox { $draftsMailboxId = $account->getMailAccount()->getDraftsMailboxId(); if ($draftsMailboxId === null) { return $this->mailManager->createMailbox( $account, 'Drafts', [Horde_Imap_Client::SPECIALUSE_DRAFTS] ); } return $this->mailboxMapper->findById($draftsMailboxId); } /** * @param NewMessageData $message * @param Message|null $previousDraft * * @return array * * @throws ClientException * @throws ServiceException */ #[\Override] 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 = new ModelMessage(); $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->getRaw(false), [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]; } #[\Override] 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.'); } $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); } } }