Passed
Pull Request — master (#160)
by
unknown
03:38
created

ImapProtocol::getQuota()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
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 Exception;
16
use Webklex\PHPIMAP\Exceptions\AuthFailedException;
17
use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
18
use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException;
19
use Webklex\PHPIMAP\Exceptions\MessageNotFoundException;
20
use Webklex\PHPIMAP\Exceptions\RuntimeException;
21
use Webklex\PHPIMAP\Header;
22
23
/**
24
 * Class ImapProtocol
25
 *
26
 * @package Webklex\PHPIMAP\Connection\Protocols
27
 */
28
class ImapProtocol extends Protocol {
29
30
    /**
31
     * Request noun
32
     * @var int
33
     */
34
    protected $noun = 0;
35
36
    /**
37
     * Imap constructor.
38
     * @param bool $cert_validation set to false to skip SSL certificate validation
39
     * @param mixed $encryption Connection encryption method
40
     */
41
    public function __construct($cert_validation = true, $encryption = false) {
42
        $this->setCertValidation($cert_validation);
43
        $this->encryption = $encryption;
44
    }
45
46
    /**
47
     * Public destructor
48
     */
49
    public function __destruct() {
50
        $this->logout();
51
    }
52
53
    /**
54
     * Open connection to IMAP server
55
     * @param string $host hostname or IP address of IMAP server
56
     * @param int|null $port of IMAP server, default is 143 and 993 for ssl
57
     *
58
     * @throws ConnectionFailedException
59
     */
60
    public function connect($host, $port = null) {
61
        $transport = 'tcp';
62
        $encryption = "";
63
64
        if ($this->encryption) {
65
            $encryption = strtolower($this->encryption);
66
            if ($encryption == "ssl") {
67
                $transport = 'ssl';
68
                $port = $port === null ? 993 : $port;
69
            }
70
        }
71
        $port = $port === null ? 143 : $port;
72
        try {
73
            $this->stream = $this->createStream($transport, $host, $port, $this->connection_timeout);
74
            if (!$this->assumedNextLine('* OK')) {
75
                throw new ConnectionFailedException('connection refused');
76
            }
77
            if ($encryption == "tls") {
78
                $this->enableTls();
79
            }
80
        } catch (Exception $e) {
81
            throw new ConnectionFailedException('connection failed', 0, $e);
82
        }
83
    }
84
85
    /**
86
     * Enable tls on the current connection
87
     *
88
     * @throws ConnectionFailedException
89
     * @throws RuntimeException
90
     */
91
    protected function enableTls(){
92
        $response = $this->requestAndResponse('STARTTLS');
93
        $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

93
        $result = $response && stream_socket_enable_crypto(/** @scrutinizer ignore-type */ $this->stream, true, $this->getCryptoMethod());
Loading history...
94
        if (!$result) {
95
            throw new ConnectionFailedException('failed to enable TLS');
96
        }
97
    }
98
99
    /**
100
     * Get the next line from stream
101
     *
102
     * @return string next line
103
     * @throws RuntimeException
104
     */
105
    public function nextLine() {
106
        $line = fgets($this->stream);
0 ignored issues
show
Bug introduced by
It seems like $this->stream can also be of type boolean; however, parameter $stream 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

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

181
                        $line = substr($token, /** @scrutinizer ignore-type */ $chars);
Loading history...
182
                        $token = substr($token, 0, $chars);
0 ignored issues
show
Bug introduced by
$chars of type string is incompatible with the type integer|null 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

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

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

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

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

677
            $uids = $this->fetch('UID', 1, /** @scrutinizer ignore-type */ INF);
Loading history...
678
            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...
679
                return $uids;
680
            }
681
682
            foreach ($uids as $k => $v) {
683
                if ($k == $id) {
684
                    return $v;
685
                }
686
            }
687
        } catch (RuntimeException $e) {}
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
688
689
        throw new MessageNotFoundException('unique id not found');
690
    }
691
692
    /**
693
     * Get a message number for a uid
694
     * @param string $id uid
695
     *
696
     * @return int message number
697
     * @throws MessageNotFoundException
698
     */
