Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * The Mail Pickup Manager.
19
 *
20
 * @package    tool_messageinbound
21
 * @copyright  2014 Andrew Nicols
22
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
namespace tool_messageinbound;
26
 
27
/**
28
 * Mail Pickup Manager.
29
 *
30
 * @copyright  2014 Andrew Nicols
31
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
32
 */
33
class manager {
34
 
35
    /**
36
     * @var string The main mailbox to check.
37
     */
38
    const MAILBOX = 'INBOX';
39
 
40
    /**
41
     * @var string The mailbox to store messages in when they are awaiting confirmation.
42
     */
43
    const CONFIRMATIONFOLDER = 'tobeconfirmed';
44
 
45
    /**
46
     * @var string The flag for seen/read messages.
47
     */
48
    const MESSAGE_SEEN = '\seen';
49
 
50
    /**
51
     * @var string The flag for flagged messages.
52
     */
53
    const MESSAGE_FLAGGED = '\flagged';
54
 
55
    /**
56
     * @var string The flag for deleted messages.
57
     */
58
    const MESSAGE_DELETED = '\deleted';
59
 
60
    /**
61
     * @var \string IMAP folder namespace.
62
     */
63
    protected $imapnamespace = null;
64
 
65
    /**
66
     * @var \rcube_imap_generic A reference to the IMAP client.
67
     */
68
    protected $client = null;
69
 
70
    /**
71
     * @var \core\message\inbound\address_manager A reference to the Inbound Message Address Manager instance.
72
     */
73
    protected $addressmanager = null;
74
 
75
    /**
76
     * @var \stdClass The data for the current message being processed.
77
     */
78
    protected $currentmessagedata = null;
79
 
80
    /**
81
     * Mail Pickup Manager.
82
     */
83
    public function __construct() {
84
        // Load dependencies.
85
        $this->load_dependencies();
86
    }
87
 
88
    /**
89
     * Retrieve the connection to the IMAP client.
90
     *
91
     * @param string $mailbox The mailbox to connect to.
92
     *
93
     * @return bool Whether a connection was successfully established.
94
     */
95
    protected function get_imap_client(
96
        string $mailbox = self::MAILBOX,
97
    ): bool {
98
        global $CFG;
99
 
100
        if (!\core\message\inbound\manager::is_enabled()) {
101
            // E-mail processing not set up.
102
            mtrace("Inbound Message not fully configured - exiting early.");
103
            return false;
104
        }
105
 
106
        mtrace("Connecting to {$CFG->messageinbound_host} as {$CFG->messageinbound_hostuser}...");
107
 
108
        $configuration = [
109
            'username' => $CFG->messageinbound_hostuser,
110
            'password' => $CFG->messageinbound_hostpass,
111
            'hostspec' => $CFG->messageinbound_host,
112
            'options' => [
113
                'ssl_mode' => strtolower($CFG->messageinbound_hostssl),
114
                'auth_type' => 'CHECK',
115
            ],
116
        ];
117
 
118
        if (strpos($configuration['hostspec'], ':')) {
119
            $hostdata = explode(':', $configuration['hostspec']);
120
            if (count($hostdata) === 2) {
121
                // A hostname in the format hostname:port has been provided.
122
                $configuration['hostspec'] = $hostdata[0];
123
                $configuration['options']['port'] = $hostdata[1];
124
            }
125
        }
126
 
127
        // XOAUTH2.
128
        if (isset($CFG->messageinbound_hostoauth) && $CFG->messageinbound_hostoauth != '') {
129
            // Get the issuer.
130
            $issuer = \core\oauth2\api::get_issuer($CFG->messageinbound_hostoauth);
131
            // Validate the issuer and check if it is enabled or not.
132
            if ($issuer && $issuer->get('enabled')) {
133
                // Get the OAuth Client.
134
                if ($oauthclient = \core\oauth2\api::get_system_oauth_client($issuer)) {
135
                    $configuration['password'] = 'Bearer ' . $oauthclient->get_accesstoken()->token;
136
                    $configuration['options']['auth_type'] = 'XOAUTH2';
137
                }
138
            }
139
        }
140
 
141
        $this->client = new \rcube_imap_generic();
142
        if (!empty($CFG->debugimap)) {
143
            $this->client->setDebug(debug: true);
144
        }
145
        $success = $this->client->connect(
146
            host: $configuration['hostspec'],
147
            user: $configuration['username'],
148
            password: $configuration['password'],
149
            options: $configuration['options'],
150
        );
151
 
152
        if ($success) {
153
            mtrace("Connection established.");
154
 
155
            // Ensure that mailboxes exist.
156
            $this->ensure_mailboxes_exist();
157
            // Select mailbox.
158
            $this->select_mailbox(mailbox: $mailbox);
159
            return true;
160
        } else {
161
            throw new \moodle_exception('imapconnectfailure', 'tool_messageinbound', '', null, 'Could not connect to IMAP server.');
162
        }
163
    }
164
 
165
    /**
166
     * Shutdown and close the connection to the IMAP client.
167
     */
168
    protected function close_connection(): void {
169
        if ($this->client) {
170
            // Close the connection and return to authenticated state.
171
            $isclosed = $this->client->close();
172
            if ($isclosed) {
173
                // Connection was closed unsuccessfully. Send the LOGOUT command and close the socket.
174
                $this->client->closeConnection();
175
            }
176
        }
177
        $this->client = null;
178
    }
179
 
180
    /**
181
     * Get the confirmation folder imap name
182
     *
183
     * @return string
184
     */
185
    protected function get_confirmation_folder(): string {
186
        if ($this->imapnamespace === null) {
187
            $namespaces = $this->client->getNamespace();
188
            if ($namespaces != $this->client::ERROR_BAD && is_array($namespaces)) {
189
                $nspersonal = reset($namespaces['personal']);
190
                if (is_array($nspersonal) && !empty($nspersonal[0])) {
191
                    // Personal namespace is an array, the first part is the name, the second part is the delimiter.
192
                    $this->imapnamespace = $nspersonal[0] . $nspersonal[1];
193
                } else {
194
                    $this->imapnamespace = '';
195
                }
196
            } else {
197
                $this->imapnamespace = '';
198
            }
199
        }
200
 
201
        return $this->imapnamespace . self::CONFIRMATIONFOLDER;
202
    }
203
 
204
    /**
205
     * Get the current mailbox name.
206
     *
207
     * @return string The current mailbox name.
208
     * @throws \core\message\inbound\processing_failed_exception if the mailbox could not be opened.
209
     */
210
    protected function get_mailbox(): string {
211
        // Get the current mailbox.
212
        if ($this->client->selected) {
213
            return $this->client->selected;
214
        } else {
215
            throw new \core\message\inbound\processing_failed_exception('couldnotopenmailbox', 'tool_messageinbound');
216
        }
217
    }
218
 
219
    /**
220
     * Execute the main Inbound Message pickup task.
221
     *
222
     * @return bool
223
     */
224
    public function pickup_messages(): bool {
225
        if (!$this->get_imap_client()) {
226
            return false;
227
        }
228
 
229
        // Restrict results to messages which are unseen, and have not been flagged.
230
        mtrace("Searching for Unseen, Unflagged email in the folder '" . self::MAILBOX . "'");
231
        $result = $this->client->search(
232
            mailbox: $this->get_mailbox(),
233
            criteria: 'UNSEEN UNFLAGGED',
234
            return_uid: true,
235
        );
236
 
237
        if (empty($result->count())) {
238
            return false;
239
        }
240
 
241
        mtrace("Found " . $result->count() . " messages to parse. Parsing...");
242
        // Retrieve the message id.
243
        $messages = $result->get();
244
        $this->addressmanager = new \core\message\inbound\address_manager();
245
        foreach ($messages as $messageuid) {
246
            $messageuid = is_numeric($messageuid) ? intval($messageuid) : $messageuid;
247
            $this->process_message(messageuid: $messageuid);
248
        }
249
 
250
        // Close the client connection.
251
        $this->close_connection();
252
 
253
        return true;
254
    }
255
 
256
    /**
257
     * Process a message received and validated by the Inbound Message processor.
258
     *
259
     * @param \stdClass $maildata The data retrieved from the database for the current record.
260
     * @return bool Whether the message was successfully processed.
261
     * @throws \core\message\inbound\processing_failed_exception if the message cannot be found.
262
     */
263
    public function process_existing_message(
264
        \stdClass $maildata,
265
    ): bool {
266
        // Grab the new IMAP client.
267
        if (!$this->get_imap_client(mailbox: $this->get_confirmation_folder())) {
268
            return false;
269
        }
270
 
271
        // When dealing with Inbound Message messages, we mark them as flagged and seen. Restrict the search to those criterion.
272
        mtrace("Searching for a Seen, Flagged message in the folder '" . $this->get_confirmation_folder() . "'");
273
 
274
        // Build the search.
275
        $result = $this->client->search(
276
            mailbox: $this->get_mailbox(),
277
            criteria: 'SEEN FLAGGED TO "' . $maildata->address . '"',
278
            return_uid: true,
279
        );
280
 
281
        $this->addressmanager = new \core\message\inbound\address_manager();
282
        if (!empty($result->count())) {
283
            $messages = $result->get();
284
            $targetsequence = 0;
285
            mtrace("Found " . $result->count() . " messages to parse. Parsing...");
286
            foreach ($messages as $messageuid) {
287
                $messageuid = is_numeric($messageuid) ? intval($messageuid) : $messageuid;
288
                $results = $this->client->fetch(
289
                    mailbox: $this->get_mailbox(),
290
                    message_set: $messageuid,
291
                    is_uid: true,
292
                    query_items: [
293
                        'BODY.PEEK[HEADER.FIELDS (Message-ID)]',
294
                    ],
295
                );
296
                $messagedata = reset($results);
297
                // Match the message id.
298
                if (htmlentities($messagedata->get('Message-ID', false)) == $maildata->messageid) {
299
                    // Found the message.
300
                    $targetsequence = $messageuid;
301
                    break;
302
                }
303
            }
304
            mtrace("--> Found the message. Passing back to the pickup system.");
305
 
306
            // Process the message.
307
            $this->process_message(
308
                messageuid: $targetsequence,
309
                viewreadmessages: true,
310
                skipsenderverification: true,
311
            );
312
 
313
            // Close the client connection.
314
            $this->close_connection();
315
 
316
            mtrace("============================================================================");
317
            return true;
318
        } else {
319
            // Close the client connection.
320
            $this->close_connection();
321
 
322
            mtrace("============================================================================");
323
            throw new \core\message\inbound\processing_failed_exception('oldmessagenotfound', 'tool_messageinbound');
324
        }
325
    }
326
 
327
    /**
328
     * Tidy up old messages in the confirmation folder.
329
     *
330
     * @return bool Whether tidying occurred successfully.
331
     */
332
    public function tidy_old_messages(): bool {
333
        // Grab the new IMAP client.
334
        if (!$this->get_imap_client()) {
335
            return false;
336
        }
337
        // Switch to the confirmation folder.
338
        $this->select_mailbox(mailbox: $this->get_confirmation_folder());
339
 
340
        // Open the mailbox.
341
        mtrace("Searching for messages older than 24 hours in the '" .
342
                $this->get_confirmation_folder() . "' folder.");
343
 
344
        // Delete messages older than 24 hours old.
345
        $date = date(
346
            format: 'Y-m-d',
347
            timestamp: time() - DAYSECS
348
        );
349
 
350
        // Retrieve the messages and mark them for removal.
351
        $result = $this->client->search(
352
            mailbox: $this->get_mailbox(),
353
            criteria: 'BEFORE "' . $date . '"',
354
            return_uid: true,
355
        );
356
 
357
        if (empty($result->count())) {
358
            $this->close_connection();
359
            return false;
360
        }
361
 
362
        mtrace("Found " . $result->count() . " messages for removal.");
363
        $messages = $result->get();
364
        foreach ($messages as $messageuid) {
365
            $messageuid = is_numeric($messageuid) ? intval($messageuid) : $messageuid;
366
            $this->add_flag_to_message(
367
                messageuid: $messageuid,
368
                flag: self::MESSAGE_DELETED
369
            );
370
        }
371
 
372
        mtrace("Finished removing messages.");
373
 
374
        $this->close_connection();
375
 
376
        return true;
377
    }
378
 
379
    /**
380
     * Remove older verification failures.
381
     *
382
     * @return void
383
     */
384
    public function tidy_old_verification_failures() {
385
        global $DB;
386
        $DB->delete_records_select('messageinbound_messagelist', 'timecreated < :time', ['time' => time() - DAYSECS]);
387
    }
388
 
389
    /**
390
     * Process a message and pass it through the Inbound Message handling systems.
391
     *
392
     * @param int $messageuid The message uid to process
393
     * @param bool $viewreadmessages Whether to also look at messages which have been marked as read
394
     * @param bool $skipsenderverification Whether to skip the sender verification stage
395
     */
396
    public function process_message(
397
        int $messageuid,
398
        bool $viewreadmessages = false,
399
        bool $skipsenderverification = false,
400
    ): void {
401
        global $USER;
402
 
403
        mtrace("- Parsing message " . $messageuid);
404
 
405
        // First flag this message to prevent another running hitting this message while we look at the headers.
406
        $this->add_flag_to_message(
407
            messageuid: $messageuid,
408
            flag: self::MESSAGE_FLAGGED,
409
        );
410
 
411
        if ($this->is_bulk_message(messageuid: $messageuid)) {
412
            mtrace("- The message " . $messageuid . " has a bulk header set. This is likely an auto-generated reply - discarding.");
413
            return;
414
        }
415
 
416
        // Record the user that this script is currently being run as.  This is important when re-processing existing
417
        // messages, as \core\cron::setup_user is called multiple times.
418
        $originaluser = $USER;
419
 
420
        $envelope = $this->client->fetch(
421
            mailbox: $this->get_mailbox(),
422
            message_set: $messageuid,
423
            is_uid: true,
424
            query_items: [
425
                'BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO)]',
426
                'ENVELOPE',
427
            ],
428
        );
429
        $envelope = array_shift($envelope);
430
        $recipients = $this->get_address_from_envelope(addresslist: $envelope->envelope[5]);
431
        foreach ($recipients as $recipient) {
432
            if (!\core\message\inbound\address_manager::is_correct_format($recipient)) {
433
                // Message did not contain a subaddress.
434
                mtrace("- Recipient '{$recipient}' did not match Inbound Message headers.");
435
                continue;
436
            }
437
 
438
            // Message contained a match.
439
            $senders = $this->get_address_from_envelope(addresslist: $envelope->envelope[2]);
440
            if (count($senders) !== 1) {
441
                mtrace("- Received multiple senders. Only the first sender will be used.");
442
            }
443
            $sender = array_shift($senders);
444
 
445
            mtrace("-- Subject:\t"      . $envelope->subject);
446
            mtrace("-- From:\t"         . $sender);
447
            mtrace("-- Recipient:\t"    . $recipient);
448
 
449
            // Check whether this message has already been processed.
450
            if (
451
                !$viewreadmessages &&
452
                $this->message_has_flag(
453
                    messageuid: $messageuid,
454
                    flag: self::MESSAGE_SEEN,
455
                )
456
            ) {
457
                // Something else has already seen this message. Skip it now.
458
                mtrace("-- Skipping the message - it has been marked as seen - perhaps by another process.");
459
                continue;
460
            }
461
 
462
            // Mark it as read to lock the message.
463
            $this->add_flag_to_message(
464
                messageuid: $messageuid,
465
                flag: self::MESSAGE_SEEN,
466
            );
467
 
468
            // Now pass it through the Inbound Message processor.
469
            $status = $this->addressmanager->process_envelope($recipient, $sender);
470
 
471
            if (($status & ~ \core\message\inbound\address_manager::VALIDATION_DISABLED_HANDLER) !== $status) {
472
                // The handler is disabled.
473
                mtrace("-- Skipped message - Handler is disabled. Fail code {$status}");
474
                // In order to handle the user error, we need more information about the message being failed.
475
                $this->process_message_data(
476
                    envelope: $envelope,
477
                    messageuid: $messageuid,
478
                );
479
                $this->inform_user_of_error(get_string('handlerdisabled', 'tool_messageinbound', $this->currentmessagedata));
480
                return;
481
            }
482
 
483
            // Check the validation status early. No point processing garbage messages, but we do need to process it
484
            // for some validation failure types.
485
            if (!$this->passes_key_validation(status: $status)) {
486
                // None of the above validation failures were found. Skip this message.
487
                mtrace("-- Skipped message - it does not appear to relate to a Inbound Message pickup. Fail code {$status}");
488
 
489
                // Remove the seen flag from the message as there may be multiple recipients.
490
                $this->remove_flag_from_message(
491
                    messageuid: $messageuid,
492
                    flag: self::MESSAGE_SEEN,
493
                );
494
 
495
                // Skip further processing for this recipient.
496
                continue;
497
            }
498
 
499
            // Process the message as the user.
500
            $user = $this->addressmanager->get_data()->user;
501
            mtrace("-- Processing the message as user {$user->id} ({$user->username}).");
502
            \core\cron::setup_user($user);
503
 
504
            // Process and retrieve the message data for this message.
505
            // This includes fetching the full content, as well as all headers, and attachments.
506
            if (
507
                !$this->process_message_data(
508
                    envelope: $envelope,
509
                    messageuid: $messageuid,
510
                )
511
            ) {
512
                mtrace("--- Message could not be found on the server. Is another process removing messages?");
513
                return;
514
            }
515
 
516
            // When processing validation replies, we need to skip the sender verification phase as this has been
517
            // manually completed.
518
            if (!$skipsenderverification && $status !== 0) {
519
                // Check the validation status for failure types which require confirmation.
520
                // The validation result is tested in a bitwise operation.
521
                mtrace("-- Message did not meet validation but is possibly recoverable. Fail code {$status}");
522
                // This is a recoverable error, but requires user input.
523
 
524
                if (
525
                    $this->handle_verification_failure(
526
                        messageuid: $messageuid,
527
                        recipient: $recipient,
528
                    )
529
                ) {
530
                    mtrace("--- Original message retained on mail server and confirmation message sent to user.");
531
                } else {
532
                    mtrace("--- Invalid Recipient Handler - unable to save. Informing the user of the failure.");
533
                    $this->inform_user_of_error(get_string('invalidrecipientfinal', 'tool_messageinbound', $this->currentmessagedata));
534
                }
535
 
536
                // Returning to normal cron user.
537
                mtrace("-- Returning to the original user.");
538
                \core\cron::setup_user($originaluser);
539
                return;
540
            }
541
 
542
            // Add the content and attachment data.
543
            mtrace("-- Validation completed. Fetching rest of message content.");
544
            $this->process_message_data_body(messageuid: $messageuid);
545
 
546
            // The message processor throws exceptions upon failure. These must be caught and notifications sent to
547
            // the user here.
548
            try {
549
                $result = $this->send_to_handler();
550
            } catch (\core\message\inbound\processing_failed_exception $e) {
551
                // We know about these kinds of errors and they should result in the user being notified of the
552
                // failure. Send the user a notification here.
553
                $this->inform_user_of_error($e->getMessage());
554
 
555
                // Returning to normal cron user.
556
                mtrace("-- Returning to the original user.");
557
                \core\cron::setup_user($originaluser);
558
                return;
559
            } catch (\Exception $e) {
560
                // An unknown error occurred. The user is not informed, but the administrator is.
561
                mtrace("-- Message processing failed. An unexpected exception was thrown. Details follow.");
562
                mtrace($e->getMessage());
563
 
564
                // Returning to normal cron user.
565
                mtrace("-- Returning to the original user.");
566
                \core\cron::setup_user($originaluser);
567
                return;
568
            }
569
 
570
            if ($result) {
571
                // Handle message cleanup. Messages are deleted once fully processed.
572
                mtrace("-- Marking the message for removal.");
573
                $this->add_flag_to_message(
574
                    messageuid: $messageuid,
575
                    flag: self::MESSAGE_DELETED
576
                );
577
            } else {
578
                mtrace("-- The Inbound Message processor did not return a success status. Skipping message removal.");
579
            }
580
 
581
            // Returning to normal cron user.
582
            mtrace("-- Returning to the original user.");
583
            \core\cron::setup_user($originaluser);
584
 
585
            mtrace("-- Finished processing " . $messageuid);
586
 
587
            // Skip the outer loop too. The message has already been processed and it could be possible for there to
588
            // be two recipients in the envelope which match somehow.
589
            return;
590
        }
591
    }
592
 
593
    /**
594
     * Process a message to retrieve it's header data without body.
595
     *
596
     * @param \rcube_message_header $envelope The Envelope of the message
597
     * @param int $messageuid The message Uid to process
598
     * @return \stdClass|null The current value of the messagedata
599
     */
600
    private function process_message_data(
601
        \rcube_message_header $envelope,
602
        int $messageuid,
603
    ): ?\stdClass {
604
        // Retrieve the message with necessary information.
605
        $messages = $this->client->fetch(
606
            mailbox: $this->get_mailbox(),
607
            message_set: $messageuid,
608
            is_uid: true,
609
            query_items: [
610
                'BODY.PEEK[HEADER.FIELDS (Message-ID SUBJECT DATE)]',
611
            ],
612
        );
613
        $messagedata = reset($messages);
614
 
615
        if (!$messagedata) {
616
            // Message was not found! Somehow it has been removed or is no longer returned.
617
            return null;
618
        }
619
 
620
        // The message ID should always be in the first part.
621
        $data = new \stdClass();
622
        $data->messageid = htmlentities($messagedata->get('Message-ID', false));
623
        $data->subject = $messagedata->get('SUBJECT', false);
624
        $data->timestamp = strtotime($messagedata->get('DATE', false));
625
        $data->envelope = $envelope;
626
        $data->data = $this->addressmanager->get_data();
627
 
628
        $this->currentmessagedata = $data;
629
 
630
        return $this->currentmessagedata;
631
    }
632
 
633
    /**
634
     * Process a message again to add body and attachment data.
635
     *
636
     * @param int $messageuid The message Uid
637
     * @return \stdClass|null The current value of the messagedata
638
     */
639
    private function process_message_data_body(
640
        int $messageuid,
641
    ): ?\stdClass {
642
        $messages = $this->client->fetch(
643
            mailbox: $this->get_mailbox(),
644
            message_set: $messageuid,
645
            is_uid: true,
646
            query_items: [
647
                'BODYSTRUCTURE',
648
            ],
649
        );
650
        $messagedata = reset($messages);
651
        $structure = $messagedata->bodystructure;
652
 
653
        // Store the data for this message.
654
        $contentplain = '';
655
        $contenthtml = '';
656
        $attachments = [
657
            'inline' => [],
658
            'attachment' => [],
659
        ];
660
        $parameters = [];
661
        foreach ($structure as $partno => $part) {
662
            if (!is_array($part)) {
663
                continue;
664
            }
665
            $section = $partno + 1;
666
 
667
            // Subpart recursion.
668
            if (is_array($part[0])) {
669
                foreach ($part as $subpartno => $subpart) {
670
                    if (!is_array($subpart)) {
671
                        continue;
672
                    }
673
                    $subsection = $subpartno + 1;
674
                    $this->process_message_data_body_part(
675
                        messageuid: $messageuid,
676
                        partstructure: $subpart,
677
                        section: $section . '.' . $subsection,
678
                        contentplain: $contentplain,
679
                        contenthtml: $contenthtml,
680
                        attachments: $attachments,
681
                        parameters: $parameters,
682
                    );
683
                }
684
            } else {
685
                $this->process_message_data_body_part(
686
                    messageuid: $messageuid,
687
                    partstructure: $part,
688
                    section: $section,
689
                    contentplain: $contentplain,
690
                    contenthtml: $contenthtml,
691
                    attachments: $attachments,
692
                    parameters: $parameters,
693
                );
694
            }
695
        }
696
 
697
        // The message ID should always be in the first part.
698
        $this->currentmessagedata->plain = $contentplain;
699
        $this->currentmessagedata->html = $contenthtml;
700
        $this->currentmessagedata->attachments = $attachments;
701
 
702
        return $this->currentmessagedata;
703
    }
704
 
705
    /**
706
     * Process message data body part.
707
     *
708
     * @param int $messageuid Message uid to process.
709
     * @param array $partstructure Body part structure.
710
     * @param string $section Section number.
711
     * @param string $contentplain Plain text content.
712
     * @param string $contenthtml HTML content.
713
     * @param array $attachments Attachments.
714
     * @param array $parameters Parameters.
715
     */
716
    private function process_message_data_body_part(
717
        int $messageuid,
718
        array $partstructure,
719
        string $section,
720
        string &$contentplain,
721
        string &$contenthtml,
722
        array &$attachments,
723
        array &$parameters,
724
    ): void {
725
        $messages = $this->client->fetch(
726
            mailbox: $this->get_mailbox(),
727
            message_set: $messageuid,
728
            is_uid: true,
729
            query_items: [
730
                'BODY[' . $section . ']',
731
            ],
732
        );
733
        if ($messages) {
734
            $messagedata = reset($messages);
735
 
736
            // Parse encoding.
737
            $encoding = array_search(
738
                needle: strtoupper($partstructure[5]),
739
                haystack: utils::get_body_encoding(),
740
            );
741
 
742
            // Parse subtype.
743
            $subtype = strtoupper($partstructure[1]);
744
 
745
            // Section part may be encoded, even plain text messages, so check everything.
746
            if ($encoding == utils::ENCQUOTEDPRINTABLE) {
747
                $data = quoted_printable_decode($messagedata->bodypart[$section]);
748
            } else if ($encoding == utils::ENCBASE64) {
749
                $data = base64_decode($messagedata->bodypart[$section]);
750
            } else {
751
                $data = $messagedata->bodypart[$section];
752
            }
753
 
754
            // Parse parameters.
755
            $parameters = $this->process_message_body_structure_parameters(
756
                attributes: $partstructure[2],
757
                parameters: $parameters,
758
            );
759
 
760
            // Parse content id.
761
            $contentid = '';
762
            if (!empty($partstructure[3])) {
763
                $contentid = htmlentities($partstructure[3]);
764
            }
765
 
766
            // Parse description.
767
            $description = '';
768
            if (!empty($partstructure[4])) {
769
                $description = $partstructure[4];
770
            }
771
 
772
            // Parse size of contents in bytes.
773
            $bytes = intval($partstructure[6]);
774
 
775
            // PLAIN text.
776
            if ($subtype == 'PLAIN') {
777
                $contentplain = $this->process_message_part_body(
778
                    bodycontent: $data,
779
                    charset: $parameters['CHARSET'],
780
                );
781
            }
782
            // HTML.
783
            if ($subtype == 'HTML') {
784
                $contenthtml = $this->process_message_part_body(
785
                    bodycontent: $data,
786
                    charset: $parameters['CHARSET'],
787
                );
788
            }
789
            // ATTACHMENT.
790
            if (isset($parameters['NAME']) || isset($parameters['FILENAME'])) {
791
                $filename = $parameters['NAME'] ?? $parameters['FILENAME'];
792
                if (
793
                    $attachment = $this->process_message_part_attachment(
794
                        filename: $filename,
795
                        filecontent: $data,
796
                        contentid: $contentid,
797
                        filesize: $bytes,
798
                        description: $description,
799
                    )
800
                ) {
801
                    // Parse disposition.
802
                    $disposition = null;
803
                    if (is_array($partstructure[8])) {
804
                        $disposition = strtolower($partstructure[8][0]);
805
                    }
806
                    $disposition = $disposition == 'inline' ? 'inline' : 'attachment';
807
                    $attachments[$disposition][] = $attachment;
808
                }
809
            }
810
        }
811
    }
812
 
813
    /**
814
     * Process message data body parameters.
815
     *
816
     * @param array $attributes List of attributes.
817
     * @param array $parameters List of parameters.
818
     * @return array
819
     */
820
    private function process_message_body_structure_parameters(
821
        array $attributes,
822
        array $parameters,
823
    ): array {
824
        if (empty($attributes)) {
825
            return [];
826
        }
827
 
828
        $attribute = null;
829
 
830
        foreach ($attributes as $value) {
831
            if (empty($attribute)) {
832
                $attribute = [
833
                    'attribute' => $value,
834
                    'value' => null,
835
                ];
836
            } else {
837
                $attribute['value'] = $value;
838
                $parameters[] = (object) $attribute;
839
                $attribute = null;
840
            }
841
        }
842
 
843
        $params = [];
844
        foreach ($parameters as $parameter) {
845
            if (isset($parameter->attribute)) {
846
                $params[$parameter->attribute] = $parameter->value;
847
            }
848
        }
849
 
850
        return $params;
851
    }
852
 
853
    /**
854
     * Process the message body content.
855
     *
856
     * @param string $bodycontent The message body.
857
     * @param string $charset The charset of the message body.
858
     * @return string Processed content.
859
     */
860
    private function process_message_part_body(
861
        string $bodycontent,
862
        string $charset,
863
    ): string {
864
        // This is a content section for the main body.
865
        // Convert the text from the current encoding to UTF8.
866
        $content = \core_text::convert($bodycontent, $charset);
867
 
868
        // Fix any invalid UTF8 characters.
869
        // Note: XSS cleaning is not the responsibility of this code. It occurs immediately before display when
870
        // format_text is called.
871
        $content = clean_param($content, PARAM_RAW);
872
 
873
        return $content;
874
    }
875
 
876
    /**
877
     * Process a message again to add body and attachment data.
878
     *
879
     * @param string $filename The filename of the attachment.
880
     * @param string $filecontent The content of the attachment.
881
     * @param string $contentid The content id of the attachment.
882
     * @param int $filesize The size of the attachment.
883
     * @param string $description The description of the attachment.
884
     * @return \stdClass
885
     */
886
    private function process_message_part_attachment(
887
        string $filename,
888
        string $filecontent,
889
        string $contentid,
890
        int $filesize,
891
        string $description = '',
892
    ): \stdClass {
893
        global $CFG;
894
 
895
        // If a filename is present, assume that this part is an attachment.
896
        $attachment = new \stdClass();
897
        $attachment->filename = $filename;
898
        $attachment->content = $filecontent;
899
        $attachment->description = $description;
900
        $attachment->contentid = $contentid;
901
        $attachment->filesize = $filesize;
902
 
903
        if (!empty($CFG->antiviruses)) {
904
            // Virus scanning is removed and will be brought back by MDL-50434.
905
        }
906
 
907
        return $attachment;
908
    }
909
 
910
    /**
911
     * Check whether the key provided is valid.
912
     *
913
     * @param int $status The status to validate.
914
     * @return bool
915
     */
916
    private function passes_key_validation(
917
        int $status,
918
    ): bool {
919
        // The validation result is tested in a bitwise operation.
920
        if ((
921
            $status & ~ \core\message\inbound\address_manager::VALIDATION_SUCCESS
922
                    & ~ \core\message\inbound\address_manager::VALIDATION_UNKNOWN_DATAKEY
923
                    & ~ \core\message\inbound\address_manager::VALIDATION_EXPIRED_DATAKEY
924
                    & ~ \core\message\inbound\address_manager::VALIDATION_INVALID_HASH
925
                    & ~ \core\message\inbound\address_manager::VALIDATION_ADDRESS_MISMATCH) !== 0) {
926
 
927
            // One of the above bits was found in the status - fail the validation.
928
            return false;
929
        }
930
        return true;
931
    }
932
 
933
    /**
934
     * Add the specified flag to the message.
935
     *
936
     * @param int $messageuid Message uid to process
937
     * @param string $flag The flag to add
938
     */
939
    private function add_flag_to_message(
940
        int $messageuid,
941
        string $flag,
942
    ): void {
943
        // Add flag to the message.
944
        $this->client->flag(
945
            mailbox: $this->get_mailbox(),
946
            messages: $messageuid,
947
            flag: strtoupper(substr($flag, 1)),
948
        );
949
    }
950
 
951
    /**
952
     * Remove the specified flag from the message.
953
     *
954
     * @param int $messageuid Message uid to process
955
     * @param string $flag The flag to remove
956
     */
957
    private function remove_flag_from_message(
958
        int $messageuid,
959
        string $flag,
960
    ): void {
961
        // Remove the flag from the message.
962
        $this->client->unflag(
963
            mailbox: $this->get_mailbox(),
964
            messages: $messageuid,
965
            flag: strtoupper(substr($flag, 1)),
966
        );
967
    }
968
 
969
    /**
970
     * Check whether the message has the specified flag
971
     *
972
     * @param int $messageuid Message uid to check.
973
     * @param string $flag The flag to check.
974
     * @return bool True if the message has the flag, false otherwise.
975
     */
976
    private function message_has_flag(
977
        int $messageuid,
978
        string $flag,
979
    ): bool {
980
        // Grab the message data with flags.
981
        $messages = $this->client->fetch(
982
            mailbox: $this->get_mailbox(),
983
            message_set: $messageuid,
984
            is_uid: true,
985
            query_items: [
986
                'FLAGS',
987
            ],
988
        );
989
        $messagedata = reset($messages);
990
        $flags = $messagedata->flags;
991
        return array_key_exists(
992
            key: strtoupper(substr($flag, 1)),
993
            array: $flags,
994
        );
995
    }
996
 
997
    /**
998
     * Ensure that all mailboxes exist.
999
     */
1000
    private function ensure_mailboxes_exist(): void {
1001
        $requiredmailboxes = [
1002
            self::MAILBOX,
1003
            $this->get_confirmation_folder(),
1004
        ];
1005
 
1006
        $existingmailboxes = $this->client->listMailboxes(
1007
            ref: '',
1008
            mailbox: '*',
1009
        );
1010
        foreach ($requiredmailboxes as $mailbox) {
1011
            if (in_array($mailbox, $existingmailboxes)) {
1012
                // This mailbox was found.
1013
                continue;
1014
            }
1015
 
1016
            mtrace("Unable to find the '{$mailbox}' mailbox - creating it.");
1017
            $this->client->createFolder(
1018
                mailbox: $mailbox,
1019
            );
1020
        }
1021
    }
1022
 
1023
    /**
1024
     * Attempt to determine whether this message is a bulk message (e.g. automated reply).
1025
     *
1026
     * @param int $messageuid The message uid to check
1027
     * @return boolean
1028
     */
1029
    private function is_bulk_message(
1030
        int $messageuid,
1031
    ): bool {
1032
        $messages = $this->client->fetch(
1033
            mailbox: $this->get_mailbox(),
1034
            message_set: $messageuid,
1035
            is_uid: true,
1036
            query_items: [
1037
                'BODY.PEEK[HEADER.FIELDS (Precedence X-Autoreply X-Autorespond Auto-Submitted)]',
1038
            ],
1039
        );
1040
        $headerinfo = reset($messages);
1041
        // Assume that this message is not bulk to begin with.
1042
        $isbulk = false;
1043
 
1044
        // An auto-reply may itself include the Bulk Precedence.
1045
        $precedence = $headerinfo->get('Precedence', false);
1046
        $isbulk = $isbulk || strtolower($precedence ?? '') == 'bulk';
1047
 
1048
        // If the X-Autoreply header is set, and not 'no', then this is an automatic reply.
1049
        $autoreply = $headerinfo->get('X-Autoreply', false);
1050
        $isbulk = $isbulk || ($autoreply && $autoreply != 'no');
1051
 
1052
        // If the X-Autorespond header is set, and not 'no', then this is an automatic response.
1053
        $autorespond = $headerinfo->get('X-Autorespond', false);
1054
        $isbulk = $isbulk || ($autorespond && $autorespond != 'no');
1055
 
1056
        // If the Auto-Submitted header is set, and not 'no', then this is a non-human response.
1057
        $autosubmitted = $headerinfo->get('Auto-Submitted', false);
1058
        $isbulk = $isbulk || ($autosubmitted && $autosubmitted != 'no');
1059
 
1060
        return $isbulk;
1061
    }
1062
 
1063
    /**
1064
     * Send the message to the appropriate handler.
1065
     *
1066
     * @return bool
1067
     * @throws \core\message\inbound\processing_failed_exception if anything goes wrong.
1068
     */
1069
    private function send_to_handler() {
1070
        try {
1071
            mtrace("--> Passing to Inbound Message handler {$this->addressmanager->get_handler()->classname}");
1072
            if ($result = $this->addressmanager->handle_message($this->currentmessagedata)) {
1073
                $this->inform_user_of_success($this->currentmessagedata, $result);
1074
                // Request that this message be marked for deletion.
1075
                return true;
1076
            }
1077
 
1078
        } catch (\core\message\inbound\processing_failed_exception $e) {
1079
            mtrace("-> The Inbound Message handler threw an exception. Unable to process this message. The user has been informed.");
1080
            mtrace("--> " . $e->getMessage());
1081
            // Throw the exception again, with additional data.
1082
            $error = new \stdClass();
1083
            $error->subject     = $this->currentmessagedata->envelope->subject;
1084
            $error->message     = $e->getMessage();
1085
            throw new \core\message\inbound\processing_failed_exception('messageprocessingfailed', 'tool_messageinbound', $error);
1086
 
1087
        } catch (\Exception $e) {
1088
            mtrace("-> The Inbound Message handler threw an exception. Unable to process this message. User informed.");
1089
            mtrace("--> " . $e->getMessage());
1090
            // An unknown error occurred. Still inform the user but, this time do not include the specific
1091
            // message information.
1092
            $error = new \stdClass();
1093
            $error->subject     = $this->currentmessagedata->envelope->subject;
1094
            throw new \core\message\inbound\processing_failed_exception('messageprocessingfailedunknown',
1095
                    'tool_messageinbound', $error);
1096
 
1097
        }
1098
 
1099
        // Something went wrong and the message was not handled well in the Inbound Message handler.
1100
        mtrace("-> The Inbound Message handler reported an error. The message may not have been processed.");
1101
 
1102
        // It is the responsiblity of the handler to throw an appropriate exception if the message was not processed.
1103
        // Do not inform the user at this point.
1104
        return false;
1105
    }
1106
 
1107
    /**
1108
     * Handle failure of sender verification.
1109
     *
1110
     * This will send a notification to the user identified in the Inbound Message address informing them that a message has been
1111
     * stored. The message includes a verification link and reply-to address which is handled by the
1112
     * invalid_recipient_handler.
1113
     *
1114
     * @param int $messageuid The message uid to process.
1115
     * @param string $recipient The message recipient
1116
     * @return bool
1117
     */
1118
    private function handle_verification_failure(
1119
        int $messageuid,
1120
        string $recipient,
1121
    ): bool {
1122
        global $DB, $USER;
1123
 
1124
        $messageid = $this->get_message_sequence_from_uid($messageuid);
1125
        if ($messageid == $this->currentmessagedata->messageid) {
1126
            mtrace("---> Warning: Unable to determine the Message-ID of the message.");
1127
            return false;
1128
        }
1129
 
1130
        // Move the message into a new mailbox.
1131
        $this->client->move(
1132
            messages: $messageuid,
1133
            from: $this->get_mailbox(),
1134
            to: $this->get_confirmation_folder(),
1135
        );
1136
 
1137
        // Store the data from the failed message in the associated table.
1138
        $record = new \stdClass();
1139
        $record->messageid = $messageuid;
1140
        $record->userid = $USER->id;
1141
        $record->address = $recipient;
1142
        $record->timecreated = time();
1143
        $record->id = $DB->insert_record('messageinbound_messagelist', $record);
1144
 
1145
        // Setup the Inbound Message generator for the invalid recipient handler.
1146
        $addressmanager = new \core\message\inbound\address_manager();
1147
        $addressmanager->set_handler('\tool_messageinbound\message\inbound\invalid_recipient_handler');
1148
        $addressmanager->set_data($record->id);
1149
 
1150
        $eventdata = new \core\message\message();
1151
        $eventdata->component           = 'tool_messageinbound';
1152
        $eventdata->name                = 'invalidrecipienthandler';
1153
 
1154
        $userfrom = clone $USER;
1155
        $userfrom->customheaders = array();
1156
        // Adding the In-Reply-To header ensures that it is seen as a reply.
1157
        $userfrom->customheaders[] = 'In-Reply-To: ' . $messageuid;
1158
 
1159
        // The message will be sent from the intended user.
1160
        $eventdata->courseid            = SITEID;
1161
        $eventdata->userfrom            = \core_user::get_noreply_user();
1162
        $eventdata->userto              = $USER;
1163
        $eventdata->subject             = $this->get_reply_subject($this->currentmessagedata->envelope->subject);
1164
        $eventdata->fullmessage         = get_string('invalidrecipientdescription', 'tool_messageinbound', $this->currentmessagedata);
1165
        $eventdata->fullmessageformat   = FORMAT_PLAIN;
1166
        $eventdata->fullmessagehtml     = get_string('invalidrecipientdescriptionhtml', 'tool_messageinbound', $this->currentmessagedata);
1167
        $eventdata->smallmessage        = $eventdata->fullmessage;
1168
        $eventdata->notification        = 1;
1169
        $eventdata->replyto             = $addressmanager->generate($USER->id);
1170
 
1171
        mtrace("--> Sending a message to the user to report an verification failure.");
1172
        if (!message_send($eventdata)) {
1173
            mtrace("---> Warning: Message could not be sent.");
1174
            return false;
1175
        }
1176
 
1177
        return true;
1178
    }
1179
 
1180
    /**
1181
     * Inform the identified sender of a processing error.
1182
     *
1183
     * @param string $error The error message
1184
     */
1185
    private function inform_user_of_error($error) {
1186
        global $USER;
1187
 
1188
        // The message will be sent from the intended user.
1189
        $userfrom = clone $USER;
1190
        $userfrom->customheaders = array();
1191
 
1192
        if ($messageid = $this->currentmessagedata->messageid) {
1193
            // Adding the In-Reply-To header ensures that it is seen as a reply and threading is maintained.
1194
            $userfrom->customheaders[] = 'In-Reply-To: ' . $messageid;
1195
        }
1196
 
1197
        $messagedata = new \stdClass();
1198
        $messagedata->subject = $this->currentmessagedata->envelope->subject;
1199
        $messagedata->error = $error;
1200
 
1201
        $eventdata = new \core\message\message();
1202
        $eventdata->courseid            = SITEID;
1203
        $eventdata->component           = 'tool_messageinbound';
1204
        $eventdata->name                = 'messageprocessingerror';
1205
        $eventdata->userfrom            = $userfrom;
1206
        $eventdata->userto              = $USER;
1207
        $eventdata->subject             = self::get_reply_subject($this->currentmessagedata->envelope->subject);
1208
        $eventdata->fullmessage         = get_string('messageprocessingerror', 'tool_messageinbound', $messagedata);
1209
        $eventdata->fullmessageformat   = FORMAT_PLAIN;
1210
        $eventdata->fullmessagehtml     = get_string('messageprocessingerrorhtml', 'tool_messageinbound', $messagedata);
1211
        $eventdata->smallmessage        = $eventdata->fullmessage;
1212
        $eventdata->notification        = 1;
1213
 
1214
        if (message_send($eventdata)) {
1215
            mtrace("---> Notification sent to {$USER->email}.");
1216
        } else {
1217
            mtrace("---> Unable to send notification.");
1218
        }
1219
    }
1220
 
1221
    /**
1222
     * Inform the identified sender that message processing was successful.
1223
     *
1224
     * @param \stdClass $messagedata The data for the current message being processed.
1225
     * @param mixed $handlerresult The result returned by the handler.
1226
     * @return bool
1227
     */
1228
    private function inform_user_of_success(\stdClass $messagedata, $handlerresult) {
1229
        global $USER;
1230
 
1231
        // Check whether the handler has a success notification.
1232
        $handler = $this->addressmanager->get_handler();
1233
        $message = $handler->get_success_message($messagedata, $handlerresult);
1234
 
1235
        if (!$message) {
1236
            mtrace("---> Handler has not defined a success notification e-mail.");
1237
            return false;
1238
        }
1239
 
1240
        // Wrap the message in the notification wrapper.
1241
        $messageparams = new \stdClass();
1242
        $messageparams->html    = $message->html;
1243
        $messageparams->plain   = $message->plain;
1244
        $messagepreferencesurl = new \moodle_url("/message/notificationpreferences.php", array('id' => $USER->id));
1245
        $messageparams->messagepreferencesurl = $messagepreferencesurl->out();
1246
        $htmlmessage = get_string('messageprocessingsuccesshtml', 'tool_messageinbound', $messageparams);
1247
        $plainmessage = get_string('messageprocessingsuccess', 'tool_messageinbound', $messageparams);
1248
 
1249
        // The message will be sent from the intended user.
1250
        $userfrom = clone $USER;
1251
        $userfrom->customheaders = array();
1252
 
1253
        if ($messageid = $this->currentmessagedata->messageid) {
1254
            // Adding the In-Reply-To header ensures that it is seen as a reply and threading is maintained.
1255
            $userfrom->customheaders[] = 'In-Reply-To: ' . $messageid;
1256
        }
1257
 
1258
        $messagedata = new \stdClass();
1259
        $messagedata->subject = $this->currentmessagedata->envelope->subject;
1260
 
1261
        $eventdata = new \core\message\message();
1262
        $eventdata->courseid            = SITEID;
1263
        $eventdata->component           = 'tool_messageinbound';
1264
        $eventdata->name                = 'messageprocessingsuccess';
1265
        $eventdata->userfrom            = $userfrom;
1266
        $eventdata->userto              = $USER;
1267
        $eventdata->subject             = self::get_reply_subject($this->currentmessagedata->envelope->subject);
1268
        $eventdata->fullmessage         = $plainmessage;
1269
        $eventdata->fullmessageformat   = FORMAT_PLAIN;
1270
        $eventdata->fullmessagehtml     = $htmlmessage;
1271
        $eventdata->smallmessage        = $eventdata->fullmessage;
1272
        $eventdata->notification        = 1;
1273
 
1274
        if (message_send($eventdata)) {
1275
            mtrace("---> Success notification sent to {$USER->email}.");
1276
        } else {
1277
            mtrace("---> Unable to send success notification.");
1278
        }
1279
        return true;
1280
    }
1281
 
1282
    /**
1283
     * Return a formatted subject line for replies.
1284
     *
1285
     * @param string $subject The subject string
1286
     * @return string The formatted reply subject
1287
     */
1288
    private function get_reply_subject($subject) {
1289
        $prefix = get_string('replysubjectprefix', 'tool_messageinbound');
1290
        if (!(substr($subject, 0, strlen($prefix)) == $prefix)) {
1291
            $subject = $prefix . ' ' . $subject;
1292
        }
1293
 
1294
        return $subject;
1295
    }
1296
 
1297
    /**
1298
     * Parse the address from the envelope.
1299
     *
1300
     * @param array $addresslist List of email addresses to parse.
1301
     * @return array|null List of parsed email addresses.
1302
     */
1303
    protected function get_address_from_envelope(array $addresslist): array|null {
1304
        if (empty($addresslist)) {
1305
            return null;
1306
        }
1307
 
1308
        $parsedaddressentry = [];
1309
        foreach ($addresslist as $addressentry) {
1310
            $parsedaddressentry[] = "{$addressentry[2]}@{$addressentry[3]}";
1311
        }
1312
 
1313
        return $parsedaddressentry;
1314
    }
1315
 
1316
    /**
1317
     * Get the message sequence number from the message uid.
1318
     *
1319
     * @param int $messageuid The message uid to process.
1320
     * @return int The message sequence number.
1321
     */
1322
    protected function get_message_sequence_from_uid(
1323
        int $messageuid,
1324
    ): int {
1325
        $messages = $this->client->fetch(
1326
            mailbox: $this->get_mailbox(),
1327
            message_set: $messageuid,
1328
            is_uid: true,
1329
            query_items: [
1330
                'SEQUENCE',
1331
            ],
1332
        );
1333
        $messagedata = reset($messages);
1334
        return $messagedata->sequence;
1335
    }
1336
 
1337
    /**
1338
     * Switch mailbox.
1339
     *
1340
     * @param string $mailbox The mailbox to switch to.
1341
     */
1342
    protected function select_mailbox(
1343
        string $mailbox,
1344
    ): void {
1345
        $this->client->select(mailbox: $mailbox);
1346
    }
1347
 
1348
    /**
1349
     * We use Roundcube Framework to receive the emails.
1350
     * This method will load the required dependencies.
1351
     */
1352
    protected function load_dependencies(): void {
1353
        global $CFG;
1354
        $dependencies = [
1355
            'rcube_charset.php',
1356
            'rcube_imap_generic.php',
1357
            'rcube_message_header.php',
1358
            'rcube_mime.php',
1359
            'rcube_result_index.php',
1360
            'rcube_result_thread.php',
1361
            'rcube_utils.php',
1362
        ];
1363
 
1364
        array_map(fn($file) => require_once("$CFG->dirroot/$CFG->admin/tool/messageinbound/roundcube/{$file}"), $dependencies);
1365
    }
1366
}