MDL-46284 core: Add e-mail fetcher from IMAP
authorAndrew Nicols <andrew@nicols.co.uk>
Wed, 16 Jul 2014 06:51:19 +0000 (14:51 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Tue, 7 Oct 2014 02:01:19 +0000 (10:01 +0800)
This issue is a part of the MDL-47194 Task.
This issue is a part of the MDL-39707 Epic.

21 files changed:
admin/tool/messageinbound/classes/edit_handler_form.php [new file with mode: 0644]
admin/tool/messageinbound/classes/manager.php [new file with mode: 0644]
admin/tool/messageinbound/classes/message/inbound/invalid_recipient_handler.php [new file with mode: 0644]
admin/tool/messageinbound/classes/task/cleanup_task.php [new file with mode: 0644]
admin/tool/messageinbound/classes/task/pickup_task.php [new file with mode: 0644]
admin/tool/messageinbound/db/messageinbound_handlers.php [new file with mode: 0644]
admin/tool/messageinbound/db/messages.php [new file with mode: 0644]
admin/tool/messageinbound/db/tasks.php [new file with mode: 0644]
admin/tool/messageinbound/index.php [new file with mode: 0644]
admin/tool/messageinbound/lang/en/tool_messageinbound.php [new file with mode: 0644]
admin/tool/messageinbound/renderer.php [new file with mode: 0644]
admin/tool/messageinbound/settings.php [new file with mode: 0644]
admin/tool/messageinbound/styles.css [new file with mode: 0644]
admin/tool/messageinbound/version.php [new file with mode: 0644]
lib/classes/message/inbound/handler.php
lib/classes/message/inbound/processing_failed_exception.php [new file with mode: 0644]
lib/classes/plugin_manager.php
lib/db/install.xml
lib/db/upgrade.php
message/tests/fixtures/inbound_fixtures.php
version.php

diff --git a/admin/tool/messageinbound/classes/edit_handler_form.php b/admin/tool/messageinbound/classes/edit_handler_form.php
new file mode 100644 (file)
index 0000000..bcb6af5
--- /dev/null
@@ -0,0 +1,106 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Form to edit handlers.
+ *
+ * @package    tool_messageinbound
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/formslib.php');
+
+/**
+ * Form to edit handlers.
+ *
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_messageinbound_edit_handler_form extends moodleform {
+    public function definition() {
+        $mform = $this->_form;
+
+        $handler = $this->_customdata['handler'];
+
+        // Set up the options for formatting text for descriptions, etc.
+        $formatoptions = new stdClass();
+        $formatoptions->trusted = false;
+        $formatoptions->noclean = false;
+        $formatoptions->smiley = false;
+        $formatoptions->filter = false;
+        $formatoptions->para = true;
+        $formatoptions->newlines = false;
+        $formatoptions->overflowdiv = true;
+
+        // General information about the handler.
+        $mform->addElement('header', 'general', get_string('general'));
+        $mform->addElement('static', 'name', get_string('name', 'tool_messageinbound'),
+            $handler->name);
+        $mform->addElement('static', 'classname', get_string('classname', 'tool_messageinbound'));
+
+        $description = format_text($handler->description, FORMAT_MARKDOWN, $formatoptions);
+
+        $mform->addElement('static', 'description', get_string('description', 'tool_messageinbound'),
+            $description);
+
+        // Items which can be configured.
+        $mform->addElement('header', 'configuration', get_string('configuration'));
+
+        $options = array(
+            HOURSECS => get_string('onehour', 'tool_messageinbound'),
+            DAYSECS => get_string('oneday', 'tool_messageinbound'),
+            WEEKSECS => get_string('oneweek', 'tool_messageinbound'),
+            YEARSECS => get_string('oneyear', 'tool_messageinbound'),
+            '' => get_string('noexpiry', 'tool_messageinbound'),
+        );
+        $mform->addElement('select', 'defaultexpiration', get_string('defaultexpiration', 'tool_messageinbound'), $options);
+        $mform->addHelpButton('defaultexpiration', 'defaultexpiration', 'tool_messageinbound');
+
+        if ($handler->can_change_validateaddress()) {
+            $mform->addElement('checkbox', 'validateaddress', get_string('requirevalidation', 'tool_messageinbound'));
+            $mform->addHelpButton('validateaddress', 'validateaddress', 'tool_messageinbound');
+        } else {
+            if ($handler->validateaddress) {
+                $text = get_string('yes');
+            } else {
+                $text = get_string('no');
+            }
+            $mform->addElement('static', 'validateaddress_fake', get_string('requirevalidation', 'tool_messageinbound'), $text);
+            $mform->addElement('hidden', 'validateaddress');
+            $mform->addHelpButton('validateaddress_fake', 'fixedvalidateaddress', 'tool_messageinbound');
+            $mform->setType('validateaddress', PARAM_INT);
+        }
+
+        if ($handler->can_change_enabled()) {
+            $mform->addElement('checkbox', 'enabled', get_string('enabled', 'tool_messageinbound'));
+        } else {
+            if ($handler->enabled) {
+                $text = get_string('yes');
+            } else {
+                $text = get_string('no');
+            }
+            $mform->addElement('static', 'enabled_fake', get_string('enabled', 'tool_messageinbound'), $text);
+            $mform->addHelpButton('enabled', 'fixedenabled', 'tool_messageinbound');
+            $mform->addElement('hidden', 'enabled');
+            $mform->setType('enabled', PARAM_INT);
+        }
+
+        $this->add_action_buttons(true, get_string('savechanges'));
+    }
+}
diff --git a/admin/tool/messageinbound/classes/manager.php b/admin/tool/messageinbound/classes/manager.php
new file mode 100644 (file)
index 0000000..ebfa8ab
--- /dev/null
@@ -0,0 +1,953 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 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,
+        );
+
+        $this->client = new \Horde_Imap_Client_Socket($configuration);
+
+        try {
+            $this->client->login();
+            mtrace("Connection established.");
+            return true;
+
+        } catch (\Horde_Imap_Client_Exception $e) {
+            $message = $e->getMessage();
+            mtrace("Unable to connect to IMAP server. Failed with '{$message}'");
+
+            return false;
+        }
+    }
+
+    /**
+     * Shutdown and close the connection to the IMAP client.
+     */
+    protected function close_connection() {
+        if ($this->client) {
+            $this->client->close();
+        }
+        $this->client = null;
+    }
+
+    /**
+     * Get the current mailbox information.
+     *
+     * @return \Horde_Imap_Client_Mailbox
+     */
+    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.
+     */
+    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.
+     */
+    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 '" . self::CONFIRMATIONFOLDER . "'");
+
+        // Match the message ID.
+        $search->headerText('message-id', $maildata->messageid);
+        $search->headerText('to', $maildata->address);
+
+        $results = $this->client->search(self::CONFIRMATIONFOLDER, $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(self::CONFIRMATIONFOLDER, $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 '" .
+                self::CONFIRMATIONFOLDER . "' folder.");
+        $this->client->openMailbox(self::CONFIRMATIONFOLDER);
+
+        $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;
+    }
+
+    /**
+     * 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 verificiation 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);
+
+        // 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);
+
+            // 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.
+            $this->process_message_data($envelope, $messagedata, $messageid);
+
+            // 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 $messagedata 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 all of the message parts too.
+        $typemap = $structure->contentTypeMap();
+        foreach ($typemap as $part => $type) {
+            // The header.
+            $query->headerText(array(
+                'id' => $part,
+            ));
+        }
+
+        $messagedata = $this->client->fetch($mailbox, $query, array('ids' => $messageid))->first();
+
+        // Store the data for this message.
+        $headers = '';
+
+        foreach ($typemap as $part => $type) {
+            // Grab all of the header data into a string.
+            $headers .= $messagedata->getHeaderText($part);
+
+            // We don't handle any of the other MIME content at this stage.
+        }
+
+        $data = new \stdClass();
+
+        // The message ID should always be in the first part.
+        $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 = $headers;
+
+        $this->currentmessagedata = $data;
+
+        return $this->currentmessagedata;
+    }
+
+    /**
+     * Process a message again to add body and attachment data.
+     *
+     * @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_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 $messagedata The structure and part of the message body
+     * @param $partdata The part data
+     * @param $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 $messagedata The structure and part of the message body
+     * @param $partdata The part data
+     * @param $filename The filename of the attachment
+     * @return \stdClass
+     */
+    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       = $messagedata->getBodyPartSize($part);
+
+        if (empty($CFG->runclamonupload) or empty($CFG->pathtoclam)) {
+            mtrace("--> Attempting virus scan of '{$attachment->filename}'");
+
+            // Store the file on disk - it will need to be virus scanned first.
+            $itemid = rand(1, 999999999);;
+            $directory = make_temp_directory("/messageinbound/{$itemid}", false);
+            $filepath = $directory . "/" . $attachment->filename;
+            if (!$fp = fopen($filepath, "w")) {
+                // Unable to open the temporary file to write this to disk.
+                mtrace("--> Unable to save the file to disk for virus scanning. Check file permissions.");
+
+                throw new \core\message\inbound\processing_failed_exception('attachmentfilepermissionsfailed',
+                        'tool_messageinbound');
+            }
+
+            fwrite($fp, $attachment->content);
+            fclose($fp);
+
+            // Perform a virus scan now.
+            try {
+                \repository::antivir_scan_file($filepath, $attachment->filename, true);
+            } catch (moodle_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 $status The Message to process
+     * @param $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 $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 $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 $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);
+    }
+
+    /**
+     * Send the message to the appropriate handler.
+     *
+     */
+    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 $recipient The message recipient
+     */
+    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, self::CONFIRMATIONFOLDER, 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 \stdClass();
+        $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->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 \stdClass();
+        $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.
+     */
+    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/edit.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 \stdClass();
+        $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 $subject string 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;
+    }
+}
diff --git a/admin/tool/messageinbound/classes/message/inbound/invalid_recipient_handler.php b/admin/tool/messageinbound/classes/message/inbound/invalid_recipient_handler.php
new file mode 100644 (file)
index 0000000..7ff3a69
--- /dev/null
@@ -0,0 +1,95 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * A Handler to re-process messages which previously failed sender
+ * verification.
+ *
+ * @package    task_messageinbound
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_messageinbound\message\inbound;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/repository/lib.php');
+
+/**
+ * A Handler to re-process messages which previously failed sender
+ * verification.
+ *
+ * This may happen if the user did not use their registerd e-mail address,
+ * the verification hash used had expired, or if some erroneous content was
+ * introduced into the content hash.
+ *
+ * @package    task
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class invalid_recipient_handler extends \core\message\inbound\handler {
+
+    /**
+     * Do not allow changes to the address validation setting.
+     */
+    public function allow_validateaddress_change() {
+        return false;
+    }
+
+    /**
+     * Return a description for the current handler.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return get_string('invalid_recipient_handler', 'tool_messageinbound');
+    }
+
+    /**
+     * Return a name for the current handler.
+     * This appears in the admin pages as a human-readable name.
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('invalid_recipient_handler_name', 'tool_messageinbound');
+    }
+
+    /**
+     * Process a message received and validated by the Inbound Message processor.
+     *
+     * @param $messagedata The Inbound Message record
+     * @param $messagedata The message data packet.
+     * @return bool Whether the message was successfully processed.
+     */
+    public function process_message(\stdClass $record, \stdClass $data) {
+        global $DB;
+
+        if (!$maildata = $DB->get_record('messageinbound_messagelist', array('id' => $record->datavalue))) {
+            // The message requested couldn't be found. Failing here will alert the user that we failed.
+            throw new \core\message\inbound\processing_failed_exception('oldmessagenotfound', 'tool_messageinbound');
+        }
+
+        mtrace("=== Request to re-process message {$record->datavalue} from server.");
+        mtrace("=== Message-Id:\t{$maildata->messageid}");
+        mtrace("=== Recipient:\t{$maildata->address}");
+
+        $manager = new \tool_messageinbound\manager();
+        return $manager->process_existing_message($maildata);
+    }
+
+}
diff --git a/admin/tool/messageinbound/classes/task/cleanup_task.php b/admin/tool/messageinbound/classes/task/cleanup_task.php
new file mode 100644 (file)
index 0000000..e507597
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * A scheduled task to handle cleanup of old, unconfirmed e-mails.
+ *
+ * @package    tool_messageinbound
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_messageinbound\task;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A scheduled task to handle cleanup of old, unconfirmed e-mails.
+ *
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cleanup_task extends \core\task\scheduled_task {
+
+    /**
+     * Get a descriptive name for this task (shown to admins).
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('taskcleanup', 'tool_messageinbound');
+    }
+
+    /**
+     * Execute the main Inbound Message pickup task.
+     */
+    public function execute() {
+        $manager = new \tool_messageinbound\manager();
+        return $manager->tidy_old_messages();
+    }
+}
diff --git a/admin/tool/messageinbound/classes/task/pickup_task.php b/admin/tool/messageinbound/classes/task/pickup_task.php
new file mode 100644 (file)
index 0000000..d10fb8c
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * A scheduled task to handle Inbound Message e-mail pickup.
+ *
+ * @package    tool_messageinbound
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_messageinbound\task;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A scheduled task to handle Inbound Message e-mail pickup.
+ *
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class pickup_task extends \core\task\scheduled_task {
+
+    /**
+     * Get a descriptive name for this task (shown to admins).
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('taskpickup', 'tool_messageinbound');
+    }
+
+    /**
+     * Execute the main Inbound Message pickup task.
+     */
+    public function execute() {
+        $manager = new \tool_messageinbound\manager();
+        return $manager->pickup_messages();
+    }
+}
diff --git a/admin/tool/messageinbound/db/messageinbound_handlers.php b/admin/tool/messageinbound/db/messageinbound_handlers.php
new file mode 100644 (file)
index 0000000..50cb34a
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Handlers for tool_messageinbound.
+ *
+ * @package    task
+ * @category   tool_messageinbound
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$handlers = array(
+    array(
+        'classname'         => '\tool_messageinbound\message\inbound\invalid_recipient_handler',
+        'enabled'           => true,
+        'validateaddress'   => false,
+    ),
+);
diff --git a/admin/tool/messageinbound/db/messages.php b/admin/tool/messageinbound/db/messages.php
new file mode 100644 (file)
index 0000000..c425c48
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Message Providers for task_messageinbound.
+ *
+ * @package    task
+ * @category   messageinbound
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$messageproviders = array (
+    // Invalid recipient handler.
+    'invalidrecipienthandler'   => array(),
+
+    // A generic message processing error.
+    'messageprocessingerror'    => array(),
+
+    // A generic message processing success message.
+    'messageprocessingsuccess'    => array(),
+);
diff --git a/admin/tool/messageinbound/db/tasks.php b/admin/tool/messageinbound/db/tasks.php
new file mode 100644 (file)
index 0000000..6e4c659
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The Main Manager tasks.
+ *
+ * @package    tool_messageinbound
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$tasks = array(
+    array(
+        'classname' => '\tool_messageinbound\task\pickup_task',
+        'blocking' => 0,
+        'minute' => '*',
+        'hour' => '*',
+        'day' => '*',
+        'dayofweek' => '*',
+        'month' => '*'
+    ),
+
+    array(
+        'classname' => '\tool_messageinbound\task\cleanup_task',
+        'blocking' => 0,
+        'minute' => '55',
+        'hour' => '1',
+        'day' => '*',
+        'dayofweek' => '*',
+        'month' => '*'
+    ),
+);
diff --git a/admin/tool/messageinbound/index.php b/admin/tool/messageinbound/index.php
new file mode 100644 (file)
index 0000000..8aa16ad
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Inbound Message Settings pages.
+ *
+ * @package    tool_messageinbound
+ * @copyright  2014 Andrew NIcols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(dirname(__FILE__) . '/../../../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+require_once($CFG->libdir.'/tablelib.php');
+
+admin_externalpage_setup('messageinbound_handlers');
+
+$classname = optional_param('classname', '', PARAM_RAW);
+
+$pageurl = new moodle_url('/admin/tool/messageinbound/index.php');
+
+if (empty($classname)) {
+    $renderer = $PAGE->get_renderer('tool_messageinbound');
+
+    $records = $DB->get_recordset('messageinbound_handlers', null, 'enabled desc', 'classname');
+    $instances = array();
+    foreach ($records as $record) {
+        $instances[] = \core\message\inbound\manager::get_handler($record->classname);
+    }
+
+    echo $OUTPUT->header();
+    echo $renderer->messageinbound_handlers_table($instances);
+    echo $OUTPUT->footer();
+
+} else {
+    // Retrieve the handler and its record.
+    $handler = \core\message\inbound\manager::get_handler($classname);
+    $record = \core\message\inbound\manager::record_from_handler($handler);
+
+    $formurl = new moodle_url($PAGE->url, array('classname' => $classname));
+    $mform = new tool_messageinbound_edit_handler_form($formurl, array(
+            'handler' => $handler,
+    ));
+
+    if ($mform->is_cancelled()) {
+        redirect($PAGE->url);
+    } else if ($data = $mform->get_data()) {
+        // Update the record from the form.
+        $record->defaultexpiration = (int) $data->defaultexpiration;
+
+        if ($handler->can_change_validateaddress()) {
+            $record->validateaddress = (int) $data->validateaddress;
+        }
+
+        if ($handler->can_change_enabled()) {
+            $record->enabled = (int) $data->enabled;
+        }
+        $DB->update_record('messageinbound_handlers', $record);
+        redirect($PAGE->url);
+    }
+
+    // Add the breadcrumb.
+    $pageurl->param('classname', $handler->classname);
+    $PAGE->navbar->add($handler->name, $pageurl);
+    echo $OUTPUT->header();
+    echo $OUTPUT->heading(get_string('editinghandler', 'tool_messageinbound', $handler->name));
+    $mform->set_data($record);
+    $mform->display();
+    echo $OUTPUT->footer();
+
+}
diff --git a/admin/tool/messageinbound/lang/en/tool_messageinbound.php b/admin/tool/messageinbound/lang/en/tool_messageinbound.php
new file mode 100644 (file)
index 0000000..19c5c75
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Strings for component 'tool_messageinbound', language 'en'
+ *
+ * @package    tool_messageinbound
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['classname'] = 'Class name';
+$string['component'] = 'Component';
+$string['configmessageinboundhost'] = 'The address of the server that Moodle should check mail against. To specify a non-default port, you can use the [server]:[port], for example mail.example.com:587 format. If you leave this field blank, Moodle will use the default port for the type of mail server you specify.';
+$string['defaultexpiration'] = 'Default address expiry period';
+$string['defaultexpiration_help'] = 'When an email address is generated by the handler, it can be set to automatically expire after a period of time, so that it can no longer be used. It is advisable to set an expiry period.';
+$string['description'] = 'Description';
+$string['domain'] = 'Email Domain';
+$string['edit'] = 'Edit';
+$string['edithandler'] = 'Edit settings for the {$a} handler';
+$string['editinghandler'] = 'Editing {$a}';
+$string['enabled'] = 'Enabled';
+$string['fixedvalidateaddress'] = 'Validate sender address';
+$string['fixedvalidateaddress_help'] = 'You cannot change the address validation for this handler. This may be because the handler requires a specific setting.';
+$string['fixedenabled_help'] = 'You cannot change the state of this handler. This may be because the handler is required by other handlers.';
+$string['incomingmailconfiguration'] = 'Incoming mail configuration';
+$string['incomingmailserversettings'] = 'Incoming mail server settings';
+$string['incomingmailserversettings_desc'] = 'Moodle is capable of connecting to appropriately configured IMAP servers. You can specify the settings used to connect to your IMAP server here.';
+$string['invalid_recipient_handler'] = 'If a valid message is received but the sender cannot be authenticated, the message is stored on the email server and the user is contacted using the email address in their user profile. The user is given the chance to reply to confirm the authenticity of the original message.
+
+This handler processes those replies.
+
+It is not possible to disable sender verification of this handler because the user may reply from an incorrect email address if their email client configuration is incorrect.';
+$string['invalid_recipient_handler_name'] = 'Invalid recipient handler';
+$string['invalidrecipientdescription'] = 'The message "{$a->subject}" could not be authenticated, since it was sent from a different email address than in your user profile. For the message to be authenticated, you need to reply to this message.';
+$string['invalidrecipientdescriptionhtml'] = 'The message "{$a->subject}" could not be authenticated, since it was sent from a different email address than in your user profile. For the message to be authenticated, you need to reply to this message.';
+$string['invalidrecipientfinal'] = 'The message you sent with subject "{$a->subject}" could not be authenticated. Please check that you are sending your message from the e-mail account listed in your Moodle profile.';
+$string['mailbox'] = 'Mailbox name';
+$string['mailboxconfiguration'] = 'Mailbox configuration';
+$string['mailboxdescription'] = '[mailbox]+subaddress@[domain]';
+$string['mailsettings'] = 'Mail settings';
+$string['message_handlers'] = 'Message handlers';
+$string['messageprocessingerror'] = 'You recently sent an e-mail to Moodle with the subject "{$a->subject}" but Moodle was unable to process it.
+
+The details of the error are shown below.
+
+{$a->error}';
+$string['messageprocessingerrorhtml'] = '<p>You recently sent an e-mail to Moodle with the subject "{$a->subject}" but Moodle was unable to process it.</p>
+<p>The details of the error are shown below.</p>
+<p>{$a->error}</p>';
+$string['messageprocessingfailed'] = 'Moodle was unable to process the e-mail you sent with subject "{$a->subject}". The following error was given: "{$a->message}".';
+$string['messageprocessingfailedunknown'] = 'Moodle was unable to process the e-mail you sent with subject "{$a->subject}". Contact your system administrator for further information.';
+$string['messageprocessingsuccess'] = '{$a->plain}
+
+If you do not wish to receive these notifications in the future, you can edit your personal messaging preferences by opening {$a->messagepreferencesurl} in your browser.';
+$string['messageprocessingsuccesshtml'] = '{$a->html}
+<p>If you do not wish to receive these notifications in the future, you can <a href="{$a->messagepreferencesurl}">edit your personal messaging preferences</a>.</p>';
+$string['messageinbound'] = 'Message Inbound';
+$string['messageinboundenabled'] = 'Enable incoming mail processing';
+$string['messageinboundenabled_desc'] = 'Incoming mail processing must be enabled in order for messages to be sent with the appropriate information.';
+$string['messageinboundgeneralconfiguration'] = 'General configuration';
+$string['messageinboundgeneralconfiguration_desc'] = 'Inbound message processing allows you to receive and process email within Moodle. This has applications such as sending email replies to forum posts or adding files to a user\'s private files.';
+$string['messageinboundhost'] = 'Incoming Mail Server';
+$string['messageinboundhostpass'] = 'Password';
+$string['messageinboundhostpass_desc'] = 'This is the password your service provider will have provided to log into your e-mail account with.';
+$string['messageinboundhostssl'] = 'Use SSL';
+$string['messageinboundhostssl_desc'] = 'Some mail servers support an additional level of security by encrypting communication between Moodle and your server. We recommend using this SSL encryption if your server supports it.';
+$string['messageinboundhosttype'] = 'Server type';
+$string['messageinboundhostuser'] = 'Username';
+$string['messageinboundhostuser_desc'] = 'This is the username your service provider will have provided to log into your e-mail account with.';
+$string['messageinboundmailboxconfiguration_desc'] = 'When messages are sent out, they fit into the format address+data@example.com. To reliably generate addresses from Moodle, please specify the address that you would normally use before the @ sign, and the domain after the @ sign separately. For example, the Mailbox name in the example would be "address", and the E-mail domain would be "example.com". You should use a dedicated e-mail account for this purpose.';
+$string['messageprovider:invalidrecipienthandler'] = 'Messages to confirm that an inbound messages came from you';
+$string['messageprovider:messageprocessingerror'] = 'Warning when an inbound message could not be processed';
+$string['messageprovider:messageprocessingsuccess'] = 'Confirmation that a message was successfully processed';
+$string['noencryption'] = 'Off - No encryption';
+$string['noexpiry'] = 'No expiry';
+$string['oldmessagenotfound'] = 'You tried to manually authenticate a message, but the message could not be found. This could be because it has already been processed, or because the message expired.';
+$string['oneday'] = 'One day';
+$string['onehour'] = 'One hour';
+$string['oneweek'] = 'One week';
+$string['oneyear'] = 'One year';
+$string['pluginname'] = 'Inbound message configuration';
+$string['replysubjectprefix'] = 'Re:';
+$string['requirevalidation'] = 'Validate sender address';
+$string['name'] = 'Name';
+$string['ssl'] = 'SSL (Auto-detect SSL version)';
+$string['sslv2'] = 'SSLv2 (Force SSL Version 2)';
+$string['sslv3'] = 'SSLv2 (Force SSL Version 3)';
+$string['taskcleanup'] = 'Cleanup of unverified incoming email';
+$string['taskpickup'] = 'Incoming email pickup';
+$string['tls'] = 'TLS (TLS; started via protocol-level negotiation over unencrypted channel; RECOMMENDED way of initiating secure connection)';
+$string['tlsv1'] = 'TLSv1 (TLS direct version 1.x connection to server)';
+$string['validateaddress'] = 'Validate sender email address';
+$string['validateaddress_help'] = 'When a message is received from a user, Moodle attempts to validate the message by comparing the email address of the sender with the email address in their user profile.
+
+If the sender does not match, then the user is sent a notification to confirm that they really did send the email.
+
+If this setting is disabled, then the email address of the sender is not checked at all.';
diff --git a/admin/tool/messageinbound/renderer.php b/admin/tool/messageinbound/renderer.php
new file mode 100644 (file)
index 0000000..d383d74
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Output rendering for the plugin.
+ *
+ * @package     tool_messageinbound
+ * @copyright   2014 Andrew Nicols
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Implements the plugin renderer
+ *
+ * @copyright 2014 Andrew Nicols
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_messageinbound_renderer extends plugin_renderer_base {
+
+    /**
+     * Render a table listing all of the Inbound Message handlers.
+     *
+     * @param array $handlers - list of all messageinbound handlers.
+     * @return string HTML to output.
+     */
+    public function messageinbound_handlers_table(array $handlers) {
+        global $CFG;
+
+        $table = new html_table();
+        $handlername = new html_table_cell(get_string('name', 'tool_messageinbound') . "\n" .
+                html_writer::tag('span', get_string('classname', 'tool_messageinbound'), array('class' => 'handler-function')));
+
+        // Prepare some of the rows with additional styling.
+        $enabled = new html_table_cell(get_string('enabled', 'tool_messageinbound'));
+        $enabled->attributes['class'] = 'state';
+        $edit = new html_table_cell(get_string('edit', 'tool_messageinbound'));
+        $edit->attributes['class'] = 'edit';
+        $table->head  = array(
+                $handlername,
+                get_string('description', 'tool_messageinbound'),
+                $enabled,
+                $edit,
+            );
+        $table->attributes['class'] = 'admintable generaltable messageinboundhandlers';
+
+        $yes = get_string('yes');
+        $no = get_string('no');
+
+        $data = array();
+
+        // Options for description formatting.
+        $descriptionoptions = new stdClass();
+        $descriptionoptions->trusted = false;
+        $descriptionoptions->noclean = false;
+        $descriptionoptions->smiley = false;
+        $descriptionoptions->filter = false;
+        $descriptionoptions->para = true;
+        $descriptionoptions->newlines = false;
+        $descriptionoptions->overflowdiv = true;
+
+        $editurlbase = new moodle_url('/admin/tool/messageinbound/index.php');
+        foreach ($handlers as $handler) {
+            $handlername = new html_table_cell($handler->name . "\n" .
+                    html_writer::tag('span', $handler->classname, array('class' => 'handler-function')));
+            $handlername->header = true;
+
+            $editurl = new moodle_url($editurlbase, array('classname' => $handler->classname));
+            $editlink = $this->action_icon($editurl, new pix_icon('t/edit',
+                    get_string('edithandler', 'tool_messageinbound', $handler->classname)));
+
+            // Prepare some of the rows with additional styling.
+            $enabled = new html_table_cell($handler->enabled ? $yes : $no);
+            $enabled->attributes['class'] = 'state';
+            $edit = new html_table_cell($editlink);
+            $edit->attributes['class'] = 'edit';
+
+            // Add the row.
+            $row = new html_table_row(array(
+                        $handlername,
+                        format_text($handler->description, FORMAT_MARKDOWN, $descriptionoptions),
+                        $enabled,
+                        $edit,
+                    ));
+
+            if (!$handler->enabled) {
+                $row->attributes['class'] = 'disabled';
+            }
+            $data[] = $row;
+        }
+        $table->data = $data;
+        return html_writer::table($table);
+    }
+
+}
diff --git a/admin/tool/messageinbound/settings.php b/admin/tool/messageinbound/settings.php
new file mode 100644 (file)
index 0000000..72d23ab
--- /dev/null
@@ -0,0 +1,87 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Inbound Message Settings.
+ *
+ * @package    tool_messageinbound
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+if ($hassiteconfig) {
+    $category = new admin_category('messageinbound', new lang_string('incomingmailconfiguration', 'tool_messageinbound'));
+
+    // Create a settings page for all of the mail server settings.
+    $settings = new admin_settingpage('messageinbound_mailsettings', new lang_string('mailsettings', 'tool_messageinbound'));
+
+    $settings->add(new admin_setting_heading('messageinbound_generalconfiguration',
+            new lang_string('messageinboundgeneralconfiguration', 'tool_messageinbound'),
+            new lang_string('messageinboundgeneralconfiguration_desc', 'tool_messageinbound'), ''));
+    $settings->add(new admin_setting_configcheckbox('messageinbound_enabled',
+            new lang_string('messageinboundenabled', 'tool_messageinbound'),
+            new lang_string('messageinboundenabled_desc', 'tool_messageinbound'), 0));
+
+    // These settings are used when generating a Inbound Message address.
+    $settings->add(new admin_setting_heading('messageinbound_mailboxconfiguration',
+            new lang_string('mailboxconfiguration', 'tool_messageinbound'),
+            new lang_string('messageinboundmailboxconfiguration_desc', 'tool_messageinbound'), ''));
+    $settings->add(new admin_setting_configtext('messageinbound_mailbox',
+            new lang_string('mailbox', 'tool_messageinbound'),
+            null, '', PARAM_RAW));
+    $settings->add(new admin_setting_configtext('messageinbound_domain',
+            new lang_string('domain', 'tool_messageinbound'),
+            null, '', PARAM_RAW));
+
+    // These settings are used when checking the incoming mailbox for mail.
+    $settings->add(new admin_setting_heading('messageinbound_serversettings',
+            new lang_string('incomingmailserversettings', 'tool_messageinbound'),
+            new lang_string('incomingmailserversettings_desc', 'tool_messageinbound'), ''));
+    $settings->add(new admin_setting_configtext('messageinbound_host',
+            new lang_string('messageinboundhost', 'tool_messageinbound'),
+            new lang_string('configmessageinboundhost', 'tool_messageinbound'), '', PARAM_RAW));
+
+    $options = array(
+        ''          => get_string('noencryption',   'tool_messageinbound'),
+        'ssl'       => get_string('ssl',            'tool_messageinbound'),
+        'sslv2'     => get_string('sslv2',          'tool_messageinbound'),
+        'sslv3'     => get_string('sslv3',          'tool_messageinbound'),
+        'tls'       => get_string('tls',            'tool_messageinbound'),
+        'tlsv1'     => get_string('tlsv1',          'tool_messageinbound'),
+    );
+    $settings->add(new admin_setting_configselect('messageinbound_hostssl',
+            new lang_string('messageinboundhostssl', 'tool_messageinbound'),
+            new lang_string('messageinboundhostssl_desc', 'tool_messageinbound'), 'ssl', $options));
+
+    $settings->add(new admin_setting_configtext('messageinbound_hostuser',
+            new lang_string('messageinboundhostuser', 'tool_messageinbound'),
+            new lang_string('messageinboundhostuser_desc', 'tool_messageinbound'), '', PARAM_NOTAGS));
+    $settings->add(new admin_setting_configpasswordunmask('messageinbound_hostpass',
+            new lang_string('messageinboundhostpass', 'tool_messageinbound'),
+            new lang_string('messageinboundhostpass_desc', 'tool_messageinbound'), ''));
+
+    $category->add('messageinbound', $settings);
+
+    // Link to the external page for Inbound Message handler configuration.
+    $category->add('messageinbound', new admin_externalpage('messageinbound_handlers',
+            new lang_string('message_handlers', 'tool_messageinbound'),
+            "$CFG->wwwroot/$CFG->admin/tool/messageinbound/index.php"));
+
+    // Add the category to the admin tree.
+    $ADMIN->add('server', $category);
+}
diff --git a/admin/tool/messageinbound/styles.css b/admin/tool/messageinbound/styles.css
new file mode 100644 (file)
index 0000000..fba5ddc
--- /dev/null
@@ -0,0 +1,11 @@
+#page-admin-tool-messageinbound-index .handler-function {
+    display: block;
+    padding: 0 0.5em;
+    color: #888;
+    font-size: 0.75em;
+}
+
+#page-admin-tool-messageinbound-index .state,
+#page-admin-tool-messageinbound-index .edit {
+    text-align: center;
+}
diff --git a/admin/tool/messageinbound/version.php b/admin/tool/messageinbound/version.php
new file mode 100644 (file)
index 0000000..57e55ae
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Plugin version info
+ *
+ * @package    tool_messageinbound
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version   = 2014091200;
+$plugin->requires  = 2014050800;
+$plugin->component = 'tool_messageinbound';
index 14b3533..ab92ee1 100644 (file)
@@ -33,6 +33,7 @@ namespace core\message\inbound;
  * @property-read string $component The component of this handler
  * @property-read int $defaultexpiration Default expiration of new addresses for this handler
  * @property-read string $description The description of this handler
