[ Index ]

PHP Cross Reference of Unnamed Project

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 \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  }


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1