699
    public function getMessageNumber($id) {
700
        $ids = $this->getUid();
701
        foreach ($ids as $k => $v) {
702
            if ($v == $id) {
703
                return $k;
704
            }
705
        }
706
707
        throw new MessageNotFoundException('message number not found');
708
    }
709
710
    /**
711
     * Get a list of available folders
712
     * @param string $reference mailbox reference for list
713
     * @param string $folder mailbox name match with wildcards
714
     *
715
     * @return array folders that matched $folder as array(name => array('delimiter' => .., 'flags' => ..))
716
     * @throws RuntimeException
717
     */
718
    public function folders($reference = '', $folder = '*') {
719
        $result = [];
720
        $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

720
        $list = $this->requestAndResponse('LIST', /** @scrutinizer ignore-type */ $this->escapeString($reference, $folder));
Loading history...
721
        if (!$list || $list === true) {
0 ignored issues
show
introduced by
The condition $list === true is always true.
Loading history...
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...
722
            return $result;
723
        }
724
725
        foreach ($list as $item) {
726
            if (count($item) != 4 || $item[0] != 'LIST') {
727
                continue;
728
            }
729
            $result[$item[3]] = ['delimiter' => $item[2], 'flags' => $item[1]];
730
        }
731
732
        return $result;
733
    }
734
735
    /**
736
     * Manage flags
737
     * @param array $flags flags to set, add or remove - see $mode
738
     * @param int $from message for items or start message if $to !== null
739
     * @param int|null $to if null only one message ($from) is fetched, else it's the
740
     *                             last message, INF means last message available
741
     * @param string|null $mode '+' to add flags, '-' to remove flags, everything else sets the flags as given
742
     * @param bool $silent if false the return values are the new flags for the wanted messages
743
     * @param bool $uid set to true if passing a unique id
744
     *
745
     * @return bool|array new flags if $silent is false, else true or false depending on success
746
     * @throws RuntimeException
747
     */
748
    public function store(array $flags, $from, $to = null, $mode = null, $silent = true, $uid = false) {
749
        $item = 'FLAGS';
750
        if ($mode == '+' || $mode == '-') {
751
            $item = $mode . $item;
752
        }
753
        if ($silent) {
754
            $item .= '.SILENT';
755
        }
756
757
        $flags = $this->escapeList($flags);
758
        $set = (int)$from;
759
        if ($to !== null) {
760
            $set .= ':' . ($to == INF ? '*' : (int)$to);
761
        }
762
763
        $command = ($uid ? "UID " : "")."STORE";
764
        $result = $this->requestAndResponse($command, [$set, $item, $flags], $silent);
765
766
        if ($silent) {
767
            return (bool)$result;
768
        }
769
770
        $tokens = $result;
771
        $result = [];
772
        foreach ($tokens as $token) {
0 ignored issues
show
Bug introduced by
The expression $tokens of type boolean|null is not traversable.
Loading history...
773
            if ($token[1] != 'FETCH' || $token[2][0] != 'FLAGS') {
774
                continue;
775
            }
776
            $result[$token[0]] = $token[2][1];
777
        }
778
779
        return $result;
780
    }
781
782
    /**
783
     * Append a new message to given folder
784
     * @param string $folder name of target folder
785
     * @param string $message full message content
786
     * @param array $flags flags for new message
787
     * @param string $date date for new message
788
     *
789
     * @return bool success
790
     * @throws RuntimeException
791
     */
792
    public function appendMessage($folder, $message, $flags = null, $date = null) {
793
        $tokens = [];
794
        $tokens[] = $this->escapeString($folder);
795
        if ($flags !== null) {
796
            $tokens[] = $this->escapeList($flags);
797
        }
798
        if ($date !== null) {
799
            $tokens[] = $this->escapeString($date);
800
        }
801
        $tokens[] = $this->escapeString($message);
802
803
        return $this->requestAndResponse('APPEND', $tokens, true);
804
    }
805
806
    /**
807
     * Copy a message set from current folder to an other folder
808
     * @param string $folder destination folder
809
     * @param $from
810
     * @param int|null $to if null only one message ($from) is fetched, else it's the
811
     *                         last message, INF means last message available
812
     * @param bool $uid set to true if passing a unique id
813
     *
814
     * @return bool success
815
     * @throws RuntimeException
816
     */
