Completed
Push — master ( 6c2c54...46c055 )
by Malte
02:27
created

ImapProtocol::readResponse()   A

Complexity

Conditions 6
Paths 16

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 2 Features 0
Metric Value
eloc 11
c 3
b 2
f 0
dl 0
loc 20
rs 9.2222
cc 6
nc 16
nop 2
1
<?php
2
/*
3
* File: ImapProtocol.php
4
* Category: Protocol
5
* Author: M.Goldenbaum
6
* Created: 16.09.20 18:27
7
* Updated: -
8
*
9
* Description:
10
*  -
11
*/
12
13
namespace Webklex\PHPIMAP\Connection\Protocols;
14
15
use Webklex\PHPIMAP\Exceptions\AuthFailedException;
16
use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
17
use Webklex\PHPIMAP\Exceptions\RuntimeException;
18
use Webklex\PHPIMAP\Header;
19
20
/**
21
 * Class ImapProtocol
22
 *
23
 * @package Webklex\PHPIMAP\Connection\Protocols
24
 */
25
class ImapProtocol extends Protocol implements ProtocolInterface {
26
27
    /**
28
     * Request noun
29
     * @var int
30
     */
31
    protected $noun = 0;
32
33
    /**
34
     * Imap constructor.
35
     * @param bool $cert_validation set to false to skip SSL certificate validation
36
     * @param mixed $encryption Connection encryption method
37
     */
38
    public function __construct($cert_validation = true, $encryption = false) {
39
        $this->setCertValidation($cert_validation);
40
        $this->encryption = $encryption;
41
    }
42
43
    /**
44
     * Public destructor
45
     */
46
    public function __destruct() {
47
        $this->logout();
48
    }
49
50
    /**
51
     * Open connection to IMAP server
52
     * @param string $host hostname or IP address of IMAP server
53
     * @param int|null $port of IMAP server, default is 143 and 993 for ssl
54
     *
55
     * @throws ConnectionFailedException
56
     */
57
    public function connect($host, $port = null) {
58
        $transport = 'tcp';
59
60
        if ($this->encryption) {
61
            $encryption = strtolower($this->encryption);
62
            if ($encryption == "ssl") {
63
                $transport = 'ssl';
64
                $port = $port === null ? 993 : $port;
65
            }
66
        }
67
        $port = $port === null ? 143 : $port;
68
        try {
69
            $this->stream = $this->createStream($transport, $host, $port, $this->connection_timeout);
70
            if (!$this->assumedNextLine('* OK')) {
71
                throw new ConnectionFailedException('connection refused');
72
            }
73
            if ($encryption == "tls") {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $encryption does not seem to be defined for all execution paths leading up to this point.
Loading history...
74
                $this->enableTls();
75
            }
76
        } catch (\Exception $e) {
77
            throw new ConnectionFailedException('connection failed', 0, $e);
78
        }
79
    }
80
81
    /**
82
     * Enable tls on the current connection
83
     *
84
     * @throws ConnectionFailedException
85
     * @throws RuntimeException
86
     */
87
    protected function enableTls(){
88
        $response = $this->requestAndResponse('STARTTLS');
89
        $result = $response && stream_socket_enable_crypto($this->stream, true, $this->getCryptoMethod());
0 ignored issues
show
Bug introduced by
It seems like $this->stream can also be of type boolean; however, parameter $stream of stream_socket_enable_crypto() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

89
        $result = $response && stream_socket_enable_crypto(/** @scrutinizer ignore-type */ $this->stream, true, $this->getCryptoMethod());
Loading history...
90
        if (!$result) {
91
            throw new ConnectionFailedException('failed to enable TLS');
92
        }
93
    }
94
95
    /**
96
     * Get the next line from stream
97
     *
98
     * @return string next line
99
     * @throws RuntimeException
100
     */
101
    public function nextLine() {
102
        $line = fgets($this->stream);
0 ignored issues
show
Bug introduced by
It seems like $this->stream can also be of type boolean; however, parameter $handle of fgets() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

102
        $line = fgets(/** @scrutinizer ignore-type */ $this->stream);
Loading history...
103
104
        if ($line === false) {
105
            throw new RuntimeException('failed to read - connection closed?');
106
        }
107
108
        return $line;
109
    }
110
111
    /**
112
     * Get the next line and check if it starts with a given string
113
     * @param string $start
114
     *
115
     * @return bool
116
     * @throws RuntimeException
117
     */
118
    protected function assumedNextLine($start) {
119
        $line = $this->nextLine();
120
        return strpos($line, $start) === 0;
121
    }
122
123
    /**
124
     * Get the next line and split the tag
125
     * @param string $tag reference tag
126
     *
127
     * @return string next line
128
     * @throws RuntimeException
129
     */
130
    protected function nextTaggedLine(&$tag) {
131
        $line = $this->nextLine();
132
        list($tag, $line) = explode(' ', $line, 2);
133
134
        return $line;
135
    }
136
137
    /**
138
     * Split a given line in values. A value is literal of any form or a list
139
     * @param string $line
140
     *
141
     * @return array
142
     * @throws RuntimeException
143
     */
144
    protected function decodeLine($line) {
145
        $tokens = [];
146
        $stack = [];
147
148
        //  replace any trailing <NL> including spaces with a single space
149
        $line = rtrim($line) . ' ';
150
        while (($pos = strpos($line, ' ')) !== false) {
151
            $token = substr($line, 0, $pos);
152
            if (!strlen($token)) {
153
                continue;
154
            }
155
            while ($token[0] == '(') {
156
                array_push($stack, $tokens);
157
                $tokens = [];
158
                $token = substr($token, 1);
159
            }
160
            if ($token[0] == '"') {
161
                if (preg_match('%^\(*"((.|\\\\|\\")*?)" *%', $line, $matches)) {
162
                    $tokens[] = $matches[1];
163
                    $line = substr($line, strlen($matches[0]));
164
                    continue;
165
                }
166
            }
167
            if ($token[0] == '{') {
168
                $endPos = strpos($token, '}');
169
                $chars = substr($token, 1, $endPos - 1);
170
                if (is_numeric($chars)) {
171
                    $token = '';
172
                    while (strlen($token) < $chars) {
173
                        $token .= $this->nextLine();
174
                    }
175
                    $line = '';
176
                    if (strlen($token) > $chars) {
177
                        $line = substr($token, $chars);
0 ignored issues
show
Bug introduced by
$chars of type string is incompatible with the type integer expected by parameter $start of substr(). ( Ignorable by Annotation )

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

177
                        $line = substr($token, /** @scrutinizer ignore-type */ $chars);
Loading history...
178
                        $token = substr($token, 0, $chars);
0 ignored issues
show
Bug introduced by
$chars of type string is incompatible with the type integer expected by parameter $length of substr(). ( Ignorable by Annotation )

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

178
                        $token = substr($token, 0, /** @scrutinizer ignore-type */ $chars);
Loading history...
179
                    } else {
180
                        $line .= $this->nextLine();
181
                    }
182
                    $tokens[] = $token;
183
                    $line = trim($line) . ' ';
184
                    continue;
185
                }
186
            }
187
            if ($stack && $token[strlen($token) - 1] == ')') {
188
                // closing braces are not separated by spaces, so we need to count them
189
                $braces = strlen($token);
190
                $token = rtrim($token, ')');
191
                // only count braces if more than one
192
                $braces -= strlen($token) + 1;
193
                // only add if token had more than just closing braces
194
                if (rtrim($token) != '') {
195
                    $tokens[] = rtrim($token);
196
                }
197
                $token = $tokens;
198
                $tokens = array_pop($stack);
199
                // special handline if more than one closing brace
200
                while ($braces-- > 0) {
201
                    $tokens[] = $token;
202
                    $token = $tokens;
203
                    $tokens = array_pop($stack);
204
                }
205
            }
206
            $tokens[] = $token;
207
            $line = substr($line, $pos + 1);
208
        }
209
210
        // maybe the server forgot to send some closing braces
211
        while ($stack) {
212
            $child = $tokens;
213
            $tokens = array_pop($stack);
214
            $tokens[] = $child;
215
        }
216
217
        return $tokens;
218
    }
219
220
    /**
221
     * Read abd decode a response "line"
222
     * @param array|string $tokens to decode
223
     * @param string $wantedTag targeted tag
224
     * @param bool $dontParse if true only the unparsed line is returned in $tokens
225
     *
226
     * @return bool
227
     * @throws RuntimeException
228
     */
229
    public function readLine(&$tokens = [], $wantedTag = '*', $dontParse = false) {
230
        $line = $this->nextTaggedLine($tag); // get next tag
231
        if (!$dontParse) {
232
            $tokens = $this->decodeLine($line);
233
        } else {
234
            $tokens = $line;
235
        }
236
        if ($this->debug) echo "<< ".$line."\n";
237
238
        // if tag is wanted tag we might be at the end of a multiline response
239
        return $tag == $wantedTag;
240
    }
241
242
    /**
243
     * Read all lines of response until given tag is found
244
     * @param string $tag request tag
245
     * @param bool $dontParse if true every line is returned unparsed instead of the decoded tokens
246
     *
247
     * @return void|null|bool|array tokens if success, false if error, null if bad request
248
     * @throws RuntimeException
249
     */
250
    public function readResponse($tag, $dontParse = false) {
251
        $lines = [];
252
        $tokens = null; // define $tokens variable before first use
253
        while (!$this->readLine($tokens, $tag, $dontParse)) {
254
            $lines[] = $tokens;
255
        }
256
257
        if ($dontParse) {
258
            // First two chars are still needed for the response code
259
            $tokens = [substr($tokens, 0, 2)];
260
        }
261
262
        // last line has response code
263
        if ($tokens[0] == 'OK') {
264
            return $lines ? $lines : true;
0 ignored issues
show
introduced by
$lines is an empty array, thus is always false.
Loading history...
265
        } elseif ($tokens[0] == 'NO') {
266
            return false;
267
        }
268
269
        return;
270
    }
271
272
    /**
273
     * Send a new request
274
     * @param string $command
275
     * @param array $tokens additional parameters to command, use escapeString() to prepare
276
     * @param string $tag provide a tag otherwise an autogenerated is returned
277
     *
278
     * @throws RuntimeException
279
     */
280
    public function sendRequest($command, $tokens = [], &$tag = null) {
281
        if (!$tag) {
282
            $this->noun++;
283
            $tag = 'TAG' . $this->noun;
284
        }
285
286
        $line = $tag . ' ' . $command;
287
288
        foreach ($tokens as $token) {
289
            if (is_array($token)) {
290
                if (fwrite($this->stream, $line . ' ' . $token[0] . "\r\n") === false) {
0 ignored issues
show
Bug introduced by
It seems like $this->stream can also be of type boolean; however, parameter $handle of fwrite() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

290
                if (fwrite(/** @scrutinizer ignore-type */ $this->stream, $line . ' ' . $token[0] . "\r\n") === false) {
Loading history...
291
                    throw new RuntimeException('failed to write - connection closed?');
292
                }
293
                if (!$this->assumedNextLine('+ ')) {
294
                    throw new RuntimeException('failed to send literal string');
295
                }
296
                $line = $token[1];
297
            } else {
298
                $line .= ' ' . $token;
299
            }
300
        }
301
        if ($this->debug) echo ">> ".$line."\n";
302
303
        if (fwrite($this->stream, $line . "\r\n") === false) {
304
            throw new RuntimeException('failed to write - connection closed?');
305
        }
306
    }
307
308
    /**
309
     * Send a request and get response at once
310
     * @param string $command
311
     * @param array $tokens parameters as in sendRequest()
312
     * @param bool $dontParse if true unparsed lines are returned instead of tokens
313
     *
314
     * @return void|null|bool|array response as in readResponse()
315
     * @throws RuntimeException
316
     */
317
    public function requestAndResponse($command, $tokens = [], $dontParse = false) {
318
        $this->sendRequest($command, $tokens, $tag);
319
320
        return $this->readResponse($tag, $dontParse);
321
    }
322
323
    /**
324
     * Escape one or more literals i.e. for sendRequest
325
     * @param string|array $string the literal/-s
326
     *
327
     * @return string|array escape literals, literals with newline ar returned
328
     *                      as array('{size}', 'string');
329
     */
330
    public function escapeString($string) {
331
        if (func_num_args() < 2) {
332
            if (strpos($string, "\n") !== false) {
333
                return ['{' . strlen($string) . '}', $string];
334
            } else {
335
                return '"' . str_replace(['\\', '"'], ['\\\\', '\\"'], $string) . '"';
336
            }
337
        }
338
        $result = [];
339
        foreach (func_get_args() as $string) {
0 ignored issues
show
introduced by
$string is overwriting one of the parameters of this function.
Loading history...
340
            $result[] = $this->escapeString($string);
341
        }
342
        return $result;
343
    }
344
345
    /**
346
     * Escape a list with literals or lists
347
     * @param array $list list with literals or lists as PHP array
348
     *
349
     * @return string escaped list for imap
350
     */
351
    public function escapeList($list) {
352
        $result = [];
353
        foreach ($list as $v) {
354
            if (!is_array($v)) {
355
                $result[] = $v;
356
                continue;
357
            }
358
            $result[] = $this->escapeList($v);
359
        }
360
        return '(' . implode(' ', $result) . ')';
361
    }
362
363
    /**
364
     * Login to a new session.
365
     * @param string $user username
366
     * @param string $password password
367
     *
368
     * @return bool|mixed
369
     * @throws AuthFailedException
370
     */
371
    public function login($user, $password) {
372
        try {
373
            return $this->requestAndResponse('LOGIN', $this->escapeString($user, $password), true);
0 ignored issues
show
Bug introduced by
It seems like $this->escapeString($user, $password) can also be of type string; however, parameter $tokens of Webklex\PHPIMAP\Connecti...l::requestAndResponse() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

373
            return $this->requestAndResponse('LOGIN', /** @scrutinizer ignore-type */ $this->escapeString($user, $password), true);
Loading history...
374
        } catch (RuntimeException $e) {
375
            throw new AuthFailedException("failed to authenticate", 0, $e);
376
        }
377
    }
378
379
    /**
380
     * Authenticate your current IMAP session.
381
     * @param string $user username
382
     * @param string $token access token
383
     *
384
     * @return bool|mixed
385
     * @throws AuthFailedException
386
     */
387
    public function authenticate($user, $token) {
388
        try {
389
            $authenticateParams = ['XOAUTH2', base64_encode("user=$user\1auth=Bearer $token\1\1")];
390
            $this->sendRequest('AUTHENTICATE', $authenticateParams);
391
392
            while (true) {
393
                $response = "";
394
                $is_plus = $this->readLine($response, '+', true);
395
                if ($is_plus) {
396
                    // try to log the challenge somewhere where it can be found
397
                    error_log("got an extra server challenge: $response");
398
                    // respond with an empty response.
399
                    $this->sendRequest('');
400
                } else {
401
                    if (preg_match('/^NO /i', $response) ||
402
                        preg_match('/^BAD /i', $response)) {
403
                        error_log("got failure response: $response");
404
                        return false;
405
                    } else if (preg_match("/^OK /i", $response)) {
406
                        return true;
407
                    }
408
                }
409
            }
410
        } catch (RuntimeException $e) {
411
            throw new AuthFailedException("failed to authenticate", 0, $e);
412
        }
413
        return false;
414
    }
415
416
    /**
417
     * Logout of imap server
418
     *
419
     * @return bool success
420
     */
421
    public function logout() {
422
        $result = false;
423
        if ($this->stream) {
424
            try {
425
                $result = $this->requestAndResponse('LOGOUT', [], true);
426
            } catch (\Exception $e) {}
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
427
            fclose($this->stream);
0 ignored issues
show
Bug introduced by
It seems like $this->stream can also be of type true; however, parameter $handle of fclose() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

427
            fclose(/** @scrutinizer ignore-type */ $this->stream);
Loading history...
428
            $this->stream = null;
429
        }
430
        return $result;
431
    }
432
433
    /**
434
     * Check if the current session is connected
435
     *
436
     * @return bool
437
     */
438
    public function connected(){
439
        return (boolean) $this->stream;
440
    }
441
442
    /**
443
     * Get an array of available capabilities
444
     *
445
     * @return array list of capabilities
446
     * @throws RuntimeException
447
     */
448
    public function getCapabilities() {
449
        $response = $this->requestAndResponse('CAPABILITY');
450
451
        if (!$response) return [];
0 ignored issues
show
Bug Best Practice introduced by
The expression $response 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 false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
452
453
        $capabilities = [];
454
        foreach ($response as $line) {
0 ignored issues
show
Bug introduced by
The expression $response of type true is not traversable.
Loading history...
455
            $capabilities = array_merge($capabilities, $line);
456
        }
457
        return $capabilities;
458
    }
459
460
    /**
461
     * Examine and select have the same response.
462
     * @param string $command can be 'EXAMINE' or 'SELECT'
463
     * @param string $folder target folder
464
     *
465
     * @return bool|array
466
     * @throws RuntimeException
467
     */
468
    public function examineOrSelect($command = 'EXAMINE', $folder = 'INBOX') {
469
        $this->sendRequest($command, [$this->escapeString($folder)], $tag);
470
471
        $result = [];
472
        $tokens = null; // define $tokens variable before first use
473
        while (!$this->readLine($tokens, $tag)) {
474
            if ($tokens[0] == 'FLAGS') {
475
                array_shift($tokens);
476
                $result['flags'] = $tokens;
477
                continue;
478
            }
479
            switch ($tokens[1]) {
480
                case 'EXISTS':
481
                case 'RECENT':
482
                    $result[strtolower($tokens[1])] = $tokens[0];
483
                    break;
484
                case '[UIDVALIDITY':
485
                    $result['uidvalidity'] = (int)$tokens[2];
486
                    break;
487
                case '[UIDNEXT':
488
                    $result['uidnext'] = (int)$tokens[2];
489
                    break;
490
                default:
491
                    // ignore
492
                    break;
493
            }
494
        }
495
496
        if ($tokens[0] != 'OK') {
497
            return false;
498
        }
499
        return $result;
500
    }
501
502
    /**
503
     * Change the current folder
504
     * @param string $folder change to this folder
505
     *
506
     * @return bool|array see examineOrselect()
507
     * @throws RuntimeException
508
     */
509
    public function selectFolder($folder = 'INBOX') {
510
        return $this->examineOrSelect('SELECT', $folder);
511
    }
512
513
    /**
514
     * Examine a given folder
515
     * @param string $folder examine this folder
516
     *
517
     * @return bool|array see examineOrselect()
518
     * @throws RuntimeException
519
     */
520
    public function examineFolder($folder = 'INBOX') {
521
        return $this->examineOrSelect('EXAMINE', $folder);
522
    }
523
524
    /**
525
     * Fetch one or more items of one or more messages
526
     * @param string|array $items items to fetch [RFC822.HEADER, FLAGS, RFC822.TEXT, etc]
527
     * @param int|array $from message for items or start message if $to !== null
528
     * @param int|null $to if null only one message ($from) is fetched, else it's the
529
     *                             last message, INF means last message available
530
     * @param bool $uid set to true if passing a unique id
531
     *
532
     * @return string|array if only one item of one message is fetched it's returned as string
533
     *                      if items of one message are fetched it's returned as (name => value)
534
     *                      if one items of messages are fetched it's returned as (msgno => value)
535
     *                      if items of messages are fetched it's returned as (msgno => (name => value))
536
     * @throws RuntimeException
537
     */
538
    protected function fetch($items, $from, $to = null, $uid = false) {
539
        if (is_array($from)) {
540
            $set = implode(',', $from);
541
        } elseif ($to === null) {
542
            $set = (int)$from;
543
        } elseif ($to === INF) {
0 ignored issues
show
introduced by
The condition $to === Webklex\PHPIMAP\Connection\Protocols\INF is always false.
Loading history...
544
            $set = (int)$from . ':*';
545
        } else {
546
            $set = (int)$from . ':' . (int)$to;
547
        }
548
549
        $items = (array)$items;
550
        $itemList = $this->escapeList($items);
551
552
        $this->sendRequest(($uid ? 'UID ' : '') . 'FETCH', [$set, $itemList], $tag);
553
554
        $result = [];
555
        $tokens = null; // define $tokens variable before first use
556
        while (!$this->readLine($tokens, $tag)) {
557
            // ignore other responses
558
            if ($tokens[1] != 'FETCH') {
559
                continue;
560
            }
561
562
            // find array key of UID value; try the last elements, or search for it
563
            if ($uid) {
564
                $count = count($tokens[2]);
565
                if ($tokens[2][$count - 2] == 'UID') {
566
                    $uidKey = $count - 1;
567
                } else if ($tokens[2][0] == 'UID') {
568
                    $uidKey = 1;
569
                } else {
570
                    $uidKey = array_search('UID', $tokens[2]) + 1;
571
                }
572
            }
573
574
            // ignore other messages
575
            if ($to === null && !is_array($from) && ($uid ? $tokens[2][$uidKey] != $from : $tokens[0] != $from)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $uidKey does not seem to be defined for all execution paths leading up to this point.
Loading history...
576
                continue;
577
            }
578
579
            // if we only want one item we return that one directly
580
            if (count($items) == 1) {
581
                if ($tokens[2][0] == $items[0]) {
582
                    $data = $tokens[2][1];
583
                } elseif ($uid && $tokens[2][2] == $items[0]) {
584
                    $data = $tokens[2][3];
585
                } else {
586
                    // maybe the server send an other field we didn't wanted
587
                    $count = count($tokens[2]);
588
                    // we start with 2, because 0 was already checked
589
                    for ($i = 2; $i < $count; $i += 2) {
590
                        if ($tokens[2][$i] != $items[0]) {
591
                            continue;
592
                        }
593
                        $data = $tokens[2][$i + 1];
594
                        break;
595
                    }
596
                }
597
            } else {
598
                $data = [];
599
                while (key($tokens[2]) !== null) {
600
                    $data[current($tokens[2])] = next($tokens[2]);
601
                    next($tokens[2]);
602
                }
603
            }
604
605
            // if we want only one message we can ignore everything else and just return
606
            if ($to === null && !is_array($from) && ($uid ? $tokens[2][$uidKey] == $from : $tokens[0] == $from)) {
607
                // we still need to read all lines
608
                while (!$this->readLine($tokens, $tag))
609
610
                    return $data;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $data does not seem to be defined for all execution paths leading up to this point.
Loading history...
611
            }
612
            if ($uid) {
613
                $result[$tokens[2][$uidKey]] = $data;
614
            }else{
615
                $result[$tokens[0]] = $data;
616
            }
617
        }
618
619
        if ($to === null && !is_array($from)) {
620
            throw new RuntimeException('the single id was not found in response');
621
        }
622
623
        return $result;
624
    }
625
626
    /**
627
     * Fetch message headers
628
     * @param array|int $uids
629
     * @param string $rfc
630
     * @param bool $uid set to true if passing a unique id
631
     *
632
     * @return array
633
     * @throws RuntimeException
634
     */
635
    public function content($uids, $rfc = "RFC822", $uid = false) {
636
        return $this->fetch(["$rfc.TEXT"], $uids, null, $uid);
637
    }
638
639
    /**
640
     * Fetch message headers
641
     * @param array|int $uids
642
     * @param string $rfc
643
     * @param bool $uid set to true if passing a unique id
644
     *
645
     * @return array
646
     * @throws RuntimeException
647
     */
648
    public function headers($uids, $rfc = "RFC822", $uid = false){
649
        return $this->fetch(["$rfc.HEADER"], $uids, null, $uid);
650
    }
651
652
    /**
653
     * Fetch message flags
654
     * @param array|int $uids
655
     * @param bool $uid set to true if passing a unique id
656
     *
657
     * @return array
658
     * @throws RuntimeException
659
     */
660
    public function flags($uids, $uid = false){
661
        return $this->fetch(["FLAGS"], $uids, null, $uid);
662
    }
663
664
    /**
665
     * Get uid for a given id
666
     * @param int|null $id message number
667
     *
668
     * @return array|string message number for given message or all messages as array
669
     * @throws RuntimeException
670
     */
671
    public function getUid($id = null) {
672
        $uids = $this->fetch('UID', 1, INF);
0 ignored issues
show
Bug introduced by
Webklex\PHPIMAP\Connection\Protocols\INF of type double is incompatible with the type integer|null expected by parameter $to of Webklex\PHPIMAP\Connecti...s\ImapProtocol::fetch(). ( Ignorable by Annotation )

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

672
        $uids = $this->fetch('UID', 1, /** @scrutinizer ignore-type */ INF);
Loading history...
673
        if ($id == null) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $id of type integer|null against null; this is ambiguous if the integer can be zero. Consider using a strict comparison === instead.
Loading history...
674
            return $uids;
675
        }
676
677
        foreach ($uids as $k => $v) {
678
            if ($k == $id) {
679
                return $v;
680
            }
681
        }
682
683
        throw new RuntimeException('unique id not found');
684
    }
685
686
    /**
687
     * Get a message number for a uid
688
     * @param string $id uid
689
     *
690
     * @return int message number
691
     * @throws RuntimeException
692
     */
693
    public function getMessageNumber($id) {
694
        $ids = $this->getUid();
695
        foreach ($ids as $k => $v) {
696
            if ($v == $id) {
697
                return $k;
698
            }
699
        }
700
701
        throw new RuntimeException('message number not found');
702
    }
703
704
    /**
705
     * Get a list of available folders
706
     * @param string $reference mailbox reference for list
707
     * @param string $folder mailbox name match with wildcards
708
     *
709
     * @return array folders that matched $folder as array(name => array('delimiter' => .., 'flags' => ..))
710
     * @throws RuntimeException
711
     */
712
    public function folders($reference = '', $folder = '*') {
713
        $result = [];
714
        $list = $this->requestAndResponse('LIST', $this->escapeString($reference, $folder));
0 ignored issues
show
Bug introduced by
It seems like $this->escapeString($reference, $folder) can also be of type string; however, parameter $tokens of Webklex\PHPIMAP\Connecti...l::requestAndResponse() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

714
        $list = $this->requestAndResponse('LIST', /** @scrutinizer ignore-type */ $this->escapeString($reference, $folder));
Loading history...
715
        if (!$list || $list === true) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $list 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 false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
introduced by
The condition $list === true is always true.
Loading history...
716
            return $result;
717
        }
718
719
        foreach ($list as $item) {
720
            if (count($item) != 4 || $item[0] != 'LIST') {
721
                continue;
722
            }
723
            $result[$item[3]] = ['delimiter' => $item[2], 'flags' => $item[1]];
724
        }
725
726
        return $result;
727
    }
