Differences Between: [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403]
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 defined('MOODLE_INTERNAL') || die(); 28 29 /** 30 * Mail Pickup Manager. 31 * 32 * @copyright 2014 Andrew Nicols 33 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 34 */ 35 class manager { 36 37 /** 38 * @var string The main mailbox to check. 39 */ 40 const MAILBOX = 'INBOX'; 41 42 /** 43 * @var string The mailbox to store messages in when they are awaiting confirmation. 44 */ 45 const CONFIRMATIONFOLDER = 'tobeconfirmed'; 46 47 /** 48 * @var string The flag for seen/read messages. 49 */ 50 const MESSAGE_SEEN = '\seen'; 51 52 /** 53 * @var string The flag for flagged messages. 54 */ 55 const MESSAGE_FLAGGED = '\flagged'; 56 57 /** 58 * @var string The flag for deleted messages. 59 */ 60 const MESSAGE_DELETED = '\deleted'; 61 62 /** 63 * @var \string IMAP folder namespace. 64 */ 65 protected $imapnamespace = null; 66 67 /** 68 * @var \Horde_Imap_Client_Socket A reference to the IMAP client. 69 */ 70 protected $client = null; 71 72 /** 73 * @var \core\message\inbound\address_manager A reference to the Inbound Message Address Manager instance. 74 */ 75 protected $addressmanager = null; 76 77 /** 78 * @var \stdClass The data for the current message being processed. 79 */ 80 protected $currentmessagedata = null; 81 82 /** 83 * Retrieve the connection to the IMAP client. 84 * 85 * @return bool Whether a connection was successfully established. 86 */ 87 protected function get_imap_client() { 88 global $CFG; 89 90 if (!\core\message\inbound\manager::is_enabled()) { 91 // E-mail processing not set up. 92 mtrace("Inbound Message not fully configured - exiting early."); 93 return false; 94 } 95 96 mtrace("Connecting to {$CFG->messageinbound_host} as {$CFG->messageinbound_hostuser}..."); 97 98 $configuration = array( 99 'username' => $CFG->messageinbound_hostuser, 100 'password' => $CFG->messageinbound_hostpass, 101 'hostspec' => $CFG->messageinbound_host, 102 'secure' => $CFG->messageinbound_hostssl, 103 'debug' => empty($CFG->debugimap) ? null : fopen('php://stderr', 'w'), 104 ); 105 106 if (strpos($configuration['hostspec'], ':')) { 107 $hostdata = explode(':', $configuration['hostspec']); 108 if (count($hostdata) === 2) { 109 // A hostname in the format hostname:port has been provided. 110 $configuration['hostspec'] = $hostdata[0]; 111 $configuration['port'] = $hostdata[1]; 112 } 113 } 114 115 $this->client = new \Horde_Imap_Client_Socket($configuration); 116 117 try { 118 $this->client->login(); 119 mtrace("Connection established."); 120 121 // Ensure that mailboxes exist. 122 $this->ensure_mailboxes_exist(); 123 124 return true; 125 126 } catch (\Horde_Imap_Client_Exception $e) { 127 $message = $e->getMessage(); 128 throw new \moodle_exception('imapconnectfailure', 'tool_messageinbound', '', null, $message); 129 } 130 } 131 132 /** 133 * Shutdown and close the connection to the IMAP client. 134 */ 135 protected function close_connection() { 136 if ($this->client) { 137 $this->client->close(); 138 } 139 $this->client = null; 140 } 141 142 /** 143 * Get the confirmation folder imap name 144 * 145 * @return string 146 */ 147 protected function get_confirmation_folder() { 148 149 if ($this->imapnamespace === null) { 150 if ($this->client->queryCapability('NAMESPACE')) { 151 $namespaces = $this->client->getNamespaces(array(), array('ob_return' => true)); 152 $this->imapnamespace = $namespaces->getNamespace('INBOX'); 153 } else { 154 $this->imapnamespace = ''; 155 } 156 } 157 158 return $this->imapnamespace . self::CONFIRMATIONFOLDER; 159 } 160 161 /** 162 * Get the current mailbox information. 163 * 164 * @return \Horde_Imap_Client_Mailbox 165 * @throws \core\message\inbound\processing_failed_exception if the mailbox could not be opened. 166 */ 167 protected function get_mailbox() { 168 // Get the current mailbox. 169 $mailbox = $this->client->currentMailbox(); 170 171 if (isset($mailbox['mailbox'])) { 172 return $mailbox['mailbox']; 173 } else { 174 throw new \core\message\inbound\processing_failed_exception('couldnotopenmailbox', 'tool_messageinbound'); 175 } 176 } 177 178 /** 179 * Execute the main Inbound Message pickup task. 180 * 181 * @return bool 182 */ 183 public function pickup_messages() { 184 if (!$this->get_imap_client()) { 185 return false; 186 } 187 188 // Restrict results to messages which are unseen, and have not been flagged. 189 $search = new \Horde_Imap_Client_Search_Query(); 190 $search->flag(self::MESSAGE_SEEN, false); 191 $search->flag(self::MESSAGE_FLAGGED, false); 192 mtrace("Searching for Unseen, Unflagged email in the folder '" . self::MAILBOX . "'"); 193 $results = $this->client->search(self::MAILBOX, $search); 194 195 // We require the envelope data and structure of each message. 196 $query = new \Horde_Imap_Client_Fetch_Query(); 197 $query->envelope(); 198 $query->structure(); 199 200 // Retrieve the message id. 201 $messages = $this->client->fetch(self::MAILBOX, $query, array('ids' => $results['match'])); 202 203 mtrace("Found " . $messages->count() . " messages to parse. Parsing..."); 204 $this->addressmanager = new \core\message\inbound\address_manager(); 205 foreach ($messages as $message) { 206 $this->process_message($message); 207 } 208 209 // Close the client connection. 210 $this->close_connection(); 211 212 return true; 213 } 214 215 /** 216 * Process a message received and validated by the Inbound Message processor. 217 * 218 * @param \stdClass $maildata The data retrieved from the database for the current record. 219 * @return bool Whether the message was successfully processed. 220 * @throws \core\message\inbound\processing_failed_exception if the message cannot be found. 221 */ 222 public function process_existing_message(\stdClass $maildata) { 223 // Grab the new IMAP client. 224 if (!$this->get_imap_client()) { 225 return false; 226 } 227 228 // Build the search. 229 $search = new \Horde_Imap_Client_Search_Query(); 230 // When dealing with Inbound Message messages, we mark them as flagged and seen. Restrict the search to those criterion. 231 $search->flag(self::MESSAGE_SEEN, true); 232 $search->flag(self::MESSAGE_FLAGGED, true); 233 mtrace("Searching for a Seen, Flagged message in the folder '" . $this->get_confirmation_folder() . "'"); 234 235 // Match the message ID. 236 $search->headerText('message-id', $maildata->messageid); 237 $search->headerText('to', $maildata->address); 238 239 $results = $this->client->search($this->get_confirmation_folder(), $search); 240 241 // Build the base query. 242 $query = new \Horde_Imap_Client_Fetch_Query(); 243 $query->envelope(); 244 $query->structure(); 245 246 247 // Fetch the first message from the client. 248 $messages = $this->client->fetch($this->get_confirmation_folder(), $query, array('ids' => $results['match'])); 249 $this->addressmanager = new \core\message\inbound\address_manager(); 250 if ($message = $messages->first()) { 251 mtrace("--> Found the message. Passing back to the pickup system."); 252 253 // Process the message. 254 $this->process_message($message, true, true); 255 256 // Close the client connection. 257 $this->close_connection(); 258 259 mtrace("============================================================================"); 260 return true; 261 } else { 262 // Close the client connection. 263 $this->close_connection(); 264 265 mtrace("============================================================================"); 266 throw new \core\message\inbound\processing_failed_exception('oldmessagenotfound', 'tool_messageinbound'); 267 } 268 } 269 270 /** 271 * Tidy up old messages in the confirmation folder. 272 * 273 * @return bool Whether tidying occurred successfully. 274 */ 275 public function tidy_old_messages() { 276 // Grab the new IMAP client. 277 if (!$this->get_imap_client()) { 278 return false; 279 } 280 281 // Open the mailbox. 282 mtrace("Searching for messages older than 24 hours in the '" . 283 $this->get_confirmation_folder() . "' folder."); 284 $this->client->openMailbox($this->get_confirmation_folder()); 285 286 $mailbox = $this->get_mailbox(); 287 288 // Build the search. 289 $search = new \Horde_Imap_Client_Search_Query(); 290 291 // Delete messages older than 24 hours old. 292 $search->intervalSearch(DAYSECS, \Horde_Imap_Client_Search_Query::INTERVAL_OLDER); 293 294 $results = $this->client->search($mailbox, $search); 295 296 // Build the base query. 297 $query = new \Horde_Imap_Client_Fetch_Query(); 298 $query->envelope(); 299 300 // Retrieve the messages and mark them for removal. 301 $messages = $this->client->fetch($mailbox, $query, array('ids' => $results['match'])); 302 mtrace("Found " . $messages->count() . " messages for removal."); 303 foreach ($messages as $message) { 304 $this->add_flag_to_message($message->getUid(), self::MESSAGE_DELETED); 305 } 306 307 mtrace("Finished removing messages."); 308 $this->close_connection(); 309 310 return true; 311 } 312 313 /** 314 * Remove older verification failures. 315 * 316 * @return void 317 */ 318 public function tidy_old_verification_failures() { 319 global $DB; 320 $DB->delete_records_select('messageinbound_messagelist', 'timecreated < :time', ['time' => time() - DAYSECS]); 321 } 322 323 /** 324 * Process a message and pass it through the Inbound Message handling systems. 325 * 326 * @param \Horde_Imap_Client_Data_Fetch $message The message to process 327 * @param bool $viewreadmessages Whether to also look at messages which have been marked as read 328 * @param bool $skipsenderverification Whether to skip the sender verification stage 329 */ 330 public function process_message( 331 \Horde_Imap_Client_Data_Fetch $message, 332 $viewreadmessages = false, 333 $skipsenderverification = false) { 334 global $USER; 335 336 // We use the Client IDs several times - store them here. 337 $messageid = new \Horde_Imap_Client_Ids($message->getUid()); 338 339 mtrace("- Parsing message " . $messageid); 340 341 // First flag this message to prevent another running hitting this message while we look at the headers. 342 $this->add_flag_to_message($messageid, self::MESSAGE_FLAGGED); 343 344 if ($this->is_bulk_message($message, $messageid)) { 345 mtrace("- The message has a bulk header set. This is likely an auto-generated reply - discarding."); 346 return; 347 } 348 349 // Record the user that this script is currently being run as. This is important when re-processing existing 350 // messages, as cron_setup_user is called multiple times. 351 $originaluser = $USER; 352 353 $envelope = $message->getEnvelope(); 354 $recipients = $envelope->to->bare_addresses; 355 foreach ($recipients as $recipient) { 356 if (!\core\message\inbound\address_manager::is_correct_format($recipient)) { 357 // Message did not contain a subaddress. 358 mtrace("- Recipient '{$recipient}' did not match Inbound Message headers."); 359 continue; 360 } 361 362 // Message contained a match. 363 $senders = $message->getEnvelope()->from->bare_addresses; 364 if (count($senders) !== 1) { 365 mtrace("- Received multiple senders. Only the first sender will be used."); 366 } 367 $sender = array_shift($senders); 368 369 mtrace("-- Subject:\t" . $envelope->subject); 370 mtrace("-- From:\t" . $sender); 371 mtrace("-- Recipient:\t" . $recipient); 372 373 // Grab messagedata including flags. 374 $query = new \Horde_Imap_Client_Fetch_Query(); 375 $query->structure(); 376 $messagedata = $this->client->fetch($this->get_mailbox(), $query, array( 377 'ids' => $messageid, 378 ))->first(); 379 380 if (!$viewreadmessages && $this->message_has_flag($messageid, self::MESSAGE_SEEN)) { 381 // Something else has already seen this message. Skip it now. 382 mtrace("-- Skipping the message - it has been marked as seen - perhaps by another process."); 383 continue; 384 } 385 386 // Mark it as read to lock the message. 387 $this->add_flag_to_message($messageid, self::MESSAGE_SEEN); 388 389 // Now pass it through the Inbound Message processor. 390 $status = $this->addressmanager->process_envelope($recipient, $sender); 391 392 if (($status & ~ \core\message\inbound\address_manager::VALIDATION_DISABLED_HANDLER) !== $status) { 393 // The handler is disabled. 394 mtrace("-- Skipped message - Handler is disabled. Fail code {$status}"); 395 // In order to handle the user error, we need more information about the message being failed. 396 $this->process_message_data($envelope, $messagedata, $messageid); 397 $this->inform_user_of_error(get_string('handlerdisabled', 'tool_messageinbound', $this->currentmessagedata)); 398 return; 399 } 400 401 // Check the validation status early. No point processing garbage messages, but we do need to process it 402 // for some validation failure types. 403 if (!$this->passes_key_validation($status, $messageid)) { 404 // None of the above validation failures were found. Skip this message. 405 mtrace("-- Skipped message - it does not appear to relate to a Inbound Message pickup. Fail code {$status}"); 406 407 // Remove the seen flag from the message as there may be multiple recipients. 408 $this->remove_flag_from_message($messageid, self::MESSAGE_SEEN); 409 410 // Skip further processing for this recipient. 411 continue; 412 } 413 414 // Process the message as the user. 415 $user = $this->addressmanager->get_data()->user; 416 mtrace("-- Processing the message as user {$user->id} ({$user->username})."); 417 cron_setup_user($user); 418 419 // Process and retrieve the message data for this message. 420 // This includes fetching the full content, as well as all headers, and attachments. 421 if (!$this->process_message_data($envelope, $messagedata, $messageid)) { 422 mtrace("--- Message could not be found on the server. Is another process removing messages?"); 423 return; 424 } 425 426 // When processing validation replies, we need to skip the sender verification phase as this has been 427 // manually completed. 428 if (!$skipsenderverification && $status !== 0) { 429 // Check the validation status for failure types which require confirmation. 430 // The validation result is tested in a bitwise operation. 431 mtrace("-- Message did not meet validation but is possibly recoverable. Fail code {$status}"); 432 // This is a recoverable error, but requires user input. 433 434 if ($this->handle_verification_failure($messageid, $recipient)) { 435 mtrace("--- Original message retained on mail server and confirmation message sent to user."); 436 } else { 437 mtrace("--- Invalid Recipient Handler - unable to save. Informing the user of the failure."); 438 $this->inform_user_of_error(get_string('invalidrecipientfinal', 'tool_messageinbound', $this->currentmessagedata)); 439 } 440 441 // Returning to normal cron user. 442 mtrace("-- Returning to the original user."); 443 cron_setup_user($originaluser); 444 return; 445 } 446 447 // Add the content and attachment data. 448 mtrace("-- Validation completed. Fetching rest of message content."); 449 $this->process_message_data_body($messagedata, $messageid); 450 451 // The message processor throws exceptions upon failure. These must be caught and notifications sent to 452 // the user here. 453 try { 454 $result = $this->send_to_handler(); 455 } catch (\core\message\inbound\processing_failed_exception $e) { 456 // We know about these kinds of errors and they should result in the user being notified of the 457 // failure. Send the user a notification here. 458 $this->inform_user_of_error($e->getMessage()); 459 460 // Returning to normal cron user. 461 mtrace("-- Returning to the original user."); 462 cron_setup_user($originaluser); 463 return; 464 } catch (\Exception $e) { 465 // An unknown error occurred. The user is not informed, but the administrator is. 466 mtrace("-- Message processing failed. An unexpected exception was thrown. Details follow."); 467 mtrace($e->getMessage()); 468 469 // Returning to normal cron user. 470 mtrace("-- Returning to the original user."); 471 cron_setup_user($originaluser); 472 return; 473 } 474 475 if ($result) { 476 // Handle message cleanup. Messages are deleted once fully processed. 477 mtrace("-- Marking the message for removal."); 478 $this->add_flag_to_message($messageid, self::MESSAGE_DELETED); 479 } else { 480 mtrace("-- The Inbound Message processor did not return a success status. Skipping message removal."); 481 } 482 483 // Returning to normal cron user. 484 mtrace("-- Returning to the original user."); 485 cron_setup_user($originaluser); 486 487 mtrace("-- Finished processing " . $message->getUid()); 488 489 // Skip the outer loop too. The message has already been processed and it could be possible for there to 490 // be two recipients in the envelope which match somehow. 491 return; 492 } 493 } 494 495 /** 496 * Process a message to retrieve it's header data without body and attachemnts. 497 * 498 * @param \Horde_Imap_Client_Data_Envelope $envelope The Envelope of the message 499 * @param \Horde_Imap_Client_Data_Fetch $basemessagedata The structure and part of the message body 500 * @param string|\Horde_Imap_Client_Ids $messageid The Hore message Uid 501 * @return \stdClass The current value of the messagedata 502 */ 503 private function process_message_data( 504 \Horde_Imap_Client_Data_Envelope $envelope, 505 \Horde_Imap_Client_Data_Fetch $basemessagedata, 506 $messageid) { 507 508 // Get the current mailbox. 509 $mailbox = $this->get_mailbox(); 510 511 // We need the structure at various points below. 512 $structure = $basemessagedata->getStructure(); 513 514 // Now fetch the rest of the message content. 515 $query = new \Horde_Imap_Client_Fetch_Query(); 516 $query->imapDate(); 517 518 // Fetch the message header. 519 $query->headerText(); 520 521 // Retrieve the message with the above components. 522 $messagedata = $this->client->fetch($mailbox, $query, array('ids' => $messageid))->first(); 523 524 if (!$messagedata) { 525 // Message was not found! Somehow it has been removed or is no longer returned. 526 return null; 527 } 528 529 // The message ID should always be in the first part. 530 $data = new \stdClass(); 531 $data->messageid = $messagedata->getHeaderText(0, \Horde_Imap_Client_Data_Fetch::HEADER_PARSE)->getValue('Message-ID'); 532 $data->subject = $envelope->subject; 533 $data->timestamp = $messagedata->getImapDate()->__toString(); 534 $data->envelope = $envelope; 535 $data->data = $this->addressmanager->get_data(); 536 $data->headers = $messagedata->getHeaderText(); 537 538 $this->currentmessagedata = $data; 539 540 return $this->currentmessagedata; 541 } 542 543 /** 544 * Process a message again to add body and attachment data. 545 * 546 * @param \Horde_Imap_Client_Data_Fetch $basemessagedata The structure and part of the message body 547 * @param string|\Horde_Imap_Client_Ids $messageid The Hore message Uid 548 * @return \stdClass The current value of the messagedata 549 */ 550 private function process_message_data_body( 551 \Horde_Imap_Client_Data_Fetch $basemessagedata, 552 $messageid) { 553 global $CFG; 554 555 // Get the current mailbox. 556 $mailbox = $this->get_mailbox(); 557 558 // We need the structure at various points below. 559 $structure = $basemessagedata->getStructure(); 560 561 // Now fetch the rest of the message content. 562 $query = new \Horde_Imap_Client_Fetch_Query(); 563 $query->fullText(); 564 565 // Fetch all of the message parts too. 566 $typemap = $structure->contentTypeMap(); 567 foreach ($typemap as $part => $type) { 568 // The body of the part - attempt to decode it on the server. 569 $query->bodyPart($part, array( 570 'decode' => true, 571 'peek' => true, 572 )); 573 $query->bodyPartSize($part); 574 } 575 576 $messagedata = $this->client->fetch($mailbox, $query, array('ids' => $messageid))->first(); 577 578 // Store the data for this message. 579 $contentplain = ''; 580 $contenthtml = ''; 581 $attachments = array( 582 'inline' => array(), 583 'attachment' => array(), 584 ); 585 586 $plainpartid = $structure->findBody('plain'); 587 $htmlpartid = $structure->findBody('html'); 588 589 foreach ($typemap as $part => $type) { 590 // Get the message data from the body part, and combine it with the structure to give a fully-formed output. 591 $stream = $messagedata->getBodyPart($part, true); 592 $partdata = $structure->getPart($part); 593 $partdata->setContents($stream, array( 594 'usestream' => true, 595 )); 596 597 if ($part == $plainpartid) { 598 $contentplain = $this->process_message_part_body($messagedata, $partdata, $part); 599 600 } else if ($part == $htmlpartid) { 601 $contenthtml = $this->process_message_part_body($messagedata, $partdata, $part); 602 603 } else if ($filename = $partdata->getName($part)) { 604 if ($attachment = $this->process_message_part_attachment($messagedata, $partdata, $part, $filename)) { 605 // The disposition should be one of 'attachment', 'inline'. 606 // If an empty string is provided, default to 'attachment'. 607 $disposition = $partdata->getDisposition(); 608 $disposition = $disposition == 'inline' ? 'inline' : 'attachment'; 609 $attachments[$disposition][] = $attachment; 610 } 611 } 612 613 // We don't handle any of the other MIME content at this stage. 614 } 615 616 // The message ID should always be in the first part. 617 $this->currentmessagedata->plain = $contentplain; 618 $this->currentmessagedata->html = $contenthtml; 619 $this->currentmessagedata->attachments = $attachments; 620 621 return $this->currentmessagedata; 622 } 623 624 /** 625 * Process the messagedata and part data to extract the content of this part. 626 * 627 * @param \Horde_Imap_Client_Data_Fetch $messagedata The structure and part of the message body 628 * @param \Horde_Mime_Part $partdata The part data 629 * @param string $part The part ID 630 * @return string 631 */ 632 private function process_message_part_body($messagedata, $partdata, $part) { 633 // This is a content section for the main body. 634 635 // Get the string version of it. 636 $content = $messagedata->getBodyPart($part); 637 if (!$messagedata->getBodyPartDecode($part)) { 638 // Decode the content. 639 $partdata->setContents($content); 640 $content = $partdata->getContents(); 641 } 642 643 // Convert the text from the current encoding to UTF8. 644 $content = \core_text::convert($content, $partdata->getCharset()); 645 646 // Fix any invalid UTF8 characters. 647 // Note: XSS cleaning is not the responsibility of this code. It occurs immediately before display when 648 // format_text is called. 649 $content = clean_param($content, PARAM_RAW); 650 651 return $content; 652 } 653 654 /** 655 * Process a message again to add body and attachment data. 656 * 657 * @param \Horde_Imap_Client_Data_Fetch $messagedata The structure and part of the message body 658 * @param \Horde_Mime_Part $partdata The part data 659 * @param string $part The part ID. 660 * @param string $filename The filename of the attachment 661 * @return \stdClass 662 * @throws \core\message\inbound\processing_failed_exception If the attachment can't be saved to disk. 663 */ 664 private function process_message_part_attachment($messagedata, $partdata, $part, $filename) { 665 global $CFG; 666 667 // If a filename is present, assume that this part is an attachment. 668 $attachment = new \stdClass(); 669 $attachment->filename = $filename; 670 $attachment->type = $partdata->getType(); 671 $attachment->content = $partdata->getContents(); 672 $attachment->charset = $partdata->getCharset(); 673 $attachment->description = $partdata->getDescription(); 674 $attachment->contentid = $partdata->getContentId(); 675 $attachment->filesize = $partdata->getBytes(); 676 677 if (!empty($CFG->antiviruses)) { 678 mtrace("--> Attempting virus scan of '{$attachment->filename}'"); 679 // Perform a virus scan now. 680 try { 681 \core\antivirus\manager::scan_data($attachment->content); 682 } catch (\core\antivirus\scanner_exception $e) { 683 mtrace("--> A virus was found in the attachment '{$attachment->filename}'."); 684 $this->inform_attachment_virus(); 685 return; 686 } 687 } 688 689 return $attachment; 690 } 691 692 /** 693 * Check whether the key provided is valid. 694 * 695 * @param bool $status 696 * @param mixed $messageid The Hore message Uid 697 * @return bool 698 */ 699 private function passes_key_validation($status, $messageid) { 700 // The validation result is tested in a bitwise operation. 701 if (( 702 $status & ~ \core\message\inbound\address_manager::VALIDATION_SUCCESS 703 & ~ \core\message\inbound\address_manager::VALIDATION_UNKNOWN_DATAKEY 704 & ~ \core\message\inbound\address_manager::VALIDATION_EXPIRED_DATAKEY 705 & ~ \core\message\inbound\address_manager::VALIDATION_INVALID_HASH 706 & ~ \core\message\inbound\address_manager::VALIDATION_ADDRESS_MISMATCH) !== 0) { 707 708 // One of the above bits was found in the status - fail the validation. 709 return false; 710 } 711 return true; 712 } 713 714 /** 715 * Add the specified flag to the message. 716 * 717 * @param mixed $messageid 718 * @param string $flag The flag to add 719 */ 720 private function add_flag_to_message($messageid, $flag) { 721 // Get the current mailbox. 722 $mailbox = $this->get_mailbox(); 723 724 // Mark it as read to lock the message. 725 $this->client->store($mailbox, array( 726 'ids' => new \Horde_Imap_Client_Ids($messageid), 727 'add' => $flag, 728 )); 729 } 730 731 /** 732 * Remove the specified flag from the message. 733 * 734 * @param mixed $messageid 735 * @param string $flag The flag to remove 736 */ 737 private function remove_flag_from_message($messageid, $flag) { 738 // Get the current mailbox. 739 $mailbox = $this->get_mailbox(); 740 741 // Mark it as read to lock the message. 742 $this->client->store($mailbox, array( 743 'ids' => $messageid, 744 'delete' => $flag, 745 )); 746 } 747 748 /** 749 * Check whether the message has the specified flag 750 * 751 * @param mixed $messageid 752 * @param string $flag The flag to check 753 * @return bool 754 */ 755 private function message_has_flag($messageid, $flag) { 756 // Get the current mailbox. 757 $mailbox = $this->get_mailbox(); 758 759 // Grab messagedata including flags. 760 $query = new \Horde_Imap_Client_Fetch_Query(); 761 $query->flags(); 762 $query->structure(); 763 $messagedata = $this->client->fetch($mailbox, $query, array( 764 'ids' => $messageid, 765 ))->first(); 766 $flags = $messagedata->getFlags(); 767 768 return in_array($flag, $flags); 769 } 770 771 /** 772 * Ensure that all mailboxes exist. 773 */ 774 private function ensure_mailboxes_exist() { 775 776 $requiredmailboxes = array( 777 self::MAILBOX, 778 $this->get_confirmation_folder(), 779 ); 780 781 $existingmailboxes = $this->client->listMailboxes($requiredmailboxes); 782 foreach ($requiredmailboxes as $mailbox) { 783 if (isset($existingmailboxes[$mailbox])) { 784 // This mailbox was found. 785 continue; 786 } 787 788 mtrace("Unable to find the '{$mailbox}' mailbox - creating it."); 789 $this->client->createMailbox($mailbox); 790 } 791 } 792 793 /** 794 * Attempt to determine whether this message is a bulk message (e.g. automated reply). 795 * 796 * @param \Horde_Imap_Client_Data_Fetch $message The message to process 797 * @param string|\Horde_Imap_Client_Ids $messageid The Hore message Uid 798 * @return boolean 799 */ 800 private function is_bulk_message( 801 \Horde_Imap_Client_Data_Fetch $message, 802 $messageid) { 803 $query = new \Horde_Imap_Client_Fetch_Query(); 804 $query->headerText(array('peek' => true)); 805 806 $messagedata = $this->client->fetch($this->get_mailbox(), $query, array('ids' => $messageid))->first(); 807 808 // Assume that this message is not bulk to begin with. 809 $isbulk = false; 810 811 // An auto-reply may itself include the Bulk Precedence. 812 $precedence = $messagedata->getHeaderText(0, \Horde_Imap_Client_Data_Fetch::HEADER_PARSE)->getValue('Precedence'); 813 $isbulk = $isbulk || strtolower($precedence) == 'bulk'; 814 815 // If the X-Autoreply header is set, and not 'no', then this is an automatic reply. 816 $autoreply = $messagedata->getHeaderText(0, \Horde_Imap_Client_Data_Fetch::HEADER_PARSE)->getValue('X-Autoreply'); 817 $isbulk = $isbulk || ($autoreply && $autoreply != 'no'); 818 819 // If the X-Autorespond header is set, and not 'no', then this is an automatic response. 820 $autorespond = $messagedata->getHeaderText(0, \Horde_Imap_Client_Data_Fetch::HEADER_PARSE)->getValue('X-Autorespond'); 821 $isbulk = $isbulk || ($autorespond && $autorespond != 'no'); 822 823 // If the Auto-Submitted header is set, and not 'no', then this is a non-human response. 824 $autosubmitted = $messagedata->getHeaderText(0, \Horde_Imap_Client_Data_Fetch::HEADER_PARSE)->getValue('Auto-Submitted'); 825 $isbulk = $isbulk || ($autosubmitted && $autosubmitted != 'no'); 826 827 return $isbulk; 828 } 829 830 /** 831 * Send the message to the appropriate handler. 832 * 833 * @return bool 834 * @throws \core\message\inbound\processing_failed_exception if anything goes wrong. 835 */ 836 private function send_to_handler() { 837 try { 838 mtrace("--> Passing to Inbound Message handler {$this->addressmanager->get_handler()->classname}"); 839 if ($result = $this->addressmanager->handle_message($this->currentmessagedata)) { 840 $this->inform_user_of_success($this->currentmessagedata, $result); 841 // Request that this message be marked for deletion. 842 return true; 843 } 844 845 } catch (\core\message\inbound\processing_failed_exception $e) { 846 mtrace("-> The Inbound Message handler threw an exception. Unable to process this message. The user has been informed."); 847 mtrace("--> " . $e->getMessage()); 848 // Throw the exception again, with additional data. 849 $error = new \stdClass(); 850 $error->subject = $this->currentmessagedata->envelope->subject; 851 $error->message = $e->getMessage(); 852 throw new \core\message\inbound\processing_failed_exception('messageprocessingfailed', 'tool_messageinbound', $error); 853 854 } catch (\Exception $e) { 855 mtrace("-> The Inbound Message handler threw an exception. Unable to process this message. User informed."); 856 mtrace("--> " . $e->getMessage()); 857 // An unknown error occurred. Still inform the user but, this time do not include the specific 858 // message information. 859 $error = new \stdClass(); 860 $error->subject = $this->currentmessagedata->envelope->subject; 861 throw new \core\message\inbound\processing_failed_exception('messageprocessingfailedunknown', 862 'tool_messageinbound', $error); 863 864 } 865 866 // Something went wrong and the message was not handled well in the Inbound Message handler. 867 mtrace("-> The Inbound Message handler reported an error. The message may not have been processed."); 868 869 // It is the responsiblity of the handler to throw an appropriate exception if the message was not processed. 870 // Do not inform the user at this point. 871 return false; 872 } 873 874 /** 875 * Handle failure of sender verification. 876 * 877 * This will send a notification to the user identified in the Inbound Message address informing them that a message has been 878 * stored. The message includes a verification link and reply-to address which is handled by the 879 * invalid_recipient_handler. 880 * 881 * @param \Horde_Imap_Client_Ids $messageids 882 * @param string $recipient The message recipient 883 * @return bool 884 */ 885 private function handle_verification_failure( 886 \Horde_Imap_Client_Ids $messageids, 887 $recipient) { 888 global $DB, $USER; 889 890 if (!$messageid = $this->currentmessagedata->messageid) { 891 mtrace("---> Warning: Unable to determine the Message-ID of the message."); 892 return false; 893 } 894 895 // Move the message into a new mailbox. 896 $this->client->copy(self::MAILBOX, $this->get_confirmation_folder(), array( 897 'create' => true, 898 'ids' => $messageids, 899 'move' => true, 900 )); 901 902 // Store the data from the failed message in the associated table. 903 $record = new \stdClass(); 904 $record->messageid = $messageid; 905 $record->userid = $USER->id; 906 $record->address = $recipient; 907 $record->timecreated = time(); 908 $record->id = $DB->insert_record('messageinbound_messagelist', $record); 909 910 // Setup the Inbound Message generator for the invalid recipient handler. 911 $addressmanager = new \core\message\inbound\address_manager(); 912 $addressmanager->set_handler('\tool_messageinbound\message\inbound\invalid_recipient_handler'); 913 $addressmanager->set_data($record->id); 914 915 $eventdata = new \core\message\message(); 916 $eventdata->component = 'tool_messageinbound'; 917 $eventdata->name = 'invalidrecipienthandler'; 918 919 $userfrom = clone $USER; 920 $userfrom->customheaders = array(); 921 // Adding the In-Reply-To header ensures that it is seen as a reply. 922 $userfrom->customheaders[] = 'In-Reply-To: ' . $messageid; 923 924 // The message will be sent from the intended user. 925 $eventdata->courseid = SITEID; 926 $eventdata->userfrom = \core_user::get_noreply_user(); 927 $eventdata->userto = $USER; 928 $eventdata->subject = $this->get_reply_subject($this->currentmessagedata->envelope->subject); 929 $eventdata->fullmessage = get_string('invalidrecipientdescription', 'tool_messageinbound', $this->currentmessagedata); 930 $eventdata->fullmessageformat = FORMAT_PLAIN; 931 $eventdata->fullmessagehtml = get_string('invalidrecipientdescriptionhtml', 'tool_messageinbound', $this->currentmessagedata); 932 $eventdata->smallmessage = $eventdata->fullmessage; 933 $eventdata->notification = 1; 934 $eventdata->replyto = $addressmanager->generate($USER->id); 935 936 mtrace("--> Sending a message to the user to report an verification failure."); 937 if (!message_send($eventdata)) { 938 mtrace("---> Warning: Message could not be sent."); 939 return false; 940 } 941 942 return true; 943 } 944 945 /** 946 * Inform the identified sender of a processing error. 947 * 948 * @param string $error The error message 949 */ 950 private function inform_user_of_error($error) { 951 global $USER; 952 953 // The message will be sent from the intended user. 954 $userfrom = clone $USER; 955 $userfrom->customheaders = array(); 956 957 if ($messageid = $this->currentmessagedata->messageid) { 958 // Adding the In-Reply-To header ensures that it is seen as a reply and threading is maintained. 959 $userfrom->customheaders[] = 'In-Reply-To: ' . $messageid; 960 } 961 962 $messagedata = new \stdClass(); 963 $messagedata->subject = $this->currentmessagedata->envelope->subject; 964 $messagedata->error = $error; 965 966 $eventdata = new \core\message\message(); 967 $eventdata->courseid = SITEID; 968 $eventdata->component = 'tool_messageinbound'; 969 $eventdata->name = 'messageprocessingerror'; 970 $eventdata->userfrom = $userfrom; 971 $eventdata->userto = $USER; 972 $eventdata->subject = self::get_reply_subject($this->currentmessagedata->envelope->subject); 973 $eventdata->fullmessage = get_string('messageprocessingerror', 'tool_messageinbound', $messagedata); 974 $eventdata->fullmessageformat = FORMAT_PLAIN; 975 $eventdata->fullmessagehtml = get_string('messageprocessingerrorhtml', 'tool_messageinbound', $messagedata); 976 $eventdata->smallmessage = $eventdata->fullmessage; 977 $eventdata->notification = 1; 978 979 if (message_send($eventdata)) { 980 mtrace("---> Notification sent to {$USER->email}."); 981 } else { 982 mtrace("---> Unable to send notification."); 983 } 984 } 985 986 /** 987 * Inform the identified sender that message processing was successful. 988 * 989 * @param \stdClass $messagedata The data for the current message being processed. 990 * @param mixed $handlerresult The result returned by the handler. 991 * @return bool 992 */ 993 private function inform_user_of_success(\stdClass $messagedata, $handlerresult) { 994 global $USER; 995 996 // Check whether the handler has a success notification. 997 $handler = $this->addressmanager->get_handler(); 998 $message = $handler->get_success_message($messagedata, $handlerresult); 999 1000 if (!$message) { 1001 mtrace("---> Handler has not defined a success notification e-mail."); 1002 return false; 1003 } 1004 1005 // Wrap the message in the notification wrapper. 1006 $messageparams = new \stdClass(); 1007 $messageparams->html = $message->html; 1008 $messageparams->plain = $message->plain; 1009 $messagepreferencesurl = new \moodle_url("/message/notificationpreferences.php", array('id' => $USER->id)); 1010 $messageparams->messagepreferencesurl = $messagepreferencesurl->out(); 1011 $htmlmessage = get_string('messageprocessingsuccesshtml', 'tool_messageinbound', $messageparams); 1012 $plainmessage = get_string('messageprocessingsuccess', 'tool_messageinbound', $messageparams); 1013 1014 // The message will be sent from the intended user. 1015 $userfrom = clone $USER; 1016 $userfrom->customheaders = array(); 1017 1018 if ($messageid = $this->currentmessagedata->messageid) { 1019 // Adding the In-Reply-To header ensures that it is seen as a reply and threading is maintained. 1020 $userfrom->customheaders[] = 'In-Reply-To: ' . $messageid; 1021 } 1022 1023 $messagedata = new \stdClass(); 1024 $messagedata->subject = $this->currentmessagedata->envelope->subject; 1025 1026 $eventdata = new \core\message\message(); 1027 $eventdata->courseid = SITEID; 1028 $eventdata->component = 'tool_messageinbound'; 1029 $eventdata->name = 'messageprocessingsuccess'; 1030 $eventdata->userfrom = $userfrom; 1031 $eventdata->userto = $USER; 1032 $eventdata->subject = self::get_reply_subject($this->currentmessagedata->envelope->subject); 1033 $eventdata->fullmessage = $plainmessage; 1034 $eventdata->fullmessageformat = FORMAT_PLAIN; 1035 $eventdata->fullmessagehtml = $htmlmessage; 1036 $eventdata->smallmessage = $eventdata->fullmessage; 1037 $eventdata->notification = 1; 1038 1039 if (message_send($eventdata)) { 1040 mtrace("---> Success notification sent to {$USER->email}."); 1041 } else { 1042 mtrace("---> Unable to send success notification."); 1043 } 1044 return true; 1045 } 1046 1047 /** 1048 * Return a formatted subject line for replies. 1049 * 1050 * @param string $subject The subject string 1051 * @return string The formatted reply subject 1052 */ 1053 private function get_reply_subject($subject) { 1054 $prefix = get_string('replysubjectprefix', 'tool_messageinbound'); 1055 if (!(substr($subject, 0, strlen($prefix)) == $prefix)) { 1056 $subject = $prefix . ' ' . $subject; 1057 } 1058 1059 return $subject; 1060 } 1061 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body