+ * @property-read string $name The name of this handler
  * @property-read bool $validateaddress Whether the address validation is a requiredment
  * @property-read bool $enabled Whether this handler is currently enabled
  * @property-read string $classname The name of handler class
@@ -193,6 +194,14 @@ abstract class handler {
      */
     protected abstract function get_description();
 
+    /**
+     * Return a name for the current handler.
+     * This appears in the admin pages as a human-readable name.
+     *
+     * @return string
+     */
+    protected abstract function get_name();
+
     /**
      * Process the message against the current handler.
      *
diff --git a/lib/classes/message/inbound/processing_failed_exception.php b/lib/classes/message/inbound/processing_failed_exception.php
new file mode 100644 (file)
index 0000000..4c463b5
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Variable Envelope Return Path message processing failure exception.
+ *
+ * @package    core_message
+ * @copyright  2014 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\message\inbound;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Variable Envelope Return Path message processing failure exception.
+ *
+ * @copyright  2014 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class processing_failed_exception extends \moodle_exception {
+    /**
+     * Constructor
+     *
+     * @param string $identifier The string identifier to use when displaying this exception.
+     * @param string $component The string component
+     * @param stdClass $data The data to pass to get_string
+     */
+    public function __construct($identifier, $component, \stdClass $data = null) {
+        return parent::__construct($identifier, $component, '', $data);
+    }
+}
index b385127..3eca388 100644 (file)
@@ -1137,7 +1137,7 @@ class core_plugin_manager {
             'tool' => array(
                 'assignmentupgrade', 'availabilityconditions', 'behat', 'capability', 'customlang',
                 'dbtransfer', 'generator', 'health', 'innodb', 'installaddon',
-                'langimport', 'log', 'multilangupgrade', 'phpunit', 'profiling',
+                'langimport', 'log', 'messageinbound', 'multilangupgrade', 'phpunit', 'profiling',
                 'replace', 'spamcleaner', 'task', 'timezoneimport',
                 'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'xmldb'
             ),
index 5716152..ae46c3b 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20140918" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20141002" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <KEY NAME="handler" TYPE="foreign" FIELDS="handler" REFTABLE="messageinbound_handlers" REFFIELDS="id" COMMENT="The handler must point to a valid record in the messageinbound_handlers table."/>
       </KEYS>
     </TABLE>
+    <TABLE NAME="messageinbound_messagelist" COMMENT="A list of message IDs for existing replies">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="messageid" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="address" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="The Inbound Message address that the message was originally sent to"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
+      </KEYS>
+    </TABLE>
   </TABLES>
 </XMLDB>
index 2ae31fa..957a0ed 100644 (file)
@@ -3837,5 +3837,30 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2014100100.00);
     }
 
+    if ($oldversion < 2014100200.01) {
+
+        // Define table messageinbound_messagelist to be created.
+        $table = new xmldb_table('messageinbound_messagelist');
+
+        // Adding fields to table messageinbound_messagelist.
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('messageid', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null);
+        $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('address', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null);
+        $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+
+        // Adding keys to table messageinbound_messagelist.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $table->add_key('userid', XMLDB_KEY_FOREIGN, array('userid'), 'user', array('id'));
+
+        // Conditionally launch create table for messageinbound_messagelist.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2014100200.01);
+    }
+
     return true;
 }
index 9af72b7..3067813 100644 (file)
@@ -38,6 +38,12 @@ class handler_base extends \core\message\inbound\handler {
     public function get_description() {
     }
 
+    /**
+     * Get the name for unit tests.
+     */
+    public function get_name() {
+    }
+
     /**
      * Process a message for unit tests.
      *
index b1c47a5..ac061cf 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2014100200.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2014100200.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.