Passed
Push — master ( 8a8321...d2e22a )
by Malte
07:56
created

ImapProtocol::noop()   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 0
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
        do {
258
            $readAll = $this->readLine($tokens, $tag, $dontParse);
259
            $lines[] = $tokens;
260
        } while (!$readAll);
261
262
        if ($dontParse) {
263
            // First two chars are still needed for the response code
264
            $tokens = [substr($tokens, 0, 2)];
265
        }
266
267
        // last line has response code
268
        if ($tokens[0] == 'OK') {
269
            return $lines ? $lines : true;
0 ignored issues
show
introduced by
$lines is an empty array, thus is always false.
Loading history...
270
        } elseif ($tokens[0] == 'NO') {
271
            return false;
272
        }
273
274
        return;
275
    }
276
277
    /**
278
     * Send a new request
279
     * @param string $command
280
     * @param array $tokens additional parameters to command, use escapeString() to prepare
281
     * @param string $tag provide a tag otherwise an autogenerated is returned
282
     *
283
     * @throws RuntimeException
284
     */
285
    public function sendRequest($command, $tokens = [], &$tag = null) {
286
        if (!$tag) {
287
            $this->noun++;
288
            $tag = 'TAG' . $this->noun;
289
        }
290
291
        $line = $tag . ' ' . $command;
292
293
        foreach ($tokens as $token) {
294
            if (is_array($token)) {
295
                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

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

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

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

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

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

929
        return $this->requestAndResponse('RENAME', /** @scrutinizer ignore-type */ $this->escapeString($old, $new), true);
Loading history...
930
    }
931
932
    /**
933
     * Delete a folder
934
     * @param string $folder folder name
935
     *
936
     * @return bool success
937
     * @throws RuntimeException
938
     */
939
    public function deleteFolder($folder) {
940
        return $this->requestAndResponse('DELETE', [$this->escapeString($folder)], true);
941
    }
942
943
    /**
944
     * Subscribe to a folder
945
     * @param string $folder folder name
946
     *
947
     * @return bool success
948
     * @throws RuntimeException
949
     */
950
    public function subscribeFolder($folder) {
951
        return $this->requestAndResponse('SUBSCRIBE', [$this->escapeString($folder)], true);
952
    }
953
954
    /**
955
     * Unsubscribe from a folder
956
     * @param string $folder folder name
957
     *
958
     * @return bool success
959
     * @throws RuntimeException
960
     */
961
    public function unsubscribeFolder($folder) {
962
        return $this->requestAndResponse('UNSUBSCRIBE', [$this->escapeString($folder)], true);
963
    }
964
965
    /**
966
     * Apply session saved changes to the server
967
     *
968
     * @return bool success
969
     * @throws RuntimeException
970
     */
971
    public function expunge() {
972
        return $this->requestAndResponse('EXPUNGE');
973
    }
974
975
    /**
976
     * Send noop command
977
     *
978
     * @return bool success
979
     * @throws RuntimeException
980
     */
981
    public function noop() {
982
        return $this->requestAndResponse('NOOP');
983
    }
984
985
    /**
986
     * Retrieve the quota level settings, and usage statics per mailbox
987
     * @param $username
988
     *
989
     * @return array
990
     * @throws RuntimeException
991
     */
992
    public function getQuota($username) {
993
        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...
994
    }
995
996
    /**
997
     * Retrieve the quota settings per user
998
     * @param string $quota_root
999
     *
1000
     * @return array
1001
     * @throws RuntimeException
1002
     */
1003
    public function getQuotaRoot($quota_root = 'INBOX') {
1004
        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...
1005
    }
1006
1007
    /**
1008
     * Send idle command
1009
     *
1010
     * @throws RuntimeException
1011
     */
1012
    public function idle() {
1013
        $this->sendRequest("IDLE");
1014
        if (!$this->assumedNextLine('+ ')) {
1015
            throw new RuntimeException('idle failed');
1016
        }
1017
    }
1018
1019
    /**
1020
     * Send done command
1021
     * @throws RuntimeException
1022
     */
1023
    public function done() {
1024
        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

1024
        if (fwrite(/** @scrutinizer ignore-type */ $this->stream, "DONE\r\n") === false) {
Loading history...
1025
            throw new RuntimeException('failed to write - connection closed?');
1026
        }
1027
        return true;
1028
    }
1029
1030
    /**
1031
     * Search for matching messages
1032
     * @param array $params
1033
     * @param bool $uid set to true if passing a unique id
1034
     *
1035
     * @return array message ids
1036
     * @throws RuntimeException
1037
     */
1038
    public function search(array $params, $uid = false) {
1039
        $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...
1040
        $response = $this->requestAndResponse($token, $params);
1041
        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...
1042
            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...
1043
        }
1044
1045
        foreach ($response as $ids) {
0 ignored issues
show
Bug introduced by
The expression $response of type true is not traversable.
Loading history...
1046
            if ($ids[0] == 'SEARCH') {
1047
                array_shift($ids);
1048
                return $ids;
1049
            }
1050
        }
1051
        return [];
1052
    }
1053
1054
    /**
1055
     * Get a message overview
1056
     * @param string $sequence
1057
     * @param bool $uid set to true if passing a unique id
1058
     *
1059
     * @return array
1060
     * @throws RuntimeException
1061
     * @throws MessageNotFoundException
1062
     * @throws InvalidMessageDateException
1063
     */
1064
    public function overview($sequence, $uid = false) {
1065
        $result = [];
1066
        list($from, $to) = explode(":", $sequence);
1067
1068
        $uids = $this->getUid();
1069
        $ids = [];
1070
        foreach ($uids as $msgn => $v) {
1071
            $id = $uid ? $v : $msgn;
1072
            if ( ($to >= $id && $from <= $id) || ($to === "*" && $from <= $id) ){
1073
                $ids[] = $id;
1074
            }
1075
        }
1076
        $headers = $this->headers($ids, "RFC822", $uid);
1077
        foreach ($headers as $id => $raw_header) {
1078
            $result[$id] = (new Header($raw_header, false))->getAttributes();
1079
        }
1080
        return $result;
1081
    }
1082
1083
    /**
1084
     * Enable the debug mode
1085
     */
1086
    public function enableDebug(){
1087
        $this->debug = true;
1088
    }
1089
1090
    /**
1091
     * Disable the debug mode
1092
     */
1093
    public function disableDebug(){
1094
        $this->debug = false;
1095
    }
1096
}
1097