PluginProbe
Mail Queue / 1.5.1
Mail Queue v1.5.1
1.6.1 1.6.0 1.5.1 trunk 1.0 1.1 1.2 1.3 1.3.1 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.5.0
mail-queue / mail-queue.php

mail-queue.php in Mail Queue 1.5.1, at mail-queue.php

755 lines 29.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Plugin Name: Mail Queue
5 * Plugin URI: https://www.webdesign-muenchen.de/wordpress-plugin-mail-queue/
6 * Description: Take Control and improve Security of wp_mail(). Queue and log outgoing emails, and get alerted, if your website wants to send more emails than usual.
7 * Version: 1.5.1
8 * Requires at least: 5.9
9 * Requires PHP: 7.4
10 * Author: WDM
11 * Author URI: https://www.webdesign-muenchen.de
12 * License: GPLv3 or later
13 * License URI: https://www.gnu.org/licenses/gpl-3.0.html
14 */
15
16 if (!defined('ABSPATH')) { exit; }
17
18
19 /* ***************************************************************
20 PLUGIN VERSION
21 **************************************************************** */
22
23 $wdm_wpma_version = '1.5.1';
24
25
26
27
28
29 /* ***************************************************************
30 PLUGIN DEFAULT SETTINGS
31 **************************************************************** */
32 function wdm_wpma_get_settings() {
33 $defaults = array(
34 'enabled' => '0',
35 'alert_enabled' => '0',
36 'email' => get_option('admin_email'),
37 'email_amount' => '10',
38 'pause_on_alert' => '0',
39 'queue_amount' => '1',
40 'queue_interval' => '5',
41 'queue_interval_unit' => 'minutes',
42 'clear_queue' => '14',
43 'tableName' => 'mail_queue',
44 'triggercount' => 0,
45 );
46 $args = get_option('wdm_wpma_settings');
47 $options = wp_parse_args($args,$defaults);
48
49 if ($options['queue_interval_unit'] == 'seconds') {
50 $options['queue_interval'] = intval($options['queue_interval']);
51 if ($options['queue_interval'] < 10) { $options['queue_interval'] = 10; } // Minimum Interval 10 Seconds
52 } else {
53 $options['queue_interval'] = intval($options['queue_interval']) * 60;
54 }
55
56 $options['clear_queue'] = intval($options['clear_queue']) * 24;
57 return $options;
58 }
59
60
61 function wdm_wpma_refresh_settings() {
62 global $wdm_wpma_options;
63 $wdm_wpma_options = wdm_wpma_get_settings();
64 return $wdm_wpma_options;
65 }
66
67
68 // Safer drop-in for maybe_unserialize(): refuses to instantiate any class.
69 // Use on DB columns we know to contain only scalars/arrays (recipient, headers, attachments).
70 function wdm_wpma_safe_unserialize ($value) {
71 if (!is_string($value) || !is_serialized($value)) { return $value; }
72 return @unserialize($value, ['allowed_classes' => false]);
73 }
74
75
76 function wdm_wpma_is_pause_on_alert_active() {
77 return get_option('wdm_wpma_pause_on_alert_active') == '1';
78 }
79
80 function wdm_wpma_clear_pause_on_alert_state() {
81 delete_option('wdm_wpma_pause_on_alert_active');
82 }
83
84 function wdm_wpma_must_activate_pause_on_alert() {
85 global $wdm_wpma_options;
86
87 if (!isset($wdm_wpma_options['enabled']) || $wdm_wpma_options['enabled'] != '1') { return false; }
88 if (!isset($wdm_wpma_options['alert_enabled']) || $wdm_wpma_options['alert_enabled'] !== '1') { return false; }
89 if (!isset($wdm_wpma_options['pause_on_alert']) || $wdm_wpma_options['pause_on_alert'] !== '1') { return false; }
90 if (wdm_wpma_is_pause_on_alert_active()) { return false; }
91
92 return true;
93 }
94
95 function wdm_wpma_maybe_activate_pause_on_alert() {
96
97 if (!wdm_wpma_must_activate_pause_on_alert()) { return false; }
98
99 $settings = get_option('wdm_wpma_settings', array());
100 if (!is_array($settings)) { $settings = array(); }
101 $settings['enabled'] = 'paused';
102
103 update_option('wdm_wpma_settings', $settings, true);
104 update_option('wdm_wpma_pause_on_alert_active', '1', false);
105
106 wdm_wpma_push_queue_event([
107 'name' => 'autopause-activated',
108 ]);
109
110 wdm_wpma_refresh_settings();
111
112 return true;
113 }
114
115
116
117 $wdm_wpma_mailid = 0;
118 $wdm_wpma_options = wdm_wpma_get_settings(); // Get Settings
119
120
121
122
123
124 /* ***************************************************************
125 Overwrite wp_mail() if Plugin enabled and no Cron is running
126 **************************************************************** */
127 $wdm_wpma_pre_wp_mail_priority = 99999;
128
129 if (in_array($wdm_wpma_options['enabled'], ['1', 'paused']) && wp_doing_cron() == false) {
130 // High priority: run late in the game to react to previous filters
131 add_filter('pre_wp_mail', 'wdm_wpma_prewpmail', $wdm_wpma_pre_wp_mail_priority, 2);
132 }
133
134 // pre WP Mail Filter
135 function wdm_wpma_prewpmail($return, $atts) {
136
137 global $wpdb, $wdm_wpma_options;
138
139 if (!is_null($return)) {
140 // Another pre_wp_mail filter has already returned a value, so the mail is not added to the queue
141 return $return;
142 }
143
144 // Mail Variables
145 $to = $atts['to'];
146 $subject = $atts['subject'];
147 $message = $atts['message'];
148 $headers = $atts['headers'];
149 $attachments = $atts['attachments'];
150 $status = 'queue';
151
152 // Make sure that $headers always is an array
153 if ($headers) {
154 if (!is_array($headers)) {
155 $headers = explode( "\n", str_replace( "\r\n", "\n", $headers ) );
156 }
157 } else {
158 $headers = [];
159 }
160
161 // Loop through email headers
162 // - Instant Sending or Prio Mail? (the first X-Mail-Queue-Prio header wins)
163 // - Track if ContentType / From header is set
164 // Scan every header (no early break) so a Content-Type / From that sits
165 // after the X-Mail-Queue-Prio header is still detected;
166 // All X-Mail-Queue-Prio headers are stripped so they don't leak outbound.
167 $filtered_headers = $headers;
168 $hasContentTypeHeader = false;
169 $hasFromHeader = false;
170 $prio_status_found = false;
171 foreach($headers as $index => $val) {
172 $val = trim($val);
173 if (preg_match("#^X-Mail-Queue-Prio: +Instant *$#i",$val)) {
174 unset($filtered_headers[$index]);
175 if (!$prio_status_found) {
176 $status = 'instant';
177 $prio_status_found = true;
178 }
179 } else if (preg_match("#^X-Mail-Queue-Prio: +High *$#i",$val)) {
180 unset($filtered_headers[$index]);
181 if (!$prio_status_found) {
182 $status = 'high';
183 $prio_status_found = true;
184 }
185 } else if (preg_match('#^Content-Type:#i',$val)) {
186 $hasContentTypeHeader = true;
187 } else if (preg_match('#^From:#i',$val)) {
188 $hasFromHeader = true;
189 }
190 }
191 $headers = array_values($filtered_headers);
192
193 // For all emails that are stored in the queue to be sent later:
194 // Store custom filtered values in headers if available.
195 // Support the following hooks used in wp_mail:
196 // - wp_mail_content_type
197 // - wp_mail_charset
198 // - wp_mail_from
199 // - wp_mail_from_name
200 if ($status !== 'instant') {
201 if (!$hasContentTypeHeader) {
202 $contentType = apply_filters('wp_mail_content_type','text/plain');
203 if ( $contentType ) {
204 if (stripos($contentType,'multipart') === false) {
205 $charset = apply_filters('wp_mail_charset',get_bloginfo('charset'));
206 } else {
207 $charset = '';
208 }
209 $headers[] = 'Content-Type: '.$contentType.($charset ? '; charset="'.$charset.'"' : '');
210 }
211 }
212 if (!$hasFromHeader) {
213 $from_Email = apply_filters('wp_mail_from','');
214 if ($from_Email) {
215 $fromName = apply_filters('wp_mail_from_name','');
216 if ($fromName) {
217 $headers[] = 'From: '.$fromName.' <'.$from_Email.'>';
218 } else {
219 $headers[] = 'From: '.$from_Email;
220 }
221 }
222 }
223 }
224
225
226 // Write email in Queue
227 $tableName = $wpdb->prefix.$wdm_wpma_options['tableName'];
228 $data = array(
229 'timestamp'=> current_time('mysql',false),
230 'recipient'=> maybe_serialize($to),
231 'subject'=> $subject,
232 'message'=> $message,
233 'status' => $status,
234 'attachments' => ''
235 );
236 if (isset($headers) && $headers) { $data['headers'] = maybe_serialize($headers); }
237
238 // store attachments in /attachments/ Folder, to address them later
239 if (isset($attachments) && $attachments && $attachments != '') {
240
241 $subfolder = time().'-'.wp_generate_password(24,/*special_chars*/false,/*extra_special_chars*/false);
242 $foldercreated = wp_mkdir_p(plugin_dir_path(__FILE__).'attachments/'.$subfolder);
243 if (!$foldercreated) {
244 if ( defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG ) {
245 error_log('[mail-queue] Could not create subfolder for email attachment');
246 }
247 $data['info'] = 'Error: Could not store attachments';
248 } else {
249 if (!is_array($attachments)) { $attachments = array($attachments); }
250 $newattachments = array();
251 global $wp_filesystem;
252 if ( ! is_a( $wp_filesystem, 'WP_Filesystem_Base') ){
253 include_once(ABSPATH . 'wp-admin/includes/file.php');
254 WP_Filesystem();
255 }
256 foreach($attachments as $item) {
257 $newfile = plugin_dir_path(__FILE__).'attachments/'.$subfolder.'/'.basename($item);
258 $wp_filesystem->copy($item,$newfile);
259 array_push($newattachments,$newfile);
260 }
261 $data['attachments'] = maybe_serialize($newattachments);
262 }
263 }
264 $inserted = $wpdb->insert($tableName,$data);
265
266 if ($inserted) {
267 global $wdm_wpma_mailid;
268 $wdm_wpma_mailid = (int) $wpdb->insert_id;
269 }
270
271 if ($status == 'instant') {
272 return null;
273 } else if ( !$inserted ) {
274 // No database entry, email cannot be send
275 return false;
276 } else {
277 // Fake Submit by returning 'True'
278 return true;
279 }
280
281 }
282
283
284
285 // show wp_mail() errors
286 function wdm_wpma_mail_failed( $wp_error ) {
287 global $wpdb,$wdm_wpma_options,$wdm_wpma_mailid;
288 if (isset($wdm_wpma_mailid) && $wdm_wpma_mailid != 0) {
289 $tableName = $wpdb->prefix.$wdm_wpma_options['tableName'];
290 $wpMailFailedError = isset( $wp_error->errors ) && isset( $wp_error->errors['wp_mail_failed'][0] ) ? implode( '; ', $wp_error->errors['wp_mail_failed'] ) : 'Unknown';
291 $wpdb->update($tableName,array('timestamp'=>current_time('mysql',false),'status'=>'error', 'info'=>$wpMailFailedError),array('id'=>intval($wdm_wpma_mailid)),array('%s', '%s', '%s'),'%d');
292 }
293 $wdm_wpma_mailid = 0;
294 if ( defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG ) {
295 error_log('[mail-queue] wp_mail_failed: '.print_r($wp_error, true));
296 }
297 }
298 add_action('wp_mail_failed','wdm_wpma_mail_failed',10,1);
299
300
301
302 // really send email function to send an email item immediately, without being added to the queue again
303 function wdm_wpma_really_send_mail($item, $args = null) {
304 global $wpdb, $wdm_wpma_options, $wdm_wpma_mailid, $wdm_wpma_pre_wp_mail_priority;
305
306 $currentstatus = isset($item['status']) ? $item['status'] : '';
307 if ($item['recipient'] && $item['recipient'] != '') { $to = wdm_wpma_safe_unserialize($item['recipient']); } else { $to = $wdm_wpma_options['email']; $item['subject'] = 'ERROR // '.$item['subject']; }
308 if ($item['headers'] && $item['headers'] != '') { $headers = wdm_wpma_safe_unserialize($item['headers']); } else { $headers = ''; }
309 if ($item['attachments'] && $item['attachments'] != '') { $attachments = wdm_wpma_safe_unserialize($item['attachments']); } else { $attachments = ''; }
310 $wdm_wpma_mailid = $item['id'];
311
312 if ( !in_array($currentstatus, ['queue', 'high'], /*strict*/true) || !$to ) {
313 return false;
314 }
315
316 remove_filter('pre_wp_mail', 'wdm_wpma_prewpmail', $wdm_wpma_pre_wp_mail_priority);
317 $sendstatus = wp_mail($to,$item['subject'],$item['message'],$headers,$attachments); // Send the email for real
318 add_filter('pre_wp_mail', 'wdm_wpma_prewpmail', $wdm_wpma_pre_wp_mail_priority, 2);
319
320 $wdm_wpma_mailid = 0;
321
322 $tableName = $wpdb->prefix.$wdm_wpma_options['tableName'];
323 if ($sendstatus) {
324 $info = '';
325 if (isset($args['send_mode']) && $args['send_mode']) {
326 $info = json_encode([
327 'send_mode' => $args['send_mode'],
328 ]);
329 } elseif ($currentstatus === 'high') {
330 $info = json_encode([
331 'prio' => 'high',
332 ]);
333 }
334 $wpdb->update($tableName,array('timestamp'=>current_time('mysql',false),'status'=>'sent','info'=>$info),array('id'=>$item['id']),'%s','%d');
335 }
336
337 // remove possible attachments from server after sending email
338 if (is_array($attachments)) {
339 global $wp_filesystem;
340 if ( ! is_a( $wp_filesystem, 'WP_Filesystem_Base') ){
341 include_once(ABSPATH . 'wp-admin/includes/file.php');
342 WP_Filesystem();
343 }
344 $attachmentfolder = pathinfo($attachments[0]);
345 $wp_filesystem->delete($attachmentfolder['dirname'],true,'d');
346 }
347
348 return $sendstatus;
349 }
350
351
352
353
354 /* ***************************************************************
355 CRON
356 **************************************************************** */
357
358 // Cross-process lock for the cron sender
359 // An expires_at value lets a crashed worker's stale
360 // lock be reclaimed instead of deadlocking the queue forever.
361 function wdm_wpma_try_lock_cron () {
362 global $wdm_wpma_options;
363 $key = 'wdm_wpma_cron_lock';
364 $ttl = max(60, intval($wdm_wpma_options['queue_interval']) * 2);
365 $expires_at = time() + $ttl;
366
367 if (add_option($key, $expires_at, '', /*autoload*/false)) { return true; }
368
369 // Option exists — check whether the previous holder's lock has expired.
370 $existing = (int) get_option($key, 0);
371 if ($existing > 0 && $existing < time()) {
372 update_option($key, $expires_at, false);
373 return true;
374 }
375 return false;
376 }
377
378 function wdm_wpma_release_lock_cron () {
379 delete_option('wdm_wpma_cron_lock');
380 }
381
382 function wdm_wpma_search_mail_from_queue() {
383 global $wdm_wpma_options;
384
385 // Only run if plugin is enabled or paused
386 if ( !in_array($wdm_wpma_options['enabled'], ['1', 'paused'], /*strict*/true) ) { return; }
387
388 // Triggercount to avoid multiple runs within the same PHP process
389 $wdm_wpma_options['triggercount']++;
390 if ($wdm_wpma_options['triggercount'] > 1) { return; }
391
392 // Cross-process lock: a second worker (e.g. a parallel cron tick) must back off.
393 // so we don't read the same rows twice and double-send them.
394 if (!wdm_wpma_try_lock_cron()) { return; }
395
396 try {
397 wdm_wpma_search_mail_from_queue_locked();
398 } finally {
399 wdm_wpma_release_lock_cron();
400 }
401 }
402
403 function wdm_wpma_search_mail_from_queue_locked() {
404 global $wpdb, $wdm_wpma_options;
405
406 $tableName = $wpdb->prefix.$wdm_wpma_options['tableName'];
407
408 // Total Mails waiting in the Queue?
409 $mailjobsTotal = $wpdb->get_var( "SELECT COUNT(*) FROM `$tableName` WHERE `status` = 'queue' OR `status` = 'high'" );
410
411 // Mails to send
412 $mailjobs = $wpdb->get_results($wpdb->prepare("SELECT * FROM `$tableName` WHERE `status` = 'queue' OR `status` = 'high' ORDER BY `status` ASC, `id` LIMIT %d", intval($wdm_wpma_options['queue_amount'])),'ARRAY_A');
413 $mailsInQueue = is_array($mailjobs) ? count($mailjobs) : 0;
414
415 // Maybe alert admin and auto-pause if too many mails in the Queue.
416 if ($wdm_wpma_options['alert_enabled'] == '1' && $mailjobsTotal > intval($wdm_wpma_options['email_amount'])) {
417
418 // Pause sending other emails if option is active and not paused already
419 $must_trigger_auto_pause = wdm_wpma_must_activate_pause_on_alert();
420
421 // Last alerts older than 6 hours?
422 $alert_cutoff = gmdate('Y-m-d H:i:s', current_time('timestamp', false) - (6 * HOUR_IN_SECONDS));
423 $alerts = $wpdb->get_results($wpdb->prepare("SELECT * FROM `$tableName` WHERE `status` = 'alert' AND `timestamp` > %s ORDER BY `id` DESC", $alert_cutoff), 'ARRAY_A');
424
425 // If no recent alert exists, send one;
426 // In case new auto-pause is triggered, always send alert independent of existing recent alerts to inform about the pause
427 if (!$alerts || $must_trigger_auto_pause) {
428
429 $alertMessage = 'Hi,';
430 $alertMessage .= "\n\n";
431 $alertMessage .= 'this is an important message from your WordPress website '.esc_url(get_option('siteurl')).'.';
432 $alertMessage .= "\n";
433 $alertMessage .= "\n".'The Mail Queue Plugin has detected that your website tries to send more emails than expected (currently '.$mailjobsTotal.').';
434 $alertMessage .= "\n".'Please take a close look at the email queue, because it contains more messages than the specified limit.';
435 $alertMessage .= "\n";
436 if ($must_trigger_auto_pause) {
437 $alertMessage .= "\n".'Please note: The email sending has been paused automatically. It will remain paused until you re-enable it manually in the plugin settings.';
438 $alertMessage .= "\n";
439 } elseif ($wdm_wpma_options['enabled'] === 'paused') {
440 if (wdm_wpma_is_pause_on_alert_active()) {
441 $alertMessage .= "\n".'Please note: The email sending has been paused automatically due to a recent alert. It will remain paused until you re-enable it manually in the plugin settings.';
442 $alertMessage .= "\n";
443 } else {
444 $alertMessage .= "\n".'Please note: The email sending is currently paused. If this is not intentional, please check the plugin settings.';
445 $alertMessage .= "\n";
446 }
447 }
448 $alertMessage .= "\n".'In case this is the usual amount of emails, you can adjust the threshold for alerts in the settings of your Mail Queue Plugin.';
449 $alertMessage .= "\n\n";
450 $alertMessage .= "-- ";
451 $alertMessage .= "\n";
452 $alertMessage .= admin_url('admin.php?page=wdm_wpma_mail_queue');
453 $alertSubject = '🔴 WordPress Mail Queue Alert - '.esc_html(get_option('blogname'));
454 $data = array(
455 'timestamp'=> current_time('mysql',false),
456 'recipient'=> sanitize_email($wdm_wpma_options['email']),
457 'subject' => $alertSubject,
458 'message' => $alertMessage,
459 'status' => 'alert',
460 'info' => json_encode([
461 'in_queue' => strval( $mailjobsTotal ),
462 'email_amount' => intval($wdm_wpma_options['email_amount']),
463 'queue_amount' => intval($wdm_wpma_options['queue_amount']),
464 'queue_interval' => intval($wdm_wpma_options['queue_interval']),
465 ]),
466 );
467 $wpdb->insert($tableName,$data);
468 wp_mail($wdm_wpma_options['email'],$alertSubject,$alertMessage);
469 }
470
471 if ($must_trigger_auto_pause) {
472 wdm_wpma_maybe_activate_pause_on_alert();
473 }
474 }
475
476 // Alert might have triggered a pause, so check again before sending emails
477 if ($wdm_wpma_options['enabled'] != '1') { return; }
478
479 // Send Mails in Queue
480 if ($mailsInQueue > 0) {
481 $results = array_slice($mailjobs,0,intval($wdm_wpma_options['queue_amount']));
482 if ($results && count($results) > 0) {
483 foreach($results as $index => $item) {
484 wdm_wpma_really_send_mail($item);
485 }
486 }
487 }
488
489 // Delete old logs
490 $clear_queue_cutoff = gmdate('Y-m-d H:i:s', current_time('timestamp', false) - (intval($wdm_wpma_options['clear_queue']) * HOUR_IN_SECONDS));
491 $wpdb->query($wpdb->prepare("DELETE FROM `$tableName` WHERE `status` != 'queue' AND `status` != 'high' AND `timestamp` < %s", $clear_queue_cutoff));
492
493 }
494 add_action('wp_mail_queue_hook','wdm_wpma_search_mail_from_queue');
495
496 // Custom Cron Interval
497 function wdm_wpma_cron_interval( $schedules ) {
498 global $wdm_wpma_options;
499 $schedules['wdm_wpma_interval'] = array(
500 'interval' => $wdm_wpma_options['queue_interval'],
501 'display' => esc_html__('WP Mail Queue'), );
502 return $schedules;
503 }
504 add_filter('cron_schedules','wdm_wpma_cron_interval');
505
506 // Set, Remove, or Reschedule Cron.
507 // Reschedule when the stored interval no longer matches the configured one,
508 // otherwise a settings change would only take effect after disable+enable.
509 $scheduled_event = wp_get_scheduled_event('wp_mail_queue_hook');
510 $should_be_active = in_array($wdm_wpma_options['enabled'], ['1', 'paused'], true);
511 $configured_interval = intval($wdm_wpma_options['queue_interval']);
512 if ($scheduled_event && !$should_be_active) {
513 wp_unschedule_event($scheduled_event->timestamp,'wp_mail_queue_hook');
514 } else if (!$scheduled_event && $should_be_active) {
515 wp_schedule_event(time(),'wdm_wpma_interval','wp_mail_queue_hook');
516 } else if ($scheduled_event && $should_be_active && intval($scheduled_event->interval) !== $configured_interval) {
517 wp_unschedule_event($scheduled_event->timestamp,'wp_mail_queue_hook');
518 wp_schedule_event(time(),'wdm_wpma_interval','wp_mail_queue_hook');
519 }
520
521
522
523
524
525 /* ***************************************************************
526 Queue Events
527 **************************************************************** */
528
529 function wdm_wpma_push_queue_event ($args) {
530 global $wpdb, $wdm_wpma_options;
531
532 $tableName = $wpdb->prefix.$wdm_wpma_options['tableName'];
533 $event_name = isset($args['name']) ? $args['name'] : '';
534 $event_data = isset($args['data']) ? $args['data'] : '';
535 if (!$event_name) { return false; }
536
537 $data = array(
538 'timestamp'=> current_time('mysql',false),
539 'status' => 'event',
540 'recipient'=> '',
541 'subject' => $event_name,
542 'message' => '',
543 'info' => $event_data ? (is_string($event_data) ? $event_data : json_encode($event_data)) : '',
544 );
545 $inserted = $wpdb->insert($tableName,$data);
546
547 return $inserted !== false;
548 }
549
550
551
552
553
554 /* ***************************************************************
555 WordPress Password Notification Emails
556 **************************************************************** */
557
558 function wdm_wpma_prioritize_password_reset_mail( $email, $key, $user_login, $user_data ) {
559 global $wdm_wpma_options;
560
561 // If Mail Queue is paused send password reset emails instantly to prevent lockouts.
562 // Otherwise, send with high priority to make sure that password reset emails are sent before other queued emails.
563 $prio_header = isset( $wdm_wpma_options['enabled'] ) && $wdm_wpma_options['enabled'] === 'paused' ? 'X-Mail-Queue-Prio: Instant' : 'X-Mail-Queue-Prio: High';
564
565 if ( empty( $email['headers'] ) ) {
566 $email['headers'] = array( $prio_header );
567 } elseif ( is_array( $email['headers'] ) ) {
568 if ( ! in_array( $prio_header, $email['headers'], true ) ) {
569 $email['headers'][] = $prio_header;
570 }
571 } elseif ( stripos( $email['headers'], $prio_header ) === false ) {
572 $email['headers'] .= ( $email['headers'] ? "\r\n" : '' ) . $prio_header;
573 }
574
575 return $email;
576 }
577 add_filter('retrieve_password_notification_email', 'wdm_wpma_prioritize_password_reset_mail', 10, 4);
578
579
580
581
582
583 /* ***************************************************************
584 Install/Uninstall/Upgrade
585 **************************************************************** */
586
587
588 /* Delete plugin options and database table */
589 function wdm_wpma_uninstall () {
590 global $wpdb;
591
592 delete_option( 'wdm_wpma_settings' );
593 delete_option( 'wdm_wpma_version' );
594 delete_option( 'wdm_wpma_pause_on_alert_active' );
595
596 $tableName = $wpdb->prefix.'mail_queue';
597 $wpdb->query( "DROP TABLE IF EXISTS $tableName" );
598 }
599
600 /* Delete Cron when Plugin deactivated */
601 function wdm_wpma_deactivate() {
602 wp_clear_scheduled_hook( 'wp_mail_queue_hook' );
603 }
604 register_deactivation_hook( __FILE__, 'wdm_wpma_deactivate' );
605
606 /* Create/Upgrade MySQL Table on Activation/Upgrade: https://codex.wordpress.org/Creating_Tables_with_Plugins */
607 function wdm_wpma_updateDatabaseTables() {
608 global $wpdb, $wdm_wpma_version;
609
610 $tableName = $wpdb->prefix.'mail_queue';
611
612 $charset_collate = $wpdb->get_charset_collate();
613
614 $sql = "CREATE TABLE $tableName (
615 id mediumint(9) NOT NULL AUTO_INCREMENT,
616 timestamp TIMESTAMP NOT NULL,
617 status varchar(55) DEFAULT '' NOT NULL,
618 recipient varchar(255) DEFAULT '' NOT NULL,
619 subject varchar(255) DEFAULT '' NOT NULL,
620 message mediumtext NOT NULL,
621 headers text NOT NULL,
622 attachments text NOT NULL,
623 info varchar(255) DEFAULT '' NOT NULL,
624 PRIMARY KEY (id)
625 ) $charset_collate;";
626
627 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
628 dbDelta( $sql );
629
630 update_option( 'wdm_wpma_version', $wdm_wpma_version, /*autoload*/true );
631 }
632
633 /* Update database and register hooks on activation */
634 function wdm_wpma_activate() {
635 wdm_wpma_updateDatabaseTables();
636 register_uninstall_hook( __FILE__, 'wdm_wpma_uninstall' );
637 }
638 register_activation_hook( __FILE__, 'wdm_wpma_activate' );
639
640 /* Upgrade routine: check for mismatching version numbers and run database update if necessary */
641 function wdm_wpma_check_update_db () {
642 global $wdm_wpma_version;
643 if ( get_option( 'wdm_wpma_version' ) !== $wdm_wpma_version ) {
644 wdm_wpma_updateDatabaseTables();
645 }
646 }
647 add_action( 'plugins_loaded', 'wdm_wpma_check_update_db', 10, 0 );
648
649
650
651
652 /* ***************************************************************
653 Options Page
654 **************************************************************** */
655 if (is_admin()) {
656 require_once( plugin_dir_path( __FILE__ ) . 'mail-queue-options.php' );
657 }
658
659
660
661
662 /* ***************************************************************
663 REST API
664 **************************************************************** */
665
666
667 function wdm_wpma_add_rest_endpoints () {
668 register_rest_route('wpma/v1', '/message/(?P<id>[\d]+)', array(
669 'methods' => 'GET',
670 'callback' => 'wdm_wpma_rest_get_message',
671 'permission_callback' => function () {
672 return current_user_can( 'manage_options' );
673 },
674 ));
675 }
676 add_action('rest_api_init', 'wdm_wpma_add_rest_endpoints', 10, 0);
677
678
679 function wdm_wpma_rest_get_message ($request) {
680 global $wpdb, $wdm_wpma_options;
681 $tableName = $wpdb->prefix.$wdm_wpma_options['tableName'];
682 $id = intval($request['id']);
683 $row = $wpdb->get_row( $wpdb->prepare("SELECT * FROM `$tableName` WHERE `id` = %d", $id ), ARRAY_A );
684 if ($row) {
685 // Search for content-type header to detect html emails
686 $is_content_type_html = false;
687 $headers = wdm_wpma_safe_unserialize( $row['headers'] );
688 if (is_string($headers)) {
689 $headers = [ $headers ];
690 } else if (!is_array($headers)) {
691 $headers = [];
692 }
693 foreach ( $headers as $header ) {
694 if ( preg_match( '/content-type: ?text\/html/i', $header ) ) {
695 $is_content_type_html = true;
696 break;
697 }
698 }
699 return array(
700 'status' => 'ok',
701 'data' => array(
702 'html' => wdm_wpma_render_list_message($row['message'],$is_content_type_html),
703 ),
704 );
705 } else {
706 return new WP_Error( 'no_message', __( 'Message not found' ), array( 'status' => 404 ) );
707 }
708 }
709
710 function wdm_wpma_render_list_message ($message, $is_content_type_html) {
711 // Split html emails into parts and extract plain text preview
712 $parts = explode( '<body', $message );
713 $is_html = $is_content_type_html || count($parts) > 1;
714 if ($is_html) {
715 if (count($parts) > 1) {
716 $header = $parts[0];
717 $body = '<body'.$parts[1];
718 } else {
719 $header = '';
720 $body = $parts[0];
721 }
722 $parts = explode('</body>', $body);
723 if (count($parts) > 1) {
724 $body = $parts[0].'</body>';
725 $footer = $parts[1];
726 } else {
727 $body = $parts[0];
728 $footer = '';
729 }
730 if (!function_exists('convert_html_to_text')) {
731 require_once __DIR__.'/lib/html2text/html2text.php';
732 }
733 // ignore warnings when converting html containing non-converted HTML entities
734 $internal_errors = libxml_use_internal_errors(true);
735 $text = convert_html_to_text( $body );
736 libxml_use_internal_errors($internal_errors);
737 } else {
738 $text = $message;
739 $header = '';
740 $body = '';
741 $footer = '';
742 }
743 $html = '';
744 $html .= '<details class="wdm-wpma-email-source-meta" open><summary>Text</summary><pre class="wdm-wpma-email-plain-text">'.esc_html( $text ).'</pre></details>';
745 $html .= $header ? '<details class="wdm-wpma-email-source-meta"><summary>HTML Header</summary><pre>'.esc_html( wdm_wpma_render_html_for_display($header) ).'</pre></details>' : '';
746 $html .= $body ? '<details class="wdm-wpma-email-source-meta"><summary>HTML Body</summary><pre>'.esc_html( wdm_wpma_render_html_for_display($body) ).'</pre></details>' : '';
747 $html .= $footer ? '<details class="wdm-wpma-email-source-meta"><summary>HTML Footer</summary><pre>'.esc_html( wdm_wpma_render_html_for_display($footer) ).'</pre></details>' : '';
748 return $html;
749 }
750
751 function wdm_wpma_render_html_for_display ($html) {
752 $html = preg_replace( '/;base64,[^"\']+("|\')+/', ';base64, [...] $1', $html );
753 return $html;
754 }
755