Passed
Push — master ( 133a7b...2b5dda )
by Malte
02:34
created

ImapProtocol::store()   C

Complexity

Conditions 12
Paths 192

Size

Total Lines 35
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
eloc 21
c 2
b 0
f 1
dl 0
loc 35
rs 6.2
cc 12
nc 192
nop 7

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

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

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

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

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

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

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

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

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

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

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