mirror of
https://gitlab.com/signalytic/client-external/streamline/streamline-emr.git
synced 2026-09-12 03:01:32 +00:00
resolved conflicts
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mime\Header;
|
||||
|
||||
use Symfony\Component\Mime\Encoder\QpMimeHeaderEncoder;
|
||||
|
||||
/**
|
||||
* An abstract base MIME Header.
|
||||
*
|
||||
* @author Chris Corbyn
|
||||
*/
|
||||
abstract class AbstractHeader implements HeaderInterface
|
||||
{
|
||||
public const PHRASE_PATTERN = '(?:(?:(?:(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?[a-zA-Z0-9!#\$%&\'\*\+\-\/=\?\^_`\{\}\|~]+(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?)|(?:(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?"((?:(?:[ \t]*(?:\r\n))?[ \t])?(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21\x23-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])))*(?:(?:[ \t]*(?:\r\n))?[ \t])?"(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?))+?)';
|
||||
|
||||
private static QpMimeHeaderEncoder $encoder;
|
||||
|
||||
private string $name;
|
||||
private int $lineLength = 76;
|
||||
private ?string $lang = null;
|
||||
private string $charset = 'utf-8';
|
||||
|
||||
public function __construct(string $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function setCharset(string $charset)
|
||||
{
|
||||
$this->charset = $charset;
|
||||
}
|
||||
|
||||
public function getCharset(): ?string
|
||||
{
|
||||
return $this->charset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the language used in this Header.
|
||||
*
|
||||
* For example, for US English, 'en-us'.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setLanguage(string $lang)
|
||||
{
|
||||
$this->lang = $lang;
|
||||
}
|
||||
|
||||
public function getLanguage(): ?string
|
||||
{
|
||||
return $this->lang;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function setMaxLineLength(int $lineLength)
|
||||
{
|
||||
$this->lineLength = $lineLength;
|
||||
}
|
||||
|
||||
public function getMaxLineLength(): int
|
||||
{
|
||||
return $this->lineLength;
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->tokensToString($this->toTokens());
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a compliant, formatted RFC 2822 'phrase' based on the string given.
|
||||
*
|
||||
* @param string $string as displayed
|
||||
* @param bool $shorten the first line to make remove for header name
|
||||
*/
|
||||
protected function createPhrase(HeaderInterface $header, string $string, string $charset, bool $shorten = false): string
|
||||
{
|
||||
// Treat token as exactly what was given
|
||||
$phraseStr = $string;
|
||||
|
||||
// If it's not valid
|
||||
if (!preg_match('/^'.self::PHRASE_PATTERN.'$/D', $phraseStr)) {
|
||||
// .. but it is just ascii text, try escaping some characters
|
||||
// and make it a quoted-string
|
||||
if (preg_match('/^[\x00-\x08\x0B\x0C\x0E-\x7F]*$/D', $phraseStr)) {
|
||||
foreach (['\\', '"'] as $char) {
|
||||
$phraseStr = str_replace($char, '\\'.$char, $phraseStr);
|
||||
}
|
||||
$phraseStr = '"'.$phraseStr.'"';
|
||||
} else {
|
||||
// ... otherwise it needs encoding
|
||||
// Determine space remaining on line if first line
|
||||
if ($shorten) {
|
||||
$usedLength = \strlen($header->getName().': ');
|
||||
} else {
|
||||
$usedLength = 0;
|
||||
}
|
||||
$phraseStr = $this->encodeWords($header, $string, $usedLength);
|
||||
}
|
||||
} elseif (str_contains($phraseStr, '(')) {
|
||||
foreach (['\\', '"'] as $char) {
|
||||
$phraseStr = str_replace($char, '\\'.$char, $phraseStr);
|
||||
}
|
||||
$phraseStr = '"'.$phraseStr.'"';
|
||||
}
|
||||
|
||||
return $phraseStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode needed word tokens within a string of input.
|
||||
*/
|
||||
protected function encodeWords(HeaderInterface $header, string $input, int $usedLength = -1): string
|
||||
{
|
||||
$value = '';
|
||||
$tokens = $this->getEncodableWordTokens($input);
|
||||
foreach ($tokens as $token) {
|
||||
// See RFC 2822, Sect 2.2 (really 2.2 ??)
|
||||
if ($this->tokenNeedsEncoding($token)) {
|
||||
// Don't encode starting WSP
|
||||
$firstChar = substr($token, 0, 1);
|
||||
switch ($firstChar) {
|
||||
case ' ':
|
||||
case "\t":
|
||||
$value .= $firstChar;
|
||||
$token = substr($token, 1);
|
||||
}
|
||||
|
||||
if (-1 == $usedLength) {
|
||||
$usedLength = \strlen($header->getName().': ') + \strlen($value);
|
||||
}
|
||||
$value .= $this->getTokenAsEncodedWord($token, $usedLength);
|
||||
} else {
|
||||
$value .= $token;
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
protected function tokenNeedsEncoding(string $token): bool
|
||||
{
|
||||
return (bool) preg_match('~[\x00-\x08\x10-\x19\x7F-\xFF\r\n]~', $token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a string into tokens in blocks of words which can be encoded quickly.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getEncodableWordTokens(string $string): array
|
||||
{
|
||||
$tokens = [];
|
||||
$encodedToken = '';
|
||||
// Split at all whitespace boundaries
|
||||
foreach (preg_split('~(?=[\t ])~', $string) as $token) {
|
||||
if ($this->tokenNeedsEncoding($token)) {
|
||||
$encodedToken .= $token;
|
||||
} else {
|
||||
if ('' !== $encodedToken) {
|
||||
$tokens[] = $encodedToken;
|
||||
$encodedToken = '';
|
||||
}
|
||||
$tokens[] = $token;
|
||||
}
|
||||
}
|
||||
if ('' !== $encodedToken) {
|
||||
$tokens[] = $encodedToken;
|
||||
}
|
||||
|
||||
foreach ($tokens as $i => $token) {
|
||||
// whitespace(s) between 2 encoded tokens
|
||||
if (
|
||||
0 < $i
|
||||
&& isset($tokens[$i + 1])
|
||||
&& preg_match('~^[\t ]+$~', $token)
|
||||
&& $this->tokenNeedsEncoding($tokens[$i - 1])
|
||||
&& $this->tokenNeedsEncoding($tokens[$i + 1])
|
||||
) {
|
||||
$tokens[$i - 1] .= $token.$tokens[$i + 1];
|
||||
array_splice($tokens, $i, 2);
|
||||
}
|
||||
}
|
||||
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a token as an encoded word for safe insertion into headers.
|
||||
*/
|
||||
protected function getTokenAsEncodedWord(string $token, int $firstLineOffset = 0): string
|
||||
{
|
||||
self::$encoder ??= new QpMimeHeaderEncoder();
|
||||
|
||||
// Adjust $firstLineOffset to account for space needed for syntax
|
||||
$charsetDecl = $this->charset;
|
||||
if (null !== $this->lang) {
|
||||
$charsetDecl .= '*'.$this->lang;
|
||||
}
|
||||
$encodingWrapperLength = \strlen('=?'.$charsetDecl.'?'.self::$encoder->getName().'??=');
|
||||
|
||||
if ($firstLineOffset >= 75) {
|
||||
// Does this logic need to be here?
|
||||
$firstLineOffset = 0;
|
||||
}
|
||||
|
||||
$encodedTextLines = explode("\r\n",
|
||||
self::$encoder->encodeString($token, $this->charset, $firstLineOffset, 75 - $encodingWrapperLength)
|
||||
);
|
||||
|
||||
if ('iso-2022-jp' !== strtolower($this->charset)) {
|
||||
// special encoding for iso-2022-jp using mb_encode_mimeheader
|
||||
foreach ($encodedTextLines as $lineNum => $line) {
|
||||
$encodedTextLines[$lineNum] = '=?'.$charsetDecl.'?'.self::$encoder->getName().'?'.$line.'?=';
|
||||
}
|
||||
}
|
||||
|
||||
return implode("\r\n ", $encodedTextLines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates tokens from the given string which include CRLF as individual tokens.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function generateTokenLines(string $token): array
|
||||
{
|
||||
return preg_split('~(\r\n)~', $token, -1, \PREG_SPLIT_DELIM_CAPTURE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a list of all tokens in the final header.
|
||||
*/
|
||||
protected function toTokens(?string $string = null): array
|
||||
{
|
||||
$string ??= $this->getBodyAsString();
|
||||
|
||||
$tokens = [];
|
||||
// Generate atoms; split at all invisible boundaries followed by WSP
|
||||
foreach (preg_split('~(?=[ \t])~', $string) as $token) {
|
||||
$newTokens = $this->generateTokenLines($token);
|
||||
foreach ($newTokens as $newToken) {
|
||||
$tokens[] = $newToken;
|
||||
}
|
||||
}
|
||||
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an array of tokens which appear in the header and turns them into
|
||||
* an RFC 2822 compliant string, adding FWSP where needed.
|
||||
*
|
||||
* @param string[] $tokens
|
||||
*/
|
||||
private function tokensToString(array $tokens): string
|
||||
{
|
||||
$lineCount = 0;
|
||||
$headerLines = [];
|
||||
$headerLines[] = $this->name.': ';
|
||||
$currentLine = &$headerLines[$lineCount++];
|
||||
|
||||
// Build all tokens back into compliant header
|
||||
foreach ($tokens as $i => $token) {
|
||||
// Line longer than specified maximum or token was just a new line
|
||||
if (("\r\n" === $token)
|
||||
|| ($i > 0 && \strlen($currentLine.$token) > $this->lineLength)
|
||||
&& '' !== $currentLine) {
|
||||
$headerLines[] = '';
|
||||
$currentLine = &$headerLines[$lineCount++];
|
||||
}
|
||||
|
||||
// Append token to the line
|
||||
if ("\r\n" !== $token) {
|
||||
$currentLine .= $token;
|
||||
}
|
||||
}
|
||||
|
||||
// Implode with FWS (RFC 2822, 2.2.3)
|
||||
return implode("\r\n", $headerLines);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mime;
|
||||
|
||||
use Symfony\Component\Mime\Exception\LogicException;
|
||||
use Symfony\Component\Mime\Header\Headers;
|
||||
use Symfony\Component\Mime\Part\AbstractPart;
|
||||
use Symfony\Component\Mime\Part\TextPart;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class Message extends RawMessage
|
||||
{
|
||||
private Headers $headers;
|
||||
private ?AbstractPart $body;
|
||||
|
||||
public function __construct(?Headers $headers = null, ?AbstractPart $body = null)
|
||||
{
|
||||
$this->headers = $headers ? clone $headers : new Headers();
|
||||
$this->body = $body;
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->headers = clone $this->headers;
|
||||
|
||||
if (null !== $this->body) {
|
||||
$this->body = clone $this->body;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setBody(?AbstractPart $body = null): static
|
||||
{
|
||||
if (1 > \func_num_args()) {
|
||||
trigger_deprecation('symfony/mime', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
|
||||
}
|
||||
$this->body = $body;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBody(): ?AbstractPart
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setHeaders(Headers $headers): static
|
||||
{
|
||||
$this->headers = $headers;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHeaders(): Headers
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
public function getPreparedHeaders(): Headers
|
||||
{
|
||||
$headers = clone $this->headers;
|
||||
|
||||
if (!$headers->has('From')) {
|
||||
if (!$headers->has('Sender')) {
|
||||
throw new LogicException('An email must have a "From" or a "Sender" header.');
|
||||
}
|
||||
$headers->addMailboxListHeader('From', [$headers->get('Sender')->getAddress()]);
|
||||
}
|
||||
|
||||
if (!$headers->has('MIME-Version')) {
|
||||
$headers->addTextHeader('MIME-Version', '1.0');
|
||||
}
|
||||
|
||||
if (!$headers->has('Date')) {
|
||||
$headers->addDateHeader('Date', new \DateTimeImmutable());
|
||||
}
|
||||
|
||||
// determine the "real" sender
|
||||
if (!$headers->has('Sender') && \count($froms = $headers->get('From')->getAddresses()) > 1) {
|
||||
$headers->addMailboxHeader('Sender', $froms[0]);
|
||||
}
|
||||
|
||||
if (!$headers->has('Message-ID')) {
|
||||
$headers->addIdHeader('Message-ID', $this->generateMessageId());
|
||||
}
|
||||
|
||||
// remove the Bcc field which should NOT be part of the sent message
|
||||
$headers->remove('Bcc');
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
if (null === $body = $this->getBody()) {
|
||||
$body = new TextPart('');
|
||||
}
|
||||
|
||||
return $this->getPreparedHeaders()->toString().$body->toString();
|
||||
}
|
||||
|
||||
public function toIterable(): iterable
|
||||
{
|
||||
if (null === $body = $this->getBody()) {
|
||||
$body = new TextPart('');
|
||||
}
|
||||
|
||||
yield $this->getPreparedHeaders()->toString();
|
||||
yield from $body->toIterable();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function ensureValidity()
|
||||
{
|
||||
if (!$this->headers->get('To')?->getBody() && !$this->headers->get('Cc')?->getBody() && !$this->headers->get('Bcc')?->getBody()) {
|
||||
throw new LogicException('An email must have a "To", "Cc", or "Bcc" header.');
|
||||
}
|
||||
|
||||
if (!$this->headers->get('From')?->getBody() && !$this->headers->get('Sender')?->getBody()) {
|
||||
throw new LogicException('An email must have a "From" or a "Sender" header.');
|
||||
}
|
||||
|
||||
parent::ensureValidity();
|
||||
}
|
||||
|
||||
public function generateMessageId(): string
|
||||
{
|
||||
if ($this->headers->has('Sender')) {
|
||||
$sender = $this->headers->get('Sender')->getAddress();
|
||||
} elseif ($this->headers->has('From')) {
|
||||
if (!$froms = $this->headers->get('From')->getAddresses()) {
|
||||
throw new LogicException('A "From" header must have at least one email address.');
|
||||
}
|
||||
$sender = $froms[0];
|
||||
} else {
|
||||
throw new LogicException('An email must have a "From" or a "Sender" header.');
|
||||
}
|
||||
|
||||
return bin2hex(random_bytes(16)).strstr($sender->getAddress(), '@');
|
||||
}
|
||||
|
||||
public function __serialize(): array
|
||||
{
|
||||
return [$this->headers, $this->body];
|
||||
}
|
||||
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
[$this->headers, $this->body] = $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mime\Part\Multipart;
|
||||
|
||||
use Symfony\Component\Mime\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Mime\Part\AbstractMultipartPart;
|
||||
use Symfony\Component\Mime\Part\DataPart;
|
||||
use Symfony\Component\Mime\Part\TextPart;
|
||||
|
||||
/**
|
||||
* Implements RFC 7578.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
final class FormDataPart extends AbstractMultipartPart
|
||||
{
|
||||
private array $fields = [];
|
||||
|
||||
/**
|
||||
* @param array<string|array|TextPart> $fields
|
||||
*/
|
||||
public function __construct(array $fields = [])
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->fields = $fields;
|
||||
|
||||
// HTTP does not support \r\n in header values
|
||||
$this->getHeaders()->setMaxLineLength(\PHP_INT_MAX);
|
||||
}
|
||||
|
||||
public function getMediaSubtype(): string
|
||||
{
|
||||
return 'form-data';
|
||||
}
|
||||
|
||||
public function getParts(): array
|
||||
{
|
||||
return $this->prepareFields($this->fields);
|
||||
}
|
||||
|
||||
private function prepareFields(array $fields): array
|
||||
{
|
||||
$values = [];
|
||||
|
||||
$prepare = function ($item, $key, $root = null) use (&$values, &$prepare) {
|
||||
if (null === $root && \is_int($key) && \is_array($item)) {
|
||||
if (1 !== \count($item)) {
|
||||
throw new InvalidArgumentException(sprintf('Form field values with integer keys can only have one array element, the key being the field name and the value being the field value, %d provided.', \count($item)));
|
||||
}
|
||||
|
||||
$key = key($item);
|
||||
$item = $item[$key];
|
||||
}
|
||||
|
||||
$fieldName = null !== $root ? sprintf('%s[%s]', $root, $key) : $key;
|
||||
|
||||
if (\is_array($item)) {
|
||||
array_walk($item, $prepare, $fieldName);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!\is_string($item) && !$item instanceof TextPart) {
|
||||
throw new InvalidArgumentException(sprintf('The value of the form field "%s" can only be a string, an array, or an instance of TextPart, "%s" given.', $fieldName, get_debug_type($item)));
|
||||
}
|
||||
|
||||
$values[] = $this->preparePart($fieldName, $item);
|
||||
};
|
||||
|
||||
array_walk($fields, $prepare);
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
private function preparePart(string $name, string|TextPart $value): TextPart
|
||||
{
|
||||
if (\is_string($value)) {
|
||||
return $this->configurePart($name, new TextPart($value, 'utf-8', 'plain', '8bit'));
|
||||
}
|
||||
|
||||
return $this->configurePart($name, $value);
|
||||
}
|
||||
|
||||
private function configurePart(string $name, TextPart $part): TextPart
|
||||
{
|
||||
static $r;
|
||||
|
||||
$r ??= new \ReflectionProperty(TextPart::class, 'encoding');
|
||||
|
||||
$part->setDisposition('form-data');
|
||||
$part->setName($name);
|
||||
// HTTP does not support \r\n in header values
|
||||
$part->getHeaders()->setMaxLineLength(\PHP_INT_MAX);
|
||||
$r->setValue($part, '8bit');
|
||||
|
||||
return $part;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mime\Part;
|
||||
|
||||
use Symfony\Component\Mime\Encoder\Base64ContentEncoder;
|
||||
use Symfony\Component\Mime\Encoder\ContentEncoderInterface;
|
||||
use Symfony\Component\Mime\Encoder\EightBitContentEncoder;
|
||||
use Symfony\Component\Mime\Encoder\QpContentEncoder;
|
||||
use Symfony\Component\Mime\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Mime\Header\Headers;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class TextPart extends AbstractPart
|
||||
{
|
||||
/** @internal */
|
||||
protected Headers $_headers;
|
||||
|
||||
private static array $encoders = [];
|
||||
|
||||
/** @var resource|string|File */
|
||||
private $body;
|
||||
private ?string $charset;
|
||||
private string $subtype;
|
||||
private ?string $disposition = null;
|
||||
private ?string $name = null;
|
||||
private string $encoding;
|
||||
private ?bool $seekable = null;
|
||||
|
||||
/**
|
||||
* @param resource|string|File $body Use a File instance to defer loading the file until rendering
|
||||
*/
|
||||
public function __construct($body, ?string $charset = 'utf-8', string $subtype = 'plain', ?string $encoding = null)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
if (!\is_string($body) && !\is_resource($body) && !$body instanceof File) {
|
||||
throw new \TypeError(sprintf('The body of "%s" must be a string, a resource, or an instance of "%s" (got "%s").', self::class, File::class, get_debug_type($body)));
|
||||
}
|
||||
|
||||
if ($body instanceof File) {
|
||||
$path = $body->getPath();
|
||||
if ((is_file($path) && !is_readable($path)) || is_dir($path)) {
|
||||
throw new InvalidArgumentException(sprintf('Path "%s" is not readable.', $path));
|
||||
}
|
||||
}
|
||||
|
||||
$this->body = $body;
|
||||
$this->charset = $charset;
|
||||
$this->subtype = $subtype;
|
||||
$this->seekable = \is_resource($body) ? stream_get_meta_data($body)['seekable'] && 0 === fseek($body, 0, \SEEK_CUR) : null;
|
||||
|
||||
if (null === $encoding) {
|
||||
$this->encoding = $this->chooseEncoding();
|
||||
} else {
|
||||
if ('quoted-printable' !== $encoding && 'base64' !== $encoding && '8bit' !== $encoding) {
|
||||
throw new InvalidArgumentException(sprintf('The encoding must be one of "quoted-printable", "base64", or "8bit" ("%s" given).', $encoding));
|
||||
}
|
||||
$this->encoding = $encoding;
|
||||
}
|
||||
}
|
||||
|
||||
public function getMediaType(): string
|
||||
{
|
||||
return 'text';
|
||||
}
|
||||
|
||||
public function getMediaSubtype(): string
|
||||
{
|
||||
return $this->subtype;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $disposition one of attachment, inline, or form-data
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setDisposition(string $disposition): static
|
||||
{
|
||||
$this->disposition = $disposition;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?string null or one of attachment, inline, or form-data
|
||||
*/
|
||||
public function getDisposition(): ?string
|
||||
{
|
||||
return $this->disposition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of the file (used by FormDataPart).
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the file.
|
||||
*/
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getBody(): string
|
||||
{
|
||||
if ($this->body instanceof File) {
|
||||
if (false === $ret = @file_get_contents($this->body->getPath())) {
|
||||
throw new InvalidArgumentException(error_get_last()['message']);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
if (null === $this->seekable) {
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
if ($this->seekable) {
|
||||
rewind($this->body);
|
||||
}
|
||||
|
||||
return stream_get_contents($this->body) ?: '';
|
||||
}
|
||||
|
||||
public function bodyToString(): string
|
||||
{
|
||||
return $this->getEncoder()->encodeString($this->getBody(), $this->charset);
|
||||
}
|
||||
|
||||
public function bodyToIterable(): iterable
|
||||
{
|
||||
if ($this->body instanceof File) {
|
||||
$path = $this->body->getPath();
|
||||
if (false === $handle = @fopen($path, 'r', false)) {
|
||||
throw new InvalidArgumentException(sprintf('Unable to open path "%s".', $path));
|
||||
}
|
||||
|
||||
yield from $this->getEncoder()->encodeByteStream($handle);
|
||||
} elseif (null !== $this->seekable) {
|
||||
if ($this->seekable) {
|
||||
rewind($this->body);
|
||||
}
|
||||
yield from $this->getEncoder()->encodeByteStream($this->body);
|
||||
} else {
|
||||
yield $this->getEncoder()->encodeString($this->body);
|
||||
}
|
||||
}
|
||||
|
||||
public function getPreparedHeaders(): Headers
|
||||
{
|
||||
$headers = parent::getPreparedHeaders();
|
||||
|
||||
$headers->setHeaderBody('Parameterized', 'Content-Type', $this->getMediaType().'/'.$this->getMediaSubtype());
|
||||
if ($this->charset) {
|
||||
$headers->setHeaderParameter('Content-Type', 'charset', $this->charset);
|
||||
}
|
||||
if ($this->name && 'form-data' !== $this->disposition) {
|
||||
$headers->setHeaderParameter('Content-Type', 'name', $this->name);
|
||||
}
|
||||
$headers->setHeaderBody('Text', 'Content-Transfer-Encoding', $this->encoding);
|
||||
|
||||
if (!$headers->has('Content-Disposition') && null !== $this->disposition) {
|
||||
$headers->setHeaderBody('Parameterized', 'Content-Disposition', $this->disposition);
|
||||
if ($this->name) {
|
||||
$headers->setHeaderParameter('Content-Disposition', 'name', $this->name);
|
||||
}
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
public function asDebugString(): string
|
||||
{
|
||||
$str = parent::asDebugString();
|
||||
if (null !== $this->charset) {
|
||||
$str .= ' charset: '.$this->charset;
|
||||
}
|
||||
if (null !== $this->disposition) {
|
||||
$str .= ' disposition: '.$this->disposition;
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
private function getEncoder(): ContentEncoderInterface
|
||||
{
|
||||
if ('8bit' === $this->encoding) {
|
||||
return self::$encoders[$this->encoding] ??= new EightBitContentEncoder();
|
||||
}
|
||||
|
||||
if ('quoted-printable' === $this->encoding) {
|
||||
return self::$encoders[$this->encoding] ??= new QpContentEncoder();
|
||||
}
|
||||
|
||||
return self::$encoders[$this->encoding] ??= new Base64ContentEncoder();
|
||||
}
|
||||
|
||||
private function chooseEncoding(): string
|
||||
{
|
||||
if (null === $this->charset) {
|
||||
return 'base64';
|
||||
}
|
||||
|
||||
return 'quoted-printable';
|
||||
}
|
||||
|
||||
public function __sleep(): array
|
||||
{
|
||||
// convert resources to strings for serialization
|
||||
if (null !== $this->seekable) {
|
||||
$this->body = $this->getBody();
|
||||
$this->seekable = null;
|
||||
}
|
||||
|
||||
$this->_headers = $this->getHeaders();
|
||||
|
||||
return ['_headers', 'body', 'charset', 'subtype', 'disposition', 'name', 'encoding'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function __wakeup()
|
||||
{
|
||||
$r = new \ReflectionProperty(AbstractPart::class, 'headers');
|
||||
$r->setValue($this, $this->_headers);
|
||||
unset($this->_headers);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Mime;
|
||||
|
||||
use Symfony\Component\Mime\Exception\LogicException;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class RawMessage
|
||||
{
|
||||
/** @var iterable<string>|string|resource */
|
||||
private $message;
|
||||
private bool $isGeneratorClosed;
|
||||
|
||||
/**
|
||||
* @param iterable<string>|string|resource $message
|
||||
*/
|
||||
public function __construct(mixed $message)
|
||||
{
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
if (\is_resource($this->message)) {
|
||||
fclose($this->message);
|
||||
}
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
if (\is_string($this->message)) {
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
if (\is_resource($this->message)) {
|
||||
return stream_get_contents($this->message, -1, 0);
|
||||
}
|
||||
|
||||
$message = '';
|
||||
foreach ($this->message as $chunk) {
|
||||
$message .= $chunk;
|
||||
}
|
||||
|
||||
return $this->message = $message;
|
||||
}
|
||||
|
||||
public function toIterable(): iterable
|
||||
{
|
||||
if ($this->isGeneratorClosed ?? false) {
|
||||
trigger_deprecation('symfony/mime', '6.4', 'Sending an email with a closed generator is deprecated and will throw in 7.0.');
|
||||
// throw new LogicException('Unable to send the email as its generator is already closed.');
|
||||
}
|
||||
|
||||
if (\is_string($this->message)) {
|
||||
yield $this->message;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (\is_resource($this->message)) {
|
||||
rewind($this->message);
|
||||
while ($line = fgets($this->message)) {
|
||||
yield $line;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->message instanceof \Generator) {
|
||||
$message = fopen('php://temp', 'w+');
|
||||
foreach ($this->message as $chunk) {
|
||||
fwrite($message, $chunk);
|
||||
yield $chunk;
|
||||
}
|
||||
$this->isGeneratorClosed = !$this->message->valid();
|
||||
$this->message = $message;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->message as $chunk) {
|
||||
yield $chunk;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*
|
||||
* @throws LogicException if the message is not valid
|
||||
*/
|
||||
public function ensureValidity()
|
||||
{
|
||||
}
|
||||
|
||||
public function __serialize(): array
|
||||
{
|
||||
return [$this->toString()];
|
||||
}
|
||||
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
[$this->message] = $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "symfony/mime",
|
||||
"type": "library",
|
||||
"description": "Allows manipulating MIME messages",
|
||||
"keywords": ["mime", "mime-type"],
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/polyfill-intl-idn": "^1.10",
|
||||
"symfony/polyfill-mbstring": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"egulias/email-validator": "^2.1.10|^3.1|^4",
|
||||
"league/html-to-markdown": "^5.0",
|
||||
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
|
||||
"symfony/dependency-injection": "^5.4|^6.0|^7.0",
|
||||
"symfony/process": "^5.4|^6.4|^7.0",
|
||||
"symfony/property-access": "^5.4|^6.0|^7.0",
|
||||
"symfony/property-info": "^5.4|^6.0|^7.0",
|
||||
"symfony/serializer": "^6.4.3|^7.0.3"
|
||||
},
|
||||
"conflict": {
|
||||
"egulias/email-validator": "~3.0.0",
|
||||
"phpdocumentor/reflection-docblock": "<3.2.2",
|
||||
"phpdocumentor/type-resolver": "<1.4.0",
|
||||
"symfony/mailer": "<5.4",
|
||||
"symfony/serializer": "<6.4.3|>7.0,<7.0.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Component\\Mime\\": "" },
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
Reference in New Issue
Block a user