Passed
Push — master ( a42b86...188068 )
by Malte
03:38 queued 13s
created

Client   F

Complexity

Total Complexity 77

Size/Duplication

Total Lines 647
Duplicated Lines 0 %

Importance

Changes 14
Bugs 2 Features 2
Metric Value
wmc 77
eloc 188
c 14
b 2
f 2
dl 0
loc 647
rs 2.24

32 Methods

Rating   Name   Duplication   Size   Complexity  
A getQuotaRoot() 0 3 1
A setEventsFromConfig() 0 5 3
A __destruct() 0 2 1
A disconnect() 0 7 3
A getFolderByName() 0 2 1
A getDefaultMessageMask() 0 2 1
A getQuota() 0 3 1
A setConfig() 0 9 2
A setTimeout() 0 3 1
A getFolders() 0 24 6
A setAccountConfig() 0 8 3
A Id() 0 3 1
A checkConnection() 0 3 2
A getDefaultEvents() 0 2 1
A isConnected() 0 2 2
A authenticate() 0 11 5
A setDefaultAttachmentMask() 0 8 2
A expunge() 0 3 1
B setMaskFromConfig() 0 41 10
A getTimeout() 0 3 1
A getFolder() 0 12 5
A checkFolder() 0 3 1
A createFolder() 0 13 4
A setDefaultMessageMask() 0 8 2
A reconnect() 0 5 2
A getConnection() 0 3 1
A __construct() 0 4 1
A getDefaultAttachmentMask() 0 2 1
A getFolderByPath() 0 2 1
B connect() 0 29 6
A getFolderPath() 0 2 1
A openFolder() 0 7 4

How to fix   Complexity   

Complex Class

Complex classes like Client often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Client, and based on these observations, apply Extract Interface, too.

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, 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
     * Account authentication method.
114
     *
115
     * @var string
116
     */
117
    public $authentication;
118
119
    /**
120
     * Active folder path.
121
     *
122
     * @var string
123
     */
124
    protected $active_folder = null;
125
126
    /**
127
     * Default message mask
128
     *
129
     * @var string $default_message_mask
130
     */
131
    protected $default_message_mask = MessageMask::class;
132
133
    /**
134
     * Default attachment mask
135
     *
136
     * @var string $default_attachment_mask
137
     */
138
    protected $default_attachment_mask = AttachmentMask::class;
139
140
    /**
141
     * Used default account values
142
     *
143
     * @var array $default_account_config
144
     */
145
    protected $default_account_config = [
146
        'host' => 'localhost',
147
        'port' => 993,
148
        'protocol'  => 'imap',
149
        'encryption' => 'ssl',
150
        'validate_cert' => true,
151
        'username' => '',
152
        'password' => '',
153
        'authentication' => null,
154
        'proxy' => [
155
            'socket' => null,
156
            'request_fulluri' => false,
157
            'username' => null,
158
            'password' => null,
159
        ],
160
        "timeout" => 30
161
    ];
162
163
    /**
164
     * Client constructor.
165
     * @param array $config
166
     *
167
     * @throws MaskNotFoundException
168
     */
169
    public function __construct($config = []) {
170
        $this->setConfig($config);
171
        $this->setMaskFromConfig($config);
172
        $this->setEventsFromConfig($config);
173
    }
174
175
    /**
176
     * Client destructor
177
     */
178
    public function __destruct() {
179
        $this->disconnect();
180
    }
181
182
    /**
183
     * Set the Client configuration
184
     * @param array $config
185
     *
186
     * @return self
187
     */
188
    public function setConfig(array $config) {
189
        $default_account = ClientManager::get('default');
190
        $default_config  = ClientManager::get("accounts.$default_account");
191
192
        foreach ($this->default_account_config as $key => $value) {
193
            $this->setAccountConfig($key, $config, $default_config);
194
        }
195
196
        return $this;
197
    }
198
199
    /**
200
     * Set a specific account config
201
     * @param string $key
202
     * @param array $config
203
     * @param array $default_config
204
     */
