MDL-64017 message_email: task to send messages as a digest
authorMark Nelson <markn@moodle.com>
Fri, 5 Apr 2019 06:48:06 +0000 (14:48 +0800)
committerMark Nelson <markn@moodle.com>
Thu, 18 Apr 2019 08:03:27 +0000 (16:03 +0800)
lib/tests/messagelib_test.php
message/output/email/classes/output/email/renderer.php [new file with mode: 0644]
message/output/email/classes/output/email/renderer_textemail.php [new file with mode: 0644]
message/output/email/classes/output/email_digest.php [new file with mode: 0644]
message/output/email/classes/output/renderer.php [new file with mode: 0644]
message/output/email/classes/task/send_email_task.php [new file with mode: 0644]
message/output/email/db/tasks.php [new file with mode: 0644]
message/output/email/lang/en/message_email.php
message/output/email/tests/send_email_task_test.php [new file with mode: 0644]

index 7faf33d..bdbf869 100644 (file)
@@ -836,13 +836,30 @@ class core_messagelib_testcase extends advanced_testcase {
         $this->preventResetByRollback();
         $this->resetAfterTest();
 
+        $course = $this->getDataGenerator()->create_course();
+
         // Create some users and a conversation between them.
         $user1 = $this->getDataGenerator()->create_user(array('maildisplay' => 1));
         $user2 = $this->getDataGenerator()->create_user();
         $user3 = $this->getDataGenerator()->create_user();
         set_config('allowedemaildomains', 'example.com');
-        $conversation = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
-            [$user1->id, $user2->id, $user3->id], 'Group project discussion');
+
+        // Create a group in the course.
+        $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+        groups_add_member($group1->id, $user1->id);
+        groups_add_member($group1->id, $user2->id);
+        groups_add_member($group1->id, $user3->id);
+
+        $conversation = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [$user1->id, $user2->id, $user3->id],
+            'Group project discussion',
+            \core_message\api::MESSAGE_CONVERSATION_ENABLED,
+            'core_group',
+            'groups',
+            $group1->id,
+            context_course::instance($course->id)->id
+        );
 
         // Generate the message.
         $message = new \core\message\message();
@@ -868,8 +885,11 @@ class core_messagelib_testcase extends advanced_testcase {
         set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user3);
 
         // Now, send a message and verify the email processor are hit.
-        $sink = $this->redirectEmails();
         $messageid = message_send($message);
+
+        $sink = $this->redirectEmails();
+        $task = new \message_email\task\send_email_task();
+        $task->execute();
         $emails = $sink->get_messages();
         $this->assertCount(2, $emails);
 
@@ -902,14 +922,29 @@ class core_messagelib_testcase extends advanced_testcase {
         $this->preventResetByRollback();
         $this->resetAfterTest();
 
+        $course = $this->getDataGenerator()->create_course();
+
         $user1 = $this->getDataGenerator()->create_user(array('maildisplay' => 1));
         $user2 = $this->getDataGenerator()->create_user();
         $user3 = $this->getDataGenerator()->create_user();
         set_config('allowedemaildomains', 'example.com');
 
-        // Create a conversation.
-        $conversation = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
-            [$user1->id, $user2->id, $user3->id], 'Group project discussion');
+        // Create a group in the course.
+        $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+        groups_add_member($group1->id, $user1->id);
+        groups_add_member($group1->id, $user2->id);
+        groups_add_member($group1->id, $user3->id);
+
+        $conversation = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [$user1->id, $user2->id, $user3->id],
+            'Group project discussion',
+            \core_message\api::MESSAGE_CONVERSATION_ENABLED,
+            'core_group',
+            'groups',
+            $group1->id,
+            context_course::instance($course->id)->id
+        );
 
         // Test basic email redirection.
         $this->assertFileExists("$CFG->dirroot/message/output/email/version.php");
