Passed
Push — master ( deab0e...aa4b53 )
by Malte
01:57
created

Message::thread()   B

Complexity

Conditions 8
Paths 40

Size

Total Lines 44
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 26
c 1
b 0
f 1
dl 0
loc 44
rs 8.4444
cc 8
nc 40
nop 3
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 Carbon\Carbon;
16
use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException;
17
use Webklex\PHPIMAP\Exceptions\MaskNotFoundException;
18
use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException;
19
use Webklex\PHPIMAP\Exceptions\MessageHeaderFetchingException;
20
use Webklex\PHPIMAP\Exceptions\MethodNotFoundException;
21
use Webklex\PHPIMAP\Support\AttachmentCollection;
22
use Webklex\PHPIMAP\Support\FlagCollection;
23
use Webklex\PHPIMAP\Support\Masks\MessageMask;
24
use Illuminate\Support\Str;
25
use Webklex\PHPIMAP\Support\MessageCollection;
26
use Webklex\PHPIMAP\Traits\HasEvents;
27
28
/**
29
 * Class Message
30
 *
31
 * @package Webklex\PHPIMAP
32
 *
33
 * @property integer msglist
34
 * @property integer uid
35
 * @property integer msgn
36
 * @property string subject
37
 * @property string message_id
38
 * @property string message_no
39
 * @property string references
40
 * @property carbon date
41
 * @property array from
42
 * @property array to
43
 * @property array cc
44
 * @property array bcc
45
 * @property array reply_to
46
 * @property array in_reply_to
47
 * @property array sender
48
 *
49
 * @method integer getMsglist()
50
 * @method integer setMsglist(integer $msglist)
51
 * @method integer getUid()
52
 * @method integer setUid(integer $uid)
53
 * @method integer getMsgn()
54
 * @method integer setMsgn(integer $msgn)
55
 * @method integer getPriority()
56
 * @method integer setPriority(integer $priority)
57
 * @method string getSubject()
58
 * @method string setSubject(string $subject)
59
 * @method string getMessageId()
60
 * @method string setMessageId(string $message_id)
61
 * @method string getMessageNo()
62
 * @method string setMessageNo(string $message_no)
63
 * @method string getReferences()
64
 * @method string setReferences(string $references)
65
 * @method carbon getDate()
66
 * @method carbon setDate(carbon $date)
67
 * @method array getFrom()
68
 * @method array setFrom(array $from)
69
 * @method array getTo()
70
 * @method array setTo(array $to)
71
 * @method array getCc()
72
 * @method array setCc(array $cc)
73
 * @method array getBcc()
74
 * @method array setBcc(array $bcc)
75
 * @method array getReplyTo()
76
 * @method array setReplyTo(array $reply_to)
77
 * @method array getInReplyTo()
78
 * @method array setInReplyTo(array $in_reply_to)
79
 * @method array getSender()
80
 * @method array setSender(array $sender)
81
 */
