Mime.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. <?php
  2. namespace Laminas\Mime;
  3. use function base64_encode;
  4. use function chunk_split;
  5. use function count;
  6. use function implode;
  7. use function max;
  8. use function md5;
  9. use function microtime;
  10. use function ord;
  11. use function preg_match;
  12. use function rtrim;
  13. use function sprintf;
  14. use function str_replace;
  15. use function strcspn;
  16. use function strlen;
  17. use function strpos;
  18. use function strrpos;
  19. use function strtoupper;
  20. use function substr;
  21. use function substr_replace;
  22. use function trim;
  23. /**
  24. * Support class for MultiPart Mime Messages
  25. */
  26. class Mime
  27. {
  28. // phpcs:disable Generic.Files.LineLength.TooLong
  29. public const TYPE_OCTETSTREAM = 'application/octet-stream';
  30. public const TYPE_TEXT = 'text/plain';
  31. public const TYPE_HTML = 'text/html';
  32. public const TYPE_ENRICHED = 'text/enriched';
  33. public const TYPE_XML = 'text/xml';
  34. public const ENCODING_7BIT = '7bit';
  35. public const ENCODING_8BIT = '8bit';
  36. public const ENCODING_QUOTEDPRINTABLE = 'quoted-printable';
  37. public const ENCODING_BASE64 = 'base64';
  38. public const DISPOSITION_ATTACHMENT = 'attachment';
  39. public const DISPOSITION_INLINE = 'inline';
  40. public const LINELENGTH = 72;
  41. public const LINEEND = "\n";
  42. public const MULTIPART_ALTERNATIVE = 'multipart/alternative';
  43. public const MULTIPART_MIXED = 'multipart/mixed';
  44. public const MULTIPART_RELATED = 'multipart/related';
  45. public const MULTIPART_RELATIVE = 'multipart/relative';
  46. public const MULTIPART_REPORT = 'multipart/report';
  47. public const MESSAGE_RFC822 = 'message/rfc822';
  48. public const MESSAGE_DELIVERY_STATUS = 'message/delivery-status';
  49. public const CHARSET_REGEX = '#=\?(?P<charset>[\x21\x23-\x26\x2a\x2b\x2d\x5e\5f\60\x7b-\x7ea-zA-Z0-9]+)\?(?P<encoding>[\x21\x23-\x26\x2a\x2b\x2d\x5e\5f\60\x7b-\x7ea-zA-Z0-9]+)\?(?P<text>[\x21-\x3e\x40-\x7e]+)#';
  50. // phpcs:enable
  51. /** @var null|string */
  52. protected $boundary;
  53. /** @var int */
  54. protected static $makeUnique = 0;
  55. /**
  56. * Lookup-tables for QuotedPrintable
  57. *
  58. * @var string[]
  59. */
  60. public static $qpKeys = [
  61. "\x00",
  62. "\x01",
  63. "\x02",
  64. "\x03",
  65. "\x04",
  66. "\x05",
  67. "\x06",
  68. "\x07",
  69. "\x08",
  70. "\x09",
  71. "\x0A",
  72. "\x0B",
  73. "\x0C",
  74. "\x0D",
  75. "\x0E",
  76. "\x0F",
  77. "\x10",
  78. "\x11",
  79. "\x12",
  80. "\x13",
  81. "\x14",
  82. "\x15",
  83. "\x16",
  84. "\x17",
  85. "\x18",
  86. "\x19",
  87. "\x1A",
  88. "\x1B",
  89. "\x1C",
  90. "\x1D",
  91. "\x1E",
  92. "\x1F",
  93. "\x7F",
  94. "\x80",
  95. "\x81",
  96. "\x82",
  97. "\x83",
  98. "\x84",
  99. "\x85",
  100. "\x86",
  101. "\x87",
  102. "\x88",
  103. "\x89",
  104. "\x8A",
  105. "\x8B",
  106. "\x8C",
  107. "\x8D",
  108. "\x8E",
  109. "\x8F",
  110. "\x90",
  111. "\x91",
  112. "\x92",
  113. "\x93",
  114. "\x94",
  115. "\x95",
  116. "\x96",
  117. "\x97",
  118. "\x98",
  119. "\x99",
  120. "\x9A",
  121. "\x9B",
  122. "\x9C",
  123. "\x9D",
  124. "\x9E",
  125. "\x9F",
  126. "\xA0",
  127. "\xA1",
  128. "\xA2",
  129. "\xA3",
  130. "\xA4",
  131. "\xA5",
  132. "\xA6",
  133. "\xA7",
  134. "\xA8",
  135. "\xA9",
  136. "\xAA",
  137. "\xAB",
  138. "\xAC",
  139. "\xAD",
  140. "\xAE",
  141. "\xAF",
  142. "\xB0",
  143. "\xB1",
  144. "\xB2",
  145. "\xB3",
  146. "\xB4",
  147. "\xB5",
  148. "\xB6",
  149. "\xB7",
  150. "\xB8",
  151. "\xB9",
  152. "\xBA",
  153. "\xBB",
  154. "\xBC",
  155. "\xBD",
  156. "\xBE",
  157. "\xBF",
  158. "\xC0",
  159. "\xC1",
  160. "\xC2",
  161. "\xC3",
  162. "\xC4",
  163. "\xC5",
  164. "\xC6",
  165. "\xC7",
  166. "\xC8",
  167. "\xC9",
  168. "\xCA",
  169. "\xCB",
  170. "\xCC",
  171. "\xCD",
  172. "\xCE",
  173. "\xCF",
  174. "\xD0",
  175. "\xD1",
  176. "\xD2",
  177. "\xD3",
  178. "\xD4",
  179. "\xD5",
  180. "\xD6",
  181. "\xD7",
  182. "\xD8",
  183. "\xD9",
  184. "\xDA",
  185. "\xDB",
  186. "\xDC",
  187. "\xDD",
  188. "\xDE",
  189. "\xDF",
  190. "\xE0",
  191. "\xE1",
  192. "\xE2",
  193. "\xE3",
  194. "\xE4",
  195. "\xE5",
  196. "\xE6",
  197. "\xE7",
  198. "\xE8",
  199. "\xE9",
  200. "\xEA",
  201. "\xEB",
  202. "\xEC",
  203. "\xED",
  204. "\xEE",
  205. "\xEF",
  206. "\xF0",
  207. "\xF1",
  208. "\xF2",
  209. "\xF3",
  210. "\xF4",
  211. "\xF5",
  212. "\xF6",
  213. "\xF7",
  214. "\xF8",
  215. "\xF9",
  216. "\xFA",
  217. "\xFB",
  218. "\xFC",
  219. "\xFD",
  220. "\xFE",
  221. "\xFF",
  222. ];
  223. /** @var string[] */
  224. public static $qpReplaceValues = [
  225. "=00",
  226. "=01",
  227. "=02",
  228. "=03",
  229. "=04",
  230. "=05",
  231. "=06",
  232. "=07",
  233. "=08",
  234. "=09",
  235. "=0A",
  236. "=0B",
  237. "=0C",
  238. "=0D",
  239. "=0E",
  240. "=0F",
  241. "=10",
  242. "=11",
  243. "=12",
  244. "=13",
  245. "=14",
  246. "=15",
  247. "=16",
  248. "=17",
  249. "=18",
  250. "=19",
  251. "=1A",
  252. "=1B",
  253. "=1C",
  254. "=1D",
  255. "=1E",
  256. "=1F",
  257. "=7F",
  258. "=80",
  259. "=81",
  260. "=82",
  261. "=83",
  262. "=84",
  263. "=85",
  264. "=86",
  265. "=87",
  266. "=88",
  267. "=89",
  268. "=8A",
  269. "=8B",
  270. "=8C",
  271. "=8D",
  272. "=8E",
  273. "=8F",
  274. "=90",
  275. "=91",
  276. "=92",
  277. "=93",
  278. "=94",
  279. "=95",
  280. "=96",
  281. "=97",
  282. "=98",
  283. "=99",
  284. "=9A",
  285. "=9B",
  286. "=9C",
  287. "=9D",
  288. "=9E",
  289. "=9F",
  290. "=A0",
  291. "=A1",
  292. "=A2",
  293. "=A3",
  294. "=A4",
  295. "=A5",
  296. "=A6",
  297. "=A7",
  298. "=A8",
  299. "=A9",
  300. "=AA",
  301. "=AB",
  302. "=AC",
  303. "=AD",
  304. "=AE",
  305. "=AF",
  306. "=B0",
  307. "=B1",
  308. "=B2",
  309. "=B3",
  310. "=B4",
  311. "=B5",
  312. "=B6",
  313. "=B7",
  314. "=B8",
  315. "=B9",
  316. "=BA",
  317. "=BB",
  318. "=BC",
  319. "=BD",
  320. "=BE",
  321. "=BF",
  322. "=C0",
  323. "=C1",
  324. "=C2",
  325. "=C3",
  326. "=C4",
  327. "=C5",
  328. "=C6",
  329. "=C7",
  330. "=C8",
  331. "=C9",
  332. "=CA",
  333. "=CB",
  334. "=CC",
  335. "=CD",
  336. "=CE",
  337. "=CF",
  338. "=D0",
  339. "=D1",
  340. "=D2",
  341. "=D3",
  342. "=D4",
  343. "=D5",
  344. "=D6",
  345. "=D7",
  346. "=D8",
  347. "=D9",
  348. "=DA",
  349. "=DB",
  350. "=DC",
  351. "=DD",
  352. "=DE",
  353. "=DF",
  354. "=E0",
  355. "=E1",
  356. "=E2",
  357. "=E3",
  358. "=E4",
  359. "=E5",
  360. "=E6",
  361. "=E7",
  362. "=E8",
  363. "=E9",
  364. "=EA",
  365. "=EB",
  366. "=EC",
  367. "=ED",
  368. "=EE",
  369. "=EF",
  370. "=F0",
  371. "=F1",
  372. "=F2",
  373. "=F3",
  374. "=F4",
  375. "=F5",
  376. "=F6",
  377. "=F7",
  378. "=F8",
  379. "=F9",
  380. "=FA",
  381. "=FB",
  382. "=FC",
  383. "=FD",
  384. "=FE",
  385. "=FF",
  386. ];
  387. // @codingStandardsIgnoreStart
  388. public static $qpKeysString =
  389. "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F\x7F\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F\xA0\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9\xAA\xAB\xAC\xAD\xAE\xAF\xB0\xB1\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA\xBB\xBC\xBD\xBE\xBF\xC0\xC1\xC2\xC3\xC4\xC5\xC6\xC7\xC8\xC9\xCA\xCB\xCC\xCD\xCE\xCF\xD0\xD1\xD2\xD3\xD4\xD5\xD6\xD7\xD8\xD9\xDA\xDB\xDC\xDD\xDE\xDF\xE0\xE1\xE2\xE3\xE4\xE5\xE6\xE7\xE8\xE9\xEA\xEB\xEC\xED\xEE\xEF\xF0\xF1\xF2\xF3\xF4\xF5\xF6\xF7\xF8\xF9\xFA\xFB\xFC\xFD\xFE\xFF";
  390. // @codingStandardsIgnoreEnd
  391. /**
  392. * Check if the given string is "printable"
  393. *
  394. * Checks that a string contains no unprintable characters. If this returns
  395. * false, encode the string for secure delivery.
  396. *
  397. * @param string $str
  398. * @return bool
  399. */
  400. public static function isPrintable($str)
  401. {
  402. return strcspn($str, static::$qpKeysString) === strlen($str);
  403. }
  404. /**
  405. * Encode a given string with the QUOTED_PRINTABLE mechanism and wrap the lines.
  406. *
  407. * @param string $str
  408. * @param int $lineLength Defaults to {@link LINELENGTH}
  409. * @param string $lineEnd Defaults to {@link LINEEND}
  410. * @return string
  411. */
  412. public static function encodeQuotedPrintable(
  413. $str,
  414. $lineLength = self::LINELENGTH,
  415. $lineEnd = self::LINEEND
  416. ) {
  417. $out = '';
  418. $str = self::_encodeQuotedPrintable($str);
  419. // Split encoded text into separate lines
  420. $initialPtr = 0;
  421. $strLength = strlen($str);
  422. while ($initialPtr < $strLength) {
  423. $continueAt = $strLength - $initialPtr;
  424. if ($continueAt > $lineLength) {
  425. $continueAt = $lineLength;
  426. }
  427. $chunk = substr($str, $initialPtr, $continueAt);
  428. // Ensure we are not splitting across an encoded character
  429. $endingMarkerPos = strrpos($chunk, '=');
  430. if ($endingMarkerPos !== false && $endingMarkerPos >= strlen($chunk) - 2) {
  431. $chunk = substr($chunk, 0, $endingMarkerPos);
  432. $continueAt = $endingMarkerPos;
  433. }
  434. if (ord($chunk[0]) === 0x2E) { // 0x2E is a dot
  435. $chunk = '=2E' . substr($chunk, 1);
  436. }
  437. // copied from swiftmailer https://git.io/vAXU1
  438. switch (ord(substr($chunk, strlen($chunk) - 1))) {
  439. case 0x09: // Horizontal Tab
  440. $chunk = substr_replace($chunk, '=09', strlen($chunk) - 1, 1);
  441. break;
  442. case 0x20: // Space
  443. $chunk = substr_replace($chunk, '=20', strlen($chunk) - 1, 1);
  444. break;
  445. }
  446. // Add string and continue
  447. $out .= $chunk . '=' . $lineEnd;
  448. $initialPtr += $continueAt;
  449. }
  450. $out = rtrim($out, $lineEnd);
  451. $out = rtrim($out, '=');
  452. return $out;
  453. }
  454. /**
  455. * Converts a string into quoted printable format.
  456. *
  457. * @param string $str
  458. * @return string
  459. */
  460. // @codingStandardsIgnoreStart
  461. private static function _encodeQuotedPrintable($str)
  462. {
  463. // @codingStandardsIgnoreEnd
  464. $str = str_replace('=', '=3D', $str);
  465. $str = str_replace(static::$qpKeys, static::$qpReplaceValues, $str);
  466. $str = rtrim($str);
  467. return $str;
  468. }
  469. /**
  470. * Encode a given string with the QUOTED_PRINTABLE mechanism for Mail Headers.
  471. *
  472. * Mail headers depend on an extended quoted printable algorithm otherwise
  473. * a range of bugs can occur.
  474. *
  475. * @param string $str
  476. * @param string $charset
  477. * @param int $lineLength Defaults to {@link LINELENGTH}
  478. * @param string $lineEnd Defaults to {@link LINEEND}
  479. * @param positive-int|0 $headerNameSize When folding a line, it is necessary to calculate
  480. * the length of the entire line (together with the header name).
  481. * Therefore, you can specify the header name and colon length
  482. * in this argument to fold the string properly.
  483. * @return string
  484. */
  485. public static function encodeQuotedPrintableHeader(
  486. $str,
  487. $charset,
  488. $lineLength = self::LINELENGTH,
  489. $lineEnd = self::LINEEND,
  490. $headerNameSize = 0
  491. ) {
  492. // Reduce line-length by the length of the required delimiter, charsets and encoding
  493. $prefix = sprintf('=?%s?Q?', $charset);
  494. $lineLength = $lineLength - strlen($prefix) - 3;
  495. $str = self::_encodeQuotedPrintable($str);
  496. // Mail-Header required chars have to be encoded also:
  497. $str = str_replace(['?', ',', ' ', '_'], ['=3F', '=2C', '=20', '=5F'], $str);
  498. // initialize first line, we need it anyways
  499. $lines = [0 => ''];
  500. // Split encoded text into separate lines
  501. $tmp = '';
  502. while (strlen($str) > 0) {
  503. $currentLine = max(count($lines) - 1, 0);
  504. $token = static::getNextQuotedPrintableToken($str);
  505. $substr = substr($str, strlen($token));
  506. $str = false === $substr ? '' : $substr;
  507. $tmp .= $token;
  508. if ($token === '=20') {
  509. // only if we have a single char token or space, we can append the
  510. // tempstring it to the current line or start a new line if necessary.
  511. if ($currentLine === 0) {
  512. // The size of the first line should be calculated with the header name.
  513. $currentLineLength = strlen($lines[$currentLine] . $tmp) + $headerNameSize;
  514. } else {
  515. $currentLineLength = strlen($lines[$currentLine] . $tmp);
  516. }
  517. $lineLimitReached = $currentLineLength > $lineLength;
  518. $noCurrentLine = $lines[$currentLine] === '';
  519. if ($noCurrentLine && $lineLimitReached) {
  520. $lines[$currentLine] = $tmp;
  521. $lines[$currentLine + 1] = '';
  522. } elseif ($lineLimitReached) {
  523. $lines[$currentLine + 1] = $tmp;
  524. } else {
  525. $lines[$currentLine] .= $tmp;
  526. }
  527. $tmp = '';
  528. }
  529. // don't forget to append the rest to the last line
  530. if (strlen($str) === 0) {
  531. $lines[$currentLine] .= $tmp;
  532. }
  533. }
  534. // assemble the lines together by pre- and appending delimiters, charset, encoding.
  535. for ($i = 0, $count = count($lines); $i < $count; $i++) {
  536. $lines[$i] = " " . $prefix . $lines[$i] . "?=";
  537. }
  538. $str = trim(implode($lineEnd, $lines));
  539. return $str;
  540. }
  541. /**
  542. * Retrieves the first token from a quoted printable string.
  543. *
  544. * @param string $str
  545. * @return string
  546. */
  547. private static function getNextQuotedPrintableToken($str)
  548. {
  549. if (0 === strpos($str, '=')) {
  550. $token = substr($str, 0, 3);
  551. } else {
  552. $token = substr($str, 0, 1);
  553. }
  554. return $token;
  555. }
  556. /**
  557. * Encode a given string in mail header compatible base64 encoding.
  558. *
  559. * @param string $str
  560. * @param string $charset
  561. * @param int $lineLength Defaults to {@link LINELENGTH}
  562. * @param string $lineEnd Defaults to {@link LINEEND}
  563. * @return string
  564. */
  565. public static function encodeBase64Header(
  566. $str,
  567. $charset,
  568. $lineLength = self::LINELENGTH,
  569. $lineEnd = self::LINEEND
  570. ) {
  571. $prefix = '=?' . $charset . '?B?';
  572. $suffix = '?=';
  573. $remainingLength = $lineLength - strlen($prefix) - strlen($suffix);
  574. $encodedValue = static::encodeBase64($str, $remainingLength, $lineEnd);
  575. $encodedValue = str_replace($lineEnd, $suffix . $lineEnd . ' ' . $prefix, $encodedValue);
  576. $encodedValue = $prefix . $encodedValue . $suffix;
  577. return $encodedValue;
  578. }
  579. /**
  580. * Encode a given string in base64 encoding and break lines
  581. * according to the maximum linelength.
  582. *
  583. * @param string $str
  584. * @param int $lineLength Defaults to {@link LINELENGTH}
  585. * @param string $lineEnd Defaults to {@link LINEEND}
  586. * @return string
  587. */
  588. public static function encodeBase64(
  589. $str,
  590. $lineLength = self::LINELENGTH,
  591. $lineEnd = self::LINEEND
  592. ) {
  593. $lineLength = $lineLength - ($lineLength % 4);
  594. return rtrim(chunk_split(base64_encode($str), $lineLength, $lineEnd));
  595. }
  596. /**
  597. * Constructor
  598. *
  599. * @param null|string $boundary
  600. * @access public
  601. */
  602. public function __construct($boundary = null)
  603. {
  604. // This string needs to be somewhat unique
  605. if ($boundary === null) {
  606. $this->boundary = '=_' . md5(microtime(1) . static::$makeUnique++);
  607. } else {
  608. $this->boundary = $boundary;
  609. }
  610. }
  611. // phpcs:disable WebimpressCodingStandard.NamingConventions.ValidVariableName.NotCamelCaps
  612. /**
  613. * Encode the given string with the given encoding.
  614. *
  615. * @param string $str
  616. * @param string $encoding
  617. * @param string $EOL EOL string; defaults to {@link LINEEND}
  618. * @return string
  619. */
  620. public static function encode($str, $encoding, $EOL = self::LINEEND)
  621. {
  622. switch ($encoding) {
  623. case self::ENCODING_BASE64:
  624. return static::encodeBase64($str, self::LINELENGTH, $EOL);
  625. case self::ENCODING_QUOTEDPRINTABLE:
  626. return static::encodeQuotedPrintable($str, self::LINELENGTH, $EOL);
  627. default:
  628. /**
  629. * @todo 7Bit and 8Bit is currently handled the same way.
  630. */
  631. return $str;
  632. }
  633. }
  634. /**
  635. * Return a MIME boundary
  636. *
  637. * @access public
  638. * @return string
  639. */
  640. public function boundary()
  641. {
  642. return $this->boundary;
  643. }
  644. /**
  645. * Return a MIME boundary line
  646. *
  647. * @param string $EOL Defaults to {@link LINEEND}
  648. * @access public
  649. * @return string
  650. */
  651. public function boundaryLine($EOL = self::LINEEND)
  652. {
  653. return $EOL . '--' . $this->boundary . $EOL;
  654. }
  655. /**
  656. * Return MIME ending
  657. *
  658. * @param string $EOL Defaults to {@link LINEEND}
  659. * @access public
  660. * @return string
  661. */
  662. public function mimeEnd($EOL = self::LINEEND)
  663. {
  664. return $EOL . '--' . $this->boundary . '--' . $EOL;
  665. }
  666. /**
  667. * Detect MIME charset
  668. *
  669. * Extract parts according to https://tools.ietf.org/html/rfc2047#section-2
  670. *
  671. * @param string $str
  672. * @return string
  673. */
  674. public static function mimeDetectCharset($str)
  675. {
  676. if (preg_match(self::CHARSET_REGEX, $str, $matches)) {
  677. return strtoupper($matches['charset']);
  678. }
  679. return 'ASCII';
  680. }
  681. }