Message::make()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 27
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
eloc 19
c 2
b 0
f 1
dl 0
loc 27
rs 9.6333
cc 2
nc 2
nop 8

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
/*
3
* File:     Message.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 ReflectionClass;
16
use ReflectionException;
17
use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException;
18
use Webklex\PHPIMAP\Exceptions\MaskNotFoundException;
19
use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException;
20
use Webklex\PHPIMAP\Exceptions\MessageFlagException;
21
use Webklex\PHPIMAP\Exceptions\MessageHeaderFetchingException;
22
use Webklex\PHPIMAP\Exceptions\MethodNotFoundException;
23
use Webklex\PHPIMAP\Support\AttachmentCollection;
24
use Webklex\PHPIMAP\Support\FlagCollection;
25
use Webklex\PHPIMAP\Support\Masks\MessageMask;
26
use Illuminate\Support\Str;
27
use Webklex\PHPIMAP\Support\MessageCollection;
28
use Webklex\PHPIMAP\Traits\HasEvents;
29
30
/**
31
 * Class Message
32
 *
33
 * @package Webklex\PHPIMAP
34
 *
35
 * @property integer msglist
36
 * @property integer uid
37
 * @property integer msgn
38
 * @property Attribute subject
39
 * @property Attribute message_id
40
 * @property Attribute message_no
41
 * @property Attribute references
42
 * @property Attribute date
43
 * @property Attribute from
44
 * @property Attribute to
45
 * @property Attribute cc
46
 * @property Attribute bcc
47
 * @property Attribute reply_to
48
 * @property Attribute in_reply_to
49
 * @property Attribute sender
50
 *
51
 * @method integer getMsglist()
52
 * @method integer setMsglist($msglist)
53
 * @method integer getUid()
54
 * @method integer getMsgn()
55
 * @method Attribute getPriority()
56
 * @method Attribute getSubject()
57
 * @method Attribute getMessageId()
58
 * @method Attribute getMessageNo()
59
 * @method Attribute getReferences()
60
 * @method Attribute getDate()
61
 * @method Attribute getFrom()
62
 * @method Attribute getTo()
63
 * @method Attribute getCc()
64
 * @method Attribute getBcc()
65
 * @method Attribute getReplyTo()
66
 * @method Attribute getInReplyTo()
67
 * @method Attribute getSender()
68
 */