205
    private function setAccountConfig($key, $config, $default_config){
206
        $value = $this->default_account_config[$key];
207
        if(isset($config[$key])) {
208
            $value = $config[$key];
209
        }elseif(isset($default_config[$key])) {
210
            $value = $default_config[$key];
211
        }
212
        $this->$key = $value;
213
    }
214
215
    /**
216
     * Look for a possible events in any available config
217
     * @param $config
218
     */
219
    protected function setEventsFromConfig($config) {
220
        $this->events = ClientManager::get("events");
221
        if(isset($config['events'])){
222
            foreach($config['events'] as $section => $events) {
223
                $this->events[$section] = array_merge($this->events[$section], $events);
224
            }
225
        }
226
    }
227
228
    /**
229
     * Look for a possible mask in any available config
230
     * @param $config
231
     *
232
     * @throws MaskNotFoundException
233
     */
234
    protected function setMaskFromConfig($config) {
235
        $default_config  = ClientManager::get("masks");
236
237
        if(isset($config['masks'])){
238
            if(isset($config['masks']['message'])) {
239
                if(class_exists($config['masks']['message'])) {
240
                    $this->default_message_mask = $config['masks']['message'];
241
                }else{
242
                    throw new MaskNotFoundException("Unknown mask provided: ".$config['masks']['message']);
243
                }
244
            }else{
245
                if(class_exists($default_config['message'])) {
246
                    $this->default_message_mask = $default_config['message'];
247
                }else{
248
                    throw new MaskNotFoundException("Unknown mask provided: ".$default_config['message']);
249
                }
250
            }
251
            if(isset($config['masks']['attachment'])) {
252
                if(class_exists($config['masks']['attachment'])) {
253
                    $this->default_attachment_mask = $config['masks']['attachment'];
254
                }else{
255
                    throw new MaskNotFoundException("Unknown mask provided: ".$config['masks']['attachment']);
256
                }
257
            }else{
258
                if(class_exists($default_config['attachment'])) {
259
                    $this->default_attachment_mask = $default_config['attachment'];
260
                }else{
261
                    throw new MaskNotFoundException("Unknown mask provided: ".$default_config['attachment']);
262
                }
263
            }
264
        }else{
265
            if(class_exists($default_config['message'])) {
266
                $this->default_message_mask = $default_config['message'];
267
            }else{
268
                throw new MaskNotFoundException("Unknown mask provided: ".$default_config['message']);
269
            }
270
271
            if(class_exists($default_config['attachment'])) {
272
                $this->default_attachment_mask = $default_config['attachment'];
273
            }else{
274
                throw new MaskNotFoundException("Unknown mask provided: ".$default_config['attachment']);
275
            }
276
        }
277
278
    }
279
280
    /**
281
     * Get the current imap resource
282
     *
283
     * @return bool|Protocol|ProtocolInterface
284
     * @throws ConnectionFailedException
285
     */
286
    public function getConnection() {
287
        $this->checkConnection();
288
        return $this->connection;
289
    }
290
291
    /**
292
     * Determine if connection was established.
293
     *
294
     * @return bool
295
     */
296
    public function isConnected() {
297
        return $this->connection ? $this->connection->connected() : false;
298
    }
299
300
    /**
301
     * Determine if connection was established and connect if not.
302
     *
303
     * @throws ConnectionFailedException
304
     */
305
    public function checkConnection() {
306
        if (!$this->isConnected()) {
307
            $this->connect();
308
        }
309
    }
310
311
    /**
312
     * Force a reconnect
313
     *
314
     * @throws ConnectionFailedException
315
     */
316
    public function reconnect() {
317
        if ($this->isConnected()) {
318
            $this->disconnect();
319
        }
320
        $this->connect();
321
    }
322
323
    /**
324
     * Connect to server.
325
     *
326
     * @return $this
327
     * @throws ConnectionFailedException
328
     */
