Passed
Push — master ( 88c81a...d8c3b6 )
by Malte
03:59
created

Query   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 491
Duplicated Lines 0 %

Importance

Changes 11
Bugs 2 Features 3
Metric Value
eloc 145
c 11
b 2
f 3
dl 0
loc 491
rs 3.6
wmc 60

36 Methods

Rating   Name   Duplication   Size   Complexity  
A boot() 0 1 1
A leaveUnread() 0 4 1
A markAsRead() 0 4 1
A parse_date() 0 10 3
A __construct() 0 10 2
A parse_value() 0 8 2
B search() 0 33 7
B get() 0 40 6
A paginate() 0 5 2
A count() 0 2 1
A setLimit() 0 3 2
A limit() 0 5 2
A idle() 0 26 5
A getFetchFlags() 0 2 1
A setQuery() 0 3 1
A getCharset() 0 2 1
A setPage() 0 3 1
A getQuery() 0 2 1
A setCharset() 0 3 1
A getRawQuery() 0 2 1
A getClient() 0 3 1
A setClient() 0 3 1
A setFetchAttachment() 0 3 1
A getFetchBody() 0 2 1
A fetchOptions() 0 2 1
A setFetchOptions() 0 3 1
A setFetchFlags() 0 3 1
A setRawQuery() 0 3 1
A generate_query() 0 19 3
A setFetchBody() 0 3 1
A getLimit() 0 2 1
A getFetchOptions() 0 2 1
A fetchBody() 0 2 1
A fetchAttachment() 0 2 1
A getFetchAttachment() 0 2 1
A getPage() 0 2 1

How to fix   Complexity   

Complex Class

Complex classes like Query 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 Query, and based on these observations, apply Extract Interface, too.

1
<?php
2
/*
3
* File:     Query.php
4
* Category: -
5
* Author:   M. Goldenbaum
6
* Created:  21.07.18 18:54
7
* Updated:  -
8
*
9
* Description:
10
*  -
11
*/
12
13
namespace Webklex\IMAP\Query;
14
15
use Carbon\Carbon;
16
use Webklex\IMAP\Client;
17
use Webklex\IMAP\Events\MessageNewEvent;
18
use Webklex\IMAP\Exceptions\GetMessagesFailedException;
19
use Webklex\IMAP\Exceptions\MessageSearchValidationException;
20
use Webklex\IMAP\IMAP;
21
use Webklex\IMAP\Message;
22
use Webklex\IMAP\Support\MessageCollection;
23
24
/**
25
 * Class Query
26
 *
27
 * @package Webklex\IMAP\Query
28
 */
