TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:09 +02:00
commit af8cc155b5
6818 changed files with 642608 additions and 0 deletions
+359
View File
@@ -0,0 +1,359 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Utility;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Exception\ExceptionInterface;
/**
* Class to handle mail specific functionality
*/
class MailUtility
{
/**
* Gets a valid "from" for mail messages (email and name).
*
* Ready to be passed to $mail->setFrom()
*
* This method can return three different variants:
* 1. An assoc. array: key => Valid email address which can be used as sender; value => Valid name which can be used as a sender
* 2. A numeric array with one entry: Valid email address which can be used as sender
* 3. Null, if no address is configured
*
* @return array<string|int, string>|null
*/
public static function getSystemFrom(): ?array
{
$address = self::getSystemFromAddress();
$name = self::getSystemFromName();
if (!$address) {
return null;
}
if ($name) {
return [$address => $name];
}
return [$address];
}
/**
* Creates a valid "from" name for mail messages.
*
* As configured in Install Tool.
*
* @return string|null The name (unquoted, unformatted). NULL if none is set or an invalid non-string value.
*/
public static function getSystemFromName(): ?string
{
$name = $GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromName'] ?? null;
return (!empty($name) && is_string($name)) ? $name : null;
}
/**
* Creates a valid email address for the sender of mail messages.
*
* Uses a fallback chain:
* $TYPO3_CONF_VARS['MAIL']['defaultMailFromAddress'] ->
* no-reply@FirstDomainRecordFound ->
* no-reply@php_uname('n') ->
* no-reply@example.com
*
* Ready to be passed to $mail->setFrom()
*
* @return string An email address
*/
public static function getSystemFromAddress(): string
{
$address = $GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress'] ?? null;
if (!is_string($address) || !GeneralUtility::validEmail($address)) {
// still nothing, get host name from server
$address = 'no-reply@' . php_uname('n');
if (!GeneralUtility::validEmail($address)) {
// if everything fails use a dummy address
$address = 'no-reply@example.com';
}
}
return $address;
}
/**
* Gets a default "reply-to" for mail messages (email and name).
*
* Ready to be passed to $mail->setReplyTo()
*
* This method returns a list of email addresses, but depending on the existence of "defaultMailReplyToName"
* the array can have a different shape:
*
* 1. An assoc. array: key => a valid reply-to address which can be used as sender; value => a valid reply-to name which can be used as a sender
* 2. A numeric array with one entry: a valid reply-to address which can be used as sender
*
* @return array<string|int, string>
*/
public static function getSystemReplyTo(): array
{
$mailConfiguration = $GLOBALS['TYPO3_CONF_VARS']['MAIL'] ?? [];
$replyToAddress = $mailConfiguration['defaultMailReplyToAddress'] ?? null;
if (empty($replyToAddress) || !GeneralUtility::validEmail($replyToAddress)) {
return [];
}
if (!empty($mailConfiguration['defaultMailReplyToName'])) {
$replyTo = [$replyToAddress => $mailConfiguration['defaultMailReplyToName']];
} else {
$replyTo = [$replyToAddress];
}
return $replyTo;
}
/**
* Breaks up a single line of text for emails
* Words - longer than $lineWidth - will not be split into parts
*
* @param string $str The string to break up
* @param string $newlineChar The string to implode the broken lines with (default/typically \n)
* @param int $lineWidth The line width
* @return string Reformatted text
*/
public static function breakLinesForEmail(string $str, string $newlineChar = LF, int $lineWidth = 76): string
{
$lines = [];
$substrStart = 0;
while (strlen($str) > $substrStart) {
$substr = substr($str, $substrStart, $lineWidth);
// has line exceeded (reached) the maximum width?
if (strlen($substr) === $lineWidth) {
// find last space-char
$spacePos = strrpos(rtrim($substr), ' ');
// space-char found?
if ($spacePos !== false) {
// take everything up to last space-char
$theLine = substr($substr, 0, $spacePos);
$substrStart++;
} else {
// search for space-char in remaining text
// makes this line longer than $lineWidth!
$afterParts = explode(' ', substr($str, $lineWidth + $substrStart), 2);
$theLine = $substr . $afterParts[0];
}
if ($theLine === '') {
// prevent endless loop because of empty line
break;
}
} else {
$theLine = $substr;
}
$lines[] = trim($theLine);
$substrStart += strlen($theLine);
if (trim(substr($str, $substrStart, $lineWidth)) === '') {
// no more text
break;
}
}
return implode($newlineChar, $lines);
}
/**
* Parses mailbox headers and turns them into an array.
*
* Mailbox headers are a comma separated list of 'name <email@example.org>' combinations
* or plain email addresses (or a mix of these).
* The resulting array has key-value pairs where the key is either a number
* (no display name in the mailbox header) and the value is the email address,
* or the key is the email address and the value is the display name.
*
* Groups (RFC 5322 section 3.4) are flattened to their members and their display
* name is discarded, comments are removed, and invalid addresses are silently skipped.
*
* @param string $rawAddresses Comma separated list of email addresses (optionally with display name)
* @return array Parsed list of addresses.
*/
public static function parseAddresses(string $rawAddresses): array
{
$addressList = [];
foreach (self::splitAddressList($rawAddresses) as $rawMailbox) {
$address = self::parseMailbox($rawMailbox);
if ($address === null) {
continue;
}
if ($address->getName() !== '') {
// item with name found ( name <email@example.org> )
$addressList[$address->getAddress()] = $address->getName();
} else {
// item without name found ( email@example.org )
$addressList[] = $address->getAddress();
}
}
return $addressList;
}
/**
* Splits a raw address-list header value into its individual mailboxes, while
* honoring quoted strings ( "last, first" <email@example.org> ), comments
* (which are removed), domain literals ( user@[IPv6:2001:db8::1] ) and
* angle-addr parts. Group members are flattened into the list, the display
* name of a group is discarded.
*
* @return string[]
*/
private static function splitAddressList(string $rawAddresses): array
{
$mailboxes = [];
$buffer = '';
$inQuotes = false;
$inAngleAddr = false;
$inDomainLiteral = false;
$commentDepth = 0;
$length = strlen($rawAddresses);
for ($i = 0; $i < $length; $i++) {
$char = $rawAddresses[$i];
if ($commentDepth > 0) {
if ($char === '\\') {
$i++;
} elseif ($char === '(') {
$commentDepth++;
} elseif ($char === ')') {
$commentDepth--;
if ($commentDepth === 0) {
// a comment is equivalent to folding white space (RFC 5322, section 3.2.2)
$buffer .= ' ';
}
}
continue;
}
if ($inQuotes) {
if ($char === '\\' && $i + 1 < $length) {
$buffer .= $char . $rawAddresses[++$i];
continue;
}
if ($char === '"') {
$inQuotes = false;
}
$buffer .= $char;
continue;
}
switch ($char) {
case '"':
$inQuotes = true;
$buffer .= $char;
break;
case '(':
$commentDepth++;
break;
case '[':
case ']':
$inDomainLiteral = $char === '[';
$buffer .= $char;
break;
case '<':
case '>':
$inAngleAddr = $char === '<';
$buffer .= $char;
break;
case ',':
case ';':
if ($inAngleAddr || $inDomainLiteral) {
$buffer .= $char;
break;
}
$mailboxes[] = $buffer;
$buffer = '';
break;
case ':':
if ($inAngleAddr || $inDomainLiteral) {
$buffer .= $char;
break;
}
// a colon ends the display name of a group ( groupname: member@example.org; )
$buffer = '';
break;
default:
$buffer .= $char;
}
}
$mailboxes[] = $buffer;
return $mailboxes;
}
private static function parseMailbox(string $rawMailbox): ?Address
{
$rawMailbox = trim($rawMailbox);
// NUL is invalid anywhere, even in the obsolete syntax (RFC 5322, section 4.1)
if ($rawMailbox === '' || str_contains($rawMailbox, "\0")) {
return null;
}
$displayName = '';
$addrSpec = $rawMailbox;
$angleStart = self::findAngleAddrStart($rawMailbox);
if ($angleStart !== null) {
$angleEnd = strrpos($rawMailbox, '>');
if ($angleEnd === false || $angleEnd < $angleStart) {
return null;
}
$displayName = self::normalizeDisplayName(substr($rawMailbox, 0, $angleStart));
$addrSpec = substr($rawMailbox, $angleStart + 1, $angleEnd - $angleStart - 1);
if (str_starts_with($addrSpec, '@')) {
// an obsolete route ( <@relay.example.org:user@example.org> ) is ignored (RFC 5322, section 4.4)
$routeEnd = strpos($addrSpec, ':');
if ($routeEnd !== false) {
$addrSpec = substr($addrSpec, $routeEnd + 1);
}
}
}
try {
return new Address($addrSpec, $displayName);
} catch (ExceptionInterface) {
return null;
}
}
/**
* Finds the position of the '<' starting an angle-addr, ignoring any '<'
* inside a quoted display name ( "Contact <va@example.org>" <real@example.org> ).
*/
private static function findAngleAddrStart(string $rawMailbox): ?int
{
$inQuotes = false;
$length = strlen($rawMailbox);
for ($i = 0; $i < $length; $i++) {
$char = $rawMailbox[$i];
if ($inQuotes && $char === '\\') {
$i++;
} elseif ($char === '"') {
$inQuotes = !$inQuotes;
} elseif ($char === '<' && !$inQuotes) {
return $i;
}
}
return null;
}
/**
* Resolves a quoted display name ( "last, first" ) and contained
* quoted-pairs ( \" ) to the plain text it represents.
*/
private static function normalizeDisplayName(string $displayName): string
{
$displayName = trim($displayName);
if (strlen($displayName) > 1 && str_starts_with($displayName, '"') && str_ends_with($displayName, '"')) {
$displayName = stripslashes(substr($displayName, 1, -1));
}
return $displayName;
}
}