PluginProbe
Mail Queue / 1.4.5
Mail Queue v1.4.5
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.5, at mail-queue.php

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