Issues (3627)

app/bundles/EmailBundle/MonitoredEmail/Mailbox.php (2 issues)

1
<?php
2
3
/*
4
 * @copyright   2015 Mautic Contributors. All rights reserved
5
 * @author      Mautic
6
 *
7
 * @link        http://mautic.org
8
 *
9
 * @license     GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
10
 *
11
 * Modified from
12
 *
13
 * @see         https://github.com/barbushin/php-imap
14
 *
15
 * @author      Barbushin Sergey http://linkedin.com/in/barbushin
16
 * @copyright   BSD (three-clause)
17
 */
18
19
namespace Mautic\EmailBundle\MonitoredEmail;
20
21
use Mautic\CoreBundle\Helper\CoreParametersHelper;
22
use Mautic\CoreBundle\Helper\PathsHelper;
23
use Mautic\EmailBundle\Exception\MailboxException;
24
use Mautic\EmailBundle\MonitoredEmail\Exception\NotConfiguredException;
25
use stdClass;
26
27
class Mailbox
28
{
29
    /**
30
     * Return all mails matching the rest of the criteria.
31
     */
32
    const CRITERIA_ALL = 'ALL';
33
34
    /**
35
     * Match mails with the \\ANSWERED flag set.
36
     */
37
    const CRITERIA_ANSWERED = 'ANSWERED';
38
39
    /**
40
     * CRITERIA_BCC "string" - match mails with "string" in the Bcc: field.
41
     */
42
    const CRITERIA_BCC = 'BCC';
43
44
    /**
45
     * CRITERIA_BEFORE "date" - match mails with Date: before "date".
46
     */
47
    const CRITERIA_BEFORE = 'BEFORE';
48
49
    /**
50
     * CRITERIA_BODY "string" - match mails with "string" in the body of the mail.
51
     */
52
    const CRITERIA_BODY = 'BODY';
53
54
    /**
55
     * CRITERIA_CC "string" - match mails with "string" in the Cc: field.
56
     */
57
    const CRITERIA_CC = 'CC';
58
59
    /**
60
     * Match deleted mails.
61
     */
62
    const CRITERIA_DELETED = 'DELETED';
63
64
    /**
65
     * Match mails with the \\FLAGGED (sometimes referred to as Important or Urgent) flag set.
66
     */
67
    const CRITERIA_FLAGGED = 'FLAGGED';
68
69
    /**
70
     * CRITERIA_FROM "string" - match mails with "string" in the From: field.
71
     */
72
    const CRITERIA_FROM = 'FROM';
73
74
    /**
75
     *  CRITERIA_KEYWORD "string" - match mails with "string" as a keyword.
76
     */
77
    const CRITERIA_KEYWORD = 'KEYWORD';
78
79
    /**
80
     * Match new mails.
81
     */
82
    const CRITERIA_NEW = 'NEW';
83
84
    /**
85
     * Match old mails.
86
     */
87
    const CRITERIA_OLD = 'OLD';
88
89
    /**
90
     * CRITERIA_ON "date" - match mails with Date: matching "date".
91
     */
92
    const CRITERIA_ON = 'ON';
93
94
    /**
95
     * Match mails with the \\RECENT flag set.
96
     */
97
    const CRITERIA_RECENT = 'RECENT';
98
99
    /**
100
     * Match mails that have been read (the \\SEEN flag is set).
101
     */
102
    const CRITERIA_SEEN = 'SEEN';
103
104
    /**
105
     * CRITERIA_SINCE "date" - match mails with Date: after "date".
106
     */
107
    const CRITERIA_SINCE = 'SINCE';
108
109
    /**
110
     *  CRITERIA_SUBJECT "string" - match mails with "string" in the Subject:.
111
     */
112
    const CRITERIA_SUBJECT = 'SUBJECT';
113
114
    /**
115
     * CRITERIA_TEXT "string" - match mails with text "string".
116
     */
117
    const CRITERIA_TEXT = 'TEXT';
118
119
    /**
120
     * CRITERIA_TO "string" - match mails with "string" in the To:.
121
     */
122
    const CRITERIA_TO = 'TO';
123
124
    /**
125
     *  Get messages since a specific UID. Eg. UID 2:* will return all messages with UID 2 and above (IMAP includes the given UID).
126
     */
127
    const CRITERIA_UID = 'UID';
128
129
    /**
130
     *  Match mails that have not been answered.
131
     */
132
    const CRITERIA_UNANSWERED = 'UNANSWERED';
133
134
    /**
135
     * Match mails that are not deleted.
136
     */
137
    const CRITERIA_UNDELETED = 'UNDELETED';
138
139
    /**
140
     * Match mails that are not flagged.
141
     */
142
    const CRITERIA_UNFLAGGED = 'UNFLAGGED';
143
144
    /**
145
     * CRITERIA_UNKEYWORD "string" - match mails that do not have the keyword "string".
146
     */
147
    const CRITERIA_UNKEYWORD = 'UNKEYWORD';
148
149
    /**
150
     * Match mails which have not been read yet.
151
     */
152
    const CRITERIA_UNSEEN = 'UNSEEN';
153
154
    /**
155
     * Match mails which have not been read yet - alias of CRITERIA_UNSEEN.
156
     */
157
    const CRITERIA_UNREAD = 'UNSEEN';
158
159
    protected $imapPath;
160
    protected $imapFullPath;
161
    protected $imapStream;
162
    protected $imapFolder     = 'INBOX';
163
    protected $imapOptions    = 0;
164
    protected $imapRetriesNum = 0;
165
    protected $imapParams     = [];
166
    protected $serverEncoding = 'UTF-8';
167
    protected $attachmentsDir;
168
    protected $settings;
169
    protected $isGmail = false;
170
    protected $mailboxes;
171
172
    private $folders = [];
173
174
    /**
175
     * Mailbox constructor.
176
     */
177
    public function __construct(CoreParametersHelper $parametersHelper, PathsHelper $pathsHelper)
178
    {
179
        $this->mailboxes = $parametersHelper->get('monitored_email', []);
180
181
        if (isset($this->mailboxes['general'])) {
182
            $this->settings = $this->mailboxes['general'];
183
        } else {
184
            $this->settings = [
185
                'host'            => '',
186
                'port'            => '',
187
                'password'        => '',
188
                'user'            => '',
189
                'encryption'      => '',
190
                'use_attachments' => false,
191
            ];
192
        }
193
194
        $this->createAttachmentsDir($pathsHelper);
195
196
        if ('imap.gmail.com' == $this->settings['host']) {
197
            $this->isGmail = true;
198
        }
199
    }
200
201
    /**
202
     * Returns if a mailbox is configured.
203
     *
204
     * @param null $bundleKey
205
     * @param null $folderKey
206
     *
207
     * @return bool
208
     *
209
     * @throws MailboxException
210
     */
211
    public function isConfigured($bundleKey = null, $folderKey = null)
212
    {
213
        if (null !== $bundleKey) {
214
            try {
215
                $this->switchMailbox($bundleKey, $folderKey);
216
            } catch (MailboxException $e) {
217
                return false;
218
            }
219
        }
220
221
        return
222
            !empty($this->settings['host']) && !empty($this->settings['port']) && !empty($this->settings['user'])
223
            && !empty($this->settings['password'])
224
        ;
225
    }
226
227
    /**
228
     * Switch to another configured monitored mailbox.
229
     *
230
     * @param        $bundle
231
     * @param string $mailbox
232
     *
233
     * @throws MailboxException
234
     */
235
    public function switchMailbox($bundle, $mailbox = '')
236
    {
237
        $key = $bundle.(!empty($mailbox) ? '_'.$mailbox : '');
238
239
        if (isset($this->mailboxes[$key])) {
240
            $this->settings           = (!empty($this->mailboxes[$key]['override_settings'])) ? $this->mailboxes[$key] : $this->mailboxes['general'];
241
            $this->imapFolder         = $this->mailboxes[$key]['folder'];
242
            $this->settings['folder'] = $this->mailboxes[$key]['folder'];
243
            // Disconnect so that new mailbox settings are used
244
            $this->disconnect();
245
            // Setup new connection
246
            $this->setImapPath();
247
        } else {
248
            throw new MailboxException($key.' not found');
249
        }
250
    }
251
252
    /**
253
     * Returns if this is a Gmail connection.
254
     *
255
     * @return mixed
256
     */
257
    public function isGmail()
258
    {
259
        return $this->isGmail();
260
    }
261
262
    /**
263
     * Set imap path based on mailbox settings.
264
     *
265
     * @param null $settings
266
     */
267
    public function setImapPath($settings = null)
268
    {
269
        if (null == $settings) {
270
            $settings = $this->settings;
271
        }
272
        $paths              = $this->getImapPath($settings);
273
        $this->imapPath     = $paths['path'];
274
        $this->imapFullPath = $paths['full'];
275
    }
276
277
    /**
278
     * @param $settings
279
     *
280
     * @return array
281
     */
282
    public function getImapPath($settings)
283
    {
284
        if (!isset($settings['encryption'])) {
285
            $settings['encryption'] = (!empty($settings['ssl'])) ? '/ssl' : '';
286
        }
287
        $path     = "{{$settings['host']}:{$settings['port']}/imap{$settings['encryption']}}";
288
        $fullPath = $path;
289
290
        if (isset($settings['folder'])) {
291
            $fullPath .= $settings['folder'];
292
        }
293
294
        return ['path' => $path, 'full' => $fullPath];
295
    }
296
297
    /**
298
     * Override mailbox settings.
299
     */
300
    public function setMailboxSettings(array $settings)
301
    {
302
        $this->settings = array_merge($this->settings, $settings);
303
304
        $this->isGmail = ('imap.gmail.com' == $this->settings['host']);
305
306
        $this->setImapPath();
307
    }
308
309
    /**
310
     * Get settings.
311
     *
312
     * @param        $bundle
313
     * @param string $mailbox
314
     *
315
     * @return mixed
316
     *
317
     * @throws MailboxException
318
     */
319
    public function getMailboxSettings($bundle = null, $mailbox = '')
320
    {
321
        if (null == $bundle) {
322
            return $this->settings;
323
        }
324
325
        $key = $bundle.(!empty($mailbox) ? '_'.$mailbox : '');
326
327
        if (isset($this->mailboxes[$key])) {
328
            $settings = (!empty($this->mailboxes[$key]['override_settings'])) ? $this->mailboxes[$key] : $this->mailboxes['general'];
329
330
            $settings['folder'] = $this->mailboxes[$key]['folder'];
331
            $this->setImapPath($settings);
332
333
            $imapPath              = $this->getImapPath($settings);
334
            $settings['imap_path'] = $imapPath['full'];
335
        } else {
336
            throw new MailboxException($key.' not found');
337
        }
338
339
        return $settings;
340
    }
341
342
    /**
343
     * Set custom connection arguments of imap_open method. See http://php.net/imap_open.
344
     *
345
     * @param int   $options
346
     * @param int   $retriesNum
347
     * @param array $params
348
     */
349
    public function setConnectionArgs($options = 0, $retriesNum = 0, array $params = null)
350
    {
351
        $this->imapOptions    = $options;
352
        $this->imapRetriesNum = $retriesNum;
353
        $this->imapParams     = $params;
354
    }
355
356
    /**
357
     * Switch to another box.
358
     *
359
     * @param $folder
360
     */
361
    public function switchFolder($folder)
362
    {
363
        if ($folder != $this->imapFolder) {
364
            $this->imapFullPath = $this->imapPath.$folder;
365
            $this->imapFolder   = $folder;
366
        }
367
368
        $this->getImapStream();
369
    }
370
371
    /**
372
     * Get IMAP mailbox connection stream.
373
     *
374
     * @return resource|null
375
     */
376
    public function getImapStream()
377
    {
378
        if (!$this->isConnected()) {
379
            $this->imapStream = $this->initImapStream();
380
        } else {
381
            @imap_reopen($this->imapStream, $this->imapFullPath);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for imap_reopen(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

381
            /** @scrutinizer ignore-unhandled */ @imap_reopen($this->imapStream, $this->imapFullPath);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
382
        }
383
384
        return $this->imapStream;
385
    }
386
387
    /**
388
     * @return resource
389
     *
390
     * @throws MailboxException
391
     */
392
    protected function initImapStream()
393
    {
394
        imap_timeout(IMAP_OPENTIMEOUT, 15);
395
        imap_timeout(IMAP_CLOSETIMEOUT, 15);
396
        imap_timeout(IMAP_READTIMEOUT, 15);
397
        imap_timeout(IMAP_WRITETIMEOUT, 15);
398
399
        $imapStream = @imap_open(
400
            $this->imapFullPath,
401
            $this->settings['user'],
402
            $this->settings['password'],
403
            $this->imapOptions,
404
            $this->imapRetriesNum,
405
            $this->imapParams
406
        );
407
        if (!$imapStream) {
408
            throw new MailboxException();
409
        }
410
411
        return $imapStream;
412
    }
413
414
    /**
415
     * Check if the stream is connected.
416
     *
417
     * @return bool
418
     */
419
    protected function isConnected()
420
    {
421
        return $this->isConfigured() && $this->imapStream && is_resource($this->imapStream) && @imap_ping($this->imapStream);
422
    }
423
424
    /**
425
     * Get information about the current mailbox.
426
     *
427
     * Returns the information in an object with following properties:
428
     *  Date - current system time formatted according to RFC2822
429
     *  Driver - protocol used to access this mailbox: POP3, IMAP, NNTP
430
     *  Mailbox - the mailbox name
431
     *  Nmsgs - number of mails in the mailbox
432
     *  Recent - number of recent mails in the mailbox
433
     *
434
     * @return stdClass
435
     */
436
    public function checkMailbox()
437
    {
438
        return imap_check($this->getImapStream());
439
    }
440
441
    /**
442
     * Creates a new mailbox specified by mailbox.
443
     *
444
     * @return bool
445
     */
446
    public function createMailbox()
447
    {
448
        return imap_createmailbox($this->getImapStream(), imap_utf7_encode($this->imapFullPath));
449
    }
450
451
    /**
452
     * Gets status information about the given mailbox.
453
     *
454
     * This function returns an object containing status information.
455
     * The object has the following properties: messages, recent, unseen, uidnext, and uidvalidity.
456
     *
457
     * @return stdClass if the box doesn't exist
458
     */
459
    public function statusMailbox()
460
    {
461
        return imap_status($this->getImapStream(), $this->imapFullPath, SA_ALL);
462
    }
463
464
    /**
465
     * Gets listing the folders.
466
     *
467
     * This function returns an object containing listing the folders.
468
     * The object has the following properties: messages, recent, unseen, uidnext, and uidvalidity.
469
     *
470
     * @return array listing the folders
471
     */
472
    public function getListingFolders()
473
    {
474
        if (!$this->isConfigured()) {
475
            throw new NotConfiguredException('mautic.email.config.monitored_email.not_configured');
476
        }
477
478
        if (!isset($this->folders[$this->imapFullPath])) {
479
            $tempFolders = @imap_list($this->getImapStream(), $this->imapPath, '*');
480
481
            if (!empty($tempFolders)) {
482
                foreach ($tempFolders as $key => $folder) {
483
                    $folder            = str_replace($this->imapPath, '', imap_utf8($folder));
484
                    $tempFolders[$key] = $folder;
485
                }
486
            } else {
487
                $tempFolders = [];
488
            }
489
490
            $this->folders[$this->imapFullPath] = $tempFolders;
491
        }
492
493
        return $this->folders[$this->imapFullPath];
494
    }
495
496
    /**
497
     * Fetch unread messages.
498
     *
499
     * @param null $folder
500
     *
501
     * @return array
502
     */
503
    public function fetchUnread($folder = null)
504
    {
505
        if (null !== $folder) {
506
            $this->switchFolder($folder);
507
        }
508
509
        return $this->searchMailBox(self::CRITERIA_UNSEEN);
510
    }
511
512
    /**
513
     * This function performs a search on the mailbox currently opened in the given IMAP stream.
514
     * For example, to match all unanswered mails sent by Mom, you'd use: "UNANSWERED FROM mom".
515
     * Searches appear to be case insensitive. This list of criteria is from a reading of the UW
516
     * c-client source code and may be incomplete or inaccurate (see also RFC2060, section 6.4.4).
517
     *
518
     * @param string $criteria String, delimited by spaces, in which the following keywords are allowed. Any multi-word arguments (e.g. FROM "joey
519
     *                         smith") must be quoted. Results will match all criteria entries.
520
     *                         ALL - return all mails matching the rest of the criteria
521
     *                         ANSWERED - match mails with the \\ANSWERED flag set
522
     *                         BCC "string" - match mails with "string" in the Bcc: field
523
     *                         BEFORE "date" - match mails with Date: before "date"
524
     *                         BODY "string" - match mails with "string" in the body of the mail
525
     *                         CC "string" - match mails with "string" in the Cc: field
526
     *                         DELETED - match deleted mails
527
     *                         FLAGGED - match mails with the \\FLAGGED (sometimes referred to as Important or Urgent) flag set
528
     *                         FROM "string" - match mails with "string" in the From: field
529
     *                         KEYWORD "string" - match mails with "string" as a keyword
530
     *                         NEW - match new mails
531
     *                         OLD - match old mails
532
     *                         ON "date" - match mails with Date: matching "date"
533
     *                         RECENT - match mails with the \\RECENT flag set
534
     *                         SEEN - match mails that have been read (the \\SEEN flag is set)
535
     *                         SINCE "date" - match mails with Date: after "date"
536
     *                         SUBJECT "string" - match mails with "string" in the Subject:
537
     *                         TEXT "string" - match mails with text "string"
538
     *                         TO "string" - match mails with "string" in the To:
539
     *                         UNANSWERED - match mails that have not been answered
540
     *                         UNDELETED - match mails that are not deleted
541
     *                         UNFLAGGED - match mails that are not flagged
542
     *                         UNKEYWORD "string" - match mails that do not have the keyword "string"
543
     *                         UNSEEN - match mails which have not been read yet
544
     *
545
     * @return array Mails ids
546
     */
547
    public function searchMailbox($criteria = self::CRITERIA_ALL)
548
    {
549
        if (preg_match('/'.self::CRITERIA_UID.' ((\d+):(\d+|\*))/', $criteria, $matches)) {
550
            // PHP imap_search does not support UID n:* so use imap_fetch_overview instead
551
            $messages = imap_fetch_overview($this->getImapStream(), $matches[1], FT_UID);
552
553
            $mailIds = [];
554
            foreach ($messages as $message) {
555
                $mailIds[] = $message->uid;
556
            }
557
        } else {
558
            $mailIds = imap_search($this->getImapStream(), $criteria, SE_UID);
559
        }
560
561
        return $mailIds ? $mailIds : [];
562
    }
563
564
    /**
565
     * Save mail body.
566
     *
567
     * @param        $mailId
568
     * @param string $filename
569
     *
570
     * @return bool
571
     */
572
    public function saveMail($mailId, $filename = 'email.eml')
573
    {
574
        return imap_savebody($this->getImapStream(), $filename, $mailId, '', FT_UID);
575
    }
576
577
    /**
578
     * Marks mails listed in mailId for deletion.
579
     *
580
     * @param $mailId
581
     *
582
     * @return bool
583
     */
584
    public function deleteMail($mailId)
585
    {
586
        return imap_delete($this->getImapStream(), $mailId, FT_UID);
587
    }
588
589
    /**
590
     * Move mail to another box.
591
     *
592
     * @param $mailId
593
     * @param $mailBox
594
     *
595
     * @return bool
596
     */
597
    public function moveMail($mailId, $mailBox)
598
    {
599
        return imap_mail_move($this->getImapStream(), $mailId, $mailBox, CP_UID) && $this->expungeDeletedMails();
600
    }
601
602
    /**
603
     * Deletes all the mails marked for deletion by imap_delete(), imap_mail_move(), or imap_setflag_full().
604
     *
605
     * @return bool
606
     */
607
    public function expungeDeletedMails()
608
    {
609
        return imap_expunge($this->getImapStream());
610
    }
611
612
    /**
613
     * Add the flag \Seen to a mail.
614
     *
615
     * @param $mailId
616
     *
617
     * @return bool
618
     */
619
    public function markMailAsRead($mailId)
620
    {
621
        return $this->setFlag([$mailId], '\\Seen');
622
    }
623
624
    /**
625
     * Remove the flag \Seen from a mail.
626
     *
627
     * @param $mailId
628
     *
629
     * @return bool
630
     */
631
    public function markMailAsUnread($mailId)
632
    {
633
        return $this->clearFlag([$mailId], '\\Seen');
634
    }
635
636
    /**
637
     * Add the flag \Flagged to a mail.
638
     *
639
     * @param $mailId
640
     *
641
     * @return bool
642
     */
643
    public function markMailAsImportant($mailId)
644
    {
645
        return $this->setFlag([$mailId], '\\Flagged');
646
    }
647
648
    /**
649
     * Add the flag \Seen to a mails.
650
     *
651
     * @param $mailIds
652
     *
653
     * @return bool
654
     */
655
    public function markMailsAsRead(array $mailIds)
656
    {
657
        return $this->setFlag($mailIds, '\\Seen');
658
    }
659
660
    /**
661
     * Remove the flag \Seen from some mails.
662
     *
663
     * @param $mailIds
664
     *
665
     * @return bool
666
     */
667
    public function markMailsAsUnread(array $mailIds)
668
    {
669
        return $this->clearFlag($mailIds, '\\Seen');
670
    }
671
672
    /**
673
     * Add the flag \Flagged to some mails.
674
     *
675
     * @param $mailIds
676
     *
677
     * @return bool
678
     */
679
    public function markMailsAsImportant(array $mailIds)
680
    {
681
        return $this->setFlag($mailIds, '\\Flagged');
682
    }
683
684
    /**
685
     * Causes a store to add the specified flag to the flags set for the mails in the specified sequence.
686
     *
687
     * @param string $flag which you can set are \Seen, \Answered, \Flagged, \Deleted, and \Draft as defined by RFC2060
688
     *
689
     * @return bool
690
     */
691
    public function setFlag(array $mailsIds, $flag)
692
    {
693
        return imap_setflag_full($this->getImapStream(), implode(',', $mailsIds), $flag, ST_UID);
694
    }
695
696
    /**
697
     * Cause a store to delete the specified flag to the flags set for the mails in the specified sequence.
698
     *
699
     * @param string $flag which you can set are \Seen, \Answered, \Flagged, \Deleted, and \Draft as defined by RFC2060
700
     *
701
     * @return bool
702
     */
703
    public function clearFlag(array $mailsIds, $flag)
704
    {
705
        return imap_clearflag_full($this->getImapStream(), implode(',', $mailsIds), $flag, ST_UID);
706
    }
707
708
    /**
709
     * Fetch mail headers for listed mails ids.
710
     *
711
     * Returns an array of objects describing one mail header each. The object will only define a property if it exists. The possible properties are:
712
     *  subject - the mails subject
713
     *  from - who sent it
714
     *  to - recipient
715
     *  date - when was it sent
716
     *  message_id - Mail-ID
717
     *  references - is a reference to this mail id
718
     *  in_reply_to - is a reply to this mail id
719
     *  size - size in bytes
720
     *  uid - UID the mail has in the mailbox
721
     *  msgno - mail sequence number in the mailbox
722
     *  recent - this mail is flagged as recent
723
     *  flagged - this mail is flagged
724
     *  answered - this mail is flagged as answered
725
     *  deleted - this mail is flagged for deletion
726
     *  seen - this mail is flagged as already read
727
     *  draft - this mail is flagged as being a draft
728
     *
729
     * @return array
730
     */
731
    public function getMailsInfo(array $mailsIds)
732
    {
733
        $mails = imap_fetch_overview($this->getImapStream(), implode(',', $mailsIds), FT_UID);
734
        if (is_array($mails) && count($mails)) {
735
            foreach ($mails as &$mail) {
736
                if (isset($mail->subject)) {
737
                    $mail->subject = $this->decodeMimeStr($mail->subject, $this->serverEncoding);
738
                }
739
                if (isset($mail->from)) {
740
                    $mail->from = $this->decodeMimeStr($mail->from, $this->serverEncoding);
741
                }
742
                if (isset($mail->to)) {
743
                    $mail->to = $this->decodeMimeStr($mail->to, $this->serverEncoding);
744
                }
745
            }
746
        }
747
748
        return $mails;
749
    }
750
751
    /**
752
     * Get information about the current mailbox.
753
     *
754
     * Returns an object with following properties:
755
     *  Date - last change (current datetime)
756
     *  Driver - driver
757
     *  Mailbox - name of the mailbox
758
     *  Nmsgs - number of messages
759
     *  Recent - number of recent messages
760
     *  Unread - number of unread messages
761
     *  Deleted - number of deleted messages
762
     *  Size - mailbox size
763
     *
764
     * @return object Object with info | FALSE on failure
765
     */
766
    public function getMailboxInfo()
767
    {
768
        return imap_mailboxmsginfo($this->getImapStream());
769
    }
770
771
    /**
772
     * Gets mails ids sorted by some criteria.
773
     *
774
     * Criteria can be one (and only one) of the following constants:
775
     *  SORTDATE - mail Date
776
     *  SORTARRIVAL - arrival date (default)
777
     *  SORTFROM - mailbox in first From address
778
     *  SORTSUBJECT - mail subject
779
     *  SORTTO - mailbox in first To address
780
     *  SORTCC - mailbox in first cc address
781
     *  SORTSIZE - size of mail in octets
782
     *
783
     * @param int  $criteria
784
     * @param bool $reverse
785
     *
786
     * @return array Mails ids
787
     */
788
    public function sortMails($criteria = SORTARRIVAL, $reverse = true)
789
    {
790
        return imap_sort($this->getImapStream(), $criteria, $reverse, SE_UID);
791
    }
792
793
    /**
794
     * Get mails count in mail box.
795
     *
796
     * @return int
797
     */
798
    public function countMails()
799
    {
800
        return imap_num_msg($this->getImapStream());
801
    }
802
803
    /**
804
     * Retrieve the quota settings per user.
805
     *
806
     * @return array - FALSE in the case of call failure
807
     */
808
    protected function getQuota()
809
    {
810
        return imap_get_quotaroot($this->getImapStream(), 'INBOX');
811
    }
812
813
    /**
814
     * Return quota limit in KB.
815
     *
816
     * @return int - FALSE in the case of call failure
817
     */
818
    public function getQuotaLimit()
819
    {
820
        $quota = $this->getQuota();
821
        if (is_array($quota)) {
822
            $quota = $quota['STORAGE']['limit'];
823
        }
824
825
        return $quota;
826
    }
827
828
    /**
829
     * Return quota usage in KB.
830
     *
831
     * @return int - FALSE in the case of call failure
832
     */
833
    public function getQuotaUsage()
834
    {
835
        $quota = $this->getQuota();
836
        if (is_array($quota)) {
837
            $quota = $quota['STORAGE']['usage'];
838
        }
839
840
        return $quota;
841
    }
842
843
    /**
844
     * Get mail data.
845
     *
846
     * @param      $mailId
847
     * @param bool $markAsSeen
848
     *
849
     * @return Message
850
     */
851
    public function getMail($mailId, $markAsSeen = true)
852
    {
853
        $header     = imap_fetchheader($this->getImapStream(), $mailId, FT_UID);
854
        $headObject = imap_rfc822_parse_headers($header);
855
856
        $mail           = new Message();
857
        $mail->id       = $mailId;
858
        $mail->date     = date('Y-m-d H:i:s', isset($headObject->date) ? strtotime(preg_replace('/\(.*?\)/', '', $headObject->date)) : time());
859
        $mail->subject  = isset($headObject->subject) ? $this->decodeMimeStr($headObject->subject, $this->serverEncoding) : null;
860
        $mail->fromName = isset($headObject->from[0]->personal) ? $this->decodeMimeStr($headObject->from[0]->personal, $this->serverEncoding)
861
            : null;
862
        $mail->fromAddress = strtolower($headObject->from[0]->mailbox.'@'.$headObject->from[0]->host);
863
864
        if (isset($headObject->to)) {
865
            $toStrings = [];
866
            foreach ($headObject->to as $to) {
867
                if (!empty($to->mailbox) && !empty($to->host)) {
868
                    $toEmail            = strtolower($to->mailbox.'@'.$to->host);
869
                    $toName             = isset($to->personal) ? $this->decodeMimeStr($to->personal, $this->serverEncoding) : null;
870
                    $toStrings[]        = $toName ? "$toName <$toEmail>" : $toEmail;
871
                    $mail->to[$toEmail] = $toName;
872
                }
873
            }
874
            $mail->toString = implode(', ', $toStrings);
875
        }
876
877
        if (isset($headObject->cc)) {
878
            foreach ($headObject->cc as $cc) {
879
                $mail->cc[strtolower($cc->mailbox.'@'.$cc->host)] = isset($cc->personal) ? $this->decodeMimeStr($cc->personal, $this->serverEncoding)
880
                    : null;
881
            }
882
        }
883
884
        if (isset($headObject->reply_to)) {
885
            foreach ($headObject->reply_to as $replyTo) {
886
                $mail->replyTo[strtolower($replyTo->mailbox.'@'.$replyTo->host)] = isset($replyTo->personal) ? $this->decodeMimeStr(
887
                    $replyTo->personal,
888
                    $this->serverEncoding
889
                ) : null;
890
            }
891
        }
892
893
        if (isset($headObject->in_reply_to)) {
894
            $mail->inReplyTo = $headObject->in_reply_to;
895
        }
896
897
        if (isset($headObject->return_path)) {
898
            $mail->returnPath = $headObject->return_path;
899
        }
900
901
        if (isset($headObject->references)) {
902
            $mail->references = explode("\n", $headObject->references);
903
        }
904
905
        $mailStructure = imap_fetchstructure($this->getImapStream(), $mailId, FT_UID);
906
907
        if (empty($mailStructure->parts)) {
908
            $this->initMailPart($mail, $mailStructure, 0, $markAsSeen);
909
        } else {
910
            foreach ($mailStructure->parts as $partNum => $partStructure) {
911
                $this->initMailPart($mail, $partStructure, $partNum + 1, $markAsSeen);
912
            }
913
        }
914
915
        // Parse X headers
916
        $tempArray = explode("\n", $header);
917
        if (is_array($tempArray) && count($tempArray)) {
918
            $headers = [];
919
            foreach ($tempArray as $line) {
920
                if (preg_match('/^X-(.*?): (.*?)$/is', trim($line), $matches)) {
921
                    $headers['x-'.strtolower($matches[1])] = $matches[2];
922
                }
923
            }
924
            $mail->xHeaders = $headers;
925
        }
926
927
        return $mail;
928
    }
929
930
    /**
931
     * @param            $partStructure
932
     * @param            $partNum
933
     * @param bool|true  $markAsSeen
934
     * @param bool|false $isDsn
935
     * @param bool|false $isFbl
936
     */
937
    protected function initMailPart(Message $mail, $partStructure, $partNum, $markAsSeen = true, $isDsn = false, $isFbl = false)
938
    {
939
        $options = FT_UID;
940
        if (!$markAsSeen) {
941
            $options |= FT_PEEK;
942
        }
943
        $data = $partNum
944
            ? imap_fetchbody($this->getImapStream(), $mail->id, $partNum, $options)
945
            : imap_body(
946
                $this->getImapStream(),
947
                $mail->id,
948
                $options
949
            );
950
951
        if (1 == $partStructure->encoding) {
952
            $data = imap_utf8($data);
953
        } elseif (2 == $partStructure->encoding) {
954
            $data = imap_binary($data);
955
        } elseif (3 == $partStructure->encoding) {
956
            $data = imap_base64($data);
957
        } elseif (4 == $partStructure->encoding) {
958
            $data = quoted_printable_decode($data);
959
        }
960
961
        $params = $this->getParameters($partStructure);
962
963
        // attachments
964
        $attachmentId = $partStructure->ifid
965
            ? trim($partStructure->id, ' <>')
966
            : (isset($params['filename']) || isset($params['name']) ? mt_rand().mt_rand() : null);
967
968
        if ($attachmentId) {
969
            if (isset($this->settings['use_attachments']) && $this->settings['use_attachments']) {
970
                if (empty($params['filename']) && empty($params['name'])) {
971
                    $fileName = $attachmentId.'.'.strtolower($partStructure->subtype);
972
                } else {
973
                    $fileName = !empty($params['filename']) ? $params['filename'] : $params['name'];
974
                    $fileName = $this->decodeMimeStr($fileName, $this->serverEncoding);
975
                    $fileName = $this->decodeRFC2231($fileName, $this->serverEncoding);
976
                }
977
                $attachment       = new Attachment();
978
                $attachment->id   = $attachmentId;
979
                $attachment->name = $fileName;
980
                if ($this->attachmentsDir) {
981
                    $replace = [
982
                        '/\s/'                   => '_',
983
                        '/[^0-9a-zа-яіїє_\.]/iu' => '',
984
                        '/_+/'                   => '_',
985
                        '/(^_)|(_$)/'            => '',
986
                    ];
987
                    $fileSysName = preg_replace(
988
                        '~[\\\\/]~',
989
                        '',
990
                        $mail->id.'_'.$attachmentId.'_'.preg_replace(array_keys($replace), $replace, $fileName)
991
                    );
992
                    $attachment->filePath = $this->attachmentsDir.DIRECTORY_SEPARATOR.$fileSysName;
993
                    file_put_contents($attachment->filePath, $data);
994
                }
995
                $mail->addAttachment($attachment);
996
            }
997
        } else {
998
            if (!empty($params['charset'])) {
999
                $data = $this->convertStringEncoding($data, $params['charset'], $this->serverEncoding);
1000
            }
1001
1002
            if (!empty($data)) {
1003
                $subtype = !empty($partStructure->ifsubtype)
1004
                    ? strtolower($partStructure->subtype)
1005
                    : '';
1006
                switch ($partStructure->type) {
1007
                    case TYPETEXT:
1008
                        switch ($subtype) {
1009
                            case 'plain':
1010
                                $mail->textPlain .= $data;
1011
                                break;
1012
                            case 'html':
1013
                            default:
1014
                                $mail->textHtml .= $data;
1015
                        }
1016
                        break;
1017
                    case TYPEMULTIPART:
1018
                        if (
1019
                            'report' != $subtype
1020
                            ||
1021
                            empty($params['report-type'])
1022
                        ) {
1023
                            break;
1024
                        }
1025
                        $reportType = strtolower($params['report-type']);
1026
                        switch ($reportType) {
1027
                            case 'delivery-status':
1028
                                $mail->dsnMessage = trim($data);
1029
                                $isDsn            = true;
1030
                                break;
1031
                            case 'feedback-report':
1032
                                $mail->fblMessage = trim($data);
1033
                                $isFbl            = true;
1034
                                break;
1035
                            default:
1036
                                // Just pass through.
1037
                        }
1038
                        break;
1039
                    case TYPEMESSAGE:
1040
                        if ($isDsn || ('delivery-status' == $subtype)) {
1041
                            $mail->dsnReport = $data;
1042
                        } elseif ($isFbl || ('feedback-report' == $subtype)) {
1043
                            $mail->fblReport = $data;
1044
                        } else {
1045
                            $mail->textPlain .= trim($data);
1046
                        }
1047
                        break;
1048
                    default:
1049
                        // Just pass through.
1050
                }
1051
            }
1052
        }
1053
        if (!empty($partStructure->parts)) {
1054
            foreach ($partStructure->parts as $subPartNum => $subPartStructure) {
1055
                if (2 == $partStructure->type && 'RFC822' == $partStructure->subtype) {
1056
                    $this->initMailPart($mail, $subPartStructure, $partNum, $markAsSeen, $isDsn, $isFbl);
1057
                } else {
1058
                    $this->initMailPart($mail, $subPartStructure, $partNum.'.'.($subPartNum + 1), $markAsSeen, $isDsn, $isFbl);
1059
                }
1060
            }
1061
        }
1062
    }
1063
1064
    /**
1065
     * @param $partStructure
1066
     *
1067
     * @return array
1068
     */
1069
    protected function getParameters($partStructure)
1070
    {
1071
        $params = [];
1072
        if (!empty($partStructure->parameters)) {
1073
            foreach ($partStructure->parameters as $param) {
1074
                $params[strtolower($param->attribute)] = $param->value;
1075
            }
1076
        }
1077
        if (!empty($partStructure->dparameters)) {
1078
            foreach ($partStructure->dparameters as $param) {
1079
                $paramName = strtolower(preg_match('~^(.*?)\*~', $param->attribute, $matches) ? $matches[1] : $param->attribute);
1080
                if (isset($params[$paramName])) {
1081
                    $params[$paramName] .= $param->value;
1082
                } else {
1083
                    $params[$paramName] = $param->value;
1084
                }
1085
            }
1086
        }
1087
1088
        return $params;
1089
    }
1090
1091
    /**
1092
     * @param        $string
1093
     * @param string $charset
1094
     *
1095
     * @return string
1096
     */
1097
    protected function decodeMimeStr($string, $charset = 'utf-8')
1098
    {
1099
        $newString = '';
1100
        $elements  = imap_mime_header_decode($string);
1101
        for ($i = 0; $i < count($elements); ++$i) {
1102
            if ('default' == $elements[$i]->charset) {
1103
                $elements[$i]->charset = 'iso-8859-1';
1104
            }
1105
            $newString .= $this->convertStringEncoding($elements[$i]->text, $elements[$i]->charset, $charset);
1106
        }
1107
1108
        return $newString;
1109
    }
1110
1111
    /**
1112
     * @param $string
1113
     *
1114
     * @return bool
1115
     */
1116
    protected function isUrlEncoded($string)
1117
    {
1118
        $hasInvalidChars = preg_match('#[^%a-zA-Z0-9\-_\.\+]#', $string);
1119
        $hasEscapedChars = preg_match('#%[a-zA-Z0-9]{2}#', $string);
1120
1121
        return !$hasInvalidChars && $hasEscapedChars;
1122
    }
1123
1124
    /**
1125
     * @param        $string
1126
     * @param string $charset
1127
     *
1128
     * @return string
1129
     */
1130
    protected function decodeRFC2231($string, $charset = 'utf-8')
1131
    {
1132
        if (preg_match("/^(.*?)'.*?'(.*?)$/", $string, $matches)) {
1133
            $encoding = $matches[1];
1134
            $data     = $matches[2];
1135
            if ($this->isUrlEncoded($data)) {
1136
                $string = $this->convertStringEncoding(urldecode($data), $encoding, $charset);
1137
            }
1138
        }
1139
1140
        return $string;
1141
    }
1142
1143
    /**
1144
     * Converts a string from one encoding to another.
1145
     *
1146
     * @param string $string
1147
     * @param string $fromEncoding
1148
     * @param string $toEncoding
1149
     *
1150
     * @return string Converted string if conversion was successful, or the original string if not
1151
     */
1152
    protected function convertStringEncoding($string, $fromEncoding, $toEncoding)
1153
    {
1154
        $convertedString = null;
1155
        if ($string && $fromEncoding != $toEncoding) {
1156
            $convertedString = @iconv($fromEncoding, $toEncoding.'//IGNORE', $string);
1157
            if (!$convertedString && extension_loaded('mbstring')) {
1158
                $convertedString = @mb_convert_encoding($string, $toEncoding, $fromEncoding);
1159
            }
1160
        }
1161
1162
        return $convertedString ?: $string;
1163
    }
1164
1165
    /**
1166
     * Close IMAP connection.
1167
     */
1168
    protected function disconnect()
1169
    {
1170
        if ($this->isConnected()) {
1171
            // Prevent these from throwing notices such as "SECURITY PROBLEM: insecure server advertised"
1172
            imap_errors();
1173
            imap_alerts();
1174
1175
            @imap_close($this->imapStream, CL_EXPUNGE);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for imap_close(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

1175
            /** @scrutinizer ignore-unhandled */ @imap_close($this->imapStream, CL_EXPUNGE);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1176
        }
1177
    }
1178
1179
    private function createAttachmentsDir(PathsHelper $pathsHelper)
1180
    {
1181
        if (!isset($this->settings['use_attachments']) || !$this->settings['use_attachments']) {
1182
            return;
1183
        }
1184
1185
        $this->attachmentsDir = $pathsHelper->getSystemPath('tmp', true);
1186
1187
        if (!file_exists($this->attachmentsDir)) {
1188
            mkdir($this->attachmentsDir);
1189
        }
1190
        $this->attachmentsDir .= '/attachments';
1191
1192
        if (!file_exists($this->attachmentsDir)) {
1193
            mkdir($this->attachmentsDir);
1194
        }
1195
    }
1196
1197
    /**
1198
     * Disconnect on destruct.
1199
     */
1200
    public function __destruct()
1201
    {
1202
        $this->disconnect();
1203
    }
1204
}
1205