728
729
    /**
730
     * Manage flags
731
     * @param array $flags flags to set, add or remove - see $mode
732
     * @param int $from message for items or start message if $to !== null
733
     * @param int|null $to if null only one message ($from) is fetched, else it's the
734
     *                             last message, INF means last message available
735
     * @param string|null $mode '+' to add flags, '-' to remove flags, everything else sets the flags as given
736
     * @param bool $silent if false the return values are the new flags for the wanted messages
737
     * @param bool $uid set to true if passing a unique id
738
     *
739
     * @return bool|array new flags if $silent is false, else true or false depending on success
740
     * @throws RuntimeException
741
     */
742
    public function store(array $flags, $from, $to = null, $mode = null, $silent = true, $uid = false) {
743
        $item = 'FLAGS';
744
        if ($mode == '+' || $mode == '-') {
745
            $item = $mode . $item;
746
        }
747
        if ($silent) {
748
            $item .= '.SILENT';
749
        }
750
751
        $flags = $this->escapeList($flags);
752
        $set = (int)$from;
753
        if ($to !== null) {
754
            $set .= ':' . ($to == INF ? '*' : (int)$to);
755
        }
756
757
        $command = ($uid ? "UID " : "")."STORE";
758
        $result = $this->requestAndResponse($command, [$set, $item, $flags], $silent);
759
760
        if ($silent) {
761
            return (bool)$result;
762
        }
763
764
        $tokens = $result;
765
        $result = [];
766
        foreach ($tokens as $token) {
0 ignored issues
show
Bug introduced by
The expression $tokens of type boolean|null is not traversable.
Loading history...
767
            if ($token[1] != 'FETCH' || $token[2][0] != 'FLAGS') {
768
                continue;
769
            }
770
            $result[$token[0]] = $token[2][1];
771
        }
772
773
        return $result;
774
    }