329
    public function connect() {
330
        $this->disconnect();
331
        $protocol = strtolower($this->protocol);
332
333
        if (in_array($protocol, ['imap', 'imap4', 'imap4rev1'])) {
334
            $this->connection = new ImapProtocol($this->validate_cert, $this->encryption);
335
            $this->connection->setConnectionTimeout($this->timeout);
336
            $this->connection->setProxy($this->proxy);
337
        }else{
338
            if (extension_loaded('imap') === false) {
339
                throw new ConnectionFailedException("connection setup failed", 0, new ProtocolNotSupportedException($protocol." is an unsupported protocol"));
340
            }
341
            $this->connection = new LegacyProtocol($this->validate_cert, $this->encryption);
342
            if (strpos($protocol, "legacy-") === 0) {
343
                $protocol = substr($protocol, 7);
344
            }
345
            $this->connection->setProtocol($protocol);
346
        }
347
348
        try {
349
            $this->connection->connect($this->host, $this->port);
350
        } catch (ErrorException $e) {
351
            throw new ConnectionFailedException("connection setup failed", 0, $e);
352
        } catch (Exceptions\RuntimeException $e) {
353
            throw new ConnectionFailedException("connection setup failed", 0, $e);
354
        }
355
        $this->authenticate();
356
357
        return $this;
358
    }
359
360
    /**
361
     * Authenticate the current session
362
     *
363
     * @throws ConnectionFailedException
364
     */
365
    protected function authenticate() {
366
        try {
367
            if ($this->authentication == "oauth") {
368
                if (!$this->connection->authenticate($this->username, $this->password)) {
369
                    throw new AuthFailedException();
370
                }
371
            } elseif (!$this->connection->login($this->username, $this->password)) {
372
                throw new AuthFailedException();
373
            }
374
        } catch (AuthFailedException $e) {
375
            throw new ConnectionFailedException("connection setup failed", 0, $e);
376
        }
377
    }
378
379
    /**
380
     * Disconnect from server.
381
     *
382
     * @return $this
383
     */
384
    public function disconnect() {
385
        if ($this->isConnected() && $this->connection !== false) {
386
            $this->connection->logout();
387
        }
388
        $this->active_folder = null;
389
390
        return $this;
391
    }
392
393
    /**
394
     * Get a folder instance by a folder name
395
     * @param string $folder_name
396
     * @param string|bool|null $delimiter
397
     *
398
     * @return mixed
399
     * @throws ConnectionFailedException
400
     * @throws FolderFetchingException
401
     * @throws Exceptions\RuntimeException
402
     */
403
    public function getFolder($folder_name, $delimiter = null) {
404
        if ($delimiter !== false && $delimiter !== null) {
405
            return $this->getFolderByPath($folder_name);
406
        }
407
408
        // Set delimiter to false to force selection via getFolderByName (maybe useful for uncommon folder names)
409
        $delimiter = is_null($delimiter) ? ClientManager::get('options.delimiter', "/") : $delimiter;
410
        if (strpos($folder_name, (string)$delimiter) !== false) {
411
            return $this->getFolderByPath($folder_name);
412
        }
413
414
        return $this->getFolderByName($folder_name);
415
    }
416
417
    /**
418
     * Get a folder instance by a folder name
419
     * @param $folder_name
420
     *
421
     * @return mixed
422
     * @throws ConnectionFailedException
423
     * @throws FolderFetchingException
424
     * @throws Exceptions\RuntimeException
425
     */
426
    public function getFolderByName($folder_name) {
427
        return $this->getFolders(false)->where("name", $folder_name)->first();
428
    }
429
430
    /**
431
     * Get a folder instance by a folder path
432
     * @param $folder_path
433
     *
434
     * @return mixed
435
     * @throws ConnectionFailedException
436
     * @throws FolderFetchingException
437
     * @throws Exceptions\RuntimeException
438
     */
439
    public function getFolderByPath($folder_path) {
440
        return $this->getFolders(false)->where("path", $folder_path)->first();
441
    }
