PluginProbe
Mail Queue / 1.4.6
Mail Queue v1.4.6
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.4.6, at mail-queue.php

514 lines 20.3 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.4.6
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.4.6';
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 'queue_amount' => '1',
39 'queue_interval' => '5',
40 'queue_interval_unit' => 'minutes',
41 'clear_queue' => '14',
42 'tableName' => 'mail_queue',
43 'triggercount' => 0,
44 );
45 $args = get_option('wdm_wpma_settings');
46 $options = wp_parse_args($args,$defaults);
47
48 if ($options['queue_interval_unit'] == 'seconds') {
49 $options['queue_interval'] = intval($options['queue_interval']);
50 if ($options['queue_interval'] < 10) { $options['queue_interval'] = 10; } // Minimum Interval 10 Seconds
51 } else {
52 $options['queue_interval'] = intval($options['queue_interval']) * 60;
53 }
54
55 $options['clear_queue'] = intval($options['clear_queue']) * 24;
56 return $options;
57 }
58
59
60
61
62
63 /* ***************************************************************
64 Overwrite wp_mail() if Plugin enabled and no Cron is running
65 **************************************************************** */
66 $wdm_wpma_mailid = 0;
67 $wdm_wpma_options = wdm_wpma_get_settings(); // Get Settings
68 $wdm_wpma_pre_wp_mail_priority = 99999;
69
70 if ($wdm_wpma_options['enabled'] == '1' && wp_doing_cron() == false) {
71 // High priority: run late in the game to react to previous filters
72 add_filter('pre_wp_mail', 'wdm_wpma_prewpmail', $wdm_wpma_pre_wp_mail_priority, 2);
73 }
74
75 // pre WP Mail Filter
76 function wdm_wpma_prewpmail($return, $atts) {
77
78 global $wpdb, $wdm_wpma_options;
79
80 if (!is_null($return)) {
81 // Another pre_wp_mail filter has already returned a value, so the mail is not added to the queue
82 return $return;
83 }
84
85 // Mail Variables
86 $to = $atts['to'];
87 $subject = $atts['subject'];
88 $message = $atts['message'];
89 $headers = $atts['headers'];
90 $attachments = $atts['attachments'];
91 $status = 'queue';
92
93 // Make sure that $headers always is an array
94 if ($headers) {
95 if (!is_array($headers)) {
96 $headers = explode( "\n", str_replace( "\r\n", "\n", $headers ) );
97 }
98 } else {
99 $headers = [];
100 }
101
102 // Loop through email headers
103 // - Instant Sending or Prio Mail?
104 // - Track if ContentType header is set
105 $hasContentTypeHeader = false;
106 $hasFromHeader = false;
107 foreach($headers as $index => $val) {
108 $val = trim($val);
109 if (preg_match("#^X-Mail-Queue-Prio: +Instant *$#i",$val)) {
110 array_splice($headers,$index,1);
111 $status = 'instant';
112 break;
113 } else if (preg_match("#^X-Mail-Queue-Prio: +High *$#i",$val)) {
114 array_splice($headers,$index,1);
115 $status = 'high';
116 break;
117 } else if (preg_match('#^Content-Type:#i',$val)) {
118 $hasContentTypeHeader = true;
119 } else if (preg_match('#^From:#i',$val)) {
120 $hasFromHeader = true;
121 }
122 }
123
124 // For all emails that are stored in the queue to be sent later:
125 // Store custom filtered values in headers if available.
126 // Support the following hooks used in wp_mail:
127 // - wp_mail_content_type
128 // - wp_mail_charset
129 // - wp_mail_from
130 // - wp_mail_from_name
131 if ($status !== 'instant') {
132 if (!$hasContentTypeHeader) {
133 $contentType = apply_filters('wp_mail_content_type','text/plain');
134 if ( $contentType ) {
135 if (stripos($contentType,'multipart') === false) {
136 $charset = apply_filters('wp_mail_charset',get_bloginfo('charset'));
137 } else {
138 $charset = '';
139 }
140 $headers[] = 'Content-Type: '.$contentType.($charset ? '; charset="'.$charset.'"' : '');
141 }
142 }
143 if (!$hasFromHeader) {
144 $from_Email = apply_filters('wp_mail_from','');
145 if ($from_Email) {
146 $fromName = apply_filters('wp_mail_from_name','');
147 if ($fromName) {
148 $headers[] = $fromName.' <'.$from_Email.'>';
149 } else {
150 $headers[] = $from_Email;
151 }
152 }
153 }
154 }
155
156
157 // Write email in Queue
158 $tableName = $wpdb->prefix.$wdm_wpma_options['tableName'];
159 $data = array(
160 'timestamp'=> current_time('mysql',false),
161 'recipient'=> maybe_serialize($to),
162 'subject'=> $subject,
163 'message'=> $message,
164 'status' => $status,
165 'attachments' => ''
166 );
167 if (isset($headers) && $headers) { $data['headers'] = maybe_serialize($headers); }
168
169 // store attachments in /attachments/ Folder, to address them later
170 if (isset($attachments) && $attachments && $attachments != '') {
171
172 $subfolder = time().'-'.rand(0,999999);
173 $foldercreated = wp_mkdir_p(plugin_dir_path(__FILE__).'attachments/'.$subfolder);
174 if (!$foldercreated) {
175 error_log('Could not create Subfolder for Email attachment');
176 $data['info'] = 'Error: Could not store attachments';
177 } else {
178 if (!is_array($attachments)) { $attachments = array($attachments); }
179 $newattachments = array();
180 global $wp_filesystem;
181 if ( ! is_a( $wp_filesystem, 'WP_Filesystem_Base') ){
182 include_once(ABSPATH . 'wp-admin/includes/file.php');
183 WP_Filesystem();
184 }
185 foreach($attachments as $item) {
186 $newfile = plugin_dir_path(__FILE__).'attachments/'.$subfolder.'/'.basename($item);
187 $wp_filesystem->copy($item,$newfile);
188 array_push($newattachments,$newfile);
189 }
190 $data['attachments'] = maybe_serialize($newattachments);
191 }
192 }
193 $inserted = $wpdb->insert($tableName,$data);
194
195 if ($status == 'instant') {
196 return null;
197 } else if ( !$inserted ) {
198 // No database entry, email cannot be send
199 return false;
200 } else {
201 // Fake Submit by returning 'True'
202 return true;
203 }
204
205 }
206
207
208
209 // show wp_mail() errors
210 function wdm_wpma_mail_failed( $wp_error ) {
211 global $wpdb,$wdm_wpma_options,$wdm_wpma_mailid;
212 if (isset($wdm_wpma_mailid) && $wdm_wpma_mailid != 0) {
213 $tableName = $wpdb->prefix.$wdm_wpma_options['tableName'];
214 $wpMailFailedError = isset( $wp_error->errors ) && isset( $wp_error->errors['wp_mail_failed'][0] ) ? implode( '; ', $wp_error->errors['wp_mail_failed'] ) : '<em>Unknown</em>';
215 $wpdb->update($tableName,array('timestamp'=>current_time('mysql',false),'status'=>'error', 'info'=>$wpMailFailedError),array('id'=>intval($wdm_wpma_mailid)),array('%s', '%s', '%s'),'%d');
216 }
217 return error_log(print_r($wp_error, true));
218 }
219 add_action('wp_mail_failed','wdm_wpma_mail_failed',10,1);
220
221
222
223
224 /* ***************************************************************
225 CRON
226 **************************************************************** */
227 function wdm_wpma_search_mail_from_queue() {
228
229 global $wpdb,$wdm_wpma_options, $wdm_wpma_mailid, $wdm_wpma_pre_wp_mail_priority;
230
231 if ($wdm_wpma_options['enabled'] != '1') { return; }
232 $tableName = $wpdb->prefix.$wdm_wpma_options['tableName'];
233
234 // Triggercount to avoid multiple runs
235 $wdm_wpma_options['triggercount']++;
236 if ($wdm_wpma_options['triggercount'] > 1) { return; }
237
238 // Total Mails waiting in the Queue?
239 $mailjobsTotal = $wpdb->get_var( "SELECT COUNT(*) FROM `$tableName` WHERE `status` = 'queue' OR `status` = 'high'" );
240
241 // Mails to send
242 $mailjobs = $wpdb->get_results("SELECT * FROM `$tableName` WHERE `status` = 'queue' OR `status` = 'high' ORDER BY `status` ASC, `id` LIMIT ".intval($wdm_wpma_options['queue_amount']),'ARRAY_A');
243 $mailsInQueue = is_array($mailjobs) ? count($mailjobs) : 0;
244
245 // Alert Admin, if too many mails in the Queue.
246 if ($wdm_wpma_options['alert_enabled'] == '1' && $mailjobsTotal > intval($wdm_wpma_options['email_amount'])) {
247
248 // Last alerts older than 6 hours?
249 $alerts = $wpdb->get_results("SELECT * FROM `$tableName` WHERE `status` = 'alert' AND `timestamp` > NOW() - INTERVAL 6 HOUR",'ARRAY_A');
250
251 // If no alerts, then send one
252 if (!$alerts) {
253 $alertMessage = 'Hi,';
254 $alertMessage .= "\n\n";
255 $alertMessage .= 'this is an important message from your WordPress website '.esc_url(get_option('siteurl')).'.';
256 $alertMessage .= "\n";
257 $alertMessage .= "\n".'The Mail Queue Plugin has detected that your website tries to send more emails than expected (currently '.$mailjobsTotal.').';
258 $alertMessage .= "\n".'Please take a close look at the email queue, because it contains more messages than the specified limit.';
259 $alertMessage .= "\n";
260 $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.';
261 $alertMessage .= "\n\n";
262 $alertMessage .= "-- ";
263 $alertMessage .= "\n";
264 $alertMessage .= admin_url();
265 $alertSubject = '🔴 WordPress Mail Queue Alert - '.esc_html(get_option('blogname'));
266 $data = array(
267 'timestamp'=> current_time('mysql',false),
268 'recipient'=> sanitize_email($wdm_wpma_options['email']),
269 'subject' => $alertSubject,
270 'message' => $alertMessage,
271 'status' => 'alert',
272 'info' => json_encode([
273 'in_queue' => strval( $mailsInQueue ),
274 'email_amount' => intval($wdm_wpma_options['email_amount']),
275 'queue_amount' => intval($wdm_wpma_options['queue_amount']),
276 'queue_interval' => intval($wdm_wpma_options['queue_interval']),
277 ]),
278 );
279 $wpdb->insert($tableName,$data);
280 wp_mail($wdm_wpma_options['email'],$alertSubject,$alertMessage);
281 }
282
283 }
284
285 // Send Mails in Queue
286 if ($mailsInQueue > 0) {
287 $results = array_slice($mailjobs,0,intval($wdm_wpma_options['queue_amount']));
288 if ($results && count($results) > 0) {
289 foreach($results as $index => $item) {
290 if ($item['recipient'] && $item['recipient'] != '') { $to = maybe_unserialize($item['recipient']); } else { $to = $wdm_wpma_options['email']; $item['subject'] = 'ERROR // '.$item['subject']; }
291 if ($item['headers'] && $item['headers'] != '') { $headers = maybe_unserialize($item['headers']); } else { $headers = ''; }
292 if ($item['attachments'] && $item['attachments'] != '') { $attachments = maybe_unserialize($item['attachments']); } else { $attachments = ''; }
293 $wdm_wpma_mailid = $item['id'];
294
295 remove_filter('pre_wp_mail', 'wdm_wpma_prewpmail', $wdm_wpma_pre_wp_mail_priority);
296 $sendstatus = wp_mail($to,$item['subject'],$item['message'],$headers,$attachments); // Finally sends the email for real
297 add_filter('pre_wp_mail', 'wdm_wpma_prewpmail', $wdm_wpma_pre_wp_mail_priority, 2);
298 if ($sendstatus) {
299 $wpdb->update($tableName,array('timestamp'=>current_time('mysql',false),'status'=>'sent'),array('id'=>$item['id']),'%s','%d');
300 }
301 if (is_array($attachments)) {
302 global $wp_filesystem;
303 if ( ! is_a( $wp_filesystem, 'WP_Filesystem_Base') ){
304 include_once(ABSPATH . 'wp-admin/includes/file.php');
305 WP_Filesystem();
306 }
307 $attachmentfolder = pathinfo($attachments[0]);
308 $wp_filesystem->delete($attachmentfolder['dirname'],true,'d');
309 }
310 }
311 }
312 }
313
314 // Delete old logs
315 $wpdb->query("DELETE FROM `$tableName` WHERE `status` != 'queue' AND `timestamp` < NOW() - INTERVAL ".esc_sql($wdm_wpma_options['clear_queue'])." HOUR");
316
317 }
318 add_action('wp_mail_queue_hook','wdm_wpma_search_mail_from_queue');
319
320 // Custom Cron Interval
321 function wdm_wpma_cron_interval( $schedules ) {
322 global $wdm_wpma_options;
323 $schedules['wdm_wpma_interval'] = array(
324 'interval' => $wdm_wpma_options['queue_interval'],
325 'display' => esc_html__('WP Mail Queue'), );
326 return $schedules;
327 }
328 add_filter('cron_schedules','wdm_wpma_cron_interval');
329
330 // Set or Remove Cron
331 $next_wpma_cron_timestamp = wp_next_scheduled('wp_mail_queue_hook');
332 if ($next_wpma_cron_timestamp && $wdm_wpma_options['enabled'] != '1') {
333 wp_unschedule_event($next_wpma_cron_timestamp,'wp_mail_queue_hook');
334 } else if (!$next_wpma_cron_timestamp && $wdm_wpma_options['enabled'] == '1') {
335 wp_schedule_event(time(),'wdm_wpma_interval','wp_mail_queue_hook');
336 }
337
338
339
340 /* ***************************************************************
341 Install/Uninstall/Upgrade
342 **************************************************************** */
343
344
345 /* Delete plugin options and database table */
346 function wdm_wpma_uninstall () {
347 global $wpdb;
348
349 $optionName = 'wdm_wpma_settings';
350 delete_option( $optionName );
351
352 $optionName = 'wdm_wpma_version';
353 delete_option( $optionName );
354
355 $tableName = $wpdb->prefix.'mail_queue';
356 $wpdb->query( "DROP TABLE IF EXISTS $tableName" );
357 }
358
359 /* Delete Cron when Plugin deactivated */
360 function wdm_wpma_deactivate() {
361 wp_clear_scheduled_hook( 'wp_mail_queue_hook' );
362 }
363
364 /* Create/Upgrade MySQL Table on Activation/Upgrade: https://codex.wordpress.org/Creating_Tables_with_Plugins */
365 function wdm_wpma_updateDatabaseTables() {
366 global $wpdb, $wdm_wpma_version;
367
368 $tableName = $wpdb->prefix.'mail_queue';
369
370 $charset_collate = $wpdb->get_charset_collate();
371
372 $sql = "CREATE TABLE $tableName (
373 id mediumint(9) NOT NULL AUTO_INCREMENT,
374 timestamp TIMESTAMP NOT NULL,
375 status varchar(55) DEFAULT '' NOT NULL,
376 recipient varchar(255) DEFAULT '' NOT NULL,
377 subject varchar(255) DEFAULT '' NOT NULL,
378 message mediumtext NOT NULL,
379 headers text NOT NULL,
380 attachments text NOT NULL,
381 info varchar(255) DEFAULT '' NOT NULL,
382 PRIMARY KEY (id)
383 ) $charset_collate;";
384
385 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
386 dbDelta( $sql );
387
388 update_option( 'wdm_wpma_version', $wdm_wpma_version, /*autoload*/true );
389 }
390
391 /* Update database and register hooks on activation */
392 function wdm_wpma_activate() {
393 wdm_wpma_updateDatabaseTables();
394 register_uninstall_hook( __FILE__, 'wdm_wpma_uninstall' );
395 register_deactivation_hook( __FILE__, 'wdm_wpma_deactivate' );
396 }
397 register_activation_hook( __FILE__, 'wdm_wpma_activate' );
398
399 /* Upgrade routine: check for mismatching version numbers and run database update if necessary */
400 function wdm_wpma_check_update_db () {
401 global $wdm_wpma_version;
402 if ( get_option( 'wdm_wpma_version' ) !== $wdm_wpma_version ) {
403 wdm_wpma_updateDatabaseTables();
404 }
405 }
406 add_action( 'plugins_loaded', 'wdm_wpma_check_update_db', 10, 0 );
407
408
409
410
411 /* ***************************************************************
412 Options Page
413 **************************************************************** */
414 if (is_admin()) {
415 require_once( plugin_dir_path( __FILE__ ) . 'mail-queue-options.php' );
416 }
417
418
419
420
421 /* ***************************************************************
422 REST API
423 **************************************************************** */
424
425
426 function wdm_wpma_add_rest_endpoints () {
427 register_rest_route('wpma/v1', '/message/(?P<id>[\d]+)', array(
428 'methods' => 'GET',
429 'callback' => 'wdm_wpma_rest_get_message',
430 'permission_callback' => function () {
431 return current_user_can( 'manage_options' );
432 },
433 ));
434 }
435 add_action('rest_api_init', 'wdm_wpma_add_rest_endpoints', 10, 0);
436
437
438 function wdm_wpma_rest_get_message ($request) {
439 global $wpdb, $wdm_wpma_options;
440 $tableName = $wpdb->prefix.$wdm_wpma_options['tableName'];
441 $id = intval($request['id']);
442 $row = $wpdb->get_row( $wpdb->prepare("SELECT * FROM `$tableName` WHERE `id` = %d", $id ), ARRAY_A );
443 if ($row) {
444 // Search for content-type header to detect html emails
445 $is_content_type_html = false;
446 $headers = maybe_unserialize( $row['headers'] );
447 if (is_string($headers)) {
448 $headers = [ $headers ];
449 } else if (!is_array($headers)) {
450 $headers = [];
451 }
452 foreach ( $headers as $header ) {
453 if ( preg_match( '/content-type: ?text\/html/i', $header ) ) {
454 $is_content_type_html = true;
455 break;
456 }
457 }
458 return array(
459 'status' => 'ok',
460 'data' => array(
461 'html' => wdm_wpma_render_list_message(maybe_unserialize($row['message']),$is_content_type_html),
462 ),
463 );
464 } else {
465 return new WP_Error( 'no_message', __( 'Message not found' ), array( 'status' => 404 ) );
466 }
467 }
468
469 function wdm_wpma_render_list_message ($message, $is_content_type_html) {
470 // Split html emails into parts and extract plain text preview
471 $parts = explode( '<body', $message );
472 $is_html = $is_content_type_html || count($parts) > 1;
473 if ($is_html) {
474 if (count($parts) > 1) {
475 $header = $parts[0];
476 $body = '<body'.$parts[1];
477 } else {
478 $header = '';
479 $body = $parts[0];
480 }
481 $parts = explode('</body>', $body);
482 if (count($parts) > 1) {
483 $body = $parts[0].'</body>';
484 $footer = $parts[1];
485 } else {
486 $body = $parts[0];
487 $footer = '';
488 }
489 if (!function_exists('convert_html_to_text')) {
490 require_once __DIR__.'/lib/html2text/html2text.php';
491 }
492 // ignore warnings when converting html containing non-converted HTML entities
493 $internal_errors = libxml_use_internal_errors(true);
494 $text = convert_html_to_text( $body );
495 libxml_use_internal_errors($internal_errors);
496 } else {
497 $text = $message;
498 $header = '';
499 $body = '';
500 $footer = '';
501 }
502 $html = '';
503 $html .= '<details class="wdm-wpma-email-source-meta" open><summary>Text</summary><pre class="wdm-wpma-email-plain-text">'.esc_html( $text ).'</pre></details>';
504 $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>' : '';
505 $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>' : '';
506 $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>' : '';
507 return $html;
508 }
509
510 function wdm_wpma_render_html_for_display ($html) {
511 $html = preg_replace( '/;base64,[^"\']+("|\')+/', ';base64, [...] $1', $html );
512 return $html;
513 }
514