Passed
Push — master ( d90f4c...999d71 )
by Malte
02:39
created

Client::getFoldersWithStatus()   A

Complexity

Conditions 6
Paths 8

Size

Total Lines 25
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 16
c 0
b 0
f 0
dl 0
loc 25
rs 9.1111
cc 6
nc 8
nop 2
1
<?php
2
/*
3
* File:     Client.php
4
* Category: -
5
* Author:   M. Goldenbaum
6
* Created:  19.01.17 22:21
7
* Updated:  -
8
*
9
* Description:
10
*  -
11
*/
12
13
namespace Webklex\PHPIMAP;
14
15
use ErrorException;
16
use Webklex\PHPIMAP\Connection\Protocols\ImapProtocol;
17
use Webklex\PHPIMAP\Connection\Protocols\LegacyProtocol;
18
use Webklex\PHPIMAP\Connection\Protocols\Protocol;
19
use Webklex\PHPIMAP\Connection\Protocols\ProtocolInterface;
20
use Webklex\PHPIMAP\Exceptions\AuthFailedException;
21
use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
22
use Webklex\PHPIMAP\Exceptions\FolderFetchingException;
23
use Webklex\PHPIMAP\Exceptions\MaskNotFoundException;
24
use Webklex\PHPIMAP\Exceptions\ProtocolNotSupportedException;
25
use Webklex\PHPIMAP\Support\FolderCollection;
26
use Webklex\PHPIMAP\Support\Masks\AttachmentMask;
27
use Webklex\PHPIMAP\Support\Masks\MessageMask;
28
use Webklex\PHPIMAP\Traits\HasEvents;
29
30
/**
31
 * Class Client
32
 *
33
 * @package Webklex\PHPIMAP
34
 */