69
class Message {
70
    use HasEvents;
71
72
    /**
73
     * Client instance
74
     *
75
     * @var Client
76
     */
77
    private $client = Client::class;
78
79
    /**
80
     * Default mask
81
     *
82
     * @var string $mask
83
     */
84
    protected $mask = MessageMask::class;
85
86
    /**
87
     * Used config
88
     *
89
     * @var array $config
90
     */
91
    protected $config = [];
92
93
    /**
94
     * Attribute holder
95
     *
96
     * @var Attribute[]|array $attributes
97
     */
98
    protected $attributes = [];
99
100
    /**
101
     * The message folder path
102
     *
103
     * @var string $folder_path
104
     */
105
    protected $folder_path;
106
107
    /**
108
     * Fetch body options
109
     *
110
     * @var integer
111
     */
112
    public $fetch_options = null;
113
114
    /**
115
     * @var integer
116
     */
117
    protected $sequence = IMAP::NIL;
118
119
    /**
120
     * Fetch body options
121
     *
122
     * @var bool
123
     */
124
    public $fetch_body = null;
125
126
    /**
127
     * Fetch flags options
128
     *
129
     * @var bool
130
     */
131
    public $fetch_flags = null;
132
133
    /**
134
     * @var Header $header
135
     */
136
    public $header = null;
137
138
    /**
139
     * Raw message body
140
     *
141
     * @var null|string $raw_body
142
     */
143
    public $raw_body = null;
144
145
    /**
146
     * Message structure
147
     *
148
     * @var Structure $structure
149
     */
150
    protected $structure = null;
151
152
    /**
153
     * Message body components
154
     *
155
     * @var array   $bodies
156
     */
157
    public $bodies = [];
158
159
    /** @var AttachmentCollection $attachments */
160
    public $attachments;
161
162
    /** @var FlagCollection $flags */
163
    public $flags;
164
165
    /**
166
     * A list of all available and supported flags
167
     *
168
     * @var array $available_flags
169
     */
170
    private $available_flags = null;
171
172
    /**
173
     * Message constructor.
174
     * @param integer $uid
175
     * @param integer|null $msglist
176
     * @param Client $client
177
     * @param integer|null $fetch_options
178
     * @param boolean $fetch_body
179
     * @param boolean $fetch_flags
180
     * @param integer|null $sequence
181
     *
182
     * @throws Exceptions\ConnectionFailedException
183
     * @throws InvalidMessageDateException
184
     * @throws Exceptions\RuntimeException
185
     * @throws MessageHeaderFetchingException
186
     * @throws MessageContentFetchingException
187
     * @throws Exceptions\EventNotFoundException
188
     * @throws MessageFlagException
189
     * @throws Exceptions\MessageNotFoundException
190
     */
191
    public function __construct(int $uid, $msglist, Client $client, int $fetch_options = null, bool $fetch_body = false, bool $fetch_flags = false, int $sequence = null) {
192
        $this->boot();
193
194
        $default_mask = $client->getDefaultMessageMask();
195
        if($default_mask != null) {
196
            $this->mask = $default_mask;
197
        }
198
        $this->events["message"] = $client->getDefaultEvents("message");
199
        $this->events["flag"] = $client->getDefaultEvents("flag");
200
201
        $this->folder_path = $client->getFolderPath();
202
203
        $this->setSequence($sequence);
204
        $this->setFetchOption($fetch_options);
205
        $this->setFetchBodyOption($fetch_body);
206
        $this->setFetchFlagsOption($fetch_flags);
207
208
        $this->client = $client;
209
        $this->client->openFolder($this->folder_path);
210
211
        $this->setSequenceId($uid, $msglist);
212
213
        if ($this->fetch_options == IMAP::FT_PEEK) {
214
            $this->parseFlags();
215
        }
216
217
        $this->parseHeader();
218
219
        if ($this->getFetchBodyOption() === true) {
220
            $this->parseBody();
221
        }
222
223
        if ($this->getFetchFlagsOption() === true && $this->fetch_options !== IMAP::FT_PEEK) {
224
            $this->parseFlags();
225
        }
226
    }
227
228
    /**
229
     * Create a new instance without fetching the message header and providing them raw instead
230
     * @param int $uid
231
     * @param int|null $msglist
232
     * @param Client $client
233
     * @param string $raw_header
234
     * @param string $raw_body
235
     * @param array $raw_flags
236
     * @param null $fetch_options
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $fetch_options is correct as it would always require null to be passed?
Loading history...
237
     * @param null $sequence
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $sequence is correct as it would always require null to be passed?
Loading history...
238
     *
239
     * @return Message
240
     * @throws Exceptions\ConnectionFailedException
241
     * @throws Exceptions\EventNotFoundException
242
     * @throws InvalidMessageDateException
243
     * @throws MessageContentFetchingException
244
     * @throws ReflectionException
245
     * @throws MessageFlagException
246
     * @throws Exceptions\RuntimeException
247
     * @throws Exceptions\MessageNotFoundException
248
     */
249
    public static function make(int $uid, $msglist, Client $client, string $raw_header, string $raw_body, array $raw_flags, $fetch_options = null, $sequence = null): Message {
250
        $reflection = new ReflectionClass(self::class);
251
        /** @var self $instance */
252
        $instance = $reflection->newInstanceWithoutConstructor();
253
        $instance->boot();
254
255
        $default_mask = $client->getDefaultMessageMask();
256
        if($default_mask != null) {
257
            $instance->setMask($default_mask);
258
        }
259
        $instance->setEvents([
260
            "message" => $client->getDefaultEvents("message"),
261
            "flag" => $client->getDefaultEvents("flag"),
262
        ]);
263
        $instance->setFolderPath($client->getFolderPath());
264
        $instance->setSequence($sequence);
265
        $instance->setFetchOption($fetch_options);
266
267
        $instance->setClient($client);
268
        $instance->setSequenceId($uid, $msglist);
269
270
        $instance->parseRawHeader($raw_header);
271
        $instance->parseRawFlags($raw_flags);
272
        $instance->parseRawBody($raw_body);
273
        $instance->peek();
274
275
        return $instance;
276
    }
277
278
    /**
279
     * Boot a new instance
280
     */
281
    public function boot(){
282
        $this->attributes = [];
283
284
        $this->config = ClientManager::get('options');
285
        $this->available_flags = ClientManager::get('flags');
286
287
        $this->attachments = AttachmentCollection::make([]);
288
        $this->flags = FlagCollection::make([]);
289
    }
290
291
    /**
292
     * Call dynamic attribute setter and getter methods
293
     * @param string $method
294
     * @param array $arguments
295
     *
296
     * @return mixed
297
     * @throws MethodNotFoundException
298
     */
299
    public function __call(string $method, array $arguments) {
300
        if(strtolower(substr($method, 0, 3)) === 'get') {
301
            $name = Str::snake(substr($method, 3));
302
            return $this->get($name);
303
        }elseif (strtolower(substr($method, 0, 3)) === 'set') {
304
            $name = Str::snake(substr($method, 3));
305
306
            if(in_array($name, array_keys($this->attributes))) {
307
                return $this->__set($name, array_pop($arguments));
308
            }
309
310
        }
311
312
        throw new MethodNotFoundException("Method ".self::class.'::'.$method.'() is not supported');
313
    }
314
315
    /**
316
     * Magic setter
317
     * @param $name
318
     * @param $value
319
     *
320
     * @return mixed
321
     */
322
    public function __set($name, $value) {
323
        $this->attributes[$name] = $value;
324
325
        return $this->attributes[$name];
326
    }
327
328
    /**
329
     * Magic getter
330
     * @param $name
331
     *
332
     * @return Attribute|mixed|null
333
     */
334
    public function __get($name) {
335
        return $this->get($name);
336
    }
337
338
    /**
339
     * Get an available message or message header attribute
340
     * @param $name
341
     *
342
     * @return Attribute|mixed|null
343
     */
344
    public function get($name) {
345
        if(isset($this->attributes[$name])) {
346
            return $this->attributes[$name];
347
        }
348
349
        return $this->header->get($name);
350
    }
351
352
    /**
353
     * Check if the Message has a text body
354
     *
355
     * @return bool
356
     */
357
    public function hasTextBody(): bool {
358
        return isset($this->bodies['text']);
359
    }
360
361
    /**
362
     * Get the Message text body
363
     *
364
     * @return mixed
365
     */
366
    public function getTextBody() {
367
        if (!isset($this->bodies['text'])) {
368
            return null;
369
        }
370
371
        return $this->bodies['text'];
372
    }
373
374
    /**
375
     * Check if the Message has a html body
376
     *
377
     * @return bool
378
     */
379
    public function hasHTMLBody(): bool {
380
        return isset($this->bodies['html']);
381
    }
382
383
    /**
384
     * Get the Message html body
385
     *
386
     * @return string|null
387
     */
388
    public function getHTMLBody() {
389
        if (!isset($this->bodies['html'])) {
390
            return null;
391
        }
392
393
        return $this->bodies['html'];
394
    }
395
396
    /**
397
     * Parse all defined headers
398
     *
399
     * @throws Exceptions\ConnectionFailedException
400
     * @throws Exceptions\RuntimeException
401
     * @throws InvalidMessageDateException
402
     * @throws MessageHeaderFetchingException
403
     */
404
    private function parseHeader() {
405
        $sequence_id = $this->getSequenceId();
406
        $headers = $this->client->getConnection()->headers([$sequence_id], "RFC822", $this->sequence === IMAP::ST_UID);
407
        if (!isset($headers[$sequence_id])) {
408
            throw new MessageHeaderFetchingException("no headers found", 0);
409
        }
410
411
        $this->parseRawHeader($headers[$sequence_id]);
412
    }
413
414
    /**
415
     * @param string $raw_header
416
     *
417
     * @throws InvalidMessageDateException
418
     */
419
    public function parseRawHeader(string $raw_header){
420
        $this->header = new Header($raw_header);
421
    }
422
423
    /**
424
     * Parse additional raw flags
425
     * @param array $raw_flags
426
     */
427
    public function parseRawFlags(array $raw_flags) {
428
        $this->flags = FlagCollection::make([]);
429
430
        foreach($raw_flags as $flag) {
431
            if (strpos($flag, "\\") === 0){
432
                $flag = substr($flag, 1);
433
            }
434
            $flag_key = strtolower($flag);
435
            if ($this->available_flags === null || in_array($flag_key, $this->available_flags)) {
436
                $this->flags->put($flag_key, $flag);
437
            }
438
        }
439
    }
440
441
    /**
442
     * Parse additional flags
443
     *
444
     * @return void
445
     * @throws Exceptions\ConnectionFailedException
446
     * @throws MessageFlagException
447
     * @throws Exceptions\RuntimeException
448
     */
449
    private function parseFlags() {
450
        $this->client->openFolder($this->folder_path);
451
        $this->flags = FlagCollection::make([]);
452
453
        $sequence_id = $this->getSequenceId();
454
        try {
455
            $flags = $this->client->getConnection()->flags([$sequence_id], $this->sequence === IMAP::ST_UID);
456
        } catch (Exceptions\RuntimeException $e) {
457
            throw new MessageFlagException("flag could not be fetched", 0, $e);
458
        }
459
460
        if (isset($flags[$sequence_id])) {
461
            $this->parseRawFlags($flags[$sequence_id]);
462
        }
463
    }
464
465
    /**
466
     * Parse the Message body
467
     *
468
     * @return $this
469
     * @throws Exceptions\ConnectionFailedException
470
     * @throws Exceptions\MessageContentFetchingException
471
     * @throws InvalidMessageDateException
472
     * @throws Exceptions\EventNotFoundException
473
     * @throws MessageFlagException
474
     * @throws Exceptions\RuntimeException
475
     */
476
    public function parseBody(): Message {
477
        $this->client->openFolder($this->folder_path);
478
479
        $sequence_id = $this->getSequenceId();
480
        try {
481
            $contents = $this->client->getConnection()->content([$sequence_id], "RFC822", $this->sequence === IMAP::ST_UID);
482
        } catch (Exceptions\RuntimeException $e) {
483
            throw new MessageContentFetchingException("failed to fetch content", 0);
484
        }
485
        if (!isset($contents[$sequence_id])) {
486
            throw new MessageContentFetchingException("no content found", 0);
487
        }
488
        $content = $contents[$sequence_id];
489
490
        $body = $this->parseRawBody($content);
491
        $this->peek();
492
493
        return $body;
494
    }
495
496
    /**
497
     * Handle auto "Seen" flag handling
498
     *
499
     * @throws Exceptions\ConnectionFailedException
500
     * @throws Exceptions\EventNotFoundException
501
     * @throws MessageFlagException
502
     * @throws Exceptions\RuntimeException
503
     */
504
    public function peek(){
505
        if ($this->fetch_options == IMAP::FT_PEEK) {
506
            if ($this->getFlags()->get("seen") == null) {
507
                $this->unsetFlag("Seen");
508
            }
509
        }elseif ($this->getFlags()->get("seen") != null) {
510
            $this->setFlag("Seen");
511
        }
512
    }
513
514
    /**
515
     * Parse a given message body
516
     * @param string $raw_body
517
     *
518
     * @return $this
519
     * @throws Exceptions\ConnectionFailedException
520
     * @throws InvalidMessageDateException
521
     * @throws MessageContentFetchingException
522
     * @throws Exceptions\RuntimeException
523
     */
524
    public function parseRawBody(string $raw_body): Message {
525
        $this->structure = new Structure($raw_body, $this->header);
526
        $this->fetchStructure($this->structure);
527
528
        return $this;
529
    }
530
531
    /**
532
     * Fetch the Message structure
533
     * @param Structure $structure
534
     *
535
     * @throws Exceptions\ConnectionFailedException
536
     * @throws Exceptions\RuntimeException
537
     */
538
    private function fetchStructure(Structure $structure) {
539
        $this->client->openFolder($this->folder_path);
540
541
        foreach ($structure->parts as $part) {
542
            $this->fetchPart($part);
543
        }
544
    }
545
546
    /**
547
     * Fetch a given part
548
     * @param Part $part
549
     */
550
    private function fetchPart(Part $part) {
551
        if ($part->isAttachment()) {
552
            $this->fetchAttachment($part);
553
        }else{
554
            $encoding = $this->getEncoding($part);
555
556
            $content = $this->decodeString($part->content, $part->encoding);
557
558
            // We don't need to do convertEncoding() if charset is ASCII (us-ascii):
559
            //     ASCII is a subset of UTF-8, so all ASCII files are already UTF-8 encoded
560
            //     https://stackoverflow.com/a/11303410
561
            //
562
            // us-ascii is the same as ASCII:
563
            //     ASCII is the traditional name for the encoding system; the Internet Assigned Numbers Authority (IANA)
564
            //     prefers the updated name US-ASCII, which clarifies that this system was developed in the US and
565
            //     based on the typographical symbols predominantly in use there.
566
            //     https://en.wikipedia.org/wiki/ASCII
567
            //
568
            // convertEncoding() function basically means convertToUtf8(), so when we convert ASCII string into UTF-8 it gets broken.
569
            if ($encoding != 'us-ascii') {
570
                $content = $this->convertEncoding($content, $encoding);
571
            }
572
573
            $subtype = strtolower($part->subtype ?? '');
574
            $subtype = $subtype == "plain" || $subtype == "" ? "text" : $subtype;
575
576
            if (isset($this->bodies[$subtype])) {
577
                $this->bodies[$subtype] .= "\n".$content;
0 ignored issues
show
Bug introduced by
Are you sure $content of type array|false|string can be used in concatenation? ( Ignorable by Annotation )

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

577
                $this->bodies[$subtype] .= "\n"./** @scrutinizer ignore-type */ $content;
Loading history...
578
            }else{
579
                $this->bodies[$subtype] = $content;
580
            }
581
        }
582
    }
583
584
    /**
585
     * Fetch the Message attachment
586
     * @param Part $part
587
     */
588
    protected function fetchAttachment(Part $part) {
589
        $oAttachment = new Attachment($this, $part);
590
591
        if ($oAttachment->getName() !== null && $oAttachment->getSize() > 0) {
592
            if ($oAttachment->getId() !== null) {
0 ignored issues
show
introduced by
The condition $oAttachment->getId() !== null is always true.
Loading history...
593
                $this->attachments->put($oAttachment->getId(), $oAttachment);
594
            } else {
595
                $this->attachments->push($oAttachment);
596
            }
597
        }
598
    }
599
600
    /**
601
     * Fail proof setter for $fetch_option
602
     * @param $option
603
     *
604
     * @return $this
605
     */
606
    public function setFetchOption($option): Message {
607
        if (is_long($option) === true) {
608
            $this->fetch_options = $option;
609
        } elseif (is_null($option) === true) {
610
            $config = ClientManager::get('options.fetch', IMAP::FT_UID);
611
            $this->fetch_options = is_long($config) ? $config : 1;
612
        }
613
614
        return $this;
615
    }
616
617
    /**
618
     * Set the sequence type
619
     * @param int|null $sequence
620
     *
621
     * @return $this
622
     */
623
    public function setSequence($sequence): Message {
624
        if (is_long($sequence)) {
625
            $this->sequence = $sequence;
626
        } elseif (is_null($sequence)) {
0 ignored issues
show
introduced by
The condition is_null($sequence) is always true.
Loading history...
627
            $config = ClientManager::get('options.sequence', IMAP::ST_MSGN);
628
            $this->sequence = is_long($config) ? $config : IMAP::ST_MSGN;
629
        }
630
631
        return $this;
632
    }
633
634
    /**
635
     * Fail proof setter for $fetch_body
636
     * @param $option
637
     *
638
     * @return $this
639
     */
640
    public function setFetchBodyOption($option): Message {
641
        if (is_bool($option)) {
642
            $this->fetch_body = $option;
643
        } elseif (is_null($option)) {
644
            $config = ClientManager::get('options.fetch_body', true);
645
            $this->fetch_body = is_bool($config) ? $config : true;
646
        }
647
648
        return $this;
649
    }
650
651
    /**
652
     * Fail proof setter for $fetch_flags
653
     * @param $option
654
     *
655
     * @return $this
656
     */
657
    public function setFetchFlagsOption($option): Message {
658
        if (is_bool($option)) {
659
            $this->fetch_flags = $option;
660
        } elseif (is_null($option)) {
661
            $config = ClientManager::get('options.fetch_flags', true);
662
            $this->fetch_flags = is_bool($config) ? $config : true;
663
        }
664
665
        return $this;
666
    }
667
668
    /**
669
     * Decode a given string
670
     * @param $string
671
     * @param $encoding
672
     *
673
     * @return string
674
     */
675
    public function decodeString($string, $encoding): string {
676
        switch ($encoding) {
677
            case IMAP::MESSAGE_ENC_BINARY:
678
                if (extension_loaded('imap')) {
679
                    return base64_decode(\imap_binary($string));
680
                }
681
                return base64_decode($string);
682
            case IMAP::MESSAGE_ENC_BASE64:
683
                return base64_decode($string);
684
            case IMAP::MESSAGE_ENC_QUOTED_PRINTABLE:
685
                return quoted_printable_decode($string);
686
            case IMAP::MESSAGE_ENC_8BIT:
687
            case IMAP::MESSAGE_ENC_7BIT:
688
            case IMAP::MESSAGE_ENC_OTHER:
689
            default:
690
                return $string;
691
        }
692
    }
693
694
    /**
695
     * Convert the encoding
696
     * @param $str
697
     * @param string $from
698
     * @param string $to
699
     *
700
     * @return mixed|string
701
     */
702
    public function convertEncoding($str, string $from = "ISO-8859-2", string $to = "UTF-8") {
703
704
        $from = EncodingAliases::get($from);
705
        $to = EncodingAliases::get($to);
706
707
        if ($from === $to) {
708
            return $str;
709
        }
710
711
        // We don't need to do convertEncoding() if charset is ASCII (us-ascii):
712
        //     ASCII is a subset of UTF-8, so all ASCII files are already UTF-8 encoded
713
        //     https://stackoverflow.com/a/11303410
714
        //
715
        // us-ascii is the same as ASCII:
716
        //     ASCII is the traditional name for the encoding system; the Internet Assigned Numbers Authority (IANA)
717
        //     prefers the updated name US-ASCII, which clarifies that this system was developed in the US and
718
        //     based on the typographical symbols predominantly in use there.
719
        //     https://en.wikipedia.org/wiki/ASCII
720
        //
721
        // convertEncoding() function basically means convertToUtf8(), so when we convert ASCII string into UTF-8 it gets broken.
722
        if (strtolower($from ?? '') == 'us-ascii' && $to == 'UTF-8') {
723
            return $str;
724
        }
725
726
        if (function_exists('iconv') && $from != 'UTF-7' && $to != 'UTF-7') {
727
            return @iconv($from, $to.'//IGNORE', $str);
728
        } else {
729
            if (!$from) {
730
                return mb_convert_encoding($str, $to);
731
            }
732
            return mb_convert_encoding($str, $to, $from);
733
        }
734
    }
735
736
    /**
737
     * Get the encoding of a given abject
738
     * @param string|object $structure
739
     *
740
     * @return string|null
741
     */
742
    public function getEncoding($structure): string {
743
        if (property_exists($structure, 'parameters')) {
744
            foreach ($structure->parameters as $parameter) {
745
                if (strtolower($parameter->attribute) == "charset") {
746
                    return EncodingAliases::get($parameter->value, "ISO-8859-2");
747
                }
748
            }
749
        }elseif (property_exists($structure, 'charset')){
750
            return EncodingAliases::get($structure->charset, "ISO-8859-2");
751
        }elseif (is_string($structure) === true){
752
            return mb_detect_encoding($structure);
753
        }
754
755
        return 'UTF-8';
756
    }
757
758
    /**
759
     * Get the messages folder
760
     *
761
     * @return mixed
762
     * @throws Exceptions\ConnectionFailedException
763
     * @throws Exceptions\FolderFetchingException
764
     * @throws Exceptions\RuntimeException
765
     */
766
    public function getFolder(){
767
        return $this->client->getFolderByPath($this->folder_path);
768
    }
769
770
    /**
771
     * Create a message thread based on the current message
772
     * @param Folder|null $sent_folder
773
     * @param MessageCollection|null $thread
774
     * @param Folder|null $folder
775
     *
776
     * @return MessageCollection|null
777
     * @throws Exceptions\ConnectionFailedException
778
     * @throws Exceptions\FolderFetchingException
779
     * @throws Exceptions\GetMessagesFailedException
780
     * @throws Exceptions\RuntimeException
781
     */
782
    public function thread(Folder $sent_folder = null, MessageCollection &$thread = null, Folder $folder = null): MessageCollection {
783
        $thread = $thread ?: MessageCollection::make([]);
784
        $folder = $folder ?:  $this->getFolder();
785
        $sent_folder = $sent_folder ?: $this->client->getFolderByPath(ClientManager::get("options.common_folders.sent", "INBOX/Sent"));
786
787
        /** @var Message $message */
788
        foreach($thread as $message) {
789
            if ($message->message_id->first() == $this->message_id->first()) {
790
                return $thread;
791
            }
792
        }
793
        $thread->push($this);
794
795
        $this->fetchThreadByInReplyTo($thread, $this->message_id, $folder, $folder, $sent_folder);
0 ignored issues
show
Bug introduced by
It seems like $sent_folder can also be of type null; however, parameter $sent_folder of Webklex\PHPIMAP\Message::fetchThreadByInReplyTo() does only seem to accept Webklex\PHPIMAP\Folder, 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

795
        $this->fetchThreadByInReplyTo($thread, $this->message_id, $folder, $folder, /** @scrutinizer ignore-type */ $sent_folder);
Loading history...
Bug introduced by
It seems like $folder can also be of type null; however, parameter $primary_folder of Webklex\PHPIMAP\Message::fetchThreadByInReplyTo() does only seem to accept Webklex\PHPIMAP\Folder, 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

795
        $this->fetchThreadByInReplyTo($thread, $this->message_id, /** @scrutinizer ignore-type */ $folder, $folder, $sent_folder);
Loading history...
Bug introduced by
It seems like $folder can also be of type null; however, parameter $secondary_folder of Webklex\PHPIMAP\Message::fetchThreadByInReplyTo() does only seem to accept Webklex\PHPIMAP\Folder, 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

795
        $this->fetchThreadByInReplyTo($thread, $this->message_id, $folder, /** @scrutinizer ignore-type */ $folder, $sent_folder);
Loading history...
796
        $this->fetchThreadByInReplyTo($thread, $this->message_id, $sent_folder, $folder, $sent_folder);
797
798
        if (is_array($this->in_reply_to)) {
0 ignored issues
show
introduced by
The condition is_array($this->in_reply_to) is always false.
Loading history...
799
            foreach($this->in_reply_to as $in_reply_to) {
800
                $this->fetchThreadByMessageId($thread, $in_reply_to, $folder, $folder, $sent_folder);
801
                $this->fetchThreadByMessageId($thread, $in_reply_to, $sent_folder, $folder, $sent_folder);
802
            }
803
        }
804
805
        return $thread;
806
    }
807
808
    /**
809
     * Fetch a partial thread by message id
810
     * @param MessageCollection $thread
811
     * @param string $in_reply_to
812
     * @param Folder $primary_folder
813
     * @param Folder $secondary_folder
814
     * @param Folder $sent_folder
815
     *
816
     * @throws Exceptions\ConnectionFailedException
817
     * @throws Exceptions\GetMessagesFailedException
818
     * @throws Exceptions\RuntimeException
819
     * @throws Exceptions\FolderFetchingException
820
     */
821
    protected function fetchThreadByInReplyTo(MessageCollection &$thread, string $in_reply_to, Folder $primary_folder, Folder $secondary_folder, Folder $sent_folder){
822
        $primary_folder->query()->inReplyTo($in_reply_to)
823
        ->setFetchBody($this->getFetchBodyOption())
824
        ->leaveUnread()->get()->each(function($message) use(&$thread, $secondary_folder, $sent_folder){
825
            /** @var Message $message */
826
            $message->thread($sent_folder, $thread, $secondary_folder);
827
        });
828
    }
829
830
    /**
831
     * Fetch a partial thread by message id
832
     * @param MessageCollection $thread
833
     * @param string $message_id
834
     * @param Folder $primary_folder
835
     * @param Folder $secondary_folder
836
     * @param Folder $sent_folder
837
     *
838
     * @throws Exceptions\ConnectionFailedException
839
     * @throws Exceptions\GetMessagesFailedException
840
     * @throws Exceptions\RuntimeException
841
     * @throws Exceptions\FolderFetchingException
842
     */
843
    protected function fetchThreadByMessageId(MessageCollection &$thread, string $message_id, Folder $primary_folder, Folder $secondary_folder, Folder $sent_folder){
844
        $primary_folder->query()->messageId($message_id)
845
        ->setFetchBody($this->getFetchBodyOption())
846
        ->leaveUnread()->get()->each(function($message) use(&$thread, $secondary_folder, $sent_folder){
847
            /** @var Message $message */
848
            $message->thread($sent_folder, $thread, $secondary_folder);
849
        });
850
    }
851
852
    /**
853
     * Copy the current Messages to a mailbox
854
     * @param string $folder_path
855
     * @param boolean $expunge
856
     *
857
     * @return null|Message
858
     * @throws Exceptions\ConnectionFailedException
859
     * @throws Exceptions\FolderFetchingException
860
     * @throws Exceptions\RuntimeException
861
     * @throws InvalidMessageDateException
862
     * @throws MessageContentFetchingException
863
     * @throws MessageHeaderFetchingException
864
     * @throws Exceptions\EventNotFoundException
865
     * @throws MessageFlagException
866
     * @throws Exceptions\MessageNotFoundException
867
     */
868
    public function copy(string $folder_path, bool $expunge = false) {
869
        $this->client->openFolder($folder_path);
870
        $status = $this->client->getConnection()->examineFolder($folder_path);
871
872
        if (isset($status["uidnext"])) {
873
            $next_uid = $status["uidnext"];
874
875
            /** @var Folder $folder */
876
            $folder = $this->client->getFolderByPath($folder_path);
877
878
            $this->client->openFolder($this->folder_path);
879
            if ($this->client->getConnection()->copyMessage($folder->path, $this->getSequenceId(), null, $this->sequence === IMAP::ST_UID) == true) {
880
                return $this->fetchNewMail($folder, $next_uid, "copied", $expunge);
881
            }
882
        }
883
884
        return null;
885
    }
886
887
    /**
888
     * Move the current Messages to a mailbox
889
     * @param string $folder_path
890
     * @param boolean $expunge
891
     *
892
     * @return Message|null
893
     * @throws Exceptions\ConnectionFailedException
894
     * @throws Exceptions\FolderFetchingException
895
     * @throws Exceptions\RuntimeException
896
     * @throws InvalidMessageDateException
897
     * @throws MessageContentFetchingException
898
     * @throws MessageHeaderFetchingException
899
     * @throws Exceptions\EventNotFoundException
900
     * @throws MessageFlagException
901
     * @throws Exceptions\MessageNotFoundException
902
     */
903
    public function move(string $folder_path, bool $expunge = false) {
904
        $this->client->openFolder($folder_path);
905
        $status = $this->client->getConnection()->examineFolder($folder_path);
906
907
        if (isset($status["uidnext"])) {
908
            $next_uid = $status["uidnext"];
909
910
            /** @var Folder $folder */
911
            $folder = $this->client->getFolderByPath($folder_path);
912
913
            $this->client->openFolder($this->folder_path);
914
            if ($this->client->getConnection()->moveMessage($folder->path, $this->getSequenceId(), null, $this->sequence === IMAP::ST_UID) == true) {
915
                return $this->fetchNewMail($folder, $next_uid, "moved", $expunge);
916
            }
917
        }
918
919
        return null;
920
    }
921
922
    /**
923
     * Fetch a new message and fire a given event
924
     * @param Folder $folder
925
     * @param int $next_uid
926
     * @param string $event
927
     * @param boolean $expunge
928
     *
929
     * @return Message
930
     * @throws Exceptions\ConnectionFailedException
931
     * @throws Exceptions\EventNotFoundException
932
     * @throws Exceptions\MessageNotFoundException
933
     * @throws Exceptions\RuntimeException
934
     * @throws InvalidMessageDateException
935
     * @throws MessageContentFetchingException
936
     * @throws MessageFlagException
937
     * @throws MessageHeaderFetchingException
938
     */
939
    protected function fetchNewMail(Folder $folder, int $next_uid, string $event, bool $expunge): Message {
940
        if($expunge) $this->client->expunge();
941
942
        $this->client->openFolder($folder->path);
943
944
        if ($this->sequence === IMAP::ST_UID) {
945
            $sequence_id = $next_uid;
946
        }else{
947
            $sequence_id = $this->client->getConnection()->getMessageNumber($next_uid);
948
        }
949
950
        $message = $folder->query()->getMessage($sequence_id, null, $this->sequence);
951
        $event = $this->getEvent("message", $event);
952
        $event::dispatch($this, $message);
953
954
        return $message;
955
    }
956
957
    /**
958
     * Delete the current Message
959
     * @param bool $expunge
960
     * @param string|null $trash_path
961
     * @param boolean $force_move
962
     *
963
     * @return bool
964
     * @throws Exceptions\ConnectionFailedException
965
     * @throws Exceptions\EventNotFoundException
966
     * @throws Exceptions\FolderFetchingException
967
     * @throws Exceptions\MessageNotFoundException
968
     * @throws Exceptions\RuntimeException
969
     * @throws InvalidMessageDateException
970
     * @throws MessageContentFetchingException
971
     * @throws MessageFlagException
972
     * @throws MessageHeaderFetchingException
973
     */
974
    public function delete(bool $expunge = true, string $trash_path = null, bool $force_move = false) {
975
        $status = $this->setFlag("Deleted");
976
        if($force_move) {
977
            $trash_path = $trash_path === null ? $this->config["common_folders"]["trash"]: $trash_path;
978
            $status = $this->move($trash_path);
979
        }
980
        if($expunge) $this->client->expunge();
981
982
        $event = $this->getEvent("message", "deleted");
983
        $event::dispatch($this);
984
985
        return $status;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $status also could return the type Webklex\PHPIMAP\Message which is incompatible with the documented return type boolean.
Loading history...
986
    }
987
988
    /**
989
     * Restore a deleted Message
990
     * @param boolean $expunge
991
     *
992
     * @return bool
993
     * @throws Exceptions\ConnectionFailedException
994
     * @throws Exceptions\EventNotFoundException
995
     * @throws MessageFlagException
996
     * @throws Exceptions\RuntimeException
997
     */
998
    public function restore(bool $expunge = true): bool {
999
        $status = $this->unsetFlag("Deleted");
1000
        if($expunge) $this->client->expunge();
1001
1002
        $event = $this->getEvent("message", "restored");
1003
        $event::dispatch($this);
1004
1005
        return $status;
1006
    }
1007
1008
    /**
1009
     * Set a given flag
1010
     * @param string|array $flag
1011
     *
1012
     * @return bool
1013
     * @throws Exceptions\ConnectionFailedException
1014
     * @throws MessageFlagException
1015
     * @throws Exceptions\EventNotFoundException
1016
     * @throws Exceptions\RuntimeException
1017
     */
1018
    public function setFlag($flag): bool {
1019
        $this->client->openFolder($this->folder_path);
1020
        $flag = "\\".trim(is_array($flag) ? implode(" \\", $flag) : $flag);
1021
        $sequence_id = $this->getSequenceId();
1022
        try {
1023
            $status = $this->client->getConnection()->store([$flag], $sequence_id, $sequence_id, "+", true, $this->sequence === IMAP::ST_UID);
1024
        } catch (Exceptions\RuntimeException $e) {
1025
            throw new MessageFlagException("flag could not be set", 0, $e);
1026
        }
1027
        $this->parseFlags();
1028
1029
        $event = $this->getEvent("flag", "new");
1030
        $event::dispatch($this, $flag);
1031
1032
        return (bool)$status;
1033
    }
1034
1035
    /**
1036
     * Unset a given flag
1037
     * @param string|array $flag
1038
     *
1039
     * @return bool
1040
     * @throws Exceptions\ConnectionFailedException
1041
     * @throws Exceptions\EventNotFoundException
1042
     * @throws MessageFlagException
1043
     * @throws Exceptions\RuntimeException
1044
     */
1045
    public function unsetFlag($flag): bool {
1046
        $this->client->openFolder($this->folder_path);
1047
1048
        $flag = "\\".trim(is_array($flag) ? implode(" \\", $flag) : $flag);
1049
        $sequence_id = $this->getSequenceId();
1050
        try {
1051
            $status = $this->client->getConnection()->store([$flag], $sequence_id, $sequence_id, "-", true, $this->sequence === IMAP::ST_UID);
1052
        } catch (Exceptions\RuntimeException $e) {
1053
            throw new MessageFlagException("flag could not be removed", 0, $e);
1054
        }
1055
        $this->parseFlags();
1056
1057
        $event = $this->getEvent("flag", "deleted");
1058
        $event::dispatch($this, $flag);
1059
1060
        return (bool)$status;
1061
    }
1062
1063
    /**
1064
     * Set a given flag
1065
     * @param string|array $flag
1066
     *
1067
     * @return bool
1068
     * @throws Exceptions\ConnectionFailedException
1069
     * @throws MessageFlagException
1070
     * @throws Exceptions\EventNotFoundException
1071
     * @throws Exceptions\RuntimeException
1072
     */
1073
    public function addFlag($flag): bool {
1074
        return $this->setFlag($flag);
1075
    }
1076
1077
    /**
1078
     * Unset a given flag
1079
     * @param string|array $flag
1080
     *
1081
     * @return bool
1082
     * @throws Exceptions\ConnectionFailedException
1083
     * @throws Exceptions\EventNotFoundException
1084
     * @throws MessageFlagException
1085
     * @throws Exceptions\RuntimeException
1086
     */
1087
    public function removeFlag($flag): bool {
1088
        return $this->unsetFlag($flag);
1089
    }
1090
1091
    /**
1092
     * Get all message attachments.
1093
     *
1094
     * @return AttachmentCollection
1095
     */
1096
    public function getAttachments(): AttachmentCollection {
1097
        return $this->attachments;
1098
    }
1099
1100
    /**
1101
     * Get all message attachments.
1102
     *
1103
     * @return AttachmentCollection
1104
     */
1105
    public function attachments(): AttachmentCollection {
1106
        return $this->getAttachments();
1107
    }
1108
1109
    /**
1110
     * Checks if there are any attachments present
1111
     *
1112
     * @return boolean
1113
     */
1114
    public function hasAttachments(): bool {
1115
        return $this->attachments->isEmpty() === false;
1116
    }
1117
1118
    /**
1119
     * Get the raw body
1120
     *
1121
     * @return string
1122
     * @throws Exceptions\ConnectionFailedException
1123
     * @throws Exceptions\RuntimeException
1124
     */
1125
    public function getRawBody() {
1126
        if ($this->raw_body === null) {
1127
            $this->client->openFolder($this->folder_path);
1128
1129
            $this->raw_body = $this->structure->raw;
1130
        }
1131
1132
        return $this->raw_body;
1133
    }
1134
1135
    /**
1136
     * Get the message header
1137
     *
1138
     * @return Header
1139
     */
1140
    public function getHeader() {
1141
        return $this->header;
1142
    }
1143
1144
    /**
1145
     * Get the current client
1146
     *
1147
     * @return Client
1148
     */
1149
    public function getClient(): Client {
1150
        return $this->client;
1151
    }
1152
1153
    /**
1154
     * Get the used fetch option
1155
     *
1156
     * @return integer
1157
     */
1158
    public function getFetchOptions() {
1159
        return $this->fetch_options;
1160
    }
1161
1162
    /**
1163
     * Get the used fetch body option
1164
     *
1165
     * @return boolean
1166
     */
1167
    public function getFetchBodyOption() {
1168
        return $this->fetch_body;
1169
    }
1170
1171
    /**
1172
     * Get the used fetch flags option
1173
     *
1174
     * @return boolean
1175
     */
1176
    public function getFetchFlagsOption() {
1177
        return $this->fetch_flags;
1178
    }
1179
1180
    /**
1181
     * Get all available bodies
1182
     *
1183
     * @return array
1184
     */
1185
    public function getBodies(): array {
1186
        return $this->bodies;
1187
    }
1188
1189
    /**
1190
     * Get all set flags
1191
     *
1192
     * @return FlagCollection
1193
     */
1194
    public function getFlags(): FlagCollection {
1195
        return $this->flags;
1196
    }
1197
1198
    /**
1199
     * Get all set flags
1200
     *
1201
     * @return FlagCollection
1202
     */
1203
    public function flags(): FlagCollection {
1204
        return $this->getFlags();
1205
    }
1206
1207
    /**
1208
     * Get the fetched structure
1209
     *
1210
     * @return Structure|null
1211
     */
1212
    public function getStructure(){
1213
        return $this->structure;
1214
    }
1215
1216
    /**
1217
     * Check if a message matches an other by comparing basic attributes
1218
     *
1219
     * @param  null|Message $message
1220
     * @return boolean
1221
     */
1222
    public function is(Message $message = null): bool {
1223
        if (is_null($message)) {
1224
            return false;
1225
        }
1226
1227
        return $this->uid == $message->uid
1228
            && $this->message_id->first() == $message->message_id->first()
1229
            && $this->subject->first() == $message->subject->first()
1230
            && $this->date->toDate()->eq($message->date);
1231
    }
1232
1233
    /**
1234
     * Get all message attributes
1235
     *
1236
     * @return array
1237
     */
1238
    public function getAttributes(): array {
1239
        return array_merge($this->attributes, $this->header->getAttributes());
1240
    }
1241
1242
    /**
1243
     * Set the message mask
1244
     * @param $mask
1245
     *
1246
     * @return $this
1247
     */
1248
    public function setMask($mask): Message {
1249
        if(class_exists($mask)){
1250
            $this->mask = $mask;
1251
        }
1252
1253
        return $this;
1254
    }
1255
1256
    /**
1257
     * Get the used message mask
1258
     *
1259
     * @return string
1260
     */
1261
    public function getMask(): string {
1262
        return $this->mask;
1263
    }
1264
1265
    /**
1266
     * Get a masked instance by providing a mask name
1267
     * @param string|mixed $mask
1268
     *
1269
     * @return mixed
1270
     * @throws MaskNotFoundException
1271
     */
1272
    public function mask($mask = null){
1273
        $mask = $mask !== null ? $mask : $this->mask;
1274
        if(class_exists($mask)){
1275
            return new $mask($this);
1276
        }
1277
1278
        throw new MaskNotFoundException("Unknown mask provided: ".$mask);
1279
    }
1280
1281
    /**
1282
     * Get the message path aka folder path
1283
     *
1284
     * @return string
1285
     */
1286
    public function getFolderPath(): string {
1287
        return $this->folder_path;
1288
    }
1289
1290
    /**
1291
     * Set the message path aka folder path
1292
     * @param $folder_path
1293
     *
1294
     * @return $this
1295
     */
1296
    public function setFolderPath($folder_path): Message {
1297
        $this->folder_path = $folder_path;
1298
1299
        return $this;
1300
    }
1301
1302
    /**
1303
     * Set the config
1304
     * @param $config
1305
     *
1306
     * @return $this
1307
     */
1308
    public function setConfig($config): Message {
1309
        $this->config = $config;
1310
1311
        return $this;
1312
    }
1313
1314
    /**
1315
     * Set the available flags
1316
     * @param $available_flags
1317
     *
1318
     * @return $this
1319
     */
1320
    public function setAvailableFlags($available_flags): Message {
1321
        $this->available_flags = $available_flags;
1322
1323
        return $this;
1324
    }
1325
1326
    /**
1327
     * Set the attachment collection
1328
     * @param $attachments
1329
     *
1330
     * @return $this
1331
     */
1332
    public function setAttachments($attachments): Message {
1333
        $this->attachments = $attachments;
1334
1335
        return $this;
1336
    }
1337
1338
    /**
1339
     * Set the flag collection
1340
     * @param $flags
1341
     *
1342
     * @return $this
1343
     */
1344
    public function setFlags($flags): Message {
1345
        $this->flags = $flags;
1346
1347
        return $this;
1348
    }
1349
1350
    /**
1351
     * Set the client
1352
     * @param $client
1353
     *
1354
     * @return $this
1355
     * @throws Exceptions\RuntimeException
1356
     * @throws Exceptions\ConnectionFailedException
1357
     */
1358
    public function setClient($client): Message {
1359
        $this->client = $client;
1360
        $this->client->openFolder($this->folder_path);
1361
1362
        return $this;
1363
    }
1364
1365
    /**
1366
     * Set the message number
1367
     * @param int $uid
1368
     *
1369
     * @return $this
1370
     * @throws Exceptions\MessageNotFoundException
1371
     * @throws Exceptions\ConnectionFailedException
1372
     */
1373
    public function setUid(int $uid): Message {
1374
        $this->uid = $uid;
1375
        $this->msgn = $this->client->getConnection()->getMessageNumber($this->uid);
1376
        $this->msglist = null;
1377
1378
        return $this;
1379
    }
1380
1381
    /**
1382
     * Set the message number
1383
     * @param int $msgn
1384
     * @param int|null $msglist
1385
     *
1386
     * @return $this
1387
     * @throws Exceptions\MessageNotFoundException
1388
     * @throws Exceptions\ConnectionFailedException
1389
     */
1390
    public function setMsgn(int $msgn, int $msglist = null): Message {
1391
        $this->msgn = $msgn;
1392
        $this->msglist = $msglist;
1393
        $this->uid = $this->client->getConnection()->getUid($this->msgn);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->client->getConnec...()->getUid($this->msgn) of type string or array is incompatible with the declared type integer of property $uid.

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...
1394
1395
        return $this;
1396
    }
1397
1398
    /**
1399
     * Get the current sequence type
1400
     *
1401
     * @return int
1402
     */
1403
    public function getSequence(): int {
1404
        return $this->sequence;
1405
    }
1406
1407
    /**
1408
     * Set the sequence type
1409
     *
1410
     * @return int
1411
     */
1412
    public function getSequenceId(): int {
1413
        return $this->sequence === IMAP::ST_UID ? $this->uid : $this->msgn;
1414
    }
1415
1416
    /**
1417
     * Set the sequence id
1418
     * @param $uid
1419
     * @param int|null $msglist
1420
     *
1421
     * @throws Exceptions\ConnectionFailedException
1422
     * @throws Exceptions\MessageNotFoundException
1423
     */
1424
    public function setSequenceId($uid, int $msglist = null){
1425
        if ($this->getSequence() === IMAP::ST_UID) {
1426
            $this->setUid($uid);
1427
            $this->setMsglist($msglist);
1428
        }else{
1429
            $this->setMsgn($uid, $msglist);
1430
        }
1431
    }
1432
}
1433