├── src ├── XPriority.php ├── Debug │ ├── EmailCollection.php │ ├── icon │ │ └── email.svg │ └── EmailCollector.php ├── Header.php ├── Mailer.php └── Message.php ├── README.md ├── LICENSE ├── composer.json └── .phpstorm.meta.php /src/XPriority.php: -------------------------------------------------------------------------------- 1 | 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 Framework\Email; 11 | 12 | /** 13 | * Enum XPriority. 14 | * 15 | * @package email 16 | */ 17 | enum XPriority : int 18 | { 19 | case HIGHEST = 1; 20 | case HIGH = 2; 21 | case NORMAL = 3; 22 | case LOW = 4; 23 | case LOWEST = 5; 24 | } 25 | -------------------------------------------------------------------------------- /src/Debug/EmailCollection.php: -------------------------------------------------------------------------------- 1 | 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 Framework\Email\Debug; 11 | 12 | use Framework\Debug\Collection; 13 | 14 | /** 15 | * Class EmailCollection. 16 | * 17 | * @package email 18 | */ 19 | class EmailCollection extends Collection 20 | { 21 | protected string $iconPath = __DIR__ . '/icon/email.svg'; 22 | } 23 | -------------------------------------------------------------------------------- /src/Debug/icon/email.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Aplus Framework Email Library 2 | 3 | # Aplus Framework Email Library 4 | 5 | - [Home](https://aplus-framework.com/packages/email) 6 | - [User Guide](https://docs.aplus-framework.com/guides/libraries/email/index.html) 7 | - [API Documentation](https://docs.aplus-framework.com/packages/email.html) 8 | 9 | [![tests](https://github.com/aplus-framework/email/actions/workflows/tests.yml/badge.svg)](https://github.com/aplus-framework/email/actions/workflows/tests.yml) 10 | [![coverage](https://coveralls.io/repos/github/aplus-framework/email/badge.svg?branch=master)](https://coveralls.io/github/aplus-framework/email?branch=master) 11 | [![packagist](https://img.shields.io/packagist/v/aplus/email)](https://packagist.org/packages/aplus/email) 12 | [![open-source](https://img.shields.io/badge/open--source-sponsor-magenta)](https://aplus-framework.com/sponsor) 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Natan Felles 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aplus/email", 3 | "description": "Aplus Framework Email Library", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "email", 8 | "smtp", 9 | "mailer", 10 | "message" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Natan Felles", 15 | "email": "natanfelles@gmail.com", 16 | "homepage": "https://natanfelles.github.io" 17 | } 18 | ], 19 | "homepage": "https://aplus-framework.com/packages/email", 20 | "support": { 21 | "email": "support@aplus-framework.com", 22 | "issues": "https://github.com/aplus-framework/email/issues", 23 | "forum": "https://aplus-framework.com/forum", 24 | "source": "https://github.com/aplus-framework/email", 25 | "docs": "https://docs.aplus-framework.com/guides/libraries/email/" 26 | }, 27 | "funding": [ 28 | { 29 | "type": "Aplus Sponsor", 30 | "url": "https://aplus-framework.com/sponsor" 31 | } 32 | ], 33 | "require": { 34 | "php": ">=8.3", 35 | "ext-fileinfo": "*", 36 | "aplus/debug": "^4.3" 37 | }, 38 | "require-dev": { 39 | "ext-xdebug": "*", 40 | "aplus/coding-standard": "^2.8", 41 | "ergebnis/composer-normalize": "^2.23", 42 | "jetbrains/phpstorm-attributes": "^1.0", 43 | "phpmd/phpmd": "^2.13", 44 | "phpstan/phpstan": "^1.9", 45 | "phpunit/phpunit": "^10.5" 46 | }, 47 | "minimum-stability": "dev", 48 | "prefer-stable": true, 49 | "autoload": { 50 | "psr-4": { 51 | "Framework\\Email\\": "src/" 52 | } 53 | }, 54 | "autoload-dev": { 55 | "psr-4": { 56 | "Tests\\Email\\": "tests/" 57 | } 58 | }, 59 | "config": { 60 | "allow-plugins": { 61 | "ergebnis/composer-normalize": true 62 | }, 63 | "optimize-autoloader": true, 64 | "preferred-install": "dist", 65 | "sort-packages": true 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /.phpstorm.meta.php: -------------------------------------------------------------------------------- 1 | 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 PHPSTORM_META; 11 | 12 | registerArgumentsSet( 13 | 'config_keys', 14 | 'add_logs', 15 | 'charset', 16 | 'connection_timeout', 17 | 'crlf', 18 | 'host', 19 | 'hostname', 20 | 'keep_alive', 21 | 'options', 22 | 'password', 23 | 'port', 24 | 'response_timeout', 25 | 'tls', 26 | 'username', 27 | ); 28 | registerArgumentsSet( 29 | 'headers', 30 | \Framework\Email\Header::AUTO_SUBMITTED, 31 | \Framework\Email\Header::BCC, 32 | \Framework\Email\Header::CC, 33 | \Framework\Email\Header::COMMENTS, 34 | \Framework\Email\Header::CONTENT_TYPE, 35 | \Framework\Email\Header::DATE, 36 | \Framework\Email\Header::DKIM_SIGNATURE, 37 | \Framework\Email\Header::FROM, 38 | \Framework\Email\Header::IN_REPLY_TO, 39 | \Framework\Email\Header::KEYWORDS, 40 | \Framework\Email\Header::LIST_UNSUBSCRIBE_POST, 41 | \Framework\Email\Header::MESSAGE_ID, 42 | \Framework\Email\Header::MIME_VERSION, 43 | \Framework\Email\Header::MT_PRIORITY, 44 | \Framework\Email\Header::ORIGINAL_FROM, 45 | \Framework\Email\Header::ORIGINAL_RECIPIENT, 46 | \Framework\Email\Header::ORIGINAL_SUBJECT, 47 | \Framework\Email\Header::PRIORITY, 48 | \Framework\Email\Header::RECEIVED, 49 | \Framework\Email\Header::RECEIVED_SPF, 50 | \Framework\Email\Header::REFERENCES, 51 | \Framework\Email\Header::REPLY_TO, 52 | \Framework\Email\Header::RESENT_BCC, 53 | \Framework\Email\Header::RESENT_CC, 54 | \Framework\Email\Header::RESENT_DATE, 55 | \Framework\Email\Header::RESENT_FROM, 56 | \Framework\Email\Header::RESENT_MESSAGE_ID, 57 | \Framework\Email\Header::RESENT_SENDER, 58 | \Framework\Email\Header::RESENT_TO, 59 | \Framework\Email\Header::RETURN_PATH, 60 | \Framework\Email\Header::SENDER, 61 | \Framework\Email\Header::SUBJECT, 62 | \Framework\Email\Header::TO, 63 | \Framework\Email\Header::X_MAILER, 64 | \Framework\Email\Header::X_PRIORITY, 65 | 'Auto-Submitted', 66 | 'Bcc', 67 | 'Cc', 68 | 'Comments', 69 | 'Content-Type', 70 | 'DKIM-Signature', 71 | 'Date', 72 | 'From', 73 | 'In-Reply-To', 74 | 'Keywords', 75 | 'List-Unsubscribe-Post', 76 | 'MIME-Version', 77 | 'MT-Priority', 78 | 'Message-ID', 79 | 'Original-From', 80 | 'Original-Recipient', 81 | 'Original-Subject', 82 | 'Priority', 83 | 'Received', 84 | 'Received-SPF', 85 | 'References', 86 | 'Reply-To', 87 | 'Resent-Bcc', 88 | 'Resent-Cc', 89 | 'Resent-Date', 90 | 'Resent-From', 91 | 'Resent-Message-ID', 92 | 'Resent-Sender', 93 | 'Resent-To', 94 | 'Return-Path', 95 | 'Sender', 96 | 'Subject', 97 | 'To', 98 | 'X-Mailer', 99 | 'X-Priority', 100 | ); 101 | expectedArguments( 102 | \Framework\Email\Mailer::getConfig(), 103 | 0, 104 | argumentsSet('config_keys') 105 | ); 106 | expectedArguments( 107 | \Framework\Email\Message::getHeader(), 108 | 0, 109 | argumentsSet('headers') 110 | ); 111 | expectedArguments( 112 | \Framework\Email\Message::setHeader(), 113 | 0, 114 | argumentsSet('headers') 115 | ); 116 | expectedArguments( 117 | \Framework\Email\Message::removeHeader(), 118 | 0, 119 | argumentsSet('headers') 120 | ); 121 | -------------------------------------------------------------------------------- /src/Header.php: -------------------------------------------------------------------------------- 1 | 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 Framework\Email; 11 | 12 | /** 13 | * Class Header. 14 | * 15 | * @package email 16 | */ 17 | class Header 18 | { 19 | public const string AUTO_SUBMITTED = 'Auto-Submitted'; 20 | public const string BCC = 'Bcc'; 21 | public const string CC = 'Cc'; 22 | public const string COMMENTS = 'Comments'; 23 | public const string CONTENT_TYPE = 'Content-Type'; 24 | public const string DATE = 'Date'; 25 | public const string DKIM_SIGNATURE = 'DKIM-Signature'; 26 | public const string FROM = 'From'; 27 | public const string IN_REPLY_TO = 'In-Reply-To'; 28 | public const string KEYWORDS = 'Keywords'; 29 | public const string LIST_UNSUBSCRIBE_POST = 'List-Unsubscribe-Post'; 30 | public const string MESSAGE_ID = 'Message-ID'; 31 | public const string MIME_VERSION = 'MIME-Version'; 32 | public const string MT_PRIORITY = 'MT-Priority'; 33 | public const string ORIGINAL_FROM = 'Original-From'; 34 | public const string ORIGINAL_RECIPIENT = 'Original-Recipient'; 35 | public const string ORIGINAL_SUBJECT = 'Original-Subject'; 36 | public const string PRIORITY = 'Priority'; 37 | public const string RECEIVED = 'Received'; 38 | public const string RECEIVED_SPF = 'Received-SPF'; 39 | public const string REFERENCES = 'References'; 40 | public const string REPLY_TO = 'Reply-To'; 41 | public const string RESENT_BCC = 'Resent-Bcc'; 42 | public const string RESENT_CC = 'Resent-Cc'; 43 | public const string RESENT_DATE = 'Resent-Date'; 44 | public const string RESENT_FROM = 'Resent-From'; 45 | public const string RESENT_MESSAGE_ID = 'Resent-Message-ID'; 46 | public const string RESENT_SENDER = 'Resent-Sender'; 47 | public const string RESENT_TO = 'Resent-To'; 48 | public const string RETURN_PATH = 'Return-Path'; 49 | public const string SENDER = 'Sender'; 50 | public const string SUBJECT = 'Subject'; 51 | public const string TO = 'To'; 52 | public const string X_MAILER = 'X-Mailer'; 53 | public const string X_PRIORITY = 'X-Priority'; 54 | /** 55 | * @var array 56 | */ 57 | protected static array $headers = [ 58 | 'auto-submitted' => 'Auto-Submitted', 59 | 'bcc' => 'Bcc', 60 | 'cc' => 'Cc', 61 | 'comments' => 'Comments', 62 | 'content-type' => 'Content-Type', 63 | 'date' => 'Date', 64 | 'dkim-signature' => 'DKIM-Signature', 65 | 'from' => 'From', 66 | 'in-reply-to' => 'In-Reply-To', 67 | 'keywords' => 'Keywords', 68 | 'list-unsubscribe-post' => 'List-Unsubscribe-Post', 69 | 'message-id' => 'Message-ID', 70 | 'mime-version' => 'MIME-Version', 71 | 'mt-priority' => 'MT-Priority', 72 | 'original-from' => 'Original-From', 73 | 'original-recipient' => 'Original-Recipient', 74 | 'original-subject' => 'Original-Subject', 75 | 'priority' => 'Priority', 76 | 'received' => 'Received', 77 | 'received-spf' => 'Received-SPF', 78 | 'references' => 'References', 79 | 'reply-to' => 'Reply-To', 80 | 'resent-bcc' => 'Resent-Bcc', 81 | 'resent-cc' => 'Resent-Cc', 82 | 'resent-date' => 'Resent-Date', 83 | 'resent-from' => 'Resent-From', 84 | 'resent-message-id' => 'Resent-Message-ID', 85 | 'resent-sender' => 'Resent-Sender', 86 | 'resent-to' => 'Resent-To', 87 | 'return-path' => 'Return-Path', 88 | 'sender' => 'Sender', 89 | 'subject' => 'Subject', 90 | 'to' => 'To', 91 | 'x-mailer' => 'X-Mailer', 92 | 'x-priority' => 'X-Priority', 93 | ]; 94 | 95 | /** 96 | * Get a correct header name. 97 | * 98 | * @param string $name The header name in any case 99 | * 100 | * @return string The correct name or the same if it is unknown 101 | */ 102 | public static function getName(string $name) : string 103 | { 104 | return static::$headers[\strtolower($name)] ?? $name; 105 | } 106 | 107 | /** 108 | * Set a correct header name. 109 | * 110 | * @param string $name The header name in the correct case 111 | * 112 | * @return void 113 | */ 114 | public static function setName(string $name) : void 115 | { 116 | static::$headers[\strtolower($name)] = $name; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Debug/EmailCollector.php: -------------------------------------------------------------------------------- 1 | 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 Framework\Email\Debug; 11 | 12 | use Framework\Debug\Collector; 13 | use Framework\Debug\Debugger; 14 | use Framework\Email\Header; 15 | use Framework\Email\Mailer; 16 | 17 | /** 18 | * Class EmailCollector. 19 | * 20 | * @package email 21 | */ 22 | class EmailCollector extends Collector 23 | { 24 | protected Mailer $mailer; 25 | 26 | public function setMailer(Mailer $mailer) : static 27 | { 28 | $this->mailer = $mailer; 29 | return $this; 30 | } 31 | 32 | public function getActivities() : array 33 | { 34 | $activities = []; 35 | foreach ($this->getData() as $index => $data) { 36 | $activities[] = [ 37 | 'collector' => $this->getName(), 38 | 'class' => static::class, 39 | 'description' => 'Send message ' . ($index + 1), 40 | 'start' => $data['start'], 41 | 'end' => $data['end'], 42 | ]; 43 | } 44 | return $activities; 45 | } 46 | 47 | public function getContents() : string 48 | { 49 | \ob_start(); 50 | if (!isset($this->mailer)) { 51 | echo '

