SmtpTransport.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  11. * @link https://cakephp.org CakePHP(tm) Project
  12. * @since 2.0.0
  13. * @license https://opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Mailer\Transport;
  16. use Cake\Mailer\AbstractTransport;
  17. use Cake\Mailer\Email;
  18. use Cake\Network\Exception\SocketException;
  19. use Cake\Network\Socket;
  20. use Exception;
  21. /**
  22. * Send mail using SMTP protocol
  23. */
  24. class SmtpTransport extends AbstractTransport
  25. {
  26. /**
  27. * Default config for this class
  28. *
  29. * @var array
  30. */
  31. protected $_defaultConfig = [
  32. 'host' => 'localhost',
  33. 'port' => 25,
  34. 'timeout' => 30,
  35. 'username' => null,
  36. 'password' => null,
  37. 'client' => null,
  38. 'tls' => false,
  39. 'keepAlive' => false,
  40. ];
  41. /**
  42. * Socket to SMTP server
  43. *
  44. * @var \Cake\Network\Socket|null
  45. */
  46. protected $_socket;
  47. /**
  48. * Content of email to return
  49. *
  50. * @var array
  51. */
  52. protected $_content = [];
  53. /**
  54. * The response of the last sent SMTP command.
  55. *
  56. * @var array
  57. */
  58. protected $_lastResponse = [];
  59. /**
  60. * Destructor
  61. *
  62. * Tries to disconnect to ensure that the connection is being
  63. * terminated properly before the socket gets closed.
  64. */
  65. public function __destruct()
  66. {
  67. try {
  68. $this->disconnect();
  69. } catch (Exception $e) {
  70. // avoid fatal error on script termination
  71. }
  72. }
  73. /**
  74. * Unserialize handler.
  75. *
  76. * Ensure that the socket property isn't reinitialized in a broken state.
  77. *
  78. * @return void
  79. */
  80. public function __wakeup()
  81. {
  82. $this->_socket = null;
  83. }
  84. /**
  85. * Connect to the SMTP server.
  86. *
  87. * This method tries to connect only in case there is no open
  88. * connection available already.
  89. *
  90. * @return void
  91. */
  92. public function connect()
  93. {
  94. if (!$this->connected()) {
  95. $this->_connect();
  96. $this->_auth();
  97. }
  98. }
  99. /**
  100. * Check whether an open connection to the SMTP server is available.
  101. *
  102. * @return bool
  103. */
  104. public function connected()
  105. {
  106. return $this->_socket !== null && $this->_socket->connected;
  107. }
  108. /**
  109. * Disconnect from the SMTP server.
  110. *
  111. * This method tries to disconnect only in case there is an open
  112. * connection available.
  113. *
  114. * @return void
  115. */
  116. public function disconnect()
  117. {
  118. if ($this->connected()) {
  119. $this->_disconnect();
  120. }
  121. }
  122. /**
  123. * Returns the response of the last sent SMTP command.
  124. *
  125. * A response consists of one or more lines containing a response
  126. * code and an optional response message text:
  127. * ```
  128. * [
  129. * [
  130. * 'code' => '250',
  131. * 'message' => 'mail.example.com'
  132. * ],
  133. * [
  134. * 'code' => '250',
  135. * 'message' => 'PIPELINING'
  136. * ],
  137. * [
  138. * 'code' => '250',
  139. * 'message' => '8BITMIME'
  140. * ],
  141. * // etc...
  142. * ]
  143. * ```
  144. *
  145. * @return array
  146. */
  147. public function getLastResponse()
  148. {
  149. return $this->_lastResponse;
  150. }
  151. /**
  152. * Send mail
  153. *
  154. * @param \Cake\Mailer\Email $email Email instance
  155. * @return array
  156. * @throws \Cake\Network\Exception\SocketException
  157. */
  158. public function send(Email $email)
  159. {
  160. if (!$this->connected()) {
  161. $this->_connect();
  162. $this->_auth();
  163. } else {
  164. $this->_smtpSend('RSET');
  165. }
  166. $this->_sendRcpt($email);
  167. $this->_sendData($email);
  168. if (!$this->_config['keepAlive']) {
  169. $this->_disconnect();
  170. }
  171. return $this->_content;
  172. }
  173. /**
  174. * Parses and stores the response lines in `'code' => 'message'` format.
  175. *
  176. * @param string[] $responseLines Response lines to parse.
  177. * @return void
  178. */
  179. protected function _bufferResponseLines(array $responseLines)
  180. {
  181. $response = [];
  182. foreach ($responseLines as $responseLine) {
  183. if (preg_match('/^(\d{3})(?:[ -]+(.*))?$/', $responseLine, $match)) {
  184. $response[] = [
  185. 'code' => $match[1],
  186. 'message' => isset($match[2]) ? $match[2] : null,
  187. ];
  188. }
  189. }
  190. $this->_lastResponse = array_merge($this->_lastResponse, $response);
  191. }
  192. /**
  193. * Connect to SMTP Server
  194. *
  195. * @return void
  196. * @throws \Cake\Network\Exception\SocketException
  197. */
  198. protected function _connect()
  199. {
  200. $this->_generateSocket();
  201. if (!$this->_socket->connect()) {
  202. throw new SocketException('Unable to connect to SMTP server.');
  203. }
  204. $this->_smtpSend(null, '220');
  205. $config = $this->_config;
  206. if (isset($config['client'])) {
  207. $host = $config['client'];
  208. } elseif ($httpHost = env('HTTP_HOST')) {
  209. list($host) = explode(':', $httpHost);
  210. } else {
  211. $host = 'localhost';
  212. }
  213. try {
  214. $this->_smtpSend("EHLO {$host}", '250');
  215. if ($config['tls']) {
  216. $this->_smtpSend('STARTTLS', '220');
  217. $this->_socket->enableCrypto('tls');
  218. $this->_smtpSend("EHLO {$host}", '250');
  219. }
  220. } catch (SocketException $e) {
  221. if ($config['tls']) {
  222. throw new SocketException('SMTP server did not accept the connection or trying to connect to non TLS SMTP server using TLS.', null, $e);
  223. }
  224. try {
  225. $this->_smtpSend("HELO {$host}", '250');
  226. } catch (SocketException $e2) {
  227. throw new SocketException('SMTP server did not accept the connection.', null, $e2);
  228. }
  229. }
  230. }
  231. /**
  232. * Send authentication
  233. *
  234. * @return void
  235. * @throws \Cake\Network\Exception\SocketException
  236. */
  237. protected function _auth()
  238. {
  239. if (!isset($this->_config['username'], $this->_config['password'])) {
  240. return;
  241. }
  242. $username = $this->_config['username'];
  243. $password = $this->_config['password'];
  244. $replyCode = $this->_authPlain($username, $password);
  245. if ($replyCode === '235') {
  246. return;
  247. }
  248. $this->_authLogin($username, $password);
  249. }
  250. /**
  251. * Authenticate using AUTH PLAIN mechanism.
  252. *
  253. * @param string $username Username.
  254. * @param string $password Password.
  255. * @return string|null Response code for the command.
  256. */
  257. protected function _authPlain($username, $password)
  258. {
  259. return $this->_smtpSend(
  260. sprintf(
  261. 'AUTH PLAIN %s',
  262. base64_encode(chr(0) . $username . chr(0) . $password)
  263. ),
  264. '235|504|534|535'
  265. );
  266. }
  267. /**
  268. * Authenticate using AUTH LOGIN mechanism.
  269. *
  270. * @param string $username Username.
  271. * @param string $password Password.
  272. * @return void
  273. */
  274. protected function _authLogin($username, $password)
  275. {
  276. $replyCode = $this->_smtpSend('AUTH LOGIN', '334|500|502|504');
  277. if ($replyCode === '334') {
  278. try {
  279. $this->_smtpSend(base64_encode($username), '334');
  280. } catch (SocketException $e) {
  281. throw new SocketException('SMTP server did not accept the username.', null, $e);
  282. }
  283. try {
  284. $this->_smtpSend(base64_encode($password), '235');
  285. } catch (SocketException $e) {
  286. throw new SocketException('SMTP server did not accept the password.', null, $e);
  287. }
  288. } elseif ($replyCode === '504') {
  289. throw new SocketException('SMTP authentication method not allowed, check if SMTP server requires TLS.');
  290. } else {
  291. throw new SocketException(
  292. 'AUTH command not recognized or not implemented, SMTP server may not require authentication.'
  293. );
  294. }
  295. }
  296. /**
  297. * Prepares the `MAIL FROM` SMTP command.
  298. *
  299. * @param string $email The email address to send with the command.
  300. * @return string
  301. */
  302. protected function _prepareFromCmd($email)
  303. {
  304. return 'MAIL FROM:<' . $email . '>';
  305. }
  306. /**
  307. * Prepares the `RCPT TO` SMTP command.
  308. *
  309. * @param string $email The email address to send with the command.
  310. * @return string
  311. */
  312. protected function _prepareRcptCmd($email)
  313. {
  314. return 'RCPT TO:<' . $email . '>';
  315. }
  316. /**
  317. * Prepares the `from` email address.
  318. *
  319. * @param \Cake\Mailer\Email $email Email instance
  320. * @return array
  321. */
  322. protected function _prepareFromAddress($email)
  323. {
  324. $from = $email->getReturnPath();
  325. if (empty($from)) {
  326. $from = $email->getFrom();
  327. }
  328. return $from;
  329. }
  330. /**
  331. * Prepares the recipient email addresses.
  332. *
  333. * @param \Cake\Mailer\Email $email Email instance
  334. * @return array
  335. */
  336. protected function _prepareRecipientAddresses($email)
  337. {
  338. $to = $email->getTo();
  339. $cc = $email->getCc();
  340. $bcc = $email->getBcc();
  341. return array_merge(array_keys($to), array_keys($cc), array_keys($bcc));
  342. }
  343. /**
  344. * Prepares the message headers.
  345. *
  346. * @param \Cake\Mailer\Email $email Email instance
  347. * @return array
  348. */
  349. protected function _prepareMessageHeaders($email)
  350. {
  351. return $email->getHeaders(['from', 'sender', 'replyTo', 'readReceipt', 'to', 'cc', 'subject', 'returnPath']);
  352. }
  353. /**
  354. * Prepares the message body.
  355. *
  356. * @param \Cake\Mailer\Email $email Email instance
  357. * @return string
  358. */
  359. protected function _prepareMessage($email)
  360. {
  361. $lines = $email->message();
  362. $messages = [];
  363. foreach ($lines as $line) {
  364. if (!empty($line) && ($line[0] === '.')) {
  365. $messages[] = '.' . $line;
  366. } else {
  367. $messages[] = $line;
  368. }
  369. }
  370. return implode("\r\n", $messages);
  371. }
  372. /**
  373. * Send emails
  374. *
  375. * @return void
  376. * @param \Cake\Mailer\Email $email Cake Email
  377. * @throws \Cake\Network\Exception\SocketException
  378. */
  379. protected function _sendRcpt($email)
  380. {
  381. $from = $this->_prepareFromAddress($email);
  382. $this->_smtpSend($this->_prepareFromCmd(key($from)));
  383. $emails = $this->_prepareRecipientAddresses($email);
  384. foreach ($emails as $mail) {
  385. $this->_smtpSend($this->_prepareRcptCmd($mail));
  386. }
  387. }
  388. /**
  389. * Send Data
  390. *
  391. * @param \Cake\Mailer\Email $email Email instance
  392. * @return void
  393. * @throws \Cake\Network\Exception\SocketException
  394. */
  395. protected function _sendData($email)
  396. {
  397. $this->_smtpSend('DATA', '354');
  398. $headers = $this->_headersToString($this->_prepareMessageHeaders($email));
  399. $message = $this->_prepareMessage($email);
  400. $this->_smtpSend($headers . "\r\n\r\n" . $message . "\r\n\r\n\r\n.");
  401. $this->_content = ['headers' => $headers, 'message' => $message];
  402. }
  403. /**
  404. * Disconnect
  405. *
  406. * @return void
  407. * @throws \Cake\Network\Exception\SocketException
  408. */
  409. protected function _disconnect()
  410. {
  411. $this->_smtpSend('QUIT', false);
  412. $this->_socket->disconnect();
  413. }
  414. /**
  415. * Helper method to generate socket
  416. *
  417. * @return void
  418. * @throws \Cake\Network\Exception\SocketException
  419. */
  420. protected function _generateSocket()
  421. {
  422. $this->_socket = new Socket($this->_config);
  423. }
  424. /**
  425. * Protected method for sending data to SMTP connection
  426. *
  427. * @param string|null $data Data to be sent to SMTP server
  428. * @param string|false $checkCode Code to check for in server response, false to skip
  429. * @return string|null The matched code, or null if nothing matched
  430. * @throws \Cake\Network\Exception\SocketException
  431. */
  432. protected function _smtpSend($data, $checkCode = '250')
  433. {
  434. $this->_lastResponse = [];
  435. if ($data !== null) {
  436. $this->_socket->write($data . "\r\n");
  437. }
  438. $timeout = $this->_config['timeout'];
  439. while ($checkCode !== false) {
  440. $response = '';
  441. $startTime = time();
  442. while (substr($response, -2) !== "\r\n" && ((time() - $startTime) < $timeout)) {
  443. $bytes = $this->_socket->read();
  444. if ($bytes === false || $bytes === null) {
  445. break;
  446. }
  447. $response .= $bytes;
  448. }
  449. if (substr($response, -2) !== "\r\n") {
  450. throw new SocketException('SMTP timeout.');
  451. }
  452. $responseLines = explode("\r\n", rtrim($response, "\r\n"));
  453. $response = end($responseLines);
  454. $this->_bufferResponseLines($responseLines);
  455. if (preg_match('/^(' . $checkCode . ')(.)/', $response, $code)) {
  456. if ($code[2] === '-') {
  457. continue;
  458. }
  459. return $code[1];
  460. }
  461. throw new SocketException(sprintf('SMTP Error: %s', $response));
  462. }
  463. }
  464. }