@@ -939,18 +974,18 @@ class core_messagelib_testcase extends advanced_testcase {
 
         $transaction = $DB->start_delegated_transaction();
         $sink = $this->redirectEmails();
-        $messageid = message_send($message);
+        message_send($message);
         $emails = $sink->get_messages();
         $this->assertCount(0, $emails);
-        $savedmessage = $DB->get_record('messages', array('id' => $messageid), '*', MUST_EXIST);
         $sink->clear();
         $this->assertFalse($DB->record_exists('message_user_actions', array()));
-        $DB->delete_records('messages', array());
         $events = $eventsink->get_events();
         $this->assertCount(0, $events);
         $eventsink->clear();
         $transaction->allow_commit();
         $events = $eventsink->get_events();
+        $task = new \message_email\task\send_email_task();
+        $task->execute();
         $emails = $sink->get_messages();
         $this->assertCount(2, $emails);
         $this->assertCount(1, $events);
diff --git a/message/output/email/classes/output/email/renderer.php b/message/output/email/classes/output/email/renderer.php
new file mode 100644 (file)
index 0000000..8be4491
--- /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/>.
+
+/**
+ * Email digest as html renderer.
+ *
+ * @package    message_email
+ * @copyright  2019 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace message_email\output\email;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Email digest as html renderer.
+ *
+ * @package    message_email
+ * @copyright  2019 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer extends \message_email\output\renderer {
+
+    /**
+     * The template name for this renderer.
+     *
+     * @return string
+     */
+    public function get_template_name() {
+        return 'email_digest_html';
+    }
+}
diff --git a/message/output/email/classes/output/email/renderer_textemail.php b/message/output/email/classes/output/email/renderer_textemail.php
new file mode 100644 (file)
index 0000000..a35e7b9
--- /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/>.
+
+/**
+ * Email digest as text renderer.
+ *
+ * @package    message_email
+ * @copyright  2019 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace message_email\output\email;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Email digest as text renderer.
+ *
+ * @package    message_email
+ * @copyright  2019 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer_textemail extends \message_email\output\renderer {
+
+    /**
+     * The template name for this renderer.
+     *
+     * @return string
+     */
+    public function get_template_name() {
+        return 'email_digest_text';
+    }
+}
diff --git a/message/output/email/classes/output/email_digest.php b/message/output/email/classes/output/email_digest.php
new file mode 100644 (file)
index 0000000..33d3486
--- /dev/null
@@ -0,0 +1,136 @@
+<?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/>.
+
+/**
+ * Email digest renderable.
+ *
+ * @package    message_email
+ * @copyright  2019 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace message_email\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Email digest renderable.
+ *
+ * @copyright  2019 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class email_digest implements \renderable, \templatable {
+
+    /**
+     * @var array The conversations
+     */
+    protected $conversations = array();
+
+    /**
+     * @var array The messages
+     */
+    protected $messages = array();
+
+    /**
+     * @var \stdClass The user we want to send the digest email to
+     */
+    protected $userto;
+
+    /**
+     * The email_digest constructor.
+     *
+     * @param \stdClass $userto
+     */
+    public function __construct(\stdClass $userto) {
+        $this->userto = $userto;
+    }
+
+    /**
+     * Adds another conversation to this digest.
+     *
+     * @param \stdClass $conversation The conversation from the 'message_conversations' table.
+     */
+    public function add_conversation(\stdClass $conversation) {
+        $this->conversations[$conversation->id] = $conversation;
+    }
+
+    /**
+     * Adds another message to this digest, using the conversation id it belongs to as a key.
+     *
+     * @param \stdClass $message The message from the 'messages' table.
+     */
+    public function add_message(\stdClass $message) {
+        $this->messages[$message->conversationid][] = $message;
+    }
+
+    /**
+     * Export this data so it can be used as the context for a mustache template.
+     *
+     * @param \renderer_base $renderer The render to be used for formatting the email
+     * @return \stdClass The data ready for use in a mustache template
+     */
+    public function export_for_template(\renderer_base $renderer) {
+        // Prepare the data we are going to send to the template.
+        $data = new \stdClass();
+        $data->conversations = [];
+
+        // Don't do anything if there are no messages.
+        foreach ($this->conversations as $conversation) {
+            $messages = $this->messages[$conversation->id] ?? [];
+
+            if (empty($messages)) {
+                continue;
+            }
+
+            $viewallmessageslink = new \moodle_url('/message/index.php', ['convid' => $conversation->id]);
+
+            $conversationformatted = new \stdClass();
+            $conversationformatted->groupname = $conversation->name;
+            $conversationformatted->coursename = $conversation->coursename;
+            $conversationformatted->numberofunreadmessages = count($messages);
+            $conversationformatted->messages = [];
+            $conversationformatted->viewallmessageslink = \html_writer::link($viewallmessageslink,
+                get_string('emaildigestviewallmessages', 'message_email'));
+
+            // We only display the last 3 messages.
+            $messages = array_slice($messages, -3, 3, true);
+            foreach ($messages as $message) {
+                $user = new \stdClass();
+                username_load_fields_from_object($user, $message);
+                $user->id = $message->useridfrom;
+                $messageformatted = new \stdClass();
+                $messageformatted->userfullname = fullname($user);
+                $messageformatted->message = message_format_message_text($message);
+
+                // Check if the message was sent today.
+                $istoday = userdate($message->timecreated, 'Y-m-d') == userdate(time(), 'Y-m-d');
+                if ($istoday) {
+                    $timesent = userdate($message->timecreated, get_string('strftimetime24', 'langconfig'));
+                } else {
+                    $timesent = userdate($message->timecreated, get_string('strftimedatefullshort', 'langconfig'));
+                }
+
+                $messageformatted->timesent = $timesent;
+
+                $conversationformatted->messages[] = $messageformatted;
+            }
+
+            $data->conversations[] = $conversationformatted;
+        }
+
+        return $data;
+    }
+}
diff --git a/message/output/email/classes/output/renderer.php b/message/output/email/classes/output/renderer.php
new file mode 100644 (file)
index 0000000..0a0e7ff
--- /dev/null
@@ -0,0 +1,50 @@
+<?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/>.
+
+/**
+ * Contains renderer class.
+ *
+ * @package   message_email
+ * @copyright 2019 Mark Nelson <markn@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace message_email\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+use plugin_renderer_base;
+
+/**
+ * Renderer class.
+ *
+ * @package    message_email
+ * @copyright  2019 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer extends plugin_renderer_base {
+
+    /**
+     * Formats the email used to send the certificate by the email_certificate_task.
+     *
+     * @param email_digest $emaildigest The certificate to email
+     * @return string
+     */
+    public function render_email_digest(email_digest $emaildigest) {
+        $data = $emaildigest->export_for_template($this);
+        return $this->render_from_template('message_email/' . $this->get_template_name(), $data);
+    }
+}
diff --git a/message/output/email/classes/task/send_email_task.php b/message/output/email/classes/task/send_email_task.php
new file mode 100644 (file)
index 0000000..33c4238
--- /dev/null
@@ -0,0 +1,173 @@
+<?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/>.
+
+/**
+ * Contains the class responsible for sending emails as a digest.
+ *
+ * @package    message_email
+ * @copyright  2019 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace message_email\task;
+
+use core\task\scheduled_task;
+use moodle_recordset;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class responsible for sending emails as a digest.
+ *
+ * @package    message_email
+ * @copyright  2019 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class send_email_task extends scheduled_task {
+
+    /**
+     * @var int $maxid This is the maximum id of the message in 'message_email_messages'.
+     *                 We use this so we know what records to process, as more records may be added
+     *                 while this task runs.
+     */
+    private $maxid;
+
+    /**
+     * Get a descriptive name for this task (shown to admins).
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('tasksendemail', 'message_email');
+    }
+
+    /**
+     * Send out emails.
+     */
+    public function execute() {
+        global $DB, $PAGE;
+
+        // Get the maximum id we are going to use.
+        // We use this as records may be added to the table while this task runs.
+        $this->maxid = $DB->get_field_sql("SELECT MAX(id) FROM {message_email_messages}");
+
+        // We are going to send these emails from 'noreplyaddress'.
+        $noreplyuser = \core_user::get_noreply_user();
+
+        // The renderers used for sending emails.
+        $htmlrenderer = $PAGE->get_renderer('message_email', 'email', 'htmlemail');
+        $textrenderer = $PAGE->get_renderer('message_email', 'email', 'textemail');
+
+        // Keep track of which emails failed to send.
+        $users = $this->get_unique_users();
+        foreach ($users as $user) {
+            $hascontent = false;
+            $renderable = new \message_email\output\email_digest($user);
+            $conversations = $this->get_conversations_for_user($user->id);
+            foreach ($conversations as $conversation) {
+                $renderable->add_conversation($conversation);
+                $messages = $this->get_users_messages_for_conversation($conversation->id, $user->id);
+                if ($messages->valid()) {
+                    $hascontent = true;
+                    foreach ($messages as $message) {
+                        $renderable->add_message($message);
+                    }
+                }
+                $messages->close();
+            }
+            $conversations->close();
+            if ($hascontent) {
+                $subject = get_string('emaildigestsubject', 'message_email');
+                $message = $textrenderer->render($renderable);
+                $messagehtml = $htmlrenderer->render($renderable);
+                if (email_to_user($user, $noreplyuser, $subject, $message, $messagehtml)) {
+                    $DB->delete_records_select('message_email_messages', 'useridto = ? AND id <= ?', [$user->id, $this->maxid]);
+                }
+            }
+        }
+        $users->close();
+    }
+
+    /**
+     * Returns an array of users in the given conversation.
+     *
+     * @return moodle_recordset A moodle_recordset instance.
+     */
+    private function get_unique_users() : moodle_recordset {
+        global $DB;
+
+        $subsql = 'SELECT DISTINCT(useridto) as id
+                     FROM {message_email_messages}
+                    WHERE id <= ?';
+
+        $sql = "SELECT *
+                  FROM {user} u
+                 WHERE id IN ($subsql)";
+
+        return $DB->get_recordset_sql($sql, [$this->maxid]);
+    }
+
+    /**
+     * Returns an array of unique conversations that require processing.
+     *
+     * @param int $userid The ID of the user we are sending a digest to.
+     * @return moodle_recordset A moodle_recordset instance.
+     */
+    private function get_conversations_for_user(int $userid) : moodle_recordset {
+        global $DB;
+
+        // We shouldn't be joining directly on the group table as group
+        // conversations may (in the future) be something created that
+        // isn't related to an actual group in a course. However, for
+        // now this will have to do before 3.7 code freeze.
+        // See related MDL-63814.
+        $sql = "SELECT mc.id, mc.name, c.id as courseid, c.fullname as coursename, g.id as groupid, g.picture, g.hidepicture
+                  FROM {message_conversations} mc
+                  JOIN {groups} g
+                    ON mc.itemid = g.id
+                  JOIN {course} c
+                    ON g.courseid = c.id
+                  JOIN {message_email_messages} mem
+                    ON mem.conversationid = mc.id  
+                 WHERE mem.useridto = ?
+                   AND mem.id <= ?";
+
+        return $DB->get_recordset_sql($sql, [$userid, $this->maxid]);
+    }
+
+    /**
+     * Returns the messages to send to a user for a given conversation
+     *
+     * @param int $conversationid
+     * @param int $userid
+     * @return moodle_recordset A moodle_recordset instance.
+     */
+    protected function get_users_messages_for_conversation(int $conversationid, int $userid) : moodle_recordset {
+        global $DB;
+
+        $usernamefields = \user_picture::fields('u');
+        $sql = "SELECT $usernamefields, m.*
+                  FROM {messages} m
+                  JOIN {user} u
+                    ON u.id = m.useridfrom
+                  JOIN {message_email_messages} mem
+                    ON mem.messageid = m.id
+                 WHERE mem.useridto = ?
+                   AND mem.conversationid = ?
+                   AND mem.id <= ?";
+
+        return $DB->get_recordset_sql($sql, [$userid, $conversationid, $this->maxid]);
+    }
+}
diff --git a/message/output/email/db/tasks.php b/message/output/email/db/tasks.php
new file mode 100644 (file)
index 0000000..8691988
--- /dev/null
@@ -0,0 +1,38 @@
+<?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/>.
+
+/**
+ * This file defines tasks performed by the plugin.
+ *
+ * @package    message_email
+ * @copyright  2019 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+// List of tasks.
+$tasks = array(
+    array(
+        'classname' => 'message_email\task\send_email_task',
+        'blocking' => 0,
+        'minute' => 0,
+        'hour' => 22,
+        'day' => '*',
+        'dayofweek' => '*',
+        'month' => '*'
+    )
+);
index 6e27fcd..e03da85 100644 (file)
@@ -23,6 +23,9 @@
  */
 
 $string['email'] = 'Send email notifications to';
