Passed
Push — master ( 812ed7...044883 )
by Malte
02:04
created

ImapProtocol::selectFolder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 2
rs 10
cc 1
nc 1
nop 1
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
     */
37
    public function __construct($cert_validation = true) {
38
        $this->setCertValidation($cert_validation);
39
    }
40
41
    /**
42
     * Public destructor
43
     */
44
    public function __destruct() {
45
        $this->logout();
46
    }
47
48
    /**
49
     * Open connection to IMAP server
50
     * @param string $host hostname or IP address of IMAP server
51
     * @param int|null $port of IMAP server, default is 143 and 993 for ssl
52
     * @param string|bool $encryption use 'SSL', 'TLS' or false
53
     *
54
     * @throws ConnectionFailedException
55
     */
56
    public function connect($host, $port = null, $encryption = false) {
57
        $transport = 'tcp';
58
59
        if ($encryption) {
60
            $encryption = strtolower($encryption);
0 ignored issues
show
Bug introduced by
It seems like $encryption can also be of type true; however, parameter $str of strtolower() does only seem to accept string, 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

60
            $encryption = strtolower(/** @scrutinizer ignore-type */ $encryption);
Loading history...
61
            if ($encryption == "ssl") {
62
                $transport = 'ssl';
63
                $port = $port === null ? 993 : $port;
64
            }
65
        }
66
        $port = $port === null ? 143 : $port;
67
        try {
68
            $this->stream = $this->createStream($transport, $host, $port, $this->connection_timeout);
69
            if (!$this->assumedNextLine('* OK')) {
70
                throw new ConnectionFailedException('connection refused');
71
            }
72
            if ($encryption == "tls") {
73
                $this->enableTls();
74
            }
75
        } catch (\Exception $e) {
76
            throw new ConnectionFailedException('connection failed', 0, $e);
77
        }
78
    }
79
80
    /**
81
     * Enable tls on the current connection
82
     *
83
     * @throws ConnectionFailedException
84
     * @throws RuntimeException
85
     */
86
    protected function enableTls(){
87
        $response = $this->requestAndResponse('STARTTLS');
88
        $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

88
        $result = $response && stream_socket_enable_crypto(/** @scrutinizer ignore-type */ $this->stream, true, $this->getCryptoMethod());
Loading history...
89
        if (!$result) {
90
            throw new ConnectionFailedException('failed to enable TLS');
91
        }
92
    }
93
94
    /**
95
     * Get the next line from stream
96
     *
97
     * @return string next line
98
     * @throws RuntimeException
99
     */
100
    public function nextLine() {
101
        $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

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

176
                        $line = substr($token, /** @scrutinizer ignore-type */ $chars);
Loading history...
177
                        $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

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

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

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

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

666
        $uids = $this->fetch('UID', 1, /** @scrutinizer ignore-type */ INF);
Loading history...
667
        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...
668
            return $uids;
669
        }
670
671
        foreach ($uids as $k => $v) {
672
            if ($k == $id) {
673
                return $v;
674
            }
675
        }
676
677
        throw new RuntimeException('unique id not found');
678
    }
679
680
    /**
681
     * Get a message number for a uid
682
     * @param string $id uid
683
     *
684
     * @return int message number
685
     * @throws RuntimeException
686
     */
687
    public function getMessageNumber($id) {
688
        $ids = $this->getUid();
689
        foreach ($ids as $k => $v) {
690
            if ($v == $id) {
691
                return $k;
692
            }
693
        }
694
695
        throw new RuntimeException('unique id not found');
696
    }
697
698
    /**
699
     * Get a list of available folders
700
     * @param string $reference mailbox reference for list
701
     * @param string $folder mailbox name match with wildcards
702
     *
703
     * @return array folders that matched $folder as array(name => array('delimiter' => .., 'flags' => ..))
704
     * @throws RuntimeException
705
     */
706
    public function folders($reference = '', $folder = '*') {
707
        $result = [];
708
        $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

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

850
        return $this->requestAndResponse('RENAME', /** @scrutinizer ignore-type */ $this->escapeString($old, $new), true);
Loading history...
851
    }
852
853
    /**
854
     * Delete a folder
855
     * @param string $folder folder name
856
     *
857
     * @return bool success
858
     * @throws RuntimeException
859
     */
