generated from Nodarx/template
feat: add mime builder for smtp and imap
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
+4
-2
@@ -20,7 +20,9 @@
|
||||
"ext-iconv": "*",
|
||||
"ext-ctype": "*",
|
||||
"psr/log": "^1.0|^2.0|^3.0",
|
||||
"doctrine/lexer": "^3.0"
|
||||
"doctrine/lexer": "^3.0",
|
||||
"symfony/mime": "^7.0",
|
||||
"egulias/email-validator": "^4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^11.0"
|
||||
@@ -43,4 +45,4 @@
|
||||
"test:unit": "phpunit --configuration tests/php/phpunit.unit.xml --colors=always --testdox",
|
||||
"test:coverage": "XDEBUG_MODE=coverage phpunit --configuration tests/php/phpunit.unit.xml --coverage-html .phpunit.coverage --coverage-text"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+488
-4
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "46ea36f63cc6f77b99348c76fc0f72b2",
|
||||
"content-hash": "63573b43a17b583b8766d74720f367a3",
|
||||
"packages": [
|
||||
{
|
||||
"name": "doctrine/lexer",
|
||||
@@ -83,6 +83,73 @@
|
||||
],
|
||||
"time": "2024-02-05T11:56:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "egulias/email-validator",
|
||||
"version": "4.0.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/egulias/EmailValidator.git",
|
||||
"reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa",
|
||||
"reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"doctrine/lexer": "^2.0 || ^3.0",
|
||||
"php": ">=8.1",
|
||||
"symfony/polyfill-intl-idn": "^1.26"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.2",
|
||||
"vimeo/psalm": "^5.12"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "4.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Egulias\\EmailValidator\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Eduardo Gulias Davis"
|
||||
}
|
||||
],
|
||||
"description": "A library for validating emails against several RFCs",
|
||||
"homepage": "https://github.com/egulias/EmailValidator",
|
||||
"keywords": [
|
||||
"email",
|
||||
"emailvalidation",
|
||||
"emailvalidator",
|
||||
"validation",
|
||||
"validator"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/egulias/EmailValidator/issues",
|
||||
"source": "https://github.com/egulias/EmailValidator/tree/4.0.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/egulias",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-03-06T22:45:56+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/log",
|
||||
"version": "3.0.2",
|
||||
@@ -132,6 +199,423 @@
|
||||
"source": "https://github.com/php-fig/log/tree/3.0.2"
|
||||
},
|
||||
"time": "2024-09-11T13:17:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
"version": "v3.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/deprecation-contracts.git",
|
||||
"reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b",
|
||||
"reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/contracts",
|
||||
"name": "symfony/contracts"
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-main": "3.7-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"function.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "A generic function and convention to trigger deprecation notices",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-13T15:52:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/mime",
|
||||
"version": "v7.4.13",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/mime.git",
|
||||
"reference": "a845722765c4f6b2ce88beaf4f4479975b186770"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/mime/zipball/a845722765c4f6b2ce88beaf4f4479975b186770",
|
||||
"reference": "a845722765c4f6b2ce88beaf4f4479975b186770",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/polyfill-intl-idn": "^1.10",
|
||||
"symfony/polyfill-mbstring": "^1.0"
|
||||
},
|
||||
"conflict": {
|
||||
"egulias/email-validator": "~3.0.0",
|
||||
"phpdocumentor/reflection-docblock": "<5.2|>=7",
|
||||
"phpdocumentor/type-resolver": "<1.5.1",
|
||||
"symfony/mailer": "<6.4",
|
||||
"symfony/serializer": "<6.4.3|>7.0,<7.0.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"egulias/email-validator": "^2.1.10|^3.1|^4",
|
||||
"league/html-to-markdown": "^5.0",
|
||||
"phpdocumentor/reflection-docblock": "^5.2|^6.0",
|
||||
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
||||
"symfony/process": "^6.4|^7.0|^8.0",
|
||||
"symfony/property-access": "^6.4|^7.0|^8.0",
|
||||
"symfony/property-info": "^6.4|^7.0|^8.0",
|
||||
"symfony/serializer": "^6.4.3|^7.0.3|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Mime\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Allows manipulating MIME messages",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"mime",
|
||||
"mime-type"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/mime/tree/v7.4.13"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-23T16:22:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-idn",
|
||||
"version": "v1.38.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-idn.git",
|
||||
"reference": "dc21118016c039a66235cf93d96b435ffb282412"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/dc21118016c039a66235cf93d96b435ffb282412",
|
||||
"reference": "dc21118016c039a66235cf93d96b435ffb282412",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2",
|
||||
"symfony/polyfill-intl-normalizer": "^1.10"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Intl\\Idn\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Laurent Bassin",
|
||||
"email": "laurent@bassin.info"
|
||||
},
|
||||
{
|
||||
"name": "Trevor Rowbotham",
|
||||
"email": "trevor.rowbotham@pm.me"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"idn",
|
||||
"intl",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.38.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-25T15:22:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-normalizer",
|
||||
"version": "v1.38.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
|
||||
"reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b",
|
||||
"reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Intl\\Normalizer\\": ""
|
||||
},
|
||||
"classmap": [
|
||||
"Resources/stubs"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for intl's Normalizer class and related functions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"intl",
|
||||
"normalizer",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-25T13:48:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.38.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
"reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6",
|
||||
"reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-iconv": "*",
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"provide": {
|
||||
"ext-mbstring": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Mbstring\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for the Mbstring extension",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"mbstring",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-27T06:59:30+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
@@ -1919,7 +2403,7 @@
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"stability-flags": {},
|
||||
"prefer-stable": true,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
@@ -1927,9 +2411,9 @@
|
||||
"ext-iconv": "*",
|
||||
"ext-ctype": "*"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"platform-dev": {},
|
||||
"platform-overrides": {
|
||||
"php": "8.2"
|
||||
},
|
||||
"plugin-api-version": "2.3.0"
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace KTXM\ProviderImap\Mime;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use KTXF\Mail\Object\AddressInterface;
|
||||
use KTXF\Mail\Object\MessagePartInterface;
|
||||
use KTXF\Mail\Object\MessagePropertiesBaseInterface;
|
||||
use Symfony\Component\Mime\Address as MimeAddress;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Mime\Part\DataPart;
|
||||
|
||||
/**
|
||||
* MIME Message Builder
|
||||
*
|
||||
* Converts a framework {@see MessagePropertiesBaseInterface} into raw RFC822
|
||||
* bytes. The output is transport-neutral and used by both of this module's
|
||||
* outbound paths: the SMTP DATA payload (submission) and the IMAP APPEND
|
||||
* literal (saving a copy to the Sent collection).
|
||||
*
|
||||
* The MIME engine (symfony/mime) is intentionally hidden behind this API so it
|
||||
* can be swapped without touching consumers.
|
||||
*
|
||||
* @since 2026.06.22
|
||||
*/
|
||||
final class MessageBuilder
|
||||
{
|
||||
/**
|
||||
* Headers that are derived from structured properties and therefore must
|
||||
* not be copied verbatim from {@see MessagePropertiesBaseInterface::getHeaders()}.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
private const RESERVED_HEADERS = [
|
||||
'from', 'sender', 'reply-to', 'to', 'cc', 'bcc', 'subject', 'date',
|
||||
'message-id', 'mime-version', 'content-type', 'content-transfer-encoding',
|
||||
'in-reply-to', 'references', 'bcc',
|
||||
];
|
||||
|
||||
/**
|
||||
* Build the complete RFC822 message bytes for the given properties.
|
||||
*/
|
||||
public function build(MessagePropertiesBaseInterface $message): string
|
||||
{
|
||||
return $this->toEmail($message)->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect the envelope recipients (To + Cc + Bcc) as bare e-mail addresses.
|
||||
*
|
||||
* Useful for the SMTP RCPT TO phase, which needs every recipient including
|
||||
* Bcc, even though Bcc never appears in the rendered headers.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function recipients(MessagePropertiesBaseInterface $message): array
|
||||
{
|
||||
$recipients = [];
|
||||
|
||||
foreach ([...$message->getTo(), ...$message->getCc(), ...$message->getBcc()] as $address) {
|
||||
$email = trim($address->getAddress());
|
||||
if ($email !== '' && !in_array($email, $recipients, true)) {
|
||||
$recipients[] = $email;
|
||||
}
|
||||
}
|
||||
|
||||
return $recipients;
|
||||
}
|
||||
|
||||
private function toEmail(MessagePropertiesBaseInterface $message): Email
|
||||
{
|
||||
$email = new Email();
|
||||
|
||||
if (($from = $message->getFrom()) !== null) {
|
||||
$email->from($this->address($from));
|
||||
}
|
||||
if (($sender = $message->getSender()) !== null) {
|
||||
$email->sender($this->address($sender));
|
||||
}
|
||||
|
||||
$replyTo = $this->addresses($message->getReplyTo());
|
||||
if ($replyTo !== []) {
|
||||
$email->replyTo(...$replyTo);
|
||||
}
|
||||
|
||||
$to = $this->addresses($message->getTo());
|
||||
if ($to !== []) {
|
||||
$email->to(...$to);
|
||||
}
|
||||
|
||||
$cc = $this->addresses($message->getCc());
|
||||
if ($cc !== []) {
|
||||
$email->cc(...$cc);
|
||||
}
|
||||
|
||||
$bcc = $this->addresses($message->getBcc());
|
||||
if ($bcc !== []) {
|
||||
$email->bcc(...$bcc);
|
||||
}
|
||||
|
||||
$email->subject($message->getSubject());
|
||||
|
||||
if (($sent = $message->getSent()) !== null) {
|
||||
$email->date($sent instanceof DateTimeImmutable ? $sent : DateTimeImmutable::createFromInterface($sent));
|
||||
}
|
||||
|
||||
$text = $message->getBodyTextPlain();
|
||||
$html = $message->getBodyTextHtml();
|
||||
// Inline attachments may carry Content-IDs without an "@"; those must be
|
||||
// normalised (symfony requires an "@") and the matching cid: references
|
||||
// in the HTML body rewritten so the parts still resolve.
|
||||
$cidRewrites = $this->inlineCidRewrites($message);
|
||||
if ($text !== null && $text !== '') {
|
||||
$email->text($text);
|
||||
}
|
||||
if ($html !== null && $html !== '') {
|
||||
$email->html($this->rewriteCids($html, $cidRewrites));
|
||||
}
|
||||
// Guarantee at least an (empty) text part so the message is well-formed.
|
||||
if (($text === null || $text === '') && ($html === null || $html === '')) {
|
||||
$email->text('');
|
||||
}
|
||||
|
||||
foreach ($message->getAttachments() as $attachment) {
|
||||
$part = $this->attachmentPart($attachment);
|
||||
if ($part !== null) {
|
||||
$email->addPart($part);
|
||||
}
|
||||
}
|
||||
|
||||
$this->applyThreadHeaders($email, $message);
|
||||
$this->applyCustomHeaders($email, $message);
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
private function attachmentPart(MessagePartInterface $attachment): ?DataPart
|
||||
{
|
||||
$content = $attachment->getContent();
|
||||
if ($content === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$part = new DataPart(
|
||||
$content,
|
||||
$attachment->getName(),
|
||||
$attachment->getType() ?? 'application/octet-stream',
|
||||
);
|
||||
|
||||
if ($attachment->getDisposition() === 'inline') {
|
||||
$part->asInline();
|
||||
$cid = $attachment->getContentId();
|
||||
if ($cid !== null && $cid !== '') {
|
||||
$part->setContentId($this->normalizeContentId(trim($cid, '<>')));
|
||||
}
|
||||
}
|
||||
|
||||
return $part;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a map of original => normalised Content-IDs for inline parts whose
|
||||
* id required normalisation (i.e. lacked an "@").
|
||||
*
|
||||
* @return array<string,string>
|
||||
*/
|
||||
private function inlineCidRewrites(MessagePropertiesBaseInterface $message): array
|
||||
{
|
||||
$rewrites = [];
|
||||
|
||||
foreach ($message->getAttachments() as $attachment) {
|
||||
if ($attachment->getDisposition() !== 'inline') {
|
||||
continue;
|
||||
}
|
||||
$cid = $attachment->getContentId();
|
||||
if ($cid === null || $cid === '') {
|
||||
continue;
|
||||
}
|
||||
$original = trim($cid, '<>');
|
||||
$normalized = $this->normalizeContentId($original);
|
||||
if ($original !== $normalized) {
|
||||
$rewrites[$original] = $normalized;
|
||||
}
|
||||
}
|
||||
|
||||
return $rewrites;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,string> $rewrites
|
||||
*/
|
||||
private function rewriteCids(string $html, array $rewrites): string
|
||||
{
|
||||
foreach ($rewrites as $original => $normalized) {
|
||||
$html = str_replace('cid:' . $original, 'cid:' . $normalized, $html);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function normalizeContentId(string $cid): string
|
||||
{
|
||||
return str_contains($cid, '@') ? $cid : $cid . '@ktrix';
|
||||
}
|
||||
|
||||
private function applyThreadHeaders(Email $email, MessagePropertiesBaseInterface $message): void
|
||||
{
|
||||
$headers = $email->getHeaders();
|
||||
|
||||
$inReplyTo = $message->getInReplyTo();
|
||||
if ($inReplyTo !== null && $inReplyTo !== '') {
|
||||
$headers->addIdHeader('In-Reply-To', $this->stripBrackets($inReplyTo));
|
||||
}
|
||||
|
||||
$references = array_values(array_filter(array_map(
|
||||
fn (string $id): string => $this->stripBrackets($id),
|
||||
$message->getReferences(),
|
||||
), static fn (string $id): bool => $id !== ''));
|
||||
|
||||
if ($references !== []) {
|
||||
$headers->addIdHeader('References', $references);
|
||||
}
|
||||
}
|
||||
|
||||
private function applyCustomHeaders(Email $email, MessagePropertiesBaseInterface $message): void
|
||||
{
|
||||
$headers = $email->getHeaders();
|
||||
|
||||
foreach ($message->getHeaders() as $name => $value) {
|
||||
if (!is_string($name) || $value === null) {
|
||||
continue;
|
||||
}
|
||||
if (in_array(strtolower($name), self::RESERVED_HEADERS, true)) {
|
||||
continue;
|
||||
}
|
||||
$headers->addTextHeader($name, is_array($value) ? implode(', ', $value) : (string) $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,AddressInterface> $addresses
|
||||
* @return list<MimeAddress>
|
||||
*/
|
||||
private function addresses(array $addresses): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($addresses as $address) {
|
||||
if ($address instanceof AddressInterface && trim($address->getAddress()) !== '') {
|
||||
$result[] = $this->address($address);
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function address(AddressInterface $address): MimeAddress
|
||||
{
|
||||
return new MimeAddress($address->getAddress(), $address->getLabel() ?? '');
|
||||
}
|
||||
|
||||
private function stripBrackets(string $id): string
|
||||
{
|
||||
return trim($id, " \t<>");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user