775
776
    /**
777
     * Append a new message to given folder
778
     * @param string $folder name of target folder
779
     * @param string $message full message content
780
     * @param array $flags flags for new message
781
     * @param string $date date for new message
782
     *
783
     * @return bool success
784
     * @throws RuntimeException
785
     */
786
    public function appendMessage($folder, $message, $flags = null, $date = null) {
787
        $tokens = [];
788
        $tokens[] = $this->escapeString($folder);
789
        if ($flags !== null) {
790
            $tokens[] = $this->escapeList($flags);
791
        }
792
        if ($date !== null) {
793
            $tokens[] = $this->escapeString($date);
794
        }
795
        $tokens[] = $this->escapeString($message);
796
797
        return $this->requestAndResponse('APPEND', $tokens, true);
798
    }
799
800
    /**
801
     * Copy a message set from current folder to an other folder
802
     * @param string $folder destination folder
803
     * @param $from
804
     * @param int|null $to if null only one message ($from) is fetched, else it's the
805
     *                         last message, INF means last message available
806
     * @param bool $uid set to true if passing a unique id
807
     *
808
     * @return bool success
809
     * @throws RuntimeException
810
     */
811
    public function copyMessage($folder, $from, $to = null, $uid = false) {
812
        $set = (int)$from;
813
        if ($to !== null) {
814
            $set .= ':' . ($to == INF ? '*' : (int)$to);
815
        }
816
        $command = ($uid ? "UID " : "")."COPY";
817
818
        return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true);
