Skip to content

Commit c7b6e4e

Browse files
committed
[#103] Add MDL-69724 patch so tests can run in ci, fix test
1 parent 0630fd2 commit c7b6e4e

File tree

3 files changed

+608
-1
lines changed

3 files changed

+608
-1
lines changed

patch/MOODLE_404_STABLE.diff

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
From f996d8c971e25b352725c00094da63c866facb61 Mon Sep 17 00:00:00 2001
2+
From: Matthew Hilton <matthewhilton@catalyst-au.net>
3+
Date: Thu, 6 Nov 2025 14:57:48 +1000
4+
Subject: [PATCH] MDL-69724 email: Add before_email_to_user hook
5+
6+
---
7+
.upgradenotes/MDL-69724-2025111105250585.yml | 9 ++
8+
lib/classes/email.php | 149 ++++++++++++++++++
9+
.../hook/email/before_email_to_user.php | 41 +++++
10+
lib/moodlelib.php | 39 +++++
11+
4 files changed, 238 insertions(+)
12+
create mode 100644 .upgradenotes/MDL-69724-2025111105250585.yml
13+
create mode 100644 lib/classes/email.php
14+
create mode 100644 lib/classes/hook/email/before_email_to_user.php
15+
16+
diff --git a/.upgradenotes/MDL-69724-2025111105250585.yml b/.upgradenotes/MDL-69724-2025111105250585.yml
17+
new file mode 100644
18+
index 00000000000..8d72db9e8a1
19+
--- /dev/null
20+
+++ b/.upgradenotes/MDL-69724-2025111105250585.yml
21+
@@ -0,0 +1,9 @@
22+
+issueNumber: MDL-69724
23+
+notes:
24+
+ core:
25+
+ - message: >-
26+
+ `email_to_user()` now emits a hook `before_email_to_user`. This hook
27+
+ allows any subscriber to modify the email contents, add additional
28+
+ headers, or add reasons to block the email. If any block reasons are
29+
+ added, the email is stopped from being sent and the reasons are output.
30+
+ type: improved
31+
diff --git a/lib/classes/email.php b/lib/classes/email.php
32+
new file mode 100644
33+
index 00000000000..a080a5c0feb
34+
--- /dev/null
35+
+++ b/lib/classes/email.php
36+
@@ -0,0 +1,149 @@
37+
+<?php
38+
+// This file is part of Moodle - http://moodle.org/
39+
+//
40+
+// Moodle is free software: you can redistribute it and/or modify
41+
+// it under the terms of the GNU General Public License as published by
42+
+// the Free Software Foundation, either version 3 of the License, or
43+
+// (at your option) any later version.
44+
+//
45+
+// Moodle is distributed in the hope that it will be useful,
46+
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
47+
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
48+
+// GNU General Public License for more details.
49+
+//
50+
+// You should have received a copy of the GNU General Public License
51+
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
52+
+
53+
+namespace core;
54+
+
55+
+use core\exception\coding_exception;
56+
+use stdClass;
57+
+
58+
+/**
59+
+ * Email container class
60+
+ *
61+
+ * @package core
62+
+ * @copyright 2025 Matthew Hilton <matthewhilton@catalyst-au.net>
63+
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
64+
+ */
65+
+class email {
66+
+ /** @var array $blockreasons Reasons for this email being blocked */
67+
+ private array $blockreasons = [];
68+
+
69+
+ /** @var array $additionalheaders Additional email headers */
70+
+ private array $additionalheaders = [];
71+
+
72+
+ /**
73+
+ * Create email instance
74+
+ *
75+
+ * @param stdClass $user A $USER object
76+
+ * @param stdClass $from A $USER object
77+
+ * @param string $subject plain text subject line of the email
78+
+ * @param string $messagetext plain text version of the message
79+
+ * @param string $messagehtml complete html version of the message (optional)
80+
+ * @param string $attachment a file on the filesystem, either relative to $CFG->dataroot or a full path to a file in one of
81+
+ * the following directories: $CFG->cachedir, $CFG->dataroot, $CFG->dirroot, $CFG->localcachedir, $CFG->tempdir
82+
+ * @param string $attachname the name of the file (extension indicates MIME)
83+
+ * @param bool $usetrueaddress determines whether $from email address should
84+
+ * be sent out. Will be overruled by user profile setting for maildisplay
85+
+ * @param string $replyto Email address to reply to
86+
+ * @param string $replytoname Name of reply to recipient
87+
+ * @param int $wordwrapwidth custom word wrap width
88+
+ */
89+
+ public function __construct(
90+
+ /** @var stdClass $user A $USER object */
91+
+ public stdClass $user,
92+
+ /** @var stdClass $from A $USER object */
93+
+ public stdClass $from,
94+
+ /** @var string $subject plain text subject line of the email */
95+
+ public string $subject,
96+
+ /** @var string $messagetext plain text version of the message */
97+
+ public string $messagetext,
98+
+ /** @var string $messagehtml complete html version of the message (optional) */
99+
+ public string $messagehtml,
100+
+ /** @var string $attachment a file on the filesystem, either relative to $CFG->dataroot or a full path to a file in one of
101+
+ * the following directories: $CFG->cachedir, $CFG->dataroot, $CFG->dirroot, $CFG->localcachedir, $CFG->tempdir */
102+
+ public string $attachment,
103+
+ /** @var string $attachname the name of the file (extension indicates MIME) */
104+
+ public string $attachname,
105+
+ /** @var bool $usetrueaddress determines whether $from email address should
106+
+ * be sent out. Will be overruled by user profile setting for maildisplay */
107+
+ public bool $usetrueaddress,
108+
+ /** @var string $replyto Email address to reply to */
109+
+ public string $replyto,
110+
+ /** @var string $replytoname Name of reply to recipient */
111+
+ public string $replytoname,
112+
+ /** @var int $wordwrapwidth custom word wrap width */
113+
+ public int $wordwrapwidth,
114+
+ ) {
115+
+ // This is a quirk from email_to_user where the headers are stored in the "from" user.
116+
+ // We break them out of there here for the clarity of hook subscribers.
117+
+ $this->additionalheaders = self::extract_headers_from_from_user($from);
118+
+
119+
+ // Remove them from $from to avoid confusion / to avoid others accidentally updating those.
120+
+ unset($from->customheaders);
121+
+ }
122+
+
123+
+ /**
124+
+ * Extract custom headers from "from" user.
125+
+ * These may be set as a string or an array.
126+
+ *
127+
+ * @param stdClass $from
128+
+ * @return array customheaders
129+
+ */
130+
+ private static function extract_headers_from_from_user(stdClass $from): array {
131+
+ if (!isset($from->customheaders)) {
132+
+ return [];
133+
+ }
134+
+
135+
+ if (is_string($from->customheaders)) {
136+
+ return [$from->customheaders];
137+
+ }
138+
+
139+
+ if (is_array($from->customheaders)) {
140+
+ return $from->customheaders;
141+
+ }
142+
+
143+
+ throw new coding_exception("Unknown custom headers set");
144+
+ }
145+
+
146+
+ /**
147+
+ * Add a reason for blocking this email.
148+
+ * @param string $reason
149+
+ */
150+
+ public function add_block_reason(string $reason) {
151+
+ $this->blockreasons[] = $reason;
152+
+ }
153+
+
154+
+ /**
155+
+ * Return the reasons why this email was blocked
156+
+ * @return array of strings
157+
+ */
158+
+ public function get_block_reasons(): array {
159+
+ return $this->blockreasons;
160+
+ }
161+
+
162+
+ /**
163+
+ * Does this email have any block reasons?
164+
+ * @return bool
165+
+ */
166+
+ public function is_blocked(): bool {
167+
+ return !empty($this->blockreasons);
168+
+ }
169+
+
170+
+ /**
171+
+ * Add additional header to be sent with the email
172+
+ * @param string $name header name
173+
+ */
174+
+ public function add_additional_header(string $name) {
175+
+ $this->additionalheaders[] = $name;
176+
+ }
177+
+
178+
+ /**
179+
+ * Return list of additional headers set
180+
+ * @return array of string values
181+
+ */
182+
+ public function get_additional_headers(): array {
183+
+ return $this->additionalheaders;
184+
+ }
185+
+}
186+
diff --git a/lib/classes/hook/email/before_email_to_user.php b/lib/classes/hook/email/before_email_to_user.php
187+
new file mode 100644
188+
index 00000000000..0dbcb427e1d
189+
--- /dev/null
190+
+++ b/lib/classes/hook/email/before_email_to_user.php
191+
@@ -0,0 +1,41 @@
192+
+<?php
193+
+// This file is part of Moodle - http://moodle.org/
194+
+//
195+
+// Moodle is free software: you can redistribute it and/or modify
196+
+// it under the terms of the GNU General Public License as published by
197+
+// the Free Software Foundation, either version 3 of the License, or
198+
+// (at your option) any later version.
199+
+//
200+
+// Moodle is distributed in the hope that it will be useful,
201+
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
202+
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
203+
+// GNU General Public License for more details.
204+
+//
205+
+// You should have received a copy of the GNU General Public License
206+
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
207+
+
208+
+namespace core\hook\email;
209+
+
210+
+use core\email;
211+
+
212+
+/**
213+
+ * Hook to allow subscribers to modify or block sending email.
214+
+ *
215+
+ * @package core
216+
+ * @copyright 2025 Matthew Hilton <matthewhilton@catalyst-au.net>
217+
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
218+
+ */
219+
+#[\core\attribute\tags('email')]
220+
+#[\core\attribute\label('Allows plugins to modify contents or block sending an email')]
221+
+final class before_email_to_user {
222+
+ /**
223+
+ * Hook to allow subscribers to modify or block sending email.
224+
+ *
225+
+ * @param email $email The email message that is attempting to be sent.
226+
+ */
227+
+ public function __construct(
228+
+ /** @var email $email The email message that is attempting to be sent. */
229+
+ public email $email,
230+
+ ) {
231+
+ }
232+
+}
233+
diff --git a/lib/moodlelib.php b/lib/moodlelib.php
234+
index b734820bffc..289eef1a74d 100644
235+
--- a/lib/moodlelib.php
236+
+++ b/lib/moodlelib.php
237+
@@ -29,7 +29,9 @@
238+
*/
239+
240+
use core\di;
241+
+use core\email;
242+
use core\hook;
243+
+use core\hook\email\before_email_to_user;
244+
245+
defined('MOODLE_INTERNAL') || die();
246+
247+
@@ -5580,6 +5582,43 @@ function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '',
248+
249+
global $CFG, $PAGE, $SITE;
250+
251+
+ // Emit email to hook subscribers, who may modify the email.
252+
+ $email = new email(
253+
+ $user,
254+
+ $from,
255+
+ $subject,
256+
+ $messagetext,
257+
+ $messagehtml,
258+
+ $attachment,
259+
+ $attachname,
260+
+ $usetrueaddress,
261+
+ $replyto,
262+
+ $replytoname,
263+
+ $wordwrapwidth
264+
+ );
265+
+ $hook = new before_email_to_user($email);
266+
+ \core\di::get(\core\hook\manager::class)->dispatch($hook);
267+
+
268+
+ // Read back out the data from the hook, as it may have been modified by a hook callback.
269+
+ $user = $hook->email->user;
270+
+ $from = $hook->email->from;
271+
+ $from->customheaders = $hook->email->get_additional_headers();
272+
+ $subject = $hook->email->subject;
273+
+ $messagetext = $hook->email->messagetext;
274+
+ $messagehtml = $hook->email->messagehtml;
275+
+ $attachment = $hook->email->attachment;
276+
+ $attachname = $hook->email->attachname;
277+
+ $usetrueaddress = $hook->email->usetrueaddress;
278+
+ $replyto = $hook->email->replyto;
279+
+ $replytoname = $hook->email->replytoname;
280+
+ $wordwrapwidth = $hook->email->wordwrapwidth;
281+
+
282+
+ // Allow plugins to block this email - if blocked log why.
283+
+ if ($hook->email->is_blocked()) {
284+
+ debugging("email_to_user: blocked by hook subscriber: " . implode(', ', $hook->email->get_block_reasons()));
285+
+ return false;
286+
+ }
287+
+
288+
if (empty($user) or empty($user->id)) {
289+
debugging('Can not send email to null user', DEBUG_DEVELOPER);
290+
return false;
291+
--
292+
2.43.0
293+

0 commit comments

Comments
 (0)