PageRenderTime 27ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/modules/mail/lib/smtp.php

https://gitlab.com/alexprowars/bitrix
PHP | 512 lines | 372 code | 91 blank | 49 comment | 52 complexity | 31253c22a00e5ad8cbb5dd5ad4ad7b8a MD5 | raw file
  1. <?php
  2. namespace Bitrix\Mail;
  3. use Bitrix\Main;
  4. use Bitrix\Main\Text\BinaryString;
  5. use Bitrix\Main\Text\Encoding;
  6. use Bitrix\Main\Localization\Loc;
  7. Loc::loadMessages(__FILE__);
  8. class Smtp
  9. {
  10. const ERR_CONNECT = 101;
  11. const ERR_REJECTED = 102;
  12. const ERR_COMMUNICATE = 103;
  13. const ERR_EMPTY_RESPONSE = 104;
  14. const ERR_STARTTLS = 201;
  15. const ERR_COMMAND_REJECTED = 202;
  16. const ERR_CAPABILITY = 203;
  17. const ERR_AUTH = 204;
  18. const ERR_AUTH_MECH = 205;
  19. protected $stream, $errors;
  20. protected $sessCapability;
  21. protected $options = array();
  22. /**
  23. * Smtp client constructor.
  24. *
  25. * @param string $host Host.
  26. * @param string $port Port.
  27. * @param string $tls Tls.
  28. * @param string $strict Strict.
  29. * @param string $login Login.
  30. * @param string $password Password.
  31. * @param string|null $encoding. If null - current site encoding.
  32. */
  33. public function __construct($host, $port, $tls, $strict, $login, $password, $encoding = null)
  34. {
  35. $this->reset();
  36. $this->options = array(
  37. 'host' => $host,
  38. 'port' => $port,
  39. 'tls' => $tls,
  40. 'socket' => sprintf('%s://%s:%s', ($tls ? 'ssl' : 'tcp'), $host, $port),
  41. 'timeout' => \COption::getOptionInt('mail', 'connect_timeout', B_MAIL_TIMEOUT),
  42. 'context' => stream_context_create(array(
  43. 'ssl' => array(
  44. 'verify_peer' => (bool) $strict,
  45. 'verify_peer_name' => (bool) $strict,
  46. 'crypto_method' => STREAM_CRYPTO_METHOD_ANY_CLIENT,
  47. )
  48. )),
  49. 'login' => $login,
  50. 'password' => $password,
  51. 'encoding' => $encoding ?: LANG_CHARSET,
  52. );
  53. }
  54. /**
  55. * Disconnects from the submission server.
  56. *
  57. * @return void
  58. */
  59. public function __destruct()
  60. {
  61. $this->disconnect();
  62. }
  63. /**
  64. * Disconnects from the submission server.
  65. *
  66. * @return void
  67. */
  68. protected function disconnect()
  69. {
  70. if (!is_null($this->stream))
  71. {
  72. @fclose($this->stream);
  73. }
  74. unset($this->stream);
  75. }
  76. protected function reset()
  77. {
  78. $this->disconnect();
  79. $this->errors = new Main\ErrorCollection();
  80. }
  81. /**
  82. * Connect to the submission server.
  83. *
  84. * @param array $error Will be filled with connection errors.
  85. * @return bool True if the connection was successful, false - otherwise.
  86. */
  87. public function connect(&$error)
  88. {
  89. $error = null;
  90. if ($this->stream)
  91. {
  92. return true;
  93. }
  94. $resource = @stream_socket_client(
  95. $this->options['socket'], $errno, $errstr, $this->options['timeout'],
  96. STREAM_CLIENT_CONNECT, $this->options['context']
  97. );
  98. if ($resource === false)
  99. {
  100. $error = $this->errorMessage(Smtp::ERR_CONNECT, $errno ?: null);
  101. return false;
  102. }
  103. $this->stream = $resource;
  104. if ($this->options['timeout'] > 0)
  105. {
  106. stream_set_timeout($this->stream, $this->options['timeout']);
  107. }
  108. $prompt = $this->readResponse();
  109. if (false === $prompt)
  110. {
  111. $error = $this->errorMessage(array(Smtp::ERR_CONNECT, Smtp::ERR_COMMUNICATE));
  112. }
  113. else if (!preg_match('/^ 220 ( \r\n | \x20 ) /x', end($prompt)))
  114. {
  115. $error = $this->errorMessage(array(Smtp::ERR_CONNECT, Smtp::ERR_REJECTED), trim(end($prompt)));
  116. }
  117. if ($error)
  118. {
  119. return false;
  120. }
  121. if (!$this->capability($error))
  122. {
  123. return false;
  124. }
  125. if (!$this->options['tls'] && preg_grep('/^ STARTTLS $/ix', $this->sessCapability))
  126. {
  127. if (!$this->starttls($error))
  128. {
  129. return false;
  130. }
  131. }
  132. return true;
  133. }
  134. protected function starttls(&$error)
  135. {
  136. $error = null;
  137. if (!$this->stream)
  138. {
  139. $error = $this->errorMessage(Smtp::ERR_STARTTLS);
  140. return false;
  141. }
  142. $response = $this->executeCommand('STARTTLS', $error);
  143. if ($error)
  144. {
  145. $error = $error == Smtp::ERR_COMMAND_REJECTED ? null : $error;
  146. $error = $this->errorMessage(array(Smtp::ERR_STARTTLS, $error), $response ? trim(end($response)) : null);
  147. return false;
  148. }
  149. if (stream_socket_enable_crypto($this->stream, true, STREAM_CRYPTO_METHOD_ANY_CLIENT))
  150. {
  151. if (!$this->capability($error))
  152. {
  153. return false;
  154. }
  155. }
  156. else
  157. {
  158. $this->reset();
  159. $error = $this->errorMessage(Smtp::ERR_STARTTLS);
  160. return false;
  161. }
  162. return true;
  163. }
  164. protected function capability(&$error)
  165. {
  166. $error = null;
  167. if (!$this->stream)
  168. {
  169. $error = $this->errorMessage(Smtp::ERR_CAPABILITY);
  170. return false;
  171. }
  172. $response = $this->executeCommand(
  173. sprintf(
  174. 'EHLO %s',
  175. Main\Context::getCurrent()->getRequest()->getHttpHost() ?: 'localhost'
  176. ),
  177. $error
  178. );
  179. if ($error || !is_array($response))
  180. {
  181. $error = $error == Smtp::ERR_COMMAND_REJECTED ? null : $error;
  182. $error = $this->errorMessage(array(Smtp::ERR_CAPABILITY, $error), $response ? trim(end($response)) : null);
  183. return false;
  184. }
  185. $this->sessCapability = array_map(
  186. function ($line)
  187. {
  188. return trim(mb_substr($line, 4));
  189. },
  190. $response
  191. );
  192. return true;
  193. }
  194. /**
  195. * Authenticate to the submission server.
  196. *
  197. * @param array $error Will be filled with authentication errors.
  198. * @return bool True if the authentication was successful, false - otherwise.
  199. */
  200. public function authenticate(&$error)
  201. {
  202. $error = null;
  203. if (!$this->connect($error))
  204. {
  205. return false;
  206. }
  207. $mech = false;
  208. if ($capabilities = preg_grep('/^ AUTH \x20 /ix', $this->sessCapability))
  209. {
  210. if (preg_grep('/ \x20 PLAIN ( \x20 | $ ) /ix', $capabilities))
  211. {
  212. $mech = 'plain';
  213. }
  214. else if (preg_grep('/ \x20 LOGIN ( \x20 | $ ) /ix', $capabilities))
  215. {
  216. $mech = 'login';
  217. }
  218. }
  219. if (!$mech)
  220. {
  221. $error = $this->errorMessage(array(Smtp::ERR_AUTH, Smtp::ERR_AUTH_MECH));
  222. return false;
  223. }
  224. if ($mech == 'plain')
  225. {
  226. $response = $this->executeCommand(
  227. sprintf(
  228. "AUTH PLAIN\x00%s",
  229. base64_encode(sprintf(
  230. "\x00%s\x00%s",
  231. Encoding::convertEncoding($this->options['login'], $this->options['encoding'], 'UTF-8'),
  232. Encoding::convertEncoding($this->options['password'], $this->options['encoding'], 'UTF-8')
  233. ))
  234. ),
  235. $error
  236. );
  237. }
  238. else
  239. {
  240. $response = $this->executeCommand(sprintf(
  241. "AUTH LOGIN\x00%s\x00%s",
  242. base64_encode($this->options['login']),
  243. base64_encode($this->options['password'])
  244. ), $error);
  245. }
  246. if ($error)
  247. {
  248. $error = $error == Smtp::ERR_COMMAND_REJECTED ? null : $error;
  249. $error = $this->errorMessage(array(Smtp::ERR_AUTH, $error), $response ? trim(end($response)) : null);
  250. return false;
  251. }
  252. return true;
  253. }
  254. protected function executeCommand($command, &$error)
  255. {
  256. $error = null;
  257. $response = false;
  258. $chunks = explode("\x00", $command);
  259. $k = count($chunks);
  260. foreach ($chunks as $chunk)
  261. {
  262. $k--;
  263. $response = (array) $this->exchange($chunk, $error);
  264. if ($k > 0 && mb_strpos(end($response), '3') !== 0)
  265. {
  266. break;
  267. }
  268. }
  269. return $response;
  270. }
  271. protected function exchange($data, &$error)
  272. {
  273. $error = null;
  274. if ($this->sendData(sprintf("%s\r\n", $data)) === false)
  275. {
  276. $error = Smtp::ERR_COMMUNICATE;
  277. return false;
  278. }
  279. $response = $this->readResponse();
  280. if ($response === false)
  281. {
  282. $error = Smtp::ERR_COMMUNICATE;
  283. return false;
  284. }
  285. if (!preg_match('/^ [23] \d{2} /ix', end($response)))
  286. {
  287. $error = Smtp::ERR_COMMAND_REJECTED;
  288. }
  289. return $response;
  290. }
  291. protected function sendData($data)
  292. {
  293. $fails = 0;
  294. while (BinaryString::getLength($data) > 0 && !feof($this->stream))
  295. {
  296. $bytes = @fputs($this->stream, $data);
  297. if (false == $bytes)
  298. {
  299. if (false === $bytes || ++$fails >= 3)
  300. {
  301. break;
  302. }
  303. continue;
  304. }
  305. $fails = 0;
  306. $data = BinaryString::getSubstring($data, $bytes);
  307. }
  308. if (BinaryString::getLength($data) > 0)
  309. {
  310. $this->reset();
  311. return false;
  312. }
  313. return true;
  314. }
  315. protected function readLine()
  316. {
  317. $line = '';
  318. while (!feof($this->stream))
  319. {
  320. $buffer = @fgets($this->stream, 4096);
  321. if ($buffer === false)
  322. {
  323. break;
  324. }
  325. $meta = ($this->options['timeout'] > 0 ? stream_get_meta_data($this->stream) : array('timed_out' => false));
  326. $line .= $buffer;
  327. if (preg_match('/\r\n$/', $buffer, $matches) || $meta['timed_out'])
  328. {
  329. break;
  330. }
  331. }
  332. if (!preg_match('/\r\n$/', $line, $matches))
  333. {
  334. $this->reset();
  335. return false;
  336. }
  337. return $line;
  338. }
  339. /**
  340. * Reads and returns server response.
  341. *
  342. * @return array|false
  343. */
  344. protected function readResponse()
  345. {
  346. $response = array();
  347. do
  348. {
  349. $line = $this->readLine();
  350. if ($line === false)
  351. {
  352. return false;
  353. }
  354. $response[] = $line;
  355. }
  356. while (!preg_match('/^ \d{3} ( \r\n | \x20 ) /x', $line));
  357. return $response;
  358. }
  359. protected function errorMessage($errors, $details = null)
  360. {
  361. $errors = array_filter((array) $errors);
  362. $details = array_filter((array) $details);
  363. foreach ($errors as $i => $error)
  364. {
  365. $errors[$i] = static::decodeError($error);
  366. $this->errors->setError(new Main\Error((string) $errors[$i], $error > 0 ? $error : 0));
  367. }
  368. $error = join(': ', $errors);
  369. if ($details)
  370. {
  371. $error .= sprintf(' (SMTP: %s)', join(': ', $details));
  372. $this->errors->setError(new Main\Error('SMTP', -1));
  373. foreach ($details as $item)
  374. {
  375. $this->errors->setError(new Main\Error((string) $item, -1));
  376. }
  377. }
  378. return $error;
  379. }
  380. /**
  381. * Returns all Smtp client errors.
  382. *
  383. * @return Main\ErrorCollection object.
  384. */
  385. public function getErrors()
  386. {
  387. return $this->errors;
  388. }
  389. /**
  390. * Returns error message by code.
  391. *
  392. * @param int $code Error code.
  393. * @return string
  394. */
  395. public static function decodeError($code)
  396. {
  397. switch ($code)
  398. {
  399. case self::ERR_CONNECT:
  400. return Loc::getMessage('MAIL_SMTP_ERR_CONNECT');
  401. case self::ERR_REJECTED:
  402. return Loc::getMessage('MAIL_SMTP_ERR_REJECTED');
  403. case self::ERR_COMMUNICATE:
  404. return Loc::getMessage('MAIL_SMTP_ERR_COMMUNICATE');
  405. case self::ERR_EMPTY_RESPONSE:
  406. return Loc::getMessage('MAIL_SMTP_ERR_EMPTY_RESPONSE');
  407. case self::ERR_STARTTLS:
  408. return Loc::getMessage('MAIL_SMTP_ERR_STARTTLS');
  409. case self::ERR_COMMAND_REJECTED:
  410. return Loc::getMessage('MAIL_SMTP_ERR_COMMAND_REJECTED');
  411. case self::ERR_CAPABILITY:
  412. return Loc::getMessage('MAIL_SMTP_ERR_CAPABILITY');
  413. case self::ERR_AUTH:
  414. return Loc::getMessage('MAIL_SMTP_ERR_AUTH');
  415. case self::ERR_AUTH_MECH:
  416. return Loc::getMessage('MAIL_SMTP_ERR_AUTH_MECH');
  417. default:
  418. return Loc::getMessage('MAIL_SMTP_ERR_DEFAULT');
  419. }
  420. }
  421. }