Passed
Push — master ( d27f53...96eb05 )
by Malte
04:59
created

Query::search()   B

Complexity

Conditions 7
Paths 25

Size

Total Lines 33
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 7
eloc 17
c 2
b 0
f 0
nc 25
nop 0
dl 0
loc 33
rs 8.8333
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
     * @throws GetMessagesFailedException
264
     * @throws \Webklex\IMAP\Exceptions\ConnectionFailedException
265
     */
266
    public function idle(callable $callback = null, $timeout = 10){
267
        $known_messages = [];
268
        $this->get()->each(function($message) use(&$known_messages){
269
            /** @var Message $message */
270
            $known_messages[] = $message->getToken();
271
        });
272
        while ($this->getClient()->isConnected()){
273
            $this->getClient()->expunge();
274
            $this->get()->each(function($message) use(&$known_messages, $callback){
275
                /** @var \Webklex\IMAP\Message $message */
276
                $token = $message->getToken();
277
                if(in_array($token, $known_messages)){
278
                    return;
279
                }
280
                $known_messages[] = $token;
281
                MessageNewEvent::dispatch($message);
282
                if ($callback){
283
                    $callback($message);
284
                }
285
            });
286
            sleep($timeout);
287
        }
288
    }
289
290
    /**
291
     * Get the raw IMAP search query
292
     *
293
     * @return string
294
     */
295
    public function generate_query() {
296
        $query = '';
297
        $this->query->each(function($statement) use(&$query) {
298
            if (count($statement) == 1) {
299
                $query .= $statement[0];
300
            } else {
301
                if($statement[1] === null){
302
                    $query .= $statement[0];
303
                }else{
304
                    $query .= $statement[0].' "'.$statement[1].'"';
305
                }
306
            }
307
            $query .= ' ';
308
309
        });
310
311
        $this->raw_query = trim($query);
312
313
        return $this->raw_query;
314
    }
315
316
    /**
317
     * @return Client
318
     * @throws \Webklex\IMAP\Exceptions\ConnectionFailedException
319
     */
320
    public function getClient() {
321
        $this->client->checkConnection();
322
        return $this->client;
323
    }
324
325
    /**
326
     * Set the limit and page for the current query
327
     * @param int $limit
328
     * @param int $page
329
     *
330
     * @return $this
331
     */
332
    public function limit($limit, $page = 1) {
333
        if($page >= 1) $this->page = $page;
334
        $this->limit = $limit;
335
336
        return $this;
337
    }
338
339
    /**
340
     * @return array
341
     */
342
    public function getQuery() {
343
        return $this->query;
344
    }
345
346
    /**
347
     * @param array $query
348
     * @return Query
349
     */
350
    public function setQuery($query) {
351
        $this->query = $query;
352
        return $this;
353
    }
354
355
    /**
356
     * @return string
357
     */
358
    public function getRawQuery() {
359
        return $this->raw_query;
360
    }
361
362
    /**
363
     * @param string $raw_query
364
     * @return Query
365
     */
366
    public function setRawQuery($raw_query) {
367
        $this->raw_query = $raw_query;
368
        return $this;
369
    }
370
371
    /**
372
     * @return string
373
     */
374
    public function getCharset() {
375
        return $this->charset;
376
    }
377
378
    /**
379
     * @param string $charset
380
     * @return Query
381
     */
382
    public function setCharset($charset) {
383
        $this->charset = $charset;
384
        return $this;
385
    }
386
387
    /**
388
     * @param Client $client
389
     * @return Query
390
     */
391
    public function setClient(Client $client) {
392
        $this->client = $client;
393
        return $this;
394
    }
395
396
    /**
397
     * @return int
398
     */
399
    public function getLimit() {
400
        return $this->limit;
401
    }
402
403
    /**
404
     * @param int $limit
405
     * @return Query
406
     */
407
    public function setLimit($limit) {
408
        $this->limit = $limit <= 0 ? null : $limit;
409
        return $this;
410
    }
411
412
    /**
413
     * @return int
414
     */
415
    public function getPage() {
416
        return $this->page;
417
    }
418
419
    /**
420
     * @param int $page
421
     * @return Query
422
     */
423
    public function setPage($page) {
424
        $this->page = $page;
425
        return $this;
426
    }
427
428
    /**
429
     * @param boolean $fetch_options
430
     * @return Query
431
     */
432
    public function setFetchOptions($fetch_options) {
433
        $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...
434
        return $this;
435
    }
436
437
    /**
438
     * @param boolean $fetch_options
439
     * @return Query
440
     */
441
    public function fetchOptions($fetch_options) {
442
        return $this->setFetchOptions($fetch_options);
443
    }
444
445
    /**
446
     * @return int
447
     */
448
    public function getFetchOptions() {
449
        return $this->fetch_options;
450
    }
451
452
    /**
453
     * @return boolean
454
     */
455
    public function getFetchBody() {
456
        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...
457
    }
458
459
    /**
460
     * @param boolean $fetch_body
461
     * @return Query
462
     */
463
    public function setFetchBody($fetch_body) {
464
        $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...
465
        return $this;
466
    }
467
468
    /**
469
     * @param boolean $fetch_body
470
     * @return Query
471
     */
472
    public function fetchBody($fetch_body) {
473
        return $this->setFetchBody($fetch_body);
474
    }
475
476
    /**
477
     * @return boolean
478
     */
479
    public function getFetchAttachment() {
480
        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...
481
    }
482
483
    /**
484
     * @param boolean $fetch_attachment
485
     * @return Query
486
     */
487
    public function setFetchAttachment($fetch_attachment) {
488
        $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...
489
        return $this;
490
    }
491
492
    /**
493
     * @param boolean $fetch_attachment
494
     * @return Query
495
     */
496
    public function fetchAttachment($fetch_attachment) {
497
        return $this->setFetchAttachment($fetch_attachment);
498
    }
499
500
    /**
501
     * @return int
502
     */
503
    public function getFetchFlags() {
504
        return $this->fetch_flags;
505
    }
506
507
    /**
508
     * @param int $fetch_flags
509
     * @return Query
510
     */
511
    public function setFetchFlags($fetch_flags) {
512
        $this->fetch_flags = $fetch_flags;
513
        return $this;
514
    }
515
}
516