29
class Query {
30
31
    /** @var array $query */
32
    protected $query;
33
34
    /** @var string $raw_query  */
35
    protected $raw_query;
36
37
    /** @var string $charset */
38
    protected $charset;
39
40
    /** @var Client $client */
41
    protected $client;
42
43
    /** @var int $limit */
44
    protected $limit = null;
45
46
    /** @var int $page */
47
    protected $page = 1;
48
49
    /** @var int $fetch_options */
50
    protected $fetch_options = null;
51
52
    /** @var int $fetch_body */
53
    protected $fetch_body = true;
54
55
    /** @var int $fetch_attachment */
56
    protected $fetch_attachment = true;
57
58
    /** @var int $fetch_flags */
59
    protected $fetch_flags = true;
60
61
    /** @var string $date_format */
62
    protected $date_format;
63
64
    /**
65
     * Query constructor.
66
     * @param Client $client
67
     * @param string $charset
68
     */
69
    public function __construct(Client $client, $charset = 'UTF-8') {
70
        $this->setClient($client);
71
72
        if(config('imap.options.fetch') === IMAP::FT_PEEK) $this->leaveUnread();
73
74
        $this->date_format = config('imap.date_format', 'd M y');
75
76
        $this->charset = $charset;
77
        $this->query = collect();
0 ignored issues
show
Documentation Bug introduced by
It seems like collect() of type Illuminate\Support\Collection is incompatible with the declared type array of property $query.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
78
        $this->boot();
79
    }
80
81
    /**
82
     * Instance boot method for additional functionality
83
     */
84
    protected function boot(){}
85
86
    /**
87
     * Parse a given value
88
     * @param mixed $value
89
     *
90
     * @return string
91
     */
92
    protected function parse_value($value){
93
        switch(true){
94
            case $value instanceof \Carbon\Carbon:
95
                $value = $value->format($this->date_format);
96
                break;
97
        }
98
99
        return (string) $value;
100
    }
101
102
    /**
103
     * Check if a given date is a valid carbon object and if not try to convert it
104
     * @param $date
105
     *
106
     * @return Carbon
107
     * @throws MessageSearchValidationException
108
     */
109
    protected function parse_date($date) {
110
        if($date instanceof \Carbon\Carbon) return $date;
111
112
        try {
113
            $date = Carbon::parse($date);
114
        } catch (\Exception $e) {
115
            throw new MessageSearchValidationException();
116
        }
117
118
        return $date;
119
    }
120
121
    /**
122
     * Don't mark messages as read when fetching
123
     *
124
     * @return $this
125
     */
126
    public function leaveUnread() {
127
        $this->setFetchOptions(IMAP::FT_PEEK);
0 ignored issues
show
Bug introduced by
Webklex\IMAP\IMAP::FT_PEEK of type integer is incompatible with the type boolean expected by parameter $fetch_options of Webklex\IMAP\Query\Query::setFetchOptions(). ( Ignorable by Annotation )

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

127
        $this->setFetchOptions(/** @scrutinizer ignore-type */ IMAP::FT_PEEK);
Loading history...
128
129
        return $this;
130
    }
131
132
    /**
133
     * Mark all messages as read when fetching
134
     *
135
     * @return $this
136
     */
137
    public function markAsRead() {
138
        $this->setFetchOptions(IMAP::FT_UID);
0 ignored issues
show
Bug introduced by
Webklex\IMAP\IMAP::FT_UID of type integer is incompatible with the type boolean expected by parameter $fetch_options of Webklex\IMAP\Query\Query::setFetchOptions(). ( Ignorable by Annotation )

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

138
        $this->setFetchOptions(/** @scrutinizer ignore-type */ IMAP::FT_UID);
Loading history...
139
140
        return $this;
141
    }
142
143
    /**
144
     * Perform an imap search request
145
     *
146
     * @return \Illuminate\Support\Collection
147
     * @throws \Webklex\IMAP\Exceptions\ConnectionFailedException
148
     */
149
    protected function search(){
150
        $this->generate_query();
151
        $available_messages = [];
152
153
        /**
154
         * Don't set the charset if it isn't used - prevent strange outlook mail server errors
155
         * @see https://github.com/Webklex/laravel-imap/issues/100
156
         *
157
         * If a "BADCHARSET" error gets caught it will be used to determine the desired charset.
158
         */
159
        try {
160
            if ($this->getCharset() == null) {
161
                $available_messages = \imap_search($this->getClient()->getConnection(), $this->getRawQuery(), IMAP::SE_UID);
0 ignored issues
show
Bug introduced by
It seems like $this->getClient()->getConnection() can also be of type true; however, parameter $imap_stream of imap_search() 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

161
                $available_messages = \imap_search(/** @scrutinizer ignore-type */ $this->getClient()->getConnection(), $this->getRawQuery(), IMAP::SE_UID);
Loading history...
162
            }else{
163
                $available_messages = \imap_search($this->getClient()->getConnection(), $this->getRawQuery(), IMAP::SE_UID, $this->getCharset());
164
            }
165
        } catch (\Exception $e) {
166
            if (strpos($e, ' [BADCHARSET (')) {
167
                preg_match('/ \[BADCHARSET \((.*)\)\]/', $e, $matches);
168
                if (isset($matches[1])) {
169
                    if ($matches[1] !== $this->getCharset()){
170
                        $this->setCharset($matches[1]);
171
                        return $this->search();
172
                    }
173
                }
174
            }
175
        }
176
177
        if ($available_messages !== false) {
0 ignored issues
show
introduced by
The condition $available_messages !== false is always true.
Loading history...
178
            return collect($available_messages);
179
        }
180
181
        return collect();
182
    }
183
184
    /**
185
     * Count all available messages matching the current search criteria
186
     *
187
     * @return int
188
     * @throws \Webklex\IMAP\Exceptions\ConnectionFailedException
189
     */
190
    public function count() {
191
        return $this->search()->count();
192
    }
193
194
    /**
195
     * Fetch the current query and return all found messages
196
     *
197
     * @return MessageCollection
198
     * @throws GetMessagesFailedException
199
     */
200
    public function get() {
201
        $messages = MessageCollection::make([]);
202
203
        try {
204
            $available_messages = $this->search();
205
            $available_messages_count = $available_messages->count();
206
207
            if ($available_messages_count > 0) {
208
209
                $messages->total($available_messages_count);
210
211
                $options = config('imap.options');
212
213
                if(strtolower($options['fetch_order']) === 'desc'){
214
                    $available_messages = $available_messages->reverse();
215
                }
216
217
                $query =& $this;
218
219
                $available_messages->forPage($this->page, $this->limit)->each(function($msgno, $msglist) use(&$messages, $options, $query) {
220
                    $oMessage = new Message($msgno, $msglist, $query->getClient(), $query->getFetchOptions(), $query->getFetchBody(), $query->getFetchAttachment(), $query->getFetchFlags());
0 ignored issues
show
Bug introduced by
$query->getFetchFlags() of type integer is incompatible with the type boolean expected by parameter $fetch_flags of Webklex\IMAP\Message::__construct(). ( Ignorable by Annotation )

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

220
                    $oMessage = new Message($msgno, $msglist, $query->getClient(), $query->getFetchOptions(), $query->getFetchBody(), $query->getFetchAttachment(), /** @scrutinizer ignore-type */ $query->getFetchFlags());
Loading history...
221
                    switch ($options['message_key']){
222
                        case 'number':
223
                            $message_key = $oMessage->getMessageNo();
224
                            break;
225
                        case 'list':
226
                            $message_key = $msglist;
227
                            break;
228
                        default:
229
                            $message_key = $oMessage->getMessageId();
230
                            break;
231
232
                    }
233
                    $messages->put($message_key, $oMessage);
234
                });
235
            }
236
237
            return $messages;
238
        } catch (\Exception $e) {
239
            throw new GetMessagesFailedException($e->getMessage());
240
        }
241
    }
242
243
    /**
244
     * Paginate the current query
245
     * @param int $per_page
246
     * @param null $page
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $page is correct as it would always require null to be passed?
Loading history...
247
     * @param string $page_name
248
     *
249
     * @return \Illuminate\Pagination\LengthAwarePaginator
250
     * @throws GetMessagesFailedException
251
     */
252
    public function paginate($per_page = 5, $page = null, $page_name = 'imap_page'){
253
        $this->page = $page > $this->page ? $page : $this->page;
254
        $this->limit = $per_page;
255
256
        return $this->get()->paginate($per_page, $this->page, $page_name);
257
    }
258
259
    /**
260
     * Create an idle like instance to catch incoming messages
261
     * @param callable|null $callback
262
     * @param int $timeout
263
     *
264
     * @throws \Webklex\IMAP\Exceptions\ConnectionFailedException
265
     * @throws \Webklex\IMAP\Exceptions\InvalidMessageDateException
266
     */
267
    public function idle(callable $callback = null, $timeout = 10){
268
        $known_messages = [];
269
        $this->getClient()->overview()->each(function($message) use(&$known_messages){
270
            /** @var object $message */
271
            $known_messages[] = $message->uid;
272
        });
273
        while ($this->getClient()->isConnected()){
274
            $this->getClient()->expunge();
275
            $new_messages = [];
276
            $this->getClient()->overview()->each(function($message) use(&$new_messages, &$known_messages){
277
                /** @var object $message */
278
                if (in_array($message->uid, $known_messages) == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
279
                    $new_messages[] = $message;
280
                    $known_messages[] = $message->uid;
281
                }
282
            });
283
284
            foreach($new_messages as $msg) {
285
                $message = new Message($msg->uid, $msg->msgno, $this->getClient());
286
                MessageNewEvent::dispatch($message);
287
                if ($callback){
288
                    $callback($message);
289
                }
290
            }
291
292
            sleep($timeout);
293
        }
294
    }
295
296
    /**
297
     * Get the raw IMAP search query
298
     *
299
     * @return string
300
     */
301
    public function generate_query() {
302
        $query = '';
303
        $this->query->each(function($statement) use(&$query) {
304
            if (count($statement) == 1) {
305
                $query .= $statement[0];
306
            } else {
307
                if($statement[1] === null){
308
                    $query .= $statement[0];
309
                }else{
310
                    $query .= $statement[0].' "'.$statement[1].'"';
311
                }
312
            }
313
            $query .= ' ';
314
315
        });
316
317
        $this->raw_query = trim($query);
318
319
        return $this->raw_query;
320
    }
321
322
    /**
323
     * @return Client
324
     * @throws \Webklex\IMAP\Exceptions\ConnectionFailedException
325
     */
326
    public function getClient() {
327
        $this->client->checkConnection();
328
        return $this->client;
329
    }
330
331
    /**
332
     * Set the limit and page for the current query
333
     * @param int $limit
334
     * @param int $page
335
     *
336
     * @return $this
337
     */
338
    public function limit($limit, $page = 1) {
339
        if($page >= 1) $this->page = $page;
340
        $this->limit = $limit;
341
342
        return $this;
343
    }
344
345
    /**
346
     * @return array
347
     */
348
    public function getQuery() {
349
        return $this->query;
350
    }
351
352
    /**
353
     * @param array $query
354
     * @return Query
355
     */
356
    public function setQuery($query) {
357
        $this->query = $query;
358
        return $this;
359
    }
360
361
    /**
362
     * @return string
363
     */
364
    public function getRawQuery() {
365
        return $this->raw_query;
366
    }
367
368
    /**
369
     * @param string $raw_query
370
     * @return Query
371
     */
372
    public function setRawQuery($raw_query) {
373
        $this->raw_query = $raw_query;
374
        return $this;
375
    }
376
377
    /**
378
     * @return string
379
     */
380
    public function getCharset() {
381
        return $this->charset;
382
    }
383
384
    /**
385
     * @param string $charset
386
     * @return Query
387
     */
388
    public function setCharset($charset) {
389
        $this->charset = $charset;
390
        return $this;
391
    }
392
393
    /**
394
     * @param Client $client
395
     * @return Query
396
     */
397
    public function setClient(Client $client) {
398
        $this->client = $client;
399
        return $this;
400
    }
401
402
    /**
403
     * @return int
404
     */
405
    public function getLimit() {
406
        return $this->limit;
407
    }
408
409
    /**
410
     * @param int $limit
411
     * @return Query
412
     */
413
    public function setLimit($limit) {
414
        $this->limit = $limit <= 0 ? null : $limit;
415
        return $this;
416
    }
417
418
    /**
419
     * @return int
420
     */
421
    public function getPage() {
422
        return $this->page;
423
    }
424
425
    /**
426
     * @param int $page
427
     * @return Query
428
     */
429
    public function setPage($page) {
430
        $this->page = $page;
431
        return $this;
432
    }
433
434
    /**
435
     * @param boolean $fetch_options
436
     * @return Query
437
     */
438
    public function setFetchOptions($fetch_options) {
439
        $this->fetch_options = $fetch_options;
0 ignored issues
show
Documentation Bug introduced by
The property $fetch_options was declared of type integer, but $fetch_options is of type boolean. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
440
        return $this;
441
    }
442
443
    /**
444
     * @param boolean $fetch_options
445
     * @return Query
446
     */
447
    public function fetchOptions($fetch_options) {
448
        return $this->setFetchOptions($fetch_options);
449
    }
450
451
    /**
452
     * @return int
453
     */
454
    public function getFetchOptions() {
455
        return $this->fetch_options;
456
    }
457
458
    /**
459
     * @return boolean
460
     */
461
    public function getFetchBody() {
462
        return $this->fetch_body;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->fetch_body returns the type integer which is incompatible with the documented return type boolean.
Loading history...
463
    }
464
465
    /**
466
     * @param boolean $fetch_body
467
     * @return Query
468
     */
469
    public function setFetchBody($fetch_body) {
470
        $this->fetch_body = $fetch_body;
0 ignored issues
show
Documentation Bug introduced by
The property $fetch_body was declared of type integer, but $fetch_body is of type boolean. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
471
        return $this;
472
    }
473
474
    /**
475
     * @param boolean $fetch_body
476
     * @return Query
477
     */
478
    public function fetchBody($fetch_body) {
479
        return $this->setFetchBody($fetch_body);
480
    }
481
482
    /**
483
     * @return boolean
484
     */
485
    public function getFetchAttachment() {
486
        return $this->fetch_attachment;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->fetch_attachment returns the type integer which is incompatible with the documented return type boolean.
Loading history...
487
    }
488
489
    /**
490
     * @param boolean $fetch_attachment
491
     * @return Query
492
     */
493
    public function setFetchAttachment($fetch_attachment) {
494
        $this->fetch_attachment = $fetch_attachment;
0 ignored issues
show
Documentation Bug introduced by
The property $fetch_attachment was declared of type integer, but $fetch_attachment is of type boolean. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
495
        return $this;
496
    }
497
498
    /**
499
     * @param boolean $fetch_attachment
500
     * @return Query
501
     */
502
    public function fetchAttachment($fetch_attachment) {
503
        return $this->setFetchAttachment($fetch_attachment);
504
    }
505
506
    /**
507
     * @return int
508
     */
509
    public function getFetchFlags() {
510
        return $this->fetch_flags;
511
    }
512
513
    /**
514
     * @param int $fetch_flags
515
     * @return Query
516
     */
517
    public function setFetchFlags($fetch_flags) {
518
        $this->fetch_flags = $fetch_flags;
519
        return $this;
520
    }
521
}
522