Skip to content

Commit ea84bd8

Browse files
bwalkerlbrendanheywood
authored andcommitted
Store notifications in emailutils log
1 parent fea50e6 commit ea84bd8

File tree

6 files changed

+131
-16
lines changed

6 files changed

+131
-16
lines changed

classes/helper.php

+76
Original file line numberDiff line numberDiff line change
@@ -151,4 +151,80 @@ public static function get_bounce_config(): array {
151151
self::get_bounce_ratio(),
152152
];
153153
}
154+
155+
/**
156+
* Get the most recent bounce recorded for an email address
157+
*
158+
* @param string $email
159+
* @return \stdClass|false
160+
*/
161+
public static function get_most_recent_bounce(string $email): mixed {
162+
global $DB;
163+
164+
$params = [
165+
'email' => $email,
166+
];
167+
$sql = "SELECT *
168+
FROM {tool_emailutils_log}
169+
WHERE type = 'Bounce' AND email = :email
170+
ORDER BY time DESC
171+
LIMIT 1";
172+
return $DB->get_record_sql($sql, $params);
173+
}
174+
175+
/**
176+
* Gets the sum of the send count for multiple users
177+
* @param array $users array of users or userids
178+
* @return int sum of send count
179+
*/
180+
public static function get_sum_send_count(array $users): int {
181+
$sendcount = 0;
182+
foreach ($users as $user) {
183+
$sendcount += get_user_preferences('email_send_count', 0, $user);
184+
}
185+
return $sendcount;
186+
}
187+
188+
/**
189+
* Gets the max bounce count from a group of users
190+
* @param array $users array of users or userids
191+
* @return int max bounce count
192+
*/
193+
public static function get_max_bounce_count(array $users): int {
194+
$maxbounces = 0;
195+
foreach ($users as $user) {
196+
$maxbounces = max($maxbounces, get_user_preferences('email_bounce_count', 0, $user));
197+
}
198+
return $maxbounces;
199+
}
200+
201+
/**
202+
* Checks whether bounces are likely to be consecutive, and if not reset the bounce count.
203+
* This is a fallback in case delivery notifications are not enabled.
204+
*
205+
* @param string $email
206+
* @param array $users
207+
* @return void
208+
*/
209+
public static function check_consecutive_bounces(string $email, array $users): void {
210+
$recentbounce = self::get_most_recent_bounce($email);
211+
if (empty($recentbounce)) {
212+
// No data on the previous bounce.
213+
return;
214+
}
215+
216+
$sendcount = self::get_sum_send_count($users);
217+
$prevsendcount = $recentbounce->sendcount ?? 0;
218+
$sincelastbounce = $sendcount - $prevsendcount;
219+
220+
// The only things we can compare are the previous send count and time.
221+
// A direct comparison in sendcount isn't accurate because notifications may be delayed, so use a buffer.
222+
// Minbounces is ideal since future notifications would push it over the threshold.
223+
$buffer = min(self::get_min_bounces(), 5);
224+
if ($sincelastbounce >= $buffer) {
225+
foreach ($users as $user) {
226+
self::reset_bounce_count($user);
227+
}
228+
}
229+
}
154230
}

classes/sns_notification.php