This collector has not been added to a Mailer instance.

'; 52 | return \ob_get_clean(); // @phpstan-ignore-line 53 | } 54 | echo $this->showHeader(); 55 | if (!$this->hasData()) { 56 | echo '

No messages have been sent.

'; 57 | return \ob_get_clean(); // @phpstan-ignore-line 58 | } 59 | $count = \count($this->getData()); ?> 60 |

Sent getTotalMessagesSent() ?> of message: 62 |

63 | getData() as $index => $data) : ?> 65 |

Message

66 |

Status: 67 |

68 |

Last Response:

69 |

From:

70 |

71 | Recipients: 72 |

73 |

Size:

74 |

75 | Time Sending: ms 76 |

77 |

Headers

78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | $value): ?> 87 | 88 | 89 | 90 | 91 | 92 | 93 |
NameValue
94 | 96 |

HTML Message

97 |
98 | 101 |

Plain Message

102 |
103 | 106 |

Attachments

107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 |
File
121 | 124 |

Inline Attachments

125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | $filename): ?> 134 | 135 | 136 | 137 | 138 | 139 | 140 |
Content-IDFile
141 | mailer->getConfigs(); 151 | ?> 152 |

Host:

153 |

Port:

154 | getData() as $data) { 162 | if ($data['code'] === 250) { 163 | $result++; 164 | } 165 | } 166 | return $result; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/Mailer.php: -------------------------------------------------------------------------------- 1 | 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 Framework\Email; 11 | 12 | use Framework\Email\Debug\EmailCollector; 13 | use JetBrains\PhpStorm\ArrayShape; 14 | use SensitiveParameter; 15 | 16 | /** 17 | * Class Mailer. 18 | * 19 | * @package email 20 | */ 21 | class Mailer 22 | { 23 | /** 24 | * @var array 25 | */ 26 | protected array $config = []; 27 | /** 28 | * @var false|resource $socket 29 | */ 30 | protected $socket = false; 31 | /** 32 | * @var array> 33 | */ 34 | protected array $logs = []; 35 | protected EmailCollector $debugCollector; 36 | protected ?string $lastResponse = null; 37 | 38 | /** 39 | * Mailer constructor. 40 | * 41 | * @param array|string $username 42 | * @param string|null $password 43 | * @param string $host 44 | * @param int $port 45 | * @param string|null $hostname 46 | */ 47 | public function __construct( 48 | #[SensitiveParameter] 49 | array | string $username, 50 | #[SensitiveParameter] 51 | ?string $password = null, 52 | string $host = 'localhost', 53 | int $port = 587, 54 | ?string $hostname = null 55 | ) { 56 | $this->config = \is_array($username) 57 | ? $this->makeConfig($username) 58 | : $this->makeConfig([ 59 | 'username' => $username, 60 | 'password' => $password, 61 | 'host' => $host, 62 | 'port' => $port, 63 | 'hostname' => $hostname ?? \gethostname(), 64 | ]); 65 | } 66 | 67 | /** 68 | * Disconnect from SMTP server. 69 | */ 70 | public function __destruct() 71 | { 72 | $this->disconnect(); 73 | } 74 | 75 | /** 76 | * Make Base configurations. 77 | * 78 | * @param array $config 79 | * 80 | * @return array 81 | */ 82 | #[ArrayShape([ 83 | 'host' => 'string', 84 | 'port' => 'int', 85 | 'tls' => 'bool', 86 | 'options' => 'array', 87 | 'username' => 'string|null', 88 | 'password' => 'string|null', 89 | 'charset' => 'string', 90 | 'crlf' => 'string', 91 | 'connection_timeout' => 'int', 92 | 'response_timeout' => 'int', 93 | 'hostname' => 'string', 94 | 'keep_alive' => 'bool', 95 | 'save_logs' => 'bool', 96 | ])] 97 | protected function makeConfig(#[SensitiveParameter] array $config) : array 98 | { 99 | return \array_replace_recursive([ 100 | 'host' => 'localhost', 101 | 'port' => 587, 102 | 'tls' => true, 103 | 'options' => [ 104 | 'ssl' => [ 105 | 'allow_self_signed' => false, 106 | 'verify_peer' => true, 107 | 'verify_peer_name' => true, 108 | ], 109 | ], 110 | 'username' => null, 111 | 'password' => null, 112 | 'charset' => 'utf-8', 113 | 'crlf' => "\r\n", 114 | 'connection_timeout' => 10, 115 | 'response_timeout' => 5, 116 | 'hostname' => \gethostname(), 117 | 'keep_alive' => false, 118 | 'save_logs' => false, 119 | ], $config); 120 | } 121 | 122 | /** 123 | * Get a config value. 124 | * 125 | * @param string $key The config key 126 | * 127 | * @return mixed The config value 128 | */ 129 | public function getConfig(string $key) : mixed 130 | { 131 | return $this->config[$key]; 132 | } 133 | 134 | /** 135 | * Get all configs. 136 | * 137 | * @return array 138 | */ 139 | #[ArrayShape([ 140 | 'host' => 'string', 141 | 'port' => 'int', 142 | 'tls' => 'bool', 143 | 'options' => 'array', 144 | 'username' => 'string|null', 145 | 'password' => 'string|null', 146 | 'charset' => 'string', 147 | 'crlf' => 'string', 148 | 'connection_timeout' => 'int', 149 | 'response_timeout' => 'int', 150 | 'hostname' => 'string', 151 | 'keep_alive' => 'bool', 152 | 'save_logs' => 'bool', 153 | ])] 154 | public function getConfigs() : array 155 | { 156 | return $this->config; 157 | } 158 | 159 | protected function setLastResponse(?string $lastResponse) : static 160 | { 161 | if ($lastResponse === null) { 162 | $this->lastResponse = null; 163 | return $this; 164 | } 165 | $parts = \explode(\PHP_EOL, $lastResponse); 166 | $this->lastResponse = $parts[\array_key_last($parts)]; 167 | return $this; 168 | } 169 | 170 | /** 171 | * Get the last response. 172 | * 173 | * @return string|null The last response or null if there is none 174 | */ 175 | public function getLastResponse() : ?string 176 | { 177 | return $this->lastResponse; 178 | } 179 | 180 | protected function connect() : bool 181 | { 182 | if ($this->socket && ($this->getConfig('keep_alive') === true)) { 183 | return $this->sendCommand('EHLO ' . $this->getConfig('hostname')) === 250; 184 | } 185 | $this->disconnect(); 186 | $this->socket = @\stream_socket_client( 187 | $this->getConfig('host') . ':' . $this->getConfig('port'), 188 | $errorCode, 189 | $errorMessage, 190 | (float) $this->getConfig('connection_timeout'), 191 | \STREAM_CLIENT_CONNECT, 192 | \stream_context_create($this->getConfig('options')) 193 | ); 194 | if ($this->socket === false) { 195 | $error = 'Socket connection error ' . $errorCode . ': ' . $errorMessage; 196 | $this->addLog('', $error); 197 | $this->setLastResponse($error); 198 | return false; 199 | } 200 | $this->addLog('', $this->getResponse()); 201 | $this->sendCommand('EHLO ' . $this->getConfig('hostname')); 202 | if ($this->getConfig('tls')) { 203 | $this->sendCommand('STARTTLS'); 204 | \stream_socket_enable_crypto($this->socket, true, \STREAM_CRYPTO_METHOD_TLS_CLIENT); 205 | $this->sendCommand('EHLO ' . $this->getConfig('hostname')); 206 | } 207 | return $this->authenticate(); 208 | } 209 | 210 | protected function disconnect() : bool 211 | { 212 | if (\is_resource($this->socket)) { 213 | $this->sendCommand('QUIT'); 214 | $closed = \fclose($this->socket); 215 | } 216 | $this->socket = false; 217 | return $closed ?? true; 218 | } 219 | 220 | /** 221 | * @see https://datatracker.ietf.org/doc/html/rfc2821#section-4.2.3 222 | * @see https://datatracker.ietf.org/doc/html/rfc4954#section-4.1 223 | * 224 | * @return bool 225 | */ 226 | protected function authenticate() : bool 227 | { 228 | if ($this->getConfig('username') === null) { 229 | $this->setLastResponse('Username is not set'); 230 | return false; 231 | } 232 | if ($this->getConfig('password') === null) { 233 | $this->setLastResponse('Password is not set'); 234 | return false; 235 | } 236 | $code = $this->sendCommand('AUTH LOGIN'); 237 | if ($code === 503) { // Already authenticated 238 | return true; 239 | } 240 | if ($code !== 334) { 241 | return false; 242 | } 243 | $code = $this->sendCommand(\base64_encode($this->getConfig('username'))); 244 | if ($code !== 334) { 245 | return false; 246 | } 247 | $code = $this->sendCommand(\base64_encode($this->getConfig('password'))); 248 | return $code === 235; 249 | } 250 | 251 | /** 252 | * Send an Email Message. 253 | * 254 | * @param Message $message The Message instance 255 | * 256 | * @return bool True if successful, otherwise false 257 | */ 258 | public function send(Message $message) : bool 259 | { 260 | if (isset($this->debugCollector)) { 261 | $start = \microtime(true); 262 | $code = $this->sendMessage($message); 263 | $end = \microtime(true); 264 | $this->debugCollector->addData([ 265 | 'start' => $start, 266 | 'end' => $end, 267 | 'code' => $code, 268 | 'last_response' => $this->getLastResponse(), 269 | 'from' => $message->getFromAddress() ?? $this->getConfig('username'), 270 | 'length' => \strlen((string) $message), 271 | 'recipients' => $message->getRecipients(), 272 | 'headers' => $message->getHeaders(), 273 | 'plain' => $message->getPlainMessage(), 274 | 'html' => $message->getHtmlMessage(), 275 | 'attachments' => $message->getAttachments(), 276 | 'inlineAttachments' => $message->getInlineAttachments(), 277 | ]); 278 | return $code === 250; 279 | } 280 | return $this->sendMessage($message) === 250; 281 | } 282 | 283 | protected function sendMessage(Message $message) : false | int 284 | { 285 | if (!$this->connect()) { 286 | return false; 287 | } 288 | $message->setMailer($this); 289 | $from = $message->getFromAddress() ?? $this->getConfig('username'); 290 | $this->sendCommand('MAIL FROM: <' . $from . '>'); 291 | foreach ($message->getRecipients() as $address) { 292 | $this->sendCommand('RCPT TO: <' . $address . '>'); 293 | } 294 | $this->sendCommand('DATA'); 295 | $code = $this->sendCommand( 296 | $message . $this->getConfig('crlf') . '.' 297 | ); 298 | if ($this->getConfig('keep_alive') !== true) { 299 | $this->disconnect(); 300 | } 301 | return $code; 302 | } 303 | 304 | /** 305 | * Get Mail Server response. 306 | * 307 | * @return string 308 | */ 309 | protected function getResponse() : string 310 | { 311 | $response = ''; 312 | // @phpstan-ignore-next-line 313 | \stream_set_timeout($this->socket, $this->getConfig('response_timeout')); 314 | // @phpstan-ignore-next-line 315 | while (($line = \fgets($this->socket, 512)) !== false) { 316 | $response .= \trim($line) . "\n"; 317 | if (isset($line[3]) && $line[3] === ' ') { 318 | break; 319 | } 320 | } 321 | return \trim($response); 322 | } 323 | 324 | /** 325 | * Send command to mail server. 326 | * 327 | * @param string $command 328 | * 329 | * @return int Response code 330 | */ 331 | protected function sendCommand(string $command) : int 332 | { 333 | // @phpstan-ignore-next-line 334 | \fwrite($this->socket, $command . $this->getConfig('crlf')); 335 | $response = $this->getResponse(); 336 | $this->addLog($command, $response); 337 | // The last command could be: "EHLO $host". 338 | // And the last response is an empty string. 339 | // So, we ignore empty responses... 340 | if ($response !== '') { 341 | $this->setLastResponse($response); 342 | } 343 | return $this->makeResponseCode($response); 344 | } 345 | 346 | /** 347 | * @see https://tools.ietf.org/html/rfc2821#section-4.2.3 348 | * @see https://en.wikipedia.org/wiki/List_of_SMTP_server_return_codes 349 | * 350 | * @param string $response 351 | * 352 | * @return int 353 | */ 354 | private function makeResponseCode(string $response) : int 355 | { 356 | return (int) \substr($response, 0, 3); 357 | } 358 | 359 | /** 360 | * Get an array of logs. 361 | * 362 | * Contains commands and responses from the Mailer server. 363 | * 364 | * @return array> 365 | */ 366 | public function getLogs() : array 367 | { 368 | return $this->logs; 369 | } 370 | 371 | /** 372 | * Reset logs. 373 | * 374 | * @return static 375 | */ 376 | public function resetLogs() : static 377 | { 378 | $this->logs = []; 379 | return $this; 380 | } 381 | 382 | /** 383 | * @param string $command 384 | * @param string $response 385 | * 386 | * @return static 387 | */ 388 | protected function addLog(string $command, string $response) : static 389 | { 390 | if (!$this->getConfig('save_logs')) { 391 | return $this; 392 | } 393 | $this->logs[] = [ 394 | 'command' => $command, 395 | 'responses' => \explode(\PHP_EOL, $response), 396 | ]; 397 | return $this; 398 | } 399 | 400 | /** 401 | * Set the debug collector. 402 | * 403 | * @param EmailCollector $collector The debug collector 404 | * 405 | * @return static 406 | */ 407 | public function setDebugCollector(EmailCollector $collector) : static 408 | { 409 | $collector->setMailer($this); 410 | $this->debugCollector = $collector; 411 | return $this; 412 | } 413 | 414 | /** 415 | * Create a new Message instance. 416 | * 417 | * @return Message 418 | */ 419 | public function createMessage() : Message 420 | { 421 | return (new Message())->setMailer($this); 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /src/Message.php: -------------------------------------------------------------------------------- 1 | 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 Framework\Email; 11 | 12 | use DateTime; 13 | use JetBrains\PhpStorm\Language; 14 | use LogicException; 15 | use Random\RandomException; 16 | 17 | /** 18 | * Class Message. 19 | * 20 | * @package email 21 | */ 22 | class Message implements \Stringable 23 | { 24 | /** 25 | * The Mailer instance. 26 | * 27 | * @var Mailer 28 | */ 29 | protected Mailer $mailer; 30 | /** 31 | * The message boundary. 32 | * 33 | * @var string 34 | */ 35 | protected string $boundary; 36 | /** 37 | * @var array 38 | */ 39 | protected array $headers = [ 40 | 'mime-version' => '1.0', 41 | ]; 42 | /** 43 | * A list of attachments with Content-Disposition equals `attachment`. 44 | * 45 | * @var array The filenames 46 | */ 47 | protected array $attachments = []; 48 | /** 49 | * An associative array of attachments with Content-Disposition equals `inline`. 50 | * 51 | * @var array The Content-ID's as keys and the filenames as values 52 | */ 53 | protected array $inlineAttachments = []; 54 | /** 55 | * The plain text message. 56 | * 57 | * @var string 58 | */ 59 | protected string $plainMessage; 60 | /** 61 | * The HTML message. 62 | * 63 | * @var string 64 | */ 65 | protected string $htmlMessage; 66 | /** 67 | * An associative array used in the `To` header. 68 | * 69 | * @var array The email addresses as keys and the optional 70 | * name as values 71 | */ 72 | protected array $to = []; 73 | /** 74 | * An associative array used in the `Cc` header. 75 | * 76 | * @var array The email addresses as keys and the optional 77 | * name as values 78 | */ 79 | protected array $cc = []; 80 | /** 81 | * An associative array used in the `Bcc` header. 82 | * 83 | * @var array The email addresses as keys and the optional 84 | * name as values 85 | */ 86 | protected array $bcc = []; 87 | /** 88 | * An associative array used in the `Reply-To` header. 89 | * 90 | * @var array The email addresses as keys and the optional 91 | * name as values 92 | */ 93 | protected array $replyTo = []; 94 | /** 95 | * The values used in the `From` header. 96 | * 97 | * @var array The email address as in the index 0 and the 98 | * optional name in the index 1 99 | */ 100 | protected array $from = []; 101 | /** 102 | * The message Date. 103 | * 104 | * @var string|null 105 | */ 106 | protected ?string $date = null; 107 | 108 | /** 109 | * Render the Message as string. 110 | * 111 | * @return string 112 | */ 113 | public function __toString() : string 114 | { 115 | return $this->renderData(); 116 | } 117 | 118 | /** 119 | * Set the Mailer instance. 120 | * 121 | * @param Mailer $mailer The Mailer instance 122 | * 123 | * @return static 124 | */ 125 | public function setMailer(Mailer $mailer) : static 126 | { 127 | $this->mailer = $mailer; 128 | return $this; 129 | } 130 | 131 | protected function getCrlf() : string 132 | { 133 | if (isset($this->mailer)) { 134 | return $this->mailer->getConfig('crlf'); 135 | } 136 | return "\r\n"; 137 | } 138 | 139 | protected function getCharset() : string 140 | { 141 | if (isset($this->mailer)) { 142 | return $this->mailer->getConfig('charset'); 143 | } 144 | return 'utf-8'; 145 | } 146 | 147 | /** 148 | * Set the boundary. 149 | * 150 | * @param string|null $boundary 151 | * 152 | * @throws RandomException 153 | * 154 | * @return static 155 | */ 156 | public function setBoundary(?string $boundary = null) : static 157 | { 158 | $this->boundary = $boundary ?? \bin2hex(\random_bytes(16)); 159 | return $this; 160 | } 161 | 162 | /** 163 | * Get the boundary. 164 | * 165 | * @throws RandomException 166 | * 167 | * @return string 168 | */ 169 | public function getBoundary() : string 170 | { 171 | if (!isset($this->boundary)) { 172 | $this->setBoundary(); 173 | } 174 | return $this->boundary; 175 | } 176 | 177 | /** 178 | * Remove a header. 179 | * 180 | * @param string $name The header name 181 | * 182 | * @return static 183 | */ 184 | public function removeHeader(string $name) : static 185 | { 186 | unset($this->headers[\strtolower($name)]); 187 | return $this; 188 | } 189 | 190 | /** 191 | * Set a header. 192 | * 193 | * @param string $name The header name 194 | * @param string $value The header value 195 | * 196 | * @return static 197 | */ 198 | public function setHeader(string $name, string $value) : static 199 | { 200 | $this->headers[\strtolower($name)] = $value; 201 | return $this; 202 | } 203 | 204 | /** 205 | * Get a header. 206 | * 207 | * @param string $name The header name 208 | * 209 | * @return string|null The header value or null if not set 210 | */ 211 | public function getHeader(string $name) : ?string 212 | { 213 | return $this->headers[\strtolower($name)] ?? null; 214 | } 215 | 216 | /** 217 | * Get all headers set. 218 | * 219 | * @return array The header names, in lowercase, as keys and 220 | * the values as values 221 | */ 222 | public function getHeaders() : array 223 | { 224 | return $this->headers; 225 | } 226 | 227 | /** 228 | * Get header lines. 229 | * 230 | * @return array The header lines 231 | */ 232 | public function getHeaderLines() : array 233 | { 234 | $lines = []; 235 | foreach ($this->getHeaders() as $name => $value) { 236 | $lines[] = Header::getName($name) . ': ' . $value; 237 | } 238 | return $lines; 239 | } 240 | 241 | protected function renderHeaders() : string 242 | { 243 | return \implode($this->getCrlf(), $this->getHeaderLines()); 244 | } 245 | 246 | protected function prepareHeaders() : void 247 | { 248 | if (!$this->getDate()) { 249 | $this->setDate(); 250 | } 251 | $multipart = $this->getInlineAttachments() ? 'related' : 'mixed'; 252 | $this->setHeader( 253 | Header::CONTENT_TYPE, 254 | 'multipart/' . $multipart . '; boundary="mixed-' . $this->getBoundary() . '"' 255 | ); 256 | } 257 | 258 | protected function renderData() : string 259 | { 260 | $boundary = $this->getBoundary(); 261 | $crlf = $this->getCrlf(); 262 | $this->prepareHeaders(); 263 | $data = $this->renderHeaders() . $crlf . $crlf; 264 | $data .= '--mixed-' . $boundary . $crlf; 265 | $data .= 'Content-Type: multipart/alternative; boundary="alt-' . $boundary . '"' 266 | . $crlf . $crlf; 267 | $data .= $this->renderPlainMessage(); 268 | $data .= $this->renderHtmlMessage(); 269 | $data .= '--alt-' . $boundary . '--' . $crlf . $crlf; 270 | $data .= $this->renderAttachments(); 271 | $data .= $this->renderInlineAttachments(); 272 | $data .= '--mixed-' . $boundary . '--'; 273 | return $data; 274 | } 275 | 276 | /** 277 | * Set the text/plain message. 278 | * 279 | * @param string $message The text/plain message 280 | * 281 | * @return static 282 | */ 283 | public function setPlainMessage(string $message) : static 284 | { 285 | $this->plainMessage = $message; 286 | return $this; 287 | } 288 | 289 | /** 290 | * Get the text/plain message. 291 | * 292 | * @return string|null The message or null if not set 293 | */ 294 | public function getPlainMessage() : ?string 295 | { 296 | return $this->plainMessage ?? null; 297 | } 298 | 299 | protected function renderPlainMessage() : ?string 300 | { 301 | $message = $this->getPlainMessage(); 302 | return $message !== null ? $this->renderMessage($message, 'text/plain') : null; 303 | } 304 | 305 | /** 306 | * Set the text/html message. 307 | * 308 | * @param string $message The text/html message 309 | * 310 | * @return static 311 | */ 312 | public function setHtmlMessage(#[Language('HTML')] string $message) : static 313 | { 314 | $this->htmlMessage = $message; 315 | return $this; 316 | } 317 | 318 | /** 319 | * Get the text/html message. 320 | * 321 | * @return string|null The text/html message or null if not set 322 | */ 323 | public function getHtmlMessage() : ?string 324 | { 325 | return $this->htmlMessage ?? null; 326 | } 327 | 328 | protected function renderHtmlMessage() : ?string 329 | { 330 | $message = $this->getHtmlMessage(); 331 | return $message !== null ? $this->renderMessage($message) : null; 332 | } 333 | 334 | protected function renderMessage( 335 | string $message, 336 | string $contentType = 'text/html' 337 | ) : string { 338 | $message = \base64_encode($message); 339 | $crlf = $this->getCrlf(); 340 | $part = '--alt-' . $this->getBoundary() . $crlf; 341 | $part .= 'Content-Type: ' . $contentType . '; charset=' 342 | . $this->getCharset() . $crlf; 343 | $part .= 'Content-Transfer-Encoding: base64' . $crlf . $crlf; 344 | $part .= \chunk_split($message) . $crlf; 345 | return $part; 346 | } 347 | 348 | /** 349 | * Get a lis of attachments. 350 | * 351 | * @return array Array of filenames 352 | */ 353 | public function getAttachments() : array 354 | { 355 | return $this->attachments; 356 | } 357 | 358 | /** 359 | * Add a filename to be attached. 360 | * 361 | * @param string $filename The filename 362 | * 363 | * @return static 364 | */ 365 | public function addAttachment(string $filename) : static 366 | { 367 | $this->attachments[] = $filename; 368 | return $this; 369 | } 370 | 371 | /** 372 | * Set a filename to be attached inline (image). 373 | * 374 | * @param string $filename The filename 375 | * @param string $cid The Content-ID 376 | * 377 | * @return static 378 | */ 379 | public function setInlineAttachment(string $filename, string $cid) : static 380 | { 381 | $this->inlineAttachments[$cid] = $filename; 382 | return $this; 383 | } 384 | 385 | /** 386 | * Get a lis of inline attachments. 387 | * 388 | * @return array Content-IDs as keys and filenames as values 389 | */ 390 | public function getInlineAttachments() : array 391 | { 392 | return $this->inlineAttachments; 393 | } 394 | 395 | protected function renderAttachments() : string 396 | { 397 | $part = ''; 398 | $crlf = $this->getCrlf(); 399 | foreach ($this->getAttachments() as $attachment) { 400 | if (!\is_file($attachment)) { 401 | throw new LogicException('Attachment file not found: ' . $attachment); 402 | } 403 | $filename = \pathinfo($attachment, \PATHINFO_BASENAME); 404 | $filename = \htmlspecialchars($filename, \ENT_QUOTES | \ENT_HTML5); 405 | $contents = \file_get_contents($attachment); 406 | $contents = \base64_encode($contents); // @phpstan-ignore-line 407 | $part .= '--mixed-' . $this->getBoundary() . $crlf; 408 | $part .= 'Content-Type: ' . $this->getContentType($attachment) 409 | . '; name="' . $filename . '"' . $crlf; 410 | $part .= 'Content-Disposition: attachment; filename="' . $filename . '"' . $crlf; 411 | $part .= 'Content-Transfer-Encoding: base64' . $crlf . $crlf; 412 | $part .= \chunk_split($contents) . $crlf; 413 | } 414 | return $part; 415 | } 416 | 417 | protected function getContentType(string $filename) : string 418 | { 419 | return \mime_content_type($filename) ?: 'application/octet-stream'; 420 | } 421 | 422 | protected function renderInlineAttachments() : string 423 | { 424 | $part = ''; 425 | $crlf = $this->getCrlf(); 426 | foreach ($this->getInlineAttachments() as $cid => $filename) { 427 | if (!\is_file($filename)) { 428 | throw new LogicException('Inline attachment file not found: ' . $filename); 429 | } 430 | $contents = \file_get_contents($filename); 431 | $contents = \base64_encode($contents); // @phpstan-ignore-line 432 | $part .= '--mixed-' . $this->getBoundary() . $crlf; 433 | $part .= 'Content-ID: ' . $cid . $crlf; 434 | $part .= 'Content-Type: ' . $this->getContentType($filename) . $crlf; 435 | $part .= 'Content-Disposition: inline' . $crlf; 436 | $part .= 'Content-Transfer-Encoding: base64' . $crlf . $crlf; 437 | $part .= \chunk_split($contents) . $crlf; 438 | } 439 | return $part; 440 | } 441 | 442 | /** 443 | * Set the Subject header. 444 | * 445 | * @param string $subject The header value 446 | * 447 | * @return static 448 | */ 449 | public function setSubject(string $subject) : static 450 | { 451 | $this->setHeader(Header::SUBJECT, $subject); 452 | return $this; 453 | } 454 | 455 | /** 456 | * Get the Subject header. 457 | * 458 | * @return string|null The header value or null if not set 459 | */ 460 | public function getSubject() : ?string 461 | { 462 | return $this->getHeader(Header::SUBJECT); 463 | } 464 | 465 | /** 466 | * Add address and name in the To header. 467 | * 468 | * @param string $address The email address 469 | * @param string|null $name The name or null to don't set 470 | * 471 | * @return static 472 | */ 473 | public function addTo(string $address, ?string $name = null) : static 474 | { 475 | $this->to[$address] = $name; 476 | $this->setHeader(Header::TO, static::formatAddressList($this->to)); 477 | return $this; 478 | } 479 | 480 | /** 481 | * Get items of the To header. 482 | * 483 | * @return array Emails as keys and names as values 484 | */ 485 | public function getTo() : array 486 | { 487 | return $this->to; 488 | } 489 | 490 | /** 491 | * Remove all items of the To header. 492 | * 493 | * @return static 494 | */ 495 | public function removeTo() : static 496 | { 497 | $this->to = []; 498 | return $this; 499 | } 500 | 501 | /** 502 | * Add address and name in the Cc header. 503 | * 504 | * @param string $address The email address 505 | * @param string|null $name The name or null to don't set 506 | * 507 | * @return static 508 | */ 509 | public function addCc(string $address, ?string $name = null) : static 510 | { 511 | $this->cc[$address] = $name; 512 | $this->setHeader(Header::CC, static::formatAddressList($this->cc)); 513 | return $this; 514 | } 515 | 516 | /** 517 | * Get items of the Cc header. 518 | * 519 | * @return array Emails as keys and names as values 520 | */ 521 | public function getCc() : array 522 | { 523 | return $this->cc; 524 | } 525 | 526 | /** 527 | * Remove all items of the Cc header. 528 | * 529 | * @return static 530 | */ 531 | public function removeCc() : static 532 | { 533 | $this->cc = []; 534 | return $this; 535 | } 536 | 537 | /** 538 | * @return array 539 | */ 540 | public function getRecipients() : array 541 | { 542 | $recipients = \array_replace($this->getTo(), $this->getCc()); 543 | return \array_keys($recipients); 544 | } 545 | 546 | /** 547 | * Add address and name in the Bcc header. 548 | * 549 | * @param string $address The email address 550 | * @param string|null $name The name or null to don't set 551 | * 552 | * @return static 553 | */ 554 | public function addBcc(string $address, ?string $name = null) : static 555 | { 556 | $this->bcc[$address] = $name; 557 | $this->setHeader(Header::BCC, static::formatAddressList($this->bcc)); 558 | return $this; 559 | } 560 | 561 | /** 562 | * Get items of the Bcc header. 563 | * 564 | * @return array Emails as keys and names as values 565 | */ 566 | public function getBcc() : array 567 | { 568 | return $this->bcc; 569 | } 570 | 571 | /** 572 | * Remove all items of the Bcc header. 573 | * 574 | * @return static 575 | */ 576 | public function removeBcc() : static 577 | { 578 | $this->bcc = []; 579 | return $this; 580 | } 581 | 582 | /** 583 | * Add address and name in the Reply-To header. 584 | * 585 | * @param string $address The email address 586 | * @param string|null $name The name or null to don't set 587 | * 588 | * @return static 589 | */ 590 | public function addReplyTo(string $address, ?string $name = null) : static 591 | { 592 | $this->replyTo[$address] = $name; 593 | $this->setHeader(Header::REPLY_TO, static::formatAddressList($this->replyTo)); 594 | return $this; 595 | } 596 | 597 | /** 598 | * Get items of the Reply-To header. 599 | * 600 | * @return array Emails as keys and names as values 601 | */ 602 | public function getReplyTo() : array 603 | { 604 | return $this->replyTo; 605 | } 606 | 607 | /** 608 | * Remove all items of the Reply-To header. 609 | * 610 | * @return static 611 | */ 612 | public function removeReplyTo() : static 613 | { 614 | $this->replyTo = []; 615 | return $this; 616 | } 617 | 618 | /** 619 | * Set the From header. 620 | * 621 | * @param string $address The email address 622 | * @param string|null $name The name or null to don't set 623 | * 624 | * @return static 625 | */ 626 | public function setFrom(string $address, ?string $name = null) : static 627 | { 628 | $this->from = [$address, $name]; 629 | $this->setHeader(Header::FROM, static::formatAddress($address, $name)); 630 | return $this; 631 | } 632 | 633 | /** 634 | * Get the From header items. 635 | * 636 | * @return array email address in key 0 and name in key 1 637 | */ 638 | public function getFrom() : array 639 | { 640 | return $this->from; 641 | } 642 | 643 | /** 644 | * Get the email address of the From header. 645 | * 646 | * @return string|null The email or null if not set 647 | */ 648 | public function getFromAddress() : ?string 649 | { 650 | return $this->from[0] ?? null; 651 | } 652 | 653 | /** 654 | * Get the name of the From header. 655 | * 656 | * @return string|null The name or null if not set 657 | */ 658 | public function getFromName() : ?string 659 | { 660 | return $this->from[1] ?? null; 661 | } 662 | 663 | /** 664 | * Remove all items of the From header. 665 | * 666 | * @return static 667 | */ 668 | public function removeFrom() : static 669 | { 670 | $this->from = []; 671 | return $this; 672 | } 673 | 674 | /** 675 | * Set the Date header. 676 | * 677 | * @param DateTime|null $datetime A custom DateTime or null to set the 678 | * current datetime 679 | * 680 | * @return static 681 | */ 682 | public function setDate(?DateTime $datetime = null) : static 683 | { 684 | $date = $datetime ? $datetime->format('r') : \date('r'); 685 | $this->setHeader(Header::DATE, $date); 686 | return $this; 687 | } 688 | 689 | /** 690 | * Get the Date header. 691 | * 692 | * @return string|null The header value or null if not set 693 | */ 694 | public function getDate() : ?string 695 | { 696 | return $this->getHeader(Header::DATE); 697 | } 698 | 699 | /** 700 | * Set the X-Priority header. 701 | * 702 | * @param XPriority $priority The {@see XPriority} case 703 | * 704 | * @return static 705 | */ 706 | public function setXPriority(XPriority $priority) : static 707 | { 708 | $this->setHeader(Header::X_PRIORITY, (string) $priority->value); 709 | return $this; 710 | } 711 | 712 | /** 713 | * Get the X-Priority header. 714 | * 715 | * @return XPriority|null The {@see XPriority} case or null 716 | */ 717 | public function getXPriority() : ?XPriority 718 | { 719 | $header = $this->getHeader(Header::X_PRIORITY); 720 | if ($header === null) { 721 | return null; 722 | } 723 | return XPriority::from((int) $header); 724 | } 725 | 726 | /** 727 | * Set the X-Mailer header. 728 | * 729 | * @param string|null $xMailer The X-Mailer header or null to set the default 730 | * 731 | * @return static 732 | */ 733 | public function setXMailer(?string $xMailer = null) : static 734 | { 735 | $xMailer ??= 'Aplus Mailer'; 736 | $this->setHeader(Header::X_MAILER, $xMailer); 737 | return $this; 738 | } 739 | 740 | /** 741 | * Get the X-Mailer header. 742 | * 743 | * @return string|null The X-Mailer header or null 744 | */ 745 | public function getXMailer() : ?string 746 | { 747 | return $this->getHeader(Header::X_MAILER); 748 | } 749 | 750 | protected static function formatAddress(string $address, ?string $name = null) : string 751 | { 752 | return $name !== null ? '"' . $name . '" <' . $address . '>' : $address; 753 | } 754 | 755 | /** 756 | * @param array $addresses 757 | * 758 | * @return string 759 | */ 760 | protected static function formatAddressList(array $addresses) : string 761 | { 762 | $data = []; 763 | foreach ($addresses as $address => $name) { 764 | $data[] = static::formatAddress($address, $name); 765 | } 766 | return \implode(', ', $data); 767 | } 768 | } 769 | --------------------------------------------------------------------------------