1 | <?php |
||
2 | /** |
||
3 | * SMTP_Validate_Email - Perform email address verification via SMTP. |
||
4 | * Copyright (C) 2009 Tomaš Trkulja [zytzagoo] <[email protected]> |
||
5 | * |
||
6 | * This program is free software: you can redistribute it and/or modify |
||
7 | * it under the terms of the GNU General Public License as published by |
||
8 | * the Free Software Foundation, either version 3 of the License, or |
||
9 | * (at your option) any later version. |
||
10 | * |
||
11 | * This program is distributed in the hope that it will be useful, |
||
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
14 | * GNU General Public License for more details. |
||
15 | * |
||
16 | * You should have received a copy of the GNU General Public License |
||
17 | * along with this program. If not, see <http://www.gnu.org/licenses/>. |
||
18 | * |
||
19 | * @version 0.7 |
||
20 | * @todo |
||
21 | * - finish the graylisting thingy |
||
22 | * - perhaps re-implement some methods as static? |
||
23 | * - introduce a main socket loop if this state-based approach doesn't work out |
||
24 | * - implement TLS probably |
||
25 | * - more code examples, more tests |
||
26 | * |
||
27 | * The class retrieves MX records for the email domain and then connects to the |
||
28 | * domain's SMTP server to try figuring out if the address is really valid. |
||
29 | * |
||
30 | * Some ideas taken from: http://code.google.com/p/php-smtp-email-validation |
||
31 | * See the source and comments for more details. |
||
32 | */ |
||
33 | |||
34 | // Exceptions we throw |
||
35 | class SMTP_Validate_Email_Exception extends Exception {} |
||
36 | class SMTP_Validate_Email_Exception_Timeout extends SMTP_Validate_Email_Exception {} |
||
37 | class SMTP_Validate_Email_Exception_Unexpected_Response extends SMTP_Validate_Email_Exception {} |
||
38 | class SMTP_Validate_Email_Exception_No_Response extends SMTP_Validate_Email_Exception {} |
||
39 | class SMTP_Validate_Email_Exception_No_Connection extends SMTP_Validate_Email_Exception {} |
||
40 | class SMTP_Validate_Email_Exception_No_Helo extends SMTP_Validate_Email_Exception {} |
||
41 | class SMTP_Validate_Email_Exception_No_Mail_From extends SMTP_Validate_Email_Exception {} |
||
42 | class SMTP_Validate_Email_Exception_No_Timeout extends SMTP_Validate_Email_Exception {} |
||
43 | class SMTP_Validate_Email_Exception_No_TLS extends SMTP_Validate_Email_Exception {} |
||
44 | class SMTP_Validate_Email_Exception_Send_Failed extends SMTP_Validate_Email_Exception {} |
||
45 | |||
46 | // SMTP validation class |
||
47 | class SMTP_Validate_Email { |
||
48 | |||
49 | // holds the socket connection resource |
||
50 | private $socket; |
||
51 | |||
52 | // holds all the domains we'll validate accounts on |
||
53 | private $domains; |
||
54 | |||
55 | private $domains_info = array(); |
||
56 | |||
57 | // connect timeout for each MTA attempted (seconds) |
||
58 | private $connect_timeout = 10; |
||
59 | |||
60 | // default username of sender |
||
61 | private $from_user = 'user'; |
||
62 | |||
63 | // default host of sender |
||
64 | private $from_domain = 'localhost'; |
||
65 | |||
66 | // the host we're currently connected to |
||
67 | private $host = null; |
||
68 | |||
69 | // holds all the debug info |
||
70 | public $log = array(); |
||
71 | |||
72 | // array of validation results |
||
73 | private $results = array(); |
||
74 | |||
75 | // states we can be in |
||
76 | private $state = array( |
||
77 | 'helo' => false, |
||
78 | 'mail' => false, |
||
79 | 'rcpt' => false |
||
80 | ); |
||
81 | |||
82 | // print stuff as it happens or not |
||
83 | public $debug = false; |
||
84 | |||
85 | // default smtp port |
||
86 | public $connect_port = 25; |
||
87 | |||
88 | /** |
||
89 | * Are 'catch-all' accounts considered valid or not? |
||
90 | * If not, the class checks for a "catch-all" and if it determines the box |
||
91 | * has a "catch-all", sets all the emails on that domain as invalid. |
||
92 | */ |
||
93 | public $catchall_is_valid = true; |
||
94 | public $catchall_test = false; // Set to true to perform a catchall test |
||
95 | |||
96 | /** |
||
97 | * Being unable to communicate with the remote MTA could mean an address |
||
98 | * is invalid, but it might not, depending on your use case, set the |
||
99 | * value appropriately. |
||
100 | */ |
||
101 | public $no_comm_is_valid = false; |
||
102 | |||
103 | /** |
||
104 | * Being unable to connect with the remote host could mean a server |
||
105 | * configuration issue, but it might not, depending on your use case, |
||
106 | * set the value appropriately. |
||
107 | */ |
||
108 | public $no_conn_is_valid = false; |
||
109 | |||
110 | // do we consider "greylisted" responses as valid or invalid addresses |
||
111 | public $greylisted_considered_valid = true; |
||
112 | |||
113 | /** |
||
114 | * If on Windows (or other places that don't have getmxrr()), this is the |
||
115 | * nameserver that will be used for MX querying. |
||
116 | * Set as empty to use the DNS specified via your current network connection. |
||
117 | * @see getmxrr() |
||
118 | */ |
||
119 | // protected $mx_query_ns = 'dns1.t-com.hr'; |
||
120 | protected $mx_query_ns = ''; |
||
121 | |||
122 | /** |
||
123 | * Timeout values for various commands (in seconds) per RFC 2821 |
||
124 | * @see expect() |
||
125 | */ |
||
126 | protected $command_timeouts = array( |
||
127 | 'ehlo' => 120, |
||
128 | 'helo' => 120, |
||
129 | 'tls' => 180, // start tls |
||
130 | 'mail' => 300, // mail from |
||
131 | 'rcpt' => 300, // rcpt to, |
||
132 | 'rset' => 30, |
||
133 | 'quit' => 60, |
||
134 | 'noop' => 60 |
||
135 | ); |
||
136 | |||
137 | // some constants |
||
138 | const CRLF = "\r\n"; |
||
139 | |||
140 | // some smtp response codes |
||
141 | const SMTP_CONNECT_SUCCESS = 220; |
||
142 | const SMTP_QUIT_SUCCESS = 221; |
||
143 | const SMTP_GENERIC_SUCCESS = 250; |
||
144 | const SMTP_USER_NOT_LOCAL = 251; |
||
145 | const SMTP_CANNOT_VRFY = 252; |
||
146 | |||
147 | const SMTP_SERVICE_UNAVAILABLE = 421; |
||
148 | |||
149 | // 450 Requested mail action not taken: mailbox unavailable (e.g., |
||
150 | // mailbox busy or temporarily blocked for policy reasons) |
||
151 | const SMTP_MAIL_ACTION_NOT_TAKEN = 450; |
||
152 | // 451 Requested action aborted: local error in processing |
||
153 | const SMTP_MAIL_ACTION_ABORTED = 451; |
||
154 | // 452 Requested action not taken: insufficient system storage |
||
155 | const SMTP_REQUESTED_ACTION_NOT_TAKEN = 452; |
||
156 | |||
157 | // 500 Syntax error (may be due to a denied command) |
||
158 | const SMTP_SYNTAX_ERROR = 500; |
||
159 | // 502 Comment not implemented |
||
160 | const SMTP_NOT_IMPLEMENTED = 502; |
||
161 | // 503 Bad sequence of commands (may be due to a denied command) |
||
162 | const SMTP_BAD_SEQUENCE = 503; |
||
163 | |||
164 | // 550 Requested action not taken: mailbox unavailable (e.g., mailbox |
||
165 | // not found, no access, or command rejected for policy reasons) |
||
166 | const SMTP_MBOX_UNAVAILABLE = 550; |
||
167 | |||
168 | // 554 Seen this from hotmail MTAs, in response to RSET :( |
||
169 | const SMTP_TRANSACTION_FAILED = 554; |
||
170 | |||
171 | // list of codes considered as "greylisted" |
||
172 | private $greylisted = array( |
||
173 | self::SMTP_MAIL_ACTION_NOT_TAKEN, |
||
174 | self::SMTP_MAIL_ACTION_ABORTED, |
||
175 | self::SMTP_REQUESTED_ACTION_NOT_TAKEN |
||
176 | ); |
||
177 | |||
178 | /** |
||
179 | * Constructor. |
||
180 | * @param $emails array [optional] Array of emails to validate |
||
181 | * @param $sender string [optional] Email address of the sender/validator |
||
182 | */ |
||
183 | function __construct($emails = array(), $sender = '') { |
||
184 | if (!empty($emails)) { |
||
185 | $this->set_emails($emails); |
||
186 | } |
||
187 | if (!empty($sender)) { |
||
188 | $this->set_sender($sender); |
||
189 | } |
||
190 | } |
||
191 | |||
192 | /** |
||
193 | * Disconnects from the SMTP server if needed. |
||
194 | * @return void |
||
195 | */ |
||
196 | public function __destruct() { |
||
197 | $this->disconnect(false); |
||
198 | } |
||
199 | |||
200 | public function accepts_any_recipient($domain) { |
||
201 | if (!$this->catchall_test) { |
||
202 | return false; |
||
203 | } |
||
204 | $test = 'catch-all-test-' . time(); |
||
205 | $accepted = $this->rcpt($test . '@' . $domain); |
||
206 | if ($accepted) { |
||
207 | // success on a non-existing address is a "catch-all" |
||
208 | $this->domains_info[$domain]['catchall'] = true; |
||
209 | return true; |
||
210 | } |
||
211 | // log the case in which we get disconnected |
||
212 | // while trying to perform a catchall detect |
||
213 | $this->noop(); |
||
214 | if (!($this->connected())) { |
||
215 | $this->debug('Disconnected after trying a non-existing recipient on ' . $domain); |
||
216 | } |
||
217 | // nb: disconnects are considered as a non-catch-all case this way |
||
218 | // this might not be true always |
||
219 | return false; |
||
220 | } |
||
221 | |||
222 | /** |
||
223 | * Performs validation of specified email addresses. |
||
224 | * @param array $emails Emails to validate (recipient emails) |
||
225 | * @param string $sender Sender email address |
||
226 | * @return array List of emails and their results |
||
227 | */ |
||
228 | public function validate($emails = array(), $sender = '') { |
||
229 | |||
230 | $this->results = array(); |
||
231 | |||
232 | if (!empty($emails)) { |
||
233 | $this->set_emails($emails); |
||
234 | } |
||
235 | if (!empty($sender)) { |
||
236 | $this->set_sender($sender); |
||
237 | } |
||
238 | |||
239 | if (!is_array($this->domains) || empty($this->domains)) { |
||
240 | return $this->results; |
||
241 | } |
||
242 | |||
243 | // query the MTAs on each domain if we have them |
||
244 | foreach ($this->domains as $domain => $users) { |
||
245 | |||
246 | $mxs = array(); |
||
247 | |||
248 | // query the mx records for the current domain |
||
249 | list($hosts, $weights) = $this->mx_query($domain); |
||
250 | |||
251 | // sort out the MX priorities |
||
252 | foreach ($hosts as $k => $host) { |
||
253 | $mxs[$host] = $weights[$k]; |
||
254 | } |
||
255 | asort($mxs); |
||
256 | |||
257 | // add the hostname itself with 0 weight (RFC 2821) |
||
258 | $mxs[$domain] = 0; |
||
259 | |||
260 | $this->debug('MX records (' . $domain . '): ' . print_r($mxs, true)); |
||
261 | $this->domains_info[$domain] = array(); |
||
262 | $this->domains_info[$domain]['users'] = $users; |
||
263 | $this->domains_info[$domain]['mxs'] = $mxs; |
||
264 | |||
265 | // try each host, $_weight unused in the foreach body, but array_keys() doesn't guarantee the order |
||
266 | foreach ($mxs as $host => $_weight) { |
||
267 | // try connecting to the remote host |
||
268 | try { |
||
269 | $this->connect($host); |
||
270 | if ($this->connected()) { |
||
271 | break; |
||
272 | } |
||
273 | } catch (SMTP_Validate_Email_Exception_No_Connection $e) { |
||
274 | // unable to connect to host, so these addresses are invalid? |
||
275 | $this->debug('Unable to connect. Exception caught: ' . $e->getMessage()); |
||
276 | $this->set_domain_results($users, $domain, $this->no_conn_is_valid ); |
||
277 | } |
||
278 | } |
||
279 | |||
280 | // are we connected? |
||
281 | if ($this->connected()) { |
||
282 | try { |
||
283 | // say helo, and continue if we can talk |
||
284 | if ($this->helo()) { |
||
285 | |||
286 | // try issuing MAIL FROM |
||
287 | if (!($this->mail($this->from_user . '@' . $this->from_domain))) { |
||
288 | // MAIL FROM not accepted, we can't talk |
||
289 | $this->set_domain_results($users, $domain, $this->no_comm_is_valid); |
||
290 | } |
||
291 | |||
292 | /** |
||
293 | * if we're still connected, proceed (cause we might get |
||
294 | * disconnected, or banned, or greylisted temporarily etc.) |
||
295 | * see mail() for more |
||
296 | */ |
||
297 | if ($this->connected()) { |
||
298 | |||
299 | $this->noop(); |
||
300 | |||
301 | // attempt a catch-all test for the domain (if configured to do so) |
||
302 | $is_catchall_domain = $this->accepts_any_recipient($domain); |
||
303 | |||
304 | // if a catchall domain is detected, and we consider |
||
305 | // accounts on such domains as invalid, mark all the |
||
306 | // users as invalid and move on |
||
307 | if ($is_catchall_domain) { |
||
308 | if (!($this->catchall_is_valid)) { |
||
309 | $this->set_domain_results($users, $domain, $this->catchall_is_valid); |
||
310 | continue; |
||
311 | } |
||
312 | } |
||
313 | |||
314 | // if we're still connected, try issuing rcpts |
||
315 | if ($this->connected()) { |
||
316 | $this->noop(); |
||
317 | // rcpt to for each user |
||
318 | foreach ($users as $user) { |
||
319 | $address = $user . '@' . $domain; |
||
320 | $this->results[$address] = $this->rcpt($address); |
||
321 | $this->noop(); |
||
322 | } |
||
323 | } |
||
324 | |||
325 | // saying buh-bye if we're still connected, cause we're done here |
||
326 | if ($this->connected()) { |
||
327 | // issue a rset for all the things we just made the MTA do |
||
328 | $this->rset(); |
||
329 | // kiss it goodbye |
||
330 | $this->disconnect(); |
||
331 | } |
||
332 | |||
333 | } |
||
334 | |||
335 | } else { |
||
336 | |||
337 | // we didn't get a good response to helo and should be disconnected already |
||
338 | $this->set_domain_results($users, $domain, $this->no_comm_is_valid); |
||
339 | |||
340 | } |
||
341 | |||
342 | } catch (SMTP_Validate_Email_Exception_Unexpected_Response $e) { |
||
343 | |||
344 | // Unexpected responses handled as $this->no_comm_is_valid, that way anyone can |
||
345 | // decide for themselves if such results are considered valid or not |
||
346 | $this->set_domain_results($users, $domain, $this->no_comm_is_valid); |
||
347 | |||
348 | } catch (SMTP_Validate_Email_Exception_Timeout $e) { |
||
349 | |||
350 | // A timeout is a comm failure, so treat the results on that domain |
||
351 | // according to $this->no_comm_is_valid as well |
||
352 | $this->set_domain_results($users, $domain, $this->no_comm_is_valid); |
||
353 | |||
354 | } |
||
355 | } |
||
356 | |||
357 | } |
||
358 | |||
359 | return $this->get_results(); |
||
360 | |||
361 | } |
||
362 | |||
363 | public function get_results($include_domains_info = true) { |
||
364 | if ($include_domains_info) { |
||
365 | $this->results['domains'] = $this->domains_info; |
||
366 | } |
||
367 | return $this->results; |
||
368 | } |
||
369 | |||
370 | /** |
||
371 | * Helper to set results for all the users on a domain to a specific value |
||
372 | * @param array $users Array of users (usernames) |
||
373 | * @param string $domain The domain |
||
374 | * @param bool $val Value to set |
||
375 | */ |
||
376 | private function set_domain_results($users, $domain, $val) { |
||
377 | if (!is_array($users)) { |
||
378 | $users = (array) $users; |
||
379 | } |
||
380 | foreach ($users as $user) { |
||
381 | $this->results[$user . '@' . $domain] = $val; |
||
382 | } |
||
383 | } |
||
384 | |||
385 | /** |
||
386 | * Returns true if we're connected to an MTA |
||
387 | * @return bool |
||
388 | */ |
||
389 | protected function connected() { |
||
390 | return is_resource($this->socket); |
||
391 | } |
||
392 | |||
393 | /** |
||
394 | * Tries to connect to the specified host on the pre-configured port. |
||
395 | * @param string $host The host to connect to |
||
396 | * @return void |
||
397 | * @throws SMTP_Validate_Email_Exception_No_Connection |
||
398 | * @throws SMTP_Validate_Email_Exception_No_Timeout |
||
399 | */ |
||
400 | protected function connect($host) { |
||
401 | $remote_socket = $host . ':' . $this->connect_port; |
||
402 | $errnum = 0; |
||
403 | $errstr = ''; |
||
404 | $this->host = $remote_socket; |
||
405 | // open connection |
||
406 | $this->debug('Connecting to ' . $this->host); |
||
407 | $this->socket = @stream_socket_client( |
||
408 | $this->host, |
||
409 | $errnum, |
||
410 | $errstr, |
||
411 | $this->connect_timeout, |
||
412 | STREAM_CLIENT_CONNECT, |
||
413 | stream_context_create(array()) |
||
414 | ); |
||
415 | // connected? |
||
416 | if (!$this->connected()) { |
||
417 | $this->debug('Connect failed: ' . $errstr . ', error number: ' . $errnum . ', host: ' . $this->host); |
||
418 | throw new SMTP_Validate_Email_Exception_No_Connection('Cannot ' . |
||
419 | 'open a connection to remote host (' . $this->host . ')'); |
||
420 | } |
||
421 | $result = stream_set_timeout($this->socket, $this->connect_timeout); |
||
422 | if (!$result) { |
||
423 | throw new SMTP_Validate_Email_Exception_No_Timeout('Cannot set timeout'); |
||
424 | } |
||
425 | $this->debug('Connected to ' . $this->host . ' successfully'); |
||
426 | } |
||
427 | |||
428 | /** |
||
429 | * Disconnects the currently connected MTA. |
||
430 | * @param bool $quit Issue QUIT before closing the socket on our end. |
||
431 | * @return void |
||
432 | */ |
||
433 | protected function disconnect($quit = true) { |
||
434 | if ($quit) { |
||
435 | $this->quit(); |
||
436 | } |
||
437 | if ($this->connected()) { |
||
438 | $this->debug('Closing socket to ' . $this->host); |
||
439 | fclose($this->socket); |
||
440 | } |
||
441 | $this->host = null; |
||
442 | $this->reset_state(); |
||
443 | } |
||
444 | |||
445 | /** |
||
446 | * Resets internal state flags to defaults |
||
447 | */ |
||
448 | private function reset_state() { |
||
449 | $this->state['helo'] = false; |
||
450 | $this->state['mail'] = false; |
||
451 | $this->state['rcpt'] = false; |
||
452 | } |
||
453 | |||
454 | /** |
||
455 | * Sends a HELO/EHLO sequence |
||
456 | * @todo Implement TLS |
||
457 | * @return bool|null True if successful, false otherwise |
||
458 | */ |
||
459 | protected function helo() { |
||
460 | // don't try if it was already done |
||
461 | if ($this->state['helo']) { |
||
462 | return null; |
||
463 | } |
||
464 | try { |
||
465 | $this->expect(self::SMTP_CONNECT_SUCCESS, $this->command_timeouts['helo']); |
||
466 | $this->ehlo(); |
||
467 | // session started |
||
468 | $this->state['helo'] = true; |
||
469 | // are we going for a TLS connection? |
||
470 | /* |
||
471 | if ($this->tls == true) { |
||
472 | // send STARTTLS, wait 3 minutes |
||
473 | $this->send('STARTTLS'); |
||
474 | $this->expect(self::SMTP_CONNECT_SUCCESS, $this->command_timeouts['tls']); |
||
475 | $result = stream_socket_enable_crypto($this->socket, true, |
||
476 | STREAM_CRYPTO_METHOD_TLS_CLIENT); |
||
477 | if (!$result) { |
||
478 | throw new SMTP_Validate_Email_Exception_No_TLS('Cannot enable TLS'); |
||
479 | } |
||
480 | } |
||
481 | */ |
||
482 | return true; |
||
483 | } catch (SMTP_Validate_Email_Exception_Unexpected_Response $e) { |
||
484 | // connected, but recieved an unexpected response, so disconnect |
||
485 | $this->debug('Unexpected response after connecting: ' . $e->getMessage()); |
||
486 | $this->disconnect(false); |
||
487 | return false; |
||
488 | } |
||
489 | } |
||
490 | |||
491 | /** |
||
492 | * Send EHLO or HELO, depending on what's supported by the remote host. |
||
493 | * @return void |
||
494 | */ |
||
495 | protected function ehlo() { |
||
496 | try { |
||
497 | // modern |
||
498 | $this->send('EHLO ' . $this->from_domain); |
||
499 | $this->expect(self::SMTP_GENERIC_SUCCESS, $this->command_timeouts['ehlo']); |
||
500 | } catch (SMTP_Validate_Email_Exception_Unexpected_Response $e) { |
||
501 | // legacy |
||
502 | $this->send('HELO ' . $this->from_domain); |
||
503 | $this->expect(self::SMTP_GENERIC_SUCCESS, $this->command_timeouts['helo']); |
||
504 | } |
||
505 | } |
||
506 | |||
507 | /** |
||
508 | * Sends a MAIL FROM command to indicate the sender. |
||
509 | * @param string $from The "From:" address |
||
510 | * @return bool If MAIL FROM command was accepted or not |
||
511 | * @throws SMTP_Validate_Email_Exception_No_Helo |
||
512 | */ |
||
513 | protected function mail($from) { |
||
514 | if (!$this->state['helo']) { |
||
515 | throw new SMTP_Validate_Email_Exception_No_Helo('Need HELO before MAIL FROM'); |
||
516 | } |
||
517 | // issue MAIL FROM, 5 minute timeout |
||
518 | $this->send('MAIL FROM:<' . $from . '>'); |
||
519 | try { |
||
520 | $this->expect(self::SMTP_GENERIC_SUCCESS, $this->command_timeouts['mail']); |
||
521 | // set state flags |
||
522 | $this->state['mail'] = true; |
||
523 | $this->state['rcpt'] = false; |
||
524 | return true; |
||
525 | } catch (SMTP_Validate_Email_Exception_Unexpected_Response $e) { |
||
526 | // got something unexpected in response to MAIL FROM |
||
527 | $this->debug("Unexpected response to MAIL FROM\n:" . $e->getMessage()); |
||
528 | // hotmail has been known to do this + was closing the connection |
||
529 | // forcibly on their end, so we're killing the socket here too |
||
530 | $this->disconnect(false); |
||
531 | return false; |
||
532 | } |
||
533 | } |
||
534 | |||
535 | /** |
||
536 | * Sends a RCPT TO command to indicate a recipient. |
||
537 | * @param string $to Recipient's email address |
||
538 | * @return bool Is the recipient accepted |
||
539 | * @throws SMTP_Validate_Email_Exception_No_Mail_From |
||
540 | */ |
||
541 | protected function rcpt($to) { |
||
542 | // need to have issued MAIL FROM first |
||
543 | if (!$this->state['mail']) { |
||
544 | throw new SMTP_Validate_Email_Exception_No_Mail_From('Need MAIL FROM before RCPT TO'); |
||
545 | } |
||
546 | $is_valid = false; |
||
547 | $expected_codes = array( |
||
548 | self::SMTP_GENERIC_SUCCESS, |
||
549 | self::SMTP_USER_NOT_LOCAL |
||
550 | ); |
||
551 | if ($this->greylisted_considered_valid) { |
||
552 | $expected_codes = array_merge($expected_codes, $this->greylisted); |
||
553 | } |
||
554 | // issue RCPT TO, 5 minute timeout |
||
555 | try { |
||
556 | $this->send('RCPT TO:<' . $to . '>'); |
||
557 | // process the response |
||
558 | try { |
||
559 | $this->expect($expected_codes, $this->command_timeouts['rcpt']); |
||
560 | $this->state['rcpt'] = true; |
||
561 | $is_valid = true; |
||
562 | } catch (SMTP_Validate_Email_Exception_Unexpected_Response $e) { |
||
563 | $this->debug('Unexpected response to RCPT TO: ' . $e->getMessage()); |
||
564 | } |
||
565 | } catch (SMTP_Validate_Email_Exception $e) { |
||
566 | $this->debug('Sending RCPT TO failed: ' . $e->getMessage()); |
||
567 | } |
||
568 | return $is_valid; |
||
569 | } |
||
570 | |||
571 | /** |
||
572 | * Sends a RSET command and resets our internal state. |
||
573 | * @return void |
||
574 | */ |
||
575 | protected function rset() { |
||
576 | $this->send('RSET'); |
||
577 | // MS ESMTP doesn't follow RFC according to ZF tracker, see [ZF-1377] |
||
578 | $expected = array( |
||
579 | self::SMTP_GENERIC_SUCCESS, |
||
580 | self::SMTP_CONNECT_SUCCESS, |
||
581 | self::SMTP_NOT_IMPLEMENTED, |
||
582 | // hotmail returns this o_O |
||
583 | self::SMTP_TRANSACTION_FAILED |
||
584 | ); |
||
585 | $this->expect($expected, $this->command_timeouts['rset'], true); |
||
586 | $this->state['mail'] = false; |
||
587 | $this->state['rcpt'] = false; |
||
588 | } |
||
589 | |||
590 | /** |
||
591 | * Sends a QUIT command. |
||
592 | * @return void |
||
593 | */ |
||
594 | protected function quit() { |
||
595 | // although RFC says QUIT can be issued at any time, we won't |
||
596 | if ($this->state['helo']) { |
||
597 | $this->send('QUIT'); |
||
598 | $this->expect(array(self::SMTP_GENERIC_SUCCESS,self::SMTP_QUIT_SUCCESS), $this->command_timeouts['quit'], true); |
||
599 | } |
||
600 | } |
||
601 | |||
602 | /** |
||
603 | * Sends a NOOP command. |
||
604 | * @return void |
||
605 | */ |
||
606 | protected function noop() { |
||
607 | $this->send('NOOP'); |
||
608 | // erg... "SMTP" code fix some bad RFC implementations |
||
609 | // Found at least 1 SMTP server replying to NOOP without |
||
610 | // any SMTP code. |
||
611 | $expected_codes = array( |
||
612 | 'SMTP', |
||
613 | self::SMTP_BAD_SEQUENCE, |
||
614 | self::SMTP_NOT_IMPLEMENTED, |
||
615 | self::SMTP_GENERIC_SUCCESS, |
||
616 | self::SMTP_SYNTAX_ERROR, |
||
617 | self::SMTP_CONNECT_SUCCESS |
||
618 | ); |
||
619 | $this->expect($expected_codes, $this->command_timeouts['noop'], true); |
||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
620 | } |
||
621 | |||
622 | /** |
||
623 | * Sends a command to the remote host. |
||
624 | * @param string $cmd The cmd to send |
||
625 | * @return int|bool Number of bytes written to the stream |
||
626 | * @throws SMTP_Validate_Email_Exception_No_Connection |
||
627 | * @throws SMTP_Validate_Email_Exception_Send_Failed |
||
628 | */ |
||
629 | protected function send($cmd) { |
||
630 | // must be connected |
||
631 | if (!$this->connected()) { |
||
632 | throw new SMTP_Validate_Email_Exception_No_Connection('No connection'); |
||
633 | } |
||
634 | $this->debug('send>>>: ' . $cmd); |
||
635 | // write the cmd to the connection stream |
||
636 | $result = fwrite($this->socket, $cmd . self::CRLF); |
||
637 | // did the send work? |
||
638 | if ($result === false) { |
||
639 | throw new SMTP_Validate_Email_Exception_Send_Failed('Send failed ' . |
||
640 | 'on: ' . $this->host); |
||
641 | } |
||
642 | return $result; |
||
643 | } |
||
644 | |||
645 | /** |
||
646 | * Receives a response line from the remote host. |
||
647 | * @param int $timeout Timeout in seconds |
||
648 | * @return string |
||
649 | * @throws SMTP_Validate_Email_Exception_No_Connection |
||
650 | * @throws SMTP_Validate_Email_Exception_Timeout |
||
651 | * @throws SMTP_Validate_Email_Exception_No_Response |
||
652 | */ |
||
653 | protected function recv($timeout = null) { |
||
654 | if (!$this->connected()) { |
||
655 | throw new SMTP_Validate_Email_Exception_No_Connection('No connection'); |
||
656 | } |
||
657 | // timeout specified? |
||
658 | if ($timeout !== null) { |
||
659 | stream_set_timeout($this->socket, $timeout); |
||
660 | } |
||
661 | // retrieve response |
||
662 | $line = fgets($this->socket, 1024); |
||
663 | $this->debug('<<<recv: ' . $line); |
||
664 | // have we timed out? |
||
665 | $info = stream_get_meta_data($this->socket); |
||
666 | if (!empty($info['timed_out'])) { |
||
667 | throw new SMTP_Validate_Email_Exception_Timeout('Timed out in recv'); |
||
668 | } |
||
669 | // did we actually receive anything? |
||
670 | if ($line === false) { |
||
671 | throw new SMTP_Validate_Email_Exception_No_Response('No response in recv'); |
||
672 | } |
||
673 | return $line; |
||
674 | } |
||
675 | |||
676 | /** |
||
677 | * Receives lines from the remote host and looks for expected response codes. |
||
678 | * @param int|int[] $codes A list of one or more expected response codes |
||
679 | * @param int $timeout The timeout for this individual command, if any |
||
680 | * @param bool $empty_response_allowed When true, empty responses are allowed |
||
681 | * @return string The last text message received |
||
682 | * @throws SMTP_Validate_Email_Exception_Unexpected_Response |
||
683 | */ |
||
684 | protected function expect($codes, $timeout = null, $empty_response_allowed = false) { |
||
685 | if (!is_array($codes)) { |
||
686 | $codes = (array) $codes; |
||
687 | } |
||
688 | $code = null; |
||
689 | $text = ''; |
||
0 ignored issues
–
show
|
|||
690 | try { |
||
691 | |||
692 | $text = $line = $this->recv($timeout); |
||
693 | while (preg_match("/^[0-9]+-/", $line)) { |
||
694 | $line = $this->recv($timeout); |
||
695 | $text .= $line; |
||
696 | } |
||
697 | sscanf($line, '%d%s', $code, $text); |
||
698 | if (($empty_response_allowed === false && ($code === null || !in_array($code, $codes))) || $code == self::SMTP_SERVICE_UNAVAILABLE) { |
||
699 | throw new SMTP_Validate_Email_Exception_Unexpected_Response($line); |
||
700 | } |
||
701 | |||
702 | } catch (SMTP_Validate_Email_Exception_No_Response $e) { |
||
703 | |||
704 | // no response in expect() probably means that the |
||
705 | // remote server forcibly closed the connection so |
||
706 | // lets clean up on our end as well? |
||
707 | $this->debug('No response in expect(): ' . $e->getMessage()); |
||
708 | $this->disconnect(false); |
||
709 | |||
710 | } |
||
711 | return $text; |
||
712 | } |
||
713 | |||
714 | /** |
||
715 | * Parses an email string into respective user and domain parts and |
||
716 | * returns those as an array. |
||
717 | * @param string $email 'user@domain' |
||
718 | * @return array ['user', 'domain'] |
||
719 | */ |
||
720 | protected function parse_email($email) { |
||
721 | $parts = explode('@', $email); |
||
722 | $domain = array_pop($parts); |
||
723 | $user= implode('@', $parts); |
||
724 | return array($user, $domain); |
||
725 | } |
||
726 | |||
727 | /** |
||
728 | * Sets the email addresses that should be validated. |
||
729 | * @param array $emails Array of emails to validate |
||
730 | * @return void |
||
731 | */ |
||
732 | public function set_emails($emails) { |
||
733 | if (!is_array($emails)) { |
||
734 | $emails = (array) $emails; |
||
735 | } |
||
736 | $this->domains = array(); |
||
737 | foreach ($emails as $email) { |
||
738 | list($user, $domain) = $this->parse_email($email); |
||
739 | if (!isset($this->domains[$domain])) { |
||
740 | $this->domains[$domain] = array(); |
||
741 | } |
||
742 | $this->domains[$domain][] = $user; |
||
743 | } |
||
744 | } |
||
745 | |||
746 | /** |
||
747 | * Sets the email address to use as the sender/validator. |
||
748 | * @param string $email |
||
749 | * @return void |
||
750 | */ |
||
751 | public function set_sender($email) { |
||
752 | $parts = $this->parse_email($email); |
||
753 | $this->from_user = $parts[0]; |
||
754 | $this->from_domain = $parts[1]; |
||
755 | } |
||
756 | |||
757 | /** |
||
758 | * Queries the DNS server for MX entries of a certain domain. |
||
759 | * @param string $domain The domain for which to retrieve MX records |
||
760 | * @return array MX hosts and their weights |
||
761 | */ |
||
762 | protected function mx_query($domain) { |
||
763 | $hosts = array(); |
||
764 | $weight = array(); |
||
765 | if (function_exists('getmxrr')) { |
||
766 | getmxrr($domain, $hosts, $weight); |
||
767 | } else { |
||
768 | $this->getmxrr($domain, $hosts, $weight); |
||
769 | } |
||
770 | return array($hosts, $weight); |
||
771 | } |
||
772 | |||
773 | /** |
||
774 | * Provides a windows replacement for the getmxrr function. |
||
775 | * Params and behaviour is that of the regular getmxrr function. |
||
776 | * @see http://www.php.net/getmxrr |
||
777 | * @param string $hostname |
||
778 | * @param string[] $mxhosts |
||
779 | * @param int[] $mxweights |
||
780 | * @return bool|null |
||
781 | */ |
||
782 | protected function getmxrr($hostname, &$mxhosts, &$mxweights) { |
||
783 | if (!is_array($mxhosts)) { |
||
784 | $mxhosts = array(); |
||
785 | } |
||
786 | if (!is_array($mxweights)) { |
||
787 | $mxweights = array(); |
||
788 | } |
||
789 | if (empty($hostname)) { |
||
790 | return null; |
||
791 | } |
||
792 | $cmd = 'nslookup -type=MX ' . escapeshellarg($hostname); |
||
793 | if (!empty($this->mx_query_ns)) { |
||
794 | $cmd .= ' ' . escapeshellarg($this->mx_query_ns); |
||
795 | } |
||
796 | exec($cmd, $output); |
||
797 | if (empty($output)) { |
||
798 | return null; |
||
799 | } |
||
800 | $i = -1; |
||
801 | foreach ($output as $line) { |
||
802 | $i++; |
||
803 | View Code Duplication | if (preg_match("/^$hostname\tMX preference = ([0-9]+), mail exchanger = (.+)$/i", $line, $parts)) { |
|
0 ignored issues
–
show
This code seems to be duplicated across your project.
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation. You can also find more detailed suggestions in the “Code” section of your repository. ![]() |
|||
804 | $mxweights[$i] = trim($parts[1]); |
||
805 | $mxhosts[$i] = trim($parts[2]); |
||
806 | } |
||
807 | View Code Duplication | if (preg_match('/responsible mail addr = (.+)$/i', $line, $parts)) { |
|
0 ignored issues
–
show
This code seems to be duplicated across your project.
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation. You can also find more detailed suggestions in the “Code” section of your repository. ![]() |
|||
808 | $mxweights[$i] = $i; |
||
809 | $mxhosts[$i] = trim($parts[1]); |
||
810 | } |
||
811 | } |
||
812 | return ($i != -1); |
||
813 | } |
||
814 | |||
815 | /** |
||
816 | * Debug helper. If run in a CLI env, it just dumps $str on a new line, |
||
817 | * else it prints stuff using <pre>. |
||
818 | * @param string $str The debug message |
||
819 | * @return void |
||
820 | */ |
||
821 | private function debug($str) { |
||
822 | $str = $this->stamp($str); |
||
823 | $this->log($str); |
||
824 | if ($this->debug == true) { |
||
0 ignored issues
–
show
|
|||
825 | if (PHP_SAPI != 'cli') { |
||
826 | $str = '<br/><pre>' . htmlspecialchars($str) . '</pre>'; |
||
827 | } |
||
828 | echo "\n" . $str; |
||
829 | } |
||
830 | } |
||
831 | |||
832 | /** |
||
833 | * Adds a message to the log array |
||
834 | * @param string $msg The message to add |
||
835 | */ |
||
836 | private function log($msg) { |
||
837 | $this->log[] = $msg; |
||
838 | } |
||
839 | |||
840 | /** |
||
841 | * Prepends the given $msg with the current date and time inside square brackets. |
||
842 | * |
||
843 | * @param string $msg |
||
844 | * |
||
845 | * @return string |
||
846 | */ |
||
847 | private function stamp($msg) { |
||
848 | $date = \DateTime::createFromFormat('U.u', sprintf('%.f', microtime(true)))->format('Y-m-d\TH:i:s.uO'); |
||
849 | $line = '[' . $date . '] ' . $msg; |
||
850 | |||
851 | return $line; |
||
852 | } |
||
853 | |||
854 | /** |
||
855 | * Returns the log array |
||
856 | */ |
||
857 | public function get_log() { |
||
858 | return $this->log; |
||
859 | } |
||
860 | |||
861 | /** |
||
862 | * Truncates the log array |
||
863 | */ |
||
864 | public function clear_log() { |
||
865 | $this->log = array(); |
||
866 | } |
||
867 | } |
||
868 |