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