1C-Bitrix 25.700.0
Загрузка...
Поиск...
Не найдено
smtp.php
См. документацию.
1<?php
2
3namespace Bitrix\Mail;
4
5use Bitrix\Main;
6use Bitrix\Main\Text\Encoding;
7use Bitrix\Main\Localization\Loc;
8
9Loc::loadMessages(__FILE__);
10
11class Smtp
12{
13 const ERR_CONNECT = 101;
14 const ERR_REJECTED = 102;
15 const ERR_COMMUNICATE = 103;
16 const ERR_EMPTY_RESPONSE = 104;
17
18 const ERR_STARTTLS = 201;
20 const ERR_CAPABILITY = 203;
21 const ERR_AUTH = 204;
22 const ERR_AUTH_MECH = 205;
23
24 protected $stream, $errors;
25 protected $sessCapability;
26
27 protected $options = array();
28
34 protected bool $isOauth = false;
35
47 public function __construct($host, $port, $tls, $strict, $login, $password, $encoding = null)
48 {
49 $this->reset();
50
51 $this->options = array(
52 'host' => $host,
53 'port' => $port,
54 'tls' => $tls,
55 'socket' => sprintf('%s://%s:%s', ($tls ? 'ssl' : 'tcp'), $host, $port),
56 'timeout' => \COption::getOptionInt('mail', 'connect_timeout', B_MAIL_TIMEOUT),
57 'context' => stream_context_create(array(
58 'ssl' => array(
59 'verify_peer' => (bool) $strict,
60 'verify_peer_name' => (bool) $strict,
61 'crypto_method' => STREAM_CRYPTO_METHOD_ANY_CLIENT,
62 )
63 )),
64 'login' => $login,
65 'password' => $password,
66 'encoding' => $encoding ?: LANG_CHARSET,
67 );
68 }
69
75 public function __destruct()
76 {
77 $this->disconnect();
78 }
79
85 protected function disconnect()
86 {
87 if (!is_null($this->stream))
88 {
89 @fclose($this->stream);
90 }
91
92 unset($this->stream);
93 }
94
95 protected function reset()
96 {
97 $this->disconnect();
98
99 $this->errors = new Main\ErrorCollection();
100 }
101
108 public function connect(&$error)
109 {
110 $error = null;
111
112 if ($this->stream)
113 {
114 return true;
115 }
116
117 $resource = @stream_socket_client(
118 $this->options['socket'], $errno, $errstr, $this->options['timeout'],
119 STREAM_CLIENT_CONNECT, $this->options['context']
120 );
121
122 if ($resource === false)
123 {
124 $error = $this->errorMessage(Smtp::ERR_CONNECT, $errno ?: null);
125 return false;
126 }
127
128 $this->stream = $resource;
129
130 if ($this->options['timeout'] > 0)
131 {
132 stream_set_timeout($this->stream, $this->options['timeout']);
133 }
134
135 $prompt = $this->readResponse();
136
137 if (false === $prompt)
138 {
140 }
141 else if (!preg_match('/^ 220 ( \r\n | \x20 ) /x', end($prompt)))
142 {
143 $error = $this->errorMessage(array(Smtp::ERR_CONNECT, Smtp::ERR_REJECTED), trim(end($prompt)));
144 }
145
146 if ($error)
147 {
148 return false;
149 }
150
151 if (!$this->capability($error))
152 {
153 return false;
154 }
155
156 if (!$this->options['tls'] && preg_grep('/^ STARTTLS $/ix', $this->sessCapability))
157 {
158 if (!$this->starttls($error))
159 {
160 return false;
161 }
162 }
163
164 return true;
165 }
166
167 protected function starttls(&$error)
168 {
169 $error = null;
170
171 if (!$this->stream)
172 {
174 return false;
175 }
176
177 $response = $this->executeCommand('STARTTLS', $error);
178
179 if ($error)
180 {
182 $error = $this->errorMessage(array(Smtp::ERR_STARTTLS, $error), $response ? trim(end($response)) : null);
183
184 return false;
185 }
186
187 if (stream_socket_enable_crypto($this->stream, true, STREAM_CRYPTO_METHOD_ANY_CLIENT))
188 {
189 if (!$this->capability($error))
190 {
191 return false;
192 }
193 }
194 else
195 {
196 $this->reset();
197
199 return false;
200 }
201
202 return true;
203 }
204
205 protected function capability(&$error)
206 {
207 $error = null;
208
209 if (!$this->stream)
210 {
212 return false;
213 }
214
215 $response = $this->executeCommand(
216 sprintf(
217 'EHLO %s',
218 Main\Context::getCurrent()->getRequest()->getHttpHost() ?: 'localhost'
219 ),
220 $error
221 );
222
223 if ($error || !is_array($response))
224 {
226 $error = $this->errorMessage(array(Smtp::ERR_CAPABILITY, $error), $response ? trim(end($response)) : null);
227
228 return false;
229 }
230
231 $this->sessCapability = array_map(
232 function ($line)
233 {
234 return trim(mb_substr($line, 4));
235 },
237 );
238
239 return true;
240 }
241
248 public function authenticate(&$error)
249 {
250 $error = null;
251
252 if (!$this->connect($error))
253 {
254 return false;
255 }
256
257 $mech = false;
258
259 if ($capabilities = preg_grep('/^ AUTH \x20 /ix', $this->sessCapability))
260 {
261 if ($this->isOauth)
262 {
263 $mech = 'oauth';
264 }
265 else if (preg_grep('/ \x20 PLAIN ( \x20 | $ ) /ix', $capabilities))
266 {
267 $mech = 'plain';
268 }
269 else if (preg_grep('/ \x20 LOGIN ( \x20 | $ ) /ix', $capabilities))
270 {
271 $mech = 'login';
272 }
273 }
274
275 if (!$mech)
276 {
278 return false;
279 }
280
281 if ($mech === 'oauth')
282 {
283 $token = Helper\OAuth::getTokenByMeta($this->options['password']);
284 if (empty($token))
285 {
287 return false;
288 }
289 $formatted = sprintf("user=%s\x01auth=Bearer %s\x01\x01", $this->options['login'], $token);
290 $response = $this->executeCommand(sprintf("AUTH XOAUTH2\x00%s", base64_encode($formatted)), $error);
291 }
292 else if ($mech === 'plain')
293 {
294 $response = $this->executeCommand(
295 sprintf(
296 "AUTH PLAIN\x00%s",
297 base64_encode(sprintf(
298 "\x00%s\x00%s",
299 Encoding::convertEncoding($this->options['login'], $this->options['encoding'], 'UTF-8'),
300 Encoding::convertEncoding($this->options['password'], $this->options['encoding'], 'UTF-8')
301 ))
302 ),
303 $error
304 );
305 }
306 else
307 {
308 $response = $this->executeCommand(sprintf(
309 "AUTH LOGIN\x00%s\x00%s",
310 base64_encode($this->options['login']),
311 base64_encode($this->options['password'])
312 ), $error);
313 }
314
315 if ($mech === 'oauth' && $response && count($response) === 1 && str_starts_with($response[0], '334'))
316 {
317 $response = $this->executeCommand("\r\n", $error);
318 }
319
320 if ($error)
321 {
323 $error = $this->errorMessage(array(Smtp::ERR_AUTH, $error), $response ? trim(end($response)) : null);
324
325 return false;
326 }
327
328 return true;
329 }
330
331 protected function executeCommand($command, &$error)
332 {
333 $error = null;
334 $response = false;
335
336 $chunks = explode("\x00", $command);
337
338 $k = count($chunks);
339 foreach ($chunks as $chunk)
340 {
341 $k--;
342
343 $response = (array) $this->exchange($chunk, $error);
344
345 if ($k > 0 && mb_strpos(end($response), '3') !== 0)
346 {
347 break;
348 }
349 }
350
351 return $response;
352 }
353
354 protected function exchange($data, &$error)
355 {
356 $error = null;
357
358 if ($this->sendData(sprintf("%s\r\n", $data)) === false)
359 {
361 return false;
362 }
363
364 $response = $this->readResponse();
365
366 if ($response === false)
367 {
369 return false;
370 }
371
372 if (!preg_match('/^ [23] \d{2} /ix', end($response)))
373 {
375 }
376
377 return $response;
378 }
379
380 protected function sendData($data)
381 {
382 $fails = 0;
383 while (strlen($data) > 0 && !feof($this->stream))
384 {
385 $bytes = @fputs($this->stream, $data);
386
387 if (false == $bytes)
388 {
389 if (false === $bytes || ++$fails >= 3)
390 {
391 break;
392 }
393
394 continue;
395 }
396
397 $fails = 0;
398
399 $data = substr($data, $bytes);
400 }
401
402 if (strlen($data) > 0)
403 {
404 $this->reset();
405 return false;
406 }
407
408 return true;
409 }
410
411 protected function readLine()
412 {
413 $line = '';
414
415 while (!feof($this->stream))
416 {
417 $buffer = @fgets($this->stream, 4096);
418 if ($buffer === false)
419 {
420 break;
421 }
422
423 $meta = ($this->options['timeout'] > 0 ? stream_get_meta_data($this->stream) : array('timed_out' => false));
424
425 $line .= $buffer;
426
427 if (preg_match('/\r\n$/', $buffer, $matches) || $meta['timed_out'])
428 {
429 break;
430 }
431 }
432
433 if (!preg_match('/\r\n$/', $line, $matches))
434 {
435 $this->reset();
436
437 return false;
438 }
439
440 return $line;
441 }
442
448 protected function readResponse()
449 {
450 $response = array();
451
452 do
453 {
454 $line = $this->readLine();
455 if ($line === false)
456 {
457 return false;
458 }
459
460 $response[] = $line;
461 }
462 while (!preg_match('/^ \d{3} ( \r\n | \x20 ) /x', $line));
463
464 return $response;
465 }
466
467 protected function errorMessage($errors, $details = null)
468 {
469 $errors = array_filter((array) $errors);
470 $details = array_filter((array) $details);
471
472 foreach ($errors as $i => $error)
473 {
474 $errors[$i] = static::decodeError($error);
475 $this->errors->setError(new Main\Error((string) $errors[$i], $error > 0 ? $error : 0));
476 }
477
478 $error = join(': ', $errors);
479 if ($details)
480 {
481 $error .= sprintf(' (SMTP: %s)', join(': ', $details));
482
483 $this->errors->setError(new Main\Error('SMTP', -1));
484 foreach ($details as $item)
485 {
486 $this->errors->setError(new Main\Error((string) $item, -1));
487 }
488 }
489
490 return $error;
491 }
492
498 public function getErrors()
499 {
500 return $this->errors;
501 }
502
509 public static function decodeError($code)
510 {
511 switch ($code)
512 {
513 case self::ERR_CONNECT:
514 return Loc::getMessage('MAIL_SMTP_ERR_CONNECT');
515 case self::ERR_REJECTED:
516 return Loc::getMessage('MAIL_SMTP_ERR_REJECTED');
517 case self::ERR_COMMUNICATE:
518 return Loc::getMessage('MAIL_SMTP_ERR_COMMUNICATE');
519 case self::ERR_EMPTY_RESPONSE:
520 return Loc::getMessage('MAIL_SMTP_ERR_EMPTY_RESPONSE');
521
522 case self::ERR_STARTTLS:
523 return Loc::getMessage('MAIL_SMTP_ERR_STARTTLS');
524 case self::ERR_COMMAND_REJECTED:
525 return Loc::getMessage('MAIL_SMTP_ERR_COMMAND_REJECTED');
526 case self::ERR_CAPABILITY:
527 return Loc::getMessage('MAIL_SMTP_ERR_CAPABILITY');
528 case self::ERR_AUTH:
529 return Loc::getMessage('MAIL_SMTP_ERR_AUTH');
530 case self::ERR_AUTH_MECH:
531 return Loc::getMessage('MAIL_SMTP_ERR_AUTH_MECH');
532
533 default:
534 return Loc::getMessage('MAIL_SMTP_ERR_DEFAULT');
535 }
536 }
537
545 public function setIsOauth(bool $value): self
546 {
547 $this->isOauth = $value;
548 return $this;
549 }
550
551}
$login
Определения change_password.php:8
Определения smtp.php:12
$options
Определения smtp.php:27
const ERR_REJECTED
Определения smtp.php:14
capability(&$error)
Определения smtp.php:205
const ERR_COMMAND_REJECTED
Определения smtp.php:19
const ERR_CONNECT
Определения smtp.php:13
__destruct()
Определения smtp.php:75
reset()
Определения smtp.php:95
bool $isOauth
Определения smtp.php:34
const ERR_AUTH_MECH
Определения smtp.php:22
errorMessage($errors, $details=null)
Определения smtp.php:467
const ERR_EMPTY_RESPONSE
Определения smtp.php:16
readLine()
Определения smtp.php:411
getErrors()
Определения smtp.php:498
const ERR_AUTH
Определения smtp.php:21
starttls(&$error)
Определения smtp.php:167
connect(&$error)
Определения smtp.php:108
$sessCapability
Определения smtp.php:25
const ERR_CAPABILITY
Определения smtp.php:20
$stream
Определения smtp.php:24
authenticate(&$error)
Определения smtp.php:248
exchange($data, &$error)
Определения smtp.php:354
$errors
Определения smtp.php:24
executeCommand($command, &$error)
Определения smtp.php:331
disconnect()
Определения smtp.php:85
static decodeError($code)
Определения smtp.php:509
sendData($data)
Определения smtp.php:380
const ERR_STARTTLS
Определения smtp.php:18
setIsOauth(bool $value)
Определения smtp.php:545
__construct($host, $port, $tls, $strict, $login, $password, $encoding=null)
Определения smtp.php:47
const ERR_COMMUNICATE
Определения smtp.php:15
readResponse()
Определения smtp.php:448
Определения error.php:15
$bytes
Определения cron_html_pages.php:17
$data['IS_AVAILABLE']
Определения .description.php:13
if(!defined("ADMIN_AJAX_MODE") &&(($_REQUEST["mode"] ?? '') !='excel')) $buffer
Определения epilog_admin_after.php:40
</td ></tr ></table ></td ></tr >< tr >< td class="bx-popup-label bx-width30"><?=GetMessage("PAGE_NEW_TAGS")?> array( $site)
Определения file_new.php:804
if(!is_null($config))($config as $configItem)(! $configItem->isVisible()) $code
Определения options.php:195
const B_MAIL_TIMEOUT
Определения constants.php:2
const LANG_CHARSET
Определения include.php:65
$password
Определения mysql_to_pgsql.php:34
$host
Определения mysql_to_pgsql.php:32
$i
Определения factura.php:643
</p ></td >< td valign=top style='border-top:none;border-left:none;border-bottom:solid windowtext 1.0pt;border-right:solid windowtext 1.0pt;padding:0cm 2.0pt 0cm 2.0pt;height:9.0pt'>< p class=Normal align=center style='margin:0cm;margin-bottom:.0001pt;text-align:center;line-height:normal'>< a name=ТекстовоеПоле54 ></a ><?=($taxRate > count( $arTaxList) > 0) ? $taxRate."%"
Определения waybill.php:936
$response
Определения result.php:21
$matches
Определения index.php:22
$error
Определения subscription_card_product.php:20
$k
Определения template_pdf.php:567