PluginProbe
Mail Queue / 1.6.1
Mail Queue v1.6.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-options.php

mail-queue-options.php in Mail Queue 1.6.1, at mail-queue-options.php

1,140 lines 65.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) { exit; }
4
5 /* ***************************************************************
6 Options Page
7 **************************************************************** */
8
9 function wdm_wpma_actionlinks ( $actions ) {
10 $links = array(
11 '<a href="'.admin_url('admin.php?page=wdm_wpma_mail_queue').'">Settings</a>',
12 '<a href="'.admin_url('admin.php?page=wdm_wpma_mail_queue-tab-log').'">Log</a>',
13 '<a href="'.admin_url('admin.php?page=wdm_wpma_mail_queue-tab-queue').'">Queue</a>',
14 '<a href="'.admin_url('admin.php?page=wdm_wpma_mail_queue-tab-faq').'">FAQs</a>',
15 );
16 return array_merge($actions,$links );
17 }
18 add_filter('plugin_action_links_mail-queue/mail-queue.php','wdm_wpma_actionlinks');
19
20 // Options Page
21 function wdm_wpma_settings_page_menuitem() {
22 add_menu_page('Mail Queue','Mail Queue','manage_options','wdm_wpma_mail_queue','wdm_wpma_settings_page','data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTc4IiBoZWlnaHQ9IjE3OCIgdmlld0JveD0iMCAwIDE3OCAxNzgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNzguNzI0MSA1LjI1MzA5Qzc2LjkwNzQgNC43MDgxIDc0Ljk0MDEgNS4wNTQxMyA3My40MTg0IDYuMTg2MjlDNzEuODk2OCA3LjMxODQ1IDcxIDkuMTAzNDEgNzEgMTEuMDAwMVYxNjdDNzEgMTY4Ljg5NyA3MS44OTY4IDE3MC42ODIgNzMuNDE4NCAxNzEuODE0Qzc0Ljk0MDEgMTcyLjk0NiA3Ni45MDc0IDE3My4yOTIgNzguNzI0MSAxNzIuNzQ3TDE1OC43MjQgMTQ4Ljc0N0MxNjEuMjYyIDE0Ny45ODYgMTYzIDE0NS42NSAxNjMgMTQzVjM1QzE2MyAzMi4zNTA0IDE2MS4yNjIgMzAuMDE0NSAxNTguNzI0IDI5LjI1MzFMNzguNzI0MSA1LjI1MzA5Wk04NS43ODg3IDIyLjM4NDZDODcuNzg1NCAyMS40Mzk0IDkwLjE3MDMgMjIuMjkxOSA5MS4xMTU0IDI0LjI4ODdMMTIyLjg1MiA5MS4zMzc4TDE0My4zMzQgNDQuNDAwMkMxNDQuMjE3IDQyLjM3NTUgMTQ2LjU3NSA0MS40NTAzIDE0OC42IDQyLjMzMzhDMTUwLjYyNSA0My4yMTc0IDE1MS41NSA0NS41NzUgMTUwLjY2NiA0Ny41OTk4TDEyNi42NjYgMTAyLjZDMTI2LjAzOSAxMDQuMDM3IDEyNC42MjkgMTA0Ljk3NiAxMjMuMDYxIDEwNUMxMjEuNDkzIDEwNS4wMjQgMTIwLjA1NiAxMDQuMTI5IDExOS4zODUgMTAyLjcxMUw4My44ODQ2IDI3LjcxMTNDODIuOTM5NCAyNS43MTQ2IDgzLjc5MTkgMjMuMzI5NyA4NS43ODg3IDIyLjM4NDZaIiBmaWxsPSIjYTdhYWFkIi8+CjxwYXRoIGQ9Ik00OSAxM0M1Mi4zMTM3IDEzIDU1IDE1LjY4NjMgNTUgMTlWMTU5QzU1IDE2Mi4zMTQgNTIuMzEzNyAxNjUgNDkgMTY1QzQ1LjY4NjMgMTY1IDQzIDE2Mi4zMTQgNDMgMTU5VjE5QzQzIDE1LjY4NjMgNDUuNjg2MyAxMyA0OSAxM1oiIGZpbGw9IiNhN2FhYWQiLz4KPHBhdGggZD0iTTIxIDIxQzI0LjMxMzcgMjEgMjcgMjMuNjg2MyAyNyAyN1YxNTFDMjcgMTU0LjMxNCAyNC4zMTM3IDE1NyAyMSAxNTdDMTcuNjg2MyAxNTcgMTUgMTU0LjMxNCAxNSAxNTFWMjdDMTUgMjMuNjg2MyAxNy42ODYzIDIxIDIxIDIxWiIgZmlsbD0iI2E3YWFhZCIvPgo8L3N2Zz4=');
23 add_submenu_page('wdm_wpma_mail_queue','Settings','Settings','manage_options','wdm_wpma_mail_queue','wdm_wpma_settings_page');
24 $hook_log = add_submenu_page('wdm_wpma_mail_queue','Log','Log','manage_options','wdm_wpma_mail_queue-tab-log','wdm_wpma_settings_page');
25 $hook_queue = add_submenu_page('wdm_wpma_mail_queue','Queue','Queue','manage_options','wdm_wpma_mail_queue-tab-queue','wdm_wpma_settings_page');
26 add_submenu_page('wdm_wpma_mail_queue','FAQ','FAQ','manage_options','wdm_wpma_mail_queue-tab-faq','wdm_wpma_settings_page');
27 if (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) {
28 add_submenu_page('wdm_wpma_mail_queue','Cron Information','Cron Information','manage_options','wdm_wpma_mail_queue-tab-croninfo','wdm_wpma_settings_page');
29 }
30 // Bulk actions run on load-{page}: that hook fires before admin-header.php produces
31 // any output, so the post-action redirect (PRG) can still send headers safely.
32 add_action('load-'.$hook_log,'wdm_wpma_handle_list_table_actions');
33 add_action('load-'.$hook_queue,'wdm_wpma_handle_list_table_actions');
34 }
35 add_action('admin_menu','wdm_wpma_settings_page_menuitem');
36
37 // Add test email hook
38 function wdm_wpma_add_testmail_action() {
39
40 if ( ! current_user_can( 'manage_options' ) ) {
41 wp_die( 'Sorry, you are not allowed to access this page.' );
42 }
43
44 check_admin_referer( 'wdm_wpma_add_testmail' );
45
46 global $wpdb;
47 $wdm_wpma_options = wdm_wpma_options();
48
49 $tableName = wdm_wpma_table();
50 $data = array(
51 'timestamp' => current_time('mysql',false),
52 'recipient' => $wdm_wpma_options['email'],
53 'subject' => 'Testmail #'.time(),
54 'message' => 'This is just a test email sent by the Mail Queue plugin.',
55 'status' => 'queue',
56 );
57 $wpdb->insert($tableName,$data);
58
59 wp_safe_redirect(admin_url('admin.php?page=wdm_wpma_mail_queue-tab-queue&testmail_added=1'));
60 exit;
61 }
62 add_action('admin_post_wdm_wpma_add_testmail', 'wdm_wpma_add_testmail_action');
63
64 function wdm_wpma_settings_page_assets () {
65 global $wdm_wpma_version;
66 $screen = get_current_screen();
67 if ( preg_match( '#wdm_wpma_mail_queue#', $screen->base ) ) {
68 wp_enqueue_style( 'wdm_wpma_style', plugins_url( 'assets/css/admin.css', __FILE__ ), /*deps*/array(), /*ver*/$wdm_wpma_version );
69 wp_enqueue_script( 'wdm_wpma_script', plugins_url( 'assets/js/wdm-wpma-admin.js', __FILE__ ), /*deps*/array( 'jquery' ), /*ver*/$wdm_wpma_version, /*in_footer*/true );
70 wp_add_inline_script( 'wdm_wpma_script', wdm_wpma_settings_page_inline_script(), 'before' );
71 }
72 }
73 add_action( 'admin_enqueue_scripts', 'wdm_wpma_settings_page_assets' );
74
75 // Options Page Script
76 function wdm_wpma_settings_page_inline_script () {
77 $d = '';
78 $d .= '( function ( global ) {';
79 $d .= '"use strict";';
80 $d .= 'const wpma = global.wpma = global.wpma || {};';
81 $d .= 'wpma.restUrl = "'.esc_url( wp_make_link_relative( rest_url() ) ).'";';
82 $d .= 'wpma.restNonce = "'.esc_html( wp_create_nonce( 'wp_rest' ) ).'";';
83 $d .= '}) ( this );';
84 return $d;
85 }
86
87 function wdm_wpma_get_pause_on_alert_notice_message() {
88 if (!wdm_wpma_is_pause_on_alert_active()) { return ''; }
89
90 return 'The Queue was paused automatically because an alert was triggered. It will stay paused until you change the status manually.';
91 }
92
93 function wdm_wpma_render_pause_on_alert_notice() {
94 $message = wdm_wpma_get_pause_on_alert_notice_message();
95 if (!$message) { return; }
96
97 echo '<div class="notice notice-warning"><p><b>The Queue is currently auto-paused.</b> <br />'.esc_html($message).'</p></div>';
98 }
99
100 // Runs the list-table bulk actions on load-{page} of the Log and Queue pages,
101 // i.e. before any page output — see wdm_wpma_settings_page_menuitem().
102 function wdm_wpma_handle_list_table_actions() {
103 if ( ! current_user_can( 'manage_options' ) ) { return; }
104 $table = new wdm_wpma_Log_Table();
105 $table->process_bulk_action();
106 }
107
108 // PRG helper: after a bulk action always redirect back to a plugin page, carrying
109 // the result counts as query args for wdm_wpma_render_bulk_action_notices().
110 function wdm_wpma_bulk_action_redirect( $page, $action, $count_ok, $count_error ) {
111 $url = add_query_arg(
112 array(
113 'page' => $page,
114 'wpma_notice' => $action,
115 'wpma_ok' => intval($count_ok),
116 'wpma_error' => intval($count_error),
117 ),
118 admin_url('admin.php')
119 );
120 wp_safe_redirect( $url );
121 exit;
122 }
123
124 // Renders the result notices after a bulk action (delete / resend / sendnow).
125 // The counts arrive as query args set by the PRG redirect in process_bulk_action().
126 function wdm_wpma_render_bulk_action_notices() {
127 if ( ! isset($_GET['wpma_notice']) || ! isset($_GET['page']) ) { return; }
128 if ( strpos( sanitize_key($_GET['page']), 'wdm_wpma_mail_queue' ) !== 0 ) { return; }
129 if ( ! current_user_can( 'manage_options' ) ) { return; }
130
131 $action = sanitize_key($_GET['wpma_notice']);
132 $ok = isset($_GET['wpma_ok']) ? max( 0, intval($_GET['wpma_ok']) ) : 0;
133 $error = isset($_GET['wpma_error']) ? max( 0, intval($_GET['wpma_error']) ) : 0;
134
135 $success = '';
136 $failure = '';
137 if ($action == 'delete') {
138 if ($ok > 0) { $success = $ok.' item(s) have been deleted.'; }
139 } else if ($action == 'resend') {
140 if ($ok > 0) { $success = $ok.' email(s) have been put again into the <a href="admin.php?page=wdm_wpma_mail_queue-tab-queue">Queue</a>.'; }
141 // Covers both skip reasons: attachments gone, and rows that aren't resendable
142 // at all (already queued/sending, queue bookkeeping, or no longer existing).
143 if ($error > 0) { $failure = $error.' email(s) could not be resent. Only already sent or failed emails without attachments can be resent (attachments are deleted after the send attempt).'; }
144 } else if ($action == 'sendnow') {
145 if ($ok > 0) { $success = $ok.' email(s) have been sent.'; }
146 if ($error > 0) { $failure = $error.' email(s) could not be sent.'; }
147 }
148
149 if ($success) { echo '<div class="notice notice-success is-dismissible"><p>'.$success.'</p></div>'; }
150 if ($failure) { echo '<div class="notice notice-error is-dismissible"><p>'.esc_html($failure).'</p></div>'; }
151 }
152 add_action('admin_notices','wdm_wpma_render_bulk_action_notices');
153
154
155 // Options Page Settings
156 function wdm_wpma_settings_page() {
157
158 // Only Admins
159 if ( ! current_user_can( 'manage_options' ) ) { return; }
160
161 // Settings
162 $wdm_wpma_options = wdm_wpma_options();
163
164 // The active tab is identified by the ?page=... URL parameter set by WordPress
165 // for each registered admin page (see add_menu_page / add_submenu_page calls above).
166 $tab = isset($_GET['page']) ? sanitize_key($_GET['page']) : '';
167
168 echo '<div class="wrap">';
169
170 // Options Header
171 echo '<h1 class="wdm-wpma-title"><img class="wdm-wpma-logo" src="'.esc_url(plugins_url('assets/img/mail-queue-logo-wordmark.svg', __FILE__)).'" alt="Mail Queue" width="308" height="56" /></h1>';
172
173 if ($tab != 'wdm_wpma_mail_queue-tab-croninfo' ) {
174 if (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) {
175 echo '<div class="notice notice-warning notice-large">';
176 $url = esc_url(get_option('siteurl').'/wp-cron.php');
177 echo '<p><strong>Please note:</strong><br />Your normal WP Cron is disabled. Please make sure you\'re running the Cron manually by calling <a href="'.$url.'" target="_blank">'.$url.'</a> every couple of minutes.</p>';
178 echo '<p><a href="?page=wdm_wpma_mail_queue-tab-croninfo">More information</a></p>';
179 echo '</div>';
180 }
181 }
182 wdm_wpma_settings_page_navi($tab); // Tabs
183
184 // Options Page Content
185 if ($tab == 'wdm_wpma_mail_queue') {
186 echo '<form action="options.php" method="post">';
187 settings_fields('wdm_wpma_settings');
188 do_settings_sections('wdm_wpma_settings_page');
189 submit_button();
190 echo '</form>';
191 } else if ($tab == 'wdm_wpma_mail_queue-tab-log') {
192 // method="get": search/filter/pagination state lives in the URL so it
193 // survives pagination; the hidden page field keeps us on this tab.
194 echo '<form method="get">';
195 echo '<input type="hidden" name="page" value="wdm_wpma_mail_queue-tab-log" />';
196 $logtable = new wdm_wpma_Log_Table();
197 $logtable->prepare_items();
198 $logtable->display();
199 echo '</form>';
200 } else if ($tab == 'wdm_wpma_mail_queue-tab-queue') {
201 $next_cron_timestamp = wp_next_scheduled('wp_mail_queue_hook');
202 if ($wdm_wpma_options['enabled'] === 'paused') {
203 // The auto-pause case already shows a warning via wdm_wpma_render_pause_on_alert_notice()
204 // (hooked to admin_notices, rendered above the page), so only the manual pause needs a notice here.
205 if (!wdm_wpma_is_pause_on_alert_active()) {
206 echo '<div class="notice notice-warning"><p><b>The Queue is currently paused.</b> <br />New emails enter the queue but are not being sent. Re-enable the Queue in the <a href="admin.php?page=wdm_wpma_mail_queue">Settings</a> to start sending emails again.</p></div>';
207 }
208 } else if ($next_cron_timestamp) {
209 if ($next_cron_timestamp > time()) {
210 echo '<div class="notice notice-success"><p>Next Sending will be triggered in '.esc_html(human_time_diff($next_cron_timestamp)).' at '.esc_html(wp_date('H:i',$next_cron_timestamp)).'.</p></div>';
211 }
212 } else {
213 echo '<div class="notice notice-error"><p><b>The Queue is not enabled at the moment.</b> <br />Enable it in the <a href="admin.php?page=wdm_wpma_mail_queue">Settings</a>.</p></div>';
214 }
215 if ( isset($_GET['testmail_added']) ) {
216 echo '<div class="notice notice-success is-dismissible"><p>The test email has been added to the queue.</p></div>';
217 }
218
219 // method="get": search/filter/pagination state lives in the URL so it
220 // survives pagination; the hidden page field keeps us on this tab.
221 echo '<form method="get">';
222 echo '<input type="hidden" name="page" value="wdm_wpma_mail_queue-tab-queue" />';
223 $queuetable = new wdm_wpma_Log_Table();
224 $queuetable->prepare_items();
225 $queuetable->display();
226 echo '</form>';
227 } else if ($tab == 'wdm_wpma_mail_queue-tab-faq') {
228 echo '<div class="wdm-wpma-box">';
229 echo '<h3>How does this Plugin work?</h3>';
230 echo '<p>When the Mail Queue is <b>enabled</b>, this plugin intercepts the <a target="_blank" href="https://developer.wordpress.org/reference/functions/wp_mail/"><i>wp_mail()</i></a> function. Instead of sending emails immediately, it stores them in the database and sends them step by step with a delay during the <i>WP Cron</i>.</p>';
231 echo '<p>The plugin offers three states:</p>';
232 echo '<p><b>Enabled</b>: emails are added to the queue and sent gradually.<br />';
233 echo '<b>Paused</b>: emails are still added to the queue, but no queued emails are sent until you enable the queue again.<br />';
234 echo '<b>Disabled</b>: the plugin does not intercept emails and has no effect on outgoing mail.</p>';
235 echo '<p>Current state: ';
236 if ($wdm_wpma_options['enabled'] === '1') {
237 echo '<b class="wdm-wpma-ok">The plugin is enabled</b> All emails sent through <a target="_blank" href="https://developer.wordpress.org/reference/functions/wp_mail/"><i>wp_mail()</i></a> are added to the <a href="admin.php?page=wdm_wpma_mail_queue-tab-queue">Queue</a> and sent gradually.';
238 } elseif ($wdm_wpma_options['enabled'] === 'paused') {
239 echo '<b class="wdm-wpma-warning">The plugin is paused</b>. New emails are still added to the queue, but queued emails are not being sent at the moment.';
240 } else {
241 echo '<b>The plugin is disabled</b>. The plugin currently has no effect on outgoing emails.';
242 }
243 echo '</p>';
244 echo '</div>';
245
246 echo '<div class="wdm-wpma-box">';
247 echo '<h3>Does this plugin change the way <b>HOW</b> emails are sent?</h3>';
248 echo '<p>No, don\'t worry. This plugin only affects <b>WHEN</b> emails are sent, not how. It delays the sending (by the Queue), nonetheless all emails are sent through the standard <a target="_blank" href="https://developer.wordpress.org/reference/functions/wp_mail/"><i>wp_mail()</i></a> function.</p>';
249 echo '<p>If you use SMTP for sending, or an external service like Mailgun, everything will still work as expected.</p>';
250 echo '</div>';
251
252 echo '<div class="wdm-wpma-box">';
253 echo '<h3>Does this plugin work, if I have a Caching Plugin installed? E.g. <i>W3 Total Cache</i> or similar?</h3>';
254 echo '<p>If you\'re using a Caching plugin like <i>W3 Total Cache</i>, <i>WP Rocket</i> or any other caching solution which generates static html-files and serves them to visitors, you\'ll have to make sure you\'re calling the <a href="'.esc_url(get_option('siteurl')).'/wp-cron.php" target="_blank">wp-cron file</a> manually every couple of minutes.</p>';
255 echo '<p>Otherwise your normal WP Cron wouldn\'t be called as often as it should be and scheduled messages would be sent with big delays.</p>';
256 echo '</div>';
257
258 echo '<div class="wdm-wpma-box">';
259 echo '<h3>What about Proxy-Caching, e.g. NGINX?</h3>';
260 echo '<p>Same situation here. Please make sure you\'re calling the <a href="'.esc_url(get_option('siteurl')).'/wp-cron.php" target="_blank">WordPress Cron</a> by an external service or your webhoster every couple of minutes.</p>';
261 echo '</div>';
262
263 echo '<div class="wdm-wpma-box">';
264 echo '<h3>My form builder supports attachments. What about them?</h3>';
265 echo '<p>You are covered. All attachments are stored temporarily in the queue until they are sent along with their corresponding emails.</p>';
266 echo '</div>';
267
268 echo '<div class="wdm-wpma-box">';
269 echo '<h3>What are Queue alerts?</h3>';
270 echo '<p>Queue alerts are a simple and effective way to improve the security of your WordPress installation.</p>';
271 echo '<p>Imagine your website starts sending spam through <a target="_blank" href="https://developer.wordpress.org/reference/functions/wp_mail/"><i>wp_mail()</i></a>. The Mail Queue would fill up quickly instead of sending everything at once. This gives you time to react, avoid a lot of trouble and can reduce the damage significantly.</p>';
272 echo '<p>Queue alerts warn you when the queue grows longer than usual. You configure in the settings at which threshold you want to be alerted. This gives you the chance to review the queue and investigate whether something unusual is happening on the website.</p>';
273 echo '<p>Current state: ';
274 if ($wdm_wpma_options['alert_enabled'] === '1') {
275 echo '<b class="wdm-wpma-ok">Alerts are enabled</b> If more than '.esc_html($wdm_wpma_options['email_amount']).' emails are waiting in the Queue, WordPress will send an alert email to <i>'.esc_html($wdm_wpma_options['email']).'</i>.';
276 } else {
277 echo '<b>Alerting is disabled</b>. No alerts will be sent.';
278 }
279 echo '</p>';
280 echo '<p>Optionally, you can enable <b>Auto-Pause on Alert</b>. In that case the queue stops sending emails when an alert is triggered until you change the queue status manually.</p>';
281 echo '<p>Current State: ';
282 if ($wdm_wpma_options['alert_enabled'] === '1' && $wdm_wpma_options['pause_on_alert'] === '1') {
283 echo '<b class="wdm-wpma-ok">Auto-Pause on Alert is enabled</b>. The Queue will be paused automatically when an alert is triggered.';
284 } else {
285 echo '<b>Auto-Pause on Alert</b> is disabled. The Queue will not be paused automatically when an alert is triggered.';
286 }
287 echo '</p>';
288 echo '<p>Please note: This plugin sends at most one alert every six hours while the queue remains above the configured threshold.</p>';
289 echo '</div>';
290
291 echo '<div class="wdm-wpma-box">';
292 echo '<h3>Can I pause the queue without disabling the plugin?</h3>';
293 echo '<p>Yes. You can set the Mail Queue status to <b>Paused</b> in the settings.</p>';
294 echo '<p>When paused, the plugin still intercepts <a target="_blank" href="https://developer.wordpress.org/reference/functions/wp_mail/"><i>wp_mail()</i></a> and stores outgoing emails in the queue, but no queued emails are sent until you change the status back to <b>Enabled</b>.</p>';
295 echo '<p>This is useful if you want to temporarily stop outgoing delivery without disabling the plugin completely.</p>';
296 echo '</div>';
297
298 echo '<div class="wdm-wpma-box">';
299 echo '<h3>Can I add emails with a high priority to the queue?</h3>';
300 echo '<p>Yes, you can add the custom <i>`X-Mail-Queue-Prio`</i> header set to <i>`High`</i> to your email. High priority emails are still processed through the normal Mail Queue cycle, but they are sent before normal queued emails.</p>';
301 echo '<p><b>Example 1 (add priority to WooCommerce emails):</b></p>';
302 echo '<pre><code>add_filter(\'woocommerce_mail_callback_params\',function ( $array ) {
303 $prio_header = \'X-Mail-Queue-Prio: High\';
304 if (is_array($array[3])) {
305 $array[3][] = $prio_header;
306 } else {
307 $array[3] .= $array[3] ? "\r\n" : \'\';
308 $array[3] .= $prio_header;
309 }
310 return $array;
311 },10,1);</code></pre>';
312 echo '<p><b>Example 2 (add priority to Contact Form 7 form emails):</b></p>';
313 echo '<p>When editing a form in Contact Form 7 just add an additional line to the <i>`Additional Headers`</i> field under the <i>`Mail`</i> tab panel.</p>';
314 echo '<pre><code>X-Mail-Queue-Prio: High</code></pre>';
315 echo '</div>';
316
317 echo '<div class="wdm-wpma-box">';
318 echo '<h3>Can I send emails <i>instantly</i> without going through the queue?</h3>';
319 echo '<p>Yes, this is possible (if you absolutely need to do this).</p>';
320 echo '<p>For this you can add the custom <i>`X-Mail-Queue-Prio`</i> header set to <i>`Instant`</i> to your email. These emails are sent immediately and bypass the queue. They still appear in the Mail Queue log so that their delivery remains visible.</p>';
321 echo '<p>Mind that this is a potential security risk and should be considered carefully. Please use only as an exception.</p>';
322 echo '<p><b>Example 1 (instantly send WooCommerce emails):</b></p>';
323 echo '<pre><code>add_filter(\'woocommerce_mail_callback_params\',function ( $array ) {
324 $prio_header = \'X-Mail-Queue-Prio: Instant\';
325 if (is_array($array[3])) {
326 $array[3][] = $prio_header;
327 } else {
328 $array[3] .= $array[3] ? "\r\n" : \'\';
329 $array[3] .= $prio_header;
330 }
331 return $array;
332 },10,1);</code></pre>';
333 echo '<p><b>Example 2 (instantly send Contact Form 7 form emails):</b></p>';
334 echo '<p>When editing a form in Contact Form 7 just add an additional line to the <i>`Additional Headers`</i> field under the <i>`Mail`</i> tab panel.</p>';
335 echo '<pre><code>X-Mail-Queue-Prio: Instant</code></pre>';
336 echo '</div>';
337
338 echo '<div class="wdm-wpma-box">';
339 echo '<h3>Can I send queued emails immediately from the backend?</h3>';
340 echo '<p>Yes. In the <a href="admin.php?page=wdm_wpma_mail_queue-tab-queue">Queue</a> tab you can select one or more queued emails and use the <b>Send now</b> bulk action.</p>';
341 echo '<p>This sends the selected queued emails immediately without waiting for the next WP-Cron cycle.</p>';
342 echo '<p>The action is only available for queued emails that are still waiting in the queue.</p>';
343 echo '</div>';
344
345 echo '<div class="wdm-wpma-box">';
346 echo '<h3>What are queue events in the log?</h3>';
347 echo '<p>In addition to email entries the Mail Queue log also shows queue events.</p>';
348 echo '<p>These entries document important queue state changes, for example when the queue was enabled, paused, disabled, or auto-paused automatically after an alert.</p>';
349 echo '<p>They are not outgoing emails. They are informational log entries to help you understand what happened and when.</p>';
350 echo '</div>';
351
352 echo '<div class="wdm-wpma-box">';
353 echo '<h3>How are WordPress password reset emails handled?</h3>';
354 echo '<p>WordPress password reset emails are handled automatically by the plugin.</p>';
355 echo '<p>Normally they are prioritized so they are sent before regular queued emails. If the Mail Queue is paused, password reset emails are sent instantly to reduce the risk of locking users out of their accounts.</p>';
356 echo '<p>You do not need to add a custom code snippet for this behavior.</p>';
357 echo '</div>';
358
359 echo '<div class="wdm-wpma-box">';
360 echo '<h3>Want to put a test email into the Queue?</h3>';
361 echo '<form method="post" action="'.esc_url( admin_url('admin-post.php') ).'">';
362 echo '<input type="hidden" name="action" value="wdm_wpma_add_testmail" />';
363 wp_nonce_field('wdm_wpma_add_testmail');
364 echo '<p><button type="submit" class="button">Sure! Put a Test Email for '.esc_html($wdm_wpma_options['email']).' into the Queue</button></p>';
365 echo '</form>';
366 echo '</div>';
367
368 } else if ($tab == 'wdm_wpma_mail_queue-tab-croninfo') {
369 echo '<div class="wdm-wpma-box">';
370 echo '<h3>Information: Your common WP Cron is disabled</h3>';
371 echo '<p>It looks like you deactivated the WP-Cron with <i>define(\'DISABLE_WP_CRON\', true)</i>.</p>';
372 $url = esc_url(get_option('siteurl').'/wp-cron.php');
373 echo '<p>In general, this is no problem at all. We just want to remind you to make sure you\'re running the Cron manually by calling <a href="'.$url.'" target="_blank">'.$url.'</a> every couple of minutes.</p>';
374 echo '</div>';
375
376
377 if (function_exists('_get_cron_array')) {
378 $next_tasks = _get_cron_array();
379 if ($next_tasks) {
380 $tasks_in_past = false;
381 $tasks_of_mailqueue_in_past = false;
382 foreach($next_tasks as $key => $val) {
383 if (time() > intval($key) + intval($wdm_wpma_options['queue_interval'])) {
384 if (is_array($val) && array_key_exists('wp_mail_queue_hook', $val)) { $tasks_of_mailqueue_in_past = intval($key); }
385 $tasks_in_past = true;
386 }
387 }
388 if ($tasks_in_past) {
389 echo '<div class="wdm-wpma-box">';
390 echo '<h3>Attention: It seems that your WP-Cron is not running. There are some jobs waiting to be completed.</h3>';
391 if ($tasks_of_mailqueue_in_past) {
392 echo '<p><b>The Queue has not been able to run for '.esc_html(human_time_diff($tasks_of_mailqueue_in_past,time())).'.</b></p>';
393 }
394 echo '</div>';
395 }
396 }
397 }
398
399
400 }
401
402 echo '</div>';
403
404 }
405
406 if(!class_exists('WP_List_Table')){
407 require_once( ABSPATH . 'wp-admin/includes/screen.php' );
408 require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
409 }
410
411 class wdm_wpma_Log_Table extends WP_List_Table {
412
413 const PER_PAGE = 50;
414
415 // Log view: one page of rows (paginated + filtered per request), newest first.
416 // Terminal/log rows only — the queue-side states (queue/high and the transient
417 // 'sending' claim) are excluded so an in-flight row never leaks onto the log tab.
418 // Returns array( $items, $total_matching_rows ).
419 public function get_log() {
420 return $this->query_view( "`status` != 'queue' AND `status` != 'high' AND `status` != 'sending'", '`timestamp` DESC' );
421 }
422
423 // Queue view: one page of rows (paginated + filtered per request), high prio first.
424 // Includes 'sending' (a row a dispatcher has claimed and is mid-sending) so an
425 // in-flight — or, after a crash mid-send, a stuck — row stays visible here.
426 // Returns array( $items, $total_matching_rows ).
427 public function get_queue() {
428 return $this->query_view( "`status` = 'queue' OR `status` = 'high' OR `status` = 'sending'", '`status` ASC, `timestamp` ASC' );
429 }
430
431 // Statuses that can appear on the current tab. Doubles as the allowlist the
432 // status filter is validated against (an out-of-range value is ignored).
433 private function tab_statuses() {
434 $page = isset($_GET['page']) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
435 if ( $page === 'wdm_wpma_mail_queue-tab-queue' ) {
436 // 'sending' = a row a dispatcher has claimed and is mid-sending (transient);
437 // it belongs to the queue side so it stays visible/filterable while in flight.
438 return array( 'queue', 'high', 'sending' );
439 }
440 return array( 'sent', 'error', 'alert', 'event', 'instant' );
441 }
442
443 // Active free-text search term (matched against recipient + subject); '' when none.
444 private function get_active_search() {
445 if ( ! isset($_REQUEST['s']) ) { return ''; }
446 return trim( sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) );
447 }
448
449 // Active status filter, validated against the current tab; '' when none/invalid.
450 private function get_active_status_filter() {
451 if ( ! isset($_GET['filter_status']) ) { return ''; }
452 $status = sanitize_key( wp_unslash( $_GET['filter_status'] ) );
453 return in_array( $status, $this->tab_statuses(), /*strict*/true ) ? $status : '';
454 }
455
456 // Active date-range bound ('from'|'to') as Y-m-d, format-validated; '' when none/invalid.
457 private function get_active_date_filter( $which ) {
458 $key = ( $which === 'from' ) ? 'filter_date_from' : 'filter_date_to';
459 if ( empty($_GET[$key]) ) { return ''; }
460 $val = sanitize_text_field( wp_unslash( $_GET[$key] ) );
461 return preg_match( '/^\d{4}-\d{2}-\d{2}$/', $val ) ? $val : '';
462 }
463
464 // Builds the WHERE clause + prepare() args from $base_where (the tab's fixed
465 // status scope) plus the active search/filter request params.
466 // Returns array( $where_sql, $args ). $where_sql always has a base fragment,
467 // so $args may be empty (no user filters active).
468 private function build_list_where( $base_where ) {
469 global $wpdb;
470 $where = array( '('.$base_where.')' );
471 $args = array();
472
473 $search = $this->get_active_search();
474 if ( $search !== '' ) {
475 // Bodies are intentionally not searched — they aren't loaded into the list query.
476 $like = '%'.$wpdb->esc_like( $search ).'%';
477 $where[] = '(`recipient` LIKE %s OR `subject` LIKE %s)';
478 $args[] = $like;
479 $args[] = $like;
480 }
481
482 $status = $this->get_active_status_filter();
483 if ( $status !== '' ) {
484 $where[] = '`status` = %s';
485 $args[] = $status;
486 }
487
488 $date_from = $this->get_active_date_filter('from');
489 if ( $date_from !== '' ) {
490 $where[] = '`timestamp` >= %s';
491 $args[] = $date_from.' 00:00:00';
492 }
493 $date_to = $this->get_active_date_filter('to');
494 if ( $date_to !== '' ) {
495 $where[] = '`timestamp` <= %s';
496 $args[] = $date_to.' 23:59:59';
497 }
498
499 return array( implode( ' AND ', $where ), $args );
500 }
501
502 // Runs the count + single-page query for a view. The message body is
503 // deliberately NOT selected (it can be large mediumtext and is lazy-loaded
504 // via REST); only its byte length is needed for the list. This keeps memory
505 // bounded even when the table holds tens of thousands of large rows (a spam
506 // flood is exactly when this page gets opened).
507 // Returns array( $items, $total_matching_rows ).
508 private function query_view( $base_where, $order_by ) {
509 global $wpdb;
510 $tableName = wdm_wpma_table();
511 list( $where_sql, $where_args ) = $this->build_list_where( $base_where );
512
513 $count_sql = "SELECT COUNT(*) FROM `$tableName` WHERE $where_sql";
514 $total_items = (int) ( $where_args
515 ? $wpdb->get_var( $wpdb->prepare( $count_sql, $where_args ) )
516 : $wpdb->get_var( $count_sql ) );
517
518 $offset = ( $this->get_pagenum() - 1 ) * self::PER_PAGE;
519
520 $select = "SELECT `id`,`timestamp`,`status`,`recipient`,`subject`,`headers`,`attachments`,`info`,LENGTH(`message`) AS `message_length`";
521 $page_sql = "$select FROM `$tableName` WHERE $where_sql ORDER BY $order_by LIMIT %d OFFSET %d";
522 $page_args = array_merge( $where_args, array( self::PER_PAGE, $offset ) );
523 $items = $wpdb->get_results( $wpdb->prepare( $page_sql, $page_args ), 'ARRAY_A' );
524
525 return array( is_array($items) ? $items : array(), $total_items );
526 }
527
528 public function get_columns() {
529 $columns = array(
530 'cb' => '<label><span class="screen-reader-text">Select all</span><input class="wdm-wpma-select-all" type="checkbox"></label>',
531 'timestamp' => 'Time',
532 'status' => 'Status',
533 'recipient' => 'Recipient',
534 'subject' => 'Subject',
535 'message' => 'Message',
536 'headers' => 'Headers',
537 'attachments' => 'Attachments',
538 );
539 return $columns;
540 }
541
542 public function prepare_items() {
543 $type = isset($_GET['page']) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
544 $columns = $this->get_columns();
545 $hidden = array();
546 $sortable = array();
547 $this->_column_headers = array($columns, $hidden, $sortable);
548 // Bulk actions are handled earlier, on load-{page} (see wdm_wpma_handle_list_table_actions()),
549 // so a redirect is still possible — do not process them here mid-render.
550 // get_log()/get_queue() paginate + filter in SQL and return [items, total].
551 if ($type === 'wdm_wpma_mail_queue-tab-log') {
552 list( $data, $total_items ) = $this->get_log();
553 } else if ($type === 'wdm_wpma_mail_queue-tab-queue') {
554 list( $data, $total_items ) = $this->get_queue();
555 } else {
556 $data = array();
557 $total_items = 0;
558 }
559 $this->set_pagination_args( array(
560 'total_items' => $total_items,
561 'per_page' => self::PER_PAGE,
562 ) );
563 $this->items = $data;
564 }
565
566 public function column_default( $item, $column_name ) {
567 switch( $column_name ) {
568 case 'timestamp':
569 return esc_html( $item[$column_name] );
570 break;
571 case 'subject':
572 if ($item['status'] === 'event') {
573 return '';
574 }
575 return esc_html( $item[$column_name] );
576 break;
577 case 'recipient':
578 case 'headers':
579 $return = wdm_wpma_safe_unserialize($item[$column_name]);
580 if (is_array($return)) {
581 return esc_html( implode(',',$return) );
582 } else {
583 return esc_html( $return );
584 }
585 break;
586 case 'attachments':
587 $return = wdm_wpma_safe_unserialize($item[$column_name]);
588 if (is_array($return)) {
589 $betterreturn = array();
590 foreach($return as $return_item) {
591 array_push($betterreturn,basename($return_item));
592 }
593 return implode('<br />',array_map('esc_html',$betterreturn));
594 } else {
595 return esc_html( basename($return) );
596 }
597 break;
598 case 'status':
599 $display_status = $item[$column_name] === 'instant' ? 'sent' : $item[$column_name];
600 $info = isset( $item[ 'info' ] ) && $item[ 'info' ] ? $item[ 'info' ] : '';
601 $infodata = is_string($info) ? json_decode($info,/*associative*/true) : null;
602
603 if ($item['status'] === 'event' ) {
604 $info = '';
605 switch ($item['subject']) {
606 case 'autopause-activated':
607 $info = 'The Queue was automatically paused because an alert was triggered.';
608 break;
609 case 'queue-enabled':
610 $info = 'The Queue was manually enabled by the user.';
611 break;
612 case 'queue-paused':
613 $info = 'The Queue was manually paused by the user.';
614 break;
615 case 'queue-disabled':
616 $info = 'The Queue was manually disabled by the user.';
617 break;
618 default: break;
619 }
620 $htmlInfo = $info ? '<span class="wdm-wpma-info">'.$info.'</span>' : '';
621 $cssStatus = $htmlInfo ? ' wdm-wpma-status-has-info' : '';
622 return '<span class="wdm-wpma-status wdm-wpma-status-event wdm-wpma-status-event-'.sanitize_title($item['subject']).esc_attr($cssStatus).'">'.$item['subject'].$htmlInfo.'</span>';
623 }
624
625 if ($item[$column_name] === 'alert') {
626 $alertData = is_array($infodata) ? $infodata : null;
627 if ( $alertData ) {
628 $display_in_seconds = $alertData['queue_interval'] < 60 || $alertData['queue_interval'] % 60 !== 0;
629 $queue_interval_display = $display_in_seconds ? esc_html($alertData['queue_interval']).' second(s)' : esc_html($alertData['queue_interval']/60).' minute(s)';
630 $info = '<strong>Emails in queue</strong>: '.esc_html($alertData['in_queue']);
631 $info .= '<br /><strong>Queue settings</strong>: Send max '.esc_html($alertData['queue_amount']).' email(s) every '.$queue_interval_display.'.';
632 $info .= '<br /><strong>Alert settings</strong>: Send alert if more than '.esc_html($alertData['email_amount']).' email(s) in the queue.';
633 } else {
634 $info = '';
635 }
636 } elseif ($item[$column_name] === 'instant') {
637 $info = 'Email was sent instantly, bypassing the queue';
638 } elseif ($item[$column_name] === 'sent') {
639 $sentData = is_array($infodata) ? $infodata : null;
640 if ($sentData && isset($sentData['send_mode']) && $sentData['send_mode'] === 'manual') {
641 $info = 'Email was sent manually from the queue';
642 } elseif ($sentData && isset($sentData['prio']) && $sentData['prio'] === 'high') {
643 $info = 'Email was sent with high priority';
644 } else {
645 $info = '';
646 }
647 }
648 $htmlInfo = $info ? '<span class="wdm-wpma-info">'.$info.'</span>' : '';
649 $cssStatus = $htmlInfo ? ' wdm-wpma-status-has-info' : '';
650 return '<span class="wdm-wpma-status wdm-wpma-status-'.sanitize_title($display_status).esc_attr($cssStatus).'">'.$display_status.$htmlInfo.'</span>';
651 break;
652 case 'message':
653 if ($item['status'] === 'event') {
654 return '';
655 }
656 // The list query selects LENGTH(message) AS message_length instead of the
657 // body (bodies can be large and are lazy-loaded via REST). Fall back to
658 // strlen() if a full 'message' column is ever present.
659 $messageLen = isset($item['message_length'])
660 ? intval($item['message_length'])
661 : ( isset($item['message']) ? strlen($item['message']) : 0 );
662 if ( $messageLen > 0 ) {
663 $return = '<details>';
664 $return .= '<summary class="wdm-wpma-view-source" data-wdm-wpma-list-message-toggle="'.esc_attr($item['id']).'">View message <i>('.esc_html($messageLen).' bytes)</i></summary>';
665 $return .= '<div class="wdm-wpma-email-source" data-wdm-wpma-list-message-content>Loading …</div>';
666 $return .= '</details>';
667 } else {
668 $return = '<em>Empty</em>';
669 }
670 return $return;
671 break;
672 default:
673 return '';
674 break;
675 }
676 }
677
678 protected function column_cb ( $item ) {
679 return '<input type="checkbox" name="id[]" value="'.esc_attr($item['id']).'" />';
680 }
681
682 public function get_bulk_actions() {
683 if (isset($_GET['page']) && $_GET['page'] == 'wdm_wpma_mail_queue-tab-queue') {
684 $actions = array(
685 'delete' => __( 'Delete'),
686 'sendnow' => __( 'Send now'),
687 );
688 } elseif (isset($_GET['page']) && $_GET['page'] == 'wdm_wpma_mail_queue-tab-log') {
689 $actions = array(
690 'delete' => __( 'Delete'),
691 'resend' => __( 'Resend'),
692 );
693 } else {
694 $actions = array();
695 }
696
697 return $actions;
698 }
699
700 public function no_items() {
701 echo 'No emails found.';
702 }
703
704 // Status dropdown + date-range inputs, rendered in the top table nav.
705 // All controls submit through the surrounding method="get" list form, so the
706 // active filters travel in the URL and survive pagination (WP_List_Table builds
707 // its pagination links from the request URL). Values are re-read + validated
708 // by the get_active_*() helpers before they touch SQL.
709 protected function extra_tablenav( $which ) {
710 if ( $which !== 'top' ) { return; }
711
712 $statuses = $this->tab_statuses();
713 $sel_status = $this->get_active_status_filter();
714 $date_from = $this->get_active_date_filter('from');
715 $date_to = $this->get_active_date_filter('to');
716 $search = $this->get_active_search();
717
718 // Hide the filters when there's nothing to filter — but only if no filter
719 // or search is active. If an active filter/search returned zero rows we must
720 // keep the controls (and the Clear filters button) visible so the user can
721 // still reset. Mirrors WP_List_Table::search_box()'s show/hide logic, so the
722 // search box and the filters appear and disappear together.
723 $has_active_filter = ( $sel_status !== '' || $date_from !== '' || $date_to !== '' || $search !== '' );
724 if ( ! $this->has_items() && ! $has_active_filter ) { return; }
725
726 // Single row: filters on the left, search on the right. No Filter button —
727 // the status dropdown and date inputs carry .wdm-wpma-autofilter and
728 // auto-submit the (GET) form on change (see assets/js/wdm-wpma-admin.js);
729 // the search input submits on Enter. One "Clear" resets everything.
730 echo '<div class="alignleft actions wdm-wpma-filters">';
731
732 echo '<label class="screen-reader-text" for="wdm-wpma-filter-status">Filter by status</label>';
733 echo '<select name="filter_status" id="wdm-wpma-filter-status" class="wdm-wpma-autofilter">';
734 echo '<option value="">All statuses</option>';
735 foreach ( $statuses as $status ) {
736 // 'instant' rows display as "sent" in the list — label the option to match.
737 $label = $status === 'instant' ? 'Sent (instant)' : ucfirst($status);
738 echo '<option value="'.esc_attr($status).'"'.selected($sel_status, $status, false).'>'.esc_html($label).'</option>';
739 }
740 echo '</select>';
741
742 echo ' <label class="screen-reader-text" for="wdm-wpma-filter-date-from">From date</label>';
743 echo '<input type="date" name="filter_date_from" id="wdm-wpma-filter-date-from" class="wdm-wpma-autofilter" value="'.esc_attr($date_from).'" />';
744 echo ' <label class="screen-reader-text" for="wdm-wpma-filter-date-to">To date</label>';
745 echo '<input type="date" name="filter_date_to" id="wdm-wpma-filter-date-to" class="wdm-wpma-autofilter" value="'.esc_attr($date_to).'" />';
746
747 echo '<span class="wdm-wpma-search-box">';
748 echo '<label class="screen-reader-text" for="wdm-wpma-search-input">Search emails</label>';
749 echo '<input type="search" name="s" id="wdm-wpma-search-input" value="'.esc_attr($search).'" placeholder="Search recipient or subject" />';
750 echo '<button type="submit" class="button" id="wdm-wpma-search-submit">Search</button>';
751 echo '</span>';
752
753 // One "Clear" for everything (status + dates + search). A link, not a submit:
754 // the GET form would otherwise re-send the current values. Shown only when
755 // something is active.
756 if ( $has_active_filter ) {
757 $clear_url = add_query_arg(
758 array( 'page' => isset($_GET['page']) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '' ),
759 admin_url('admin.php')
760 );
761 echo ' <a class="button wdm-wpma-clear" href="'.esc_url($clear_url).'">Clear</a>';
762 }
763
764 echo '</div>';
765 }
766
767 public function process_bulk_action() {
768
769 // No action selected (e.g. the page is just being rendered) — nothing to do.
770 $current_action = $this->current_action();
771 if ( ! $current_action ) { return; }
772
773 // Security check: any dispatched bulk action requires a valid nonce.
774 // Read from $_REQUEST — the list forms submit via GET (see the method="get"
775 // forms in wdm_wpma_settings_page()), so the WP_List_Table bulk nonce arrives
776 // as a query arg, not a POST field.
777 $nonce = isset( $_REQUEST['_wpnonce'] ) ? sanitize_key( wp_unslash( $_REQUEST['_wpnonce'] ) ) : '';
778 if ( ! $nonce || ! wp_verify_nonce( $nonce, 'bulk-' . $this->_args['plural'] ) ) {
779 wp_die( 'Security check failed. Please go back, reload the page, and try again.' );
780 }
781
782 // get IDs
783 $request_ids = isset( $_REQUEST['id'] ) ? wp_parse_id_list( wp_unslash( $_REQUEST['id'] ) ) : array();
784 if ( empty( $request_ids ) ) { return; }
785
786 // Default page to return to after the action (PRG) — the tab the action ran on.
787 $current_page = isset($_GET['page']) ? sanitize_key($_GET['page']) : 'wdm_wpma_mail_queue';
788
789 // Replay guard: these actions change state (delete / resend / send now) and
790 // the list form submits via GET, so the dispatched action is a bookmarkable
791 // URL. Without this, revisiting it (Back button, history restore, prefetch)
792 // would run it again while the nonce is still valid. Claim the specific
793 // action once — keyed on action + selected ids + nonce — and treat any exact
794 // repeat as a no-op that just redirects back with a zero result, so the same
795 // mails are never deleted, re-queued or re-sent twice. A different selection
796 // under the same reusable nonce is a distinct fingerprint and runs normally.
797 $action_fingerprint = $current_action . '|' . implode( ',', $request_ids ) . '|' . $nonce;
798 if ( ! wdm_wpma_claim_action_once( $action_fingerprint ) ) {
799 wdm_wpma_bulk_action_redirect( $current_page, $current_action, 0, 0 );
800 }
801
802 global $wpdb;
803 $tableName = wdm_wpma_table();
804
805 switch ( $current_action ) {
806 case 'delete':
807 $count_deleted = 0;
808 foreach($request_ids as $id) {
809 // Read the attachments column before deleting the row: a deleted queued
810 // mail must also lose its attachment folder, or the folder is orphaned forever.
811 $attachments = $wpdb->get_var($wpdb->prepare("SELECT `attachments` FROM `$tableName` WHERE `id` = %d", $id));
812 if ( $wpdb->delete($tableName,array('id'=>intval($id)),'%d') ) {
813 $count_deleted++;
814 wdm_wpma_delete_attachment_folder( $attachments );
815 }
816 }
817 wdm_wpma_bulk_action_redirect( $current_page, 'delete', $count_deleted, 0 );
818 break;
819 case 'resend':
820 // Security note: this re-queues the stored body verbatim. That body may have
821 // originated from an unauthenticated caller (any wp_mail() the plugin intercepted),
822 // so it is only safe because rows can't be edited here — the admin resends the exact
823 // bytes, never crafts them. For any "edit before resend" feature this trust boundary
824 // must be re-examined (treat the edited content as admin-supplied input).
825 $count_resend = 0;
826 $count_error = 0;
827 foreach($request_ids as $id) {
828 $maildata = $wpdb->get_row($wpdb->prepare("SELECT * FROM `$tableName` WHERE `id` = %d", $id));
829 // Skip rows that aren't resendable real mails. Allowlist, not denylist:
830 // an unknown/new status must never become silently resendable.
831 // - null: deleted or foreign id (PHP 8 fatals on ->attachments otherwise,
832 // and an empty row would be dispatched as 'ERROR // ' junk to the alert address)
833 // - alert/event: queue bookkeeping, not outgoing mail (mirrors the REST 404 guard)
834 // - queue/high/sending: already pending or mid-flight — re-queuing duplicates them
835 if ( !$maildata || !in_array( $maildata->status, array( 'sent', 'error', 'instant' ), /*strict*/true ) ) {
836 $count_error++;
837 continue;
838 }
839 if (!$maildata->attachments || $maildata->attachments == '') {
840 $count_resend++;
841 $data = array(
842 'timestamp'=> current_time('mysql',false),
843 'recipient'=> $maildata->recipient,
844 'subject'=> $maildata->subject,
845 'message'=> $maildata->message,
846 'status' => 'queue',
847 'attachments' => '',
848 'headers' => $maildata->headers,
849 );
850 $wpdb->insert($tableName,$data);
851 } else {
852 // Emails with attachments can't be resent — the attachment files
853 // are deleted after the original send attempt.
854 $count_error++;
855 }
856 }
857 // If anything was re-queued, land on the Queue tab (that's where the new rows are).
858 $target = ($count_resend > 0) ? 'wdm_wpma_mail_queue-tab-queue' : $current_page;
859 wdm_wpma_bulk_action_redirect( $target, 'resend', $count_resend, $count_error );
860 break;
861 case 'sendnow':
862 $count_sent = 0;
863 $count_error = 0;
864 foreach($request_ids as $id) {
865 $mailitem = $wpdb->get_row($wpdb->prepare("SELECT * FROM `$tableName` WHERE `id` = %d", $id), ARRAY_A);
866 if ( wdm_wpma_really_send_mail($mailitem,['send_mode'=>'manual']) ) {
867 $count_sent++;
868 } else {
869 $count_error++;
870 }
871 }
872 // If anything was sent, land on the Log tab (that's where sent rows show up).
873 $target = ($count_sent > 0) ? 'wdm_wpma_mail_queue-tab-log' : $current_page;
874 wdm_wpma_bulk_action_redirect( $target, 'sendnow', $count_sent, $count_error );
875 break;
876 default:
877 break;
878 }
879
880 return;
881
882 }
883
884 }
885
886 function wdm_wpma_settings_page_navi($tab) {
887 echo '<nav class="nav-tab-wrapper">';
888 echo '<a href="?page=wdm_wpma_mail_queue" class="nav-tab'; if($tab==='wdm_wpma_mail_queue') { echo ' nav-tab-active'; } echo '">Settings</a>';
889 echo '<a href="?page=wdm_wpma_mail_queue-tab-log" class="nav-tab'; if($tab==='wdm_wpma_mail_queue-tab-log') { echo ' nav-tab-active'; } echo '">Log</a>';
890 echo '<a href="?page=wdm_wpma_mail_queue-tab-queue" class="nav-tab'; if($tab==='wdm_wpma_mail_queue-tab-queue') { echo ' nav-tab-active'; } echo '">Queue</a>';
891 echo '<a href="?page=wdm_wpma_mail_queue-tab-faq" class="nav-tab'; if($tab==='wdm_wpma_mail_queue-tab-faq') { echo ' nav-tab-active'; } echo '">FAQ</a>';
892 if (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) {
893 echo '<a href="?page=wdm_wpma_mail_queue-tab-croninfo" class="nav-tab'; if($tab==='wdm_wpma_mail_queue-tab-croninfo') { echo ' nav-tab-active'; } echo '">Cron Information</a>';
894 }
895 echo '</nav>';
896 }
897
898 function wdm_wpma_settings_init() {
899 register_setting('wdm_wpma_settings','wdm_wpma_settings','wdm_wpma_sanitize_settings');
900
901 add_settings_section('wdm_wpma_settings_section','Mail Queue',null,'wdm_wpma_settings_page');
902 add_settings_field('wdm_wpma_status','Status','wdm_wpma_render_option_status','wdm_wpma_settings_page','wdm_wpma_settings_section');
903 add_settings_field('wdm_wpma_queue','Sending frequency','wdm_wpma_render_option_queue','wdm_wpma_settings_page','wdm_wpma_settings_section');
904
905 add_settings_section('wdm_wpma_settings_section_log','Log',null,'wdm_wpma_settings_page');
906 add_settings_field('wdm_wpma_log','Keep log entries','wdm_wpma_render_option_log','wdm_wpma_settings_page','wdm_wpma_settings_section_log');
907
908 add_settings_section('wdm_wpma_settings_section_alert','Alerts',null,'wdm_wpma_settings_page');
909 add_settings_field('wdm_wpma_alert_status','Alerts enabled','wdm_wpma_render_option_alert_status','wdm_wpma_settings_page','wdm_wpma_settings_section_alert');
910 add_settings_field('wdm_wpma_sensitivity','Alert Sensitivity','wdm_wpma_render_option_sensitivity','wdm_wpma_settings_page','wdm_wpma_settings_section_alert');
911 add_settings_field('wdm_wpma_pause_on_alert','Auto-Pause on Alert','wdm_wpma_render_option_pause_on_alert','wdm_wpma_settings_page','wdm_wpma_settings_section_alert');
912 }
913 add_action('admin_init','wdm_wpma_settings_init');
914
915
916 // Sanitize and validate plugin settings. Rebuilds the array from an allowlist:
917 // only known keys survive, each validated — unexpected keys from a crafted
918 // POST are dropped. tableName is deliberately NOT accepted from input (the
919 // settings form never submits it); it is carried over from the stored
920 // settings, so a forged request cannot repoint the plugin at another table.
921 // Also compares old and new settings to detect manual changes of the "enabled"
922 // status and clear the "pause on alert" state if necessary.
923 function wdm_wpma_sanitize_settings ($settings) {
924 $settings = is_array($settings) ? $settings : array();
925
926 $old_settings = get_option('wdm_wpma_settings', array());
927 if (!is_array($old_settings)) { $old_settings = array(); }
928
929 $old_enabled = isset($old_settings['enabled']) ? $old_settings['enabled'] : '0';
930 $new_enabled = isset($settings['enabled']) ? $settings['enabled'] : '0';
931 if ( !in_array($new_enabled, array('0', '1', 'paused'), true) ) {
932 $new_enabled = '0';
933 }
934
935 $clean = array();
936 $clean['enabled'] = $new_enabled;
937
938 $clean['pause_on_alert'] = (isset($settings['pause_on_alert']) && (string) $settings['pause_on_alert'] === '1') ? '1' : '0';
939 $clean['alert_enabled'] = (isset($settings['alert_enabled']) && (string) $settings['alert_enabled'] === '1') ? '1' : '0';
940
941 // Positive integers; on a missing key fall back to the stored value (the
942 // sanitizer also runs on programmatic update_option() calls that may not
943 // carry the full form field set), then to the shipped default.
944 $int_defaults = array('queue_amount' => '1', 'queue_interval' => '5', 'email_amount' => '10', 'clear_queue' => '14');
945 foreach ($int_defaults as $key => $default) {
946 if (isset($settings[$key])) {
947 $raw = $settings[$key];
948 } elseif (isset($old_settings[$key])) {
949 $raw = $old_settings[$key];
950 } else {
951 $raw = $default;
952 }
953 $clean[$key] = (string) max(1, intval($raw));
954 }
955
956 $unit = isset($settings['queue_interval_unit']) ? $settings['queue_interval_unit'] : (isset($old_settings['queue_interval_unit']) ? $old_settings['queue_interval_unit'] : 'minutes');
957 $clean['queue_interval_unit'] = in_array($unit, array('minutes', 'seconds'), true) ? $unit : 'minutes';
958
959 $email = isset($settings['email']) ? sanitize_email($settings['email']) : '';
960 if ($email === '') {
961 $old_email = isset($old_settings['email']) ? sanitize_email($old_settings['email']) : '';
962 $email = $old_email !== '' ? $old_email : sanitize_email(get_option('admin_email'));
963 }
964 $clean['email'] = $email;
965
966 // Carried over, never taken from input. Same charset filter as
967 // wdm_wpma_table(); empty result → omit, the defaults fill it in.
968 if (isset($old_settings['tableName'])) {
969 $tableName = preg_replace('/[^A-Za-z0-9_]/', '', (string) $old_settings['tableName']);
970 if ($tableName !== '') { $clean['tableName'] = $tableName; }
971 }
972
973 if ($new_enabled !== 'paused' || ($new_enabled === 'paused' && $old_enabled !== 'paused')) {
974 wdm_wpma_clear_pause_on_alert_state();
975 }
976
977 if ($new_enabled !== $old_enabled) {
978 if ($new_enabled === '1') {
979 wdm_wpma_push_queue_event(['name' => 'queue-enabled']);
980 } elseif ($new_enabled === 'paused') {
981 wdm_wpma_push_queue_event(['name' => 'queue-paused']);
982 } else {
983 wdm_wpma_push_queue_event(['name' => 'queue-disabled']);
984 }
985 }
986
987 return $clean;
988 }
989
990 function wdm_wpma_render_option_status() {
991 $wdm_wpma_options = wdm_wpma_options();
992
993 echo '<select name="wdm_wpma_settings[enabled]">';
994 echo '<option value="1" '.($wdm_wpma_options['enabled'] === '1' ? 'selected' : '').'>Enabled</option>';
995 echo '<option value="paused" '.($wdm_wpma_options['enabled'] === 'paused' ? 'selected' : '').'>Paused</option>';
996 echo '<option value="0" '.(!in_array($wdm_wpma_options['enabled'], ['1','paused'], /*strict*/true) ? 'selected' : '').'>Disabled</option>';
997 echo '</select>';
998
999 if ($wdm_wpma_options['enabled'] === 'paused') {
1000 echo ' &nbsp; &nbsp; <span class="wdm-wpma-warning"> &larr; The Queue is paused. </span> &nbsp;No emails are currently being sent, but all emails are still being added to the Queue and can be sent later by changing the status to "Enabled".';
1001 } elseif ($wdm_wpma_options['enabled'] !== '1') {
1002 echo ' &nbsp; &nbsp; <span class="wdm-wpma-warning"> &larr; Check this to enable the Queue. </span> &nbsp;Otherwise this plugin won\'t have any effect on your website.';
1003 }
1004 }
1005
1006 function wdm_wpma_render_option_alert_status() {
1007 $wdm_wpma_options = wdm_wpma_options();
1008 if ($wdm_wpma_options['alert_enabled'] === '1') {
1009 echo '<input type="checkbox" name="wdm_wpma_settings[alert_enabled]" value="1" checked />';
1010 } else {
1011 echo '<input type="checkbox" name="wdm_wpma_settings[alert_enabled]" value="1" />';
1012 }
1013 }
1014
1015 function wdm_wpma_render_option_queue() {
1016 $wdm_wpma_options = wdm_wpma_options();
1017 if ($wdm_wpma_options['queue_interval_unit'] === 'seconds') {
1018 $number = intval($wdm_wpma_options['queue_interval']);
1019 } else {
1020 $number = intval($wdm_wpma_options['queue_interval']) / 60;
1021 }
1022
1023 echo 'Send max. <input name="wdm_wpma_settings[queue_amount]" type="number" min="1" value="'.esc_attr($wdm_wpma_options['queue_amount']).'" /> email(s) every <input name="wdm_wpma_settings[queue_interval]" type="number" min="1" value="'.esc_attr($number).'" />';
1024
1025 if ($wdm_wpma_options['queue_interval_unit'] === 'seconds') {
1026 echo '<select name="wdm_wpma_settings[queue_interval_unit]"><option value="minutes">minute(s)</option><option selected value="seconds">second(s)</option></select>';
1027 } else {
1028 echo '<select name="wdm_wpma_settings[queue_interval_unit]"><option selected value="minutes">minute(s)</option><option value="seconds">second(s)</option></select>';
1029 }
1030
1031 echo ' by <i><a href="https://developer.wordpress.org/plugins/cron/" target="_blank">WP Cron</a></i>. ';
1032
1033 // Shown/hidden by JS (assets/js/wdm-wpma-admin.js) when the configured rate
1034 // exceeds 20 mails/minute (minutes unit) or 1 mail per 2 seconds (seconds unit).
1035 echo '<p id="wdm-wpma-rate-notice" class="wdm-wpma-rate-notice" hidden><b>That\'s a lot of email.</b> One of this plugin\'s jobs is to act as a safety net: if your site is ever compromised or a form gets abused, the queue is what keeps mass spam from leaving your server unnoticed. At this rate that safety net has little effect. Consider a slower rate unless you really need it.</p>';
1036 }
1037
1038 function wdm_wpma_render_option_log() {
1039 $wdm_wpma_options = wdm_wpma_options();
1040 echo 'for <input name="wdm_wpma_settings[clear_queue]" type="number" min="1" value="'.esc_attr(intval($wdm_wpma_options['clear_queue']) / 24).'" /> days.';
1041 }
1042
1043 function wdm_wpma_render_option_sensitivity() {
1044 $wdm_wpma_options = wdm_wpma_options();
1045 echo 'Send alert to <input type="text" name="wdm_wpma_settings[email]" value="'.esc_attr(sanitize_email($wdm_wpma_options['email'])).'" /> if more than <input name="wdm_wpma_settings[email_amount]" type="number" min="1" value="'.esc_attr(intval($wdm_wpma_options['email_amount'])).'" /> emails are in the <a href="admin.php?page=wdm_wpma_mail_queue-tab-queue">Queue</a>.';
1046 }
1047
1048
1049
1050 function wdm_wpma_render_option_pause_on_alert() {
1051 $wdm_wpma_options = wdm_wpma_options();
1052 $is_enabled = isset($wdm_wpma_options['pause_on_alert']) && $wdm_wpma_options['pause_on_alert'] === '1';
1053
1054 echo '<select name="wdm_wpma_settings[pause_on_alert]">';
1055 echo '<option value="0" '.(!$is_enabled ? 'selected' : '').'>Disabled</option>';
1056 echo '<option value="1" '.($is_enabled ? 'selected' : '').'>Enabled</option>';
1057 echo '</select>';
1058
1059 if (!$is_enabled) {
1060 echo ' &nbsp; <span> &larr; Automatically pause the Queue if an alert is triggered. </span> This could help to prevent e.g. spam emails being sent in case your website is compromised.';
1061 } else {
1062 echo ' &nbsp; <span> &larr; The Queue will stay paused until you manually change the Mail Queue status to "Enabled".</span>';
1063 }
1064 }
1065
1066
1067
1068
1069
1070 /* ***************************************************************
1071 Alert WordPress User if last email in log could not be sent
1072 **************************************************************** */
1073 function wdm_wpma_check_log_for_errors() {
1074
1075 global $wpdb;
1076 $wdm_wpma_options = wdm_wpma_options();
1077 $currentScreen = function_exists('get_current_screen') ? get_current_screen() : null;
1078
1079 if ($currentScreen && preg_match('#wdm_wpma_mail_queue#', $currentScreen->base) && $wdm_wpma_options['enabled'] === 'paused' && wdm_wpma_is_pause_on_alert_active()) {
1080 wdm_wpma_render_pause_on_alert_notice();
1081 }
1082
1083 if ( !in_array($wdm_wpma_options['enabled'], ['1', 'paused'], true) ) { return; }
1084
1085 $tableName = wdm_wpma_table();
1086 // Only the newest non-queue/non-event row is needed, and only its status + info.
1087 // LIMIT 1 lets MySQL reverse-walk the PRIMARY key and stop at the first match
1088 // instead of get_row() pulling the whole matching set into memory; selecting
1089 // just the two used columns avoids fetching the mediumtext body. This runs on
1090 // admin_notices for every wp-admin page, so it must stay cheap under a flood.
1091 $last_mail = $wpdb->get_row("SELECT `status`,`info` FROM `$tableName` WHERE `status` != 'queue' AND `status` != 'high' AND `status` != 'event' ORDER BY `id` DESC LIMIT 1",'ARRAY_A');
1092 if (!$last_mail) { return; }
1093
1094 if ($last_mail['status'] == 'error') {
1095 if (current_user_can('manage_options')) {
1096 $notice = '<div class="notice notice-error is-dismissible">';
1097 $notice .= '<h1>Attention: Your website has problems sending e-mails</h1>';
1098 $notice .= '<p>This is an important message from your <i>Mail Queue</i> plugin. Please take a look at your <a href="admin.php?page=wdm_wpma_mail_queue-tab-log">Mail Log</a>. The last email(s) couldn\'t be sent properly.</p>';
1099 $notice .= '<p>Last error message was: <b>'.esc_html($last_mail['info']).'</b></p>';
1100 $notice .= '</div>';
1101 echo $notice;
1102 } else if (current_user_can('edit_posts')) {
1103 $notice = '<div class="notice notice-error is-dismissible">';
1104 $notice .= '<h1>Attention: Your website has problems sending e-mails</h1>';
1105 $notice .= '<p>Please contact your Administrator. It seems that WordPress is not able to send emails.</p>';
1106 $notice .= '<p>Last error message: <b>'.esc_html($last_mail['info']).'</b></p>';
1107 $notice .= '</div>';
1108 echo $notice;
1109 }
1110 }
1111
1112 // notices for the plugin options page
1113 if ($currentScreen && $currentScreen->base == 'toplevel_page_wdm_wpma_mail_queue') {
1114 $wpMailOmittingPlugins = [
1115 'mailpoet/mailpoet.php' => 'MailPoet',
1116 ];
1117 $wpMailOmittingPluginsInstalled = [];
1118 foreach (array_keys($wpMailOmittingPlugins) as $plugin) {
1119 if(is_plugin_active($plugin)) {
1120 $wpMailOmittingPluginsInstalled[] = $plugin;
1121 }
1122 }
1123 if (count($wpMailOmittingPluginsInstalled) > 0) {
1124 $notice = '<div class="notice notice-warning is-dismissible">';
1125 $notice .= '<p>';
1126 $notice .= '<strong>Please note:</strong>';
1127 $notice .= '<br />This plugin is not supported when using in combination with plugins that do not use the standard <i>wp_mail()</i> function.';
1128 $notice .= '</p>';
1129 $notice .= '<p>';
1130 $notice .= 'It seems you are using the following plugin(s) that do not use <i>wp_mail()</i>:';
1131 $notice .= '<br />'.implode(', ', array_map(function($plugin) use ($wpMailOmittingPlugins) { return $wpMailOmittingPlugins[$plugin]; },$wpMailOmittingPluginsInstalled));
1132 $notice .= '</p>';
1133 $notice .= '<p><a href="'.get_admin_url(null,'admin.php?page=wdm_wpma_mail_queue-tab-faq').'">More information</a></p>';
1134 $notice .= '</div>';
1135 echo $notice;
1136 }
1137 }
1138 }
1139 add_action('admin_notices', 'wdm_wpma_check_log_for_errors');
1140