. /** * The Mail Pickup Manager. * * @package tool_messageinbound * @copyright 2014 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_messageinbound; defined('MOODLE_INTERNAL') || die(); /** * Mail Pickup Manager. * * @copyright 2014 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class manager { /** * @var string The main mailbox to check. */ const MAILBOX = 'INBOX'; /** * @var string The mailbox to store messages in when they are awaiting confirmation. */ const CONFIRMATIONFOLDER = 'tobeconfirmed'; /** * @var string The flag for seen/read messages. */ const MESSAGE_SEEN = '\seen'; /** * @var string The flag for flagged messages. */ const MESSAGE_FLAGGED = '\flagged'; /** * @var string The flag for deleted messages. */ const MESSAGE_DELETED = '\deleted'; /** * @var \string IMAP folder namespace. */ protected $imapnamespace = null; /** * @var \Horde_Imap_Client_Socket A reference to the IMAP client. */ protected $client = null; /** * @var \core\message\inbound\address_manager A reference to the Inbound Message Address Manager instance. */ protected $addressmanager = null; /** * @var \stdClass The data for the current message being processed. */ protected $currentmessagedata = null; /** * Retrieve the connection to the IMAP client. * * @return bool Whether a connection was successfully established. */ protected function get_imap_client() { global $CFG; if (!\core\message\inbound\manager::is_enabled()) { // E-mail processing not set up. mtrace("Inbound Message not fully configured - exiting early."); return false; } mtrace("Connecting to {$CFG->messageinbound_host} as {$CFG->messageinbound_hostuser}..."); $configuration = array( 'username' => $CFG->messageinbound_hostuser, 'password' => $CFG->messageinbound_hostpass, 'hostspec' => $CFG->messageinbound_host, 'secure' => $CFG->messageinbound_hostssl, 'debug' => empty($CFG->debugimap) ? null : fopen('php://stderr', 'w'), ); if (strpos($configuration['hostspec'], ':')) { $hostdata = explode(':', $configuration['hostspec']); if (count($hostdata) === 2) { // A hostname in the format hostname:port has been provided. $configuration['hostspec'] = $hostdata[0]; $configuration['port'] = $hostdata[1]; } } $this->client = new \Horde_Imap_Client_Socket($configuration); try { $this->client->login(); mtrace("Connection established."); // Ensure that mailboxes exist. $this->ensure_mailboxes_exist(); return true; } catch (\Horde_Imap_Client_Exception $e) { $message = $e->getMessage(); throw new \moodle_exception('imapconnectfailure', 'tool_messageinbound', '', null, $message); } } /** * Shutdown and close the connection to the IMAP client. */ protected function close_connection() { if ($this->client) { $this->client->close(); } $this->client = null; } /** * Get the confirmation folder imap name * * @return string */ protected function get_confirmation_folder() { if ($this->imapnamespace === null) { if ($this->client->queryCapability('NAMESPACE')) { $namespaces = $this->client->getNamespaces(array(), array('ob_return' => true)); $this->imapnamespace = $namespaces->getNamespace('INBOX'); } else { $this->imapnamespace = ''; } } return $this->imapnamespace . self::CONFIRMATIONFOLDER; } /** * Get the current mailbox information. * * @return \Horde_Imap_Client_Mailbox * @throws \core\message\inbound\processing_failed_exception if the mailbox could not be opened. */ protected function get_mailbox() { // Get the current mailbox. $mailbox = $this->client->currentMailbox(); if (isset($mailbox['mailbox'])) { return $mailbox['mailbox']; } else { throw new \core\message\inbound\processing_failed_exception('couldnotopenmailbox', 'tool_messageinbound'); } } /** * Execute the main Inbound Message pickup task. * * @return bool */ public function pickup_messages() { if (!$this->get_imap_client()) { return false; } // Restrict results to messages which are unseen, and have not been flagged. $search = new \Horde_Imap_Client_Search_Query(); $search->flag(self::MESSAGE_SEEN, false); $search->flag(self::MESSAGE_FLAGGED, false); mtrace("Searching for Unseen, Unflagged email in the folder '" . self::MAILBOX . "'"); $results = $this->client->search(self::MAILBOX, $search); // We require the envelope data and structure of each message. $query = new \Horde_Imap_Client_Fetch_Query(); $query->envelope(); $query->structure(); // Retrieve the message id. $messages = $this->client->fetch(self::MAILBOX, $query, array('ids' => $results['match'])); mtrace("Found " . $messages->count() . " messages to parse. Parsing..."); $this->addressmanager = new \core\message\inbound\address_manager(); foreach ($messages as $message) { $this->process_message($message); } // Close the client connection. $this->close_connection(); return true; } /** * Process a message received and validated by the Inbound Message processor. * * @param \stdClass $maildata The data retrieved from the database for the current record. * @return bool Whether the message was successfully processed. * @throws \core\message\inbound\processing_failed_exception if the message cannot be found. */ public function process_existing_message(\stdClass $maildata) { // Grab the new IMAP client. if (!$this->get_imap_client()) { return false; } // Build the search. $search = new \Horde_Imap_Client_Search_Query(); // When dealing with Inbound Message messages, we mark them as flagged and seen. Restrict the search to those criterion. $search->flag(self::MESSAGE_SEEN, true); $search->flag(self::MESSAGE_FLAGGED, true); mtrace("Searching for a Seen, Flagged message in the folder '" . $this->get_confirmation_folder() . "'"); // Match the message ID. $search->headerText('message-id', $maildata->messageid); $search->headerText('to', $maildata->address); $results = $this->client->search($this->get_confirmation_folder(), $search); // Build the base query. $query = new \Horde_Imap_Client_Fetch_Query(); $query->envelope(); $query->structure(); // Fetch the first message from the client. $messages = $this->client->fetch($this->get_confirmation_folder(), $query, array('ids' => $results['match'])); $this->addressmanager = new \core\message\inbound\address_manager(); if ($message = $messages->first()) { mtrace("--> Found the message. Passing back to the pickup system."); // Process the message. $this->process_message($message, true, true); // Close the client connection. $this->close_connection(); mtrace("============================================================================"); return true; } else { // Close the client connection. $this->close_connection(); mtrace("============================================================================"); throw new \core\message\inbound\processing_failed_exception('oldmessagenotfound', 'tool_messageinbound'); } } /** * Tidy up old messages in the confirmation folder. * * @return bool Whether tidying occurred successfully. */ public function tidy_old_messages() { // Grab the new IMAP client. if (!$this->get_imap_client()) { return false; } // Open the mailbox. mtrace("Searching for messages older than 24 hours in the '" . $this->get_confirmation_folder() . "' folder."); $this->client->openMailbox($this->get_confirmation_folder()); $mailbox = $this->get_mailbox(); // Build the search. $search = new \Horde_Imap_Client_Search_Query(); // Delete messages older than 24 hours old. $search->intervalSearch(DAYSECS, \Horde_Imap_Client_Search_Query::INTERVAL_OLDER); $results = $this->client->search($mailbox, $search); // Build the base query. $query = new \Horde_Imap_Client_Fetch_Query(); $query->envelope(); // Retrieve the messages and mark them for removal. $messages = $this->client->fetch($mailbox, $query, array('ids' => $results['match'])); mtrace("Found " . $messages->count() . " messages for removal."); foreach ($messages as $message) { $this->add_flag_to_message($message->getUid(), self::MESSAGE_DELETED); } mtrace("Finished removing messages."); $this->close_connection(); return true; } /** * Remove older verification failures. * * @return void */ public function tidy_old_verification_failures() { global $DB; $DB->delete_records_select('messageinbound_messagelist', 'timecreated < :time', ['time' => time() - DAYSECS]); } /** * Process a message and pass it through the Inbound Message handling systems. * * @param \Horde_Imap_Client_Data_Fetch $message The message to process * @param bool $viewreadmessages Whether to also look at messages which have been marked as read * @param bool $skipsenderverification Whether to skip the sender verification stage */ public function process_message( \Horde_Imap_Client_Data_Fetch $message, $viewreadmessages = false, $skipsenderverification = false) { global $USER; // We use the Client IDs several times - store them here. $messageid = new \Horde_Imap_Client_Ids($message->getUid()); mtrace("- Parsing message " . $messageid); // First flag this message to prevent another running hitting this message while we look at the headers. $this->add_flag_to_message($messageid, self::MESSAGE_FLAGGED); if ($this->is_bulk_message($message, $messageid)) { mtrace("- The message has a bulk header set. This is likely an auto-generated reply - discarding."); return; } // Record the user that this script is currently being run as. This is important when re-processing existing // messages, as cron_setup_user is called multiple times. $originaluser = $USER; $envelope = $message->getEnvelope(); $recipients = $envelope->to->bare_addresses; foreach ($recipients as $recipient) { if (!\core\message\inbound\address_manager::is_correct_format($recipient)) { // Message did not contain a subaddress. mtrace("- Recipient '{$recipient}' did not match Inbound Message headers."); continue; } // Message contained a match. $senders = $message->getEnvelope()->from->bare_addresses; if (count($senders) !== 1) { mtrace("- Received multiple senders. Only the first sender will be used."); } $sender = array_shift($senders); mtrace("-- Subject:\t" . $envelope->subject); mtrace("-- From:\t" . $sender); mtrace("-- Recipient:\t" . $recipient); // Grab messagedata including flags. $query = new \Horde_Imap_Client_Fetch_Query(); $query->structure(); $messagedata = $this->client->fetch($this->get_mailbox(), $query, array( 'ids' => $messageid, ))->first(); if (!$viewreadmessages && $this->message_has_flag($messageid, self::MESSAGE_SEEN)) { // Something else has already seen this message. Skip it now. mtrace("-- Skipping the message - it has been marked as seen - perhaps by another process."); continue; } // Mark it as read to lock the message. $this->add_flag_to_message($messageid, self::MESSAGE_SEEN); // Now pass it through the Inbound Message processor. $status = $this->addressmanager->process_envelope($recipient, $sender); if (($status & ~ \core\message\inbound\address_manager::VALIDATION_DISABLED_HANDLER) !== $status) { // The handler is disabled. mtrace("-- Skipped message - Handler is disabled. Fail code {$status}"); // In order to handle the user error, we need more information about the message being failed. $this->process_message_data($envelope, $messagedata, $messageid); $this->inform_user_of_error(get_string('handlerdisabled', 'tool_messageinbound', $this->currentmessagedata)); return; } // Check the validation status early. No point processing garbage messages, but we do need to process it // for some validation failure types. if (!$this->passes_key_validation($status, $messageid)) { // None of the above validation failures were found. Skip this message. mtrace("-- Skipped message - it does not appear to relate to a Inbound Message pickup. Fail code {$status}"); // Remove the seen flag from the message as there may be multiple recipients. $this->remove_flag_from_message($messageid, self::MESSAGE_SEEN); // Skip further processing for this recipient. continue; } // Process the message as the user. $user = $this->addressmanager->get_data()->user; mtrace("-- Processing the message as user {$user->id} ({$user->username})."); cron_setup_user($user); // Process and retrieve the message data for this message. // This includes fetching the full content, as well as all headers, and attachments. if (!$this->process_message_data($envelope, $messagedata, $messageid)) { mtrace("--- Message could not be found on the server. Is another process removing messages?"); return; } // When processing validation replies, we need to skip the sender verification phase as this has been // manually completed. if (!$skipsenderverification && $status !== 0) { // Check the validation status for failure types which require confirmation. // The validation result is tested in a bitwise operation. mtrace("-- Message did not meet validation but is possibly recoverable. Fail code {$status}"); // This is a recoverable error, but requires user input. if ($this->handle_verification_failure($messageid, $recipient)) { mtrace("--- Original message retained on mail server and confirmation message sent to user."); } else { mtrace("--- Invalid Recipient Handler - unable to save. Informing the user of the failure."); $this->inform_user_of_error(get_string('invalidrecipientfinal', 'tool_messageinbound', $this->currentmessagedata)); } // Returning to normal cron user. mtrace("-- Returning to the original user."); cron_setup_user($originaluser); return; } // Add the content and attachment data. mtrace("-- Validation completed. Fetching rest of message content."); $this->process_message_data_body($messagedata, $messageid); // The message processor throws exceptions upon failure. These must be caught and notifications sent to // the user here. try { $result = $this->send_to_handler(); } catch (\core\message\inbound\processing_failed_exception $e) { // We know about these kinds of errors and they should result in the user being notified of the // failure. Send the user a notification here. $this->inform_user_of_error($e->getMessage()); // Returning to normal cron user. mtrace("-- Returning to the original user."); cron_setup_user($originaluser); return; } catch (\Exception $e) { // An unknown error occurred. The user is not informed, but the administrator is. mtrace("-- Message processing failed. An unexpected exception was thrown. Details follow."); mtrace($e->getMessage()); // Returning to normal cron user. mtrace("-- Returning to the original user."); cron_setup_user($originaluser); return; } if ($result) { // Handle message cleanup. Messages are deleted once fully processed. mtrace("-- Marking the message for removal."); $this->add_flag_to_message($messageid, self::MESSAGE_DELETED); } else { mtrace("-- The Inbound Message processor did not return a success status. Skipping message removal."); } // Returning to normal cron user. mtrace("-- Returning to the original user."); cron_setup_user($originaluser); mtrace("-- Finished processing " . $message->getUid()); // Skip the outer loop too. The message has already been processed and it could be possible for there to // be two recipients in the envelope which match somehow. return; } } /** * Process a message to retrieve it's header data without body and attachemnts. * * @param \Horde_Imap_Client_Data_Envelope $envelope The Envelope of the message * @param \Horde_Imap_Client_Data_Fetch $basemessagedata The structure and part of the message body * @param string|\Horde_Imap_Client_Ids $messageid The Hore message Uid * @return \stdClass The current value of the messagedata */ private function process_message_data( \Horde_Imap_Client_Data_Envelope $envelope, \Horde_Imap_Client_Data_Fetch $basemessagedata, $messageid) { // Get the current mailbox. $mailbox = $this->get_mailbox(); // We need the structure at various points below. $structure = $basemessagedata->getStructure(); // Now fetch the rest of the message content. $query = new \Horde_Imap_Client_Fetch_Query(); $query->imapDate(); // Fetch the message header. $query->headerText(); // Retrieve the message with the above components. $messagedata = $this->client->fetch($mailbox, $query, array('ids' => $messageid))->first(); if (!$messagedata) { // Message was not found! Somehow it has been removed or is no longer returned. return null; } // The message ID should always be in the first part. $data = new \stdClass(); $data->messageid = $messagedata->getHeaderText(0, \Horde_Imap_Client_Data_Fetch::HEADER_PARSE)->getValue('Message-ID'); $data->subject = $envelope->subject; $data->timestamp = $messagedata->getImapDate()->__toString(); $data->envelope = $envelope; $data->data = $this->addressmanager->get_data(); $data->headers = $messagedata->getHeaderText(); $this->currentmessagedata = $data; return $this->currentmessagedata; } /** * Process a message again to add body and attachment data. * * @param \Horde_Imap_Client_Data_Fetch $basemessagedata The structure and part of the message body * @param string|\Horde_Imap_Client_Ids $messageid The Hore message Uid * @return \stdClass The current value of the messagedata */ private function process_message_data_body( \Horde_Imap_Client_Data_Fetch $basemessagedata, $messageid) { global $CFG; // Get the current mailbox. $mailbox = $this->get_mailbox(); // We need the structure at various points below. $structure = $basemessagedata->getStructure(); // Now fetch the rest of the message content. $query = new \Horde_Imap_Client_Fetch_Query(); $query->fullText(); // Fetch all of the message parts too. $typemap = $structure->contentTypeMap(); foreach ($typemap as $part => $type) { // The body of the part - attempt to decode it on the server. $query->bodyPart($part, array( 'decode' => true, 'peek' => true, )); $query->bodyPartSize($part); } $messagedata = $this->client->fetch($mailbox, $query, array('ids' => $messageid))->first(); // Store the data for this message. $contentplain = ''; $contenthtml = ''; $attachments = array( 'inline' => array(), 'attachment' => array(), ); $plainpartid = $structure->findBody('plain'); $htmlpartid = $structure->findBody('html'); foreach ($typemap as $part => $type) { // Get the message data from the body part, and combine it with the structure to give a fully-formed output. $stream = $messagedata->getBodyPart($part, true); $partdata = $structure->getPart($part); $partdata->setContents($stream, array( 'usestream' => true, )); if ($part == $plainpartid) { $contentplain = $this->process_message_part_body($messagedata, $partdata, $part); } else if ($part == $htmlpartid) { $contenthtml = $this->process_message_part_body($messagedata, $partdata, $part); } else if ($filename = $partdata->getName($part)) { if ($attachment = $this->process_message_part_attachment($messagedata, $partdata, $part, $filename)) { // The disposition should be one of 'attachment', 'inline'. // If an empty string is provided, default to 'attachment'. $disposition = $partdata->getDisposition(); $disposition = $disposition == 'inline' ? 'inline' : 'attachment'; $attachments[$disposition][] = $attachment; } } // We don't handle any of the other MIME content at this stage. } // The message ID should always be in the first part. $this->currentmessagedata->plain = $contentplain; $this->currentmessagedata->html = $contenthtml; $this->currentmessagedata->attachments = $attachments; return $this->currentmessagedata; } /** * Process the messagedata and part data to extract the content of this part. * * @param \Horde_Imap_Client_Data_Fetch $messagedata The structure and part of the message body * @param \Horde_Mime_Part $partdata The part data * @param string $part The part ID * @return string */ private function process_message_part_body($messagedata, $partdata, $part) { // This is a content section for the main body. // Get the string version of it. $content = $messagedata->getBodyPart($part); if (!$messagedata->getBodyPartDecode($part)) { // Decode the content. $partdata->setContents($content); $content = $partdata->getContents(); } // Convert the text from the current encoding to UTF8. $content = \core_text::convert($content, $partdata->getCharset()); // Fix any invalid UTF8 characters. // Note: XSS cleaning is not the responsibility of this code. It occurs immediately before display when // format_text is called. $content = clean_param($content, PARAM_RAW); return $content; } /** * Process a message again to add body and attachment data. * * @param \Horde_Imap_Client_Data_Fetch $messagedata The structure and part of the message body * @param \Horde_Mime_Part $partdata The part data * @param string $part The part ID. * @param string $filename The filename of the attachment * @return \stdClass * @throws \core\message\inbound\processing_failed_exception If the attachment can't be saved to disk. */ private function process_message_part_attachment($messagedata, $partdata, $part, $filename) { global $CFG; // If a filename is present, assume that this part is an attachment. $attachment = new \stdClass(); $attachment->filename = $filename; $attachment->type = $partdata->getType(); $attachment->content = $partdata->getContents(); $attachment->charset = $partdata->getCharset(); $attachment->description = $partdata->getDescription(); $attachment->contentid = $partdata->getContentId(); $attachment->filesize = $partdata->getBytes(); if (!empty($CFG->antiviruses)) { mtrace("--> Attempting virus scan of '{$attachment->filename}'"); // Perform a virus scan now. try { \core\antivirus\manager::scan_data($attachment->content); } catch (\core\antivirus\scanner_exception $e) { mtrace("--> A virus was found in the attachment '{$attachment->filename}'."); $this->inform_attachment_virus(); return; } } return $attachment; } /** * Check whether the key provided is valid. * * @param bool $status * @param mixed $messageid The Hore message Uid * @return bool */ private function passes_key_validation($status, $messageid) { // The validation result is tested in a bitwise operation. if (( $status & ~ \core\message\inbound\address_manager::VALIDATION_SUCCESS & ~ \core\message\inbound\address_manager::VALIDATION_UNKNOWN_DATAKEY & ~ \core\message\inbound\address_manager::VALIDATION_EXPIRED_DATAKEY & ~ \core\message\inbound\address_manager::VALIDATION_INVALID_HASH & ~ \core\message\inbound\address_manager::VALIDATION_ADDRESS_MISMATCH) !== 0) { // One of the above bits was found in the status - fail the validation. return false; } return true; } /** * Add the specified flag to the message. * * @param mixed $messageid * @param string $flag The flag to add */ private function add_flag_to_message($messageid, $flag) { // Get the current mailbox. $mailbox = $this->get_mailbox(); // Mark it as read to lock the message. $this->client->store($mailbox, array( 'ids' => new \Horde_Imap_Client_Ids($messageid), 'add' => $flag, )); } /** * Remove the specified flag from the message. * * @param mixed $messageid * @param string $flag The flag to remove */ private function remove_flag_from_message($messageid, $flag) { // Get the current mailbox. $mailbox = $this->get_mailbox(); // Mark it as read to lock the message. $this->client->store($mailbox, array( 'ids' => $messageid, 'delete' => $flag, )); } /** * Check whether the message has the specified flag * * @param mixed $messageid * @param string $flag The flag to check * @return bool */ private function message_has_flag($messageid, $flag) { // Get the current mailbox. $mailbox = $this->get_mailbox(); // Grab messagedata including flags. $query = new \Horde_Imap_Client_Fetch_Query(); $query->flags(); $query->structure(); $messagedata = $this->client->fetch($mailbox, $query, array( 'ids' => $messageid, ))->first(); $flags = $messagedata->getFlags(); return in_array($flag, $flags); } /** * Ensure that all mailboxes exist. */ private function ensure_mailboxes_exist() { $requiredmailboxes = array( self::MAILBOX, $this->get_confirmation_folder(), ); $existingmailboxes = $this->client->listMailboxes($requiredmailboxes); foreach ($requiredmailboxes as $mailbox) { if (isset($existingmailboxes[$mailbox])) { // This mailbox was found. continue; } mtrace("Unable to find the '{$mailbox}' mailbox - creating it."); $this->client->createMailbox($mailbox); } } /** * Attempt to determine whether this message is a bulk message (e.g. automated reply). * * @param \Horde_Imap_Client_Data_Fetch $message The message to process * @param string|\Horde_Imap_Client_Ids $messageid The Hore message Uid * @return boolean */ private function is_bulk_message( \Horde_Imap_Client_Data_Fetch $message, $messageid) { $query = new \Horde_Imap_Client_Fetch_Query(); $query->headerText(array('peek' => true)); $messagedata = $this->client->fetch($this->get_mailbox(), $query, array('ids' => $messageid))->first(); // Assume that this message is not bulk to begin with. $isbulk = false; // An auto-reply may itself include the Bulk Precedence. $precedence = $messagedata->getHeaderText(0, \Horde_Imap_Client_Data_Fetch::HEADER_PARSE)->getValue('Precedence'); $isbulk = $isbulk || strtolower($precedence) == 'bulk'; // If the X-Autoreply header is set, and not 'no', then this is an automatic reply. $autoreply = $messagedata->getHeaderText(0, \Horde_Imap_Client_Data_Fetch::HEADER_PARSE)->getValue('X-Autoreply'); $isbulk = $isbulk || ($autoreply && $autoreply != 'no'); // If the X-Autorespond header is set, and not 'no', then this is an automatic response. $autorespond = $messagedata->getHeaderText(0, \Horde_Imap_Client_Data_Fetch::HEADER_PARSE)->getValue('X-Autorespond'); $isbulk = $isbulk || ($autorespond && $autorespond != 'no'); // If the Auto-Submitted header is set, and not 'no', then this is a non-human response. $autosubmitted = $messagedata->getHeaderText(0, \Horde_Imap_Client_Data_Fetch::HEADER_PARSE)->getValue('Auto-Submitted'); $isbulk = $isbulk || ($autosubmitted && $autosubmitted != 'no'); return $isbulk; } /** * Send the message to the appropriate handler. * * @return bool * @throws \core\message\inbound\processing_failed_exception if anything goes wrong. */ private function send_to_handler() { try { mtrace("--> Passing to Inbound Message handler {$this->addressmanager->get_handler()->classname}"); if ($result = $this->addressmanager->handle_message($this->currentmessagedata)) { $this->inform_user_of_success($this->currentmessagedata, $result); // Request that this message be marked for deletion. return true; } } catch (\core\message\inbound\processing_failed_exception $e) { mtrace("-> The Inbound Message handler threw an exception. Unable to process this message. The user has been informed."); mtrace("--> " . $e->getMessage()); // Throw the exception again, with additional data. $error = new \stdClass(); $error->subject = $this->currentmessagedata->envelope->subject; $error->message = $e->getMessage(); throw new \core\message\inbound\processing_failed_exception('messageprocessingfailed', 'tool_messageinbound', $error); } catch (\Exception $e) { mtrace("-> The Inbound Message handler threw an exception. Unable to process this message. User informed."); mtrace("--> " . $e->getMessage()); // An unknown error occurred. Still inform the user but, this time do not include the specific // message information. $error = new \stdClass(); $error->subject = $this->currentmessagedata->envelope->subject; throw new \core\message\inbound\processing_failed_exception('messageprocessingfailedunknown', 'tool_messageinbound', $error); } // Something went wrong and the message was not handled well in the Inbound Message handler. mtrace("-> The Inbound Message handler reported an error. The message may not have been processed."); // It is the responsiblity of the handler to throw an appropriate exception if the message was not processed. // Do not inform the user at this point. return false; } /** * Handle failure of sender verification. * * This will send a notification to the user identified in the Inbound Message address informing them that a message has been * stored. The message includes a verification link and reply-to address which is handled by the * invalid_recipient_handler. * * @param \Horde_Imap_Client_Ids $messageids * @param string $recipient The message recipient * @return bool */ private function handle_verification_failure( \Horde_Imap_Client_Ids $messageids, $recipient) { global $DB, $USER; if (!$messageid = $this->currentmessagedata->messageid) { mtrace("---> Warning: Unable to determine the Message-ID of the message."); return false; } // Move the message into a new mailbox. $this->client->copy(self::MAILBOX, $this->get_confirmation_folder(), array( 'create' => true, 'ids' => $messageids, 'move' => true, )); // Store the data from the failed message in the associated table. $record = new \stdClass(); $record->messageid = $messageid; $record->userid = $USER->id; $record->address = $recipient; $record->timecreated = time(); $record->id = $DB->insert_record('messageinbound_messagelist', $record); // Setup the Inbound Message generator for the invalid recipient handler. $addressmanager = new \core\message\inbound\address_manager(); $addressmanager->set_handler('\tool_messageinbound\message\inbound\invalid_recipient_handler'); $addressmanager->set_data($record->id); $eventdata = new \core\message\message(); $eventdata->component = 'tool_messageinbound'; $eventdata->name = 'invalidrecipienthandler'; $userfrom = clone $USER; $userfrom->customheaders = array(); // Adding the In-Reply-To header ensures that it is seen as a reply. $userfrom->customheaders[] = 'In-Reply-To: ' . $messageid; // The message will be sent from the intended user. $eventdata->courseid = SITEID; $eventdata->userfrom = \core_user::get_noreply_user(); $eventdata->userto = $USER; $eventdata->subject = $this->get_reply_subject($this->currentmessagedata->envelope->subject); $eventdata->fullmessage = get_string('invalidrecipientdescription', 'tool_messageinbound', $this->currentmessagedata); $eventdata->fullmessageformat = FORMAT_PLAIN; $eventdata->fullmessagehtml = get_string('invalidrecipientdescriptionhtml', 'tool_messageinbound', $this->currentmessagedata); $eventdata->smallmessage = $eventdata->fullmessage; $eventdata->notification = 1; $eventdata->replyto = $addressmanager->generate($USER->id); mtrace("--> Sending a message to the user to report an verification failure."); if (!message_send($eventdata)) { mtrace("---> Warning: Message could not be sent."); return false; } return true; } /** * Inform the identified sender of a processing error. * * @param string $error The error message */ private function inform_user_of_error($error) { global $USER; // The message will be sent from the intended user. $userfrom = clone $USER; $userfrom->customheaders = array(); if ($messageid = $this->currentmessagedata->messageid) { // Adding the In-Reply-To header ensures that it is seen as a reply and threading is maintained. $userfrom->customheaders[] = 'In-Reply-To: ' . $messageid; } $messagedata = new \stdClass(); $messagedata->subject = $this->currentmessagedata->envelope->subject; $messagedata->error = $error; $eventdata = new \core\message\message(); $eventdata->courseid = SITEID; $eventdata->component = 'tool_messageinbound'; $eventdata->name = 'messageprocessingerror'; $eventdata->userfrom = $userfrom; $eventdata->userto = $USER; $eventdata->subject = self::get_reply_subject($this->currentmessagedata->envelope->subject); $eventdata->fullmessage = get_string('messageprocessingerror', 'tool_messageinbound', $messagedata); $eventdata->fullmessageformat = FORMAT_PLAIN; $eventdata->fullmessagehtml = get_string('messageprocessingerrorhtml', 'tool_messageinbound', $messagedata); $eventdata->smallmessage = $eventdata->fullmessage; $eventdata->notification = 1; if (message_send($eventdata)) { mtrace("---> Notification sent to {$USER->email}."); } else { mtrace("---> Unable to send notification."); } } /** * Inform the identified sender that message processing was successful. * * @param \stdClass $messagedata The data for the current message being processed. * @param mixed $handlerresult The result returned by the handler. * @return bool */ private function inform_user_of_success(\stdClass $messagedata, $handlerresult) { global $USER; // Check whether the handler has a success notification. $handler = $this->addressmanager->get_handler(); $message = $handler->get_success_message($messagedata, $handlerresult); if (!$message) { mtrace("---> Handler has not defined a success notification e-mail."); return false; } // Wrap the message in the notification wrapper. $messageparams = new \stdClass(); $messageparams->html = $message->html; $messageparams->plain = $message->plain; $messagepreferencesurl = new \moodle_url("/message/notificationpreferences.php", array('id' => $USER->id)); $messageparams->messagepreferencesurl = $messagepreferencesurl->out(); $htmlmessage = get_string('messageprocessingsuccesshtml', 'tool_messageinbound', $messageparams); $plainmessage = get_string('messageprocessingsuccess', 'tool_messageinbound', $messageparams); // The message will be sent from the intended user. $userfrom = clone $USER; $userfrom->customheaders = array(); if ($messageid = $this->currentmessagedata->messageid) { // Adding the In-Reply-To header ensures that it is seen as a reply and threading is maintained. $userfrom->customheaders[] = 'In-Reply-To: ' . $messageid; } $messagedata = new \stdClass(); $messagedata->subject = $this->currentmessagedata->envelope->subject; $eventdata = new \core\message\message(); $eventdata->courseid = SITEID; $eventdata->component = 'tool_messageinbound'; $eventdata->name = 'messageprocessingsuccess'; $eventdata->userfrom = $userfrom; $eventdata->userto = $USER; $eventdata->subject = self::get_reply_subject($this->currentmessagedata->envelope->subject); $eventdata->fullmessage = $plainmessage; $eventdata->fullmessageformat = FORMAT_PLAIN; $eventdata->fullmessagehtml = $htmlmessage; $eventdata->smallmessage = $eventdata->fullmessage; $eventdata->notification = 1; if (message_send($eventdata)) { mtrace("---> Success notification sent to {$USER->email}."); } else { mtrace("---> Unable to send success notification."); } return true; } /** * Return a formatted subject line for replies. * * @param string $subject The subject string * @return string The formatted reply subject */ private function get_reply_subject($subject) { $prefix = get_string('replysubjectprefix', 'tool_messageinbound'); if (!(substr($subject, 0, strlen($prefix)) == $prefix)) { $subject = $prefix . ' ' . $subject; } return $subject; } }