diff options
author | Daniel Kesselberg <mail@danielkesselberg.de> | 2022-08-23 23:57:12 +0300 |
---|---|---|
committer | Daniel Kesselberg <mail@danielkesselberg.de> | 2022-09-02 16:02:46 +0300 |
commit | 50ad334480d4b260ceeefc20b523de89f9878220 (patch) | |
tree | e0e8d0444ae1c53ee1aa2ab6348118e41f1c0273 /lib | |
parent | 2df77810de01a42ad21fdb57a28a83eeeeed0068 (diff) |
Create a multipart/related message for html, text and inline images
Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
Diffstat (limited to 'lib')
-rw-r--r-- | lib/Service/MailTransmission.php | 81 | ||||
-rw-r--r-- | lib/Service/MimeMessage.php | 173 |
2 files changed, 181 insertions, 73 deletions
diff --git a/lib/Service/MailTransmission.php b/lib/Service/MailTransmission.php index 79c65bcd8..73b50c8c1 100644 --- a/lib/Service/MailTransmission.php +++ b/lib/Service/MailTransmission.php @@ -38,8 +38,6 @@ use Horde_Mime_Headers_MessageId; use Horde_Mime_Headers_Subject; use Horde_Mime_Mail; use Horde_Mime_Mdn; -use Horde_Mime_Part; -use Horde_Text_Filter; use OCA\Mail\Account; use OCA\Mail\Address; use OCA\Mail\AddressList; @@ -59,7 +57,6 @@ use OCA\Mail\Events\MessageSentEvent; use OCA\Mail\Events\SaveDraftEvent; use OCA\Mail\Exception\AttachmentNotFoundException; use OCA\Mail\Exception\ClientException; -use OCA\Mail\Exception\InvalidDataUriException; use OCA\Mail\Exception\SentMailboxNotSetException; use OCA\Mail\Exception\ServiceException; use OCA\Mail\IMAP\IMAPClientFactory; @@ -191,59 +188,16 @@ class MailTransmission implements IMailTransmission { $mail = new Horde_Mime_Mail(); $mail->addHeaders($headers); - if ($messageData->isHtml()) { - $doc = new \DOMDocument(); - $doc->loadHTML($message->getContent(), LIBXML_HTML_NODEFDTD | LIBXML_HTML_NOIMPLIED); - - $uriParser = new DataUriParser(); - - $images = $doc->getElementsByTagName('img'); - for ($i = 0; $i < $images->count(); $i++) { - $image = $images->item($i); - if ($image === null) { - continue; - } - - $src = $image->getAttribute('src'); - if ($src === '') { - continue; - } - - try { - $dataUri = $uriParser->parse($src); - } catch (InvalidDataUriException $e) { - continue; - } - - $part = new Horde_Mime_Part(); - $part->setCharset($dataUri->getParameters()['charset']); - $part->setDisposition('inline'); - $part->setName('embedded_image_' . $i); - $part->setContents($dataUri->getData()); - $part->setType($dataUri->getMediaType()); - if ($dataUri->isBase64()) { - $part->setTransferEncoding('base64'); - } - - $cid = $part->setContentId(); - $mail->addMimePart($part); - - $image->setAttribute('src', 'cid:' . $cid); - } - - $htmlContent = $doc->saveHTML(); - $mail->setHtmlBody($htmlContent, null, false); - $mail->setBody(Horde_Text_Filter::filter($htmlContent, 'Html2text', - ['callback' => [$this, 'htmlToTextCallback']])); - } else { - $mail->setBody($message->getContent()); - } + $mimeMessage = new MimeMessage( + new DataUriParser() + ); - // Append local attachments - foreach ($message->getAttachments() as $attachment) { - $mail->addMimePart($attachment); - } + $mail->setBasePart($mimeMessage->build( + $messageData->isHtml(), + $message->getContent(), + $message->getAttachments() + )); $this->eventDispatcher->dispatchTyped( new BeforeMessageSentEvent($account, $messageData, $repliedToMessageId, $draft, $message, $mail) @@ -326,25 +280,6 @@ class MailTransmission implements IMailTransmission { } /** - * A callback for Horde_Text_Filter. - * - * The purpose of this callback is to overwrite the default behaviour - * of html2text filter to convert <p>Hello</p> => Hello\n\n with - * <p>Hello</p> => 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 * diff --git a/lib/Service/MimeMessage.php b/lib/Service/MimeMessage.php new file mode 100644 index 000000000..bc5d8a0e7 --- /dev/null +++ b/lib/Service/MimeMessage.php @@ -0,0 +1,173 @@ +<?php + +declare(strict_types=1); + +/** + * @author Daniel Kesselberg <mail@danielkesselberg.de> + * + * 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 <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Mail\Service; + +use DOMDocument; +use DOMNode; +use Horde_Mime_Part; +use Horde_Text_Filter; +use OCA\Mail\Exception\InvalidDataUriException; +use OCA\Mail\Service\DataUri\DataUriParser; + +class MimeMessage { + private DataUriParser $uriParser; + + public function __construct(DataUriParser $uriParser) { + $this->uriParser = $uriParser; + } + + /** + * @param bool $isHtml + * @param string $content + * @param Horde_Mime_Part[] $attachments + * @return void + */ + public function build(bool $isHtml, string $content, array $attachments): Horde_Mime_Part { + if ($isHtml) { + $doc = new DOMDocument(); + $doc->loadHTML($content, LIBXML_HTML_NODEFDTD | LIBXML_HTML_NOIMPLIED); + + $images = $doc->getElementsByTagName('img'); + $imageParts = []; + + for ($i = 0; $i < $images->count(); $i++) { + $image = $images->item($i); + if ($image === null) { + continue; + } + + $src = $image->getAttribute('src'); + if ($src === '') { + continue; + } + + try { + $dataUri = $this->uriParser->parse($src); + } catch (InvalidDataUriException $e) { + continue; + } + + $part = new Horde_Mime_Part(); + $part->setType($dataUri->getMediaType()); + $part->setCharset($dataUri->getParameters()['charset']); + $part->setName('embedded_image_' . $i); + $part->setDisposition('inline'); + if ($dataUri->isBase64()) { + $part->setTransferEncoding('base64'); + } + $part->setContents($dataUri->getData()); + + $cid = $part->setContentId(); + $imageParts[] = $part; + + $image->setAttribute('src', 'cid:' . $cid); + } + + $htmlContent = $doc->saveHTML(); + $textContent = Horde_Text_Filter::filter($htmlContent, 'Html2text', ['callback' => [$this, 'htmlToTextCallback']]); + + $alternativePart = new Horde_Mime_Part(); + $alternativePart->setType('multipart/alternative'); + + $htmlPart = new Horde_Mime_Part(); + $htmlPart->setType('text/html'); + $htmlPart->setCharset('UTF-8'); + $htmlPart->setContents($htmlContent); + $htmlPart->setDescription('HTML Version of Message'); + + $textPart = new Horde_Mime_Part(); + $textPart->setType('text/plain'); + $textPart->setCharset('UTF-8'); + $textPart->setContents($textContent); + $textPart->setDescription('Plaintext Version of Message'); + + /* + * RFC1341: In general, user agents that compose multipart/alternative entities should place the + * body parts in increasing order of preference, that is, with the preferred format last. + */ + $alternativePart[] = $textPart; + $alternativePart[] = $htmlPart; + + /* + * Wrap the multipart/alternative parts in multipart/related when inline images are found. + */ + if (count($imageParts) > 0) { + $bodyPart = new Horde_Mime_Part(); + $bodyPart->setType('multipart/related'); + $bodyPart[] = $alternativePart; + foreach ($imageParts as $imagePart) { + $bodyPart[] = $imagePart; + } + } else { + $bodyPart = $alternativePart; + } + } else { + $bodyPart = new Horde_Mime_Part(); + $bodyPart->setType('text/plain'); + $bodyPart->setCharset('UTF-8'); + $bodyPart->setContents($content); + } + + /* + * For attachments wrap the body (multipart/related, multipart/alternative or text/plain) in + * a multipart/mixed part. + */ + if (count($attachments) > 0) { + $basePart = new Horde_Mime_Part(); + $basePart->setType('multipart/mixed'); + $basePart[] = $bodyPart; + foreach ($attachments as $attachment) { + $basePart[] = $attachment; + } + } else { + $basePart = $bodyPart; + } + + /* + * To add the Mime-Version-Header + */ + $basePart->isBasePart(true); + + return $basePart; + } + + /** + * A callback for Horde_Text_Filter. + * + * The purpose of this callback is to overwrite the default behaviour + * of html2text filter to convert <p>Hello</p> => Hello\n\n with + * <p>Hello</p> => 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; + } +} |