Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
26.43% |
231 / 874 |
|
45.83% |
22 / 48 |
CRAP | |
0.00% |
0 / 1 |
| |
26.43% |
231 / 874 |
|
45.83% |
22 / 48 |
35898.26 | |
0.00% |
0 / 1 |
|
| setFrom | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
| setHtml | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| getContentType | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| isHtml | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| addTo | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| addCC | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| addBCC | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| addReplyTo | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| parseAddresses | |
85.71% |
30 / 35 |
|
0.00% |
0 / 1 |
13.49 | |||
| preSend | |
12.50% |
4 / 32 |
|
0.00% |
0 / 1 |
126.22 | |||
| createHeader | |
0.00% |
0 / 41 |
|
0.00% |
0 / 1 |
342 | |||
| addrAppend | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| addrFormat | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| getMailMime | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
156 | |||
| punyencodeAddress | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
56 | |||
| generateId | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
| createBody | |
0.00% |
0 / 151 |
|
0.00% |
0 / 1 |
870 | |||
| getBoundary | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
30 | |||
| attachAll | |
0.00% |
0 / 53 |
|
0.00% |
0 / 1 |
240 | |||
| quotedString | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| encodeFile | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
| encodeString | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
72 | |||
| setMessageType | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 | |||
| hasInlineImage | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
| hasAttachment | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
| createAddressList | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| setWordWrap | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
56 | |||
| wrapText | |
0.00% |
0 / 56 |
|
0.00% |
0 / 1 |
462 | |||
| encodeHeader | |
18.60% |
8 / 43 |
|
0.00% |
0 / 1 |
172.85 | |||
| encodeQ | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
72 | |||
| base64EncodeWrapMB | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
6 | |||
| addAttachment | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
4 | |||
| getAttachments | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| addStringAttachment | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
2 | |||
| addEmbeddedImage | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
4 | |||
| addStringEmbeddedImage | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
3 | |||
| cidExists | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 | |||
| addCustomHeader | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
| getCustomHeaders | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| msgHTML | |
91.67% |
44 / 48 |
|
0.00% |
0 / 1 |
21.26 | |||
| normalizeBreaks | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| html2text | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
2.01 | |||
| sign | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
| dkimQP | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
7 | |||
| dkimSign | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
| dkimHeaderC | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
3.01 | |||
| dkimBodyC | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| dkimAdd | |
0.00% |
0 / 95 |
|
0.00% |
0 / 1 |
420 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Jingga |
| 4 | * |
| 5 | * PHP Version 8.1 |
| 6 | * |
| 7 | * @package phpOMS\Message\Mail |
| 8 | * @copyright Dennis Eichhorn |
| 9 | * @license OMS License 2.0 |
| 10 | * @version 1.0.0 |
| 11 | * @link https://jingga.app |
| 12 | * |
| 13 | * Extended based on: |
| 14 | * GLGPL 2.1 License |
| 15 | * (c) 2012 - 2015 Marcus Bointon, 2010 - 2012 Jim Jagielski, 2004 - 2009 Andy Prevost |
| 16 | * (c) PHPMailer |
| 17 | */ |
| 18 | declare(strict_types=1); |
| 19 | |
| 20 | namespace phpOMS\Message\Mail; |
| 21 | |
| 22 | use phpOMS\System\CharsetType; |
| 23 | use phpOMS\System\File\FileUtils; |
| 24 | use phpOMS\System\MimeType; |
| 25 | use phpOMS\System\SystemUtils; |
| 26 | use phpOMS\Utils\MbStringUtils; |
| 27 | use phpOMS\Validation\Network\Email as EmailValidator; |
| 28 | |
| 29 | /** |
| 30 | * Mail class. |
| 31 | * |
| 32 | * @package phpOMS\Message\Mail |
| 33 | * @license OMS License 2.0 |
| 34 | * @link https://jingga.app |
| 35 | * @since 1.0.0 |
| 36 | */ |
| 37 | class Email implements MessageInterface |
| 38 | { |
| 39 | /** |
| 40 | * Mailer name. |
| 41 | * |
| 42 | * @var string |
| 43 | * @since 1.0.0 |
| 44 | */ |
| 45 | public const XMAILER = 'phpOMS'; |
| 46 | |
| 47 | /** |
| 48 | * The maximum line length supported by mail(). |
| 49 | * |
| 50 | * @var int |
| 51 | * @since 1.0.0 |
| 52 | */ |
| 53 | public const MAIL_MAX_LINE_LENGTH = 63; |
| 54 | |
| 55 | /** |
| 56 | * The maximum line length allowed by RFC 2822 section 2.1.1. |
| 57 | * |
| 58 | * @var int |
| 59 | * @since 1.0.0 |
| 60 | */ |
| 61 | public const MAX_LINE_LENGTH = 998; |
| 62 | |
| 63 | /** |
| 64 | * The lower maximum line length allowed by RFC 2822 section 2.1.1. |
| 65 | * |
| 66 | * @var int |
| 67 | * @since 1.0.0 |
| 68 | */ |
| 69 | public const STD_LINE_LENGTH = 76; |
| 70 | |
| 71 | /** |
| 72 | * Folding White Space. |
| 73 | * |
| 74 | * @var string |
| 75 | * @since 1.0.0 |
| 76 | */ |
| 77 | public const FWS = ' '; |
| 78 | |
| 79 | /** |
| 80 | * SMTP RFC standard line ending |
| 81 | * |
| 82 | * @var string |
| 83 | * @since 1.0.0 |
| 84 | */ |
| 85 | protected static string $LE = "\r\n"; |
| 86 | |
| 87 | /** |
| 88 | * Message id. |
| 89 | * |
| 90 | * Format <id@domain>. If empty this is automatically generated. |
| 91 | * |
| 92 | * @var string |
| 93 | * @since 1.0.0 |
| 94 | */ |
| 95 | public string $messageId = ''; |
| 96 | |
| 97 | /** |
| 98 | * Unique ID used for message ID and boundaries. |
| 99 | * |
| 100 | * @var string |
| 101 | * @since 1.0.0 |
| 102 | */ |
| 103 | public string $uniqueid = ''; |
| 104 | |
| 105 | /** |
| 106 | * Hostname coming from the mail handler. |
| 107 | * |
| 108 | * @var string |
| 109 | * @since 1.0.0 |
| 110 | */ |
| 111 | public string $hostname = ''; |
| 112 | |
| 113 | /** |
| 114 | * Mailer for sending message |
| 115 | * |
| 116 | * @var string |
| 117 | * @since 1.0.0 |
| 118 | */ |
| 119 | public string $mailer = SubmitType::MAIL; |
| 120 | |
| 121 | /** |
| 122 | * Mail from. |
| 123 | * |
| 124 | * @var array |
| 125 | * @since 1.0.0 |
| 126 | */ |
| 127 | public array $from = []; |
| 128 | |
| 129 | /** |
| 130 | * Return path/bounce address |
| 131 | * |
| 132 | * @var string |
| 133 | * @since 1.0.0 |
| 134 | */ |
| 135 | public string $sender = ''; |
| 136 | |
| 137 | /** |
| 138 | * Confirm address. |
| 139 | * |
| 140 | * @var string |
| 141 | */ |
| 142 | public string $confirmationAddress = ''; |
| 143 | |
| 144 | /** |
| 145 | * Mail to. |
| 146 | * |
| 147 | * @var array |
| 148 | * @since 1.0.0 |
| 149 | */ |
| 150 | public array $to = []; |
| 151 | |
| 152 | /** |
| 153 | * Mail subject. |
| 154 | * |
| 155 | * @var string |
| 156 | * @since 1.0.0 |
| 157 | */ |
| 158 | public string $subject = ''; |
| 159 | |
| 160 | /** |
| 161 | * Mail cc. |
| 162 | * |
| 163 | * @var array |
| 164 | * @since 1.0.0 |
| 165 | */ |
| 166 | public array $cc = []; |
| 167 | |
| 168 | /** |
| 169 | * Mail bcc. |
| 170 | * |
| 171 | * @var array |
| 172 | * @since 1.0.0 |
| 173 | */ |
| 174 | public array $bcc = []; |
| 175 | |
| 176 | /** |
| 177 | * The array of reply-to names and addresses. |
| 178 | * |
| 179 | * @var array |
| 180 | * @since 1.0.0 |
| 181 | */ |
| 182 | public array $replyTo = []; |
| 183 | |
| 184 | /** |
| 185 | * Mail attachments. |
| 186 | * |
| 187 | * @var array |
| 188 | * @since 1.0.0 |
| 189 | */ |
| 190 | protected array $attachment = []; |
| 191 | |
| 192 | /** |
| 193 | * Mail body. |
| 194 | * |
| 195 | * @var string |
| 196 | * @since 1.0.0 |
| 197 | */ |
| 198 | public string $body = ''; |
| 199 | |
| 200 | /** |
| 201 | * Mail alt. |
| 202 | * |
| 203 | * @var string |
| 204 | * @since 1.0.0 |
| 205 | */ |
| 206 | public string $bodyAlt = ''; |
| 207 | |
| 208 | /** |
| 209 | * Ical body. |
| 210 | * |
| 211 | * @var string |
| 212 | * @since 1.0.0 |
| 213 | */ |
| 214 | public string $ical = ''; |
| 215 | |
| 216 | /** |
| 217 | * Mail mime. |
| 218 | * |
| 219 | * @var string |
| 220 | * @since 1.0.0 |
| 221 | */ |
| 222 | public string $bodyMime = ''; |
| 223 | |
| 224 | /** |
| 225 | * The array of MIME boundary strings. |
| 226 | * |
| 227 | * @var array |
| 228 | * @since 1.0.0 |
| 229 | */ |
| 230 | protected array $boundary = []; |
| 231 | |
| 232 | /** |
| 233 | * Mail header. |
| 234 | * |
| 235 | * @var string |
| 236 | * @since 1.0.0 |
| 237 | */ |
| 238 | protected string $header = ''; |
| 239 | |
| 240 | /** |
| 241 | * Mail header. |
| 242 | * |
| 243 | * @var string |
| 244 | * @since 1.0.0 |
| 245 | */ |
| 246 | public string $headerMime = ''; |
| 247 | |
| 248 | /** |
| 249 | * The array of custom headers. |
| 250 | * |
| 251 | * @var array |
| 252 | * @since 1.0.0 |
| 253 | */ |
| 254 | protected array $customHeader = []; |
| 255 | |
| 256 | /** |
| 257 | * Word wrap. |
| 258 | * |
| 259 | * @var int |
| 260 | * @since 1.0.0 |
| 261 | */ |
| 262 | protected int $wordWrap = 72; |
| 263 | |
| 264 | /** |
| 265 | * Encoding. |
| 266 | * |
| 267 | * @var string |
| 268 | * @since 1.0.0 |
| 269 | */ |
| 270 | protected string $encoding = EncodingType::E_8BIT; |
| 271 | |
| 272 | /** |
| 273 | * Mail content type. |
| 274 | * |
| 275 | * @var string |
| 276 | * @since 1.0.0 |
| 277 | */ |
| 278 | protected string $contentType = MimeType::M_TXT; |
| 279 | |
| 280 | /** |
| 281 | * Character set |
| 282 | * |
| 283 | * @var string |
| 284 | * @since 1.0.0 |
| 285 | */ |
| 286 | public string $charset = CharsetType::ISO_8859_1; |
| 287 | |
| 288 | /** |
| 289 | * Mail message type. |
| 290 | * |
| 291 | * @var string |
| 292 | * @since 1.0.0 |
| 293 | */ |
| 294 | protected string $messageType = ''; |
| 295 | |
| 296 | /** |
| 297 | * Mail from. |
| 298 | * |
| 299 | * @var null|\DateTime |
| 300 | * @since 1.0.0 |
| 301 | */ |
| 302 | public ?\DateTimeImmutable $messageDate = null; |
| 303 | |
| 304 | /** |
| 305 | * Priority |
| 306 | * |
| 307 | * @var int |
| 308 | * @since 1.0.0 |
| 309 | */ |
| 310 | public int $priority = 0; |
| 311 | |
| 312 | /** |
| 313 | * The S/MIME certificate file path. |
| 314 | * |
| 315 | * @var string |
| 316 | * @since 1.0.0 |
| 317 | */ |
| 318 | protected string $signCertFile = ''; |
| 319 | |
| 320 | /** |
| 321 | * The S/MIME key file path. |
| 322 | * |
| 323 | * @var string |
| 324 | * @since 1.0.0 |
| 325 | */ |
| 326 | protected string $signKeyFile = ''; |
| 327 | |
| 328 | /** |
| 329 | * The optional S/MIME extra certificates ("CA Chain") file path. |
| 330 | * |
| 331 | * @var string |
| 332 | * @since 1.0.0 |
| 333 | */ |
| 334 | protected string $signExtracertFiles = ''; |
| 335 | |
| 336 | /** |
| 337 | * The S/MIME password for the key. |
| 338 | * Used only if the key is encrypted. |
| 339 | * |
| 340 | * @var string |
| 341 | * @since 1.0.0 |
| 342 | */ |
| 343 | protected string $signKeyPass = ''; |
| 344 | |
| 345 | /** |
| 346 | * DKIM selector. |
| 347 | * |
| 348 | * @var string |
| 349 | * @since 1.0.0 |
| 350 | */ |
| 351 | public string $dkimSelector = ''; |
| 352 | |
| 353 | /** |
| 354 | * DKIM Identity. |
| 355 | * Usually the email address used as the source of the email. |
| 356 | * |
| 357 | * @var string |
| 358 | * @since 1.0.0 |
| 359 | */ |
| 360 | public string $dkimIdentity = ''; |
| 361 | |
| 362 | /** |
| 363 | * DKIM passphrase. |
| 364 | * Used if your key is encrypted. |
| 365 | * |
| 366 | * @var string |
| 367 | * @since 1.0.0 |
| 368 | */ |
| 369 | public string $dkimPass = ''; |
| 370 | |
| 371 | /** |
| 372 | * DKIM signing domain name. |
| 373 | * |
| 374 | * @var string |
| 375 | * @since 1.0.0 |
| 376 | */ |
| 377 | public string $dkimDomain = ''; |
| 378 | |
| 379 | /** |
| 380 | * DKIM Copy header field values for diagnostic use. |
| 381 | * |
| 382 | * @var bool |
| 383 | * @since 1.0.0 |
| 384 | */ |
| 385 | public bool $dkimCopyHeader = true; |
| 386 | |
| 387 | /** |
| 388 | * DKIM Extra signing headers. |
| 389 | * |
| 390 | * @example ['List-Unsubscribe', 'List-Help'] |
| 391 | * |
| 392 | * @var array |
| 393 | * @since 1.0.0 |
| 394 | */ |
| 395 | public array $dkimHeaders = []; |
| 396 | |
| 397 | /** |
| 398 | * DKIM private key file path. |
| 399 | * |
| 400 | * @var string |
| 401 | * @since 1.0.0 |
| 402 | */ |
| 403 | public string $dkimPrivatePath = ''; |
| 404 | |
| 405 | /** |
| 406 | * DKIM private key string. |
| 407 | * |
| 408 | * If set, takes precedence over `$dkimPrivatePath`. |
| 409 | * |
| 410 | * @var string |
| 411 | * @since 1.0.0 |
| 412 | */ |
| 413 | public string $dkimPrivateKey = ''; |
| 414 | |
| 415 | /** |
| 416 | * Set the From and FromName. |
| 417 | * |
| 418 | * @param string $address Email address |
| 419 | * @param string $name Name |
| 420 | * |
| 421 | * @return bool |
| 422 | * |
| 423 | * @since 1.0.0 |
| 424 | */ |
| 425 | public function setFrom(string $address, string $name = '') : bool |
| 426 | { |
| 427 | $address = \trim($address); |
| 428 | $name = \trim(\preg_replace('/[\r\n]+/', '', $name)); |
| 429 | |
| 430 | if (!EmailValidator::isValid($address)) { |
| 431 | return false; |
| 432 | } |
| 433 | |
| 434 | $this->from = [$address, $name]; |
| 435 | |
| 436 | if (empty($this->sender)) { |
| 437 | $this->sender = $address; |
| 438 | } |
| 439 | |
| 440 | return true; |
| 441 | } |
| 442 | |
| 443 | /** |
| 444 | * Sets message type to html or plain. |
| 445 | * |
| 446 | * @param bool $isHtml Html mode |
| 447 | * |
| 448 | * @return void |
| 449 | * |
| 450 | * @since 1.0.0 |
| 451 | */ |
| 452 | public function setHtml(bool $isHtml = true) : void |
| 453 | { |
| 454 | $this->contentType = $isHtml ? MimeType::M_HTML : MimeType::M_TEXT; |
| 455 | } |
| 456 | |
| 457 | public function getContentType() : string |
| 458 | { |
| 459 | return $this->contentType; |
| 460 | } |
| 461 | |
| 462 | public function isHtml() : bool |
| 463 | { |
| 464 | return $this->contentType === MimeType::M_HTML; |
| 465 | } |
| 466 | |
| 467 | /** |
| 468 | * Add a "To" address. |
| 469 | * |
| 470 | * @param string $address Email address |
| 471 | * @param string $name Name |
| 472 | * |
| 473 | * @return bool |
| 474 | * |
| 475 | * @since 1.0.0 |
| 476 | */ |
| 477 | public function addTo(string $address, string $name = '') : bool |
| 478 | { |
| 479 | if (!EmailValidator::isValid($address)) { |
| 480 | return false; |
| 481 | } |
| 482 | |
| 483 | $this->to[$address] = [$address, $name]; |
| 484 | |
| 485 | return true; |
| 486 | } |
| 487 | |
| 488 | /** |
| 489 | * Add a "CC" address. |
| 490 | * |
| 491 | * @param string $address Email address |
| 492 | * @param string $name Name |
| 493 | * |
| 494 | * @return bool |
| 495 | * |
| 496 | * @since 1.0.0 |
| 497 | */ |
| 498 | public function addCC(string $address, string $name = '') : bool |
| 499 | { |
| 500 | if (!EmailValidator::isValid($address)) { |
| 501 | return false; |
| 502 | } |
| 503 | |
| 504 | $this->cc[$address] = [$address, $name]; |
| 505 | |
| 506 | return true; |
| 507 | } |
| 508 | |
| 509 | /** |
| 510 | * Add a "BCC" address. |
| 511 | * |
| 512 | * @param string $address Email address |
| 513 | * @param string $name Name |
| 514 | * |
| 515 | * @return bool |
| 516 | * |
| 517 | * @since 1.0.0 |
| 518 | */ |
| 519 | public function addBCC(string $address, string $name = '') : bool |
| 520 | { |
| 521 | if (!EmailValidator::isValid($address)) { |
| 522 | return false; |
| 523 | } |
| 524 | |
| 525 | $this->bcc[$address] = [$address, $name]; |
| 526 | |
| 527 | return true; |
| 528 | } |
| 529 | |
| 530 | /** |
| 531 | * Add a "Reply-To" address. |
| 532 | * |
| 533 | * @param string $address Email address |
| 534 | * @param string $name Name |
| 535 | * |
| 536 | * @return bool |
| 537 | * |
| 538 | * @since 1.0.0 |
| 539 | */ |
| 540 | public function addReplyTo(string $address, string $name = '') : bool |
| 541 | { |
| 542 | if (!EmailValidator::isValid($address)) { |
| 543 | return false; |
| 544 | } |
| 545 | |
| 546 | $this->replyTo[$address] = [$address, $name]; |
| 547 | |
| 548 | return true; |
| 549 | } |
| 550 | |
| 551 | /** |
| 552 | * Parse and validate a string containing one or more RFC822-style comma-separated email addresses |
| 553 | * of the form "display name <address>" into an array of name/address pairs. |
| 554 | * |
| 555 | * @param string $addrstr Address line |
| 556 | * @param bool $useImap Use imap for parsing |
| 557 | * @param string $charset Charset for email |
| 558 | * |
| 559 | * @return array |
| 560 | * |
| 561 | * @since 1.0.0 |
| 562 | */ |
| 563 | public static function parseAddresses(string $addrstr, bool $useimap = true, string $charset = CharsetType::ISO_8859_1) : array |
| 564 | { |
| 565 | $addresses = []; |
| 566 | if ($useimap && \function_exists('imap_rfc822_parse_adrlist')) { |
| 567 | $list = \imap_rfc822_parse_adrlist($addrstr, ''); |
| 568 | foreach ($list as $address) { |
| 569 | if (($address->host !== '.SYNTAX-ERROR.') |
| 570 | && EmailValidator::isValid($address->mailbox . '@' . $address->host) |
| 571 | ) { |
| 572 | if (\property_exists($address, 'personal') |
| 573 | && \preg_match('/^=\?.*\?=$/s', $address->personal) |
| 574 | ) { |
| 575 | $origCharset = \mb_internal_encoding(); |
| 576 | \mb_internal_encoding($charset); |
| 577 | $address->personal = \str_replace('_', '=20', $address->personal); |
| 578 | $address->personal = \mb_decode_mimeheader($address->personal); |
| 579 | \mb_internal_encoding($origCharset); |
| 580 | } |
| 581 | |
| 582 | $addresses[] = [ |
| 583 | 'name' => (\property_exists($address, 'personal') ? $address->personal : ''), |
| 584 | 'address' => $address->mailbox . '@' . $address->host, |
| 585 | ]; |
| 586 | } |
| 587 | } |
| 588 | |
| 589 | return $addresses; |
| 590 | } |
| 591 | |
| 592 | $list = \explode(',', $addrstr); |
| 593 | foreach ($list as $address) { |
| 594 | $address = \trim($address); |
| 595 | if (\strpos($address, '<') === false) { |
| 596 | if (EmailValidator::isValid($address)) { |
| 597 | $addresses[] = [ |
| 598 | 'name' => '', |
| 599 | 'address' => $address, |
| 600 | ]; |
| 601 | } |
| 602 | } else { |
| 603 | $addr = \explode('<', $address); |
| 604 | $email = \trim(\str_replace('>', '', $addr[1])); |
| 605 | |
| 606 | if (EmailValidator::isValid($email)) { |
| 607 | $addresses[] = [ |
| 608 | 'name' => \trim($addr[0], '\'" '), |
| 609 | 'address' => $email, |
| 610 | ]; |
| 611 | } |
| 612 | } |
| 613 | } |
| 614 | |
| 615 | return $addresses; |
| 616 | } |
| 617 | |
| 618 | /** |
| 619 | * Pre-send preparations |
| 620 | * |
| 621 | * @param string $mailer Mailer tool |
| 622 | * |
| 623 | * @return bool |
| 624 | * |
| 625 | * @since 1.0.0 |
| 626 | */ |
| 627 | public function preSend(string $mailer) : bool |
| 628 | { |
| 629 | $this->header = ''; |
| 630 | $this->mailer = $mailer; |
| 631 | |
| 632 | if (empty($this->to) && empty($this->cc) && empty($this->bcc)) { |
| 633 | return false; |
| 634 | } |
| 635 | |
| 636 | if (!empty($this->bodyAlt)) { |
| 637 | $this->contentType = MimeType::M_ALT; |
| 638 | } |
| 639 | |
| 640 | $this->setMessageType(); |
| 641 | |
| 642 | $this->headerMime = ''; |
| 643 | $this->bodyMime = $this->createBody(); |
| 644 | |
| 645 | $tempheaders = $this->headerMime; |
| 646 | $this->headerMime = $this->createHeader(); |
| 647 | $this->headerMime .= $tempheaders; |
| 648 | |
| 649 | if ($this->mailer === SubmitType::MAIL) { |
| 650 | $this->header .= empty($this->to) |
| 651 | ? 'Subject: undisclosed-recipients:;' . self::$LE |
| 652 | : $this->createAddressList('To', $this->to); |
| 653 | |
| 654 | $this->header .= 'Subject: ' . $this->encodeHeader(\trim(\str_replace(["\r", "\n"], '', $this->subject))) . self::$LE; |
| 655 | } |
| 656 | |
| 657 | // Sign with DKIM if enabled |
| 658 | if (!empty($this->dkimDomain) |
| 659 | && !empty($this->dkimSelector) |
| 660 | && (!empty($this->dkimPrivateKey) |
| 661 | || (!empty($this->dkimPrivatePath) |
| 662 | && FileUtils::isPermittedPath($this->dkimPrivatePath) |
| 663 | && \is_file($this->dkimPrivatePath) |
| 664 | ) |
| 665 | ) |
| 666 | ) { |
| 667 | $headerDkim = $this->dkimAdd( |
| 668 | $this->headerMime . $this->header, |
| 669 | $this->encodeHeader(\trim(\str_replace(["\r", "\n"], '', $this->subject))), |
| 670 | $this->bodyMime |
| 671 | ); |
| 672 | |
| 673 | $this->headerMime = \rtrim($this->headerMime, " \r\n\t") . self::$LE . |
| 674 | self::normalizeBreaks($headerDkim, self::$LE) . self::$LE; |
| 675 | } |
| 676 | |
| 677 | return true; |
| 678 | } |
| 679 | |
| 680 | /** |
| 681 | * Assemble message headers. |
| 682 | * |
| 683 | * @return string The assembled headers |
| 684 | * |
| 685 | * @since 1.0.0 |
| 686 | */ |
| 687 | private function createHeader() : string |
| 688 | { |
| 689 | $result = 'Date : ' . ($this->messageDate === null |
| 690 | ? (new \DateTime('now'))->format('D, j M Y H:i:s O') |
| 691 | : $this->messageDate->format('D, j M Y H:i:s O')) |
| 692 | . self::$LE; |
| 693 | |
| 694 | if ($this->mailer !== SubmitType::MAIL) { |
| 695 | $result .= empty($this->to) |
| 696 | ? 'To: undisclosed-recipients:;' . self::$LE |
| 697 | : $this->addrAppend('To', $this->to); |
| 698 | } |
| 699 | |
| 700 | $result .= $this->addrAppend('From', [$this->from]); |
| 701 | |
| 702 | // sendmail and mail() extract Cc from the header before sending |
| 703 | if (!empty($this->cc)) { |
| 704 | $result .= $this->addrAppend('Cc', $this->cc); |
| 705 | } |
| 706 | |
| 707 | // sendmail and mail() extract Bcc from the header before sending |
| 708 | if (($this->mailer === SubmitType::MAIL |
| 709 | || $this->mailer === SubmitType::SENDMAIL |
| 710 | || $this->mailer === SubmitType::QMAIL) |
| 711 | && !empty($this->bcc) |
| 712 | ) { |
| 713 | $result .= $this->addrAppend('Bcc', $this->bcc); |
| 714 | } |
| 715 | |
| 716 | if (!empty($this->replyTo)) { |
| 717 | $result .= $this->addrAppend('Reply-To', $this->replyTo); |
| 718 | } |
| 719 | |
| 720 | // mail() sets the subject itself |
| 721 | if ($this->mailer !== SubmitType::MAIL) { |
| 722 | $result .= 'Subject: ' . $this->encodeHeader(\trim(\str_replace(["\r", "\n"], '', $this->subject))) . self::$LE; |
| 723 | } |
| 724 | |
| 725 | $this->hostname = empty($this->hostname) ? SystemUtils::getHostname() : $this->hostname; |
| 726 | |
| 727 | // Only allow a custom message Id if it conforms to RFC 5322 section 3.6.4 |
| 728 | // https://tools.ietf.org/html/rfc5322#section-3.6.4 |
| 729 | $this->messageId = $this->messageId !== '' |
| 730 | && \preg_match('/^<((([a-z\d!#$%&\'*+\/=?^_`{|}~-]+(\.[a-z\d!#$%&\'*+\/=?^_`{|}~-]+)*)' . |
| 731 | '|("(([\x01-\x08\x0B\x0C\x0E-\x1F\x7F]|[\x21\x23-\x5B\x5D-\x7E])' . |
| 732 | '|(\\[\x01-\x09\x0B\x0C\x0E-\x7F]))*"))@(([a-z\d!#$%&\'*+\/=?^_`{|}~-]+' . |
| 733 | '(\.[a-z\d!#$%&\'*+\/=?^_`{|}~-]+)*)|(\[(([\x01-\x08\x0B\x0C\x0E-\x1F\x7F]' . |
| 734 | '|[\x21-\x5A\x5E-\x7E])|(\\[\x01-\x09\x0B\x0C\x0E-\x7F]))*\])))>$/Di', $this->messageId) |
| 735 | ? $this->messageId |
| 736 | : \sprintf('<%s@%s>', $this->uniqueid, $this->hostname); |
| 737 | |
| 738 | $result .= 'Message-ID: ' . $this->messageId . self::$LE; |
| 739 | |
| 740 | if ($this->priority > 0) { |
| 741 | $result .= 'X-Priority: ' . $this->priority . self::$LE; |
| 742 | } |
| 743 | |
| 744 | $result .= 'X-Mailer: ' . self::XMAILER . self::$LE; |
| 745 | |
| 746 | if ($this->confirmationAddress !== '') { |
| 747 | $result .= 'Disposition-Notification-To: ' . '<' . $this->confirmationAddress . '>' . self::$LE; |
| 748 | } |
| 749 | |
| 750 | // Add custom headers |
| 751 | foreach ($this->customHeader as $header) { |
| 752 | $result .= \trim($header[0]) . ': ' . $this->encodeHeader(\trim($header[1])) . self::$LE; |
| 753 | } |
| 754 | |
| 755 | if (empty($this->signKeyFile)) { |
| 756 | $result .= 'MIME-Version: 1.0' . self::$LE; |
| 757 | $result .= $this->getMailMime(); |
| 758 | } |
| 759 | |
| 760 | return $result; |
| 761 | } |
| 762 | |
| 763 | /** |
| 764 | * Create recipient headers. |
| 765 | * |
| 766 | * @param string $type Address type |
| 767 | * @param array $addr Address 0 = address, 1 = name ['joe@example.com', 'Joe User'] |
| 768 | * |
| 769 | * @return string |
| 770 | * |
| 771 | * @since 1.0.0 |
| 772 | */ |
| 773 | private function addrAppend(string $type, array $addr) : string |
| 774 | { |
| 775 | $addresses = []; |
| 776 | foreach ($addr as $address) { |
| 777 | $addresses[] = $this->addrFormat($address); |
| 778 | } |
| 779 | |
| 780 | return $type . ': ' . \implode(', ', $addresses) . self::$LE; |
| 781 | } |
| 782 | |
| 783 | /** |
| 784 | * Format an address for use in a message header. |
| 785 | * |
| 786 | * @param array $addr Address 0 = address, 1 = name ['joe@example.com', 'Joe User'] |
| 787 | * |
| 788 | * @return string |
| 789 | * |
| 790 | * @since 1.0.0 |
| 791 | */ |
| 792 | public function addrFormat(array $addr) : string |
| 793 | { |
| 794 | if (empty($addr[1])) { |
| 795 | return \trim(\str_replace(["\r", "\n"], '', $addr[0])); |
| 796 | } |
| 797 | |
| 798 | return $this->encodeHeader(\trim(\str_replace(["\r", "\n"], '', $addr[1])), 'phrase') . |
| 799 | ' <' . \trim(\str_replace(["\r", "\n"], '', $addr[0])) . '>'; |
| 800 | } |
| 801 | |
| 802 | /** |
| 803 | * Get the message MIME type headers. |
| 804 | * |
| 805 | * @return string |
| 806 | * |
| 807 | * @since 1.0.0 |
| 808 | */ |
| 809 | private function getMailMime() : string |
| 810 | { |
| 811 | $result = ''; |
| 812 | $isMultipart = true; |
| 813 | |
| 814 | switch ($this->messageType) { |
| 815 | case 'inline': |
| 816 | $result .= 'Content-Type: ' . MimeType::M_RELATED . ';' . self::$LE; |
| 817 | $result .= ' boundary="' . $this->boundary[1] . '"' . self::$LE; |
| 818 | break; |
| 819 | case 'attach': |
| 820 | case 'inline_attach': |
| 821 | case 'alt_attach': |
| 822 | case 'alt_inline_attach': |
| 823 | $result .= 'Content-Type: ' . MimeType::M_MIXED . ';' . self::$LE; |
| 824 | $result .= ' boundary="' . $this->boundary[1] . '"' . self::$LE; |
| 825 | break; |
| 826 | case 'alt': |
| 827 | case 'alt_inline': |
| 828 | $result .= 'Content-Type: ' . MimeType::M_ALT . ';' . self::$LE; |
| 829 | $result .= ' boundary="' . $this->boundary[1] . '"' . self::$LE; |
| 830 | break; |
| 831 | default: |
| 832 | // Catches case 'plain': and case '': |
| 833 | $result .= 'Content-Type: ' . $this->contentType . '; charset=' . $this->charset . self::$LE; |
| 834 | $isMultipart = false; |
| 835 | break; |
| 836 | } |
| 837 | |
| 838 | // RFC1341 part 5 says 7bit is assumed if not specified |
| 839 | if ($this->encoding === EncodingType::E_7BIT) { |
| 840 | return $result; |
| 841 | } |
| 842 | |
| 843 | // RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE |
| 844 | if ($isMultipart) { |
| 845 | if ($this->encoding === EncodingType::E_8BIT) { |
| 846 | $result .= 'Content-Transfer-Encoding: ' . EncodingType::E_8BIT . self::$LE; |
| 847 | } |
| 848 | |
| 849 | // quoted-printable and base64 are 7bit compatible |
| 850 | } else { |
| 851 | $result .= 'Content-Transfer-Encoding: ' . $this->encoding . self::$LE; |
| 852 | } |
| 853 | |
| 854 | return $result; |
| 855 | } |
| 856 | |
| 857 | /** |
| 858 | * Converts IDN in given email address to its ASCII form |
| 859 | * |
| 860 | * @param string $charset Charset |
| 861 | * @param string $address Email address |
| 862 | * |
| 863 | * @return string The encoded address in ASCII form |
| 864 | * |
| 865 | * @since 1.0.0 |
| 866 | */ |
| 867 | private function punyencodeAddress(string $charset, string $address) : string |
| 868 | { |
| 869 | if (empty($charset) || !EmailValidator::isValid($address)) { |
| 870 | return $address; |
| 871 | } |
| 872 | |
| 873 | $pos = \strrpos($address, '@'); |
| 874 | $domain = \substr($address, ++$pos); |
| 875 | |
| 876 | if (!((bool) \preg_match('/[\x80-\xFF]/', $domain)) || !\mb_check_encoding($domain, $charset)) { |
| 877 | return $address; |
| 878 | } |
| 879 | |
| 880 | $domain = \mb_convert_encoding($domain, 'UTF-8', $charset); |
| 881 | |
| 882 | $errorcode = 0; |
| 883 | if (\defined('INTL_IDNA_VARIANT_UTS46')) { |
| 884 | $punycode = \idn_to_ascii( |
| 885 | $domain, |
| 886 | \IDNA_DEFAULT | \IDNA_USE_STD3_RULES | \IDNA_CHECK_BIDI | \IDNA_CHECK_CONTEXTJ | \IDNA_NONTRANSITIONAL_TO_ASCII, |
| 887 | \INTL_IDNA_VARIANT_UTS46); |
| 888 | } else { |
| 889 | $punycode = \idn_to_ascii($domain, $errorcode); |
| 890 | } |
| 891 | |
| 892 | if ($punycode !== false) { |
| 893 | return \substr($address, 0, $pos) . $punycode; |
| 894 | } |
| 895 | |
| 896 | return $address; |
| 897 | } |
| 898 | |
| 899 | /** |
| 900 | * Create a unique ID to use for boundaries. |
| 901 | * |
| 902 | * @return string |
| 903 | * |
| 904 | * @since 1.0.0 |
| 905 | */ |
| 906 | protected function generateId() : string |
| 907 | { |
| 908 | $len = 32; //32 bytes = 256 bits |
| 909 | $bytes = ''; |
| 910 | $bytes = \random_bytes($len); |
| 911 | |
| 912 | if ($bytes === '') { |
| 913 | $bytes = \hash('sha256', \uniqid((string) \mt_rand(), true), true); // @codeCoverageIgnore |
| 914 | } |
| 915 | |
| 916 | return \str_replace(['=', '+', '/'], '', \base64_encode(\hash('sha256', $bytes, true))); |
| 917 | } |
| 918 | |
| 919 | /** |
| 920 | * Assemble the message body. |
| 921 | * |
| 922 | * @return string Empty on failure |
| 923 | * |
| 924 | * @since 1.0.0 |
| 925 | */ |
| 926 | public function createBody() : string |
| 927 | { |
| 928 | $body = ''; |
| 929 | $this->uniqueid = $this->generateId(); |
| 930 | |
| 931 | $this->boundary[1] = 'b1=_' . $this->uniqueid; |
| 932 | $this->boundary[2] = 'b2=_' . $this->uniqueid; |
| 933 | $this->boundary[3] = 'b3=_' . $this->uniqueid; |
| 934 | |
| 935 | if (!empty($this->signKeyFile)) { |
| 936 | $body .= $this->getMailMime() . self::$LE; |
| 937 | } |
| 938 | |
| 939 | $this->setWordWrap(); |
| 940 | |
| 941 | $bodyEncoding = $this->encoding; |
| 942 | $bodyCharSet = $this->charset; |
| 943 | |
| 944 | // Can we do a 7-bit downgrade? |
| 945 | if ($bodyEncoding === EncodingType::E_8BIT && !((bool) \preg_match('/[\x80-\xFF]/', $this->body))) { |
| 946 | $bodyEncoding = EncodingType::E_7BIT; |
| 947 | |
| 948 | //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit |
| 949 | $bodyCharSet = CharsetType::ASCII; |
| 950 | } |
| 951 | |
| 952 | // If lines are too long, and we're not already using an encoding that will shorten them, |
| 953 | // change to quoted-printable transfer encoding for the body part only |
| 954 | if ($this->encoding !== EncodingType::E_BASE64 |
| 955 | && ((bool) \preg_match('/^(.{' . (self::MAX_LINE_LENGTH + \strlen(self::$LE)) . ',})/m', $this->body)) |
| 956 | ) { |
| 957 | $bodyEncoding = EncodingType::E_QUOTED; |
| 958 | } |
| 959 | |
| 960 | $altBodyEncoding = $this->encoding; |
| 961 | $altBodyCharSet = $this->charset; |
| 962 | |
| 963 | //Can we do a 7-bit downgrade? |
| 964 | if ($altBodyEncoding === EncodingType::E_8BIT && !((bool) \preg_match('/[\x80-\xFF]/', $this->bodyAlt))) { |
| 965 | $altBodyEncoding = EncodingType::E_7BIT; |
| 966 | |
| 967 | //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit |
| 968 | $altBodyCharSet = CharsetType::ASCII; |
| 969 | } |
| 970 | |
| 971 | //If lines are too long, and we're not already using an encoding that will shorten them, |
| 972 | //change to quoted-printable transfer encoding for the alt body part only |
| 973 | if ($altBodyEncoding !== EncodingType::E_BASE64 |
| 974 | && ((bool) \preg_match('/^(.{' . (self::MAX_LINE_LENGTH + \strlen(self::$LE)) . ',})/m', $this->bodyAlt)) |
| 975 | ) { |
| 976 | $altBodyEncoding = EncodingType::E_QUOTED; |
| 977 | } |
| 978 | |
| 979 | //Use this as a preamble in all multipart message types |
| 980 | $mimePre = ''; |
| 981 | switch ($this->messageType) { |
| 982 | case 'inline': |
| 983 | $body .= $mimePre; |
| 984 | $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding); |
| 985 | $body .= $this->encodeString($this->body, $bodyEncoding) . self::$LE; |
| 986 | $body .= $this->attachAll('inline', $this->boundary[1]); |
| 987 | break; |
| 988 | case 'attach': |
| 989 | $body .= $mimePre; |
| 990 | $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding); |
| 991 | $body .= $this->encodeString($this->body, $bodyEncoding) . self::$LE; |
| 992 | $body .= $this->attachAll('attachment', $this->boundary[1]); |
| 993 | break; |
| 994 | case 'inline_attach': |
| 995 | $body .= $mimePre; |
| 996 | $body .= '--' . $this->boundary[1] . self::$LE; |
| 997 | $body .= 'Content-Type: ' . MimeType::M_RELATED . ';' . self::$LE; |
| 998 | $body .= ' boundary="' . $this->boundary[2] . '";' . self::$LE; |
| 999 | $body .= ' type="' . MimeType::M_HTML . '"' . self::$LE . self::$LE; |
| 1000 | $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, '', $bodyEncoding); |
| 1001 | $body .= $this->encodeString($this->body, $bodyEncoding) . self::$LE; |
| 1002 | $body .= $this->attachAll('inline', $this->boundary[2]) . self::$LE; |
| 1003 | $body .= $this->attachAll('attachment', $this->boundary[1]); |
| 1004 | break; |
| 1005 | case 'alt': |
| 1006 | $body .= $mimePre; |
| 1007 | $body .= $this->getBoundary($this->boundary[1], $altBodyCharSet, MimeType::M_TEXT, $altBodyEncoding); |
| 1008 | $body .= $this->encodeString($this->bodyAlt, $altBodyEncoding) . self::$LE; |
| 1009 | $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, MimeType::M_HTML, $bodyEncoding); |
| 1010 | $body .= $this->encodeString($this->body, $bodyEncoding) . self::$LE; |
| 1011 | |
| 1012 | if (!empty($this->ical)) { |
| 1013 | $method = ICALMethodType::REQUEST; |
| 1014 | $methods = ICALMethodType::getConstants(); |
| 1015 | |
| 1016 | foreach ($methods as $imethod) { |
| 1017 | if (\stripos($this->ical, 'METHOD:' . $imethod) !== false) { |
| 1018 | $method = $imethod; |
| 1019 | break; |
| 1020 | } |
| 1021 | } |
| 1022 | |
| 1023 | $body .= $this->getBoundary($this->boundary[1], '', MimeType::M_ICS . '; method=' . $method, ''); |
| 1024 | $body .= $this->encodeString($this->ical, $this->encoding) . self::$LE; |
| 1025 | } |
| 1026 | |
| 1027 | $body .= self::$LE . '--' . $this->boundary[1] . '--' . self::$LE; |
| 1028 | break; |
| 1029 | case 'alt_inline': |
| 1030 | $body .= $mimePre; |
| 1031 | $body .= $this->getBoundary($this->boundary[1], $altBodyCharSet, MimeType::M_TEXT, $altBodyEncoding); |
| 1032 | $body .= $this->encodeString($this->bodyAlt, $altBodyEncoding) . self::$LE; |
| 1033 | $body .= '--' . $this->boundary[1] . self::$LE; |
| 1034 | $body .= 'Content-Type: ' . MimeType::M_RELATED . ';' . self::$LE; |
| 1035 | $body .= ' boundary="' . $this->boundary[2] . '";' . self::$LE; |
| 1036 | $body .= ' type="' . MimeType::M_HTML . '"' . self::$LE . self::$LE; |
| 1037 | $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, MimeType::M_HTML, $bodyEncoding); |
| 1038 | $body .= $this->encodeString($this->body, $bodyEncoding) . self::$LE; |
| 1039 | $body .= $this->attachAll('inline', $this->boundary[2]) . self::$LE; |
| 1040 | $body .= self::$LE . '--' . $this->boundary[1] . '--' . self::$LE; |
| 1041 | break; |
| 1042 | case 'alt_attach': |
| 1043 | $body .= $mimePre; |
| 1044 | $body .= '--' . $this->boundary[1] . self::$LE; |
| 1045 | $body .= 'Content-Type: ' . MimeType::M_ALT . ';' . self::$LE; |
| 1046 | $body .= ' boundary="' . $this->boundary[2] . '"' . self::$LE . self::$LE; |
| 1047 | $body .= $this->getBoundary($this->boundary[2], $altBodyCharSet, MimeType::M_TEXT, $altBodyEncoding); |
| 1048 | $body .= $this->encodeString($this->bodyAlt, $altBodyEncoding) . self::$LE; |
| 1049 | $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, MimeType::M_HTML, $bodyEncoding); |
| 1050 | $body .= $this->encodeString($this->body, $bodyEncoding) . self::$LE; |
| 1051 | |
| 1052 | if (!empty($this->ical)) { |
| 1053 | $method = ICALMethodType::REQUEST; |
| 1054 | $methods = ICALMethodType::getConstants(); |
| 1055 | |
| 1056 | foreach ($methods as $imethod) { |
| 1057 | if (\stripos($this->ical, 'METHOD:' . $imethod) !== false) { |
| 1058 | $method = $imethod; |
| 1059 | break; |
| 1060 | } |
| 1061 | } |
| 1062 | |
| 1063 | $body .= $this->getBoundary($this->boundary[2], '', MimeType::M_ICS . '; method=' . $method, ''); |
| 1064 | $body .= $this->encodeString($this->ical, $this->encoding); |
| 1065 | } |
| 1066 | |
| 1067 | $body .= self::$LE . '--' . $this->boundary[2] . '--' . self::$LE . self::$LE; |
| 1068 | $body .= $this->attachAll('attachment', $this->boundary[1]); |
| 1069 | break; |
| 1070 | case 'alt_inline_attach': |
| 1071 | $body .= $mimePre; |
| 1072 | $body .= '--' . $this->boundary[1] . self::$LE; |
| 1073 | $body .= 'Content-Type: ' . MimeType::M_ALT . ';' . self::$LE; |
| 1074 | $body .= ' boundary="' . $this->boundary[2] . '"' . self::$LE; |
| 1075 | $body .= $this->getBoundary($this->boundary[2], $altBodyCharSet, MimeType::M_TEXT, $altBodyEncoding); |
| 1076 | $body .= $this->encodeString($this->bodyAlt, $altBodyEncoding) . self::$LE; |
| 1077 | $body .= '--' . $this->boundary[2] . self::$LE; |
| 1078 | $body .= 'Content-Type: ' . MimeType::M_RELATED . ';' . self::$LE; |
| 1079 | $body .= ' boundary="' . $this->boundary[3] . '";' . self::$LE; |
| 1080 | $body .= ' type="' . MimeType::M_HTML . '"' . self::$LE . self::$LE; |
| 1081 | $body .= $this->getBoundary($this->boundary[3], $bodyCharSet, MimeType::M_HTML, $bodyEncoding); |
| 1082 | $body .= $this->encodeString($this->body, $bodyEncoding) . self::$LE; |
| 1083 | $body .= $this->attachAll('inline', $this->boundary[3]) . self::$LE; |
| 1084 | $body .= self::$LE . '--' . $this->boundary[2] . '--' . self::$LE . self::$LE; |
| 1085 | $body .= $this->attachAll('attachment', $this->boundary[1]); |
| 1086 | break; |
| 1087 | default: |
| 1088 | // Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types |
| 1089 | $this->encoding = $bodyEncoding; |
| 1090 | $body .= $this->encodeString($this->body, $this->encoding); |
| 1091 | break; |
| 1092 | } |
| 1093 | |
| 1094 | if (!empty($this->signKeyFile)) { |
| 1095 | if (!\defined('PKCS7_TEXT')) { |
| 1096 | return ''; |
| 1097 | } |
| 1098 | |
| 1099 | $file = \tempnam($tmpDir = \sys_get_temp_dir(), 'srcsign'); |
| 1100 | $signed = \tempnam($tmpDir, 'mailsign'); |
| 1101 | \file_put_contents($file, $body); |
| 1102 | |
| 1103 | try { |
| 1104 | // Workaround for PHP bug https://bugs.php.net/bug.php?id=69197 |
| 1105 | $sign = empty($this->signExtracertFiles) |
| 1106 | ? \openssl_pkcs7_sign(\realpath($file), $signed, |
| 1107 | 'file://' . \realpath($this->signCertFile), |
| 1108 | ['file://' . \realpath($this->signKeyFile), $this->signKeyPass], |
| 1109 | [], |
| 1110 | ) |
| 1111 | : \openssl_pkcs7_sign(\realpath($file), $signed, |
| 1112 | 'file://' . \realpath($this->signCertFile), |
| 1113 | ['file://' . \realpath($this->signKeyFile), $this->signKeyPass], |
| 1114 | [], |
| 1115 | \PKCS7_DETACHED, |
| 1116 | $this->signExtracertFiles |
| 1117 | ); |
| 1118 | } catch (\Throwable $_) { |
| 1119 | $sign = false; |
| 1120 | } |
| 1121 | |
| 1122 | \unlink($file); |
| 1123 | if ($sign === false) { |
| 1124 | \unlink($signed); |
| 1125 | return ''; |
| 1126 | } |
| 1127 | |
| 1128 | $body = \file_get_contents($signed); |
| 1129 | \unlink($signed); |
| 1130 | |
| 1131 | //The message returned by openssl contains both headers and body, so need to split them up |
| 1132 | $parts = \explode("\n\n", $body, 2); |
| 1133 | $this->headerMime .= $parts[0] . self::$LE . self::$LE; |
| 1134 | $body = $parts[1]; |
| 1135 | } |
| 1136 | |
| 1137 | return $body; |
| 1138 | } |
| 1139 | |
| 1140 | /** |
| 1141 | * Return the start of a message boundary. |
| 1142 | * |
| 1143 | * @param string $boundary Boundary |
| 1144 | * @param string $charset Charset |
| 1145 | * @param string $contentType Content type |
| 1146 | * @param string $encoding Concoding |
| 1147 | * |
| 1148 | * @return string |
| 1149 | * |
| 1150 | * @since 1.0.0 |
| 1151 | */ |
| 1152 | protected function getBoundary(string $boundary, string $charset, string $contentType, string $encoding) : string |
| 1153 | { |
| 1154 | $result = ''; |
| 1155 | if ($charset === '') { |
| 1156 | $charset = $this->charset; |
| 1157 | } |
| 1158 | |
| 1159 | if ($contentType === '') { |
| 1160 | $contentType = $this->contentType; |
| 1161 | } |
| 1162 | |
| 1163 | if ($encoding === '') { |
| 1164 | $encoding = $this->encoding; |
| 1165 | } |
| 1166 | |
| 1167 | $result .= '--' . $boundary . self::$LE; |
| 1168 | $result .= \sprintf('Content-Type: %s; charset=%s', $contentType, $charset); |
| 1169 | $result .= self::$LE; |
| 1170 | |
| 1171 | // RFC1341 part 5 says 7bit is assumed if not specified |
| 1172 | if ($encoding !== EncodingType::E_7BIT) { |
| 1173 | $result .= 'Content-Transfer-Encoding: ' . $encoding . self::$LE; |
| 1174 | } |
| 1175 | |
| 1176 | return $result . self::$LE; |
| 1177 | } |
| 1178 | |
| 1179 | /** |
| 1180 | * Attach all file, string, and binary attachments to the message. |
| 1181 | * |
| 1182 | * @param string $dispositionType Disposition type |
| 1183 | * @param string $boundary Boundary string |
| 1184 | * |
| 1185 | * @return string |
| 1186 | * |
| 1187 | * @since 1.0.0 |
| 1188 | */ |
| 1189 | protected function attachAll(string $dispositionType, string $boundary) : string |
| 1190 | { |
| 1191 | $mime = []; |
| 1192 | $cidUniq = []; |
| 1193 | $incl = []; |
| 1194 | |
| 1195 | $attachments = $this->getAttachments(); |
| 1196 | foreach ($attachments as $attachment) { |
| 1197 | if ($attachment[6] !== $dispositionType) { |
| 1198 | continue; |
| 1199 | } |
| 1200 | |
| 1201 | $bString = $attachment[5]; |
| 1202 | $string = $bString ? $attachment[0] : ''; |
| 1203 | $path = $bString ? '' : $attachment[0]; |
| 1204 | |
| 1205 | $inclHash = \hash('sha256', \serialize($attachment)); |
| 1206 | if (\in_array($inclHash, $incl, true)) { |
| 1207 | continue; |
| 1208 | } |
| 1209 | |
| 1210 | $incl[] = $inclHash; |
| 1211 | $name = $attachment[2]; |
| 1212 | $encoding = $attachment[3]; |
| 1213 | $type = $attachment[4]; |
| 1214 | $disposition = $attachment[6]; |
| 1215 | $cid = $attachment[7]; |
| 1216 | |
| 1217 | if ($disposition === 'inline' && isset($cidUniq[$cid])) { |
| 1218 | continue; |
| 1219 | } |
| 1220 | |
| 1221 | $cidUniq[$cid] = true; |
| 1222 | $mime[] = \sprintf('--%s%s', $boundary, self::$LE); |
| 1223 | |
| 1224 | //Only include a filename property if we have one |
| 1225 | $mime[] = empty($name) |
| 1226 | ? \sprintf('Content-Type: %s%s', |
| 1227 | $type, |
| 1228 | self::$LE |
| 1229 | ) |
| 1230 | : \sprintf('Content-Type: %s; name=%s%s', |
| 1231 | $type, |
| 1232 | self::quotedString($this->encodeHeader(\trim(\str_replace(["\r", "\n"], '', $name)))), |
| 1233 | self::$LE |
| 1234 | ); |
| 1235 | |
| 1236 | // RFC1341 part 5 says 7bit is assumed if not specified |
| 1237 | if ($encoding !== EncodingType::E_7BIT) { |
| 1238 | $mime[] = \sprintf('Content-Transfer-Encoding: %s%s', $encoding, self::$LE); |
| 1239 | } |
| 1240 | |
| 1241 | //Only set Content-IDs on inline attachments |
| 1242 | if ((string) $cid !== '' && $disposition === 'inline') { |
| 1243 | $mime[] = 'Content-ID: <' . $this->encodeHeader(\trim(\str_replace(["\r", "\n"], '', $cid))) . '>' . self::$LE; |
| 1244 | } |
| 1245 | |
| 1246 | // Allow for bypassing the Content-Disposition header |
| 1247 | if (!empty($disposition)) { |
| 1248 | $encodedName = $this->encodeHeader(\trim(\str_replace(["\r", "\n"], '', $name))); |
| 1249 | $mime[] = empty($encodedName) |
| 1250 | ? \sprintf('Content-Disposition: %s%s', $disposition, self::$LE . self::$LE) |
| 1251 | : \sprintf('Content-Disposition: %s; filename=%s%s', |
| 1252 | $disposition, |
| 1253 | self::quotedString($encodedName), |
| 1254 | self::$LE . self::$LE |
| 1255 | ); |
| 1256 | } else { |
| 1257 | $mime[] = self::$LE; |
| 1258 | } |
| 1259 | |
| 1260 | // Encode as string attachment |
| 1261 | $mime[] = $bString |
| 1262 | ? $this->encodeString($string, $encoding) |
| 1263 | : $this->encodeFile($path, $encoding); |
| 1264 | |
| 1265 | $mime[] = self::$LE; |
| 1266 | } |
| 1267 | |
| 1268 | $mime[] = \sprintf('--%s--%s', $boundary, self::$LE); |
| 1269 | |
| 1270 | return \implode('', $mime); |
| 1271 | } |
| 1272 | |
| 1273 | /** |
| 1274 | * If a string contains any "special" characters, double-quote the name, |
| 1275 | * and escape any double quotes with a backslash. |
| 1276 | * |
| 1277 | * @param string $str String to quote |
| 1278 | * |
| 1279 | * @return string |
| 1280 | * |
| 1281 | * @since 1.0.0 |
| 1282 | */ |
| 1283 | private static function quotedString(string $str) : string |
| 1284 | { |
| 1285 | if (\preg_match('/[ ()<>@,;:"\/\[\]?=]/', $str)) { |
| 1286 | return '"' . \str_replace('"', '\\"', $str) . '"'; |
| 1287 | } |
| 1288 | |
| 1289 | return $str; |
| 1290 | } |
| 1291 | |
| 1292 | /** |
| 1293 | * Encode a file attachment in requested format. |
| 1294 | * |
| 1295 | * @param string $path Path |
| 1296 | * @param string $encoding Encoding |
| 1297 | * |
| 1298 | * @return string |
| 1299 | * |
| 1300 | * @since 1.0.0 |
| 1301 | */ |
| 1302 | private function encodeFile(string $path, string $encoding = EncodingType::E_BASE64) : string |
| 1303 | { |
| 1304 | if (!FileUtils::isAccessible($path)) { |
| 1305 | return ''; |
| 1306 | } |
| 1307 | |
| 1308 | $fileBuffer = \file_get_contents($path); |
| 1309 | |
| 1310 | return $fileBuffer === false ? '' : $this->encodeString($fileBuffer, $encoding); |
| 1311 | } |
| 1312 | |
| 1313 | /** |
| 1314 | * Encode a string in requested format. |
| 1315 | * |
| 1316 | * @param string $str The text to encode |
| 1317 | * @param string $encoding Encoding |
| 1318 | * |
| 1319 | * @return string |
| 1320 | * |
| 1321 | * @since 1.0.0 |
| 1322 | */ |
| 1323 | private function encodeString(string $str, string $encoding = EncodingType::E_BASE64) : string |
| 1324 | { |
| 1325 | $encoded = ''; |
| 1326 | switch (\strtolower($encoding)) { |
| 1327 | case EncodingType::E_BASE64: |
| 1328 | $encoded = \chunk_split(\base64_encode($str), self::STD_LINE_LENGTH, self::$LE); |
| 1329 | break; |
| 1330 | case EncodingType::E_7BIT: |
| 1331 | case EncodingType::E_8BIT: |
| 1332 | $encoded = self::normalizeBreaks($str, self::$LE); |
| 1333 | if (\substr($encoded, -(\strlen(self::$LE))) !== self::$LE) { |
| 1334 | $encoded .= self::$LE; |
| 1335 | } |
| 1336 | |
| 1337 | break; |
| 1338 | case EncodingType::E_BINARY: |
| 1339 | $encoded = $str; |
| 1340 | break; |
| 1341 | case EncodingType::E_QUOTED: |
| 1342 | $encoded = self::normalizeBreaks(\quoted_printable_encode($str), self::$LE); |
| 1343 | break; |
| 1344 | default: |
| 1345 | return ''; |
| 1346 | } |
| 1347 | |
| 1348 | return $encoded; |
| 1349 | } |
| 1350 | |
| 1351 | /** |
| 1352 | * Set message type based on content |
| 1353 | * |
| 1354 | * @return void |
| 1355 | * |
| 1356 | * @since 1.0.0 |
| 1357 | */ |
| 1358 | protected function setMessageType() : void |
| 1359 | { |
| 1360 | $type = []; |
| 1361 | if (!empty($this->bodyAlt)) { |
| 1362 | $type[] = 'alt'; |
| 1363 | } |
| 1364 | |
| 1365 | if ($this->hasInlineImage()) { |
| 1366 | $type[] = 'inline'; |
| 1367 | } |
| 1368 | |
| 1369 | if ($this->hasAttachment()) { |
| 1370 | $type[] = 'attach'; |
| 1371 | } |
| 1372 | |
| 1373 | $this->messageType = \implode('_', $type); |
| 1374 | if ($this->messageType === '') { |
| 1375 | $this->messageType = 'plain'; |
| 1376 | } |
| 1377 | } |
| 1378 | |
| 1379 | /** |
| 1380 | * Mail has inline image |
| 1381 | * |
| 1382 | * @return bool |
| 1383 | * |
| 1384 | * @since 1.0.0 |
| 1385 | */ |
| 1386 | public function hasInlineImage() : bool |
| 1387 | { |
| 1388 | foreach ($this->attachment as $attachment) { |
| 1389 | if ($attachment[6] === 'inline') { |
| 1390 | return true; |
| 1391 | } |
| 1392 | } |
| 1393 | |
| 1394 | return false; |
| 1395 | } |
| 1396 | |
| 1397 | /** |
| 1398 | * Mail has attachment |
| 1399 | * |
| 1400 | * @return bool |
| 1401 | * |
| 1402 | * @since 1.0.0 |
| 1403 | */ |
| 1404 | public function hasAttachment() : bool |
| 1405 | { |
| 1406 | foreach ($this->attachment as $attachment) { |
| 1407 | if ($attachment[6] === 'attachment') { |
| 1408 | return true; |
| 1409 | } |
| 1410 | } |
| 1411 | |
| 1412 | return false; |
| 1413 | } |
| 1414 | |
| 1415 | /** |
| 1416 | * Create address list |
| 1417 | * |
| 1418 | * @param string $type Address type |
| 1419 | * @param array $addr Addresses |
| 1420 | * |
| 1421 | * @return string |
| 1422 | * |
| 1423 | * @since 1.0.0 |
| 1424 | */ |
| 1425 | public function createAddressList(string $type, array $addr) : string |
| 1426 | { |
| 1427 | $addresses = []; |
| 1428 | foreach ($addr as $address) { |
| 1429 | $addresses[] = $this->addrFormat($address); |
| 1430 | } |
| 1431 | |
| 1432 | return $type . ': ' . \implode(', ', $addresses) . static::$LE; |
| 1433 | } |
| 1434 | |
| 1435 | /** |
| 1436 | * Apply word wrapping |
| 1437 | * |
| 1438 | * @return void |
| 1439 | * |
| 1440 | * @return 1.0.0 |
| 1441 | */ |
| 1442 | public function setWordWrap() : void |
| 1443 | { |
| 1444 | if ($this->wordWrap < 1) { |
| 1445 | return; |
| 1446 | } |
| 1447 | |
| 1448 | switch ($this->messageType) { |
| 1449 | case 'alt': |
| 1450 | case 'alt_inline': |
| 1451 | case 'alt_attach': |
| 1452 | case 'alt_inline_attach': |
| 1453 | $this->bodyAlt = $this->wrapText($this->bodyAlt, $this->wordWrap); |
| 1454 | break; |
| 1455 | default: |
| 1456 | $this->body = $this->wrapText($this->body, $this->wordWrap); |
| 1457 | break; |
| 1458 | } |
| 1459 | } |
| 1460 | |
| 1461 | /** |
| 1462 | * Word-wrap message. |
| 1463 | * Original written by philippe. |
| 1464 | * |
| 1465 | * @param string $message The message to wrap |
| 1466 | * @param int $length The line length to wrap to |
| 1467 | * @param bool $qpMode Use Quoted-Printable mode |
| 1468 | * |
| 1469 | * @return string |
| 1470 | * |
| 1471 | * @since 1.0.0 |
| 1472 | */ |
| 1473 | private function wrapText(string $message, int $length, bool $qpMode = false) : string |
| 1474 | { |
| 1475 | $softBreak = $qpMode ? \sprintf(' =%s', self::$LE) : self::$LE; |
| 1476 | |
| 1477 | // Don't split multibyte characters |
| 1478 | $isUtf8 = \strtolower($this->charset) === CharsetType::UTF_8; |
| 1479 | $leLen = \strlen(self::$LE); |
| 1480 | $crlfLen = \strlen(self::$LE); |
| 1481 | |
| 1482 | $message = self::normalizeBreaks($message, self::$LE); |
| 1483 | |
| 1484 | //Remove a trailing line break |
| 1485 | if (\substr($message, -$leLen) === self::$LE) { |
| 1486 | $message = \substr($message, 0, -$leLen); |
| 1487 | } |
| 1488 | |
| 1489 | //Split message into lines |
| 1490 | $lines = \explode(self::$LE, $message); |
| 1491 | |
| 1492 | $message = ''; |
| 1493 | foreach ($lines as $line) { |
| 1494 | $words = \explode(' ', $line); |
| 1495 | $buf = ''; |
| 1496 | $firstword = true; |
| 1497 | |
| 1498 | foreach ($words as $word) { |
| 1499 | if ($qpMode && \strlen($word) > $length) { |
| 1500 | $spaceLeft = $length - \strlen($buf) - $crlfLen; |
| 1501 | |
| 1502 | if (!$firstword) { |
| 1503 | if ($spaceLeft > 20) { |
| 1504 | $len = $spaceLeft; |
| 1505 | if ($isUtf8) { |
| 1506 | $len = MbStringUtils::utf8CharBoundary($word, $len); |
| 1507 | } elseif (\substr($word, $len - 1, 1) === '=') { |
| 1508 | --$len; |
| 1509 | } elseif (\substr($word, $len - 2, 1) === '=') { |
| 1510 | $len -= 2; |
| 1511 | } |
| 1512 | |
| 1513 | $part = \substr($word, 0, $len); |
| 1514 | $word = \substr($word, $len); |
| 1515 | $buf .= ' ' . $part; |
| 1516 | $message .= $buf . \sprintf('=%s', self::$LE); |
| 1517 | } else { |
| 1518 | $message .= $buf . $softBreak; |
| 1519 | } |
| 1520 | |
| 1521 | $buf = ''; |
| 1522 | } |
| 1523 | |
| 1524 | while ($word !== '') { |
| 1525 | if ($length <= 0) { |
| 1526 | break; |
| 1527 | } |
| 1528 | |
| 1529 | $len = $length; |
| 1530 | if ($isUtf8) { |
| 1531 | $len = MbStringUtils::utf8CharBoundary($word, $len); |
| 1532 | } elseif (\substr($word, $len - 1, 1) === '=') { |
| 1533 | --$len; |
| 1534 | } elseif (\substr($word, $len - 2, 1) === '=') { |
| 1535 | $len -= 2; |
| 1536 | } |
| 1537 | |
| 1538 | $part = \substr($word, 0, $len); |
| 1539 | $word = (string) \substr($word, $len); |
| 1540 | |
| 1541 | if ($word !== '') { |
| 1542 | $message .= $part . \sprintf('=%s', self::$LE); |
| 1543 | } else { |
| 1544 | $buf = $part; |
| 1545 | } |
| 1546 | } |
| 1547 | } else { |
| 1548 | $bufO = $buf; |
| 1549 | if (!$firstword) { |
| 1550 | $buf .= ' '; |
| 1551 | } |
| 1552 | |
| 1553 | $buf .= $word; |
| 1554 | if ($bufO !== '' && \strlen($buf) > $length) { |
| 1555 | $message .= $bufO . $softBreak; |
| 1556 | $buf = $word; |
| 1557 | } |
| 1558 | } |
| 1559 | |
| 1560 | $firstword = false; |
| 1561 | } |
| 1562 | |
| 1563 | $message .= $buf . self::$LE; |
| 1564 | } |
| 1565 | |
| 1566 | return $message; |
| 1567 | } |
| 1568 | |
| 1569 | /** |
| 1570 | * Encode a header value (not including its label) optimally. |
| 1571 | * Picks shortest of Q, B, or none. Result includes folding if needed. |
| 1572 | * |
| 1573 | * @param string $str Header value |
| 1574 | * @param string $position Context |
| 1575 | * |
| 1576 | * @return string |
| 1577 | * |
| 1578 | * @since 1.0.0 |
| 1579 | */ |
| 1580 | public function encodeHeader(string $str, string $position = 'text') : string |
| 1581 | { |
| 1582 | $matchcount = 0; |
| 1583 | switch (\strtolower($position)) { |
| 1584 | case 'phrase': |
| 1585 | if (!\preg_match('/[\200-\377]/', $str)) { |
| 1586 | $encoded = \addcslashes($str, "\0..\37\177\\\""); |
| 1587 | |
| 1588 | return $str === $encoded && !\preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str) |
| 1589 | ? $encoded |
| 1590 | : '"' . $encoded . '"'; |
| 1591 | } |
| 1592 | |
| 1593 | $matchcount = \preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches); |
| 1594 | break; |
| 1595 | /* @noinspection PhpMissingBreakStatementInspection */ |
| 1596 | case 'comment': |
| 1597 | $matchcount = \preg_match_all('/[()"]/', $str, $matches); |
| 1598 | case 'text': |
| 1599 | default: |
| 1600 | $matchcount += \preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches); |
| 1601 | break; |
| 1602 | } |
| 1603 | |
| 1604 | $charset = ((bool) \preg_match('/[\x80-\xFF]/', $str)) ? $this->charset : CharsetType::ASCII; |
| 1605 | |
| 1606 | // Q/B encoding adds 8 chars and the charset ("` =?<charset>?[QB]?<content>?=`"). |
| 1607 | $overhead = 8 + \strlen($charset); |
| 1608 | $maxLen = $this->mailer === SubmitType::MAIL |
| 1609 | ? self::MAIL_MAX_LINE_LENGTH - $overhead |
| 1610 | : self::MAX_LINE_LENGTH - $overhead; |
| 1611 | |
| 1612 | // Select the encoding that produces the shortest output and/or prevents corruption. |
| 1613 | if ($matchcount > \strlen($str) / 3) { |
| 1614 | // More than 1/3 of the content needs encoding, use B-encode. |
| 1615 | $encoding = 'B'; |
| 1616 | } elseif ($matchcount > 0) { |
| 1617 | // Less than 1/3 of the content needs encoding, use Q-encode. |
| 1618 | $encoding = 'Q'; |
| 1619 | } elseif (\strlen($str) > $maxLen) { |
| 1620 | // No encoding needed, but value exceeds max line length, use Q-encode to prevent corruption. |
| 1621 | $encoding = 'Q'; |
| 1622 | } else { |
| 1623 | // No reformatting needed |
| 1624 | $encoding = ''; |
| 1625 | } |
| 1626 | |
| 1627 | switch ($encoding) { |
| 1628 | case 'B': |
| 1629 | if (\strlen($str) > \mb_strlen($str, $this->charset)) { |
| 1630 | $encoded = $this->base64EncodeWrapMB($str, "\n"); |
| 1631 | } else { |
| 1632 | $encoded = \base64_encode($str); |
| 1633 | $maxLen -= $maxLen % 4; |
| 1634 | $encoded = \trim(\chunk_split($encoded, $maxLen, "\n")); |
| 1635 | } |
| 1636 | $encoded = \preg_replace('/^(.*)$/m', ' =?' . $charset . '?' . $encoding . '?\\1?=', $encoded); |
| 1637 | break; |
| 1638 | case 'Q': |
| 1639 | $encoded = $this->encodeQ($str, $position); |
| 1640 | $encoded = $this->wrapText($encoded, $maxLen, true); |
| 1641 | $encoded = \str_replace('=' . self::$LE, "\n", \trim($encoded)); |
| 1642 | $encoded = \preg_replace('/^(.*)$/m', ' =?' . $charset . '?' . $encoding . '?\\1?=', $encoded); |
| 1643 | break; |
| 1644 | default: |
| 1645 | return $str; |
| 1646 | } |
| 1647 | |
| 1648 | return \trim(self::normalizeBreaks($encoded, self::$LE)); |
| 1649 | } |
| 1650 | |
| 1651 | /** |
| 1652 | * Encode a string using Q encoding. |
| 1653 | * |
| 1654 | * @param string $str Text to encode |
| 1655 | * @param string $position Where the text is going to be used, see the RFC for what that means |
| 1656 | * |
| 1657 | * @return string |
| 1658 | * |
| 1659 | * @since 1.0.0 |
| 1660 | */ |
| 1661 | private function encodeQ(string $str, string $position = 'text') : string |
| 1662 | { |
| 1663 | $pattern = ''; |
| 1664 | $encoded = \str_replace(["\r", "\n"], '', $str); |
| 1665 | |
| 1666 | switch (\strtolower($position)) { |
| 1667 | case 'phrase': |
| 1668 | $pattern = '^A-Za-z0-9!*+\/ -'; |
| 1669 | break; |
| 1670 | case 'comment': |
| 1671 | $pattern = '\(\)"'; |
| 1672 | case 'text': |
| 1673 | default: |
| 1674 | // Replace every high ascii, control, =, ? and _ characters |
| 1675 | $pattern = '\000-\011\013\014\016-\037\075\077\137\177-\377' . $pattern; |
| 1676 | break; |
| 1677 | } |
| 1678 | |
| 1679 | if (\preg_match_all("/[{$pattern}]/", $encoded, $matches) !== false) { |
| 1680 | return \strtr($encoded, ' ', '_'); |
| 1681 | } |
| 1682 | |
| 1683 | $matches = []; |
| 1684 | // If the string contains an '=', make sure it's the first thing we replace |
| 1685 | // so as to avoid double-encoding |
| 1686 | $eqkey = \array_search('=', $matches[0], true); |
| 1687 | if ($eqkey !== false) { |
| 1688 | unset($matches[0][$eqkey]); |
| 1689 | \array_unshift($matches[0], '='); |
| 1690 | } |
| 1691 | |
| 1692 | $unique = \array_unique($matches[0]); |
| 1693 | foreach ($unique as $char) { |
| 1694 | $encoded = \str_replace($char, '=' . \sprintf('%02X', \ord($char)), $encoded); |
| 1695 | } |
| 1696 | |
| 1697 | // Replace spaces with _ (more readable than =20) |
| 1698 | // RFC 2047 section 4.2(2) |
| 1699 | return \strtr($encoded, ' ', '_'); |
| 1700 | } |
| 1701 | |
| 1702 | /** |
| 1703 | * Encode and wrap long multibyte strings for mail headers |
| 1704 | * |
| 1705 | * @param string $str Multi-byte text to wrap encode |
| 1706 | * @param string $linebreak string to use as linefeed/end-of-line |
| 1707 | * |
| 1708 | * @return string |
| 1709 | * |
| 1710 | * @since 1.0.0 |
| 1711 | */ |
| 1712 | private function base64EncodeWrapMB(string $str, string $linebreak) : string |
| 1713 | { |
| 1714 | $start = '=?' . $this->charset . '?B?'; |
| 1715 | $end = '?='; |
| 1716 | $encoded = ''; |
| 1717 | |
| 1718 | $mbLength = \mb_strlen($str, $this->charset); |
| 1719 | $length = 75 - \strlen($start) - \strlen($end); |
| 1720 | $ratio = $mbLength / \strlen($str); |
| 1721 | $avgLength = \floor($length * $ratio * .75); |
| 1722 | |
| 1723 | $offset = 0; |
| 1724 | for ($i = 0; $i < $mbLength; $i += $offset) { |
| 1725 | $lookBack = 0; |
| 1726 | |
| 1727 | do { |
| 1728 | $offset = $avgLength - $lookBack; |
| 1729 | $chunk = \mb_substr($str, $i, $offset, $this->charset); |
| 1730 | $chunk = \base64_encode($chunk); |
| 1731 | ++$lookBack; |
| 1732 | } while (\strlen($chunk) > $length); |
| 1733 | |
| 1734 | $encoded .= $chunk . $linebreak; |
| 1735 | } |
| 1736 | |
| 1737 | return \substr($encoded, 0, -\strlen($linebreak)); |
| 1738 | } |
| 1739 | |
| 1740 | /** |
| 1741 | * Add an attachment from a path on the filesystem. |
| 1742 | * |
| 1743 | * @param string $path Path |
| 1744 | * @param string $name Overrides the attachment name |
| 1745 | * @param string $encoding File encoding |
| 1746 | * @param string $type Mime type; determined automatically from $path if not specified |
| 1747 | * @param string $disposition Disposition to use |
| 1748 | * |
| 1749 | * @return bool |
| 1750 | * |
| 1751 | * @since 1.0.0 |
| 1752 | */ |
| 1753 | public function addAttachment( |
| 1754 | string $path, |
| 1755 | string $name = '', |
| 1756 | string $encoding = EncodingType::E_BASE64, |
| 1757 | string $type = '', |
| 1758 | string $disposition = 'attachment' |
| 1759 | ) : bool { |
| 1760 | if (!FileUtils::isAccessible($path)) { |
| 1761 | return false; |
| 1762 | } |
| 1763 | |
| 1764 | // Mime from file |
| 1765 | if ($type === '') { |
| 1766 | $type = MimeType::extensionToMime(FileUtils::mb_pathinfo($path, \PATHINFO_EXTENSION)); |
| 1767 | } |
| 1768 | |
| 1769 | $filename = FileUtils::mb_pathinfo($path, \PATHINFO_BASENAME); |
| 1770 | if ($name === '') { |
| 1771 | $name = $filename; |
| 1772 | } |
| 1773 | |
| 1774 | $this->attachment[] = [ |
| 1775 | 0 => $path, |
| 1776 | 1 => $filename, |
| 1777 | 2 => $name, |
| 1778 | 3 => $encoding, |
| 1779 | 4 => $type, |
| 1780 | 5 => false, // isStringAttachment |
| 1781 | 6 => $disposition, |
| 1782 | 7 => $name, |
| 1783 | ]; |
| 1784 | |
| 1785 | return true; |
| 1786 | } |
| 1787 | |
| 1788 | /** |
| 1789 | * Return the array of attachments. |
| 1790 | * |
| 1791 | * @return array |
| 1792 | */ |
| 1793 | public function getAttachments() |
| 1794 | { |
| 1795 | return $this->attachment; |
| 1796 | } |
| 1797 | |
| 1798 | /** |
| 1799 | * Add a string or binary attachment (non-filesystem). |
| 1800 | * |
| 1801 | * @param string $string String attachment data |
| 1802 | * @param string $filename Name of the attachment |
| 1803 | * @param string $encoding File encoding (see $encoding) |
| 1804 | * @param string $type File extension (MIME) type |
| 1805 | * @param string $disposition Disposition to use |
| 1806 | * |
| 1807 | * @return bool |
| 1808 | * |
| 1809 | * @since 1.0.0 |
| 1810 | */ |
| 1811 | public function addStringAttachment( |
| 1812 | string $string, |
| 1813 | string $filename, |
| 1814 | string $encoding = EncodingType::E_BASE64, |
| 1815 | string $type = '', |
| 1816 | string $disposition = 'attachment' |
| 1817 | ) : bool { |
| 1818 | // Mime from file |
| 1819 | if ($type === '') { |
| 1820 | $type = MimeType::extensionToMime(FileUtils::mb_pathinfo($filename, \PATHINFO_EXTENSION)); |
| 1821 | } |
| 1822 | |
| 1823 | $this->attachment[] = [ |
| 1824 | 0 => $string, |
| 1825 | 1 => $filename, |
| 1826 | 2 => FileUtils::mb_pathinfo($filename, \PATHINFO_BASENAME), |
| 1827 | 3 => $encoding, |
| 1828 | 4 => $type, |
| 1829 | 5 => true, // isStringAttachment |
| 1830 | 6 => $disposition, |
| 1831 | 7 => 0, |
| 1832 | ]; |
| 1833 | |
| 1834 | return true; |
| 1835 | } |
| 1836 | |
| 1837 | /** |
| 1838 | * Add an embedded (inline) attachment from a file. |
| 1839 | * This can include images, sounds, and just about any other document type. |
| 1840 | * |
| 1841 | * @param string $path Path to the attachment |
| 1842 | * @param string $cid Content ID of the attachment |
| 1843 | * @param string $name Overrides the attachment name |
| 1844 | * @param string $encoding File encoding (see $encoding) |
| 1845 | * @param string $type File MIME type |
| 1846 | * @param string $disposition Disposition to use |
| 1847 | * |
| 1848 | * @return bool |
| 1849 | * |
| 1850 | * @since 1.0.0 |
| 1851 | */ |
| 1852 | public function addEmbeddedImage( |
| 1853 | string $path, |
| 1854 | string $cid, |
| 1855 | string $name = '', |
| 1856 | string $encoding = EncodingType::E_BASE64, |
| 1857 | string $type = '', |
| 1858 | string $disposition = 'inline' |
| 1859 | ) : bool { |
| 1860 | if (!FileUtils::isAccessible($path)) { |
| 1861 | return false; |
| 1862 | } |
| 1863 | |
| 1864 | // Mime from file |
| 1865 | if ($type === '') { |
| 1866 | $type = MimeType::extensionToMime(FileUtils::mb_pathinfo($path, \PATHINFO_EXTENSION)); |
| 1867 | } |
| 1868 | |
| 1869 | $filename = FileUtils::mb_pathinfo($path, \PATHINFO_BASENAME); |
| 1870 | if ($name === '') { |
| 1871 | $name = $filename; |
| 1872 | } |
| 1873 | |
| 1874 | // Append to $attachment array |
| 1875 | $this->attachment[] = [ |
| 1876 | 0 => $path, |
| 1877 | 1 => $filename, |
| 1878 | 2 => $name, |
| 1879 | 3 => $encoding, |
| 1880 | 4 => $type, |
| 1881 | 5 => false, // isStringAttachment |
| 1882 | 6 => $disposition, |
| 1883 | 7 => $cid, |
| 1884 | ]; |
| 1885 | |
| 1886 | return true; |
| 1887 | } |
| 1888 | |
| 1889 | /** |
| 1890 | * Add an embedded stringified attachment. |
| 1891 | * This can include images, sounds, and just about any other document type. |
| 1892 | * |
| 1893 | * @param string $string The attachment binary data |
| 1894 | * @param string $cid Content ID of the attachment |
| 1895 | * @param string $name A filename for the attachment. Should use extension. |
| 1896 | * @param string $encoding File encoding (see $encoding), defaults to 'base64' |
| 1897 | * @param string $type MIME type - will be used in preference to any automatically derived type |
| 1898 | * @param string $disposition Disposition to use |
| 1899 | * |
| 1900 | * @return bool |
| 1901 | * |
| 1902 | * @since 1.0.0 |
| 1903 | */ |
| 1904 | public function addStringEmbeddedImage( |
| 1905 | string $string, |
| 1906 | string $cid, |
| 1907 | string $name = '', |
| 1908 | string $encoding = EncodingType::E_BASE64, |
| 1909 | string $type = '', |
| 1910 | string $disposition = 'inline' |
| 1911 | ) : bool { |
| 1912 | // Mime from file |
| 1913 | if ($type === '' && !empty($name)) { |
| 1914 | $type = MimeType::extensionToMime(FileUtils::mb_pathinfo($name, \PATHINFO_EXTENSION)); |
| 1915 | } |
| 1916 | |
| 1917 | // Append to $attachment array |
| 1918 | $this->attachment[] = [ |
| 1919 | 0 => $string, |
| 1920 | 1 => $name, |
| 1921 | 2 => $name, |
| 1922 | 3 => $encoding, |
| 1923 | 4 => $type, |
| 1924 | 5 => true, // isStringAttachment |
| 1925 | 6 => $disposition, |
| 1926 | 7 => $cid, |
| 1927 | ]; |
| 1928 | |
| 1929 | return true; |
| 1930 | } |
| 1931 | |
| 1932 | /** |
| 1933 | * Check if an embedded attachment is present with this cid. |
| 1934 | * |
| 1935 | * @param string $cid Cid |
| 1936 | * |
| 1937 | * @return bool |
| 1938 | * |
| 1939 | * @since 1.0.0 |
| 1940 | */ |
| 1941 | protected function cidExists(string $cid) : bool |
| 1942 | { |
| 1943 | foreach ($this->attachment as $attachment) { |
| 1944 | if ($attachment[6] === 'inline' && $cid === $attachment[7]) { |
| 1945 | return true; |
| 1946 | } |
| 1947 | } |
| 1948 | |
| 1949 | return false; |
| 1950 | } |
| 1951 | |
| 1952 | /** |
| 1953 | * Add a custom header. |
| 1954 | * |
| 1955 | * @param string $name Name |
| 1956 | * @param null|string $value Value |
| 1957 | * |
| 1958 | * @return bool |
| 1959 | * |
| 1960 | * @since 1.0.0 |
| 1961 | */ |
| 1962 | public function addCustomHeader(string $name, string $value = null) : bool |
| 1963 | { |
| 1964 | $name = \trim($name); |
| 1965 | $value = \trim($value); |
| 1966 | |
| 1967 | if (empty($name) || \strpbrk($name . $value, "\r\n") !== false) { |
| 1968 | return false; |
| 1969 | } |
| 1970 | |
| 1971 | $this->customHeader[] = [$name, $value]; |
| 1972 | |
| 1973 | return true; |
| 1974 | } |
| 1975 | |
| 1976 | /** |
| 1977 | * Returns all custom headers. |
| 1978 | * |
| 1979 | * @return array |
| 1980 | * |
| 1981 | * @since 1.0.0 |
| 1982 | */ |
| 1983 | public function getCustomHeaders() : array |
| 1984 | { |
| 1985 | return $this->customHeader; |
| 1986 | } |
| 1987 | |
| 1988 | /** |
| 1989 | * Create a message body from an HTML string. |
| 1990 | * |
| 1991 | * $basedir is prepended when handling relative URLs, e.g. <img src="/images/a.png"> and must not be empty |
| 1992 | * will look for an image file in $basedir/images/a.png and convert it to inline. |
| 1993 | * If you don't provide a $basedir, relative paths will be left untouched (and thus probably break in email) |
| 1994 | * Converts data-uri images into embedded attachments. |
| 1995 | * |
| 1996 | * If you don't want to apply these transformations to your HTML, just set Body and AltBody directly. |
| 1997 | * |
| 1998 | * @param string $message HTML message string |
| 1999 | * @param string $basedir Absolute path to a base directory to prepend to relative paths to images |
| 2000 | * @param null|\Closure $advanced Internal or external text to html converter |
| 2001 | * |
| 2002 | * @return string |
| 2003 | * |
| 2004 | * @since 1.0.0 |
| 2005 | */ |
| 2006 | public function msgHTML(string $message, string $basedir = '', \Closure $advanced = null) |
| 2007 | { |
| 2008 | \preg_match_all('/(?<!-)(src|background)=["\'](.*)["\']/Ui', $message, $images); |
| 2009 | |
| 2010 | if (isset($images[2])) { |
| 2011 | if (\strlen($basedir) > 1 && \substr($basedir, -1) !== '/') { |
| 2012 | $basedir .= '/'; |
| 2013 | } |
| 2014 | |
| 2015 | foreach ($images[2] as $imgindex => $url) { |
| 2016 | // Convert data URIs into embedded images |
| 2017 | $match = []; |
| 2018 | if (\preg_match('#^data:(image/(?:jpe?g|gif|png));?(base64)?,(.+)#', $url, $match)) { |
| 2019 | if (\count($match) === 4 && $match[2] === EncodingType::E_BASE64) { |
| 2020 | $data = \base64_decode($match[3]); |
| 2021 | } elseif ($match[2] === '') { |
| 2022 | $data = \rawurldecode($match[3]); |
| 2023 | } else { |
| 2024 | continue; |
| 2025 | } |
| 2026 | |
| 2027 | $cid = \substr(\hash('sha256', $data), 0, 32) . '@phpoms.0'; // RFC2392 S 2 |
| 2028 | if (!$this->cidExists($cid)) { |
| 2029 | $this->addStringEmbeddedImage($data, $cid, 'embed' . $imgindex, EncodingType::E_BASE64, $match[1]); |
| 2030 | } |
| 2031 | |
| 2032 | $message = \str_replace($images[0][$imgindex], $images[1][$imgindex] . '="cid:' . $cid . '"', $message); |
| 2033 | |
| 2034 | continue; |
| 2035 | } |
| 2036 | |
| 2037 | if (!empty($basedir) |
| 2038 | && (\strpos($url, '..') === false) |
| 2039 | && !\str_starts_with($url, 'cid:') |
| 2040 | && !\preg_match('#^[a-z][a-z0-9+.-]*:?//#i', $url) |
| 2041 | ) { |
| 2042 | $filename = FileUtils::mb_pathinfo($url, \PATHINFO_BASENAME); |
| 2043 | $directory = \dirname($url); |
| 2044 | |
| 2045 | if ($directory === '.') { |
| 2046 | $directory = ''; |
| 2047 | } |
| 2048 | |
| 2049 | // RFC2392 S 2 |
| 2050 | $cid = \substr(\hash('sha256', $url), 0, 32) . '@phpoms.0'; |
| 2051 | if (\strlen($basedir) > 1 && \substr($basedir, -1) !== '/') { |
| 2052 | $basedir .= '/'; |
| 2053 | } |
| 2054 | |
| 2055 | if (\strlen($directory) > 1 && \substr($directory, -1) !== '/') { |
| 2056 | $directory .= '/'; |
| 2057 | } |
| 2058 | |
| 2059 | if ($this->addEmbeddedImage( |
| 2060 | $basedir . $directory . $filename, |
| 2061 | $cid, |
| 2062 | $filename, |
| 2063 | EncodingType::E_BASE64, |
| 2064 | MimeType::extensionToMime((string) FileUtils::mb_pathinfo($filename, \PATHINFO_EXTENSION)) |
| 2065 | ) |
| 2066 | ) { |
| 2067 | $message = \preg_replace( |
| 2068 | '/' . $images[1][$imgindex] . '=["\']' . \preg_quote($url, '/') . '["\']/Ui', |
| 2069 | $images[1][$imgindex] . '="cid:' . $cid . '"', |
| 2070 | $message |
| 2071 | ); |
| 2072 | } |
| 2073 | } |
| 2074 | } |
| 2075 | } |
| 2076 | |
| 2077 | $this->contentType = MimeType::M_HTML; |
| 2078 | $this->body = self::normalizeBreaks($message, self::$LE); |
| 2079 | $this->bodyAlt = self::normalizeBreaks($this->html2text($message, $advanced), self::$LE); |
| 2080 | |
| 2081 | if (empty($this->bodyAlt)) { |
| 2082 | $this->bodyAlt = 'This is an HTML-only message. To view it, activate HTML in your email application.' . self::$LE; |
| 2083 | } |
| 2084 | |
| 2085 | return $this->body; |
| 2086 | } |
| 2087 | |
| 2088 | /** |
| 2089 | * Normalize line breaks in a string. |
| 2090 | * |
| 2091 | * @param string $text |
| 2092 | * @param string $breaktype What kind of line break to use; defaults to self::$LE |
| 2093 | * |
| 2094 | * @return string |
| 2095 | * |
| 2096 | * @since 1.0.0 |
| 2097 | */ |
| 2098 | private static function normalizeBreaks(string $text, string $breaktype) : string |
| 2099 | { |
| 2100 | $text = \str_replace(["\r\n", "\r"], "\n", $text); |
| 2101 | |
| 2102 | if ($breaktype !== "\n") { |
| 2103 | $text = \str_replace("\n", $breaktype, $text); |
| 2104 | } |
| 2105 | |
| 2106 | return $text; |
| 2107 | } |
| 2108 | |
| 2109 | /** |
| 2110 | * Convert an HTML string into plain text. |
| 2111 | * |
| 2112 | * @param string $html The HTML text to convert |
| 2113 | * @param null|\Closure $advanced Internal or external text to html converter |
| 2114 | * |
| 2115 | * @return string |
| 2116 | * |
| 2117 | * @since 1.0.0 |
| 2118 | */ |
| 2119 | private function html2text(string $html, \Closure $advanced = null) : string |
| 2120 | { |
| 2121 | if ($advanced !== null) { |
| 2122 | return $advanced($html); |
| 2123 | } |
| 2124 | |
| 2125 | return \html_entity_decode( |
| 2126 | \trim(\strip_tags(\preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))), |
| 2127 | \ENT_QUOTES, |
| 2128 | $this->charset |
| 2129 | ); |
| 2130 | } |
| 2131 | |
| 2132 | /** |
| 2133 | * Set the public and private key files and password for S/MIME signing. |
| 2134 | * |
| 2135 | * @param string $certFile Certification file |
| 2136 | * @param string $keyFile Key file |
| 2137 | * @param string $keyPass Password for private key |
| 2138 | * @param string $extracertsFile Optional path to chain certificate |
| 2139 | * |
| 2140 | * @return void |
| 2141 | * |
| 2142 | * @since 1.0.0 |
| 2143 | */ |
| 2144 | public function sign($certFile, $keyFile, $keyPass, $extracertsFile = '') : void |
| 2145 | { |
| 2146 | $this->signCertFile = $certFile; |
| 2147 | $this->signKeyFile = $keyFile; |
| 2148 | $this->signKeyPass = $keyPass; |
| 2149 | $this->signExtracertFiles = $extracertsFile; |
| 2150 | } |
| 2151 | |
| 2152 | /** |
| 2153 | * Quoted-Printable-encode a DKIM header. |
| 2154 | * |
| 2155 | * @param string $txt Text |
| 2156 | * |
| 2157 | * @return string |
| 2158 | * |
| 2159 | * @since 1.0.0 |
| 2160 | */ |
| 2161 | public function dkimQP(string $txt) : string |
| 2162 | { |
| 2163 | $line = ''; |
| 2164 | $len = \strlen($txt); |
| 2165 | |
| 2166 | for ($i = 0; $i < $len; ++$i) { |
| 2167 | $ord = \ord($txt[$i]); |
| 2168 | $line .= (($ord >= 0x21) && ($ord <= 0x3A)) || $ord === 0x3C || (($ord >= 0x3E) && ($ord <= 0x7E)) |
| 2169 | ? $txt[$i] |
| 2170 | : '=' . \sprintf('%02X', $ord); |
| 2171 | } |
| 2172 | |
| 2173 | return $line; |
| 2174 | } |
| 2175 | |
| 2176 | /** |
| 2177 | * Generate a DKIM signature. |
| 2178 | * |
| 2179 | * @param string $signHeader |
| 2180 | * |
| 2181 | * @return string The DKIM signature value |
| 2182 | * |
| 2183 | * @since 1.0.0 |
| 2184 | */ |
| 2185 | public function dkimSign(string $signHeader) : string |
| 2186 | { |
| 2187 | if (!\defined('PKCS7_TEXT')) { |
| 2188 | return ''; |
| 2189 | } |
| 2190 | |
| 2191 | $privKeyStr = empty($this->dkimPrivateKey) |
| 2192 | ? \file_get_contents($this->dkimPrivatePath) |
| 2193 | : $this->dkimPrivateKey; |
| 2194 | |
| 2195 | $privKey = $this->dkimPass === '' |
| 2196 | ? \openssl_pkey_get_private($privKeyStr) |
| 2197 | : \openssl_pkey_get_private($privKeyStr, $this->dkimPass); |
| 2198 | |
| 2199 | return \openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryption') |
| 2200 | ? \base64_encode($signature) |
| 2201 | : ''; |
| 2202 | } |
| 2203 | |
| 2204 | /** |
| 2205 | * Generate a DKIM canonicalization header. |
| 2206 | * |
| 2207 | * @param string $signHeader Header |
| 2208 | * |
| 2209 | * @return string |
| 2210 | * |
| 2211 | * @since 1.0.0 |
| 2212 | */ |
| 2213 | public function dkimHeaderC(string $signHeader) : string |
| 2214 | { |
| 2215 | $signHeader = self::normalizeBreaks($signHeader, "\r\n"); |
| 2216 | $signHeader = \preg_replace('/\r\n[ \t]+/', ' ', $signHeader); |
| 2217 | $lines = \explode("\r\n", $signHeader); |
| 2218 | |
| 2219 | foreach ($lines as $key => $line) { |
| 2220 | if (\strpos($line, ':') === false) { |
| 2221 | continue; |
| 2222 | } |
| 2223 | |
| 2224 | list($heading, $value) = \explode(':', $line, 2); |
| 2225 | $heading = \strtolower($heading); |
| 2226 | $value = \preg_replace('/[ \t]+/', ' ', $value); |
| 2227 | |
| 2228 | $lines[$key] = \trim($heading, " \t") . ':' . \trim($value, " \t"); |
| 2229 | } |
| 2230 | |
| 2231 | return \implode("\r\n", $lines); |
| 2232 | } |
| 2233 | |
| 2234 | /** |
| 2235 | * Generate a DKIM canonicalization body. |
| 2236 | * |
| 2237 | * @param string $body Message Body |
| 2238 | * |
| 2239 | * @return string |
| 2240 | * |
| 2241 | * @since 1.0.0 |
| 2242 | */ |
| 2243 | public function dkimBodyC(string $body) : string |
| 2244 | { |
| 2245 | if (empty($body)) { |
| 2246 | return "\r\n"; |
| 2247 | } |
| 2248 | |
| 2249 | $body = self::normalizeBreaks($body, "\r\n"); |
| 2250 | |
| 2251 | return \rtrim($body, " \r\n\t") . "\r\n"; |
| 2252 | } |
| 2253 | |
| 2254 | /** |
| 2255 | * Create the DKIM header and body in a new message header. |
| 2256 | * |
| 2257 | * @param string $headersLine Header lines |
| 2258 | * @param string $subject Subject |
| 2259 | * @param string $body Body |
| 2260 | * |
| 2261 | * @return string |
| 2262 | * |
| 2263 | * @since 1.0.0 |
| 2264 | */ |
| 2265 | public function dkimAdd(string $headersLine, string $subject, string $body) : string |
| 2266 | { |
| 2267 | $DKIMsignatureType = 'rsa-sha256'; |
| 2268 | $DKIMcanonicalization = 'relaxed/simple'; |
| 2269 | $DKIMquery = 'dns/txt'; |
| 2270 | $DKIMtime = \time(); |
| 2271 | |
| 2272 | $autoSignHeaders = [ |
| 2273 | 'from', |
| 2274 | 'to', |
| 2275 | 'cc', |
| 2276 | 'date', |
| 2277 | 'subject', |
| 2278 | 'reply-to', |
| 2279 | 'message-id', |
| 2280 | 'content-type', |
| 2281 | 'mime-version', |
| 2282 | 'x-mailer', |
| 2283 | ]; |
| 2284 | |
| 2285 | if (\stripos($headersLine, 'Subject') === false) { |
| 2286 | $headersLine .= 'Subject: ' . $subject . self::$LE; |
| 2287 | } |
| 2288 | |
| 2289 | $headerLines = \explode(self::$LE, $headersLine); |
| 2290 | $currentHeaderLabel = ''; |
| 2291 | $currentHeaderValue = ''; |
| 2292 | $parsedHeaders = []; |
| 2293 | $headerLineIndex = 0; |
| 2294 | $headerLineCount = \count($headerLines); |
| 2295 | |
| 2296 | foreach ($headerLines as $headerLine) { |
| 2297 | $matches = []; |
| 2298 | if (\preg_match('/^([^ \t]*?)(?::[ \t]*)(.*)$/', $headerLine, $matches)) { |
| 2299 | if ($currentHeaderLabel !== '') { |
| 2300 | $parsedHeaders[] = ['label' => $currentHeaderLabel, 'value' => $currentHeaderValue]; |
| 2301 | } |
| 2302 | |
| 2303 | $currentHeaderLabel = $matches[1]; |
| 2304 | $currentHeaderValue = $matches[2]; |
| 2305 | } elseif (\preg_match('/^[ \t]+(.*)$/', $headerLine, $matches)) { |
| 2306 | $currentHeaderValue .= ' ' . $matches[1]; |
| 2307 | } |
| 2308 | |
| 2309 | ++$headerLineIndex; |
| 2310 | |
| 2311 | if ($headerLineIndex >= $headerLineCount) { |
| 2312 | $parsedHeaders[] = ['label' => $currentHeaderLabel, 'value' => $currentHeaderValue]; |
| 2313 | } |
| 2314 | } |
| 2315 | |
| 2316 | $copiedHeaders = []; |
| 2317 | $headersToSignKeys = []; |
| 2318 | $headersToSign = []; |
| 2319 | |
| 2320 | foreach ($parsedHeaders as $header) { |
| 2321 | if (\in_array(\strtolower($header['label']), $autoSignHeaders, true)) { |
| 2322 | $headersToSignKeys[] = $header['label']; |
| 2323 | $headersToSign[] = $header['label'] . ': ' . $header['value']; |
| 2324 | |
| 2325 | if ($this->dkimCopyHeader) { |
| 2326 | $copiedHeaders[] = $header['label'] . ':' |
| 2327 | . \str_replace('|', '=7C', $this->dkimQP($header['value'])); |
| 2328 | } |
| 2329 | |
| 2330 | continue; |
| 2331 | } |
| 2332 | |
| 2333 | if (\in_array($header['label'], $this->dkimHeaders, true)) { |
| 2334 | foreach ($this->customHeader as $customHeader) { |
| 2335 | if ($customHeader[0] === $header['label']) { |
| 2336 | $headersToSignKeys[] = $header['label']; |
| 2337 | $headersToSign[] = $header['label'] . ': ' . $header['value']; |
| 2338 | |
| 2339 | if ($this->dkimCopyHeader) { |
| 2340 | $copiedHeaders[] = $header['label'] . ':' |
| 2341 | . \str_replace('|', '=7C', $this->dkimQP($header['value'])); |
| 2342 | } |
| 2343 | |
| 2344 | continue 2; |
| 2345 | } |
| 2346 | } |
| 2347 | } |
| 2348 | } |
| 2349 | |
| 2350 | $copiedHeaderFields = ''; |
| 2351 | if ($this->dkimCopyHeader && !empty($copiedHeaders)) { |
| 2352 | $copiedHeaderFields = ' z='; |
| 2353 | $first = true; |
| 2354 | |
| 2355 | foreach ($copiedHeaders as $copiedHeader) { |
| 2356 | if (!$first) { |
| 2357 | $copiedHeaderFields .= self::$LE . ' |'; |
| 2358 | } |
| 2359 | |
| 2360 | $copiedHeaderFields .= \strlen($copiedHeader) > self::STD_LINE_LENGTH - 3 |
| 2361 | ? \substr( |
| 2362 | \chunk_split($copiedHeader, self::STD_LINE_LENGTH - 3, self::$LE . self::FWS), |
| 2363 | 0, |
| 2364 | -\strlen(self::$LE . self::FWS) |
| 2365 | ) |
| 2366 | : $copiedHeader; |
| 2367 | |
| 2368 | $first = false; |
| 2369 | } |
| 2370 | |
| 2371 | $copiedHeaderFields .= ';' . self::$LE; |
| 2372 | } |
| 2373 | |
| 2374 | $headerKeys = ' h=' . \implode(':', $headersToSignKeys) . ';' . self::$LE; |
| 2375 | $headerValues = \implode(self::$LE, $headersToSign); |
| 2376 | $body = $this->dkimBodyC($body); |
| 2377 | $DKIMb64 = \base64_encode(\pack('H*', \hash('sha256', $body))); |
| 2378 | $ident = ''; |
| 2379 | |
| 2380 | if ($this->dkimIdentity !== '') { |
| 2381 | $ident = ' i=' . $this->dkimIdentity . ';' . self::$LE; |
| 2382 | } |
| 2383 | |
| 2384 | $dkimSignatureHeader = 'DKIM-Signature: v=1;' |
| 2385 | . ' d=' . $this->dkimDomain . ';' |
| 2386 | . ' s=' . $this->dkimSelector . ';' . self::$LE |
| 2387 | . ' a=' . $DKIMsignatureType . ';' |
| 2388 | . ' q=' . $DKIMquery . ';' |
| 2389 | . ' t=' . $DKIMtime . ';' |
| 2390 | . ' c=' . $DKIMcanonicalization . ';' . self::$LE |
| 2391 | . $headerKeys . $ident . $copiedHeaderFields |
| 2392 | . ' bh=' . $DKIMb64 . ';' . self::$LE |
| 2393 | . ' b='; |
| 2394 | |
| 2395 | $canonicalizedHeaders = $this->dkimHeaderC( |
| 2396 | $headerValues . self::$LE . $dkimSignatureHeader |
| 2397 | ); |
| 2398 | |
| 2399 | $signature = $this->dkimSign($canonicalizedHeaders); |
| 2400 | $signature = \trim(\chunk_split($signature, self::STD_LINE_LENGTH - 3, self::$LE . self::FWS)); |
| 2401 | |
| 2402 | return self::normalizeBreaks($dkimSignatureHeader . $signature, self::$LE); |
| 2403 | } |
| 2404 | } |