[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/admin/tool/messageinbound/classes/ -> manager.php (source)

   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  }


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1