819
    }
820
821
    /**
822
     * Move a message set from current folder to an other folder
823
     * @param string $folder destination folder
824
     * @param $from
825
     * @param int|null $to if null only one message ($from) is fetched, else it's the
826
     *                         last message, INF means last message available
827
     * @param bool $uid set to true if passing a unique id
828
     *
829
     * @return bool success
830
     * @throws RuntimeException
831
     */
832
    public function moveMessage($folder, $from, $to = null, $uid = false) {
833
        $set = (int)$from;
834
        if ($to !== null) {
835
            $set .= ':' . ($to == INF ? '*' : (int)$to);
836
        }
837
        $command = ($uid ? "UID " : "")."MOVE";
838
839
        return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true);
840
    }
841
842
    /**
843
     * Create a new folder (and parent folders if needed)
844
     * @param string $folder folder name
845
     *
846
     * @return bool success
847
     * @throws RuntimeException
848
     */
849
    public function createFolder($folder) {
850
        return $this->requestAndResponse('CREATE', [$this->escapeString($folder)], true);
851
    }
852
853
    /**
854
     * Rename an existing folder
855
     * @param string $old old name
856
     * @param string $new new name
857
     *
858
     * @return bool success
859
     * @throws RuntimeException
860
     */
861
    public function renameFolder($old, $new) {
862
        return $this->requestAndResponse('RENAME', $this->escapeString($old, $new), true);
0 ignored issues
show
Bug introduced by
It seems like $this->escapeString($old, $new) can also be of type string; however, parameter $tokens of Webklex\PHPIMAP\Connecti...l::requestAndResponse() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

862
        return $this->requestAndResponse('RENAME', /** @scrutinizer ignore-type */ $this->escapeString($old, $new), true);
Loading history...
863
    }
