Passed
Push — master ( 190707...14a343 )
by Malte
02:09
created

ImapProtocol::enableTls()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
c 1
b 0
f 0
dl 0
loc 5
rs 10
cc 3
nc 4
nop 0
1
<?php
2
/*
3
* File: ImapProtocol.php
4
* Category: Protocol
5
* Author: M.Goldenbaum
6
* Created: 16.09.20 18:27
7
* Updated: -
8
*
9
* Description:
10
*  -
11
*/
12
13
namespace Webklex\PHPIMAP\Connection\Protocols;
14
15
use Exception;
16
use Webklex\PHPIMAP\Exceptions\AuthFailedException;
17
use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
18
use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException;
19
use Webklex\PHPIMAP\Exceptions\MessageNotFoundException;
20
use Webklex\PHPIMAP\Exceptions\RuntimeException;
21
use Webklex\PHPIMAP\Header;
22
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 (in_array($encryption, ['ssl', 'tls'])) {
68
                $transport = $encryption;
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 == 'starttls') {
79
                $this->enableStartTls();
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 enableStartTls(){
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($this->buildUIDCommand("FETCH", $uid), [$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
        $flags = $this->escapeList($flags);
757
        $set = $this->buildSet($from, $to);
0 ignored issues
show
Bug introduced by
It seems like $to can also be of type integer; however, parameter $to of Webklex\PHPIMAP\Connecti...mapProtocol::buildSet() does only seem to accept null, 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

757
        $set = $this->buildSet($from, /** @scrutinizer ignore-type */ $to);
Loading history...
758
759
        $command = $this->buildUIDCommand("STORE", $uid);
760
        $item = ($mode == '-' ? "-" : "+").($item === null ? "FLAGS" : $item).($silent ? '.SILENT' : "");
761
762
        $response = $this->requestAndResponse($command, [$set, $item, $flags], $silent);
763
764
        if ($silent) {
765
            return (bool)$response;
766
        }
767
768
        $result = [];
769
        foreach ($response as $token) {
0 ignored issues
show
Bug introduced by
The expression $response of type boolean|null is not traversable.
Loading history...
770
            if ($token[1] != 'FETCH' || $token[2][0] != 'FLAGS') {
771
                continue;
772
            }
773
            $result[$token[0]] = $token[2][1];
774
        }
775
776
        return $result;
777
    }
778
779
    /**
780
     * Append a new message to given folder
781
     * @param string $folder name of target folder
782
     * @param string $message full message content
783
     * @param array $flags flags for new message
784
     * @param string $date date for new message
785
     *
786
     * @return bool success
787
     * @throws RuntimeException
788
     */
789
    public function appendMessage($folder, $message, $flags = null, $date = null) {
790
        $tokens = [];
791
        $tokens[] = $this->escapeString($folder);
792
        if ($flags !== null) {
793
            $tokens[] = $this->escapeList($flags);
794
        }
795
        if ($date !== null) {
796
            $tokens[] = $this->escapeString($date);
797
        }
798
        $tokens[] = $this->escapeString($message);
799
800
        return $this->requestAndResponse('APPEND', $tokens, true);
801
    }
802
803
    /**
804
     * Copy a message set from current folder to an other folder
805
     * @param string $folder destination folder
806
     * @param $from
807
     * @param int|null $to if null only one message ($from) is fetched, else it's the
808
     *                         last message, INF means last message available
809
     * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use
810
     * message numbers instead.
811
     *
812
     * @return bool success
813
     * @throws RuntimeException
814
     */
815
    public function copyMessage($folder, $from, $to = null, $uid = IMAP::ST_UID) {
816
        $set = $this->buildSet($from, $to);
0 ignored issues
show
Bug introduced by
It seems like $to can also be of type integer; however, parameter $to of Webklex\PHPIMAP\Connecti...mapProtocol::buildSet() does only seem to accept null, 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

816
        $set = $this->buildSet($from, /** @scrutinizer ignore-type */ $to);
Loading history...
817
        $command = $this->buildUIDCommand("COPY", $uid);
818
        return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true);
819
    }
820
821
    /**
822
     * Copy multiple messages to the target folder
823
     *
824
     * @param array<string> $messages List of message identifiers
825
     * @param string $folder Destination folder
826
     * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use
827
     * message numbers instead.
828
     * @return array|bool Tokens if operation successful, false if an error occurred
829
     *
830
     * @throws RuntimeException
831
     */
832
    public function copyManyMessages($messages, $folder, $uid = IMAP::ST_UID) {
833
        $command = $this->buildUIDCommand("COPY", $uid);
834
835
        $set = implode(',', $messages);
836
        $tokens = [$set, $this->escapeString($folder)];
837
838
        return $this->requestAndResponse($command, $tokens, true);
839
    }
840
841
    /**
842
     * Move a message set from current folder to an other folder
843
     * @param string $folder destination folder
844
     * @param $from
845
     * @param int|null $to if null only one message ($from) is fetched, else it's the
846
     *                         last message, INF means last message available
847
     * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use
848
     * message numbers instead.
849
     *
850
     * @return bool success
851
     * @throws RuntimeException
852
     */
853
    public function moveMessage($folder, $from, $to = null, $uid = IMAP::ST_UID) {
854
        $set = $this->buildSet($from, $to);
0 ignored issues
show
Bug introduced by
It seems like $to can also be of type integer; however, parameter $to of Webklex\PHPIMAP\Connecti...mapProtocol::buildSet() does only seem to accept null, 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

854
        $set = $this->buildSet($from, /** @scrutinizer ignore-type */ $to);
Loading history...
855
        $command = $this->buildUIDCommand("MOVE", $uid);
856
857
        return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true);
858
    }
859
860
    /**
861
     * Move multiple messages to the target folder
862
     * @param array<string> $messages List of message identifiers
863
     * @param string $folder Destination folder
864
     * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use
865
     * message numbers instead.
866
     *
867
     * @return array|bool Tokens if operation successful, false if an error occurred
868
     * @throws RuntimeException
869
     */
870
    public function moveManyMessages($messages, $folder, $uid = IMAP::ST_UID) {
871
        $command = $this->buildUIDCommand("MOVE", $uid);
872
873
        $set = implode(',', $messages);
874
        $tokens = [$set, $this->escapeString($folder)];
875
876
        return $this->requestAndResponse($command, $tokens, true);
877
    }
878
879
    /**
880
     * Exchange identification information
881
     * Ref.: https://datatracker.ietf.org/doc/html/rfc2971
882
     *
883
     * @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...
884
     * @return array|bool|void|null
885
     *
886
     * @throws RuntimeException
887
     */
888
    public function ID($ids = null) {
889
        $token = "NIL";
890
        if (is_array($ids) && !empty($ids)) {
0 ignored issues
show
introduced by
The condition is_array($ids) is always false.
Loading history...
891
            $token = "(";
892
            foreach ($ids as $id) {
893
                $token .= '"'.$id.'" ';
894
            }
895
            $token = rtrim($token).")";
896
        }
897
898
        return $this->requestAndResponse("ID", [$token], true);
899
    }
900
901
    /**
902
     * Create a new folder (and parent folders if needed)
903
     * @param string $folder folder name
904
     *
905
     * @return bool success
906
     * @throws RuntimeException
907
     */
908
    public function createFolder($folder) {
909
        return $this->requestAndResponse('CREATE', [$this->escapeString($folder)], true);
910
    }
911
912
    /**
913
     * Rename an existing folder
914
     * @param string $old old name
915
     * @param string $new new name
916
     *
917
     * @return bool success
918
     * @throws RuntimeException
919
     */
920
    public function renameFolder($old, $new) {
921
        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

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

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