817
    public function copyMessage($folder, $from, $to = null, $uid = false) {
818
        $set = (int)$from;
819
        if ($to !== null) {
820
            $set .= ':' . ($to == INF ? '*' : (int)$to);
821
        }
822
        $command = ($uid ? "UID " : "")."COPY";
823
824
        return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true);
825
    }
826
827
    /**
828
     * Copy multiple messages to the target folder
829
     *
830
     * @param array<string> $messages List of message identifiers
831
     * @param string $folder Destination folder
832
     * @param bool $uid Set to true if you pass message unique identifiers instead of numbers
833
     * @return array|bool Tokens if operation successful, false if an error occurred
834
     *
835
     * @throws RuntimeException
836
     */
837
    public function copyManyMessages($messages, $folder, $uid = false) {
838
        $command = $uid ? 'UID COPY' : 'COPY';
839
840
        $set = implode(',', $messages);
841
        $tokens = [$set, $this->escapeString($folder)];
842
843
        return $this->requestAndResponse($command, $tokens, true);
844
    }
845
846
    /**
847
     * Move a message set from current folder to an other folder
848
     * @param string $folder destination folder
849
     * @param $from
850
     * @param int|null $to if null only one message ($from) is fetched, else it's the
851
     *                         last message, INF means last message available
852
     * @param bool $uid set to true if passing a unique id
853
     *
854
     * @return bool success
855
     * @throws RuntimeException
856
     */
857
    public function moveMessage($folder, $from, $to = null, $uid = false) {
858
        $set = (int)$from;
859
        if ($to !== null) {
860
            $set .= ':' . ($to == INF ? '*' : (int)$to);
861
        }
862
        $command = ($uid ? "UID " : "")."MOVE";
863
864
        return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true);
865
    }
866
867
    /**
868
     * Move multiple messages to the target folder
869
     *
870
     * @param array<string> $messages List of message identifiers
871
     * @param string $folder Destination folder
872
     * @param bool $uid Set to true if you pass message unique identifiers instead of numbers
873
     * @return array|bool Tokens if operation successful, false if an error occurred
874
     *
875
     * @throws RuntimeException
876
     */
877
    public function moveManyMessages($messages, $folder, $uid = false) {
878
        $command = $uid ? 'UID MOVE' : 'MOVE';
879
880
        $set = implode(',', $messages);
881
        $tokens = [$set, $this->escapeString($folder)];
882
883
        return $this->requestAndResponse($command, $tokens, true);
884
    }
885
886
    /**
887
     * Create a new folder (and parent folders if needed)
888
     * @param string $folder folder name
889
     *
890
     * @return bool success
891
     * @throws RuntimeException
892
     */
893
    public function createFolder($folder) {
894
        return $this->requestAndResponse('CREATE', [$this->escapeString($folder)], true);
895
    }
896
897
    /**
898
     * Rename an existing folder
899
     * @param string $old old name
900
     * @param string $new new name
901
     *
902
     * @return bool success
903
     * @throws RuntimeException
904
     */
905
    public function renameFolder($old, $new) {
906
        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

906
        return $this->requestAndResponse('RENAME', /** @scrutinizer ignore-type */ $this->escapeString($old, $new), true);
Loading history...
907
    }
908
909
    /**
910
     * Delete a folder
911
     * @param string $folder folder name
912
     *
913
     * @return bool success
914
     * @throws RuntimeException
915
     */
916
    public function deleteFolder($folder) {
917
        return $this->requestAndResponse('DELETE', [$this->escapeString($folder)], true);
918
    }
919
920
    /**
921
     * Subscribe to a folder
922
     * @param string $folder folder name
923
     *
924
     * @return bool success
925
     * @throws RuntimeException
926
     */
927
    public function subscribeFolder($folder) {
928
        return $this->requestAndResponse('SUBSCRIBE', [$this->escapeString($folder)], true);
929
    }
930
931
    /**
932
     * Unsubscribe from a folder
933
     * @param string $folder folder name
934
     *
935
     * @return bool success
936
     * @throws RuntimeException
937
     */