864
865
    /**
866
     * Delete a folder
867
     * @param string $folder folder name
868
     *
869
     * @return bool success
870
     * @throws RuntimeException
871
     */
872
    public function deleteFolder($folder) {
873
        return $this->requestAndResponse('DELETE', [$this->escapeString($folder)], true);
874
    }
875
876
    /**
877
     * Subscribe to a folder
878
     * @param string $folder folder name
879
     *
880
     * @return bool success
881
     * @throws RuntimeException
882
     */
883
    public function subscribeFolder($folder) {
884
        return $this->requestAndResponse('SUBSCRIBE', [$this->escapeString($folder)], true);
885
    }
886
887
    /**
888
     * Unsubscribe from a folder
889
     * @param string $folder folder name
890
     *
891
     * @return bool success
892
     * @throws RuntimeException
893
     */
894
    public function unsubscribeFolder($folder) {
895
        return $this->requestAndResponse('UNSUBSCRIBE', [$this->escapeString($folder)], true);
896
    }
897
898
    /**
899
     * Apply session saved changes to the server
900
     *
901
     * @return bool success
902
     * @throws RuntimeException
903
     */
904
    public function expunge() {
905
        return $this->requestAndResponse('EXPUNGE');
906
    }
907
908
    /**
909
     * Send noop command
910
     *
911
     * @return bool success
912
     * @throws RuntimeException
913
     */
