Completed
Push — master ( 4995a3...c888a3 )
by Malte
01:44
created

ImapProtocol::moveMessage()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 7
rs 10
cc 3
nc 3
nop 3
1
<?php
2
/*
3
* File: ImapProtocol.php
4
* Category: Protocol
5
* Author: M.Goldenbaum
6
* Created: 16.09.20 18:27
7
* Updated: -
8
*
9
* Description:
10
*  -
11
*/
12
13
namespace Webklex\PHPIMAP\Connection\Protocols;
14
15
use Webklex\PHPIMAP\Exceptions\AuthFailedException;
16
use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
17
use Webklex\PHPIMAP\Exceptions\RuntimeException;
18
19
/**
20
 * Class ImapProtocol
21
 *
22
 * @package Webklex\PHPIMAP\Connection\Protocols
23
 */
24
class ImapProtocol extends Protocol implements ProtocolInterface {
25
26
    /**
27
     * Request noun
28
     * @var int
29
     */
30
    protected $noun = 0;
31
32
    /**
33
     * Imap constructor.
34
     * @param bool $cert_validation set to false to skip SSL certificate validation
35
     */
36
    public function __construct($cert_validation = true) {
37
        $this->setCertValidation($cert_validation);
38
    }
39
40
    /**
41
     * Public destructor
42
     */
43
    public function __destruct() {
44
        $this->logout();
45
    }
46
47
    /**
48
     * Open connection to IMAP server
49
     * @param string $host hostname or IP address of IMAP server
50
     * @param int|null $port of IMAP server, default is 143 and 993 for ssl
51
     * @param string|bool $encryption use 'SSL', 'TLS' or false
52
     *
53
     * @throws ConnectionFailedException
54
     */
55
    public function connect($host, $port = null, $encryption = false) {
56
        $transport = 'tcp';
57
58
        if ($encryption) {
59
            $encryption = strtolower($encryption);
0 ignored issues
show
Bug introduced by
It seems like $encryption can also be of type true; however, parameter $str of strtolower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

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

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

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

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

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

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

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

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

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

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

427
            fclose(/** @scrutinizer ignore-type */ $this->stream);
Loading history...
428
            $this->stream = null;
429
        }
430
        return $result;
431
    }
432
433
    /**
434
     * Check if the current session is connected
435
     *
436
     * @return bool
437
     */
438
    public function connected(){
439
        return (boolean) $this->stream;
440
    }
441
442
    /**
443
     * Get an array of available capabilities
444
     *
445
     * @return array list of capabilities
446
     * @throws RuntimeException
447
     */
448
    public function getCapabilities() {
449
        $response = $this->requestAndResponse('CAPABILITY');
450
451
        if (!$response) return [];
0 ignored issues
show
Bug Best Practice introduced by
The expression $response of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

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

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

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

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

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

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

941
        if (fwrite(/** @scrutinizer ignore-type */ $this->stream, "DONE\r\n") === false) {
Loading history...
942
            throw new RuntimeException('failed to write - connection closed?');
943
        }
944
        return $this->readResponse("*", false);
945
    }
946
947
    /**
948
     * Search for matching messages
949
     *
950
     * @param array $params
951
     * @return array message ids
952
     * @throws RuntimeException
953
     */
954
    public function search(array $params) {
955
        $response = $this->requestAndResponse('SEARCH', $params);
956
        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...
957
            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...
958
        }
959
960
        foreach ($response as $ids) {
0 ignored issues
show
Bug introduced by
The expression $response of type true is not traversable.
Loading history...
961
            if ($ids[0] == 'SEARCH') {
962
                array_shift($ids);
963
                return $ids;
964
            }
965
        }
966
        return [];
967
    }
968
969
    /**
970
     * Enable the debug mode
971
     */
972
    public function enableDebug(){
973
        $this->debug = true;
974
    }
975
976
    /**
977
     * Disable the debug mode
978
     */
979
    public function disableDebug(){
980
        $this->debug = false;
981
    }
982
}