1 | <?php declare(strict_types=1); |
||
2 | |||
3 | namespace SMTPValidateEmail; |
||
4 | |||
5 | use SMTPValidateEmail\Exceptions\Exception; |
||
6 | use SMTPValidateEmail\Exceptions\Timeout as TimeoutException; |
||
7 | use SMTPValidateEmail\Exceptions\NoTimeout as NoTimeoutException; |
||
8 | use SMTPValidateEmail\Exceptions\NoConnection as NoConnectionException; |
||
9 | use SMTPValidateEmail\Exceptions\UnexpectedResponse as UnexpectedResponseException; |
||
10 | use SMTPValidateEmail\Exceptions\NoHelo as NoHeloException; |
||
11 | use SMTPValidateEmail\Exceptions\NoMailFrom as NoMailFromException; |
||
12 | use SMTPValidateEmail\Exceptions\NoResponse as NoResponseException; |
||
13 | use SMTPValidateEmail\Exceptions\SendFailed as SendFailedException; |
||
14 | |||
15 | class Validator |
||
16 | { |
||
17 | |||
18 | public $log = []; |
||
19 | |||
20 | /** |
||
21 | * Print stuff as it happens or not |
||
22 | * |
||
23 | * @var bool |
||
24 | */ |
||
25 | public $debug = false; |
||
26 | |||
27 | /** |
||
28 | * Default smtp port to connect to |
||
29 | * |
||
30 | * @var int |
||
31 | */ |
||
32 | public $connect_port = 25; |
||
33 | |||
34 | /** |
||
35 | * Are "catch-all" accounts considered valid or not? |
||
36 | * If not, the class checks for a "catch-all" and if it determines the box |
||
37 | * has a "catch-all", sets all the emails on that domain as invalid. |
||
38 | * |
||
39 | * @var bool |
||
40 | */ |
||
41 | public $catchall_is_valid = true; |
||
42 | |||
43 | /** |
||
44 | * Whether to perform the "catch-all" test or not |
||
45 | * |
||
46 | * @var bool |
||
47 | */ |
||
48 | public $catchall_test = false; // Set to true to perform a catchall test |
||
49 | |||
50 | /** |
||
51 | * Being unable to communicate with the remote MTA could mean an address |
||
52 | * is invalid, but it might not, depending on your use case, set the |
||
53 | * value appropriately. |
||
54 | * |
||
55 | * @var bool |
||
56 | */ |
||
57 | public $no_comm_is_valid = false; |
||
58 | |||
59 | /** |
||
60 | * Being unable to connect with the remote host could mean a server |
||
61 | * configuration issue, but it might not, depending on your use case, |
||
62 | * set the value appropriately. |
||
63 | */ |
||
64 | public $no_conn_is_valid = false; |
||
65 | |||
66 | /** |
||
67 | * Whether "greylisted" responses are considered as valid or invalid addresses |
||
68 | * |
||
69 | * @var bool |
||
70 | */ |
||
71 | public $greylisted_considered_valid = true; |
||
72 | |||
73 | /** |
||
74 | * Stream context arguments for connection socket, necessary to initiate |
||
75 | * Server IP (in case reverse IP), see: https://stackoverflow.com/a/8968016 |
||
76 | */ |
||
77 | public $stream_context_args = []; |
||
78 | |||
79 | /** |
||
80 | * Timeout values for various commands (in seconds) per RFC 2821 |
||
81 | * |
||
82 | * @var array |
||
83 | */ |
||
84 | protected $command_timeouts = [ |
||
85 | 'ehlo' => 120, |
||
86 | 'helo' => 120, |
||
87 | 'tls' => 180, // start tls |
||
88 | 'mail' => 300, // mail from |
||
89 | 'rcpt' => 300, // rcpt to, |
||
90 | 'rset' => 30, |
||
91 | 'quit' => 60, |
||
92 | 'noop' => 60 |
||
93 | ]; |
||
94 | |||
95 | /** |
||
96 | * Whether NOOP commands are sent at all. |
||
97 | * |
||
98 | * @var bool |
||
99 | */ |
||
100 | protected $send_noops = true; |
||
101 | |||
102 | public const CRLF = "\r\n"; |
||
103 | |||
104 | // Some smtp response codes |
||
105 | public const SMTP_CONNECT_SUCCESS = 220; |
||
106 | public const SMTP_QUIT_SUCCESS = 221; |
||
107 | public const SMTP_GENERIC_SUCCESS = 250; |
||
108 | public const SMTP_USER_NOT_LOCAL = 251; |
||
109 | public const SMTP_CANNOT_VRFY = 252; |
||
110 | |||
111 | public const SMTP_SERVICE_UNAVAILABLE = 421; |
||
112 | |||
113 | // 450 Requested mail action not taken: mailbox unavailable (e.g., |
||
114 | // mailbox busy or temporarily blocked for policy reasons) |
||
115 | public const SMTP_MAIL_ACTION_NOT_TAKEN = 450; |
||
116 | // 451 Requested action aborted: local error in processing |
||
117 | public const SMTP_MAIL_ACTION_ABORTED = 451; |
||
118 | // 452 Requested action not taken: insufficient system storage |
||
119 | public const SMTP_REQUESTED_ACTION_NOT_TAKEN = 452; |
||
120 | |||
121 | // 500 Syntax error (may be due to a denied command) |
||
122 | public const SMTP_SYNTAX_ERROR = 500; |
||
123 | // 502 Comment not implemented |
||
124 | public const SMTP_NOT_IMPLEMENTED = 502; |
||
125 | // 503 Bad sequence of commands (may happen due to a denied command) |
||
126 | public const SMTP_BAD_SEQUENCE = 503; |
||
127 | |||
128 | // 550 Requested action not taken: mailbox unavailable (e.g., mailbox |
||
129 | // not found, no access, or command rejected for policy reasons) |
||
130 | public const SMTP_MBOX_UNAVAILABLE = 550; |
||
131 | |||
132 | // 554 Seen this from hotmail MTAs, in response to RSET :( |
||
133 | public const SMTP_TRANSACTION_FAILED = 554; |
||
134 | |||
135 | /** |
||
136 | * List of response codes considered as "greylisted" |
||
137 | * |
||
138 | * @var array |
||
139 | */ |
||
140 | private $greylisted = [ |
||
141 | self::SMTP_MAIL_ACTION_NOT_TAKEN, |
||
142 | self::SMTP_MAIL_ACTION_ABORTED, |
||
143 | self::SMTP_REQUESTED_ACTION_NOT_TAKEN |
||
144 | ]; |
||
145 | |||
146 | /** |
||
147 | * Internal states we can be in |
||
148 | * |
||
149 | * @var array |
||
150 | */ |
||
151 | private $state = [ |
||
152 | 'helo' => false, |
||
153 | 'mail' => false, |
||
154 | 'rcpt' => false |
||
155 | ]; |
||
156 | |||
157 | /** |
||
158 | * Holds the socket connection resource |
||
159 | * |
||
160 | * @var resource |
||
161 | */ |
||
162 | private $socket; |
||
163 | |||
164 | /** |
||
165 | * Holds all the domains we'll validate accounts on |
||
166 | * |
||
167 | * @var array |
||
168 | */ |
||
169 | private $domains = []; |
||
170 | |||
171 | /** |
||
172 | * @var array |
||
173 | */ |
||
174 | private $domains_info = []; |
||
175 | |||
176 | /** |
||
177 | * Default connect timeout for each MTA attempted (seconds) |
||
178 | * |
||
179 | * @var int |
||
180 | */ |
||
181 | private $connect_timeout = 10; |
||
182 | |||
183 | /** |
||
184 | * Default sender username |
||
185 | * |
||
186 | * @var string |
||
187 | */ |
||
188 | private $from_user = 'user'; |
||
189 | |||
190 | /** |
||
191 | * Default sender host |
||
192 | * |
||
193 | * @var string |
||
194 | */ |
||
195 | private $from_domain = 'localhost'; |
||
196 | |||
197 | /** |
||
198 | * The host we're currently connected to |
||
199 | * |
||
200 | * @var string|null |
||
201 | */ |
||
202 | private $host; |
||
203 | |||
204 | /** |
||
205 | * List of validation results |
||
206 | * |
||
207 | * @var array |
||
208 | */ |
||
209 | private $results = []; |
||
210 | |||
211 | /** |
||
212 | * @param array|string $emails Email(s) to validate |
||
213 | * @param string|null $sender Sender's email address |
||
214 | */ |
||
215 | 19 | public function __construct($emails = [], ?string $sender = null) |
|
216 | { |
||
217 | 19 | if (!empty($emails)) { |
|
218 | 13 | $this->setEmails($emails); |
|
219 | } |
||
220 | 19 | if (null !== $sender) { |
|
221 | 13 | $this->setSender($sender); |
|
222 | } |
||
223 | 19 | } |
|
224 | |||
225 | /** |
||
226 | * Disconnects from the SMTP server if needed to release resources. |
||
227 | * |
||
228 | * @throws NoConnectionException |
||
229 | * @throws SendFailedException |
||
230 | * @throws TimeoutException |
||
231 | * @throws UnexpectedResponseException |
||
232 | */ |
||
233 | 19 | public function __destruct() |
|
234 | { |
||
235 | 19 | $this->disconnect(false); |
|
236 | 19 | } |
|
237 | |||
238 | /** |
||
239 | * Does a catch-all test for the given domain. |
||
240 | * |
||
241 | * @param string $domain |
||
242 | * |
||
243 | * @return bool Whether the MTA accepts any random recipient. |
||
244 | * |
||
245 | * @throws NoConnectionException |
||
246 | * @throws NoMailFromException |
||
247 | * @throws SendFailedException |
||
248 | * @throws TimeoutException |
||
249 | * @throws UnexpectedResponseException |
||
250 | */ |
||
251 | 5 | public function acceptsAnyRecipient(string $domain): bool |
|
252 | { |
||
253 | 5 | if (!$this->catchall_test) { |
|
254 | 3 | return false; |
|
255 | } |
||
256 | |||
257 | 2 | $test = 'catch-all-test-' . time(); |
|
258 | 2 | $accepted = $this->rcpt($test . '@' . $domain); |
|
259 | 2 | if ($accepted) { |
|
260 | // Success on a non-existing address is a "catch-all" |
||
261 | 2 | $this->domains_info[$domain]['catchall'] = true; |
|
262 | 2 | return true; |
|
263 | } |
||
264 | |||
265 | // Log when we get disconnected while trying catchall detection |
||
266 | $this->noop(); |
||
267 | if (!$this->connected()) { |
||
268 | $this->debug('Disconnected after trying a non-existing recipient on ' . $domain); |
||
269 | } |
||
270 | |||
271 | /** |
||
272 | * N.B.: |
||
273 | * Disconnects are considered as a non-catch-all case this way, but |
||
274 | * that might not always be the case. |
||
275 | */ |
||
276 | return false; |
||
277 | } |
||
278 | |||
279 | /** |
||
280 | * Performs validation of specified email addresses. |
||
281 | * |
||
282 | * @param array|string $emails Emails to validate (or a single one as a string). |
||
283 | * @param string|null $sender Sender email address. |
||
284 | * |
||
285 | * @return array List of emails and their results. |
||
286 | * |
||
287 | * @throws NoConnectionException |
||
288 | * @throws NoHeloException |
||
289 | * @throws NoMailFromException |
||
290 | * @throws NoTimeoutException |
||
291 | * @throws SendFailedException |
||
292 | */ |
||
293 | 13 | public function validate($emails = [], ?string $sender = null): array |
|
294 | { |
||
295 | 13 | $this->results = []; |
|
296 | |||
297 | 13 | if (!empty($emails)) { |
|
298 | 1 | $this->setEmails($emails); |
|
299 | } |
||
300 | 13 | if (null !== $sender) { |
|
301 | 1 | $this->setSender($sender); |
|
302 | } |
||
303 | |||
304 | 13 | if (empty($this->domains)) { |
|
305 | 1 | return $this->results; |
|
306 | } |
||
307 | |||
308 | 12 | $this->loop(); |
|
309 | |||
310 | 10 | return $this->getResults(); |
|
311 | } |
||
312 | |||
313 | /** |
||
314 | * @throws NoConnectionException |
||
315 | * @throws NoHeloException |
||
316 | * @throws NoMailFromException |
||
317 | * @throws NoTimeoutException |
||
318 | * @throws SendFailedException |
||
319 | */ |
||
320 | 12 | protected function loop(): void |
|
321 | { |
||
322 | // Query the MTAs on each domain if we have them |
||
323 | 12 | foreach ($this->domains as $domain => $users) { |
|
324 | 12 | $mxs = $this->buildMxs($domain); |
|
325 | |||
326 | 12 | $this->debug('MX records (' . $domain . '): ' . print_r($mxs, true)); |
|
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
327 | 12 | $this->domains_info[$domain] = []; |
|
328 | 12 | $this->domains_info[$domain]['users'] = $users; |
|
329 | 12 | $this->domains_info[$domain]['mxs'] = $mxs; |
|
330 | |||
331 | // Set default results as though we can't communicate at all... |
||
332 | 12 | $this->setDomainResults($users, $domain, $this->no_conn_is_valid); |
|
333 | 12 | $this->attemptConnection($mxs); |
|
334 | 12 | $this->performSmtpDance($domain, $users); |
|
335 | } |
||
336 | 10 | } |
|
337 | |||
338 | /** |
||
339 | * @param string $domain |
||
340 | * @return array |
||
341 | */ |
||
342 | 12 | protected function buildMxs(string $domain): array |
|
343 | { |
||
344 | 12 | $mxs = []; |
|
345 | |||
346 | $this->debug('Building MX records for domain: ' . $domain); |
||
347 | 12 | ||
348 | // Query the MX records for the current domain |
||
349 | [$hosts, $weights] = $this->mxQuery($domain); |
||
350 | 12 | ||
351 | // Sort out the MX priorities |
||
352 | foreach ($hosts as $k => $host) { |
||
353 | 12 | $mxs[$host] = $weights[$k]; |
|
354 | } |
||
355 | asort($mxs); |
||
356 | 12 | ||
357 | // Add the hostname itself with 0 weight (RFC 2821) |
||
358 | 12 | $mxs[$domain] = 0; |
|
359 | |||
360 | return $mxs; |
||
361 | } |
||
362 | |||
363 | /** |
||
364 | * @param array $mxs |
||
365 | * |
||
366 | 12 | * @throws NoTimeoutException |
|
367 | */ |
||
368 | protected function attemptConnection(array $mxs): void |
||
369 | 12 | { |
|
370 | // Try each host, $_weight unused in the foreach body, but array_keys() doesn't guarantee the order |
||
371 | 12 | foreach ($mxs as $host => $_weight) { |
|
372 | 8 | try { |
|
373 | 8 | $this->connect($host); |
|
374 | if ($this->connected()) { |
||
375 | 4 | break; |
|
376 | } |
||
377 | 4 | } catch (NoConnectionException $e) { |
|
378 | // Unable to connect to host, so these addresses are invalid? |
||
379 | $this->debug('Unable to connect. Exception caught: ' . $e->getMessage()); |
||
380 | 12 | } |
|
381 | } |
||
382 | } |
||
383 | |||
384 | /** |
||
385 | * @param string $domain |
||
386 | * @param array $users |
||
387 | * |
||
388 | * @throws NoConnectionException |
||
389 | * @throws NoHeloException |
||
390 | * @throws NoMailFromException |
||
391 | 12 | * @throws SendFailedException |
|
392 | */ |
||
393 | protected function performSmtpDance(string $domain, array $users): void |
||
394 | 12 | { |
|
395 | 4 | // Bail early if not connected for whatever reason... |
|
396 | if (!$this->connected()) { |
||
397 | return; |
||
398 | } |
||
399 | 8 | ||
400 | 2 | try { |
|
401 | $this->attemptMailCommands($domain, $users); |
||
402 | } catch (UnexpectedResponseException $e) { |
||
403 | // Unexpected responses handled as $this->no_comm_is_valid, that way anyone can |
||
404 | 2 | // decide for themselves if such results are considered valid or not |
|
405 | $this->setDomainResults($users, $domain, $this->no_comm_is_valid); |
||
406 | } catch (TimeoutException $e) { |
||
407 | // A timeout is a comm failure, so treat the results on that domain |
||
408 | // according to $this->no_comm_is_valid as well |
||
409 | 6 | $this->setDomainResults($users, $domain, $this->no_comm_is_valid); |
|
410 | } |
||
411 | } |
||
412 | |||
413 | /** |
||
414 | * @param string $domain |
||
415 | * @param array $users |
||
416 | * |
||
417 | * @throws NoConnectionException |
||
418 | * @throws NoHeloException |
||
419 | * @throws NoMailFromException |
||
420 | * @throws SendFailedException |
||
421 | * @throws TimeoutException |
||
422 | 8 | * @throws UnexpectedResponseException |
|
423 | */ |
||
424 | protected function attemptMailCommands(string $domain, array $users): void |
||
425 | 8 | { |
|
426 | // Bail if HELO doesn't go through... |
||
427 | if (!$this->helo()) { |
||
0 ignored issues
–
show
The expression
$this->helo() of type boolean|null is loosely compared to false ; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.
If an expression can have both $a = canBeFalseAndNull();
// Instead of
if ( ! $a) { }
// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
![]() |
|||
428 | return; |
||
429 | } |
||
430 | 6 | ||
431 | // Try issuing MAIL FROM |
||
432 | 1 | if (!$this->mail($this->from_user . '@' . $this->from_domain)) { |
|
433 | 1 | // MAIL FROM not accepted, we can't talk |
|
434 | $this->setDomainResults($users, $domain, $this->no_comm_is_valid); |
||
435 | return; |
||
436 | } |
||
437 | |||
438 | /** |
||
439 | * If we're still connected, proceed (because we might get disconnected, or banned, or |
||
440 | * greylisted temporarily etc.). See mail() for more info. |
||
441 | 5 | */ |
|
442 | if (!$this->connected()) { |
||
443 | return; |
||
444 | } |
||
445 | |||
446 | 5 | // Attempt a catch-all test for the domain (if configured to do so) |
|
447 | $is_catchall_domain = $this->acceptsAnyRecipient($domain); |
||
448 | |||
449 | // If a catchall domain is detected, and we consider |
||
450 | // accounts on such domains as invalid, mark all the |
||
451 | 5 | // users as invalid and move on |
|
452 | 2 | if ($is_catchall_domain && !$this->catchall_is_valid) { |
|
453 | 1 | $this->setDomainResults($users, $domain, $this->catchall_is_valid); |
|
454 | 1 | return; |
|
455 | } |
||
456 | |||
457 | $this->noop(); |
||
458 | 4 | ||
459 | // RCPT for each user |
||
460 | foreach ($users as $user) { |
||
461 | 4 | $address = $user . '@' . $domain; |
|
462 | 4 | $this->results[$address] = $this->rcpt($address); |
|
463 | 4 | } |
|
464 | |||
465 | // Issue a RSET for all the things we just made the MTA do |
||
466 | $this->rset(); |
||
467 | 4 | $this->disconnect(); |
|
468 | 4 | } |
|
469 | 4 | ||
470 | /** |
||
471 | * Get validation results |
||
472 | * |
||
473 | * @param bool $include_domains_info Whether to include extra info in the results |
||
474 | * |
||
475 | * @return array |
||
476 | */ |
||
477 | public function getResults(bool $include_domains_info = true): array |
||
478 | 11 | { |
|
479 | if ($include_domains_info) { |
||
480 | 11 | $this->results['domains'] = $this->domains_info; |
|
481 | 11 | } else { |
|
482 | unset($this->results['domains']); |
||
483 | 1 | } |
|
484 | |||
485 | return $this->results; |
||
486 | 11 | } |
|
487 | |||
488 | /** |
||
489 | * Helper to set results for all the users on a domain to a specific value |
||
490 | * |
||
491 | * @param array $users Users (usernames) |
||
492 | * @param string $domain The domain for the users/usernames |
||
493 | * @param bool $val Value to set |
||
494 | * |
||
495 | * @return void |
||
496 | */ |
||
497 | private function setDomainResults(array $users, string $domain, bool $val): void |
||
498 | 12 | { |
|
499 | foreach ($users as $user) { |
||
500 | 12 | $this->results[$user . '@' . $domain] = $val; |
|
501 | 12 | } |
|
502 | } |
||
503 | 12 | ||
504 | /** |
||
505 | * Returns true if we're connected to an MTA |
||
506 | * |
||
507 | * @return bool |
||
508 | */ |
||
509 | protected function connected(): bool |
||
510 | 19 | { |
|
511 | return is_resource($this->socket); |
||
512 | 19 | } |
|
513 | |||
514 | /** |
||
515 | * Tries to connect to the specified host on the pre-configured port. |
||
516 | * |
||
517 | * @param string $host Host to connect to |
||
518 | * |
||
519 | * @throws NoConnectionException |
||
520 | * @throws NoTimeoutException |
||
521 | * |
||
522 | * @return void |
||
523 | */ |
||
524 | protected function connect(string $host): void |
||
525 | 12 | { |
|
526 | $remote_socket = $host . ':' . $this->connect_port; |
||
527 | 12 | $errnum = 0; |
|
528 | 12 | $errstr = ''; |
|
529 | 12 | $this->host = $remote_socket; |
|
530 | 12 | ||
531 | // Open connection |
||
532 | $this->debug('Connecting to ' . $this->host . ' (timeout: ' . $this->connect_timeout . ')'); |
||
533 | 12 | // @codingStandardsIgnoreLine |
|
534 | $this->socket = /** @scrutinizer ignore-unhandled */ @stream_socket_client( |
||
0 ignored issues
–
show
It seems like
@stream_socket_client($t...->stream_context_args)) can also be of type false . However, the property $socket is declared as type resource . Maybe add an additional type check?
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly. For example, imagine you have a variable Either this assignment is in error or a type check should be added for that assignment. class Id
{
public $id;
public function __construct($id)
{
$this->id = $id;
}
}
class Account
{
/** @var Id $id */
public $id;
}
$account_id = false;
if (starsAreRight()) {
$account_id = new Id(42);
}
$account = new Account();
if ($account instanceof Id)
{
$account->id = $account_id;
}
![]() |
|||
535 | 12 | $this->host, |
|
536 | 12 | $errnum, |
|
537 | 12 | $errstr, |
|
538 | 12 | $this->connect_timeout, |
|
539 | 12 | STREAM_CLIENT_CONNECT, |
|
540 | 12 | stream_context_create($this->stream_context_args) |
|
541 | 12 | ); |
|
542 | |||
543 | // Clear any errors that may have happened due to @ suppression above: https://github.com/zytzagoo/smtp-validate-email/issues/77 |
||
544 | error_clear_last(); |
||
545 | 12 | ||
546 | 4 | // Check and throw if not connected |
|
547 | 4 | if (!$this->connected()) { |
|
548 | $this->debug('Connect failed: ' . $errstr . ', error number: ' . $errnum . ', host: ' . $this->host); |
||
549 | throw new NoConnectionException('Cannot open a connection to remote host (' . $this->host . ')'); |
||
550 | 8 | } |
|
551 | 8 | ||
552 | $result = stream_set_timeout($this->socket, $this->connect_timeout); |
||
553 | if (!$result) { |
||
554 | throw new NoTimeoutException('Cannot set timeout'); |
||
555 | 8 | } |
|
556 | 8 | ||
557 | $this->debug('Connected to ' . $this->host . ' successfully'); |
||
558 | } |
||
559 | |||
560 | /** |
||
561 | * Disconnects the currently connected MTA. |
||
562 | * |
||
563 | * @param bool $quit Whether to send QUIT command before closing the socket on our end. |
||
564 | * |
||
565 | * @throws NoConnectionException |
||
566 | * @throws SendFailedException |
||
567 | * @throws TimeoutException |
||
568 | 19 | * @throws UnexpectedResponseException |
|
569 | */ |
||
570 | 19 | protected function disconnect(bool $quit = true): void |
|
571 | 4 | { |
|
572 | if ($quit) { |
||
573 | $this->quit(); |
||
574 | 19 | } |
|
575 | 8 | ||
576 | 8 | if ($this->connected()) { |
|
577 | $this->debug('Closing socket to ' . $this->host); |
||
578 | fclose($this->socket); |
||
579 | 19 | } |
|
580 | 19 | ||
581 | 19 | $this->host = null; |
|
582 | $this->resetState(); |
||
583 | } |
||
584 | |||
585 | /** |
||
586 | 19 | * Resets internal state flags to defaults |
|
587 | */ |
||
588 | 19 | private function resetState(): void |
|
589 | 19 | { |
|
590 | 19 | $this->state['helo'] = false; |
|
591 | 19 | $this->state['mail'] = false; |
|
592 | $this->state['rcpt'] = false; |
||
593 | } |
||
594 | |||
595 | /** |
||
596 | * Sends a HELO/EHLO sequence. |
||
597 | * |
||
598 | * @return bool|null True if successful, false otherwise. Null if already done. |
||
599 | * |
||
600 | * @throws NoConnectionException |
||
601 | * @throws SendFailedException |
||
602 | * @throws TimeoutException |
||
603 | 8 | * @throws UnexpectedResponseException |
|
604 | */ |
||
605 | protected function helo(): ?bool |
||
606 | 8 | { |
|
607 | // Don't do it if already done |
||
608 | if ($this->state['helo']) { |
||
609 | return null; |
||
610 | } |
||
611 | 8 | ||
612 | 8 | try { |
|
613 | $this->expect(self::SMTP_CONNECT_SUCCESS, $this->command_timeouts['helo']); |
||
614 | $this->ehlo(); |
||
615 | 6 | ||
616 | // Session started |
||
617 | $this->state['helo'] = true; |
||
618 | |||
619 | // Are we going for a TLS connection? |
||
620 | /* |
||
621 | if ($this->tls) { |
||
622 | // send STARTTLS, wait 3 minutes |
||
623 | $this->send('STARTTLS'); |
||
624 | $this->expect(self::SMTP_CONNECT_SUCCESS, $this->command_timeouts['tls']); |
||
625 | $result = stream_socket_enable_crypto($this->socket, true, |
||
626 | STREAM_CRYPTO_METHOD_TLS_CLIENT); |
||
627 | if (!$result) { |
||
628 | throw new SMTP_Validate_Email_Exception_No_TLS('Cannot enable TLS'); |
||
629 | } |
||
630 | } |
||
631 | 6 | */ |
|
632 | 2 | ||
633 | $result = true; |
||
634 | } catch (UnexpectedResponseException $e) { |
||
635 | // Connected, but got an unexpected response, so disconnect |
||
636 | $result = false; |
||
637 | $this->debug('Unexpected response after connecting: ' . $e->getMessage()); |
||
638 | $this->disconnect(false); |
||
639 | 6 | } |
|
640 | |||
641 | return $result; |
||
642 | } |
||
643 | |||
644 | /** |
||
645 | * Sends `EHLO` or `HELO`, depending on what's supported by the remote host. |
||
646 | * |
||
647 | * @throws NoConnectionException |
||
648 | * @throws SendFailedException |
||
649 | * @throws TimeoutException |
||
650 | 8 | * @throws UnexpectedResponseException |
|
651 | */ |
||
652 | protected function ehlo(): void |
||
653 | { |
||
654 | 8 | try { |
|
655 | 6 | // Modern |
|
656 | 2 | $this->send('EHLO ' . $this->from_domain); |
|
657 | $this->expect(self::SMTP_GENERIC_SUCCESS, $this->command_timeouts['ehlo']); |
||
658 | } catch (UnexpectedResponseException $e) { |
||
659 | // Legacy |
||
660 | $this->send('HELO ' . $this->from_domain); |
||
661 | 6 | $this->expect(self::SMTP_GENERIC_SUCCESS, $this->command_timeouts['helo']); |
|
662 | } |
||
663 | } |
||
664 | |||
665 | /** |
||
666 | * Sends a `MAIL FROM` command which indicates the sender. |
||
667 | * |
||
668 | * @param string $from |
||
669 | * |
||
670 | * @return bool Whether the command was accepted or not. |
||
671 | * |
||
672 | * @throws NoConnectionException |
||
673 | * @throws NoHeloException |
||
674 | * @throws SendFailedException |
||
675 | * @throws TimeoutException |
||
676 | 6 | * @throws UnexpectedResponseException |
|
677 | */ |
||
678 | 6 | protected function mail(string $from): bool |
|
679 | { |
||
680 | if (!$this->state['helo']) { |
||
681 | throw new NoHeloException('Need HELO before MAIL FROM'); |
||
682 | } |
||
683 | 6 | ||
684 | // Issue MAIL FROM, 5 minute timeout |
||
685 | $this->send('MAIL FROM:<' . $from . '>'); |
||
686 | 6 | ||
687 | try { |
||
688 | $this->expect(self::SMTP_GENERIC_SUCCESS, $this->command_timeouts['mail']); |
||
689 | 5 | ||
690 | 5 | // Set state flags |
|
691 | $this->state['mail'] = true; |
||
692 | 5 | $this->state['rcpt'] = false; |
|
693 | 1 | ||
694 | 1 | $result = true; |
|
695 | } catch (UnexpectedResponseException $e) { |
||
696 | $result = false; |
||
697 | 1 | ||
698 | // Got something unexpected in response to MAIL FROM |
||
699 | $this->debug("Unexpected response to MAIL FROM\n:" . $e->getMessage()); |
||
700 | |||
701 | 1 | // Hotmail has been known to do this + was closing the connection |
|
702 | // forcibly on their end, so we're killing the socket here too |
||
703 | $this->disconnect(false); |
||
704 | 6 | } |
|
705 | |||
706 | return $result; |
||
707 | } |
||
708 | |||
709 | /** |
||
710 | * Sends a RCPT TO command to indicate a recipient. Returns whether the |
||
711 | * recipient was accepted or not. |
||
712 | * |
||
713 | * @param string $to Recipient (email address). |
||
714 | * |
||
715 | * @return bool Whether the address was accepted or not. |
||
716 | * |
||
717 | 5 | * @throws NoMailFromException |
|
718 | */ |
||
719 | protected function rcpt(string $to): bool |
||
720 | 5 | { |
|
721 | // Need to have issued MAIL FROM first |
||
722 | if (!$this->state['mail']) { |
||
723 | throw new NoMailFromException('Need MAIL FROM before RCPT TO'); |
||
724 | 5 | } |
|
725 | |||
726 | 5 | $valid = false; |
|
727 | 5 | $expected_codes = [ |
|
728 | self::SMTP_GENERIC_SUCCESS, |
||
729 | self::SMTP_USER_NOT_LOCAL |
||
730 | 5 | ]; |
|
731 | 5 | ||
732 | if ($this->greylisted_considered_valid) { |
||
733 | $expected_codes = array_merge($expected_codes, $this->greylisted); |
||
734 | } |
||
735 | |||
736 | 5 | // Issue RCPT TO, 5 minute timeout |
|
737 | try { |
||
738 | $this->send('RCPT TO:<' . $to . '>'); |
||
739 | 5 | // Handle response |
|
740 | 5 | try { |
|
741 | 5 | $this->expect($expected_codes, $this->command_timeouts['rcpt']); |
|
742 | $this->state['rcpt'] = true; |
||
743 | 5 | $valid = true; |
|
744 | } catch (UnexpectedResponseException $e) { |
||
745 | $this->debug('Unexpected response to RCPT TO: ' . $e->getMessage()); |
||
746 | } |
||
747 | } catch (Exception $e) { |
||
748 | $this->debug('Sending RCPT TO failed: ' . $e->getMessage()); |
||
749 | 5 | } |
|
750 | |||
751 | return $valid; |
||
752 | } |
||
753 | |||
754 | /** |
||
755 | * Sends a RSET command and resets certain parts of internal state. |
||
756 | * |
||
757 | * @throws NoConnectionException |
||
758 | * @throws SendFailedException |
||
759 | * @throws TimeoutException |
||
760 | 4 | * @throws UnexpectedResponseException |
|
761 | */ |
||
762 | 4 | protected function rset(): void |
|
763 | { |
||
764 | $this->send('RSET'); |
||
765 | |||
766 | 4 | // MS ESMTP doesn't follow RFC according to ZF tracker, see [ZF-1377] |
|
767 | 4 | $expected = [ |
|
768 | 4 | self::SMTP_GENERIC_SUCCESS, |
|
769 | self::SMTP_CONNECT_SUCCESS, |
||
770 | 4 | self::SMTP_NOT_IMPLEMENTED, |
|
771 | // hotmail returns this o_O |
||
772 | 4 | self::SMTP_TRANSACTION_FAILED |
|
773 | 4 | ]; |
|
774 | 4 | $this->expect($expected, $this->command_timeouts['rset'], true); |
|
775 | 4 | $this->state['mail'] = false; |
|
776 | $this->state['rcpt'] = false; |
||
777 | } |
||
778 | |||
779 | /** |
||
780 | * Sends a QUIT command. |
||
781 | * |
||
782 | * @throws NoConnectionException |
||
783 | * @throws SendFailedException |
||
784 | * @throws TimeoutException |
||
785 | 4 | * @throws UnexpectedResponseException |
|
786 | */ |
||
787 | protected function quit(): void |
||
788 | 4 | { |
|
789 | 4 | // Although RFC says QUIT can be issued at any time, we won't |
|
790 | 4 | if ($this->state['helo']) { |
|
791 | 4 | $this->send('QUIT'); |
|
792 | 4 | $this->expect( |
|
793 | 4 | [self::SMTP_GENERIC_SUCCESS,self::SMTP_QUIT_SUCCESS], |
|
794 | $this->command_timeouts['quit'], |
||
795 | true |
||
796 | 4 | ); |
|
797 | } |
||
798 | } |
||
799 | |||
800 | /** |
||
801 | * Sends a NOOP command. |
||
802 | * |
||
803 | * @throws NoConnectionException |
||
804 | * @throws SendFailedException |
||
805 | * @throws TimeoutException |
||
806 | 4 | * @throws UnexpectedResponseException |
|
807 | */ |
||
808 | protected function noop(): void |
||
809 | 4 | { |
|
810 | 1 | // Bail if NOOPs are not to be sent. |
|
811 | if (!$this->send_noops) { |
||
812 | return; |
||
813 | 3 | } |
|
814 | |||
815 | $this->send('NOOP'); |
||
816 | |||
817 | /** |
||
818 | * The `SMTP` string is here to fix issues with some bad RFC implementations. |
||
819 | * Found at least 1 server replying to NOOP without any code. |
||
820 | 3 | */ |
|
821 | 3 | $expected_codes = [ |
|
822 | 3 | 'SMTP', |
|
823 | 3 | self::SMTP_BAD_SEQUENCE, |
|
824 | 3 | self::SMTP_NOT_IMPLEMENTED, |
|
825 | 3 | self::SMTP_GENERIC_SUCCESS, |
|
826 | self::SMTP_SYNTAX_ERROR, |
||
827 | 3 | self::SMTP_CONNECT_SUCCESS |
|
828 | 3 | ]; |
|
829 | $this->expect($expected_codes, $this->command_timeouts['noop'], true); |
||
830 | } |
||
831 | |||
832 | /** |
||
833 | * Sends a command to the remote host. |
||
834 | * |
||
835 | * @param string $cmd The command to send. |
||
836 | * |
||
837 | * @return int Number of bytes written to the stream. |
||
838 | * |
||
839 | * @throws NoConnectionException |
||
840 | 8 | * @throws SendFailedException |
|
841 | */ |
||
842 | protected function send(string $cmd): int |
||
843 | 8 | { |
|
844 | // Must be connected |
||
845 | 6 | $this->throwIfNotConnected(); |
|
846 | |||
847 | 6 | $this->debug('send>>>: ' . $cmd); |
|
848 | // Write the cmd to the connection stream |
||
849 | $result = fwrite($this->socket, $cmd . self::CRLF); |
||
850 | 6 | ||
851 | // Did it work? |
||
852 | if (false === $result) { |
||
853 | throw new SendFailedException('Send failed on: ' . $this->host); |
||
854 | 6 | } |
|
855 | |||
856 | return $result; |
||
857 | } |
||
858 | |||
859 | /** |
||
860 | * Receives a response line from the remote host. |
||
861 | * |
||
862 | * @param int|null $timeout Timeout in seconds. |
||
863 | * |
||
864 | * @return string Response line from the remote host. |
||
865 | * |
||
866 | * @throws NoConnectionException |
||
867 | * @throws TimeoutException |
||
868 | 8 | * @throws NoResponseException |
|
869 | */ |
||
870 | protected function recv(?int $timeout = null): string |
||
871 | 8 | { |
|
872 | // Must be connected |
||
873 | $this->throwIfNotConnected(); |
||
874 | 8 | ||
875 | 8 | // Has a custom timeout been specified? |
|
876 | if (null !== $timeout) { |
||
877 | stream_set_timeout($this->socket, $timeout); |
||
878 | } |
||
879 | 8 | ||
880 | 8 | // Retrieve response |
|
881 | $line = fgets($this->socket, 1024); |
||
882 | $this->debug('<<<recv: ' . $line); |
||
883 | 8 | ||
884 | 8 | // Have we timed out? |
|
885 | $info = stream_get_meta_data($this->socket); |
||
886 | if (!empty($info['timed_out'])) { |
||
887 | throw new TimeoutException('Timed out in recv'); |
||
888 | } |
||
889 | 8 | ||
890 | 2 | // Did we actually receive anything? |
|
891 | if (false === $line) { |
||
892 | throw new NoResponseException('No response in recv'); |
||
893 | 6 | } |
|
894 | |||
895 | return $line; |
||
896 | } |
||
897 | |||
898 | /** |
||
899 | * @param int|int[]|array|string $codes List of one or more expected response codes. |
||
900 | * @param int|null $timeout The timeout for this individual command, if any. |
||
901 | * @param bool $empty_response_allowed When true, empty responses are allowed. |
||
902 | * |
||
903 | * @return string The last text message received. |
||
904 | * |
||
905 | * @throws NoConnectionException |
||
906 | * @throws SendFailedException |
||
907 | * @throws TimeoutException |
||
908 | 8 | * @throws UnexpectedResponseException |
|
909 | */ |
||
910 | 8 | protected function expect($codes, ?int $timeout = null, bool $empty_response_allowed = false): string |
|
911 | 8 | { |
|
912 | if (!is_array($codes)) { |
||
913 | $codes = (array) $codes; |
||
914 | 8 | } |
|
915 | 8 | ||
916 | $code = null; |
||
917 | $text = ''; |
||
918 | 8 | ||
919 | 6 | try { |
|
920 | 6 | $line = $this->recv($timeout); |
|
921 | 6 | $text = $line; |
|
922 | 6 | while (preg_match('/^\d+-/', $line)) { |
|
923 | $line = $this->recv($timeout); |
||
924 | 6 | $text .= $line; |
|
925 | } |
||
926 | 6 | sscanf($line, '%d%s', $code, $text); |
|
927 | 6 | // TODO/FIXME: This is terrible to read/comprehend |
|
928 | 6 | if ($code === self::SMTP_SERVICE_UNAVAILABLE || |
|
929 | (false === $empty_response_allowed && (null === $code || !in_array($code, $codes, true)))) { |
||
930 | 3 | throw new UnexpectedResponseException($line); |
|
931 | } |
||
932 | } catch (NoResponseException $e) { |
||
933 | /** |
||
934 | * No response in expect() probably means that the remote server |
||
935 | 2 | * forcibly closed the connection so let's clean up on our end as well? |
|
936 | 2 | */ |
|
937 | $this->debug('No response in expect(): ' . $e->getMessage()); |
||
938 | $this->disconnect(false); |
||
939 | 8 | } |
|
940 | |||
941 | return $text; |
||
942 | } |
||
943 | |||
944 | /** |
||
945 | * Splits the email address string into its respective user and domain parts |
||
946 | * and returns those as an array. |
||
947 | * |
||
948 | * @param string $email Email address. |
||
949 | * |
||
950 | 14 | * @return array ['user', 'domain'] |
|
951 | */ |
||
952 | 14 | protected function splitEmail(string $email): array |
|
953 | 14 | { |
|
954 | 14 | $parts = explode('@', $email); |
|
955 | $domain = array_pop($parts); |
||
956 | 14 | $user = implode('@', $parts); |
|
957 | |||
958 | return [$user, $domain]; |
||
959 | } |
||
960 | |||
961 | /** |
||
962 | * Sets the email addresses that should be validated. |
||
963 | * |
||
964 | 13 | * @param array|string $emails List of email addresses (or a single one a string). |
|
965 | */ |
||
966 | 13 | public function setEmails($emails): void |
|
967 | 12 | { |
|
968 | if (!is_array($emails)) { |
||
969 | $emails = (array) $emails; |
||
970 | 13 | } |
|
971 | |||
972 | 13 | $this->domains = []; |
|
973 | 13 | ||
974 | 13 | foreach ($emails as $email) { |
|
975 | 13 | [$user, $domain] = $this->splitEmail($email); |
|
976 | if (!isset($this->domains[$domain])) { |
||
977 | 13 | $this->domains[$domain] = []; |
|
978 | } |
||
979 | 13 | $this->domains[$domain][] = $user; |
|
980 | } |
||
981 | } |
||
982 | |||
983 | /** |
||
984 | * Sets the email address to use as the sender/validator. |
||
985 | * |
||
986 | 13 | * @param string $email |
|
987 | */ |
||
988 | 13 | public function setSender(string $email): void |
|
989 | 13 | { |
|
990 | 13 | $parts = $this->splitEmail($email); |
|
991 | 13 | $this->from_user = $parts[0]; |
|
992 | $this->from_domain = $parts[1]; |
||
993 | } |
||
994 | |||
995 | /** |
||
996 | * Queries the DNS server for MX entries of a certain domain. |
||
997 | * |
||
998 | * @param string $domain The domain for which to retrieve MX records. |
||
999 | * |
||
1000 | 12 | * @return array MX hosts and their weights. |
|
1001 | */ |
||
1002 | 12 | protected function mxQuery(string $domain): array |
|
1003 | 12 | { |
|
1004 | 12 | // If the domain does not end with a '.', add it (making it an absolute fqdn, which prevents any |
|
1005 | // further suffixing attempts by wrongly configured resolvers etc.) |
||
1006 | 12 | if (!preg_match('/\.$/', $domain)) { |
|
1007 | $domain .= '.'; |
||
1008 | } |
||
1009 | |||
1010 | $hosts = []; |
||
1011 | $weight = []; |
||
1012 | getmxrr($domain, $hosts, $weight); |
||
1013 | |||
1014 | 8 | return [$hosts, $weight]; |
|
1015 | } |
||
1016 | 8 | ||
1017 | 2 | /** |
|
1018 | * Throws if not currently connected. |
||
1019 | 8 | * |
|
1020 | * @throws NoConnectionException |
||
1021 | */ |
||
1022 | private function throwIfNotConnected(): void |
||
1023 | { |
||
1024 | if (!$this->connected()) { |
||
1025 | throw new NoConnectionException('No connection'); |
||
1026 | } |
||
1027 | 12 | } |
|
1028 | |||
1029 | 12 | /** |
|
1030 | 12 | * Debug helper. If it detects a CLI env, it just dumps given `$str` on a |
|
1031 | 12 | * new line, otherwise it prints stuff wrapped in <pre> tags. |
|
1032 | 1 | * |
|
1033 | * @param string $str |
||
1034 | */ |
||
1035 | 1 | private function debug(string $str): void |
|
1036 | { |
||
1037 | 12 | $str = $this->stamp($str); |
|
1038 | |||
1039 | $this->log($str); |
||
1040 | |||
1041 | if ($this->debug) { |
||
1042 | if ('cli' !== PHP_SAPI) { |
||
1043 | $str = '<br/><pre>' . htmlspecialchars($str) . '</pre>'; |
||
1044 | 12 | } |
|
1045 | echo "\n" . $str; |
||
1046 | 12 | } |
|
1047 | 12 | } |
|
1048 | |||
1049 | /** |
||
1050 | * Adds a message to the log array |
||
1051 | * |
||
1052 | * @param string $msg |
||
1053 | */ |
||
1054 | private function log(string $msg): void |
||
1055 | { |
||
1056 | 12 | $this->log[] = $msg; |
|
1057 | } |
||
1058 | 12 | ||
1059 | 12 | /** |
|
1060 | * Prepends the given $msg with the current date and time inside square brackets. |
||
1061 | 12 | * |
|
1062 | * @param string $msg |
||
1063 | * |
||
1064 | * @return string |
||
1065 | */ |
||
1066 | private function stamp(string $msg): string |
||
1067 | { |
||
1068 | return '[' . $this->getLogDate() . '] ' . $msg; |
||
1069 | 6 | } |
|
1070 | |||
1071 | 6 | /** |
|
1072 | * Logging helper which returns (formatted) current date and time |
||
1073 | * (with microseconds) but avoids sprintf/microtime(true) combo. |
||
1074 | * Empty string returned on failure. |
||
1075 | * |
||
1076 | * @see https://github.com/zytzagoo/smtp-validate-email/pull/58 |
||
1077 | 1 | * |
|
1078 | * @return string |
||
1079 | 1 | */ |
|
1080 | 1 | public function getLogDate(): string |
|
1081 | { |
||
1082 | $dt = \DateTime::createFromFormat('0.u00 U', microtime()); |
||
1083 | |||
1084 | $date = ''; |
||
1085 | if (false !== $dt) { |
||
1086 | $date = $dt->format('Y-m-d\TH:i:s.uO'); |
||
1087 | } |
||
1088 | |||
1089 | return $date; |
||
1090 | 2 | } |
|
1091 | |||
1092 | 2 | /** |
|
1093 | 2 | * Returns the log array. |
|
1094 | 2 | * |
|
1095 | * @return array |
||
1096 | 1 | */ |
|
1097 | public function getLog(): array |
||
1098 | { |
||
1099 | return $this->log; |
||
1100 | } |
||
1101 | |||
1102 | /** |
||
1103 | * Truncates the log array. |
||
1104 | */ |
||
1105 | 10 | public function clearLog(): void |
|
1106 | { |
||
1107 | 10 | $this->log = []; |
|
1108 | 10 | } |
|
1109 | |||
1110 | /** |
||
1111 | * Compat for old lower_cased method calls. |
||
1112 | * |
||
1113 | * @param string $name |
||
1114 | * @param array $args |
||
1115 | 1 | * |
|
1116 | * @return mixed |
||
1117 | 1 | */ |
|
1118 | public function __call(string $name, array $args) |
||
1119 | { |
||
1120 | $camelized = self::camelize($name); |
||
1121 | if (\method_exists($this, $camelized)) { |
||
1122 | return \call_user_func_array([$this, $camelized], $args); |
||
1123 | } |
||
1124 | |||
1125 | 9 | trigger_error('Fatal error: Call to undefined method ' . self::class . '::' . $name . '()', E_USER_ERROR); |
|
1126 | } |
||
1127 | 9 | ||
1128 | 9 | /** |
|
1129 | * Set the desired connect timeout. |
||
1130 | * |
||
1131 | * @param int $timeout Connect timeout in seconds. |
||
1132 | */ |
||
1133 | public function setConnectTimeout(int $timeout): void |
||
1134 | { |
||
1135 | 1 | $this->connect_timeout = $timeout; |
|
1136 | } |
||
1137 | 1 | ||
1138 | /** |
||
1139 | * Get the current connect timeout. |
||
1140 | * |
||
1141 | * @return int |
||
1142 | */ |
||
1143 | 3 | public function getConnectTimeout(): int |
|
1144 | { |
||
1145 | 3 | return $this->connect_timeout; |
|
1146 | 3 | } |
|
1147 | |||
1148 | /** |
||
1149 | * Set connect port. |
||
1150 | * |
||
1151 | 1 | * @param int $port |
|
1152 | */ |
||
1153 | 1 | public function setConnectPort(int $port): void |
|
1154 | 1 | { |
|
1155 | $this->connect_port = $port; |
||
1156 | } |
||
1157 | |||
1158 | /** |
||
1159 | * Get current connect port. |
||
1160 | * |
||
1161 | 1 | * @return int |
|
1162 | */ |
||
1163 | 1 | public function getConnectPort(): int |
|
1164 | { |
||
1165 | return $this->connect_port; |
||
1166 | } |
||
1167 | |||
1168 | /** |
||
1169 | * Turn on "catch-all" detection. |
||
1170 | */ |
||
1171 | 2 | public function enableCatchAllTest(): void |
|
1172 | { |
||
1173 | 2 | $this->catchall_test = true; |
|
1174 | 2 | } |
|
1175 | |||
1176 | /** |
||
1177 | * Turn off "catch-all" detection. |
||
1178 | */ |
||
1179 | public function disableCatchAllTest(): void |
||
1180 | { |
||
1181 | 1 | $this->catchall_test = false; |
|
1182 | } |
||
1183 | 1 | ||
1184 | /** |
||
1185 | * Returns whether "catch-all" test is to be performed or not. |
||
1186 | * |
||
1187 | * @return bool |
||
1188 | */ |
||
1189 | public function isCatchAllEnabled(): bool |
||
1190 | { |
||
1191 | 2 | return $this->catchall_test; |
|
1192 | } |
||
1193 | 2 | ||
1194 | 2 | /** |
|
1195 | * Set whether "catch-all" results are considered valid or not. |
||
1196 | * |
||
1197 | * @param bool $flag When true, "catch-all" accounts are considered valid |
||
1198 | */ |
||
1199 | 1 | public function setCatchAllValidity(bool $flag): void |
|
1200 | { |
||
1201 | 1 | $this->catchall_is_valid = $flag; |
|
1202 | } |
||
1203 | |||
1204 | /** |
||
1205 | * Get current state of "catch-all" validity flag. |
||
1206 | * |
||
1207 | * @return bool |
||
1208 | */ |
||
1209 | public function getCatchAllValidity(): bool |
||
1210 | { |
||
1211 | 2 | return $this->catchall_is_valid; |
|
1212 | } |
||
1213 | 2 | ||
1214 | 2 | /** |
|
1215 | 2 | * Control sending of NOOP commands. |
|
1216 | 2 | * |
|
1217 | 2 | * @param bool $val |
|
1218 | */ |
||
1219 | public function sendNoops(bool $val): void |
||
1220 | 2 | { |
|
1221 | $this->send_noops = $val; |
||
1222 | } |
||
1223 | |||
1224 | /** |
||
1225 | * @return bool |
||
1226 | */ |
||
1227 | public function sendingNoops(): bool |
||
1228 | { |
||
1229 | return $this->send_noops; |
||
1230 | } |
||
1231 | |||
1232 | /** |
||
1233 | * Specify the socket bind address. |
||
1234 | * |
||
1235 | * This can be used to specify the IP address (v4 or v6) and/or the port number that |
||
1236 | * PHP will use to access the network. The syntax is ip:port for v4 and [ip]:port for v6. |
||
1237 | * Setting the IP or the port to 0 lets the system choose the IP and/or port. |
||
1238 | * When no port is explicitly provided, it's defaulted to 0. |
||
1239 | * |
||
1240 | * @param string $bindAddress Socket bind address in `ip:port` or `[ip]:port` syntax |
||
1241 | * |
||
1242 | * @return void |
||
1243 | */ |
||
1244 | public function setBindAddress(string $bindAddress): void |
||
1245 | { |
||
1246 | $ipWithPort = $this->parseBindAddress($bindAddress); |
||
1247 | |||
1248 | $this->stream_context_args['socket']['bindto'] = $ipWithPort; |
||
1249 | } |
||
1250 | |||
1251 | /** |
||
1252 | * Get the configured socket bind address. Null means system default is used. |
||
1253 | * |
||
1254 | * @return string|null |
||
1255 | */ |
||
1256 | public function getBindAddress(): ?string |
||
1257 | { |
||
1258 | return $this->stream_context_args['socket']['bindto'] ?? null; |
||
1259 | } |
||
1260 | |||
1261 | /** |
||
1262 | * Parse most commonly used ways of specifying socket bind addresses |
||
1263 | * into an `ip:port` or `[ip]:port` format/syntax. |
||
1264 | * |
||
1265 | * @param string $bindAddress |
||
1266 | * |
||
1267 | * @return string |
||
1268 | */ |
||
1269 | protected function parseBindAddress(string $bindAddress): string |
||
1270 | { |
||
1271 | // TODO/FIXME: This should be way more robust if all possible edge-cases are supposed to work |
||
1272 | |||
1273 | if (($bindAddress[0] !== '[') && filter_var($bindAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { |
||
1274 | // If given string does not start with [, but is valid ipv6, wrap it in [] and |
||
1275 | // assume port is 0 |
||
1276 | $ip = '[' . $bindAddress . ']'; |
||
1277 | $port = '0'; |
||
1278 | } else { |
||
1279 | // If address starts with [ or does not appear to be ipv6, let parse_url() handle it |
||
1280 | $parts = @parse_url('https://' . $bindAddress); |
||
1281 | $ip = $parts['host'] ?? $bindAddress; |
||
1282 | $port = $parts['port'] ?? '0'; |
||
1283 | } |
||
1284 | |||
1285 | return $ip . ':' . $port; |
||
1286 | } |
||
1287 | |||
1288 | /** |
||
1289 | * Camelizes a string. |
||
1290 | * |
||
1291 | * @param string $id String to camelize. |
||
1292 | * |
||
1293 | * @return string |
||
1294 | */ |
||
1295 | private static function camelize(string $id): string |
||
1296 | { |
||
1297 | return strtr( |
||
1298 | ucwords( |
||
1299 | strtr( |
||
1300 | $id, |
||
1301 | ['_' => ' ', '.' => '_ ', '\\' => '_ '] |
||
1302 | ) |
||
1303 | ), |
||
1304 | [' ' => ''] |
||
1305 | ); |
||
1306 | } |
||
1307 | } |
||
1308 |