2023-07-15 10:16:32 +08:00
< ? 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\Part\AbstractPart ;
use Symfony\Component\Mime\Part\DataPart ;
use Symfony\Component\Mime\Part\Multipart\AlternativePart ;
use Symfony\Component\Mime\Part\Multipart\MixedPart ;
use Symfony\Component\Mime\Part\Multipart\RelatedPart ;
use Symfony\Component\Mime\Part\TextPart ;
/**
* @ author Fabien Potencier < fabien @ symfony . com >
*/
class Email extends Message
{
public const PRIORITY_HIGHEST = 1 ;
public const PRIORITY_HIGH = 2 ;
public const PRIORITY_NORMAL = 3 ;
public const PRIORITY_LOW = 4 ;
public const PRIORITY_LOWEST = 5 ;
private const PRIORITY_MAP = [
self :: PRIORITY_HIGHEST => 'Highest' ,
self :: PRIORITY_HIGH => 'High' ,
self :: PRIORITY_NORMAL => 'Normal' ,
self :: PRIORITY_LOW => 'Low' ,
self :: PRIORITY_LOWEST => 'Lowest' ,
];
/**
* @ var resource | string | null
*/
private $text ;
private ? string $textCharset = null ;
/**
* @ var resource | string | null
*/
private $html ;
private ? string $htmlCharset = null ;
private array $attachments = [];
private ? AbstractPart $cachedBody = null ; // Used to avoid wrong body hash in DKIM signatures with multiple parts (e.g. HTML + TEXT) due to multiple boundaries.
/**
* @ return $this
*/
public function subject ( string $subject ) : static
{
return $this -> setHeaderBody ( 'Text' , 'Subject' , $subject );
}
public function getSubject () : ? string
{
return $this -> getHeaders () -> getHeaderBody ( 'Subject' );
}
/**
* @ return $this
*/
public function date ( \DateTimeInterface $dateTime ) : static
{
return $this -> setHeaderBody ( 'Date' , 'Date' , $dateTime );
}
public function getDate () : ? \DateTimeImmutable
{
return $this -> getHeaders () -> getHeaderBody ( 'Date' );
}
/**
* @ return $this
*/
public function returnPath ( Address | string $address ) : static
{
return $this -> setHeaderBody ( 'Path' , 'Return-Path' , Address :: create ( $address ));
}
public function getReturnPath () : ? Address
{
return $this -> getHeaders () -> getHeaderBody ( 'Return-Path' );
}
/**
* @ return $this
*/
public function sender ( Address | string $address ) : static
{
return $this -> setHeaderBody ( 'Mailbox' , 'Sender' , Address :: create ( $address ));
}
public function getSender () : ? Address
{
return $this -> getHeaders () -> getHeaderBody ( 'Sender' );
}
/**
* @ return $this
*/
public function addFrom ( Address | string ... $addresses ) : static
{
return $this -> addListAddressHeaderBody ( 'From' , $addresses );
}
/**
* @ return $this
*/
public function from ( Address | string ... $addresses ) : static
{
return $this -> setListAddressHeaderBody ( 'From' , $addresses );
}
/**
* @ return Address []
*/
public function getFrom () : array
{
return $this -> getHeaders () -> getHeaderBody ( 'From' ) ? : [];
}
/**
* @ return $this
*/
public function addReplyTo ( Address | string ... $addresses ) : static
{
return $this -> addListAddressHeaderBody ( 'Reply-To' , $addresses );
}
/**
* @ return $this
*/
public function replyTo ( Address | string ... $addresses ) : static
{
return $this -> setListAddressHeaderBody ( 'Reply-To' , $addresses );
}
/**
* @ return Address []
*/
public function getReplyTo () : array
{
return $this -> getHeaders () -> getHeaderBody ( 'Reply-To' ) ? : [];
}
/**
* @ return $this
*/
public function addTo ( Address | string ... $addresses ) : static
{
return $this -> addListAddressHeaderBody ( 'To' , $addresses );
}
/**
* @ return $this
*/
public function to ( Address | string ... $addresses ) : static
{
return $this -> setListAddressHeaderBody ( 'To' , $addresses );
}
/**
* @ return Address []
*/
public function getTo () : array
{
return $this -> getHeaders () -> getHeaderBody ( 'To' ) ? : [];
}
/**
* @ return $this
*/
public function addCc ( Address | string ... $addresses ) : static
{
return $this -> addListAddressHeaderBody ( 'Cc' , $addresses );
}
/**
* @ return $this
*/
public function cc ( Address | string ... $addresses ) : static
{
return $this -> setListAddressHeaderBody ( 'Cc' , $addresses );
}
/**
* @ return Address []
*/
public function getCc () : array
{
return $this -> getHeaders () -> getHeaderBody ( 'Cc' ) ? : [];
}
/**
* @ return $this
*/
public function addBcc ( Address | string ... $addresses ) : static
{
return $this -> addListAddressHeaderBody ( 'Bcc' , $addresses );
}
/**
* @ return $this
*/
public function bcc ( Address | string ... $addresses ) : static
{
return $this -> setListAddressHeaderBody ( 'Bcc' , $addresses );
}
/**
* @ return Address []
*/
public function getBcc () : array
{
return $this -> getHeaders () -> getHeaderBody ( 'Bcc' ) ? : [];
}
/**
* Sets the priority of this message .
*
* The value is an integer where 1 is the highest priority and 5 is the lowest .
*
* @ return $this
*/
public function priority ( int $priority ) : static
{
if ( $priority > 5 ) {
$priority = 5 ;
} elseif ( $priority < 1 ) {
$priority = 1 ;
}
return $this -> setHeaderBody ( 'Text' , 'X-Priority' , sprintf ( '%d (%s)' , $priority , self :: PRIORITY_MAP [ $priority ]));
}
/**
* Get the priority of this message .
*
* The returned value is an integer where 1 is the highest priority and 5
* is the lowest .
*/
public function getPriority () : int
{
[ $priority ] = sscanf ( $this -> getHeaders () -> getHeaderBody ( 'X-Priority' ) ? ? '' , '%[1-5]' );
return $priority ? ? 3 ;
}
/**
* @ param resource | string | null $body
*
* @ return $this
*/
public function text ( $body , string $charset = 'utf-8' ) : static
{
if ( null !== $body && ! \is_string ( $body ) && ! \is_resource ( $body )) {
throw new \TypeError ( sprintf ( 'The body must be a string, a resource or null (got "%s").' , get_debug_type ( $body )));
}
$this -> cachedBody = null ;
$this -> text = $body ;
$this -> textCharset = $charset ;
return $this ;
}
/**
* @ return resource | string | null
*/
public function getTextBody ()
{
return $this -> text ;
}
public function getTextCharset () : ? string
{
return $this -> textCharset ;
}
/**
* @ param resource | string | null $body
*
* @ return $this
*/
public function html ( $body , string $charset = 'utf-8' ) : static
{
if ( null !== $body && ! \is_string ( $body ) && ! \is_resource ( $body )) {
throw new \TypeError ( sprintf ( 'The body must be a string, a resource or null (got "%s").' , get_debug_type ( $body )));
}
$this -> cachedBody = null ;
$this -> html = $body ;
$this -> htmlCharset = $charset ;
return $this ;
}
/**
* @ return resource | string | null
*/
public function getHtmlBody ()
{
return $this -> html ;
}
public function getHtmlCharset () : ? string
{
return $this -> htmlCharset ;
}
/**
* @ param resource | string $body
*
* @ return $this
*/
public function attach ( $body , string $name = null , string $contentType = null ) : static
{
2023-08-09 15:01:26 +08:00
if ( ! \is_string ( $body ) && ! \is_resource ( $body )) {
throw new \TypeError ( sprintf ( 'The body must be a string or a resource (got "%s").' , get_debug_type ( $body )));
}
$this -> cachedBody = null ;
$this -> attachments [] = [ 'body' => $body , 'name' => $name , 'content-type' => $contentType , 'inline' => false ];
return $this ;
2023-07-15 10:16:32 +08:00
}
/**
* @ return $this
*/
public function attachFromPath ( string $path , string $name = null , string $contentType = null ) : static
{
2023-08-09 15:01:26 +08:00
$this -> cachedBody = null ;
$this -> attachments [] = [ 'path' => $path , 'name' => $name , 'content-type' => $contentType , 'inline' => false ];
return $this ;
2023-07-15 10:16:32 +08:00
}
/**
* @ param resource | string $body
*
* @ return $this
*/
public function embed ( $body , string $name = null , string $contentType = null ) : static
{
2023-08-09 15:01:26 +08:00
if ( ! \is_string ( $body ) && ! \is_resource ( $body )) {
throw new \TypeError ( sprintf ( 'The body must be a string or a resource (got "%s").' , get_debug_type ( $body )));
}
2023-07-15 10:16:32 +08:00
2023-08-09 15:01:26 +08:00
$this -> cachedBody = null ;
$this -> attachments [] = [ 'body' => $body , 'name' => $name , 'content-type' => $contentType , 'inline' => true ];
return $this ;
2023-07-17 09:56:51 +08:00
}
/**
* @ return $this
*/
2023-08-09 15:01:26 +08:00
public function embedFromPath ( string $path , string $name = null , string $contentType = null ) : static
2023-08-09 14:43:30 +08:00
{
2023-08-09 15:01:26 +08:00
$this -> cachedBody = null ;
$this -> attachments [] = [ 'path' => $path , 'name' => $name , 'content-type' => $contentType , 'inline' => true ];
2023-08-09 14:43:30 +08:00
2023-08-09 15:01:26 +08:00
return $this ;
2023-08-09 14:43:30 +08:00
}
/**
* @ return $this
*/
2023-08-09 15:01:26 +08:00
public function attachPart ( DataPart $part ) : static
2023-07-15 10:16:32 +08:00
{
$this -> cachedBody = null ;
2023-08-09 15:01:26 +08:00
$this -> attachments [] = [ 'part' => $part ];
2023-07-15 10:16:32 +08:00
return $this ;
}
/**
2023-08-09 15:01:26 +08:00
* @ return array | DataPart []
2023-07-15 10:16:32 +08:00
*/
public function getAttachments () : array
{
2023-08-09 15:01:26 +08:00
$parts = [];
foreach ( $this -> attachments as $attachment ) {
$parts [] = $this -> createDataPart ( $attachment );
}
return $parts ;
2023-07-15 10:16:32 +08:00
}
public function getBody () : AbstractPart
{
if ( null !== $body = parent :: getBody ()) {
return $body ;
}
return $this -> generateBody ();
}
public function ensureValidity ()
2023-08-09 14:43:30 +08:00
{
if ( null === $this -> text && null === $this -> html && ! $this -> attachments ) {
throw new LogicException ( 'A message must have a text or an HTML part or attachments.' );
}
2023-08-09 15:01:26 +08:00
parent :: ensureValidity ();
2023-08-09 14:43:30 +08:00
}
2023-07-15 10:16:32 +08:00
/**
* Generates an AbstractPart based on the raw body of a message .
*
* The most " complex " part generated by this method is when there is text and HTML bodies
* with related images for the HTML part and some attachments :
*
* multipart / mixed
* |
* |------------> multipart / related
* | |
* | |------------> multipart / alternative
* | | |
* | | ------------> text / plain ( with content )
* | | |
* | | ------------> text / html ( with content )
* | |
* | ------------> image / png ( with content )
* |
* ------------> application / pdf ( with content )
*/
private function generateBody () : AbstractPart
{
if ( null !== $this -> cachedBody ) {
return $this -> cachedBody ;
}
2023-08-09 15:01:26 +08:00
$this -> ensureValidity ();
2023-07-15 10:16:32 +08:00
[ $htmlPart , $otherParts , $relatedParts ] = $this -> prepareParts ();
$part = null === $this -> text ? null : new TextPart ( $this -> text , $this -> textCharset );
if ( null !== $htmlPart ) {
if ( null !== $part ) {
$part = new AlternativePart ( $part , $htmlPart );
} else {
$part = $htmlPart ;
}
}
if ( $relatedParts ) {
$part = new RelatedPart ( $part , ... $relatedParts );
}
if ( $otherParts ) {
if ( $part ) {
$part = new MixedPart ( $part , ... $otherParts );
} else {
$part = new MixedPart ( ... $otherParts );
}
}
return $this -> cachedBody = $part ;
}
private function prepareParts () : ? array
{
$names = [];
$htmlPart = null ;
$html = $this -> html ;
if ( null !== $html ) {
$htmlPart = new TextPart ( $html , $this -> htmlCharset , 'html' );
$html = $htmlPart -> getBody ();
2023-08-09 15:01:26 +08:00
preg_match_all ( '(<img\s+[^>]*src\s*=\s*(?:([\'"])cid:(.+?)\\1|cid:([^>\s]+)))i' , $html , $names );
$names = array_filter ( array_unique ( array_merge ( $names [ 2 ], $names [ 3 ])));
2023-08-09 14:43:30 +08:00
}
2023-07-18 11:17:17 +08:00
2023-08-09 15:01:26 +08:00
// usage of reflection is a temporary workaround for missing getters that will be added in 6.2
$nameRef = new \ReflectionProperty ( TextPart :: class , 'name' );
$nameRef -> setAccessible ( true );
2023-08-09 14:43:30 +08:00
$otherParts = $relatedParts = [];
2023-08-09 15:01:26 +08:00
foreach ( $this -> attachments as $attachment ) {
$part = $this -> createDataPart ( $attachment );
if ( isset ( $attachment [ 'part' ])) {
$attachment [ 'name' ] = $nameRef -> getValue ( $part );
}
$related = false ;
2023-07-15 10:16:32 +08:00
foreach ( $names as $name ) {
2023-08-09 15:01:26 +08:00
if ( $name !== $attachment [ 'name' ]) {
2023-07-15 10:16:32 +08:00
continue ;
}
if ( isset ( $relatedParts [ $name ])) {
continue 2 ;
}
2023-08-09 15:01:26 +08:00
$part -> setDisposition ( 'inline' );
$html = str_replace ( 'cid:' . $name , 'cid:' . $part -> getContentId (), $html , $count );
if ( $count ) {
$related = true ;
2023-07-15 10:16:32 +08:00
}
2023-08-09 15:01:26 +08:00
$part -> setName ( $part -> getContentId ());
2023-07-15 10:16:32 +08:00
2023-08-09 15:01:26 +08:00
break ;
2023-07-15 10:16:32 +08:00
}
2023-08-09 15:01:26 +08:00
if ( $related ) {
$relatedParts [ $attachment [ 'name' ]] = $part ;
} else {
$otherParts [] = $part ;
}
2023-07-15 10:16:32 +08:00
}
if ( null !== $htmlPart ) {
$htmlPart = new TextPart ( $html , $this -> htmlCharset , 'html' );
}
return [ $htmlPart , $otherParts , array_values ( $relatedParts )];
}
2023-08-09 15:01:26 +08:00
private function createDataPart ( array $attachment ) : DataPart
{
if ( isset ( $attachment [ 'part' ])) {
return $attachment [ 'part' ];
}
if ( isset ( $attachment [ 'body' ])) {
$part = new DataPart ( $attachment [ 'body' ], $attachment [ 'name' ] ? ? null , $attachment [ 'content-type' ] ? ? null );
} else {
$part = DataPart :: fromPath ( $attachment [ 'path' ] ? ? '' , $attachment [ 'name' ] ? ? null , $attachment [ 'content-type' ] ? ? null );
}
if ( $attachment [ 'inline' ]) {
$part -> asInline ();
}
return $part ;
}
2023-07-15 10:16:32 +08:00
/**
* @ return $this
*/
private function setHeaderBody ( string $type , string $name , $body ) : static
{
$this -> getHeaders () -> setHeaderBody ( $type , $name , $body );
return $this ;
}
2023-08-09 15:01:26 +08:00
private function addListAddressHeaderBody ( string $name , array $addresses )
2023-07-15 10:16:32 +08:00
{
if ( ! $header = $this -> getHeaders () -> get ( $name )) {
return $this -> setListAddressHeaderBody ( $name , $addresses );
}
$header -> addAddresses ( Address :: createArray ( $addresses ));
return $this ;
}
/**
* @ return $this
*/
private function setListAddressHeaderBody ( string $name , array $addresses ) : static
{
$addresses = Address :: createArray ( $addresses );
$headers = $this -> getHeaders ();
if ( $header = $headers -> get ( $name )) {
$header -> setAddresses ( $addresses );
} else {
$headers -> addMailboxListHeader ( $name , $addresses );
}
return $this ;
}
/**
* @ internal
*/
public function __serialize () : array
{
if ( \is_resource ( $this -> text )) {
$this -> text = ( new TextPart ( $this -> text )) -> getBody ();
}
if ( \is_resource ( $this -> html )) {
$this -> html = ( new TextPart ( $this -> html )) -> getBody ();
}
2023-08-09 15:01:26 +08:00
foreach ( $this -> attachments as $i => $attachment ) {
if ( isset ( $attachment [ 'body' ]) && \is_resource ( $attachment [ 'body' ])) {
$this -> attachments [ $i ][ 'body' ] = ( new TextPart ( $attachment [ 'body' ])) -> getBody ();
}
}
2023-07-15 10:16:32 +08:00
return [ $this -> text , $this -> textCharset , $this -> html , $this -> htmlCharset , $this -> attachments , parent :: __serialize ()];
}
/**
* @ internal
*/
public function __unserialize ( array $data ) : void
{
[ $this -> text , $this -> textCharset , $this -> html , $this -> htmlCharset , $this -> attachments , $parentData ] = $data ;
parent :: __unserialize ( $parentData );
}
}