PageRenderTime 47ms CodeModel.GetById 13ms RepoModel.GetById 1ms app.codeStats 0ms

/Jyxo/Mail/Sender.php

http://github.com/jyxo/php
PHP | 944 lines | 470 code | 152 blank | 322 comment | 36 complexity | 3d4ebcb67681f7f3e2781a75d5a27146 MD5 | raw file
  1. <?php declare(strict_types = 1);
  2. /**
  3. * Jyxo PHP Library
  4. *
  5. * LICENSE
  6. *
  7. * This source file is subject to the new BSD license that is bundled
  8. * with this package in the file license.txt.
  9. * It is also available through the world-wide-web at this URL:
  10. * https://github.com/jyxo/php/blob/master/license.txt
  11. */
  12. namespace Jyxo\Mail;
  13. use ArrayIterator;
  14. use InvalidArgumentException;
  15. use Jyxo\Mail\Email\Address;
  16. use Jyxo\Mail\Sender\RecipientUnknownException;
  17. use Jyxo\Mail\Sender\Result;
  18. use Jyxo\Mail\Sender\Smtp;
  19. use Jyxo\Mail\Sender\SmtpException;
  20. use Jyxo\StringUtil;
  21. use Jyxo\Time\Time;
  22. use function array_merge;
  23. use function class_exists;
  24. use function count;
  25. use function current;
  26. use function iconv;
  27. use function implode;
  28. use function in_array;
  29. use function mail;
  30. use function md5;
  31. use function next;
  32. use function preg_match;
  33. use function preg_match_all;
  34. use function preg_replace;
  35. use function sprintf;
  36. use function strlen;
  37. use function strtr;
  38. use function time;
  39. use function trim;
  40. use function uniqid;
  41. /**
  42. * Class for sending emails.
  43. * Based on PhpMailer class (C) Copyright 2001-2003 Brent R. Matzelle
  44. *
  45. * @copyright Copyright (c) 2005-2011 Jyxo, s.r.o.
  46. * @license https://github.com/jyxo/php/blob/master/license.txt
  47. * @author Jaroslav HanslĂ­k
  48. */
  49. class Sender
  50. {
  51. /**
  52. * Send using the internal mail() function.
  53. */
  54. public const MODE_MAIL = 'mail';
  55. /**
  56. * Send using a SMTP server.
  57. */
  58. public const MODE_SMTP = 'smtp';
  59. /**
  60. * No sending.
  61. * Useful if we actually don't want to send the message but just generate it.
  62. */
  63. public const MODE_NONE = 'none';
  64. /**
  65. * Maximum line length.
  66. */
  67. public const LINE_LENGTH = 74;
  68. /**
  69. * Line ending.
  70. */
  71. public const LINE_END = "\n";
  72. /**
  73. * Simple mail type.
  74. */
  75. public const TYPE_SIMPLE = 'simple';
  76. /**
  77. * Email with a HTML and plaintext part.
  78. */
  79. public const TYPE_ALTERNATIVE = 'alternative';
  80. /**
  81. * Email with a HTML and plaintext part and attachments.
  82. */
  83. public const TYPE_ALTERNATIVE_ATTACHMENTS = 'alternative_attachments';
  84. /**
  85. * Email with attachments.
  86. */
  87. public const TYPE_ATTACHMENTS = 'attachments';
  88. /**
  89. * Charset.
  90. *
  91. * @var string
  92. */
  93. private $charset = 'iso-8859-2';
  94. /**
  95. * Hostname.
  96. *
  97. * @var string
  98. */
  99. private $hostname = '';
  100. /**
  101. * X-Mailer header value.
  102. *
  103. * @var string
  104. */
  105. private $xmailer = '';
  106. /**
  107. * Mail encoding (8bit, 7bit, binary, base64, quoted-printable).
  108. *
  109. * @var string
  110. */
  111. private $encoding = Encoding::QUOTED_PRINTABLE;
  112. /**
  113. * Email instance to be sent.
  114. *
  115. * @var Email
  116. */
  117. private $email = null;
  118. /**
  119. * SMTP server.
  120. *
  121. * @var string
  122. */
  123. private $smtpHost = 'localhost';
  124. /**
  125. * SMTP port.
  126. *
  127. * @var int
  128. */
  129. private $smtpPort = 25;
  130. /**
  131. * SMTP HELO value.
  132. *
  133. * @var string
  134. */
  135. private $smtpHelo = '';
  136. /**
  137. * SMTP username.
  138. *
  139. * @var string
  140. */
  141. private $smtpUser = '';
  142. /**
  143. * SMTP password.
  144. *
  145. * @var string
  146. */
  147. private $smtpPsw = '';
  148. /**
  149. * SMTP connection timeout.
  150. *
  151. * @var string
  152. */
  153. private $smtpTimeout = 5;
  154. /**
  155. * Sending result.
  156. *
  157. * @var Result
  158. */
  159. private $result = null;
  160. /**
  161. * Generated boundaries of mail parts.
  162. *
  163. * @var array
  164. */
  165. private $boundary = [];
  166. /**
  167. * Sending mode.
  168. *
  169. * @var int
  170. */
  171. private $mode = self::MODE_MAIL;
  172. /**
  173. * Email type.
  174. *
  175. * @var string
  176. */
  177. private $type = self::TYPE_SIMPLE;
  178. /**
  179. * Generated email headers.
  180. *
  181. * @var array
  182. */
  183. private $createdHeader = [];
  184. /**
  185. * Generated email body.
  186. *
  187. * @var string
  188. */
  189. private $createdBody = '';
  190. /**
  191. * Returns charset.
  192. *
  193. * @return string
  194. */
  195. public function getCharset(): string
  196. {
  197. return $this->charset;
  198. }
  199. /**
  200. * Sets charset.
  201. *
  202. * @param string $charset Final charset
  203. * @return Sender
  204. */
  205. public function setCharset(string $charset): self
  206. {
  207. $this->charset = $charset;
  208. return $this;
  209. }
  210. /**
  211. * Returns hostname.
  212. *
  213. * @return string
  214. */
  215. public function getHostname(): string
  216. {
  217. if (empty($this->hostname)) {
  218. $this->hostname = $_SERVER['HTTP_HOST'] ?? 'localhost';
  219. }
  220. return $this->hostname;
  221. }
  222. /**
  223. * Sets hostname.
  224. *
  225. * @param string $hostname Hostname
  226. * @return Sender
  227. */
  228. public function setHostname(string $hostname): self
  229. {
  230. $this->hostname = $hostname;
  231. return $this;
  232. }
  233. /**
  234. * Returns X-Mailer header value.
  235. *
  236. * @return string
  237. */
  238. public function getXmailer(): string
  239. {
  240. return $this->xmailer;
  241. }
  242. /**
  243. * Sets X-Mailer header value.
  244. *
  245. * @param string $xmailer X-Mailer header value
  246. * @return Sender
  247. */
  248. public function setXmailer(string $xmailer): self
  249. {
  250. $this->xmailer = $xmailer;
  251. return $this;
  252. }
  253. /**
  254. * Returns encoding.
  255. *
  256. * @return string
  257. */
  258. public function getEncoding(): string
  259. {
  260. return $this->encoding;
  261. }
  262. /**
  263. * Sets encoding.
  264. *
  265. * @param string $encoding Encoding
  266. * @return Sender
  267. */
  268. public function setEncoding(string $encoding): self
  269. {
  270. if (!Encoding::isCompatible($encoding)) {
  271. throw new InvalidArgumentException(sprintf('Incompatible encoding %s.', $encoding));
  272. }
  273. $this->encoding = $encoding;
  274. return $this;
  275. }
  276. /**
  277. * Returns the email to be sent.
  278. *
  279. * @return Email|null
  280. */
  281. public function getEmail(): ?Email
  282. {
  283. return $this->email;
  284. }
  285. /**
  286. * Sets the email to be sent.
  287. *
  288. * @param Email $email Email instance
  289. * @return Sender
  290. */
  291. public function setEmail(Email $email): self
  292. {
  293. $this->email = $email;
  294. return $this;
  295. }
  296. /**
  297. * Sets SMTP parameters.
  298. *
  299. * @param string $host Hostname
  300. * @param int $port Port
  301. * @param string $helo HELO value
  302. * @param string $user Username
  303. * @param string $password Password
  304. * @param int $timeout Connection timeout
  305. * @return Sender
  306. */
  307. public function setSmtp(
  308. string $host,
  309. int $port = 25,
  310. string $helo = '',
  311. string $user = '',
  312. string $password = '',
  313. int $timeout = 5
  314. ): self
  315. {
  316. $this->smtpHost = $host;
  317. $this->smtpPort = $port;
  318. $this->smtpHelo = !empty($helo) ? $helo : $this->getHostname();
  319. $this->smtpUser = $user;
  320. $this->smtpPsw = $password;
  321. $this->smtpTimeout = $timeout;
  322. return $this;
  323. }
  324. /**
  325. * Sends an email using the given mode.
  326. *
  327. * @param string $mode Sending mode
  328. * @return Result
  329. */
  330. public function send(string $mode): Result
  331. {
  332. // Sending modes
  333. static $modes = [
  334. self::MODE_SMTP => true,
  335. self::MODE_MAIL => true,
  336. self::MODE_NONE => true,
  337. ];
  338. if (!isset($modes[$mode])) {
  339. throw new InvalidArgumentException(sprintf('Unknown sending mode %s.', $mode));
  340. }
  341. $this->mode = $mode;
  342. // Check of required parameters
  343. if ($this->email->from === null) {
  344. throw new Sender\CreateException('No sender was set.');
  345. }
  346. if ((count($this->email->to) + count($this->email->cc) + count($this->email->bcc)) < 1) {
  347. throw new Sender\CreateException('No recipient was set.');
  348. }
  349. // Creates a result
  350. $this->result = new Sender\Result();
  351. // Creates an email
  352. $this->create();
  353. $body = trim($this->createdBody);
  354. if (empty($body)) {
  355. throw new Sender\CreateException('No body was created.');
  356. }
  357. // Choose the appropriate sending method
  358. switch ($this->mode) {
  359. case self::MODE_SMTP:
  360. $this->sendBySmtp();
  361. break;
  362. case self::MODE_MAIL:
  363. $this->sendByMail();
  364. break;
  365. case self::MODE_NONE:
  366. // Break missing intentionally
  367. default:
  368. // No sending
  369. break;
  370. }
  371. // Save the generated source code to the result
  372. $this->result->source = $this->getHeader() . $this->createdBody;
  373. // Flush of created email
  374. $this->createdHeader = [];
  375. $this->createdBody = '';
  376. return $this->result;
  377. }
  378. /**
  379. * Sends an email using the mail() function.
  380. */
  381. private function sendByMail(): void
  382. {
  383. $recipients = '';
  384. $iterator = new ArrayIterator($this->email->to);
  385. while ($iterator->valid()) {
  386. $recipients .= $this->formatAddress($iterator->current());
  387. $iterator->next();
  388. if ($iterator->valid()) {
  389. $recipients .= ', ';
  390. }
  391. }
  392. $subject = $this->changeCharset($this->clearHeaderValue($this->email->subject));
  393. if (!mail($recipients, $this->encodeHeader($subject), $this->createdBody, $this->getHeader(['To', 'Subject']))) {
  394. throw new Sender\Exception('Could not send the message.');
  395. }
  396. }
  397. /**
  398. * Sends an email using a SMTP server.
  399. */
  400. private function sendBySmtp(): void
  401. {
  402. if (!class_exists(Smtp::class)) {
  403. throw new Sender\Exception(sprintf('Could not sent the message. Required class %s is missing.', Smtp::class));
  404. }
  405. try {
  406. $smtp = new Sender\Smtp($this->smtpHost, $this->smtpPort, $this->smtpHelo, $this->smtpTimeout);
  407. $smtp->connect();
  408. if (!empty($this->smtpUser)) {
  409. $smtp->auth($this->smtpUser, $this->smtpPsw);
  410. }
  411. // Sender
  412. $smtp->from($this->email->from->email);
  413. // Recipients
  414. $unknownRecipients = [];
  415. foreach (array_merge($this->email->to, $this->email->cc, $this->email->bcc) as $recipient) {
  416. try {
  417. $smtp->recipient($recipient->email);
  418. } catch (SmtpException $e) {
  419. $unknownRecipients[] = $recipient->email;
  420. }
  421. }
  422. if (!empty($unknownRecipients)) {
  423. throw new Sender\RecipientUnknownException('Unknown email recipients.', 0, $unknownRecipients);
  424. }
  425. // Data
  426. $smtp->data($this->getHeader(), $this->createdBody);
  427. $smtp->disconnect();
  428. } catch (RecipientUnknownException $e) {
  429. $smtp->disconnect();
  430. throw $e;
  431. } catch (SmtpException $e) {
  432. $smtp->disconnect();
  433. throw new Sender\Exception('Cannot send email: ' . $e->getMessage());
  434. }
  435. }
  436. /**
  437. * Creates an email.
  438. */
  439. private function create(): void
  440. {
  441. $uniqueId = md5(uniqid((string) time()));
  442. $hostname = $this->clearHeaderValue($this->getHostname());
  443. // Unique email Id
  444. $this->result->messageId = $uniqueId . '@' . $hostname;
  445. // Sending time
  446. $this->result->datetime = Time::now();
  447. // Parts boundaries
  448. $this->boundary = [
  449. 1 => '====b1' . $uniqueId . '====' . $hostname . '====',
  450. 2 => '====b2' . $uniqueId . '====' . $hostname . '====',
  451. ];
  452. // Determine the message type
  453. if (!empty($this->email->attachments)) {
  454. // Are there any attachments?
  455. $this->type = !empty($this->email->body->alternative) ? self::TYPE_ALTERNATIVE_ATTACHMENTS : self::TYPE_ATTACHMENTS;
  456. } else {
  457. // No attachments
  458. $this->type = !empty($this->email->body->alternative) ? self::TYPE_ALTERNATIVE : self::TYPE_SIMPLE;
  459. }
  460. // Creates header and body
  461. $this->createHeader();
  462. $this->createBody();
  463. }
  464. /**
  465. * Creates header.
  466. */
  467. private function createHeader(): void
  468. {
  469. $this->addHeaderLine('Date', $this->result->datetime->email);
  470. $this->addHeaderLine('Return-Path', '<' . $this->clearHeaderValue($this->email->from->email) . '>');
  471. $this->addHeaderLine('From', $this->formatAddress($this->email->from));
  472. $this->addHeaderLine('Subject', $this->encodeHeader($this->changeCharset($this->clearHeaderValue($this->email->subject))));
  473. if (!empty($this->email->to)) {
  474. $this->addHeaderLine('To', $this->formatAddressList($this->email->to));
  475. } elseif (empty($this->email->cc)) {
  476. // Only blind carbon copy recipients
  477. $this->addHeaderLine('To', 'undisclosed-recipients:;');
  478. }
  479. if (!empty($this->email->cc)) {
  480. $this->addHeaderLine('Cc', $this->formatAddressList($this->email->cc));
  481. }
  482. if (!empty($this->email->bcc)) {
  483. $this->addHeaderLine('Bcc', $this->formatAddressList($this->email->bcc));
  484. }
  485. if (!empty($this->email->replyTo)) {
  486. $this->addHeaderLine('Reply-To', $this->formatAddressList($this->email->replyTo));
  487. }
  488. if (!empty($this->email->confirmReadingTo)) {
  489. $this->addHeaderLine('Disposition-Notification-To', $this->formatAddress($this->email->confirmReadingTo));
  490. }
  491. if (!empty($this->email->priority)) {
  492. $this->addHeaderLine('X-Priority', (string) $this->email->priority);
  493. }
  494. $this->addHeaderLine('Message-ID', '<' . $this->result->messageId . '>');
  495. if (!empty($this->email->inReplyTo)) {
  496. $this->addHeaderLine('In-Reply-To', '<' . $this->clearHeaderValue($this->email->inReplyTo) . '>');
  497. }
  498. if (!empty($this->email->references)) {
  499. $references = $this->email->references;
  500. foreach ($references as $key => $reference) {
  501. $references[$key] = $this->clearHeaderValue($reference);
  502. }
  503. $this->addHeaderLine('References', '<' . implode('> <', $references) . '>');
  504. }
  505. if (!empty($this->xmailer)) {
  506. $this->addHeaderLine('X-Mailer', $this->changeCharset($this->clearHeaderValue($this->xmailer)));
  507. }
  508. $this->addHeaderLine('MIME-Version', '1.0');
  509. // Custom headers
  510. foreach ($this->email->headers as $header) {
  511. $this->addHeaderLine(
  512. $this->changeCharset($this->clearHeaderValue($header->name)),
  513. $this->encodeHeader($this->clearHeaderValue($header->value))
  514. );
  515. }
  516. switch ($this->type) {
  517. case self::TYPE_ATTACHMENTS:
  518. // Break missing intentionally
  519. case self::TYPE_ALTERNATIVE_ATTACHMENTS:
  520. $subtype = $this->email->hasInlineAttachments() ? 'related' : 'mixed';
  521. $this->addHeaderLine(
  522. 'Content-Type',
  523. 'multipart/' . $subtype . ';' . self::LINE_END . ' boundary="' . $this->boundary[1] . '"'
  524. );
  525. break;
  526. case self::TYPE_ALTERNATIVE:
  527. $this->addHeaderLine('Content-Type', 'multipart/alternative;' . self::LINE_END . ' boundary="' . $this->boundary[1] . '"');
  528. break;
  529. case self::TYPE_SIMPLE:
  530. // Break missing intentionally
  531. default:
  532. $contentType = $this->email->body->isHtml() ? 'text/html' : 'text/plain';
  533. $this->addHeaderLine('Content-Type', $contentType . '; charset="' . $this->clearHeaderValue($this->charset) . '"');
  534. $this->addHeaderLine('Content-Transfer-Encoding', $this->encoding);
  535. break;
  536. }
  537. }
  538. /**
  539. * Creates body.
  540. */
  541. private function createBody(): void
  542. {
  543. switch ($this->type) {
  544. case self::TYPE_ATTACHMENTS:
  545. $contentType = $this->email->body->isHtml() ? 'text/html' : 'text/plain';
  546. $this->createdBody .= $this->getBoundaryStart(
  547. $this->boundary[1],
  548. $contentType,
  549. $this->charset,
  550. $this->encoding
  551. ) . self::LINE_END;
  552. $this->createdBody .= $this->encodeString($this->changeCharset($this->email->body->main), $this->encoding) . self::LINE_END;
  553. $this->createdBody .= $this->attachAll();
  554. break;
  555. case self::TYPE_ALTERNATIVE_ATTACHMENTS:
  556. $this->createdBody .= '--' . $this->boundary[1] . self::LINE_END;
  557. $this->createdBody .= 'Content-Type: multipart/alternative;' . self::LINE_END . ' boundary="' . $this->boundary[2] . '"' . self::LINE_END . self::LINE_END;
  558. $this->createdBody .= $this->getBoundaryStart(
  559. $this->boundary[2],
  560. 'text/plain',
  561. $this->charset,
  562. $this->encoding
  563. ) . self::LINE_END;
  564. $this->createdBody .= $this->encodeString(
  565. $this->changeCharset($this->email->body->alternative),
  566. $this->encoding
  567. ) . self::LINE_END;
  568. $this->createdBody .= $this->getBoundaryStart(
  569. $this->boundary[2],
  570. 'text/html',
  571. $this->charset,
  572. $this->encoding
  573. ) . self::LINE_END;
  574. $this->createdBody .= $this->encodeString($this->changeCharset($this->email->body->main), $this->encoding) . self::LINE_END;
  575. $this->createdBody .= $this->getBoundaryEnd($this->boundary[2]) . self::LINE_END;
  576. $this->createdBody .= $this->attachAll();
  577. break;
  578. case self::TYPE_ALTERNATIVE:
  579. $this->createdBody .= $this->getBoundaryStart(
  580. $this->boundary[1],
  581. 'text/plain',
  582. $this->charset,
  583. $this->encoding
  584. ) . self::LINE_END;
  585. $this->createdBody .= $this->encodeString(
  586. $this->changeCharset($this->email->body->alternative),
  587. $this->encoding
  588. ) . self::LINE_END;
  589. $this->createdBody .= $this->getBoundaryStart(
  590. $this->boundary[1],
  591. 'text/html',
  592. $this->charset,
  593. $this->encoding
  594. ) . self::LINE_END;
  595. $this->createdBody .= $this->encodeString($this->changeCharset($this->email->body->main), $this->encoding) . self::LINE_END;
  596. $this->createdBody .= $this->getBoundaryEnd($this->boundary[1]);
  597. break;
  598. case self::TYPE_SIMPLE:
  599. // Break missing intentionally
  600. default:
  601. $this->createdBody = $this->encodeString($this->changeCharset($this->email->body->main), $this->encoding);
  602. break;
  603. }
  604. }
  605. /**
  606. * Adds all attachments to the email.
  607. *
  608. * @return string
  609. */
  610. private function attachAll(): string
  611. {
  612. $mime = [];
  613. foreach ($this->email->attachments as $attachment) {
  614. $encoding = !empty($attachment->encoding) ? $attachment->encoding : Encoding::BASE64;
  615. $name = $this->changeCharset($this->clearHeaderValue($attachment->name));
  616. $mime[] = '--' . $this->boundary[1] . self::LINE_END;
  617. $mime[] = 'Content-Type: ' . $this->clearHeaderValue($attachment->mimeType) . ';' . self::LINE_END;
  618. $mime[] = ' name="' . $this->encodeHeader($name) . '"' . self::LINE_END;
  619. $mime[] = 'Content-Transfer-Encoding: ' . $encoding . self::LINE_END;
  620. if ($attachment->isInline()) {
  621. $mime[] = 'Content-ID: <' . $this->clearHeaderValue($attachment->cid) . '>' . self::LINE_END;
  622. }
  623. $mime[] = 'Content-Disposition: ' . $attachment->disposition . ';' . self::LINE_END;
  624. $mime[] = ' filename="' . $this->encodeHeader($name) . '"' . self::LINE_END . self::LINE_END;
  625. // Just fix line endings in case of encoded attachments, encode otherwise
  626. $mime[] = !empty($attachment->encoding)
  627. ? StringUtil::fixLineEnding($attachment->content, self::LINE_END)
  628. : $this->encodeString($attachment->content, $encoding);
  629. $mime[] = self::LINE_END . self::LINE_END;
  630. }
  631. $mime[] = '--' . $this->boundary[1] . '--' . self::LINE_END;
  632. return implode('', $mime);
  633. }
  634. /**
  635. * Returns headers except given lines.
  636. * Various sending methods need some headers removed, because they add them on their own.
  637. *
  638. * @param array $except Headers to be removed
  639. * @return string
  640. */
  641. private function getHeader(array $except = []): string
  642. {
  643. $header = '';
  644. foreach ($this->createdHeader as $headerLine) {
  645. if (!in_array($headerLine['name'], $except, true)) {
  646. $header .= $headerLine['name'] . ': ' . $headerLine['value'] . self::LINE_END;
  647. }
  648. }
  649. return $header . self::LINE_END;
  650. }
  651. /**
  652. * Formats an email address.
  653. *
  654. * @param Address $address Address
  655. * @return string
  656. */
  657. private function formatAddress(Address $address): string
  658. {
  659. $name = $this->changeCharset($this->clearHeaderValue($address->name));
  660. $email = $this->clearHeaderValue($address->email);
  661. // No name is set
  662. if (empty($name) || ($name === $email)) {
  663. return $email;
  664. }
  665. if (preg_match('~[\200-\377]~', $name)) {
  666. // High ascii
  667. $name = $this->encodeHeader($name);
  668. } elseif (preg_match('~[^\\w\\s!#\$%&\'*+/=?^_`{|}\~-]~', $name)) {
  669. // Dangerous characters
  670. $name = '"' . $name . '"';
  671. }
  672. return $name . ' <' . $email . '>';
  673. }
  674. /**
  675. * Formats a list of addresses.
  676. *
  677. * @param array $addressList Array of addresses
  678. * @return string
  679. */
  680. private function formatAddressList(array $addressList): string
  681. {
  682. $formated = '';
  683. while ($address = current($addressList)) {
  684. $formated .= $this->formatAddress($address);
  685. if (next($addressList) !== false) {
  686. $formated .= ', ';
  687. }
  688. }
  689. return $formated;
  690. }
  691. /**
  692. * Adds a header line.
  693. *
  694. * @param string $name Header name
  695. * @param string $value Header value
  696. */
  697. private function addHeaderLine(string $name, string $value): void
  698. {
  699. $this->createdHeader[] = [
  700. 'name' => $name,
  701. 'value' => trim($value),
  702. ];
  703. }
  704. /**
  705. * Encodes headers.
  706. *
  707. * @param string $string Header definition
  708. * @return string
  709. */
  710. private function encodeHeader(string $string): string
  711. {
  712. // There might be dangerous characters in the string
  713. $count = preg_match_all('~[^\040-\176]~', $string, $matches);
  714. if ($count === 0) {
  715. return $string;
  716. }
  717. // 7 is =? + ? + Q/B + ? + ?=
  718. $maxlen = 75 - 7 - strlen($this->charset);
  719. // We have to use base64 always, because Thunderbird has problems with quoted-printable
  720. $encoding = 'B';
  721. $maxlen -= $maxlen % 4;
  722. $encoded = $this->encodeString($string, Encoding::BASE64, $maxlen);
  723. // Splitting to multiple lines
  724. $encoded = trim(preg_replace('~^(.*)$~m', ' =?' . $this->clearHeaderValue($this->charset) . '?' . $encoding . '?\1?=', $encoded));
  725. return $encoded;
  726. }
  727. /**
  728. * Encodes a string using the given encoding.
  729. *
  730. * @param string $string Input string
  731. * @param string $encoding Encoding
  732. * @param int $lineLength Line length
  733. * @return string
  734. */
  735. private function encodeString(string $string, string $encoding, int $lineLength = self::LINE_LENGTH): string
  736. {
  737. return Encoding::encode($string, $encoding, $lineLength, self::LINE_END);
  738. }
  739. /**
  740. * Returns a beginning of an email part.
  741. *
  742. * @param string $boundary Boundary
  743. * @param string $contentType Mime-type
  744. * @param string $charset Charset
  745. * @param string $encoding Encoding
  746. * @return string
  747. */
  748. private function getBoundaryStart(string $boundary, string $contentType, string $charset, string $encoding): string
  749. {
  750. $start = '--' . $boundary . self::LINE_END;
  751. $start .= 'Content-Type: ' . $contentType . '; charset="' . $this->clearHeaderValue($charset) . '"' . self::LINE_END;
  752. $start .= 'Content-Transfer-Encoding: ' . $encoding . self::LINE_END;
  753. return $start;
  754. }
  755. /**
  756. * Returns an end of an email part.
  757. *
  758. * @param string $boundary Boundary
  759. * @return string
  760. */
  761. private function getBoundaryEnd(string $boundary): string
  762. {
  763. return self::LINE_END . '--' . $boundary . '--' . self::LINE_END;
  764. }
  765. /**
  766. * Clears headers from line endings.
  767. *
  768. * @param string $string Headers definition
  769. * @return string
  770. */
  771. private function clearHeaderValue(string $string): string
  772. {
  773. return strtr(trim($string), "\r\n\t", ' ');
  774. }
  775. /**
  776. * Converts a string from UTF-8 into the email encoding.
  777. *
  778. * @param string $string Input string
  779. * @return string
  780. */
  781. private function changeCharset(string $string): string
  782. {
  783. if ($this->charset !== 'utf-8') {
  784. // Triggers a notice on an invalid character
  785. $string = @iconv('utf-8', $this->charset . '//TRANSLIT', $string);
  786. if ($string === false) {
  787. $string = '';
  788. }
  789. }
  790. return $string;
  791. }
  792. }