914
    public function noop() {
915
        return $this->requestAndResponse('NOOP');
916
    }
917
918
    /**
919
     * Retrieve the quota level settings, and usage statics per mailbox
920
     * @param $username
921
     *
922
     * @return array
923
     * @throws RuntimeException
924
     */
925
    public function getQuota($username) {
926
        return $this->requestAndResponse("GETQUOTA", ['"#user/'.$username.'"']);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->requestAnd...r/' . $username . '"')) also could return the type boolean which is incompatible with the documented return type array.
Loading history...
927
    }
928
929
    /**
930
     * Retrieve the quota settings per user
931
     * @param string $quota_root
932
     *
933
     * @return array
934
     * @throws RuntimeException
935
     */
936
    public function getQuotaRoot($quota_root = 'INBOX') {
937
        return $this->requestAndResponse("QUOTA", [$quota_root]);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->requestAnd...A', array($quota_root)) also could return the type boolean which is incompatible with the documented return type array.
Loading history...
938
    }
939
940
    /**
941
     * Send idle command
942
     * @param bool $uid set to true if passing a unique id (depreciated argument: will be removed. CMD UID IDLE is not supported)
943
     *
944
     * @throws RuntimeException
945
     */
946
    public function idle($uid = false) {
0 ignored issues
show
Unused Code introduced by
The parameter $uid is not used and could be removed. ( Ignorable by Annotation )

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

946
    public function idle(/** @scrutinizer ignore-unused */ $uid = false) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
947
        $this->sendRequest("IDLE");
948
        if (!$this->assumedNextLine('+ ')) {
949
            throw new RuntimeException('idle failed');
950
        }
951
    }