35
class Client {
36
    use HasEvents;
37
38
    /**
39
     * Connection resource
40
     *
41
     * @var boolean|Protocol|ProtocolInterface
42
     */
43
    public $connection = false;
44
45
    /**
46
     * Server hostname.
47
     *
48
     * @var string
49
     */
50
    public $host;
51
52
    /**
53
     * Server port.
54
     *
55
     * @var int
56
     */
57
    public $port;
58
59
    /**
60
     * Service protocol.
61
     *
62
     * @var int
63
     */
64
    public $protocol;
65
66
    /**
67
     * Server encryption.
68
     * Supported: none, ssl, tls, starttls or notls.
69
     *
70
     * @var string
71
     */
72
    public $encryption;
73
74
    /**
75
     * If server has to validate cert.
76
     *
77
     * @var bool
78
     */
79
    public $validate_cert = true;
80
81
    /**
82
     * Proxy settings
83
     * @var array
84
     */
85
    protected $proxy = [
86
        'socket' => null,
87
        'request_fulluri' => false,
88
        'username' => null,
89
        'password' => null,
90
    ];
91
92
    /**
93
     * Connection timeout
94
     * @var int $timeout
95
     */
96
    public $timeout;
97
98
    /**
99
     * Account username/
100
     *
101
     * @var mixed
102
     */
103
    public $username;
104
105
    /**
106
     * Account password.
107
     *
108
     * @var string
109
     */
110
    public $password;
111
112
    /**
113
     * Additional data fetched from the server.
114
     *
115
     * @var string
116
     */
117
    public $extensions;
118
119
    /**
120
     * Account authentication method.
121
     *
122
     * @var string
123
     */
124
    public $authentication;
125
126
    /**
127
     * Active folder path.
128
     *
129
     * @var string
130
     */
131
    protected $active_folder = null;
132
133
    /**
134
     * Default message mask
135
     *
136
     * @var string $default_message_mask
137
     */
138
    protected $default_message_mask = MessageMask::class;
139
140
    /**
141
     * Default attachment mask
142
     *
143
     * @var string $default_attachment_mask
144
     */
145
    protected $default_attachment_mask = AttachmentMask::class;
146
147
    /**
148
     * Used default account values
149
     *
150
     * @var array $default_account_config
151
     */
152
    protected $default_account_config = [
153
        'host' => 'localhost',
154
        'port' => 993,
155
        'protocol'  => 'imap',
156
        'encryption' => 'ssl',
157
        'validate_cert' => true,
158
        'username' => '',
159
        'password' => '',
160
        'authentication' => null,
161
        "extensions" => [],
162
        'proxy' => [
163
            'socket' => null,
164
            'request_fulluri' => false,
165
            'username' => null,
166
            'password' => null,
167
        ],
168
        "timeout" => 30
169
    ];
170
171
    /**
172
     * Client constructor.
173
     * @param array $config
174
     *
175
     * @throws MaskNotFoundException
176
     */
177
    public function __construct(array $config = []) {
178
        $this->setConfig($config);
179
        $this->setMaskFromConfig($config);
180
        $this->setEventsFromConfig($config);
181
    }
182
183
    /**
184
     * Client destructor
185
     */
186
    public function __destruct() {
187
        $this->disconnect();
188
    }
189
190
    /**
191
     * Set the Client configuration
192
     * @param array $config
193
     *
194
     * @return self
195
     */
196
    public function setConfig(array $config): Client {
197
        $default_account = ClientManager::get('default');
198
        $default_config  = ClientManager::get("accounts.$default_account");
199
200
        foreach ($this->default_account_config as $key => $value) {
201
            $this->setAccountConfig($key, $config, $default_config);
0 ignored issues
show
Bug introduced by
It seems like $default_config can also be of type null; however, parameter $default_config of Webklex\PHPIMAP\Client::setAccountConfig() 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

201
            $this->setAccountConfig($key, $config, /** @scrutinizer ignore-type */ $default_config);
Loading history...
202
        }
203
204
        return $this;
205
    }
206
207
    /**
208
     * Set a specific account config
209
     * @param string $key
210
     * @param array $config
211
     * @param array $default_config
212
     */
213
    private function setAccountConfig(string $key, array $config, array $default_config){
214
        $value = $this->default_account_config[$key];
215
        if(isset($config[$key])) {
216
            $value = $config[$key];
217
        }elseif(isset($default_config[$key])) {
218
            $value = $default_config[$key];
219
        }
220
        $this->$key = $value;
221
    }
222
223
    /**
224
     * Look for a possible events in any available config
225
     * @param $config
226
     */
227
    protected function setEventsFromConfig($config) {
228
        $this->events = ClientManager::get("events");
229
        if(isset($config['events'])){
230
            foreach($config['events'] as $section => $events) {
231
                $this->events[$section] = array_merge($this->events[$section], $events);
232
            }
233
        }
234
    }
235
236
    /**
237
     * Look for a possible mask in any available config
238
     * @param $config
239
     *
240
     * @throws MaskNotFoundException
241
     */
242
    protected function setMaskFromConfig($config) {
243
        $default_config  = ClientManager::get("masks");
244
245
        if(isset($config['masks'])){
246
            if(isset($config['masks']['message'])) {
247
                if(class_exists($config['masks']['message'])) {
248
                    $this->default_message_mask = $config['masks']['message'];
249
                }else{
250
                    throw new MaskNotFoundException("Unknown mask provided: ".$config['masks']['message']);
251
                }
252
            }else{
253
                if(class_exists($default_config['message'])) {
254
                    $this->default_message_mask = $default_config['message'];
255
                }else{
256
                    throw new MaskNotFoundException("Unknown mask provided: ".$default_config['message']);
257
                }
258
            }
259
            if(isset($config['masks']['attachment'])) {
260
                if(class_exists($config['masks']['attachment'])) {
261
                    $this->default_attachment_mask = $config['masks']['attachment'];
262
                }else{
263
                    throw new MaskNotFoundException("Unknown mask provided: ".$config['masks']['attachment']);
264
                }
265
            }else{
266
                if(class_exists($default_config['attachment'])) {
267
                    $this->default_attachment_mask = $default_config['attachment'];
268
                }else{
269
                    throw new MaskNotFoundException("Unknown mask provided: ".$default_config['attachment']);
270
                }
271
            }
272
        }else{
273
            if(class_exists($default_config['message'])) {
274
                $this->default_message_mask = $default_config['message'];
275
            }else{
276
                throw new MaskNotFoundException("Unknown mask provided: ".$default_config['message']);
277
            }
278
279
            if(class_exists($default_config['attachment'])) {
280
                $this->default_attachment_mask = $default_config['attachment'];
281
            }else{
282
                throw new MaskNotFoundException("Unknown mask provided: ".$default_config['attachment']);
283
            }
284
        }
285
286
    }
287
288
    /**
289
     * Get the current imap resource
290
     *
291
     * @return bool|Protocol|ProtocolInterface
292
     * @throws ConnectionFailedException
293
     */
294
    public function getConnection() {
295
        $this->checkConnection();
296
        return $this->connection;
297
    }
298
299
    /**
300
     * Determine if connection was established.
301
     *
302
     * @return bool
303
     */
304
    public function isConnected(): bool {
305
        return $this->connection && $this->connection->connected();
306
    }
307
308
    /**
309
     * Determine if connection was established and connect if not.
310
     *
311
     * @throws ConnectionFailedException
312
     */
313
    public function checkConnection() {
314
        if (!$this->isConnected()) {
315
            $this->connect();
316
        }
317
    }
318
319
    /**
320
     * Force the connection to reconnect
321
     *
322
     * @throws ConnectionFailedException
323
     */
324
    public function reconnect() {
325
        if ($this->isConnected()) {
326
            $this->disconnect();
327
        }
328
        $this->connect();
329
    }
330
331
    /**
332
     * Connect to server.
333
     *
334
     * @return $this
335
     * @throws ConnectionFailedException
336
     */
337
    public function connect(): Client {
338
        $this->disconnect();
339
        $protocol = strtolower($this->protocol);
340
341
        if (in_array($protocol, ['imap', 'imap4', 'imap4rev1'])) {
342
            $this->connection = new ImapProtocol($this->validate_cert, $this->encryption);
343
            $this->connection->setConnectionTimeout($this->timeout);
344
            $this->connection->setProxy($this->proxy);
345
        }else{
346
            if (extension_loaded('imap') === false) {
347
                throw new ConnectionFailedException("connection setup failed", 0, new ProtocolNotSupportedException($protocol." is an unsupported protocol"));
348
            }
349
            $this->connection = new LegacyProtocol($this->validate_cert, $this->encryption);
350
            if (strpos($protocol, "legacy-") === 0) {
351
                $protocol = substr($protocol, 7);
352
            }
353
            $this->connection->setProtocol($protocol);
354
        }
355
356
        if (ClientManager::get('options.debug')) {
357
            $this->connection->enableDebug();
358
        }
359
360
        if (!ClientManager::get('options.uid_cache')) {
361
            $this->connection->disableUidCache();
362
        }
363
364
        try {
365
            $this->connection->connect($this->host, $this->port);
366
        } catch (ErrorException $e) {
367
            throw new ConnectionFailedException("connection setup failed", 0, $e);
368
        } catch (Exceptions\RuntimeException $e) {
369
            throw new ConnectionFailedException("connection setup failed", 0, $e);
370
        }
371
        $this->authenticate();
372
373
        return $this;
374
    }
375
376
    /**
377
     * Authenticate the current session
378
     *
379
     * @throws ConnectionFailedException
380
     */
381
    protected function authenticate() {
382
        try {
383
            if ($this->authentication == "oauth") {
384
                if (!$this->connection->authenticate($this->username, $this->password)) {
385
                    throw new AuthFailedException();
386
                }
387
            } elseif (!$this->connection->login($this->username, $this->password)) {
388
                throw new AuthFailedException();
389
            }
390
        } catch (AuthFailedException $e) {
391
            throw new ConnectionFailedException("connection setup failed", 0, $e);
392
        }
393
    }
394
395
    /**
396
     * Disconnect from server.
397
     *
398
     * @return $this
399
     */
400
    public function disconnect(): Client {
401
        if ($this->isConnected() && $this->connection !== false) {
402
            $this->connection->logout();
403
        }
404
        $this->active_folder = null;
405
406
        return $this;
407
    }
408
409
    /**
410
     * Get a folder instance by a folder name
411
     * @param string $folder_name
412
     * @param string|bool|null $delimiter
413
     *
414
     * @return mixed
415
     * @throws ConnectionFailedException
416
     * @throws FolderFetchingException
417
     * @throws Exceptions\RuntimeException
418
     */
419
    public function getFolder(string $folder_name, $delimiter = null) {
420
        if ($delimiter !== false && $delimiter !== null) {
421
            return $this->getFolderByPath($folder_name);
422
        }
423
424
        // Set delimiter to false to force selection via getFolderByName (maybe useful for uncommon folder names)
425
        $delimiter = is_null($delimiter) ? ClientManager::get('options.delimiter', "/") : $delimiter;
426
        if (strpos($folder_name, (string)$delimiter) !== false) {
427
            return $this->getFolderByPath($folder_name);
428
        }
429
430
        return $this->getFolderByName($folder_name);
431
    }
432
433
    /**
434
     * Get a folder instance by a folder name
435
     * @param $folder_name
436
     *
437
     * @return mixed
438
     * @throws ConnectionFailedException
439
     * @throws FolderFetchingException
440
     * @throws Exceptions\RuntimeException
441
     */
442
    public function getFolderByName($folder_name) {
443
        return $this->getFolders(false)->where("name", $folder_name)->first();
444
    }
445
446
    /**
447
     * Get a folder instance by a folder path
448
     * @param $folder_path
449
     *
450
     * @return mixed
451
     * @throws ConnectionFailedException
452
     * @throws FolderFetchingException
453
     * @throws Exceptions\RuntimeException
454
     */
455
    public function getFolderByPath($folder_path) {
456
        return $this->getFolders(false)->where("path", $folder_path)->first();
457
    }
458
459
    /**
460
     * Get folders list.
461
     * If hierarchical order is set to true, it will make a tree of folders, otherwise it will return flat array.
462
     *
463
     * @param boolean $hierarchical
464
     * @param string|null $parent_folder
465
     *
466
     * @return FolderCollection
467
     * @throws ConnectionFailedException
468
     * @throws FolderFetchingException
469
     * @throws Exceptions\RuntimeException
470
     */
471
    public function getFolders(bool $hierarchical = true, string $parent_folder = null): FolderCollection {
472
        $this->checkConnection();
473
        $folders = FolderCollection::make([]);
474
475
        $pattern = $parent_folder.($hierarchical ? '%' : '*');
476
        $items = $this->connection->folders('', $pattern);
477
478
        if(is_array($items)){
479
            foreach ($items as $folder_name => $item) {
480
                $folder = new Folder($this, $folder_name, $item["delimiter"], $item["flags"]);
481
482
                if ($hierarchical && $folder->hasChildren()) {
483
                    $pattern = $folder->full_name.$folder->delimiter.'%';
484
485
                    $children = $this->getFolders(true, $pattern);
486
                    $folder->setChildren($children);
487
                }
488
489
                $folders->push($folder);
490
            }
491
492
            return $folders;
493
        }else{
494
            throw new FolderFetchingException("failed to fetch any folders");
495
        }
496
    }
497
498
    /**
499
     * Get folders list.
500
     * If hierarchical order is set to true, it will make a tree of folders, otherwise it will return flat array.
501
     *
502
     * @param boolean $hierarchical
503
     * @param string|null $parent_folder
504
     *
505
     * @return FolderCollection
506
     * @throws ConnectionFailedException
507
     * @throws FolderFetchingException
508
     * @throws Exceptions\RuntimeException
509
     */
510
    public function getFoldersWithStatus(bool $hierarchical = true, string $parent_folder = null): FolderCollection {
511
        $this->checkConnection();
512
        $folders = FolderCollection::make([]);
513
514
        $pattern = $parent_folder.($hierarchical ? '%' : '*');
515
        $items = $this->connection->folders('', $pattern);
516
517
        if(is_array($items)){
518
            foreach ($items as $folder_name => $item) {
519
                $folder = new Folder($this, $folder_name, $item["delimiter"], $item["flags"]);
520
521
                if ($hierarchical && $folder->hasChildren()) {
522
                    $pattern = $folder->full_name.$folder->delimiter.'%';
523
524
                    $children = $this->getFolders(true, $pattern);
525
                    $folder->setChildren($children);
526
                }
527
528
                $folder->loadStatus();
529
                $folders->push($folder);
530
            }
531
532
            return $folders;
533
        }else{
534
            throw new FolderFetchingException("failed to fetch any folders");
535
        }
536
    }
537
538
    /**
539
     * Open a given folder.
540
     * @param string $folder_path
541
     * @param boolean $force_select
542
     *
543
     * @return array|bool
544
     * @throws ConnectionFailedException
545
     * @throws Exceptions\RuntimeException
546
     */
547
    public function openFolder(string $folder_path, bool $force_select = false) {
548
        if ($this->active_folder == $folder_path && $this->isConnected() && $force_select === false) {
549
            return true;
550
        }
551
        $this->checkConnection();
552
        $this->active_folder = $folder_path;
553
        return $this->connection->selectFolder($folder_path);
554
    }
555
556
    /**
557
     * Create a new Folder
558
     * @param string $folder
559
     * @param boolean $expunge
560
     *
561
     * @return Folder
562
     * @throws ConnectionFailedException
563
     * @throws FolderFetchingException
564
     * @throws Exceptions\EventNotFoundException
565
     * @throws Exceptions\RuntimeException
566
     */
567
    public function createFolder(string $folder, bool $expunge = true): Folder {
568
        $this->checkConnection();
569
        $status = $this->connection->createFolder($folder);
570
571
        if($expunge) $this->expunge();
572
573
        $folder = $this->getFolder($folder);
574
        if($status && $folder) {
575
            $event = $this->getEvent("folder", "new");
576
            $event::dispatch($folder);
577
        }
578
579
        return $folder;
580
    }
581
582
    /**
583
     * Check a given folder
584
     * @param $folder
585
     *
586
     * @return array|bool
587
     * @throws ConnectionFailedException
588
     * @throws Exceptions\RuntimeException
589
     */
590
    public function checkFolder($folder) {
591
        $this->checkConnection();
592
        return $this->connection->examineFolder($folder);
593
    }
594
595
    /**
596
     * Get the current active folder
597
     *
598
     * @return string
599
     */
600
    public function getFolderPath(){
601
        return $this->active_folder;
602
    }
603
604
    /**
605
     * Exchange identification information
606
     * Ref.: https://datatracker.ietf.org/doc/html/rfc2971
607
     *
608
     * @param array|null $ids
609
     * @return array|bool|void|null
610
     *
611
     * @throws ConnectionFailedException
612
     * @throws Exceptions\RuntimeException
613
     */
614
    public function Id(array $ids = null) {
615
        $this->checkConnection();
616
        return $this->connection->ID($ids);
617
    }
618
619
    /**
620
     * Retrieve the quota level settings, and usage statics per mailbox
621
     *
622
     * @return array
623
     * @throws ConnectionFailedException
624
     * @throws Exceptions\RuntimeException
625
     */
626
    public function getQuota(): array {
627
        $this->checkConnection();
628
        return $this->connection->getQuota($this->username);
629
    }
630
631
    /**
632
     * Retrieve the quota settings per user
633
     * @param string $quota_root
634
     *
635
     * @return array
636
     * @throws ConnectionFailedException
637
     */
638
    public function getQuotaRoot(string $quota_root = 'INBOX'): array {
639
        $this->checkConnection();
640
        return $this->connection->getQuotaRoot($quota_root);
641
    }
642
643
    /**
644
     * Delete all messages marked for deletion
645
     *
646
     * @return bool
647
     * @throws ConnectionFailedException
648
     * @throws Exceptions\RuntimeException
649
     */
650
    public function expunge(): bool {
651
        $this->checkConnection();
652
        return $this->connection->expunge();
653
    }
654
655
    /**
656
     * Set the connection timeout
657
     * @param integer $timeout
658
     *
659
     * @return Protocol
660
     * @throws ConnectionFailedException
661
     */
662
    public function setTimeout(int $timeout): Protocol {
663
        $this->timeout = $timeout;
664
        if ($this->isConnected()) {
665
            $this->connection->setConnectionTimeout($timeout);
0 ignored issues
show
Bug introduced by
The method setConnectionTimeout() does not exist on Webklex\PHPIMAP\Connecti...ocols\ProtocolInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Webklex\PHPIMAP\Connecti...ocols\ProtocolInterface. ( Ignorable by Annotation )

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

665
            $this->connection->/** @scrutinizer ignore-call */ 
666
                               setConnectionTimeout($timeout);
Loading history...
666
            $this->reconnect();
667
        }
668
        return $this->connection;
669
    }
670
671
    /**
672
     * Get the connection timeout
673
     *
674
     * @return int
675
     * @throws ConnectionFailedException
676
     */
677
    public function getTimeout(): int {
678
        $this->checkConnection();
679
        return $this->connection->getConnectionTimeout();
680
    }
681
682
    /**
683
     * Get the default message mask
684
     *
685
     * @return string
686
     */
687
    public function getDefaultMessageMask(): string {
688
        return $this->default_message_mask;
689
    }
690
691
    /**
692
     * Get the default events for a given section
693
     * @param $section
694
     *
695
     * @return array
696
     */
697
    public function getDefaultEvents($section): array {
698
        if (isset($this->events[$section])) {
699
            return is_array($this->events[$section]) ? $this->events[$section] : [];
700
        }
701
        return [];
702
    }
703
704
    /**
705
     * Set the default message mask
706
     * @param string $mask
707
     *
708
     * @return $this
709
     * @throws MaskNotFoundException
710
     */
711
    public function setDefaultMessageMask(string $mask): Client {
712
        if(class_exists($mask)) {
713
            $this->default_message_mask = $mask;
714
715
            return $this;
716
        }
717
718
        throw new MaskNotFoundException("Unknown mask provided: ".$mask);
719
    }
720
721
    /**
722
     * Get the default attachment mask
723
     *
724
     * @return string
725
     */
726
    public function getDefaultAttachmentMask(): string {
727
        return $this->default_attachment_mask;
728
    }
729
730
    /**
731
     * Set the default attachment mask
732
     * @param string $mask
733
     *
734
     * @return $this
735
     * @throws MaskNotFoundException
736
     */
737
    public function setDefaultAttachmentMask(string $mask): Client {
738
        if(class_exists($mask)) {
739
            $this->default_attachment_mask = $mask;
740
741
            return $this;
742
        }
743
744
        throw new MaskNotFoundException("Unknown mask provided: ".$mask);
745
    }
746
}
747