442
443
    /**
444
     * Get folders list.
445
     * If hierarchical order is set to true, it will make a tree of folders, otherwise it will return flat array.
446
     *
447
     * @param boolean $hierarchical
448
     * @param string|null $parent_folder
449
     *
450
     * @return FolderCollection
451
     * @throws ConnectionFailedException
452
     * @throws FolderFetchingException
453
     * @throws Exceptions\RuntimeException
454
     */
455
    public function getFolders($hierarchical = true, $parent_folder = null) {
456
        $this->checkConnection();
457
        $folders = FolderCollection::make([]);
458
459
        $pattern = $parent_folder.($hierarchical ? '%' : '*');
460
        $items = $this->connection->folders('', $pattern);
461
462
        if(is_array($items)){
0 ignored issues
show
introduced by
The condition is_array($items) is always true.
Loading history...
463
            foreach ($items as $folder_name => $item) {
464
                $folder = new Folder($this, $folder_name, $item["delimiter"], $item["flags"]);
465
466
                if ($hierarchical && $folder->hasChildren()) {
467
                    $pattern = $folder->full_name.$folder->delimiter.'%';
468
469
                    $children = $this->getFolders(true, $pattern);
470
                    $folder->setChildren($children);
471
                }
472
473
                $folders->push($folder);
474
            }
475
476
            return $folders;
477
        }else{
478
            throw new FolderFetchingException("failed to fetch any folders");
479
        }
480
    }
481
482
    /**
483
     * Open a given folder.
484
     * @param string $folder_path
485
     * @param boolean $force_select
486
     *
487
     * @return mixed
488
     * @throws ConnectionFailedException
489
     * @throws Exceptions\RuntimeException
490
     */
491
    public function openFolder($folder_path, $force_select = false) {
492
        if ($this->active_folder == $folder_path && $this->isConnected() && $force_select === false) {
493
            return true;
494
        }
495
        $this->checkConnection();
496
        $this->active_folder = $folder_path;
497
        return $this->connection->selectFolder($folder_path);
498
    }
499
500
    /**
501
     * Create a new Folder
502
     * @param string $folder
503
     * @param boolean $expunge
504
     *
505
     * @return bool
506
     * @throws ConnectionFailedException
507
     * @throws FolderFetchingException
508
     * @throws Exceptions\EventNotFoundException
509
     * @throws Exceptions\RuntimeException
510
     */
511
    public function createFolder($folder, $expunge = true) {
512
        $this->checkConnection();
513
        $status = $this->connection->createFolder($folder);
514
515
        if($expunge) $this->expunge();
516
517
        $folder = $this->getFolder($folder);
518
        if($status && $folder) {
519
            $event = $this->getEvent("folder", "new");
520
            $event::dispatch($folder);
521
        }
522
523
        return $folder;
524
    }
525
526
    /**
527
     * Check a given folder
528
     * @param $folder
529
     *
530
     * @return false|object
531
     * @throws ConnectionFailedException
532
     * @throws Exceptions\RuntimeException
533
     */
534
    public function checkFolder($folder) {
535
        $this->checkConnection();
536
        return $this->connection->examineFolder($folder);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->connection->examineFolder($folder) returns the type array|boolean which is incompatible with the documented return type false|object.
Loading history...
537
    }
538
539
    /**
540
     * Get the current active folder
541
     *
542
     * @return string
543
     */
544
    public function getFolderPath(){
545
        return $this->active_folder;
546
    }
547
548
    /**
549
     * Exchange identification information
550
     * Ref.: https://datatracker.ietf.org/doc/html/rfc2971
551
     *
552
     * @param null|array $ids
553
     * @return array|bool|void|null
554
     *
555
     * @throws ConnectionFailedException
556
     * @throws Exceptions\RuntimeException
557
     */