952
953
    /**
954
     * Send done command
955
     * @throws RuntimeException
956
     */
957
    public function done() {
958
        if (fwrite($this->stream, "DONE\r\n") === false) {
0 ignored issues
show
Bug introduced by
It seems like $this->stream can also be of type boolean; however, parameter $handle of fwrite() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

958
        if (fwrite(/** @scrutinizer ignore-type */ $this->stream, "DONE\r\n") === false) {
Loading history...
959
            throw new RuntimeException('failed to write - connection closed?');
960
        }
961
        return true;
962
    }
963
964
    /**
965
     * Search for matching messages
966
     * @param array $params
967
     * @param bool $uid set to true if passing a unique id
968
     *
969
     * @return array message ids
970
     * @throws RuntimeException
971
     */
972
    public function search(array $params, $uid = false) {
973
        $token = $uid == true ? "UID SEARCH" : "SEARCH";
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...
974
        $response = $this->requestAndResponse($token, $params);
975
        if (!$response) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $response 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 false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
976
            return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response could also return false which is incompatible with the documented return type array. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
977
        }
978
979
        foreach ($response as $ids) {
0 ignored issues
show
Bug introduced by
The expression $response of type true is not traversable.
Loading history...
980
            if ($ids[0] == 'SEARCH') {
981
                array_shift($ids);
982
                return $ids;
983
            }
984
        }
985
        return [];
986
    }
