Passed
Push — master ( dcf803...924cb8 )
by zyt
03:04
created

smtp-validate-email.php (5 issues)

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
$expected_codes of type array<integer,integer|string> is incompatible with the type integer[]|integer expected by parameter $codes of SMTP_Validate_Email::expect(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

619
        $this->expect(/** @scrutinizer ignore-type */ $expected_codes, $this->command_timeouts['noop'], true);
Loading history...
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
The assignment to $text is dead and can be removed.
Loading history...
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.

Loading history...
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.

Loading history...
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
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
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