| 1 |
<?php |
| 2 |
namespace WPEXtra\Modules\Common; |
| 3 |
|
| 4 |
if ( ! defined( 'ABSPATH' ) ) { |
| 5 |
exit; |
| 6 |
} |
| 7 |
|
| 8 |
use WPEXtra\Settings; |
| 9 |
use WPEXtra\Helper; |
| 10 |
use WPEXtra\Base; |
| 11 |
|
| 12 |
class SMTP extends Base { |
| 13 |
|
| 14 |
private static $active_account_key = null; |
| 15 |
private static $active_account_name = 'Default'; |
| 16 |
private static $current_mail_data = null; |
| 17 |
|
| 18 |
public function __construct() { |
| 19 |
parent::__construct(); |
| 20 |
|
| 21 |
$from_email = Helper::get_option('from_email'); |
| 22 |
if (!empty($from_email)) { |
| 23 |
add_filter('wp_mail_from', function($email) use ($from_email) { |
| 24 |
return sanitize_email($from_email); |
| 25 |
}); |
| 26 |
} |
| 27 |
|
| 28 |
$from_name = Helper::get_option('from_name'); |
| 29 |
if (!empty($from_name)) { |
| 30 |
add_filter('wp_mail_from_name', function($name) use ($from_name) { |
| 31 |
return $from_name; |
| 32 |
}); |
| 33 |
} |
| 34 |
|
| 35 |
add_action('phpmailer_init', [$this, 'process_mail']); |
| 36 |
add_filter('wp_mail', [$this, 'capture_mail_data']); |
| 37 |
add_action('wp_mail_succeeded', [$this, 'on_mail_succeeded']); |
| 38 |
add_action('wp_mail_failed', [$this, 'on_mail_failed']); |
| 39 |
|
| 40 |
if (Helper::get_option('email_domain')) { |
| 41 |
add_action('register_post', [$this, 'is_valid_email_domain'], 10, 3); |
| 42 |
} |
| 43 |
|
| 44 |
if (is_admin()) { |
| 45 |
add_action('admin_notices', [$this, 'display_quota_admin_notices']); |
| 46 |
} |
| 47 |
|
| 48 |
// Initialize table if logging is enabled |
| 49 |
if (Helper::get_option('email_log')) { |
| 50 |
$this->maybe_create_logs_table(); |
| 51 |
} |
| 52 |
} |
| 53 |
|
| 54 |
public function display_quota_admin_notices() { |
| 55 |
if (!current_user_can('manage_options')) { |
| 56 |
return; |
| 57 |
} |
| 58 |
|
| 59 |
$today = current_time('Y-m-d'); |
| 60 |
$accounts = (array) Helper::get_option('smtp_accounts', []); |
| 61 |
if (empty($accounts)) { |
| 62 |
return; |
| 63 |
} |
| 64 |
|
| 65 |
$total_accounts = 0; |
| 66 |
$exhausted_accounts = []; |
| 67 |
|
| 68 |
foreach ($accounts as $idx => $acc) { |
| 69 |
if (empty($acc['username']) || empty($acc['password'])) { |
| 70 |
continue; |
| 71 |
} |
| 72 |
$total_accounts++; |
| 73 |
$limit = intval($acc['daily_limit'] ?? 0); |
| 74 |
$sent_today = intval(get_option('wpex_smtp_sent_' . $idx . '_' . $today, 0)); |
| 75 |
$title = !empty($acc['title']) ? $acc['title'] : ($acc['username'] ?? 'Account #' . ($idx + 1)); |
| 76 |
|
| 77 |
if ($limit > 0 && $sent_today >= $limit) { |
| 78 |
$exhausted_accounts[] = [ |
| 79 |
'title' => $title, |
| 80 |
'sent' => $sent_today, |
| 81 |
'limit' => $limit |
| 82 |
]; |
| 83 |
} |
| 84 |
} |
| 85 |
|
| 86 |
if (empty($exhausted_accounts)) { |
| 87 |
return; |
| 88 |
} |
| 89 |
|
| 90 |
if (count($exhausted_accounts) >= $total_accounts && $total_accounts > 0) { |
| 91 |
?> |
| 92 |
<div class="notice notice-error is-dismissible"> |
| 93 |
<p> |
| 94 |
<strong><?php esc_html_e('WP EXtra - SMTP Quota Alert:', 'wp-extra'); ?></strong> |
| 95 |
<?php printf( |
| 96 |
esc_html__('All %d configured SMTP accounts have reached their daily sending limit for today (%s). Outgoing WordPress emails may fail or be delayed.', 'wp-extra'), |
| 97 |
$total_accounts, |
| 98 |
esc_html($today) |
| 99 |
); ?> |
| 100 |
<a href="<?php echo esc_url(admin_url('admin.php?page=wp-extra')); ?>"><?php esc_html_e('Manage SMTP Settings →', 'wp-extra'); ?></a> |
| 101 |
</p> |
| 102 |
</div> |
| 103 |
<?php |
| 104 |
} else { |
| 105 |
$names = implode(', ', array_map(function($a) { |
| 106 |
return sprintf('<strong>%s</strong> (%d/%d)', esc_html($a['title']), $a['sent'], $a['limit']); |
| 107 |
}, $exhausted_accounts)); |
| 108 |
?> |
| 109 |
<div class="notice notice-warning is-dismissible"> |
| 110 |
<p> |
| 111 |
<strong><?php esc_html_e('WP EXtra - SMTP Limit Reached:', 'wp-extra'); ?></strong> |
| 112 |
<?php printf( |
| 113 |
esc_html__('The following account(s) reached their daily quota today: %s. Outgoing emails are automatically routed to your next available SMTP account.', 'wp-extra'), |
| 114 |
$names |
| 115 |
); ?> |
| 116 |
</p> |
| 117 |
</div> |
| 118 |
<?php |
| 119 |
} |
| 120 |
} |
| 121 |
|
| 122 |
public function maybe_create_logs_table() { |
| 123 |
global $wpdb; |
| 124 |
$table_name = $wpdb->prefix . 'wpex_email_logs'; |
| 125 |
if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) { |
| 126 |
$charset_collate = $wpdb->get_charset_collate(); |
| 127 |
$sql = "CREATE TABLE $table_name ( |
| 128 |
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, |
| 129 |
to_email VARCHAR(255) NOT NULL, |
| 130 |
subject VARCHAR(255) DEFAULT '', |
| 131 |
message LONGTEXT DEFAULT '', |
| 132 |
headers TEXT DEFAULT '', |
| 133 |
status VARCHAR(20) NOT NULL DEFAULT 'success', |
| 134 |
error_message TEXT DEFAULT '', |
| 135 |
mailer_account VARCHAR(100) DEFAULT '', |
| 136 |
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| 137 |
PRIMARY KEY (id), |
| 138 |
KEY status (status), |
| 139 |
KEY to_email (to_email(191)), |
| 140 |
KEY created_at (created_at) |
| 141 |
) $charset_collate;"; |
| 142 |
|
| 143 |
require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); |
| 144 |
dbDelta($sql); |
| 145 |
} |
| 146 |
} |
| 147 |
|
| 148 |
public function process_mail($phpmailer) { |
| 149 |
$today = current_time('Y-m-d'); |
| 150 |
$accounts = (array) Helper::get_option('smtp_accounts', []); |
| 151 |
|
| 152 |
// Fallback for legacy single account settings if repeater is empty |
| 153 |
if (empty($accounts) && Helper::get_option('smtp_username') && Helper::get_option('smtp_password')) { |
| 154 |
$accounts = [ |
| 155 |
[ |
| 156 |
'title' => 'Main Account', |
| 157 |
'host' => Helper::get_option('smtp') ? 'smtp.gmail.com' : Helper::get_option('smtp_host', 'smtp.gmail.com'), |
| 158 |
'port' => Helper::get_option('smtp') ? 465 : intval(Helper::get_option('smtp_port', 465)), |
| 159 |
'encryption' => Helper::get_option('smtp') ? 'ssl' : Helper::get_option('smtp_encryption', 'ssl'), |
| 160 |
'username' => Helper::get_option('smtp_username'), |
| 161 |
'password' => Helper::get_option('smtp_password'), |
| 162 |
'daily_limit'=> 0 |
| 163 |
] |
| 164 |
]; |
| 165 |
} |
| 166 |
|
| 167 |
if (empty($accounts)) { |
| 168 |
self::$active_account_name = 'WP Mail (PHP)'; |
| 169 |
return $phpmailer; |
| 170 |
} |
| 171 |
|
| 172 |
$selected_account = null; |
| 173 |
$selected_idx = null; |
| 174 |
|
| 175 |
// Find the first account that hasn't exceeded its daily quota |
| 176 |
foreach ($accounts as $idx => $acc) { |
| 177 |
if (empty($acc['username']) || empty($acc['password'])) { |
| 178 |
continue; |
| 179 |
} |
| 180 |
$limit = intval($acc['daily_limit'] ?? 0); |
| 181 |
$sent_today = intval(get_option('wpex_smtp_sent_' . $idx . '_' . $today, 0)); |
| 182 |
|
| 183 |
if ($limit === 0 || $sent_today < $limit) { |
| 184 |
$selected_account = $acc; |
| 185 |
$selected_idx = $idx; |
| 186 |
break; |
| 187 |
} |
| 188 |
} |
| 189 |
|
| 190 |
// If all accounts reached their limit, fallback to the first active account |
| 191 |
if ($selected_account === null) { |
| 192 |
foreach ($accounts as $idx => $acc) { |
| 193 |
if (!empty($acc['username']) && !empty($acc['password'])) { |
| 194 |
$selected_account = $acc; |
| 195 |
$selected_idx = $idx; |
| 196 |
break; |
| 197 |
} |
| 198 |
} |
| 199 |
} |
| 200 |
|
| 201 |
if ($selected_account) { |
| 202 |
$providers = [ |
| 203 |
'gmail' => ['host' => 'smtp.gmail.com', 'port' => 587, 'encryption' => 'tls'], |
| 204 |
'mailgun' => ['host' => 'smtp.mailgun.org', 'port' => 587, 'encryption' => 'tls'], |
| 205 |
'outlook' => ['host' => 'smtp.office365.com', 'port' => 587, 'encryption' => 'tls'], |
| 206 |
'yahoo' => ['host' => 'smtp.mail.yahoo.com', 'port' => 587, 'encryption' => 'tls'], |
| 207 |
'aws_ses' => ['host' => 'email-smtp.us-east-1.amazonaws.com', 'port' => 587, 'encryption' => 'tls'], |
| 208 |
'zoho' => ['host' => 'smtp.zoho.com', 'port' => 587, 'encryption' => 'tls'], |
| 209 |
'sendgrid' => ['host' => 'smtp.sendgrid.net', 'port' => 587, 'encryption' => 'tls'], |
| 210 |
'sendinblue' => ['host' => 'smtp-relay.brevo.com', 'port' => 587, 'encryption' => 'tls'], |
| 211 |
]; |
| 212 |
|
| 213 |
$prov_key = $selected_account['provider'] ?? 'gmail'; |
| 214 |
$preset = $providers[$prov_key] ?? null; |
| 215 |
|
| 216 |
self::$active_account_key = $selected_idx; |
| 217 |
self::$active_account_name = !empty($selected_account['title']) ? $selected_account['title'] : (!empty($selected_account['username']) ? $selected_account['username'] : 'SMTP Account #' . ($selected_idx + 1)); |
| 218 |
|
| 219 |
$host = !empty($selected_account['host']) ? $selected_account['host'] : ($preset['host'] ?? 'smtp.gmail.com'); |
| 220 |
$port = !empty($selected_account['port']) ? intval($selected_account['port']) : ($preset['port'] ?? 465); |
| 221 |
$encryption = !empty($selected_account['encryption']) && $selected_account['encryption'] !== 'none' ? $selected_account['encryption'] : ($preset['encryption'] ?? 'ssl'); |
| 222 |
|
| 223 |
$phpmailer->Host = $host; |
| 224 |
$phpmailer->Port = $port; |
| 225 |
$phpmailer->SMTPSecure = ($encryption !== 'none') ? $encryption : ''; |
| 226 |
$phpmailer->SMTPAuth = true; |
| 227 |
$phpmailer->Username = $selected_account['username']; |
| 228 |
|
| 229 |
$raw_pass = $selected_account['password'] ?? ''; |
| 230 |
$decoded = base64_decode($raw_pass, true); |
| 231 |
$phpmailer->Password = ($decoded !== false && base64_encode($decoded) === $raw_pass) ? $decoded : $raw_pass; |
| 232 |
|
| 233 |
$global_from_email = Helper::get_option('from_email'); |
| 234 |
$global_from_name = Helper::get_option('from_name'); |
| 235 |
|
| 236 |
$phpmailer->From = !empty($global_from_email) ? sanitize_email($global_from_email) : sanitize_email($selected_account['username']); |
| 237 |
if (!empty($global_from_name)) { |
| 238 |
$phpmailer->FromName = $global_from_name; |
| 239 |
} |
| 240 |
} |
| 241 |
|
| 242 |
$smtp_options = (array) Helper::get_option('smtp_options', []); |
| 243 |
if (in_array('noverifyssl', $smtp_options, true)) { |
| 244 |
$phpmailer->SMTPOptions = [ |
| 245 |
'ssl' => [ |
| 246 |
'verify_peer' => false, |
| 247 |
'verify_peer_name' => false, |
| 248 |
'allow_self_signed' => true |
| 249 |
] |
| 250 |
]; |
| 251 |
} |
| 252 |
|
| 253 |
$phpmailer->IsSMTP(); |
| 254 |
return $phpmailer; |
| 255 |
} |
| 256 |
|
| 257 |
public function capture_mail_data($mail_args) { |
| 258 |
self::$current_mail_data = $mail_args; |
| 259 |
return $mail_args; |
| 260 |
} |
| 261 |
|
| 262 |
public function on_mail_succeeded() { |
| 263 |
$today = current_time('Y-m-d'); |
| 264 |
|
| 265 |
// Increment daily sent counter |
| 266 |
if (self::$active_account_key !== null) { |
| 267 |
$opt_name = 'wpex_smtp_sent_' . self::$active_account_key . '_' . $today; |
| 268 |
$count = intval(get_option($opt_name, 0)); |
| 269 |
update_option($opt_name, $count + 1, false); |
| 270 |
} |
| 271 |
|
| 272 |
// Record log if enabled |
| 273 |
if (Helper::get_option('email_log') && !empty(self::$current_mail_data)) { |
| 274 |
$this->save_email_log('success'); |
| 275 |
} |
| 276 |
} |
| 277 |
|
| 278 |
public function on_mail_failed($wp_error) { |
| 279 |
if (Helper::get_option('email_log') && !empty(self::$current_mail_data)) { |
| 280 |
$error_msg = ($wp_error instanceof \WP_Error) ? $wp_error->get_error_message() : ''; |
| 281 |
$this->save_email_log('failed', $error_msg); |
| 282 |
} |
| 283 |
} |
| 284 |
|
| 285 |
private function save_email_log($status = 'success', $error_msg = '') { |
| 286 |
global $wpdb; |
| 287 |
$table_name = $wpdb->prefix . 'wpex_email_logs'; |
| 288 |
|
| 289 |
$data = self::$current_mail_data; |
| 290 |
if (empty($data)) { |
| 291 |
return; |
| 292 |
} |
| 293 |
|
| 294 |
$to = is_array($data['to']) ? implode(', ', $data['to']) : (string)$data['to']; |
| 295 |
$subject = isset($data['subject']) ? (string)$data['subject'] : ''; |
| 296 |
$message = isset($data['message']) ? (string)$data['message'] : ''; |
| 297 |
$headers = isset($data['headers']) ? (is_array($data['headers']) ? maybe_serialize($data['headers']) : (string)$data['headers']) : ''; |
| 298 |
|
| 299 |
$wpdb->insert( |
| 300 |
$table_name, |
| 301 |
[ |
| 302 |
'to_email' => $to, |
| 303 |
'subject' => $subject, |
| 304 |
'message' => $message, |
| 305 |
'headers' => $headers, |
| 306 |
'status' => $status, |
| 307 |
'error_message' => $error_msg, |
| 308 |
'mailer_account' => self::$active_account_name, |
| 309 |
'created_at' => current_time('mysql'), |
| 310 |
], |
| 311 |
['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s'] |
| 312 |
); |
| 313 |
|
| 314 |
// Auto purge old logs based on retention days |
| 315 |
$retention = intval(Helper::get_option('email_log_retention', 30)); |
| 316 |
if ($retention > 0 && rand(1, 20) === 1) { |
| 317 |
$wpdb->query($wpdb->prepare( |
| 318 |
"DELETE FROM $table_name WHERE created_at < DATE_SUB(NOW(), INTERVAL %d DAY)", |
| 319 |
$retention |
| 320 |
)); |
| 321 |
} |
| 322 |
} |
| 323 |
|
| 324 |
public function is_valid_email_domain($login, $email, $errors) { |
| 325 |
$valid_email_domains_string = Helper::get_option('email_domain', ''); |
| 326 |
$valid_email_domains = array_filter(array_map('trim', explode("\n", $valid_email_domains_string))); |
| 327 |
if (empty($valid_email_domains)) { |
| 328 |
return; |
| 329 |
} |
| 330 |
$email_domain = substr(strrchr($email, "@"), 1); |
| 331 |
if (!in_array($email_domain, $valid_email_domains, true)) { |
| 332 |
$errors->add('domain_whitelist_error', __('<strong>ERROR</strong>: you can only register using allowed email domains', 'wp-extra')); |
| 333 |
} |
| 334 |
} |
| 335 |
|
| 336 |
protected $features = [ |
| 337 |
'smtp_options', |
| 338 |
'no_emails', |
| 339 |
]; |
| 340 |
|
| 341 |
public function smtp_options() { |
| 342 |
$options = (array) Helper::get_option('smtp_options', []); |
| 343 |
if (in_array('antispam', $options, true)) { |
| 344 |
add_action('wp_enqueue_scripts', [$this, 'smtpmail_scripts'], 99); |
| 345 |
} |
| 346 |
} |
| 347 |
|
| 348 |
public function smtpmail_scripts() { |
| 349 |
$anti_spam_form = in_array('antispam', (array) Helper::get_option('smtp_options', []), true) ? 1 : 0; |
| 350 |
wp_enqueue_script('security', plugins_url('/assets/js/security.min.js', WPEX_FILE), ['jquery'], defined('WPEX_VERSION') ? WPEX_VERSION : null, true); |
| 351 |
wp_localize_script('security', 'security_setting', ['anti_spam_form' => $anti_spam_form]); |
| 352 |
} |
| 353 |
|
| 354 |
public function no_emails() { |
| 355 |
$no_emails = (array) Helper::get_option('no_emails', []); |
| 356 |
if (in_array('remove_admin', $no_emails, true)) { |
| 357 |
add_filter('admin_email_check_interval', '__return_false'); |
| 358 |
} |
| 359 |
if (in_array('auto_update', $no_emails, true)) { |
| 360 |
add_filter('send_core_update_notification_email', '__return_false'); |
| 361 |
add_filter('auto_plugin_update_send_email', '__return_false'); |
| 362 |
add_filter('auto_theme_update_send_email', '__return_false'); |
| 363 |
} |
| 364 |
if (in_array('new_user', $no_emails, true)) { |
| 365 |
add_filter('wp_send_new_user_notification_to_admin', '__return_false'); |
| 366 |
} |
| 367 |
if (in_array('password_reset', $no_emails, true)) { |
| 368 |
remove_action('after_password_reset', 'wp_password_change_notification'); |
| 369 |
add_filter('send_password_change_email', '__return_false'); |
| 370 |
add_filter('woocommerce_disable_password_change_notification', '__return_false'); |
| 371 |
} |
| 372 |
} |
| 373 |
|
| 374 |
} |