+$string['emaildigestsubject'] = 'Message digest';
+$string['emaildigestunreadmessages'] = 'Unread messages';
+$string['emaildigestviewallmessages'] = 'View all messages';
 $string['emailonlyfromnoreplyaddress'] = 'Always send email from the no-reply address?';
 $string['ifemailleftempty'] = 'Leave empty to send notifications to {$a}';
 $string['pluginname'] = 'Email';
@@ -40,3 +43,5 @@ $string['privacy:metadata:replyto'] = 'The email address to reply to.';
 $string['privacy:metadata:replytoname'] = 'Name of reply to recipient.';
 $string['privacy:metadata:subject'] = 'The subject line of the message.';
 $string['privacy:metadata:userfrom'] = 'The user sending the message.';
+$string['tasksendemail'] = 'Task responsible for sending messages as a digest.';
+
diff --git a/message/output/email/tests/send_email_task_test.php b/message/output/email/tests/send_email_task_test.php
new file mode 100644 (file)
index 0000000..cd59f5d
--- /dev/null
@@ -0,0 +1,128 @@
+<?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/>.
+
+/**
+ * Tests the send email task.
+ *
+ * @package message_email
+ * @category test
+ * @copyright 2018 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+require_once($CFG->dirroot . '/message/tests/messagelib_test.php');
+
+/**
+ * Class for testing the send email task.
+ *
+ * @package message_email
+ * @category test
+ * @copyright 2019 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_message_send_email_task_testcase extends advanced_testcase {
+
+    /**
+     * Test sending email task.
+     */
+    public function test_sending_email_task() {
+        global $DB;
+
+        $this->preventResetByRollback(); // Messaging is not compatible with transactions.
+
+        $this->resetAfterTest();
+
+        // Create a course.
+        $course = $this->getDataGenerator()->create_course();
+
+        $user1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
+        $user2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+        // Create two groups in the course.
+        $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+        $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+
+        groups_add_member($group1->id, $user1->id);
+        groups_add_member($group2->id, $user1->id);
+
+        groups_add_member($group1->id, $user2->id);
+        groups_add_member($group2->id, $user2->id);
+
+        $conversation1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [$user1->id, $user2->id],
+            'Group 1', \core_message\api::MESSAGE_CONVERSATION_ENABLED,
+            'core_group',
+            'groups',
+            $group1->id,
+            context_course::instance($course->id)->id
+        );
+
+        $conversation2 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [$user1->id, $user2->id],
+            'Group 2',
+            \core_message\api::MESSAGE_CONVERSATION_ENABLED,
+            'core_group',
+            'groups',
+            $group2->id,
+            context_course::instance($course->id)->id
+        );
+
+        // Go through each conversation.
+        if ($conversations = $DB->get_records('message_conversations')) {
+            foreach ($conversations as $conversation) {
+                $conversationid = $conversation->id;
+
+                $message = new \core\message\message();
+                $message->courseid = 1;
+                $message->component = 'moodle';
+                $message->name = 'instantmessage';
+                $message->userfrom = $user1;
+                $message->convid = $conversationid;
+                $message->subject = 'message subject';
+                $message->fullmessage = 'message body';
+                $message->fullmessageformat = FORMAT_MARKDOWN;
+                $message->fullmessagehtml = '<p>message body</p>';
+                $message->smallmessage = 'small message';
+                $message->notification = '0';
+
+                message_send($message);
+            }
+        }
+
+        $this->assertEquals(2, $DB->count_records('message_email_messages'));
+
+        // Only 1 email is sent as the 2 messages are included in it at a digest.
+        $sink = $this->redirectEmails();
+        $task = new \message_email\task\send_email_task();
+        $task->execute();
+        $this->assertEquals(1, $sink->count());
+
+        // Confirm table was emptied after task was run.
+        $this->assertEquals(0, $DB->count_records('message_email_messages'));
+
+        // Confirm running it again does not send another.
+        $sink = $this->redirectEmails();
+        $task = new \message_email\task\send_email_task();
+        $task->execute();
+        $this->assertEquals(0, $sink->count());
+    }
+}