82
class Message {
83
    use HasEvents;
84
85
    /**
86
     * Client instance
87
     *
88
     * @var Client
89
     */
90
    private $client = Client::class;
91
92
    /**
93
     * Default mask
94
     *
95
     * @var string $mask
96
     */
97
    protected $mask = MessageMask::class;
98
99
    /**
100
     * Used config
101
     *
102
     * @var array $config
103
     */
104
    protected $config = [];
105
106
    /**
107
     * Attribute holder
108
     *
109
     * @var array $attributes
110
     */
111
    protected $attributes = [
112
        'message_no' => null,
113
    ];
114
115
    /**
116
     * The message folder path
117
     *
118
     * @var string $folder_path
119
     */
120
    protected $folder_path;
121
122
    /**
123
     * Fetch body options
124
     *
125
     * @var integer
126
     */
127
    public $fetch_options = null;
128
129
    /**
130
     * Fetch body options
131
     *
132
     * @var bool
133
     */
134
    public $fetch_body = null;
135
136
    /**
137
     * Fetch flags options
138
     *
139
     * @var bool
140
     */
141
    public $fetch_flags = null;
142
143
    /**
144
     * @var Header $header
145
     */
146
    public $header = null;
147
148
    /**
149
     * Raw message body
150
     *
151
     * @var null|string $raw_body
152
     */
153
    public $raw_body = null;
154
155
    /**
156
     * Message structure
157
     *
158
     * @var Structure $structure
159
     */
160
    protected $structure = null;
161
162
    /**
163
     * Message body components
164
     *
165
     * @var array   $bodies
166
     * @var AttachmentCollection|array $attachments
167
     * @var FlagCollection|array       $flags
168
     */
169
    public $bodies = [];
170
    public $attachments = [];
171
    public $flags = [];
172
173
    /**
174
     * A list of all available and supported flags
175
     *
176
     * @var array $available_flags
177
     */
178
    private $available_flags = ['recent', 'flagged', 'answered', 'deleted', 'seen', 'draft'];
179
180
    /**
181
     * Message constructor.
182
     * @param integer $msgn
183
     * @param integer|null $msglist
184
     * @param Client $client
185
     * @param integer|null $fetch_options
186
     * @param boolean $fetch_body
187
     * @param boolean $fetch_flags
188
     *
189
     * @throws Exceptions\ConnectionFailedException
190
     * @throws InvalidMessageDateException
191
     * @throws Exceptions\RuntimeException
192
     * @throws MessageHeaderFetchingException
193
     * @throws MessageContentFetchingException
194
     * @throws Exceptions\EventNotFoundException
195
     */
196
    public function __construct($msgn, $msglist, Client $client, $fetch_options = null, $fetch_body = false, $fetch_flags = false) {
197
198
        $default_mask = $client->getDefaultMessageMask();
199
        if($default_mask != null) {
200
            $this->mask = $default_mask;
201
        }
202
        $this->events["message"] = $client->getDefaultEvents("message");
203
        $this->events["flag"] = $client->getDefaultEvents("flag");
204
205
        $this->folder_path = $client->getFolderPath();
0 ignored issues
show
Documentation Bug introduced by
It seems like $client->getFolderPath() of type Webklex\PHPIMAP\Folder is incompatible with the declared type string of property $folder_path.

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...
206
207
        $this->config = ClientManager::get('options');
208
209
        $this->setFetchOption($fetch_options);
210
        $this->setFetchBodyOption($fetch_body);
211
        $this->setFetchFlagsOption($fetch_flags);
212
213
        $this->attachments = AttachmentCollection::make([]);
214
        $this->flags = FlagCollection::make([]);
215
216
        $this->client = $client;
217
        $this->client->openFolder($this->folder_path);
0 ignored issues
show
Bug introduced by
$this->folder_path of type Webklex\PHPIMAP\Folder is incompatible with the type string expected by parameter $folder of Webklex\PHPIMAP\Client::openFolder(). ( Ignorable by Annotation )

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

217
        $this->client->openFolder(/** @scrutinizer ignore-type */ $this->folder_path);
Loading history...
218
219
        $this->msgn = $msgn;
220
        $this->msglist = $msglist;
221
222
        $this->uid = $this->client->getConnection()->getUid($this->msgn);
0 ignored issues
show
Bug introduced by
The method getUid() does not exist on Webklex\PHPIMAP\Connection\Protocols\Protocol. Since it exists in all sub-types, consider adding an abstract or default implementation to Webklex\PHPIMAP\Connection\Protocols\Protocol. ( Ignorable by Annotation )

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

222
        $this->uid = $this->client->getConnection()->/** @scrutinizer ignore-call */ getUid($this->msgn);
Loading history...
223
224
        $this->parseHeader();
225
226
        if ($this->getFetchBodyOption() === true) {
227
            $this->parseBody();
228
        }
229
230
        if ($this->getFetchFlagsOption() === true && $this->flags->count() == 0) {
231
            $this->parseFlags();
232
        }
233
    }
234
235
    /**
236
     * Call dynamic attribute setter and getter methods
237
     * @param string $method
238
     * @param array $arguments
239
     *
240
     * @return mixed
241
     * @throws MethodNotFoundException
242
     */
243
    public function __call($method, $arguments) {
244
        if(strtolower(substr($method, 0, 3)) === 'get') {
245
            $name = Str::snake(substr($method, 3));
246
            return $this->get($name);
247
        }elseif (strtolower(substr($method, 0, 3)) === 'set') {
248
            $name = Str::snake(substr($method, 3));
249
250
            if(in_array($name, array_keys($this->attributes))) {
251
                $this->attributes[$name] = array_pop($arguments);
252
253
                return $this->attributes[$name];
254
            }
255
256
        }
257
258
        throw new MethodNotFoundException("Method ".self::class.'::'.$method.'() is not supported');
259
    }
260
261
    /**
262
     * Magic setter
263
     * @param $name
264
     * @param $value
265
     *
266
     * @return mixed
267
     */
268
    public function __set($name, $value) {
269
        $this->attributes[$name] = $value;
270
271
        return $this->attributes[$name];
272
    }
273
274
    /**
275
     * Magic getter
276
     * @param $name
277
     *
278
     * @return mixed|null
279
     */
280
    public function __get($name) {
281
        return $this->get($name);
282
    }
283
284
    /**
285
     * Get an available message or message header attribute
286
     * @param $name
287
     *
288
     * @return mixed|null
289
     */
290
    public function get($name) {
291
        if(isset($this->attributes[$name])) {
292
            return $this->attributes[$name];
293
        }
294
295
        return $this->header->get($name);
296
    }
297
298
    /**
299
     * Check if the Message has a text body
300
     *
301
     * @return bool
302
     */
303
    public function hasTextBody() {
304
        return isset($this->bodies['text']);
305
    }
306
307
    /**
308
     * Get the Message text body
309
     *
310
     * @return mixed
311
     */
312
    public function getTextBody() {
313
        if (!isset($this->bodies['text'])) {
314
            return false;
315
        }
316
317
        return $this->bodies['text'];
318
    }
319
320
    /**
321
     * Check if the Message has a html body
322
     *
323
     * @return bool
324
     */
325
    public function hasHTMLBody() {
326
        return isset($this->bodies['html']);
327
    }
328
329
    /**
330
     * Get the Message html body
331
     *
332
     * @return string|null
333
     */
334
    public function getHTMLBody() {
335
        if (!isset($this->bodies['html'])) {
336
            return null;
337
        }
338
339
        return $this->bodies['html'];
340
    }
341
342
    /**
343
     * Parse all defined headers
344
     *
345
     * @throws Exceptions\ConnectionFailedException
346
     * @throws Exceptions\RuntimeException
347
     * @throws InvalidMessageDateException
348
     * @throws MessageHeaderFetchingException
349
     */
350
    private function parseHeader() {
351
        $headers = $this->client->getConnection()->headers([$this->msgn]);
0 ignored issues
show
Bug introduced by
The method headers() does not exist on Webklex\PHPIMAP\Connection\Protocols\Protocol. Since it exists in all sub-types, consider adding an abstract or default implementation to Webklex\PHPIMAP\Connection\Protocols\Protocol. ( Ignorable by Annotation )

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

351
        $headers = $this->client->getConnection()->/** @scrutinizer ignore-call */ headers([$this->msgn]);
Loading history...
352
        if (!isset($headers[$this->msgn])) {
353
            throw new MessageHeaderFetchingException("no headers found", 0);
354
        }
355
356
        $this->header = new Header($headers[$this->msgn]);
357
    }
358
359
    /**
360
     * Parse additional flags
361
     *
362
     * @return void
363
     * @throws Exceptions\ConnectionFailedException
364
     * @throws Exceptions\RuntimeException
365
     */
366
    private function parseFlags() {
367
        $this->client->openFolder($this->folder_path);
368
        $this->flags = FlagCollection::make([]);
369
370
        $flags = $this->client->getConnection()->flags([$this->msgn]);
0 ignored issues
show
Bug introduced by
The method flags() does not exist on Webklex\PHPIMAP\Connection\Protocols\Protocol. Since it exists in all sub-types, consider adding an abstract or default implementation to Webklex\PHPIMAP\Connection\Protocols\Protocol. ( Ignorable by Annotation )

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

370
        $flags = $this->client->getConnection()->/** @scrutinizer ignore-call */ flags([$this->msgn]);
Loading history...
371
372
        if (isset($flags[$this->msgn])) {
373
            foreach($flags[$this->msgn] as $flag) {
374
                if (strpos($flag, "\\") === 0){
375
                    $flag = substr($flag, 1);
376
                }
377
                $flag_key = strtolower($flag);
378
                if (in_array($flag_key, $this->available_flags)) {
379
                    $this->flags->put($flag_key, $flag);
380
                }
381
            }
382
        }
383
    }
384
385
    /**
386
     * Parse the Message body
387
     *
388
     * @return $this
389
     * @throws Exceptions\ConnectionFailedException
390
     * @throws Exceptions\MessageContentFetchingException
391
     * @throws InvalidMessageDateException
392
     * @throws Exceptions\RuntimeException
393
     * @throws Exceptions\EventNotFoundException
394
     */
395
    public function parseBody() {
396
        $this->client->openFolder($this->folder_path);
397
398
        $contents = $this->client->getConnection()->content([$this->msgn]);
0 ignored issues
show
Bug introduced by
The method content() does not exist on Webklex\PHPIMAP\Connection\Protocols\Protocol. Since it exists in all sub-types, consider adding an abstract or default implementation to Webklex\PHPIMAP\Connection\Protocols\Protocol. ( Ignorable by Annotation )

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

398
        $contents = $this->client->getConnection()->/** @scrutinizer ignore-call */ content([$this->msgn]);
Loading history...
399
        if (!isset($contents[$this->msgn])) {
400
            throw new MessageContentFetchingException("no content found", 0);
401
        }
402
        $content = $contents[$this->msgn];
403
404
        $this->structure = new Structure($content, $this->header);
405
406
        $this->fetchStructure($this->structure);
407
408
        if ($this->fetch_options == IMAP::FT_PEEK) {
409
            $this->parseFlags();
410
            if ($this->getFlags()->get("seen") !== null) {
411
                $this->unsetFlag("Seen");
412
            }
413
        }
414
415
        return $this;
416
    }
417
418
    /**
419
     * Fetch the Message structure
420
     * @param $structure
421
     *
422
     * @throws Exceptions\ConnectionFailedException
423
     */
424
    private function fetchStructure($structure) {
425
        $this->client->openFolder($this->folder_path);
426
427
        foreach ($structure->parts as $part) {
428
            $this->fetchPart($part);
429
        }
430
    }
431
432
    /**
433
     * Fetch a given part
434
     * @param Part $part
435
     */
436
    private function fetchPart(Part $part) {
437
438
        if ($part->type == IMAP::MESSAGE_TYPE_TEXT && ($part->ifdisposition == 0 || (empty($part->disposition) || !in_array(strtolower($part->disposition), ['attachment', 'inline'])) ) ) {
439
440
            if ( in_array(($subtype = strtolower($part->subtype)), ["plain", "csv", "html"]) && $part->filename == null && $part->name == null) {
441
                $encoding = $this->getEncoding($part);
442
443
                $content = $this->decodeString($part->content, $part->encoding);
444
445
                // We don't need to do convertEncoding() if charset is ASCII (us-ascii):
446
                //     ASCII is a subset of UTF-8, so all ASCII files are already UTF-8 encoded
447
                //     https://stackoverflow.com/a/11303410
448
                //
449
                // us-ascii is the same as ASCII:
450
                //     ASCII is the traditional name for the encoding system; the Internet Assigned Numbers Authority (IANA)
451
                //     prefers the updated name US-ASCII, which clarifies that this system was developed in the US and
452
                //     based on the typographical symbols predominantly in use there.
453
                //     https://en.wikipedia.org/wiki/ASCII
454
                //
455
                // convertEncoding() function basically means convertToUtf8(), so when we convert ASCII string into UTF-8 it gets broken.
456
                if ($encoding != 'us-ascii') {
457
                    $content = $this->convertEncoding($content, $encoding);
458
                }
459
460
                $this->bodies[$subtype == "plain" ? "text" : $subtype] = $content;
461
            } else {
462
                $this->fetchAttachment($part);
463
            }
464
        } else {
465
            $this->fetchAttachment($part);
466
        }
467
    }
468
469
    /**
470
     * Fetch the Message attachment
471
     * @param Part $part
472
     */
473
    protected function fetchAttachment($part) {
474
475
        $oAttachment = new Attachment($this, $part);
476
477
        if ($oAttachment->getName() !== null) {
0 ignored issues
show
introduced by
The condition $oAttachment->getName() !== null is always true.
Loading history...
478
            if ($oAttachment->getId() !== null) {
0 ignored issues
show
introduced by
The condition $oAttachment->getId() !== null is always true.
Loading history...
479
                $this->attachments->put($oAttachment->getId(), $oAttachment);
480
            } else {
481
                $this->attachments->push($oAttachment);
482
            }
483
        }
484
    }
485
486
    /**
487
     * Fail proof setter for $fetch_option
488
     * @param $option
489
     *
490
     * @return $this
491
     */
492
    public function setFetchOption($option) {
493
        if (is_long($option) === true) {
494
            $this->fetch_options = $option;
495
        } elseif (is_null($option) === true) {
496
            $config = ClientManager::get('options.fetch', IMAP::FT_UID);
497
            $this->fetch_options = is_long($config) ? $config : 1;
498
        }
499
500
        return $this;
501
    }
502
503
    /**
504
     * Fail proof setter for $fetch_body
505
     * @param $option
506
     *
507
     * @return $this
508
     */
509
    public function setFetchBodyOption($option) {
510
        if (is_bool($option)) {
511
            $this->fetch_body = $option;
512
        } elseif (is_null($option)) {
513
            $config = ClientManager::get('options.fetch_body', true);
514
            $this->fetch_body = is_bool($config) ? $config : true;
515
        }
516
517
        return $this;
518
    }
519
520
    /**
521
     * Fail proof setter for $fetch_flags
522
     * @param $option
523
     *
524
     * @return $this
525
     */
526
    public function setFetchFlagsOption($option) {
527
        if (is_bool($option)) {
528
            $this->fetch_flags = $option;
529
        } elseif (is_null($option)) {
530
            $config = ClientManager::get('options.fetch_flags', true);
531
            $this->fetch_flags = is_bool($config) ? $config : true;
532
        }
533
534
        return $this;
535
    }
536
537
    /**
538
     * Decode a given string
539
     * @param $string
540
     * @param $encoding
541
     *
542
     * @return string
543
     */
544
    public function decodeString($string, $encoding) {
545
        switch ($encoding) {
546
            case IMAP::MESSAGE_ENC_BINARY:
547
                if (extension_loaded('imap')) {
548
                    return base64_decode(\imap_binary($string));
549
                }
550
                return base64_decode($string);
551
            case IMAP::MESSAGE_ENC_BASE64:
552
                return base64_decode($string);
553
            case IMAP::MESSAGE_ENC_8BIT:
554
            case IMAP::MESSAGE_ENC_QUOTED_PRINTABLE:
555
                return quoted_printable_decode($string);
556
            case IMAP::MESSAGE_ENC_7BIT:
557
            case IMAP::MESSAGE_ENC_OTHER:
558
            default:
559
                return $string;
560
        }
561
    }
562
563
    /**
564
     * Convert the encoding
565
     * @param $str
566
     * @param string $from
567
     * @param string $to
568
     *
569
     * @return mixed|string
570
     */
571
    public function convertEncoding($str, $from = "ISO-8859-2", $to = "UTF-8") {
572
573
        $from = EncodingAliases::get($from);
574
        $to = EncodingAliases::get($to);
575
576
        if ($from === $to) {
577
            return $str;
578
        }
579
580
        // We don't need to do convertEncoding() if charset is ASCII (us-ascii):
581
        //     ASCII is a subset of UTF-8, so all ASCII files are already UTF-8 encoded
582
        //     https://stackoverflow.com/a/11303410
583
        //
584
        // us-ascii is the same as ASCII:
585
        //     ASCII is the traditional name for the encoding system; the Internet Assigned Numbers Authority (IANA)
586
        //     prefers the updated name US-ASCII, which clarifies that this system was developed in the US and
587
        //     based on the typographical symbols predominantly in use there.
588
        //     https://en.wikipedia.org/wiki/ASCII
589
        //
590
        // convertEncoding() function basically means convertToUtf8(), so when we convert ASCII string into UTF-8 it gets broken.
591
        if (strtolower($from) == 'us-ascii' && $to == 'UTF-8') {
592
            return $str;
593
        }
594
595
        if (function_exists('iconv') && $from != 'UTF-7' && $to != 'UTF-7') {
596
            return @iconv($from, $to.'//IGNORE', $str);
597
        } else {
598
            if (!$from) {
599
                return mb_convert_encoding($str, $to);
600
            }
601
            return mb_convert_encoding($str, $to, $from);
602
        }
603
    }
604
605
    /**
606
     * Get the encoding of a given abject
607
     * @param object|string $structure
608
     *
609
     * @return string
610
     */
611
    public function getEncoding($structure) {
612
        if (property_exists($structure, 'parameters')) {
613
            foreach ($structure->parameters as $parameter) {
614
                if (strtolower($parameter->attribute) == "charset") {
615
                    return EncodingAliases::get($parameter->value);
616
                }
617
            }
618
        }elseif (property_exists($structure, 'charset')){
619
            return EncodingAliases::get($structure->charset);
620
        }elseif (is_string($structure) === true){
621
            return mb_detect_encoding($structure);
622
        }
623
624
        return 'UTF-8';
625
    }
626
627
    /**
628
     * Get the messages folder
629
     *
630
     * @return mixed
631
     * @throws Exceptions\ConnectionFailedException
632
     * @throws Exceptions\FolderFetchingException
633
     */
634
    public function getFolder(){
635
        return $this->client->getFolder($this->folder_path);
636
    }
637
638
    /**
639
     * Create a message thread based on the current message
640
     * @param Folder|null $sent_folder
641
     * @param MessageCollection|null $thread
642
     * @param Folder|null $folder
643
     *
644
     * @return MessageCollection|null
645
     * @throws Exceptions\ConnectionFailedException
646
     * @throws Exceptions\FolderFetchingException
647
     * @throws Exceptions\GetMessagesFailedException
648
     */
649
    public function thread($sent_folder = null, &$thread = null, $folder = null){
650
        $thread = $thread ? $thread : MessageCollection::make([]);
651
        $folder = $folder ? $folder :  $this->getFolder();
652
        $sent_folder = $sent_folder ? $sent_folder : $this->client->getFolder(ClientManager::get("options.common_folders.sent", "INBOX/Sent"));
653
654
        /** @var Message $message */
655
        foreach($thread as $message) {
656
            if ($message->message_id == $this->message_id) {
657
                return $thread;
658
            }
659
        }
660
        $thread->push($this);
661
662
        $folder->query()->inReplyTo($this->message_id)
663
            ->setFetchBody($this->getFetchBodyOption())
664
            ->leaveUnread()->get()->each(function($message) use(&$thread, $folder, $sent_folder){
665
            /** @var Message $message */
666
            $message->thread($sent_folder, $thread, $folder);
667
        });
668
        $sent_folder->query()->inReplyTo($this->message_id)
669
            ->setFetchBody($this->getFetchBodyOption())
670
            ->leaveUnread()->get()->each(function($message) use(&$thread, $folder, $sent_folder){
671
            /** @var Message $message */
672
                $message->thread($sent_folder, $thread, $folder);
673
        });
674
675
        if (is_array($this->in_reply_to)) {
0 ignored issues
show
introduced by
The condition is_array($this->in_reply_to) is always true.
Loading history...
676
            foreach($this->in_reply_to as $in_reply_to) {
677
                $folder->query()->messageId($in_reply_to)
678
                    ->setFetchBody($this->getFetchBodyOption())
679
                    ->leaveUnread()->get()->each(function($message) use(&$thread, $folder, $sent_folder){
680
                    /** @var Message $message */
681
                        $message->thread($sent_folder, $thread, $folder);
682
                });
683
                $sent_folder->query()->messageId($in_reply_to)
684
                    ->setFetchBody($this->getFetchBodyOption())
685
                    ->leaveUnread()->get()->each(function($message) use(&$thread, $folder, $sent_folder){
686
                    /** @var Message $message */
687
                        $message->thread($sent_folder, $thread, $folder);
688
                });
689
            }
690
        }
691
692
        return $thread;
693
    }
694
695
    /**
696
     * Copy the current Messages to a mailbox
697
     * @param string $folder_path
698
     * @param boolean $expunge
699
     *
700
     * @return null|Message
701
     * @throws Exceptions\ConnectionFailedException
702
     * @throws Exceptions\FolderFetchingException
703
     * @throws Exceptions\RuntimeException
704
     * @throws InvalidMessageDateException
705
     * @throws MessageContentFetchingException
706
     * @throws MessageHeaderFetchingException
707
     * @throws Exceptions\EventNotFoundException
708
     */
709
    public function copy($folder_path, $expunge = false) {
710
        $this->client->openFolder($folder_path);
711
        $status = $this->client->getConnection()->examineFolder($folder_path);
0 ignored issues
show
Bug introduced by
The method examineFolder() does not exist on Webklex\PHPIMAP\Connection\Protocols\Protocol. Since it exists in all sub-types, consider adding an abstract or default implementation to Webklex\PHPIMAP\Connection\Protocols\Protocol. ( Ignorable by Annotation )

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

711
        $status = $this->client->getConnection()->/** @scrutinizer ignore-call */ examineFolder($folder_path);
Loading history...
712
713
        if (isset($status["uidnext"])) {
714
            $next_uid = $status["uidnext"];
715
716
            /** @var Folder $folder */
717
            $folder = $this->client->getFolder($folder_path);
718
719
            $this->client->openFolder($this->folder_path);
720
            if ($this->client->getConnection()->copyMessage($folder->path, $this->msgn) == true) {
0 ignored issues
show
Bug introduced by
The method copyMessage() does not exist on Webklex\PHPIMAP\Connection\Protocols\Protocol. Since it exists in all sub-types, consider adding an abstract or default implementation to Webklex\PHPIMAP\Connection\Protocols\Protocol. ( Ignorable by Annotation )

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

720
            if ($this->client->getConnection()->/** @scrutinizer ignore-call */ copyMessage($folder->path, $this->msgn) == true) {
Loading history...
721
                if($expunge) $this->client->expunge();
722
723
                $this->client->openFolder($folder->path);
724
                $message_num = $this->client->getConnection()->getMessageNumber($next_uid);
0 ignored issues
show
Bug introduced by
The method getMessageNumber() does not exist on Webklex\PHPIMAP\Connection\Protocols\Protocol. Since it exists in all sub-types, consider adding an abstract or default implementation to Webklex\PHPIMAP\Connection\Protocols\Protocol. ( Ignorable by Annotation )

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

724
                $message_num = $this->client->getConnection()->/** @scrutinizer ignore-call */ getMessageNumber($next_uid);
Loading history...
725
726
                $message = $folder->query()->getMessage($message_num);
727
                $event = $this->getEvent("message", "copied");
728
                $event::dispatch($this, $message);
729
730
                return $message;
731
            }
732
        }
733
734
        return null;
735
    }
736
737
    /**
738
     * Move the current Messages to a mailbox
739
     * @param string $folder_path
740
     * @param boolean $expunge
741
     *
742
     * @return Message|null
743
     * @throws Exceptions\ConnectionFailedException
744
     * @throws Exceptions\FolderFetchingException
745
     * @throws Exceptions\RuntimeException
746
     * @throws InvalidMessageDateException
747
     * @throws MessageContentFetchingException
748
     * @throws MessageHeaderFetchingException
749
     * @throws Exceptions\EventNotFoundException
750
     */
751
    public function move($folder_path, $expunge = false) {
752
        $this->client->openFolder($folder_path);
753
        $status = $this->client->getConnection()->examineFolder($folder_path);
754
755
        if (isset($status["uidnext"])) {
756
            $next_uid = $status["uidnext"];
757
758
            /** @var Folder $folder */
759
            $folder = $this->client->getFolder($folder_path);
760
761
            $this->client->openFolder($this->folder_path);
762
            if ($this->client->getConnection()->moveMessage($folder->path, $this->msgn) == true) {
0 ignored issues
show
Bug introduced by
The method moveMessage() does not exist on Webklex\PHPIMAP\Connection\Protocols\Protocol. Since it exists in all sub-types, consider adding an abstract or default implementation to Webklex\PHPIMAP\Connection\Protocols\Protocol. ( Ignorable by Annotation )

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

762
            if ($this->client->getConnection()->/** @scrutinizer ignore-call */ moveMessage($folder->path, $this->msgn) == true) {
Loading history...
763
                if($expunge) $this->client->expunge();
764
765
                $this->client->openFolder($folder->path);
766
                $message_num = $this->client->getConnection()->getMessageNumber($next_uid);
767
768
                $message = $folder->query()->getMessage($message_num);
769
                $event = $this->getEvent("message", "moved");
770
                $event::dispatch($this, $message);
771
772
                return $message;
773
            }
774
        }
775
776
        return null;
777
    }
778
779
    /**
780
     * Delete the current Message
781
     * @param bool $expunge
782
     *
783
     * @return bool
784
     * @throws Exceptions\ConnectionFailedException
785
     * @throws Exceptions\RuntimeException
786
     * @throws Exceptions\EventNotFoundException
787
     */
788
    public function delete($expunge = true) {
789
        $status = $this->setFlag("Deleted");
790
        if($expunge) $this->client->expunge();
791
792
        $event = $this->getEvent("message", "deleted");
793
        $event::dispatch($this);
794
795
        return $status;
796
    }
797
798
    /**
799
     * Restore a deleted Message
800
     * @param boolean $expunge
801
     *
802
     * @return bool
803
     * @throws Exceptions\ConnectionFailedException
804
     * @throws Exceptions\RuntimeException
805
     * @throws Exceptions\EventNotFoundException
806
     */
807
    public function restore($expunge = true) {
808
        $status = $this->unsetFlag("Deleted");
809
        if($expunge) $this->client->expunge();
810
811
        $event = $this->getEvent("message", "restored");
812
        $event::dispatch($this);
813
814
        return $status;
815
    }
816
817
    /**
818
     * Set a given flag
819
     * @param string|array $flag
820
     *
821
     * @return bool
822
     * @throws Exceptions\ConnectionFailedException
823
     * @throws Exceptions\RuntimeException
824
     * @throws Exceptions\EventNotFoundException
825
     */
826
    public function setFlag($flag) {
827
        $this->client->openFolder($this->folder_path);
828
        $flag = "\\".trim(is_array($flag) ? implode(" \\", $flag) : $flag);
829
        $status = $this->client->getConnection()->store([$flag], $this->msgn, $this->msgn, "+");
0 ignored issues
show
Bug introduced by
The method store() does not exist on Webklex\PHPIMAP\Connection\Protocols\Protocol. Since it exists in all sub-types, consider adding an abstract or default implementation to Webklex\PHPIMAP\Connection\Protocols\Protocol. ( Ignorable by Annotation )

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

829
        $status = $this->client->getConnection()->/** @scrutinizer ignore-call */ store([$flag], $this->msgn, $this->msgn, "+");
Loading history...
830
        $this->parseFlags();
831
832
        $event = $this->getEvent("flag", "new");
833
        $event::dispatch($this, $flag);
834
835
        return $status;
836
    }
837
838
    /**
839
     * Unset a given flag
840
     * @param string|array $flag
841
     *
842
     * @return bool
843
     * @throws Exceptions\ConnectionFailedException
844
     * @throws Exceptions\RuntimeException
845
     * @throws Exceptions\EventNotFoundException
846
     */
847
    public function unsetFlag($flag) {
848
        $this->client->openFolder($this->folder_path);
849
850
        $flag = "\\".trim(is_array($flag) ? implode(" \\", $flag) : $flag);
851
        $status = $this->client->getConnection()->store([$flag], $this->msgn, $this->msgn, "-");
852
        $this->parseFlags();
853
854
        $event = $this->getEvent("flag", "deleted");
855
        $event::dispatch($this, $flag);
856
857
        return $status;
858
    }
859
860
    /**
861
     * Get all message attachments.
862
     *
863
     * @return AttachmentCollection
864
     */
865
    public function getAttachments() {
866
        return $this->attachments;
867
    }
868
869
    /**
870
     * Checks if there are any attachments present
871
     *
872
     * @return boolean
873
     */
874
    public function hasAttachments() {
875
        return $this->attachments->isEmpty() === false;
876
    }
877
878
    /**
879
     * Get the raw body
880
     *
881
     * @return string
882
     * @throws Exceptions\ConnectionFailedException
883
     */
884
    public function getRawBody() {
885
        if ($this->raw_body === null) {
886
            $this->client->openFolder($this->folder_path);
887
888
            $this->raw_body = $this->structure->raw;
889
        }
890
891
        return $this->raw_body;
892
    }
893
894
    /**
895
     * Get the message header
896
     *
897
     * @return Header
898
     */
899
    public function getHeader() {
900
        return $this->header;
901
    }
902
903
    /**
904
     * Get the current client
905
     *
906
     * @return Client
907
     */
908
    public function getClient() {
909
        return $this->client;
910
    }
911
912
    /**
913
     * Get the used fetch option
914
     *
915
     * @return integer
916
     */
917
    public function getFetchOptions() {
918
        return $this->fetch_options;
919
    }
920
921
    /**
922
     * Get the used fetch body option
923
     *
924
     * @return boolean
925
     */
926
    public function getFetchBodyOption() {
927
        return $this->fetch_body;
928
    }
929
930
    /**
931
     * Get the used fetch flags option
932
     *
933
     * @return boolean
934
     */
935
    public function getFetchFlagsOption() {
936
        return $this->fetch_flags;
937
    }
938
939
    /**
940
     * Get all available bodies
941
     *
942
     * @return array
943
     */
944
    public function getBodies() {
945
        return $this->bodies;
946
    }
947
948
    /**
949
     * Get all set flags
950
     *
951
     * @return FlagCollection
952
     */
953
    public function getFlags() {
954
        return $this->flags;
955
    }
956
957
    /**
958
     * Get the fetched structure
959
     *
960
     * @return object|null
961
     */
962
    public function getStructure(){
963
        return $this->structure;
964
    }
965
966
    /**
967
     * Check if a message matches an other by comparing basic attributes
968
     *
969
     * @param  null|Message $message
970
     * @return boolean
971
     */
972
    public function is(Message $message = null) {
973
        if (is_null($message)) {
974
            return false;
975
        }
976
977
        return $this->uid == $message->uid
978
            && $this->message_id == $message->message_id
979
            && $this->subject == $message->subject
980
            && $this->date->eq($message->date);
981
    }
982
983
    /**
984
     * Get all message attributes
985
     *
986
     * @return array
987
     */
988
    public function getAttributes(){
989
        return array_merge($this->attributes, $this->header->getAttributes());
990
    }
991
992
    /**
993
     * Set the message mask
994
     * @param $mask
995
     *
996
     * @return $this
997
     */
998
    public function setMask($mask){
999
        if(class_exists($mask)){
1000
            $this->mask = $mask;
1001
        }
1002
1003
        return $this;
1004
    }
1005
1006
    /**
1007
     * Get the used message mask
1008
     *
1009
     * @return string
1010
     */
1011
    public function getMask(){
1012
        return $this->mask;
1013
    }
1014
1015
    /**
1016
     * Get a masked instance by providing a mask name
1017
     * @param string|null $mask
1018
     *
1019
     * @return mixed
1020
     * @throws MaskNotFoundException
1021
     */
1022
    public function mask($mask = null){
1023
        $mask = $mask !== null ? $mask : $this->mask;
1024
        if(class_exists($mask)){
1025
            return new $mask($this);
1026
        }
1027
1028
        throw new MaskNotFoundException("Unknown mask provided: ".$mask);
1029
    }
1030
}
1031