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|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 */ 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 ' 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 ) $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" ), 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 " ). */ 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; } }