Completed
Push — master ( 2c2bb8...3cab00 )
by Malte
02:14
created

Message::copy()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 31
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 18
c 2
b 1
f 0
dl 0
loc 31
rs 9.3554
cc 5
nc 6
nop 2
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 getMsgn()
53
 * @method integer getPriority()
54
 * @method integer setPriority(integer $priority)
55
 * @method string getSubject()
56
 * @method string setSubject(string $subject)
57
 * @method string getMessageId()
58
 * @method string setMessageId(string $message_id)
59
 * @method string getMessageNo()
60
 * @method string setMessageNo(string $message_no)
61
 * @method string getReferences()
62
 * @method string setReferences(string $references)
63
 * @method carbon getDate()
64
 * @method carbon setDate(carbon $date)
65
 * @method array getFrom()
66
 * @method array setFrom(array $from)
67
 * @method array getTo()
68
 * @method array setTo(array $to)
69
 * @method array getCc()
70
 * @method array setCc(array $cc)
71
 * @method array getBcc()
72
 * @method array setBcc(array $bcc)
73
 * @method array getReplyTo()
74
 * @method array setReplyTo(array $reply_to)
75
 * @method array getInReplyTo()
76
 * @method array setInReplyTo(array $in_reply_to)
77
 * @method array getSender()
78
 * @method array setSender(array $sender)
79
 */
