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