Bitrix-D7 23.9
 
Загрузка...
Поиск...
Не найдено
smtp.php
1<?php
2
3namespace Bitrix\Mail;
4
9
10Loc::loadMessages(__FILE__);
11
12class Smtp
13{
14 const ERR_CONNECT = 101;
15 const ERR_REJECTED = 102;
16 const ERR_COMMUNICATE = 103;
17 const ERR_EMPTY_RESPONSE = 104;
18
19 const ERR_STARTTLS = 201;
21 const ERR_CAPABILITY = 203;
22 const ERR_AUTH = 204;
23 const ERR_AUTH_MECH = 205;
24
25 protected $stream, $errors;
26 protected $sessCapability;
27
28 protected $options = array();
29
35 protected bool $isOauth = false;
36
48 public function __construct($host, $port, $tls, $strict, $login, $password, $encoding = null)
49 {
50 $this->reset();
51
52 $this->options = array(
53 'host' => $host,
54 'port' => $port,
55 'tls' => $tls,
56 'socket' => sprintf('%s://%s:%s', ($tls ? 'ssl' : 'tcp'), $host, $port),
57 'timeout' => \COption::getOptionInt('mail', 'connect_timeout', B_MAIL_TIMEOUT),
58 'context' => stream_context_create(array(
59 'ssl' => array(
60 'verify_peer' => (bool) $strict,
61 'verify_peer_name' => (bool) $strict,
62 'crypto_method' => STREAM_CRYPTO_METHOD_ANY_CLIENT,
63 )
64 )),
65 'login' => $login,
66 'password' => $password,
67 'encoding' => $encoding ?: LANG_CHARSET,
68 );
69 }
70
76 public function __destruct()
77 {
78 $this->disconnect();
79 }
80
86 protected function disconnect()
87 {
88 if (!is_null($this->stream))
89 {
90 @fclose($this->stream);
91 }
92
93 unset($this->stream);
94 }
95
96 protected function reset()
97 {
98 $this->disconnect();
99
100 $this->errors = new Main\ErrorCollection();
101 }
102
109 public function connect(&$error)
110 {
111 $error = null;
112
113 if ($this->stream)
114 {
115 return true;
116 }
117
118 $resource = @stream_socket_client(
119 $this->options['socket'], $errno, $errstr, $this->options['timeout'],
120 STREAM_CLIENT_CONNECT, $this->options['context']
121 );
122
123 if ($resource === false)
124 {
125 $error = $this->errorMessage(Smtp::ERR_CONNECT, $errno ?: null);
126 return false;
127 }
128
129 $this->stream = $resource;
130
131 if ($this->options['timeout'] > 0)
132 {
133 stream_set_timeout($this->stream, $this->options['timeout']);
134 }
135
136 $prompt = $this->readResponse();
137
138 if (false === $prompt)
139 {
141 }
142 else if (!preg_match('/^ 220 ( \r\n | \x20 ) /x', end($prompt)))
143 {
144 $error = $this->errorMessage(array(Smtp::ERR_CONNECT, Smtp::ERR_REJECTED), trim(end($prompt)));
145 }
146
147 if ($error)
148 {
149 return false;
150 }
151
152 if (!$this->capability($error))
153 {
154 return false;
155 }
156
157 if (!$this->options['tls'] && preg_grep('/^ STARTTLS $/ix', $this->sessCapability))
158 {
159 if (!$this->starttls($error))
160 {
161 return false;
162 }
163 }
164
165 return true;
166 }
167
168 protected function starttls(&$error)
169 {
170 $error = null;
171
172 if (!$this->stream)
173 {
174 $error = $this->errorMessage(Smtp::ERR_STARTTLS);
175 return false;
176 }
177
178 $response = $this->executeCommand('STARTTLS', $error);
179
180 if ($error)
181 {
182 $error = $error == Smtp::ERR_COMMAND_REJECTED ? null : $error;
183 $error = $this->errorMessage(array(Smtp::ERR_STARTTLS, $error), $response ? trim(end($response)) : null);
184
185 return false;
186 }
187
188 if (stream_socket_enable_crypto($this->stream, true, STREAM_CRYPTO_METHOD_ANY_CLIENT))
189 {
190 if (!$this->capability($error))
191 {
192 return false;
193 }
194 }
195 else
196 {
197 $this->reset();
198
199 $error = $this->errorMessage(Smtp::ERR_STARTTLS);
200 return false;
201 }
202
203 return true;
204 }
205
206 protected function capability(&$error)
207 {
208 $error = null;
209
210 if (!$this->stream)
211 {
212 $error = $this->errorMessage(Smtp::ERR_CAPABILITY);
213 return false;
214 }
215
216 $response = $this->executeCommand(
217 sprintf(
218 'EHLO %s',
219 Main\Context::getCurrent()->getRequest()->getHttpHost() ?: 'localhost'
220 ),
221 $error
222 );
223
224 if ($error || !is_array($response))
225 {
226 $error = $error == Smtp::ERR_COMMAND_REJECTED ? null : $error;
227 $error = $this->errorMessage(array(Smtp::ERR_CAPABILITY, $error), $response ? trim(end($response)) : null);
228
229 return false;
230 }
231
232 $this->sessCapability = array_map(
233 function ($line)
234 {
235 return trim(mb_substr($line, 4));
236 },
237 $response
238 );
239
240 return true;
241 }
242
249 public function authenticate(&$error)
250 {
251 $error = null;
252
253 if (!$this->connect($error))
254 {
255 return false;
256 }
257
258 $mech = false;
259
260 if ($capabilities = preg_grep('/^ AUTH \x20 /ix', $this->sessCapability))
261 {
262 if ($this->isOauth)
263 {
264 $mech = 'oauth';
265 }
266 else if (preg_grep('/ \x20 PLAIN ( \x20 | $ ) /ix', $capabilities))
267 {
268 $mech = 'plain';
269 }
270 else if (preg_grep('/ \x20 LOGIN ( \x20 | $ ) /ix', $capabilities))
271 {
272 $mech = 'login';
273 }
274 }
275
276 if (!$mech)
277 {
278 $error = $this->errorMessage(array(Smtp::ERR_AUTH, Smtp::ERR_AUTH_MECH));
279 return false;
280 }
281
282 if ($mech === 'oauth')
283 {
284 $token = Helper\OAuth::getTokenByMeta($this->options['password']);
285 if (empty($token))
286 {
287 $error = $this->errorMessage(array(Smtp::ERR_AUTH, Smtp::ERR_AUTH_MECH));
288 return false;
289 }
290 $formatted = sprintf("user=%s\x01auth=Bearer %s\x01\x01", $this->options['login'], $token);
291 $response = $this->executeCommand(sprintf("AUTH XOAUTH2\x00%s", base64_encode($formatted)), $error);
292 }
293 else if ($mech === 'plain')
294 {
295 $response = $this->executeCommand(
296 sprintf(
297 "AUTH PLAIN\x00%s",
298 base64_encode(sprintf(
299 "\x00%s\x00%s",
300 Encoding::convertEncoding($this->options['login'], $this->options['encoding'], 'UTF-8'),
301 Encoding::convertEncoding($this->options['password'], $this->options['encoding'], 'UTF-8')
302 ))
303 ),
304 $error
305 );
306 }
307 else
308 {
309 $response = $this->executeCommand(sprintf(
310 "AUTH LOGIN\x00%s\x00%s",
311 base64_encode($this->options['login']),
312 base64_encode($this->options['password'])
313 ), $error);
314 }
315
316 if ($error)
317 {
318 $error = $error == Smtp::ERR_COMMAND_REJECTED ? null : $error;
319 $error = $this->errorMessage(array(Smtp::ERR_AUTH, $error), $response ? trim(end($response)) : null);
320
321 return false;
322 }
323
324 return true;
325 }
326
327 protected function executeCommand($command, &$error)
328 {
329 $error = null;
330 $response = false;
331
332 $chunks = explode("\x00", $command);
333
334 $k = count($chunks);
335 foreach ($chunks as $chunk)
336 {
337 $k--;
338
339 $response = (array) $this->exchange($chunk, $error);
340
341 if ($k > 0 && mb_strpos(end($response), '3') !== 0)
342 {
343 break;
344 }
345 }
346
347 return $response;
348 }
349
350 protected function exchange($data, &$error)
351 {
352 $error = null;
353
354 if ($this->sendData(sprintf("%s\r\n", $data)) === false)
355 {
356 $error = Smtp::ERR_COMMUNICATE;
357 return false;
358 }
359
360 $response = $this->readResponse();
361
362 if ($response === false)
363 {
364 $error = Smtp::ERR_COMMUNICATE;
365 return false;
366 }
367
368 if (!preg_match('/^ [23] \d{2} /ix', end($response)))
369 {
371 }
372
373 return $response;
374 }
375
376 protected function sendData($data)
377 {
378 $fails = 0;
379 while (BinaryString::getLength($data) > 0 && !feof($this->stream))
380 {
381 $bytes = @fputs($this->stream, $data);
382
383 if (false == $bytes)
384 {
385 if (false === $bytes || ++$fails >= 3)
386 {
387 break;
388 }
389
390 continue;
391 }
392
393 $fails = 0;
394
395 $data = BinaryString::getSubstring($data, $bytes);
396 }
397
398 if (BinaryString::getLength($data) > 0)
399 {
400 $this->reset();
401 return false;
402 }
403
404 return true;
405 }
406
407 protected function readLine()
408 {
409 $line = '';
410
411 while (!feof($this->stream))
412 {
413 $buffer = @fgets($this->stream, 4096);
414 if ($buffer === false)
415 {
416 break;
417 }
418
419 $meta = ($this->options['timeout'] > 0 ? stream_get_meta_data($this->stream) : array('timed_out' => false));
420
421 $line .= $buffer;
422
423 if (preg_match('/\r\n$/', $buffer, $matches) || $meta['timed_out'])
424 {
425 break;
426 }
427 }
428
429 if (!preg_match('/\r\n$/', $line, $matches))
430 {
431 $this->reset();
432
433 return false;
434 }
435
436 return $line;
437 }
438
444 protected function readResponse()
445 {
446 $response = array();
447
448 do
449 {
450 $line = $this->readLine();
451 if ($line === false)
452 {
453 return false;
454 }
455
456 $response[] = $line;
457 }
458 while (!preg_match('/^ \d{3} ( \r\n | \x20 ) /x', $line));
459
460 return $response;
461 }
462
463 protected function errorMessage($errors, $details = null)
464 {
465 $errors = array_filter((array) $errors);
466 $details = array_filter((array) $details);
467
468 foreach ($errors as $i => $error)
469 {
470 $errors[$i] = static::decodeError($error);
471 $this->errors->setError(new Main\Error((string) $errors[$i], $error > 0 ? $error : 0));
472 }
473
474 $error = join(': ', $errors);
475 if ($details)
476 {
477 $error .= sprintf(' (SMTP: %s)', join(': ', $details));
478
479 $this->errors->setError(new Main\Error('SMTP', -1));
480 foreach ($details as $item)
481 {
482 $this->errors->setError(new Main\Error((string) $item, -1));
483 }
484 }
485
486 return $error;
487 }
488
494 public function getErrors()
495 {
496 return $this->errors;
497 }
498
505 public static function decodeError($code)
506 {
507 switch ($code)
508 {
510 return Loc::getMessage('MAIL_SMTP_ERR_CONNECT');
512 return Loc::getMessage('MAIL_SMTP_ERR_REJECTED');
514 return Loc::getMessage('MAIL_SMTP_ERR_COMMUNICATE');
516 return Loc::getMessage('MAIL_SMTP_ERR_EMPTY_RESPONSE');
517
519 return Loc::getMessage('MAIL_SMTP_ERR_STARTTLS');
521 return Loc::getMessage('MAIL_SMTP_ERR_COMMAND_REJECTED');
523 return Loc::getMessage('MAIL_SMTP_ERR_CAPABILITY');
524 case self::ERR_AUTH:
525 return Loc::getMessage('MAIL_SMTP_ERR_AUTH');
527 return Loc::getMessage('MAIL_SMTP_ERR_AUTH_MECH');
528
529 default:
530 return Loc::getMessage('MAIL_SMTP_ERR_DEFAULT');
531 }
532 }
533
541 public function setIsOauth(bool $value): self
542 {
543 $this->isOauth = $value;
544 return $this;
545 }
546
547}
const ERR_REJECTED
Definition smtp.php:15
capability(&$error)
Definition smtp.php:206
const ERR_COMMAND_REJECTED
Definition smtp.php:20
const ERR_CONNECT
Definition smtp.php:14
bool $isOauth
Definition smtp.php:35
const ERR_AUTH_MECH
Definition smtp.php:23
errorMessage($errors, $details=null)
Definition smtp.php:463
const ERR_EMPTY_RESPONSE
Definition smtp.php:17
const ERR_AUTH
Definition smtp.php:22
starttls(&$error)
Definition smtp.php:168
connect(&$error)
Definition smtp.php:109
const ERR_CAPABILITY
Definition smtp.php:21
authenticate(&$error)
Definition smtp.php:249
exchange($data, &$error)
Definition smtp.php:350
executeCommand($command, &$error)
Definition smtp.php:327
static decodeError($code)
Definition smtp.php:505
sendData($data)
Definition smtp.php:376
const ERR_STARTTLS
Definition smtp.php:19
setIsOauth(bool $value)
Definition smtp.php:541
__construct($host, $port, $tls, $strict, $login, $password, $encoding=null)
Definition smtp.php:48
const ERR_COMMUNICATE
Definition smtp.php:16
static getCurrent()
Definition context.php:241
static loadMessages($file)
Definition loc.php:64
static getMessage($code, $replace=null, $language=null)
Definition loc.php:29