80
class Message {
81
    use HasEvents;
82
83
    /**
84
     * Client instance
85
     *
86
     * @var Client
87
     */
88
    private $client = Client::class;
89
90
    /**
91
     * Default mask
92
     *
93
     * @var string $mask
94
     */
95
    protected $mask = MessageMask::class;
96
97
    /**
98
     * Used config
99
     *
100
     * @var array $config
101
     */
102
    protected $config = [];
103
104
    /**
105
     * Attribute holder
106
     *
107
     * @var array $attributes
108
     */
109
    protected $attributes = [
110
        'message_no' => null,
111
    ];
112
113
    /**
114
     * The message folder path
115
     *
116
     * @var string $folder_path
117
     */
118
    protected $folder_path;
119
120
    /**
121
     * Fetch body options
122
     *
123
     * @var integer
124
     */
125
    public $fetch_options = null;
126
127
    /**
128
     * @var integer
129
     */
130
    protected $sequence = IMAP::NIL;
131
132
    /**
133
     * Fetch body options
134
     *
135
     * @var bool
136
     */
137
    public $fetch_body = null;
138
139
    /**
140
     * Fetch flags options
141
     *
142
     * @var bool
143
     */
144
    public $fetch_flags = null;
145
146
    /**
147
     * @var Header $header
148
     */
149
    public $header = null;
150
151
    /**
152
     * Raw message body
153
     *
154
     * @var null|string $raw_body
155
     */
156
    public $raw_body = null;
157
158
    /**
159
     * Message structure
160
     *
161
     * @var Structure $structure
162
     */
163
    protected $structure = null;
164
165
    /**
166
     * Message body components
167
     *
168
     * @var array   $bodies
169
     * @var AttachmentCollection|array $attachments
170
     * @var FlagCollection|array       $flags
171
     */
172
    public $bodies = [];
173
    public $attachments = [];
174
    public $flags = [];
175
176
    /**
177
     * A list of all available and supported flags
178
     *
179
     * @var array $available_flags
180
     */
181
    private $available_flags = null;
182
183
    /**
184
     * Message constructor.
185
     * @param integer $uid
186
     * @param integer|null $msglist
187
     * @param Client $client
188
     * @param integer|null $fetch_options
189
     * @param boolean $fetch_body
190
     * @param boolean $fetch_flags
191
     * @param integer $sequence
192
     *
193
     * @throws Exceptions\ConnectionFailedException
194
     * @throws InvalidMessageDateException
195
     * @throws Exceptions\RuntimeException
196
     * @throws MessageHeaderFetchingException
197
     * @throws MessageContentFetchingException
198
     * @throws Exceptions\EventNotFoundException
199
     */
200
    public function __construct($uid, $msglist, Client $client, $fetch_options = null, $fetch_body = false, $fetch_flags = false, $sequence = null) {
201
202
        $default_mask = $client->getDefaultMessageMask();
203
        if($default_mask != null) {
204
            $this->mask = $default_mask;
205
        }
206
        $this->events["message"] = $client->getDefaultEvents("message");
207
        $this->events["flag"] = $client->getDefaultEvents("flag");
208
209
        $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...
210
211
        $this->config = ClientManager::get('options');
212
        $this->available_flags = ClientManager::get('flags');
213
214
        $this->setSequence($sequence);
215
        $this->setFetchOption($fetch_options);
216
        $this->setFetchBodyOption($fetch_body);
217
        $this->setFetchFlagsOption($fetch_flags);
218
219
        $this->attachments = AttachmentCollection::make([]);
220
        $this->flags = FlagCollection::make([]);
221
222
        $this->client = $client;
223
        $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

223
        $this->client->openFolder(/** @scrutinizer ignore-type */ $this->folder_path);
Loading history...
224
225
        if ($this->sequence === IMAP::ST_UID) {
226
            $this->uid = $uid;
227
            $this->msgn = $this->client->getConnection()->getMessageNumber($this->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

227
            $this->msgn = $this->client->getConnection()->/** @scrutinizer ignore-call */ getMessageNumber($this->uid);
Loading history...
228
        }else{
229
            $this->msgn = $uid;
230
            $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

230
            $this->uid = $this->client->getConnection()->/** @scrutinizer ignore-call */ getUid($this->msgn);
Loading history...
231
        }
232
        $this->msglist = $msglist;
233
234
        if ($this->fetch_options == IMAP::FT_PEEK) {
235
            $this->parseFlags();
236
        }
237
238
        $this->parseHeader();
239
240
        if ($this->getFetchBodyOption() === true) {
241
            $this->parseBody();
242
        }
243
244
        if ($this->getFetchFlagsOption() === true && $this->fetch_options !== IMAP::FT_PEEK) {
245
            $this->parseFlags();
246
        }
247
    }
248
249
    /**
250
     * Create a new instance without fetching the message header and providing them raw instead
251
     * @param int $uid
252
     * @param int|null $msglist
253
     * @param Client $client
254
     * @param string $raw_header
255
     * @param string $raw_body
256
     * @param string $raw_flags
257
     * @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...
258
     * @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...
259
     *
260
     * @return Message
261
     * @throws Exceptions\ConnectionFailedException
262
     * @throws Exceptions\EventNotFoundException
263
     * @throws Exceptions\RuntimeException
264
     * @throws InvalidMessageDateException
265
     * @throws MessageContentFetchingException
266
     * @throws \ReflectionException
267
     */
268
    public static function make($uid, $msglist, Client $client, $raw_header, $raw_body, $raw_flags, $fetch_options = null, $sequence = null){
269
        $reflection = new \ReflectionClass(self::class);
270
        /** @var self $instance */
271
        $instance = $reflection->newInstanceWithoutConstructor();
272
273
        $default_mask = $client->getDefaultMessageMask();
274
        if($default_mask != null) {
275
            $instance->setMask($default_mask);
276
        }
277
        $instance->setEvents([
278
            "message" => $client->getDefaultEvents("message"),
279
            "flag" => $client->getDefaultEvents("flag"),
280
        ]);
281
        $instance->setFolderPath($client->getFolderPath());
282
        $instance->setConfig(ClientManager::get('options'));
283
        $instance->setAvailableFlags(ClientManager::get('flags'));
284
        $instance->setSequence($sequence);
285
        $instance->setFetchOption($fetch_options);
286
287
        $instance->setAttachments(AttachmentCollection::make([]));
288
289
        $instance->setClient($client);
290
291
        if ($instance->getSequence() === IMAP::ST_UID) {
292
            $instance->setUid($uid);
293
            $instance->setMsglist($msglist);
294
        }else{
295
            $instance->setMsgn($uid, $msglist);
0 ignored issues
show
Bug introduced by
It seems like $msglist can also be of type integer; however, parameter $msglist of Webklex\PHPIMAP\Message::setMsgn() does only seem to accept null, maybe add an additional type check? ( Ignorable by Annotation )

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

295
            $instance->setMsgn($uid, /** @scrutinizer ignore-type */ $msglist);
Loading history...
296
        }
297
298
        $instance->parseRawHeader($raw_header);
299
        $instance->parseRawFlags($raw_flags);
300
        $instance->parseRawBody($raw_body);
301
302
        if ($fetch_options == IMAP::FT_PEEK) {
303
            if ($instance->getFlags()->get("seen") == null) {
304
                $instance->unsetFlag("Seen");
305
            }
306
        } elseif ($instance->getFlags()->get("seen") == null) {
307
            $instance->setFlag("Seen");
308
        }
309
310
        return $instance;
311
    }
312
313
    /**
314
     * Call dynamic attribute setter and getter methods
315
     * @param string $method
316
     * @param array $arguments
317
     *
318
     * @return mixed
319
     * @throws MethodNotFoundException
320
     */
321
    public function __call($method, $arguments) {
322
        if(strtolower(substr($method, 0, 3)) === 'get') {
323
            $name = Str::snake(substr($method, 3));
324
            return $this->get($name);
325
        }elseif (strtolower(substr($method, 0, 3)) === 'set') {
326
            $name = Str::snake(substr($method, 3));
327
328
            if(in_array($name, array_keys($this->attributes))) {
329
                $this->attributes[$name] = array_pop($arguments);
330
331
                return $this->attributes[$name];
332
            }
333
334
        }
335
336
        throw new MethodNotFoundException("Method ".self::class.'::'.$method.'() is not supported');
337
    }
338
339
    /**
340
     * Magic setter
341
     * @param $name
342
     * @param $value
343
     *
344
     * @return mixed
345
     */
346
    public function __set($name, $value) {
347
        $this->attributes[$name] = $value;
348
349
        return $this->attributes[$name];
350
    }
351
352
    /**
353
     * Magic getter
354
     * @param $name
355
     *
356
     * @return mixed|null
357
     */
358
    public function __get($name) {
359
        return $this->get($name);
360
    }
361
362
    /**
363
     * Get an available message or message header attribute
364
     * @param $name
365
     *
366
     * @return mixed|null
367
     */
368
    public function get($name) {
369
        if(isset($this->attributes[$name])) {
370
            return $this->attributes[$name];
371
        }
372
373
        return $this->header->get($name);
374
    }
375
376
    /**
377
     * Check if the Message has a text body
378
     *
379
     * @return bool
380
     */
381
    public function hasTextBody() {
382
        return isset($this->bodies['text']);
383
    }
384
385
    /**
386
     * Get the Message text body
387
     *
388
     * @return mixed
389
     */
390
    public function getTextBody() {
391
        if (!isset($this->bodies['text'])) {
392
            return null;
393
        }
394
395
        return $this->bodies['text'];
396
    }
397
398
    /**
399
     * Check if the Message has a html body
400
     *
401
     * @return bool
402
     */
403
    public function hasHTMLBody() {
404
        return isset($this->bodies['html']);
405
    }
406
407
    /**
408
     * Get the Message html body
409
     *
410
     * @return string|null
411
     */
412
    public function getHTMLBody() {
413
        if (!isset($this->bodies['html'])) {
414
            return null;
415
        }
416
417
        return $this->bodies['html'];
418
    }
419
420
    /**
421
     * Parse all defined headers
422
     *
423
     * @throws Exceptions\ConnectionFailedException
424
     * @throws Exceptions\RuntimeException
425
     * @throws InvalidMessageDateException
426
     * @throws MessageHeaderFetchingException
427
     */
428
    private function parseHeader() {
429
        $sequence_id = $this->getSequenceId();
430
        $headers = $this->client->getConnection()->headers([$sequence_id], "RFC822", $this->sequence === IMAP::ST_UID);
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

430
        $headers = $this->client->getConnection()->/** @scrutinizer ignore-call */ headers([$sequence_id], "RFC822", $this->sequence === IMAP::ST_UID);
Loading history...
431
        if (!isset($headers[$sequence_id])) {
432
            throw new MessageHeaderFetchingException("no headers found", 0);
433
        }
434
435
        $this->parseRawHeader($headers[$sequence_id]);
436
    }
437
438
    /**
439
     * @param string $raw_header
440
     *
441
     * @throws InvalidMessageDateException
442
     */
443
    public function parseRawHeader($raw_header){
444
        $this->header = new Header($raw_header);
445
    }
446
447
    /**
448
     * Parse additional raw flags
449
     * @param $raw_flags
450
     */
451
    public function parseRawFlags($raw_flags) {
452
        $this->flags = FlagCollection::make([]);
453
454
        foreach($raw_flags as $flag) {
455
            if (strpos($flag, "\\") === 0){
456
                $flag = substr($flag, 1);
457
            }
458
            $flag_key = strtolower($flag);
459
            if (in_array($flag_key, $this->available_flags) || $this->available_flags === null) {
460
                $this->flags->put($flag_key, $flag);
461
            }
462
        }
463
    }
464
465
    /**
466
     * Parse additional flags
467
     *
468
     * @return void
469
     * @throws Exceptions\ConnectionFailedException
470
     * @throws Exceptions\RuntimeException
471
     */
472
    private function parseFlags() {
473
        $this->client->openFolder($this->folder_path);
474
        $this->flags = FlagCollection::make([]);
475
476
        $sequence_id = $this->getSequenceId();
477
        $flags = $this->client->getConnection()->flags([$sequence_id], $this->sequence === IMAP::ST_UID);
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

477
        $flags = $this->client->getConnection()->/** @scrutinizer ignore-call */ flags([$sequence_id], $this->sequence === IMAP::ST_UID);
Loading history...
478
479
        if (isset($flags[$sequence_id])) {
480
            $this->parseRawFlags($flags[$sequence_id]);
481
        }
482
    }
483
484
    /**
485
     * Parse the Message body
486
     *
487
     * @return $this
488
     * @throws Exceptions\ConnectionFailedException
489
     * @throws Exceptions\MessageContentFetchingException
490
     * @throws InvalidMessageDateException
491
     * @throws Exceptions\RuntimeException
492
     * @throws Exceptions\EventNotFoundException
493
     */
494
    public function parseBody() {
495
        $this->client->openFolder($this->folder_path);
496
497
        $sequence_id = $this->getSequenceId();
498
        $contents = $this->client->getConnection()->content([$sequence_id], "RFC822", $this->sequence === IMAP::ST_UID);
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

498
        $contents = $this->client->getConnection()->/** @scrutinizer ignore-call */ content([$sequence_id], "RFC822", $this->sequence === IMAP::ST_UID);
Loading history...
499
        if (!isset($contents[$sequence_id])) {
500
            throw new MessageContentFetchingException("no content found", 0);
501
        }
502
        $content = $contents[$sequence_id];
503
504
        $body = $this->parseRawBody($content);
505
506
        if ($this->fetch_options == IMAP::FT_PEEK) {
507
            if ($this->getFlags()->get("seen") == null) {
508
                $this->unsetFlag("Seen");
509
            }
510
        }elseif ($this->getFlags()->get("seen") != null) {
511
            $this->setFlag("Seen");
512
        }
513
514
        return $body;
515
    }
516
517
    /**
518
     * Parse a given message body
519
     * @param string $raw_body
520
     *
521
     * @return $this
522
     * @throws Exceptions\ConnectionFailedException
523
     * @throws Exceptions\EventNotFoundException
524
     * @throws Exceptions\RuntimeException
525
     * @throws InvalidMessageDateException
526
     * @throws MessageContentFetchingException
527
     */
528
    public function parseRawBody($raw_body) {
529
        $this->structure = new Structure($raw_body, $this->header);
530
531
        $this->fetchStructure($this->structure);
532
533
        return $this;
534
    }
535
536
    /**
537
     * Fetch the Message structure
538
     * @param $structure
539
     *
540
     * @throws Exceptions\ConnectionFailedException
541
     */
542
    private function fetchStructure($structure) {
543
        $this->client->openFolder($this->folder_path);
544
545
        foreach ($structure->parts as $part) {
546
            $this->fetchPart($part);
547
        }
548
    }
549
550
    /**
551
     * Fetch a given part
552
     * @param Part $part
553
     */
554
    private function fetchPart(Part $part) {
555
556
        if ($part->isAttachment()) {
557
            $this->fetchAttachment($part);
558
        }else{
559
            $encoding = $this->getEncoding($part);
560
561
            $content = $this->decodeString($part->content, $part->encoding);
562
563
            // We don't need to do convertEncoding() if charset is ASCII (us-ascii):
564
            //     ASCII is a subset of UTF-8, so all ASCII files are already UTF-8 encoded
565
            //     https://stackoverflow.com/a/11303410
566
            //
567
            // us-ascii is the same as ASCII:
568
            //     ASCII is the traditional name for the encoding system; the Internet Assigned Numbers Authority (IANA)
569
            //     prefers the updated name US-ASCII, which clarifies that this system was developed in the US and
570
            //     based on the typographical symbols predominantly in use there.
571
            //     https://en.wikipedia.org/wiki/ASCII
572
            //
573
            // convertEncoding() function basically means convertToUtf8(), so when we convert ASCII string into UTF-8 it gets broken.
574
            if ($encoding != 'us-ascii') {
575
                $content = $this->convertEncoding($content, $encoding);
576
            }
577
578
            $subtype = strtolower($part->subtype);
579
            $subtype = $subtype == "plain" || $subtype == "" ? "text" : $subtype;
580
581
            $this->bodies[$subtype] = $content;
582
        }
583
    }
584
585
    /**
586
     * Fetch the Message attachment
587
     * @param Part $part
588
     */
589
    protected function fetchAttachment($part) {
590
591
        $oAttachment = new Attachment($this, $part);
592
593
        if ($oAttachment->getName() !== null && $oAttachment->getSize() > 0) {
594
            if ($oAttachment->getId() !== null) {
0 ignored issues
show
introduced by
The condition $oAttachment->getId() !== null is always true.
Loading history...
595
                $this->attachments->put($oAttachment->getId(), $oAttachment);
596
            } else {
597
                $this->attachments->push($oAttachment);
598
            }
599
        }
600
    }
601
602
    /**
603
     * Fail proof setter for $fetch_option
604
     * @param $option
605
     *
606
     * @return $this
607
     */
608
    public function setFetchOption($option) {
609
        if (is_long($option) === true) {
610
            $this->fetch_options = $option;
611
        } elseif (is_null($option) === true) {
612
            $config = ClientManager::get('options.fetch', IMAP::FT_UID);
613
            $this->fetch_options = is_long($config) ? $config : 1;
614
        }
615
616
        return $this;
617
    }
618
619
    /**
620
     * Set the sequence type
621
     * @param int $sequence
622
     *
623
     * @return $this
624
     */
625
    public function setSequence($sequence) {
626
        if (is_long($sequence)) {
0 ignored issues
show
introduced by
The condition is_long($sequence) is always true.
Loading history...
627
            $this->sequence = $sequence;
628
        } elseif (is_null($sequence)) {
629
            $config = ClientManager::get('options.sequence', IMAP::ST_MSGN);
630
            $this->sequence = is_long($config) ? $config : IMAP::ST_MSGN;
631
        }
632
633
        return $this;
634
    }
635
636
    /**
637
     * Fail proof setter for $fetch_body
638
     * @param $option
639
     *
640
     * @return $this
641
     */
642
    public function setFetchBodyOption($option) {
643
        if (is_bool($option)) {
644
            $this->fetch_body = $option;
645
        } elseif (is_null($option)) {
646
            $config = ClientManager::get('options.fetch_body', true);
647
            $this->fetch_body = is_bool($config) ? $config : true;
648
        }
649
650
        return $this;
651
    }
652
653
    /**
654
     * Fail proof setter for $fetch_flags
655
     * @param $option
656
     *
657
     * @return $this
658
     */
659
    public function setFetchFlagsOption($option) {
660
        if (is_bool($option)) {
661
            $this->fetch_flags = $option;
662
        } elseif (is_null($option)) {
663
            $config = ClientManager::get('options.fetch_flags', true);
664
            $this->fetch_flags = is_bool($config) ? $config : true;
665
        }
666
667
        return $this;
668
    }
669
670
    /**
671
     * Decode a given string
672
     * @param $string
673
     * @param $encoding
674
     *
675
     * @return string
676
     */
677
    public function decodeString($string, $encoding) {
678
        switch ($encoding) {
679
            case IMAP::MESSAGE_ENC_BINARY:
680
                if (extension_loaded('imap')) {
681
                    return base64_decode(\imap_binary($string));
682
                }
683
                return base64_decode($string);
684
            case IMAP::MESSAGE_ENC_BASE64:
685
                return base64_decode($string);
686
            case IMAP::MESSAGE_ENC_8BIT:
687
            case IMAP::MESSAGE_ENC_QUOTED_PRINTABLE:
688
                return quoted_printable_decode($string);
689
            case IMAP::MESSAGE_ENC_7BIT:
690
            case IMAP::MESSAGE_ENC_OTHER:
691
            default:
692
                return $string;
693
        }
694
    }
695
696
    /**
697
     * Convert the encoding
698
     * @param $str
699
     * @param string $from
700
     * @param string $to
701
     *
702
     * @return mixed|string
703
     */
704
    public function convertEncoding($str, $from = "ISO-8859-2", $to = "UTF-8") {
705
706
        $from = EncodingAliases::get($from);
707
        $to = EncodingAliases::get($to);
708
709
        if ($from === $to) {
710
            return $str;
711
        }
712
713
        // We don't need to do convertEncoding() if charset is ASCII (us-ascii):
714
        //     ASCII is a subset of UTF-8, so all ASCII files are already UTF-8 encoded
715
        //     https://stackoverflow.com/a/11303410
716
        //
717
        // us-ascii is the same as ASCII:
718
        //     ASCII is the traditional name for the encoding system; the Internet Assigned Numbers Authority (IANA)
719
        //     prefers the updated name US-ASCII, which clarifies that this system was developed in the US and
720
        //     based on the typographical symbols predominantly in use there.
721
        //     https://en.wikipedia.org/wiki/ASCII
722
        //
723
        // convertEncoding() function basically means convertToUtf8(), so when we convert ASCII string into UTF-8 it gets broken.
724
        if (strtolower($from) == 'us-ascii' && $to == 'UTF-8') {
725
            return $str;
726
        }
727
728
        if (function_exists('iconv') && $from != 'UTF-7' && $to != 'UTF-7') {
729
            return @iconv($from, $to.'//IGNORE', $str);
730
        } else {
731
            if (!$from) {
732
                return mb_convert_encoding($str, $to);
733
            }
734
            return mb_convert_encoding($str, $to, $from);
735
        }
736
    }
737
738
    /**
739
     * Get the encoding of a given abject
740
     * @param object|string $structure
741
     *
742
     * @return string
743
     */
744
    public function getEncoding($structure) {
745
        if (property_exists($structure, 'parameters')) {
746
            foreach ($structure->parameters as $parameter) {
747
                if (strtolower($parameter->attribute) == "charset") {
748
                    return EncodingAliases::get($parameter->value);
749
                }
750
            }
751
        }elseif (property_exists($structure, 'charset')){
752
            return EncodingAliases::get($structure->charset);
753
        }elseif (is_string($structure) === true){
754
            return mb_detect_encoding($structure);
755
        }
756
757
        return 'UTF-8';
758
    }
759
760
    /**
761
     * Get the messages folder
762
     *
763
     * @return mixed
764
     * @throws Exceptions\ConnectionFailedException
765
     * @throws Exceptions\FolderFetchingException
766
     */
767
    public function getFolder(){
768
        return $this->client->getFolder($this->folder_path);
769
    }
770
771
    /**
772
     * Create a message thread based on the current message
773
     * @param Folder|null $sent_folder
774
     * @param MessageCollection|null $thread
775
     * @param Folder|null $folder
776
     *
777
     * @return MessageCollection|null
778
     * @throws Exceptions\ConnectionFailedException
779
     * @throws Exceptions\FolderFetchingException
780
     * @throws Exceptions\GetMessagesFailedException
781
     */
782
    public function thread($sent_folder = null, &$thread = null, $folder = null){
783
        $thread = $thread ? $thread : MessageCollection::make([]);
784
        $folder = $folder ? $folder :  $this->getFolder();
785
        $sent_folder = $sent_folder ? $sent_folder : $this->client->getFolder(ClientManager::get("options.common_folders.sent", "INBOX/Sent"));
786
787
        /** @var Message $message */
788
        foreach($thread as $message) {
789
            if ($message->message_id == $this->message_id) {
790
                return $thread;
791
            }
792
        }
793
        $thread->push($this);
794
795
        $folder->query()->inReplyTo($this->message_id)
796
            ->setFetchBody($this->getFetchBodyOption())
797
            ->leaveUnread()->get()->each(function($message) use(&$thread, $folder, $sent_folder){
798
            /** @var Message $message */
799
            $message->thread($sent_folder, $thread, $folder);
800
        });
801
        $sent_folder->query()->inReplyTo($this->message_id)
802
            ->setFetchBody($this->getFetchBodyOption())
803
            ->leaveUnread()->get()->each(function($message) use(&$thread, $folder, $sent_folder){
804
            /** @var Message $message */
805
                $message->thread($sent_folder, $thread, $folder);
806
        });
807
808
        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...
809
            foreach($this->in_reply_to as $in_reply_to) {
810
                $folder->query()->messageId($in_reply_to)
811
                    ->setFetchBody($this->getFetchBodyOption())
812
                    ->leaveUnread()->get()->each(function($message) use(&$thread, $folder, $sent_folder){
813
                    /** @var Message $message */
814
                        $message->thread($sent_folder, $thread, $folder);
815
                });
816
                $sent_folder->query()->messageId($in_reply_to)
817
                    ->setFetchBody($this->getFetchBodyOption())
818
                    ->leaveUnread()->get()->each(function($message) use(&$thread, $folder, $sent_folder){
819
                    /** @var Message $message */
820
                        $message->thread($sent_folder, $thread, $folder);
821
                });
822
            }
823
        }
824
825
        return $thread;
826
    }
827
828
    /**
829
     * Copy the current Messages to a mailbox
830
     * @param string $folder_path
831
     * @param boolean $expunge
832
     *
833
     * @return null|Message
834
     * @throws Exceptions\ConnectionFailedException
835
     * @throws Exceptions\FolderFetchingException
836
     * @throws Exceptions\RuntimeException
837
     * @throws InvalidMessageDateException
838
     * @throws MessageContentFetchingException
839
     * @throws MessageHeaderFetchingException
840
     * @throws Exceptions\EventNotFoundException
841
     */
842
    public function copy($folder_path, $expunge = false) {
843
        $this->client->openFolder($folder_path);
844
        $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

844
        $status = $this->client->getConnection()->/** @scrutinizer ignore-call */ examineFolder($folder_path);
Loading history...
845
846
        if (isset($status["uidnext"])) {
847
            $next_uid = $status["uidnext"];
848
849
            /** @var Folder $folder */
850
            $folder = $this->client->getFolder($folder_path);
851
852
            $this->client->openFolder($this->folder_path);
853
            if ($this->client->getConnection()->copyMessage($folder->path, $this->getSequenceId(), null, $this->sequence === IMAP::ST_UID) == 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

853
            if ($this->client->getConnection()->/** @scrutinizer ignore-call */ copyMessage($folder->path, $this->getSequenceId(), null, $this->sequence === IMAP::ST_UID) == true) {
Loading history...
854
                if($expunge) $this->client->expunge();
855
856
                $this->client->openFolder($folder->path);
857
858
                if ($this->sequence === IMAP::ST_UID) {
859
                    $sequence_id = $next_uid;
860
                }else{
861
                    $sequence_id = $this->client->getConnection()->getMessageNumber($next_uid);
862
                }
863
864
                $message = $folder->query()->getMessage($sequence_id, null, $this->sequence);
865
                $event = $this->getEvent("message", "copied");
866
                $event::dispatch($this, $message);
867
868
                return $message;
869
            }
870
        }
871
872
        return null;
873
    }
874
875
    /**
876
     * Move the current Messages to a mailbox
877
     * @param string $folder_path
878
     * @param boolean $expunge
879
     *
880
     * @return Message|null
881
     * @throws Exceptions\ConnectionFailedException
882
     * @throws Exceptions\FolderFetchingException
883
     * @throws Exceptions\RuntimeException
884
     * @throws InvalidMessageDateException
885
     * @throws MessageContentFetchingException
886
     * @throws MessageHeaderFetchingException
887
     * @throws Exceptions\EventNotFoundException
888
     */
889
    public function move($folder_path, $expunge = false) {
890
        $this->client->openFolder($folder_path);
891
        $status = $this->client->getConnection()->examineFolder($folder_path);
892
893
        if (isset($status["uidnext"])) {
894
            $next_uid = $status["uidnext"];
895
896
            /** @var Folder $folder */
897
            $folder = $this->client->getFolder($folder_path);
898
899
            $this->client->openFolder($this->folder_path);
900
            if ($this->client->getConnection()->moveMessage($folder->path, $this->getSequenceId(), null, $this->sequence === IMAP::ST_UID) == 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

900
            if ($this->client->getConnection()->/** @scrutinizer ignore-call */ moveMessage($folder->path, $this->getSequenceId(), null, $this->sequence === IMAP::ST_UID) == true) {
Loading history...
901
                if($expunge) $this->client->expunge();
902
903
                $this->client->openFolder($folder->path);
904
905
                if ($this->sequence === IMAP::ST_UID) {
906
                    $sequence_id = $next_uid;
907
                }else{
908
                    $sequence_id = $this->client->getConnection()->getMessageNumber($next_uid);
909
                }
910
911
                $message = $folder->query()->getMessage($sequence_id, null, $this->sequence);
912
                $event = $this->getEvent("message", "moved");
913
                $event::dispatch($this, $message);
914
915
                return $message;
916
            }
917
        }
918
919
        return null;
920
    }
921
922
    /**
923
     * Delete the current Message
924
     * @param bool $expunge
925
     *
926
     * @return bool
927
     * @throws Exceptions\ConnectionFailedException
928
     * @throws Exceptions\RuntimeException
929
     * @throws Exceptions\EventNotFoundException
930
     */
931
    public function delete($expunge = true) {
932
        $status = $this->setFlag("Deleted");
933
        if($expunge) $this->client->expunge();
934
935
        $event = $this->getEvent("message", "deleted");
936
        $event::dispatch($this);
937
938
        return $status;
939
    }
940
941
    /**
942
     * Restore a deleted Message
943
     * @param boolean $expunge
944
     *
945
     * @return bool
946
     * @throws Exceptions\ConnectionFailedException
947
     * @throws Exceptions\RuntimeException
948
     * @throws Exceptions\EventNotFoundException
949
     */
950
    public function restore($expunge = true) {
951
        $status = $this->unsetFlag("Deleted");
952
        if($expunge) $this->client->expunge();
953
954
        $event = $this->getEvent("message", "restored");
955
        $event::dispatch($this);
956
957
        return $status;
958
    }
959
960
    /**
961
     * Set a given flag
962
     * @param string|array $flag
963
     *
964
     * @return bool
965
     * @throws Exceptions\ConnectionFailedException
966
     * @throws Exceptions\RuntimeException
967
     * @throws Exceptions\EventNotFoundException
968
     */
969
    public function setFlag($flag) {
970
        $this->client->openFolder($this->folder_path);
971
        $flag = "\\".trim(is_array($flag) ? implode(" \\", $flag) : $flag);
972
        $sequence_id = $this->getSequenceId();
973
        $status = $this->client->getConnection()->store([$flag], $sequence_id, $sequence_id, "+", true, $this->sequence === IMAP::ST_UID);
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

973
        $status = $this->client->getConnection()->/** @scrutinizer ignore-call */ store([$flag], $sequence_id, $sequence_id, "+", true, $this->sequence === IMAP::ST_UID);
Loading history...
974
        $this->parseFlags();
975
976
        $event = $this->getEvent("flag", "new");
977
        $event::dispatch($this, $flag);
978
979
        return $status;
980
    }
981
982
    /**
983
     * Unset a given flag
984
     * @param string|array $flag
985
     *
986
     * @return bool
987
     * @throws Exceptions\ConnectionFailedException
988
     * @throws Exceptions\RuntimeException
989
     * @throws Exceptions\EventNotFoundException
990
     */
991
    public function unsetFlag($flag) {
992
        $this->client->openFolder($this->folder_path);
993
994
        $flag = "\\".trim(is_array($flag) ? implode(" \\", $flag) : $flag);
995
        $sequence_id = $this->getSequenceId();
996
        $status = $this->client->getConnection()->store([$flag], $sequence_id, $sequence_id, "-", true, $this->sequence === IMAP::ST_UID);
997
        $this->parseFlags();
998
999
        $event = $this->getEvent("flag", "deleted");
1000
        $event::dispatch($this, $flag);
1001
1002
        return $status;
1003
    }
1004
1005
    /**
1006
     * Get all message attachments.
1007
     *
1008
     * @return AttachmentCollection
1009
     */
1010
    public function getAttachments() {
1011
        return $this->attachments;
1012
    }
1013
1014
    /**
1015
     * Checks if there are any attachments present
1016
     *
1017
     * @return boolean
1018
     */
1019
    public function hasAttachments() {
1020
        return $this->attachments->isEmpty() === false;
1021
    }
1022
1023
    /**
1024
     * Get the raw body
1025
     *
1026
     * @return string
1027
     * @throws Exceptions\ConnectionFailedException
1028
     */
1029
    public function getRawBody() {
1030
        if ($this->raw_body === null) {
1031
            $this->client->openFolder($this->folder_path);
1032
1033
            $this->raw_body = $this->structure->raw;
1034
        }
1035
1036
        return $this->raw_body;
1037
    }
1038
1039
    /**
1040
     * Get the message header
1041
     *
1042
     * @return Header
1043
     */
1044
    public function getHeader() {
1045
        return $this->header;
1046
    }
1047
1048
    /**
1049
     * Get the current client
1050
     *
1051
     * @return Client
1052
     */
1053
    public function getClient() {
1054
        return $this->client;
1055
    }
1056
1057
    /**
1058
     * Get the used fetch option
1059
     *
1060
     * @return integer
1061
     */
1062
    public function getFetchOptions() {
1063
        return $this->fetch_options;
1064
    }
1065
1066
    /**
1067
     * Get the used fetch body option
1068
     *
1069
     * @return boolean
1070
     */
1071
    public function getFetchBodyOption() {
1072
        return $this->fetch_body;
1073
    }
1074
1075
    /**
1076
     * Get the used fetch flags option
1077
     *
1078
     * @return boolean
1079
     */
1080
    public function getFetchFlagsOption() {
1081
        return $this->fetch_flags;
1082
    }
1083
1084
    /**
1085
     * Get all available bodies
1086
     *
1087
     * @return array
1088
     */
1089
    public function getBodies() {
1090
        return $this->bodies;
1091
    }
1092
1093
    /**
1094
     * Get all set flags
1095
     *
1096
     * @return FlagCollection
1097
     */
1098
    public function getFlags() {
1099
        return $this->flags;
1100
    }
1101
1102
    /**
1103
     * Get the fetched structure
1104
     *
1105
     * @return Structure|null
1106
     */
1107
    public function getStructure(){
1108
        return $this->structure;
1109
    }
1110
1111
    /**
1112
     * Check if a message matches an other by comparing basic attributes
1113
     *
1114
     * @param  null|Message $message
1115
     * @return boolean
1116
     */
1117
    public function is(Message $message = null) {
1118
        if (is_null($message)) {
1119
            return false;
1120
        }
1121
1122
        return $this->uid == $message->uid
1123
            && $this->message_id == $message->message_id
1124
            && $this->subject == $message->subject
1125
            && $this->date->eq($message->date);
1126
    }
1127
1128
    /**
1129
     * Get all message attributes
1130
     *
1131
     * @return array
1132
     */
1133
    public function getAttributes(){
1134
        return array_merge($this->attributes, $this->header->getAttributes());
1135
    }
1136
1137
    /**
1138
     * Set the message mask
1139
     * @param $mask
1140
     *
1141
     * @return $this
1142
     */
1143
    public function setMask($mask){
1144
        if(class_exists($mask)){
1145
            $this->mask = $mask;
1146
        }
1147
1148
        return $this;
1149
    }
1150
1151
    /**
1152
     * Get the used message mask
1153
     *
1154
     * @return string
1155
     */
1156
    public function getMask(){
1157
        return $this->mask;
1158
    }
1159
1160
    /**
1161
     * Get a masked instance by providing a mask name
1162
     * @param string|null $mask
1163
     *
1164
     * @return mixed
1165
     * @throws MaskNotFoundException
1166
     */
1167
    public function mask($mask = null){
1168
        $mask = $mask !== null ? $mask : $this->mask;
1169
        if(class_exists($mask)){
1170
            return new $mask($this);
1171
        }
1172
1173
        throw new MaskNotFoundException("Unknown mask provided: ".$mask);
1174
    }
1175
1176
    /**
1177
     * Set the message path aka folder path
1178
     * @param $folder_path
1179
     *
1180
     * @return $this
1181
     */
1182
    public function setFolderPath($folder_path){
1183
        $this->folder_path = $folder_path;
1184
1185
        return $this;
1186
    }
1187
1188
    /**
1189
     * Set the config
1190
     * @param $config
1191
     *
1192
     * @return $this
1193
     */
1194
    public function setConfig($config){
1195
        $this->config = $config;
1196
1197
        return $this;
1198
    }
1199
1200
    /**
1201
     * Set the available flags
1202
     * @param $available_flags
1203
     *
1204
     * @return $this
1205
     */
1206
    public function setAvailableFlags($available_flags){
1207
        $this->available_flags = $available_flags;
1208
1209
        return $this;
1210
    }
1211
1212
    /**
1213
     * Set the attachment collection
1214
     * @param $attachments
1215
     *
1216
     * @return $this
1217
     */
1218
    public function setAttachments($attachments){
1219
        $this->attachments = $attachments;
1220
1221
        return $this;
1222
    }
1223
1224
    /**
1225
     * Set the flag collection
1226
     * @param $flags
1227
     *
1228
     * @return $this
1229
     */
1230
    public function setFlags($flags){
1231
        $this->flags = $flags;
1232
1233
        return $this;
1234
    }
1235
1236
    /**
1237
     * Set the client
1238
     * @param $client
1239
     *
1240
     * @throws Exceptions\ConnectionFailedException
1241
     * @return $this
1242
     */
1243
    public function setClient($client){
1244
        $this->client = $client;
1245
        $this->client->openFolder($this->folder_path);
1246
1247
        return $this;
1248
    }
1249
1250
    /**
1251
     * Set the message number
1252
     * @param int $uid
1253
     *
1254
     * @throws Exceptions\ConnectionFailedException
1255
     * @throws Exceptions\RuntimeException
1256
     * @return $this
1257
     */
1258
    public function setUid($uid){
1259
        $this->uid = $uid;
1260
        $this->msgn = $this->client->getConnection()->getMessageNumber($this->uid);
1261
        $this->msglist = null;
1262
1263
        return $this;
1264
    }
1265
1266
    /**
1267
     * Set the message number
1268
     * @param $msgn
1269
     * @param null $msglist
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $msglist is correct as it would always require null to be passed?
Loading history...
1270
     *
1271
     * @throws Exceptions\ConnectionFailedException
1272
     * @throws Exceptions\RuntimeException
1273
     * @return $this
1274
     */
1275
    public function setMsgn($msgn, $msglist = null){
1276
        $this->msgn = $msgn;
1277
        $this->msglist = $msglist;
1278
        $this->uid = $this->client->getConnection()->getUid($this->msgn);
1279
1280
        return $this;
1281
    }
1282
1283
    /**
1284
     * Get the current sequence type
1285
     *
1286
     * @return int
1287
     */
1288
    public function getSequence(){
1289
        return $this->sequence;
1290
    }
1291
1292
    /**
1293
     * Set the sequence type
1294
     *
1295
     * @return int
1296
     */
1297
    public function getSequenceId(){
1298
        return $this->sequence === IMAP::ST_UID ? $this->uid : $this->msgn;
1299
    }
1300
}
1301