860
    public function deleteFolder($folder) {
861
        return $this->requestAndResponse('DELETE', [$this->escapeString($folder)], true);
862
    }
863
864
    /**
865
     * Subscribe to a folder
866
     * @param string $folder folder name
867
     *
868
     * @return bool success
869
     * @throws RuntimeException
870
     */
871
    public function subscribeFolder($folder) {
872
        return $this->requestAndResponse('SUBSCRIBE', [$this->escapeString($folder)], true);
873
    }
874
875
    /**
876
     * Unsubscribe from a folder
877
     * @param string $folder folder name
878
     *
879
     * @return bool success
880
     * @throws RuntimeException
881
     */
882
    public function unsubscribeFolder($folder) {
883
        return $this->requestAndResponse('UNSUBSCRIBE', [$this->escapeString($folder)], true);
884
    }
885
886
    /**
887
     * Apply session saved changes to the server
888
     *
889
     * @return bool success
890
     * @throws RuntimeException
891
     */
892
    public function expunge() {
893
        return $this->requestAndResponse('EXPUNGE');
894
    }
895
896
    /**
897
     * Send noop command
898
     *
899
     * @return bool success
900
     * @throws RuntimeException
901
     */
902
    public function noop() {
903
        return $this->requestAndResponse('NOOP');
904
    }
905
906
    /**
907
     * Retrieve the quota level settings, and usage statics per mailbox
908
     * @param $username
909
     *
910
     * @return array
911
     * @throws RuntimeException
912
     */
913
    public function getQuota($username) {
914
        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...
915
    }
916
917
    /**
918
     * Retrieve the quota settings per user
919
     * @param string $quota_root
920
     *
921
     * @return array
922
     * @throws RuntimeException
923
     */
924
    public function getQuotaRoot($quota_root = 'INBOX') {
925
        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...
926
    }
927
928
    /**
929
     * Send idle command
930
     * @throws RuntimeException
931
     */
932
    public function idle() {
933
        $this->sendRequest('IDLE');
934
        if (!$this->assumedNextLine('+ ')) {
935
            throw new RuntimeException('idle failed');
936
        }
937
    }
938
939
    /**
940
     * Send done command
941
     * @throws RuntimeException
942
     */
943
    public function done() {
944
        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

944
        if (fwrite(/** @scrutinizer ignore-type */ $this->stream, "DONE\r\n") === false) {
Loading history...
945
            throw new RuntimeException('failed to write - connection closed?');
946
        }
947
        return $this->readResponse("*", false);
948
    }
949
950
    /**
951
     * Search for matching messages
952
     *
953
     * @param array $params
954
     * @return array message ids
955
     * @throws RuntimeException
956
     */
957
    public function search(array $params) {
958
        $response = $this->requestAndResponse('SEARCH', $params);
959
        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...
960
            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...
961
        }
962
963
        foreach ($response as $ids) {
0 ignored issues
show
Bug introduced by
The expression $response of type true is not traversable.
Loading history...
964
            if ($ids[0] == 'SEARCH') {
965
                array_shift($ids);
966
                return $ids;
967
            }
968
        }
969
        return [];
970
    }
971
972
    /**
973
     * Get a message overview
974
     * @param string $sequence
975
     * @return array
976
     *
977
     * @throws RuntimeException
978
     * @throws \Webklex\PHPIMAP\Exceptions\InvalidMessageDateException
979
     */
980
    public function overview($sequence) {
981
        $result = [];
982
        list($from, $to) = explode(":", $sequence);
983
984
        $uids = $this->getUid();
985
        $ids = [];
986
        foreach ($uids as $msgn => $v) {
987
            if ( ($to >= $msgn && $from <= $msgn) || ($to === "*" && $from <= $msgn) ){
988
                $ids[] = $msgn;
989
            }
990
        }
991
        $headers = $this->headers($ids);
992
        foreach ($headers as $msgn => $raw_header) {
993
            $result[$msgn] = (new Header($raw_header))->getAttributes();
994
        }
995
        return $result;
996
    }
997
998
    /**
999
     * Enable the debug mode
1000
     */
1001
    public function enableDebug(){
1002
        $this->debug = true;
1003
    }
1004
1005
    /**
1006
     * Disable the debug mode
1007
     */
1008
    public function disableDebug(){
1009
        $this->debug = false;
1010
    }
1011
}