+15-2
Original file line numberDiff line numberDiff line change
@@ -285,8 +285,7 @@ protected function process_bounce_notification(\stdClass $user): void {
285285

286286
if ($this->should_block_immediately()) {
287287
// User should only be able to recover from this if they change their email or have their bounces reset.
288-
// This sets the bounce ratio to 1 to improve visibility when something is a hard bounce.
289-
$bouncecount = max($sendcount, helper::get_min_bounces());
288+
$bouncecount = helper::get_min_bounces();
290289
set_user_preference('email_bounce_count', $bouncecount, $user);
291290
} else if ($this->should_block_softly()) {
292291
// Swap back to set_bounce_count($user) once MDL-73798 is integrated.
@@ -344,12 +343,26 @@ public function process_notification(): void {
344343

345344
if ($this->is_bounce()) {
346345
// Ideally bounce handling would be tracked per email instead of user.
346+
if (helper::use_consecutive_bounces()) {
347+
helper::check_consecutive_bounces($this->get_destination(), $users);
348+
}
347349
foreach ($users as $user) {
348350
$this->process_bounce_notification($user);
349351
}
350352
}
351353

352354
// TODO: Implement complaint handling.
355+
356+
// Save to emailutils log. This should be done last as processing may increase send count.
357+
// Time should represent when this was processed - send time will be in message if needed.
358+
$DB->insert_record('tool_emailutils_log', [
359+
'time' => time(),
360+
'type' => $this->get_type(),
361+
'subtypes' => $this->get_subtypes(),
362+
'email' => $this->get_destination(),
363+
'message' => $this->messageraw,
364+
'sendcount' => helper::get_sum_send_count($users),
365+
]);
353366
}
354367

355368
/**

db/install.xml

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8" ?>
2-
<XMLDB PATH="admin/tool/emailutils/db" VERSION="20241114" COMMENT="XMLDB file for plugin admin/tool/emailutils"
2+
<XMLDB PATH="admin/tool/emailutils/db" VERSION="20241128" COMMENT="XMLDB file for plugin admin/tool/emailutils"
33
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
44
xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
55
>
@@ -9,8 +9,10 @@
99
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
1010
<FIELD NAME="time" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
1111
<FIELD NAME="type" TYPE="char" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
12+
<FIELD NAME="subtypes" TYPE="char" LENGTH="32" NOTNULL="false" SEQUENCE="false"/>
1213
<FIELD NAME="email" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
1314
<FIELD NAME="message" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
15+
<FIELD NAME="sendcount" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
1416
</FIELDS>
1517
<KEYS>
1618
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>

db/upgrade.php

+24
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,29 @@ function xmldb_tool_emailutils_upgrade($oldversion) {
7878
// Emailutils savepoint reached.
7979
upgrade_plugin_savepoint(true, 2024111800, 'tool', 'emailutils');
8080
}
81+
82+
if ($oldversion < 2024112801) {
83+
84+
// Define field subtypes to be added to tool_emailutils_log.
85+
$table = new xmldb_table('tool_emailutils_log');
86+
$field = new xmldb_field('subtypes', XMLDB_TYPE_CHAR, '32', null, null, null, null, 'type');
87+
88+
// Conditionally launch add field subtypes.
89+
if (!$dbman->field_exists($table, $field)) {
90+
$dbman->add_field($table, $field);
91+
}
92+
93+
// Define field sendcount to be added to tool_emailutils_log.
94+
$field = new xmldb_field('sendcount', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'message');
95+
96+
// Conditionally launch add field sendcount.
97+
if (!$dbman->field_exists($table, $field)) {
98+
$dbman->add_field($table, $field);
99+
}
100+
101+
// Emailutils savepoint reached.
102+
upgrade_plugin_savepoint(true, 2024112801, 'tool', 'emailutils');
103+
}
104+
81105
return true;
82106
}

tests/sns_client_test.php

+11-11
Original file line numberDiff line numberDiff line change
@@ -145,22 +145,14 @@ private function get_mock_bounce_notification(string $bouncetype, string $bounce
145145
public function bounce_processing_provider(): array {
146146
// To be tested with minbounces of 3 and bounceratio of -1.
147147
return [
148-
'Block immediately with low send count' => [
148+
'Block immediately' => [
149149
'type' => 'Permanent',
150150
'subtype' => 'General',
151151
'notifications' => 1,
152152
'sendcount' => 1,
153153
'expectedbounces' => 3,
154154
'overthreshold' => true,
155155
],
156-
'Block immediately with high send count' => [
157-
'type' => 'Permanent',
158-
'subtype' => 'General',
159-
'notifications' => 1,
160-
'sendcount' => 100,
161-
'expectedbounces' => 100,
162-
'overthreshold' => true,
163-
],
164156
'Block softly' => [
165157
'type' => 'Transient',
166158
'subtype' => 'General',
@@ -218,7 +210,7 @@ public function bounce_processing_provider(): array {
218210
**/
219211
public function test_bounce_processing(string $type, string $subtype, int $notifications, int $sendcount,
220212
int $expectedbounces, bool $overthreshold): void {
221-
global $CFG;
213+
global $CFG, $DB;
222214

223215
// Setup config and users.
224216
$this->resetAfterTest();
@@ -249,6 +241,10 @@ public function test_bounce_processing(string $type, string $subtype, int $notif
249241
$bounceevents = 2 * (int) $overthreshold;
250242
$this->assertCount($notifications + $bounceevents, $events);
251243

244+
// Confirm bounce notification stored in emailutils log table.
245+
$records = $DB->get_records('tool_emailutils_log', null, 'id ASC');
246+
$this->assertCount($notifications, $records);
247+
252248
// Confirm that shared email addresses have the same status.
253249
$this->assertSame($overthreshold, over_bounce_threshold($user2));
254250
}
@@ -259,7 +255,7 @@ public function test_bounce_processing(string $type, string $subtype, int $notif
259255
* @covers \tool_emailutils\sns_notification::process_notification()
260256
**/
261257
public function test_delivery_processing(): void {
262-
global $CFG;
258+
global $CFG, $DB;
263259

264260
// Setup config and users.
265261
$this->resetAfterTest();
@@ -297,6 +293,10 @@ public function test_delivery_processing(): void {
297293
// This also confirms the third user didn't have their count reset.
298294
$this->assertCount(2, $events);
299295

296+
// Confirm delivery notification isn't stored in emailutils log table.
297+
$records = $DB->get_records('tool_emailutils_log');
298+
$this->assertCount(0, $records);
299+
300300
// Ensure bounces aren't reset when bounce ratio config is positive.
301301
$CFG->bounceratio = 0.5;
302302
$this->assertFalse(helper::use_consecutive_bounces());

version.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525

2626
defined('MOODLE_INTERNAL') || die();
2727

28-
$plugin->version = 2024112200;
29-
$plugin->release = 2024112200;
28+
$plugin->version = 2024112801;
29+
$plugin->release = 2024112801;
3030
$plugin->requires = 2024042200;
3131
$plugin->component = 'tool_emailutils';
3232
$plugin->maturity = MATURITY_STABLE;

0 commit comments

Comments
 (0)