938
    public function unsubscribeFolder($folder) {
939
        return $this->requestAndResponse('UNSUBSCRIBE', [$this->escapeString($folder)], true);
940
    }
941
942
    /**
943
     * Apply session saved changes to the server
944
     *
945
     * @return bool success
946
     * @throws RuntimeException
947
     */
948
    public function expunge() {
949
        return $this->requestAndResponse('EXPUNGE');
950
    }
951
952
    /**
953
     * Send noop command
954
     *
955
     * @return bool success
956
     * @throws RuntimeException
957
     */
958
    public function noop() {
959
        return $this->requestAndResponse('NOOP');
960
    }
961
962
    /**
963
     * Retrieve the quota level settings, and usage statics per mailbox
964
     * @param $username
965
     *
966
     * @return array
967
     * @throws RuntimeException
968
     */
969
    public function getQuota($username) {
970
        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...
971
    }
972
973
    /**
974
     * Retrieve the quota settings per user
975
     * @param string $quota_root
976
     *
977
     * @return array
978
     * @throws RuntimeException
979
     */
980
    public function getQuotaRoot($quota_root = 'INBOX') {
981
        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...
982
    }
983
984
    /**
985
     * Send idle command
986
     *
987
     * @throws RuntimeException
988
     */
989
    public function idle() {
990
        $this->sendRequest("IDLE");
991
        if (!$this->assumedNextLine('+ ')) {
992
            throw new RuntimeException('idle failed');
993
        }
994
    }
995
996
    /**
997
     * Send done command
998
     * @throws RuntimeException
999
     */
1000
    public function done() {
1001
        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 $stream 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

1001
        if (fwrite(/** @scrutinizer ignore-type */ $this->stream, "DONE\r\n") === false) {
Loading history...
1002
            throw new RuntimeException('failed to write - connection closed?');
1003
        }
1004
        return true;
1005
    }
1006
1007
    /**
1008
     * Search for matching messages
1009
     * @param array $params
1010
     * @param bool $uid set to true if passing a unique id
1011
     *
1012
     * @return array message ids
1013
     * @throws RuntimeException
1014
     */
1015
    public function search(array $params, $uid = false) {
1016
        $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...
1017
        $response = $this->requestAndResponse($token, $params);
1018
        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...
1019
            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...
1020
        }
1021
1022
        foreach ($response as $ids) {
0 ignored issues
show
Bug introduced by
The expression $response of type true is not traversable.
Loading history...
1023
            if ($ids[0] == 'SEARCH') {
1024
                array_shift($ids);
1025
                return $ids;
1026
            }
1027
        }
1028
        return [];
1029
    }
1030
1031
    /**
1032
     * Get a message overview
1033
     * @param string $sequence
1034
     * @param bool $uid set to true if passing a unique id
1035
     *
1036
     * @return array
1037
     * @throws RuntimeException
1038
     * @throws MessageNotFoundException
1039
     * @throws InvalidMessageDateException
1040
     */
1041
    public function overview($sequence, $uid = false) {
1042
        $result = [];
1043
        list($from, $to) = explode(":", $sequence);
1044
1045
        $uids = $this->getUid();
1046
        $ids = [];
1047
        foreach ($uids as $msgn => $v) {
1048
            $id = $uid ? $v : $msgn;
1049
            if ( ($to >= $id && $from <= $id) || ($to === "*" && $from <= $id) ){
1050
                $ids[] = $id;
1051
            }
1052
        }
1053
        $headers = $this->headers($ids, $rfc = "RFC822", $uid);
1054
        foreach ($headers as $id => $raw_header) {
1055
            $result[$id] = (new Header($raw_header, false))->getAttributes();
1056
        }
1057
        return $result;
1058
    }
1059
1060
    /**
1061
     * Enable the debug mode
1062
     */
1063
    public function enableDebug(){
1064
        $this->debug = true;
1065
    }
1066
1067
    /**
1068
     * Disable the debug mode
1069
     */
1070
    public function disableDebug(){
1071
        $this->debug = false;
1072
    }
1073
}
1074