vendor/symfony/mime/Email.php line 168

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\Mime;
  11. use Symfony\Component\Mime\Exception\LogicException;
  12. use Symfony\Component\Mime\Part\AbstractPart;
  13. use Symfony\Component\Mime\Part\DataPart;
  14. use Symfony\Component\Mime\Part\Multipart\AlternativePart;
  15. use Symfony\Component\Mime\Part\Multipart\MixedPart;
  16. use Symfony\Component\Mime\Part\Multipart\RelatedPart;
  17. use Symfony\Component\Mime\Part\TextPart;
  18. /**
  19.  * @author Fabien Potencier <fabien@symfony.com>
  20.  */
  21. class Email extends Message
  22. {
  23.     public const PRIORITY_HIGHEST 1;
  24.     public const PRIORITY_HIGH 2;
  25.     public const PRIORITY_NORMAL 3;
  26.     public const PRIORITY_LOW 4;
  27.     public const PRIORITY_LOWEST 5;
  28.     private const PRIORITY_MAP = [
  29.         self::PRIORITY_HIGHEST => 'Highest',
  30.         self::PRIORITY_HIGH => 'High',
  31.         self::PRIORITY_NORMAL => 'Normal',
  32.         self::PRIORITY_LOW => 'Low',
  33.         self::PRIORITY_LOWEST => 'Lowest',
  34.     ];
  35.     /**
  36.      * @var resource|string|null
  37.      */
  38.     private $text;
  39.     private ?string $textCharset null;
  40.     /**
  41.      * @var resource|string|null
  42.      */
  43.     private $html;
  44.     private ?string $htmlCharset null;
  45.     private array $attachments = [];
  46.     private ?AbstractPart $cachedBody null// Used to avoid wrong body hash in DKIM signatures with multiple parts (e.g. HTML + TEXT) due to multiple boundaries.
  47.     /**
  48.      * @return $this
  49.      */
  50.     public function subject(string $subject): static
  51.     {
  52.         return $this->setHeaderBody('Text''Subject'$subject);
  53.     }
  54.     public function getSubject(): ?string
  55.     {
  56.         return $this->getHeaders()->getHeaderBody('Subject');
  57.     }
  58.     /**
  59.      * @return $this
  60.      */
  61.     public function date(\DateTimeInterface $dateTime): static
  62.     {
  63.         return $this->setHeaderBody('Date''Date'$dateTime);
  64.     }
  65.     public function getDate(): ?\DateTimeImmutable
  66.     {
  67.         return $this->getHeaders()->getHeaderBody('Date');
  68.     }
  69.     /**
  70.      * @return $this
  71.      */
  72.     public function returnPath(Address|string $address): static
  73.     {
  74.         return $this->setHeaderBody('Path''Return-Path'Address::create($address));
  75.     }
  76.     public function getReturnPath(): ?Address
  77.     {
  78.         return $this->getHeaders()->getHeaderBody('Return-Path');
  79.     }
  80.     /**
  81.      * @return $this
  82.      */
  83.     public function sender(Address|string $address): static
  84.     {
  85.         return $this->setHeaderBody('Mailbox''Sender'Address::create($address));
  86.     }
  87.     public function getSender(): ?Address
  88.     {
  89.         return $this->getHeaders()->getHeaderBody('Sender');
  90.     }
  91.     /**
  92.      * @return $this
  93.      */
  94.     public function addFrom(Address|string ...$addresses): static
  95.     {
  96.         return $this->addListAddressHeaderBody('From'$addresses);
  97.     }
  98.     /**
  99.      * @return $this
  100.      */
  101.     public function from(Address|string ...$addresses): static
  102.     {
  103.         return $this->setListAddressHeaderBody('From'$addresses);
  104.     }
  105.     /**
  106.      * @return Address[]
  107.      */
  108.     public function getFrom(): array
  109.     {
  110.         return $this->getHeaders()->getHeaderBody('From') ?: [];
  111.     }
  112.     /**
  113.      * @return $this
  114.      */
  115.     public function addReplyTo(Address|string ...$addresses): static
  116.     {
  117.         return $this->addListAddressHeaderBody('Reply-To'$addresses);
  118.     }
  119.     /**
  120.      * @return $this
  121.      */
  122.     public function replyTo(Address|string ...$addresses): static
  123.     {
  124.         return $this->setListAddressHeaderBody('Reply-To'$addresses);
  125.     }
  126.     /**
  127.      * @return Address[]
  128.      */
  129.     public function getReplyTo(): array
  130.     {
  131.         return $this->getHeaders()->getHeaderBody('Reply-To') ?: [];
  132.     }
  133.     /**
  134.      * @return $this
  135.      */
  136.     public function addTo(Address|string ...$addresses): static
  137.     {
  138.         return $this->addListAddressHeaderBody('To'$addresses);
  139.     }
  140.     /**
  141.      * @return $this
  142.      */
  143.     public function to(Address|string ...$addresses): static
  144.     {
  145.         return $this->setListAddressHeaderBody('To'$addresses);
  146.     }
  147.     /**
  148.      * @return Address[]
  149.      */
  150.     public function getTo(): array
  151.     {
  152.         return $this->getHeaders()->getHeaderBody('To') ?: [];
  153.     }
  154.     /**
  155.      * @return $this
  156.      */
  157.     public function addCc(Address|string ...$addresses): static
  158.     {
  159.         return $this->addListAddressHeaderBody('Cc'$addresses);
  160.     }
  161.     /**
  162.      * @return $this
  163.      */
  164.     public function cc(Address|string ...$addresses): static
  165.     {
  166.         return $this->setListAddressHeaderBody('Cc'$addresses);
  167.     }
  168.     /**
  169.      * @return Address[]
  170.      */
  171.     public function getCc(): array
  172.     {
  173.         return $this->getHeaders()->getHeaderBody('Cc') ?: [];
  174.     }
  175.     /**
  176.      * @return $this
  177.      */
  178.     public function addBcc(Address|string ...$addresses): static
  179.     {
  180.         return $this->addListAddressHeaderBody('Bcc'$addresses);
  181.     }
  182.     /**
  183.      * @return $this
  184.      */
  185.     public function bcc(Address|string ...$addresses): static
  186.     {
  187.         return $this->setListAddressHeaderBody('Bcc'$addresses);
  188.     }
  189.     /**
  190.      * @return Address[]
  191.      */
  192.     public function getBcc(): array
  193.     {
  194.         return $this->getHeaders()->getHeaderBody('Bcc') ?: [];
  195.     }
  196.     /**
  197.      * Sets the priority of this message.
  198.      *
  199.      * The value is an integer where 1 is the highest priority and 5 is the lowest.
  200.      *
  201.      * @return $this
  202.      */
  203.     public function priority(int $priority): static
  204.     {
  205.         if ($priority 5) {
  206.             $priority 5;
  207.         } elseif ($priority 1) {
  208.             $priority 1;
  209.         }
  210.         return $this->setHeaderBody('Text''X-Priority'sprintf('%d (%s)'$priorityself::PRIORITY_MAP[$priority]));
  211.     }
  212.     /**
  213.      * Get the priority of this message.
  214.      *
  215.      * The returned value is an integer where 1 is the highest priority and 5
  216.      * is the lowest.
  217.      */
  218.     public function getPriority(): int
  219.     {
  220.         [$priority] = sscanf($this->getHeaders()->getHeaderBody('X-Priority') ?? '''%[1-5]');
  221.         return $priority ?? 3;
  222.     }
  223.     /**
  224.      * @param resource|string|null $body
  225.      *
  226.      * @return $this
  227.      */
  228.     public function text($bodystring $charset 'utf-8'): static
  229.     {
  230.         if (null !== $body && !\is_string($body) && !\is_resource($body)) {
  231.             throw new \TypeError(sprintf('The body must be a string, a resource or null (got "%s").'get_debug_type($body)));
  232.         }
  233.         $this->cachedBody null;
  234.         $this->text $body;
  235.         $this->textCharset $charset;
  236.         return $this;
  237.     }
  238.     /**
  239.      * @return resource|string|null
  240.      */
  241.     public function getTextBody()
  242.     {
  243.         return $this->text;
  244.     }
  245.     public function getTextCharset(): ?string
  246.     {
  247.         return $this->textCharset;
  248.     }
  249.     /**
  250.      * @param resource|string|null $body
  251.      *
  252.      * @return $this
  253.      */
  254.     public function html($bodystring $charset 'utf-8'): static
  255.     {
  256.         if (null !== $body && !\is_string($body) && !\is_resource($body)) {
  257.             throw new \TypeError(sprintf('The body must be a string, a resource or null (got "%s").'get_debug_type($body)));
  258.         }
  259.         $this->cachedBody null;
  260.         $this->html $body;
  261.         $this->htmlCharset $charset;
  262.         return $this;
  263.     }
  264.     /**
  265.      * @return resource|string|null
  266.      */
  267.     public function getHtmlBody()
  268.     {
  269.         return $this->html;
  270.     }
  271.     public function getHtmlCharset(): ?string
  272.     {
  273.         return $this->htmlCharset;
  274.     }
  275.     /**
  276.      * @param resource|string $body
  277.      *
  278.      * @return $this
  279.      */
  280.     public function attach($bodystring $name nullstring $contentType null): static
  281.     {
  282.         if (!\is_string($body) && !\is_resource($body)) {
  283.             throw new \TypeError(sprintf('The body must be a string or a resource (got "%s").'get_debug_type($body)));
  284.         }
  285.         $this->cachedBody null;
  286.         $this->attachments[] = ['body' => $body'name' => $name'content-type' => $contentType'inline' => false];
  287.         return $this;
  288.     }
  289.     /**
  290.      * @return $this
  291.      */
  292.     public function attachFromPath(string $pathstring $name nullstring $contentType null): static
  293.     {
  294.         $this->cachedBody null;
  295.         $this->attachments[] = ['path' => $path'name' => $name'content-type' => $contentType'inline' => false];
  296.         return $this;
  297.     }
  298.     /**
  299.      * @param resource|string $body
  300.      *
  301.      * @return $this
  302.      */
  303.     public function embed($bodystring $name nullstring $contentType null): static
  304.     {
  305.         if (!\is_string($body) && !\is_resource($body)) {
  306.             throw new \TypeError(sprintf('The body must be a string or a resource (got "%s").'get_debug_type($body)));
  307.         }
  308.         $this->cachedBody null;
  309.         $this->attachments[] = ['body' => $body'name' => $name'content-type' => $contentType'inline' => true];
  310.         return $this;
  311.     }
  312.     /**
  313.      * @return $this
  314.      */
  315.     public function embedFromPath(string $pathstring $name nullstring $contentType null): static
  316.     {
  317.         $this->cachedBody null;
  318.         $this->attachments[] = ['path' => $path'name' => $name'content-type' => $contentType'inline' => true];
  319.         return $this;
  320.     }
  321.     /**
  322.      * @return $this
  323.      */
  324.     public function attachPart(DataPart $part): static
  325.     {
  326.         $this->cachedBody null;
  327.         $this->attachments[] = ['part' => $part];
  328.         return $this;
  329.     }
  330.     /**
  331.      * @return array|DataPart[]
  332.      */
  333.     public function getAttachments(): array
  334.     {
  335.         $parts = [];
  336.         foreach ($this->attachments as $attachment) {
  337.             $parts[] = $this->createDataPart($attachment);
  338.         }
  339.         return $parts;
  340.     }
  341.     public function getBody(): AbstractPart
  342.     {
  343.         if (null !== $body parent::getBody()) {
  344.             return $body;
  345.         }
  346.         return $this->generateBody();
  347.     }
  348.     public function ensureValidity()
  349.     {
  350.         $this->ensureBodyValid();
  351.         if ('1' === $this->getHeaders()->getHeaderBody('X-Unsent')) {
  352.             throw new LogicException('Cannot send messages marked as "draft".');
  353.         }
  354.         parent::ensureValidity();
  355.     }
  356.     private function ensureBodyValid(): void
  357.     {
  358.         if (null === $this->text && null === $this->html && !$this->attachments) {
  359.             throw new LogicException('A message must have a text or an HTML part or attachments.');
  360.         }
  361.     }
  362.     /**
  363.      * Generates an AbstractPart based on the raw body of a message.
  364.      *
  365.      * The most "complex" part generated by this method is when there is text and HTML bodies
  366.      * with related images for the HTML part and some attachments:
  367.      *
  368.      * multipart/mixed
  369.      *         |
  370.      *         |------------> multipart/related
  371.      *         |                      |
  372.      *         |                      |------------> multipart/alternative
  373.      *         |                      |                      |
  374.      *         |                      |                       ------------> text/plain (with content)
  375.      *         |                      |                      |
  376.      *         |                      |                       ------------> text/html (with content)
  377.      *         |                      |
  378.      *         |                       ------------> image/png (with content)
  379.      *         |
  380.      *          ------------> application/pdf (with content)
  381.      */
  382.     private function generateBody(): AbstractPart
  383.     {
  384.         if (null !== $this->cachedBody) {
  385.             return $this->cachedBody;
  386.         }
  387.         $this->ensureBodyValid();
  388.         [$htmlPart$attachmentParts$inlineParts] = $this->prepareParts();
  389.         $part null === $this->text null : new TextPart($this->text$this->textCharset);
  390.         if (null !== $htmlPart) {
  391.             if (null !== $part) {
  392.                 $part = new AlternativePart($part$htmlPart);
  393.             } else {
  394.                 $part $htmlPart;
  395.             }
  396.         }
  397.         if ($inlineParts) {
  398.             $part = new RelatedPart($part, ...$inlineParts);
  399.         }
  400.         if ($attachmentParts) {
  401.             if ($part) {
  402.                 $part = new MixedPart($part, ...$attachmentParts);
  403.             } else {
  404.                 $part = new MixedPart(...$attachmentParts);
  405.             }
  406.         }
  407.         return $this->cachedBody $part;
  408.     }
  409.     private function prepareParts(): ?array
  410.     {
  411.         $names = [];
  412.         $htmlPart null;
  413.         $html $this->html;
  414.         if (null !== $html) {
  415.             $htmlPart = new TextPart($html$this->htmlCharset'html');
  416.             $html $htmlPart->getBody();
  417.             $regexes = [
  418.                 '<img\s+[^>]*src\s*=\s*(?:([\'"])cid:(.+?)\\1|cid:([^>\s]+))',
  419.                 '<\w+\s+[^>]*background\s*=\s*(?:([\'"])cid:(.+?)\\1|cid:([^>\s]+))',
  420.             ];
  421.             $tmpMatches = [];
  422.             foreach ($regexes as $regex) {
  423.                 preg_match_all('/'.$regex.'/i'$html$tmpMatches);
  424.                 $names array_merge($names$tmpMatches[2], $tmpMatches[3]);
  425.             }
  426.             $names array_filter(array_unique($names));
  427.         }
  428.         // usage of reflection is a temporary workaround for missing getters that will be added in 6.2
  429.         $dispositionRef = new \ReflectionProperty(TextPart::class, 'disposition');
  430.         $dispositionRef->setAccessible(true);
  431.         $nameRef = new \ReflectionProperty(TextPart::class, 'name');
  432.         $nameRef->setAccessible(true);
  433.         $attachmentParts $inlineParts = [];
  434.         foreach ($this->attachments as $attachment) {
  435.             $part $this->createDataPart($attachment);
  436.             if (isset($attachment['part'])) {
  437.                 $attachment['name'] = $nameRef->getValue($part);
  438.             }
  439.             foreach ($names as $name) {
  440.                 if ($name !== $attachment['name']) {
  441.                     continue;
  442.                 }
  443.                 if (isset($inlineParts[$name])) {
  444.                     continue 2;
  445.                 }
  446.                 $part->setDisposition('inline');
  447.                 $html str_replace('cid:'.$name'cid:'.$part->getContentId(), $html);
  448.                 $part->setName($part->getContentId());
  449.                 break;
  450.             }
  451.             if ('inline' === $dispositionRef->getValue($part)) {
  452.                 $inlineParts[$attachment['name']] = $part;
  453.             } else {
  454.                 $attachmentParts[] = $part;
  455.             }
  456.         }
  457.         if (null !== $htmlPart) {
  458.             $htmlPart = new TextPart($html$this->htmlCharset'html');
  459.         }
  460.         return [$htmlPart$attachmentPartsarray_values($inlineParts)];
  461.     }
  462.     private function createDataPart(array $attachment): DataPart
  463.     {
  464.         if (isset($attachment['part'])) {
  465.             return $attachment['part'];
  466.         }
  467.         if (isset($attachment['body'])) {
  468.             $part = new DataPart($attachment['body'], $attachment['name'] ?? null$attachment['content-type'] ?? null);
  469.         } else {
  470.             $part DataPart::fromPath($attachment['path'] ?? ''$attachment['name'] ?? null$attachment['content-type'] ?? null);
  471.         }
  472.         if ($attachment['inline']) {
  473.             $part->asInline();
  474.         }
  475.         return $part;
  476.     }
  477.     /**
  478.      * @return $this
  479.      */
  480.     private function setHeaderBody(string $typestring $name$body): static
  481.     {
  482.         $this->getHeaders()->setHeaderBody($type$name$body);
  483.         return $this;
  484.     }
  485.     private function addListAddressHeaderBody(string $name, array $addresses)
  486.     {
  487.         if (!$header $this->getHeaders()->get($name)) {
  488.             return $this->setListAddressHeaderBody($name$addresses);
  489.         }
  490.         $header->addAddresses(Address::createArray($addresses));
  491.         return $this;
  492.     }
  493.     /**
  494.      * @return $this
  495.      */
  496.     private function setListAddressHeaderBody(string $name, array $addresses): static
  497.     {
  498.         $addresses Address::createArray($addresses);
  499.         $headers $this->getHeaders();
  500.         if ($header $headers->get($name)) {
  501.             $header->setAddresses($addresses);
  502.         } else {
  503.             $headers->addMailboxListHeader($name$addresses);
  504.         }
  505.         return $this;
  506.     }
  507.     /**
  508.      * @internal
  509.      */
  510.     public function __serialize(): array
  511.     {
  512.         if (\is_resource($this->text)) {
  513.             $this->text = (new TextPart($this->text))->getBody();
  514.         }
  515.         if (\is_resource($this->html)) {
  516.             $this->html = (new TextPart($this->html))->getBody();
  517.         }
  518.         foreach ($this->attachments as $i => $attachment) {
  519.             if (isset($attachment['body']) && \is_resource($attachment['body'])) {
  520.                 $this->attachments[$i]['body'] = (new TextPart($attachment['body']))->getBody();
  521.             }
  522.         }
  523.         return [$this->text$this->textCharset$this->html$this->htmlCharset$this->attachmentsparent::__serialize()];
  524.     }
  525.     /**
  526.      * @internal
  527.      */
  528.     public function __unserialize(array $data): void
  529.     {
  530.         [$this->text$this->textCharset$this->html$this->htmlCharset$this->attachments$parentData] = $data;
  531.         parent::__unserialize($parentData);
  532.     }
  533. }