987
988
    /**
989
     * Get a message overview
990
     * @param string $sequence
991
     * @param bool $uid set to true if passing a unique id
992
     *
993
     * @return array
994
     * @throws RuntimeException
995
     * @throws \Webklex\PHPIMAP\Exceptions\InvalidMessageDateException
996
     */
997
    public function overview($sequence, $uid = false) {
998
        $result = [];
999
        list($from, $to) = explode(":", $sequence);
1000
1001
        $uids = $this->getUid();
1002
        $ids = [];
1003
        foreach ($uids as $msgn => $v) {
1004
            $id = $uid ? $v : $msgn;
1005
            if ( ($to >= $id && $from <= $id) || ($to === "*" && $from <= $id) ){
1006
                $ids[] = $id;
1007
            }
1008
        }
1009
        $headers = $this->headers($ids, $rfc = "RFC822", $uid);
1010
        foreach ($headers as $id => $raw_header) {
1011
            $result[$id] = (new Header($raw_header, false))->getAttributes();
1012
        }
1013
        return $result;
1014
    }
1015
1016
    /**
1017
     * Enable the debug mode
1018
     */
1019
    public function enableDebug(){
1020
        $this->debug = true;
1021
    }
1022
1023
    /**
1024
     * Disable the debug mode
1025
     */
1026
    public function disableDebug(){
1027
        $this->debug = false;
1028
    }
1029
}