558
    public function Id($ids = null) {
559
        $this->checkConnection();
560
        return $this->connection->ID($ids);
0 ignored issues
show
Bug introduced by
It seems like $ids can also be of type array; however, parameter $ids of Webklex\PHPIMAP\Connecti...ProtocolInterface::ID() 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

560
        return $this->connection->ID(/** @scrutinizer ignore-type */ $ids);
Loading history...
561
    }
562
563
    /**
564
     * Retrieve the quota level settings, and usage statics per mailbox
565
     *
566
     * @return array
567
     * @throws ConnectionFailedException
568
     * @throws Exceptions\RuntimeException
569
     */
570
    public function getQuota() {
571
        $this->checkConnection();
572
        return $this->connection->getQuota($this->username);
573
    }
574
575
    /**
576
     * Retrieve the quota settings per user
577
     * @param string $quota_root
578
     *
579
     * @return array
580
     * @throws ConnectionFailedException
581
     */
582
    public function getQuotaRoot($quota_root = 'INBOX') {
583
        $this->checkConnection();
584
        return $this->connection->getQuotaRoot($quota_root);
585
    }
586
587
    /**
588
     * Delete all messages marked for deletion
589
     *
590
     * @return bool
591
     * @throws ConnectionFailedException
592
     * @throws Exceptions\RuntimeException
593
     */
594
    public function expunge() {
595
        $this->checkConnection();
596
        return $this->connection->expunge();
597
    }
598
599
    /**
600
     * Set the connection timeout
601
     * @param integer $timeout
602
     *
603
     * @return Protocol
604
     * @throws ConnectionFailedException
605
     */
606
    public function setTimeout($timeout) {
607
        $this->checkConnection();
608
        return $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

608
        return $this->connection->/** @scrutinizer ignore-call */ setConnectionTimeout($timeout);
Loading history...
Bug Best Practice introduced by
The expression return $this->connection...ectionTimeout($timeout) also could return the type boolean which is incompatible with the documented return type Webklex\PHPIMAP\Connection\Protocols\Protocol.
Loading history...
609
    }
610
611
    /**
612
     * Get the connection timeout
613
     *
614
     * @return int
615
     * @throws ConnectionFailedException
616
     */
617
    public function getTimeout(){
618
        $this->checkConnection();
619
        return $this->connection->getConnectionTimeout();
0 ignored issues
show
Bug introduced by
The method getConnectionTimeout() 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

619
        return $this->connection->/** @scrutinizer ignore-call */ getConnectionTimeout();
Loading history...
620
    }
621
622
    /**
623
     * Get the default message mask
624
     *
625
     * @return string
626
     */
627
    public function getDefaultMessageMask(){
628
        return $this->default_message_mask;
629
    }
630
631
    /**
632
     * Get the default events for a given section
633
     * @param $section
634
     *
635
     * @return array
636
     */
637
    public function getDefaultEvents($section){
638
        return $this->events[$section];
639
    }
640
641
    /**
642
     * Set the default message mask
643
     * @param $mask
644
     *
645
     * @return $this
646
     * @throws MaskNotFoundException
647
     */
648
    public function setDefaultMessageMask($mask) {
649
        if(class_exists($mask)) {
650
            $this->default_message_mask = $mask;
651
652
            return $this;
653
        }
654
655
        throw new MaskNotFoundException("Unknown mask provided: ".$mask);
656
    }
657
658
    /**
659
     * Get the default attachment mask
660
     *
661
     * @return string
662
     */
663
    public function getDefaultAttachmentMask(){
664
        return $this->default_attachment_mask;
665
    }
666
667
    /**
668
     * Set the default attachment mask
669
     * @param $mask
670
     *
671
     * @return $this
672
     * @throws MaskNotFoundException
673
     */
674
    public function setDefaultAttachmentMask($mask) {
675
        if(class_exists($mask)) {
676
            $this->default_attachment_mask = $mask;
677
678
            return $this;
679
        }
680
681
        throw new MaskNotFoundException("Unknown mask provided: ".$mask);
682
    }
683
}
684