| Server IP : 13.126.101.145 / Your IP : 216.73.217.50 Web Server : Apache/2.4.52 (Ubuntu) System : Linux ip-11-115-0-196 6.8.0-1039-aws #41~22.04.1-Ubuntu SMP Thu Sep 11 10:54:48 UTC 2025 x86_64 User : www-data ( 33) PHP Version : 8.3.17 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /var/www/html/rentals_updated/wp-content/plugins/wpo365-mail/Mail/ |
Upload File : |
<?php
namespace Wpo\Mail;
use WP_Error;
use Wpo\Core\WordPress_Helpers;
use Wpo\Core\Wpmu_Helpers;
use \Wpo\Services\Log_Service;
use \Wpo\Services\Options_Service;
use Wpo\Services\Request_Service;
// Prevent public access to this script
defined('ABSPATH') or die();
if (!class_exists('\Wpo\Mail\Mail_Db')) {
class Mail_Db
{
/**
* Logs a wp_mail email message to the wpo365_mail table if that feature is enabled.
* Creates the table if it does not exist.
*
* @since 17.0
*
* @param array $wp_mail WP Mail message as an array.
*
* @return mixed Id of the row inserted or false if the row was not inserted.
*/
public static function add_mail_log($wp_mail)
{
if (!Options_Service::get_global_boolean_var('mail_log')) {
return $wp_mail;
}
$request_service = Request_Service::get_instance();
$request = $request_service->get_request($GLOBALS['WPO_CONFIG']['request_id']);
if (!empty($request->get_item('mail_log_id'))) {
Log_Service::write_log('DEBUG', sprintf('%s -> Not creating a new log entry for an item that is being send again', __METHOD__));
return $wp_mail;
}
$to = $wp_mail['to'];
$subject = $wp_mail['subject'];
$body = $wp_mail['message'];
$headers = $wp_mail['headers'];
$attachments = $wp_mail['attachments'];
global $wpdb;
if (!self::mail_table_exists()) {
self::create_mail_table();
}
$table_name = self::get_mail_table_name();
$data = array(
'mail_to' => \is_string($to) ? json_encode(array($to)) : json_encode($to),
'mail_subject' => esc_sql($subject),
'mail_body' => $body,
'mail_headers' => \is_string($headers) ? json_encode(array($headers)) : json_encode($headers),
'mail_attachments' => json_encode($attachments),
'mail_success' => false,
'mail_error' => null,
);
$rows_inserted = $wpdb->insert(
$table_name,
$data
);
if ($rows_inserted !== 1) {
Log_Service::write_log('ERROR', __METHOD__ . ' -> Could not write mail log entry to the database (Check next line for the raw data that has not been inserted)');
Log_Service::write_log('DEBUG', $data);
} else {
// Memoize the ID of the row inserted so we can update it to report success or errors
$request->set_item('mail_log_id', $wpdb->insert_id);
}
// After every 100th logged item check if any older entries needs deleting
if ($wpdb->insert_id % 100 === 0) {
self::mail_log_retention();
}
return $wp_mail;
}
/**
* Get the last inserted mail log entry for the specified recipient and updates it according. Returns false
*
* @since 17.0
*
* @param bool $success The recipient string.
* @param string $error_message The recipient string.
*
* @return void
*/
public static function update_mail_log($success = false, $error_message = null, $count_attempt = false)
{
if (!Options_Service::get_global_boolean_var('mail_log') || !self::mail_table_exists()) {
return false;
}
// Get the memoized ID of the current mail log entry
$request_service = Request_Service::get_instance();
$request = $request_service->get_request($GLOBALS['WPO_CONFIG']['request_id']);
$mail_log_id = $request->get_item('mail_log_id');
if (empty($mail_log_id)) {
Log_Service::write_log('WARN', sprintf('%s -> Failed to obtain the memoized mail log ID', __METHOD__));
return;
}
global $wpdb;
$table_name = self::get_mail_table_name();
$results = $wpdb->get_results("SELECT * from $table_name WHERE id = $mail_log_id");
if (empty($results) || sizeof($results) !== 1) {
Log_Service::write_log('WARN', sprintf('%s -> Failed to retrieve an existing mail log entry for entry with ID %d', __METHOD__, $mail_log_id));
return;
}
if (empty($error_message)) {
$mail_error = $results[0]->mail_error;
} elseif (empty($results[0]->mail_error)) {
$mail_error = $error_message;
} else {
$mail_error = sprintf('%s | %s', $results[0]->mail_error, $error_message);
}
$updates = array(
'mail_success' => $success,
'mail_error' => $mail_error
);
if ($success) {
$updates['mail_sent'] = self::time_zone_corrected_formatted_date_string();
}
// mail_attemps does not exist if table has not been updated
if (property_exists($results[0], 'mail_attempts')) {
if ($count_attempt) {
$attempts = empty($results[0]->mail_attempts) ? 1 : $results[0]->mail_attempts + 1;
$updates['mail_attempts'] = $attempts;
$time_in_minutes = floor(time() / 60) * 60;
$updates['mail_last_attempt'] = self::time_zone_corrected_formatted_date_string($time_in_minutes);
}
}
$update_result = $wpdb->update($table_name, $updates, array('id' => intval($results[0]->id)));
}
/**
* Get a virtual page of a configurable number of rows from the mail log table.
*
* @since 17.0
*
* @param int $page The zero-based page to start retrieving the next page.
* @param int $page_size The number of rows to retrieve.
* @param string $filter all or error
*
* @return array Max. 100 rows from the mail log starting from the first row for the page.
*/
public static function get_mail_log($start_row = 0, $page_size = 100, $filter = 'all')
{
if (!Options_Service::get_global_boolean_var('mail_log')) {
return array();
}
global $wpdb;
$table_name = self::get_mail_table_name();
if (!self::mail_table_exists()) {
self::create_mail_table();
}
if ($start_row == 0) {
$next_id = $wpdb->get_var("SELECT MAX(id) FROM $table_name");
if (empty($next_id)) {
Log_Service::write_log('DEBUG', __METHOD__ . " -> Cannot retrieve rows from the mail log table because the next ID is not initialized [$next_id]");
return array();
}
$start_row = \intval($next_id) + 1;
}
$filter_clause = $filter == 'error' ? " AND mail_success = false " : "";
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM `$table_name` WHERE `id` < %d " . $filter_clause . " ORDER BY `id` DESC LIMIT %d",
$start_row,
$page_size
));
foreach ($rows as $row) {
if (isset($row->mail_headers)) {
$mail_headers_raw = json_decode($row->mail_headers, true);
$temp_headers = self::get_mail_headers($mail_headers_raw);
$row->mail_headers = json_encode($temp_headers);
}
}
return $rows;
}
/**
* Try to send the mail with the specified id again.
*
* @since 17.0
*
* @param int $id The (wpo365_mail table's) id.
*
* @return bool True if the mail was sent successfully.
*/
public static function send_mail_again($id)
{
Log_Service::write_log('DEBUG', '##### -> ' . __METHOD__);
if (!\is_int($id)) {
Log_Service::write_log('WARN', __METHOD__ . " -> Trying to send mail again but the id $id provided is not valid");
return false;
}
global $wpdb;
$table_name = self::get_mail_table_name();
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM $table_name WHERE id = %d",
$id
));
if (!\is_array($rows) || count($rows) != 1) {
Log_Service::write_log('WARN', __METHOD__ . " -> Trying to send mail again but could not find a matching database record for id $id");
return false;
}
/** @since 21.6 Headers are compacted as a string with new line breaks */
$mail_headers_raw = json_decode($rows[0]->mail_headers, true);
$mail_headers = self::get_mail_headers($mail_headers_raw);
Log_Service::write_log('DEBUG', sprintf(
'%s -> Trying to send mail with ID %d again',
__METHOD__,
$id
));
$request_service = Request_Service::get_instance();
$request = $request_service->get_request($GLOBALS['WPO_CONFIG']['request_id']);
$request->set_item('mail_log_id', $id);
return wp_mail(
json_decode($rows[0]->mail_to, true),
$rows[0]->mail_subject,
$rows[0]->mail_body,
$mail_headers,
json_decode($rows[0]->mail_attachments, true)
);
}
/**
* Helper method to the wpo365_mail table.
*
* @since 17.0
*
* @return bool True if truncated, false if the table was not found.
*/
public static function truncate_mail_log()
{
global $wpdb;
if (self::mail_table_exists()) {
$table_name = self::get_mail_table_name();
$wpdb->query("TRUNCATE TABLE $table_name");
Log_Service::write_log('DEBUG', __METHOD__ . " -> Truncated the wpo365_mail table successfully");
return true;
}
Log_Service::write_log('WARN', __METHOD__ . " -> Trying to truncate the mail log but the wpo365_mail table does not exist");
return false;
}
/**
* See https://learn.microsoft.com/en-us/office365/servicedescriptions/exchange-online-service-description/exchange-online-limits#sending-limits-1
*
* @since 24.0
*
* @return bool|WP_Error True if the limit has not yet been reached otherwise a WP_Error with detailed information.
*/
public static function check_message_rate_limit($db_updated = false)
{
global $wpdb;
$table_name = self::get_mail_table_name();
if (!self::mail_table_exists()) {
self::create_mail_table();
}
$time_in_minutes = floor(time() / 60) * 60;
$items_sent = $wpdb->get_var(sprintf("SELECT COUNT(*) AS SENT FROM %s WHERE mail_last_attempt = '%s';", $table_name, date('Y-m-d H:i:s', $time_in_minutes)));
if (!empty($wpdb->last_error)) {
if (!$db_updated && false !== WordPress_Helpers::stripos($wpdb->last_error, 'mail_last_attempt')) {
self::alter_mail_table_v2();
return self::check_message_rate_limit(true);
}
Log_Service::write_log('ERROR', sprintf('%s -> An error occurred whilst checking the message rate limit [error: %s]', __METHOD__, $wpdb->last_error));
return true; // Still Microsoft will throttle messages
}
$threshold = Options_Service::get_global_numeric_var('mail_threshold');
if (empty($threshold)) {
$threshold = 20;
}
if (intval($items_sent) < $threshold) {
return true;
}
return new WP_Error('MessageRateLimitException', sprintf('Cannot send more than %d messages per minute (check https://learn.microsoft.com/en-us/office365/servicedescriptions/exchange-online-service-description/exchange-online-limits#receiving-and-sending-limits for details)', $threshold));
}
/**
* Get any messages that have not been sent successfully up until one minute ago (oldest message first).
*
* @since 24.0
*
* @param int $attempts
* @param int $limit
* @return void
*/
public static function process_unsent_messages($db_updated = false)
{
global $wpdb;
$table_name = self::get_mail_table_name();
if (!self::mail_table_exists()) {
self::create_mail_table();
}
$time_in_minutes = floor(time() / 60) * 60;
$intervals = Options_Service::get_global_list_var('mail_intervals');
if (!empty($intervals)) {
for ($i = 0; $i < sizeof($intervals); $i++) {
$intervals[$i] = absint($intervals[$i]);
}
}
if (empty($intervals) || sizeof($intervals) != 3) {
$intervals = array(3600, 7200, 14400);
}
$unsent_items = $wpdb->get_results(sprintf(
"SELECT * FROM %s WHERE mail_sent IS NULL AND mail_success = 0 AND (
(mail_attempts = 1 AND mail_last_attempt < '%s') OR
(mail_attempts = 2 AND mail_last_attempt < '%s') OR
(mail_attempts = 3 AND mail_last_attempt < '%s')) ORDER BY id ASC LIMIT 30",
$table_name,
self::time_zone_corrected_formatted_date_string($time_in_minutes - $intervals[0]),
self::time_zone_corrected_formatted_date_string($time_in_minutes - $intervals[1]),
self::time_zone_corrected_formatted_date_string($time_in_minutes - $intervals[2])
));
$db_last_error = $wpdb->last_error;
if (!empty($db_last_error)) {
if (!$db_updated && false !== WordPress_Helpers::stripos($db_last_error, 'mail_attempts')) {
self::alter_mail_table_v2();
return self::process_unsent_messages(true);
}
Log_Service::write_log('ERROR', sprintf('%s -> An error occurred whilst attempting to retrieve unsent messages [error: %s]', __METHOD__, $db_last_error));
return;
}
foreach ($unsent_items as $unsent_item) {
self::send_mail_again(absint($unsent_item->id));
}
}
/**
* Removes the wpo_process_unsent_messages WP-Cron event and adds it if $remove equals false.
*
* @since 24.0
*
* @param mixed $remove
* @return void
*/
public static function ensure_unsent_messages($remove)
{
if ($remove) {
wp_clear_scheduled_hook('wpo_process_unsent_messages');
} elseif (false === wp_next_scheduled('wpo_process_unsent_messages')) {
$activation_result = wp_schedule_event(time(), 'wpo_every_minute', 'wpo_process_unsent_messages', array(), true);
if (is_wp_error($activation_result)) {
Log_Service::write_log('WARN', sprintf(
'%s -> Could not create WP Cron Job to automatically resend failed emails. Please ensure that you are using WPO365 | LOGIN v24.0 or later or WPO365 | MICROSOFT GRAPH MAILER v2.20 or later. [Error: %s]',
__METHOD__,
$activation_result->get_error_message()
));
return false;
}
}
return true;
}
/**
* Delete any records in the database older than the configured retention period (default 45 days).
*
* @since 28.x
*
* @return void
*/
public static function mail_log_retention($db_updated = false)
{
global $wpdb;
if (0 === ($mail_log_retention = Options_Service::get_global_numeric_var('mail_log_retention'))) {
$mail_log_retention = 90;
}
$table_name = self::get_mail_table_name();
if (!self::mail_table_exists()) {
self::create_mail_table();
}
$sql = "DELETE FROM $table_name WHERE `mail_last_attempt` < CURDATE() - INTERVAL $mail_log_retention DAY ORDER BY `mail_last_attempt` DESC";
$result = $wpdb->query($sql);
$db_last_error = $wpdb->last_error;
if (!empty($db_last_error) && false !== WordPress_Helpers::stripos($db_last_error, 'unknown column') && !$db_updated) {
self::alter_mail_table_v2();
return self::mail_log_retention(true);
}
if (!empty($db_last_error)) {
Log_Service::write_log('ERROR', sprintf('%s -> Failed to delete items older than %d from %s [error: %s]', __METHOD__, $mail_log_retention, $table_name, $db_last_error));
} else {
Log_Service::write_log('DEBUG', sprintf('%s -> Successfully deleted %d items older than %d days from %s', __METHOD__, $result, $mail_log_retention, $table_name));
}
}
/**
* Helper method to create / update the custom Mail DB table used for logging.
*
* @since 17.0
*
* @return void
*/
private static function create_mail_table()
{
global $wpdb;
$table_name = self::get_mail_table_name();
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS $table_name (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
mail_sent DATETIME DEFAULT NULL,
mail_to TEXT NOT NULL,
mail_subject TEXT,
mail_body LONGTEXT,
mail_headers TEXT,
mail_attachments TEXT,
mail_success BOOLEAN,
mail_error TEXT,
mail_attempts TINYINT DEFAULT 0,
mail_last_attempt DATETIME DEFAULT NULL
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
}
/**
* Updates the WPO365 Mail table to version 2.
*
* @since 24.0
*
* @return bool True if the update to the table structure was successful.
*/
private static function alter_mail_table_v2()
{
global $wpdb;
$table_name = self::get_mail_table_name();
if (!self::mail_table_exists()) {
self::create_mail_table();
return true;
}
$sql = "ALTER TABLE $table_name
ADD COLUMN mail_attempts TINYINT DEFAULT 0,
ADD COLUMN mail_last_attempt DATETIME NULL DEFAULT NULL,
MODIFY COLUMN mail_sent DATETIME NULL DEFAULT NULL;";
$wpdb->query($sql);
$db_last_error = $wpdb->last_error;
if (!empty($db_last_error)) {
Log_Service::write_log('ERROR', sprintf('%s -> Updating the WPO365 Mail table in the database to version 2.0 failed [error: %s]', __METHOD__, $db_last_error));
return false;
}
return true;
}
/**
* Helper to deal with both string and array headers that will return an associative array of headers.
*
* @since 21.8
*
* @param mixed $wp_mail_headers
* @return array An associative array of headers
*/
private static function get_mail_headers($wp_mail_headers)
{
if (!empty($wp_mail_headers)) {
// Fix what I have broken :)
if (is_array($wp_mail_headers) && sizeof($wp_mail_headers) === 1) {
$wp_mail_headers = $wp_mail_headers[0];
}
if (!is_array($wp_mail_headers)) {
// Explode the headers out, so this function can take
// both string headers and an array of headers.
$temp_headers = explode("\n", str_replace("\r\n", "\n", $wp_mail_headers));
return array_filter($temp_headers, function ($value) {
return !empty($value);
});
}
}
return $wp_mail_headers;
}
/**
* Helper method to centrally provide the custom WordPress table name.
*
* @since 3.0
*
* @return string
*/
private static function get_mail_table_name()
{
global $wpdb;
if (Options_Service::mu_use_subsite_options() && !Wpmu_Helpers::mu_is_network_admin()) {
return $wpdb->prefix . "wpo365_mail";
}
return $wpdb->base_prefix . "wpo365_mail";
}
/**
* Helper method to check whether the custom WordPress table exists.
*
* @since 3.0
*
* @return boolean
*/
private static function mail_table_exists()
{
global $wpdb;
$table_name = self::get_mail_table_name();
if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") == $table_name) {
return true;
}
return false;
}
/**
* Helper to format a date using a helper in the WordPress Helpers class with fallback.
*
* @since 28.1
*
* @param mixed $time
* @return string
*/
private static function time_zone_corrected_formatted_date_string($time = null)
{
if (\method_exists('\Wpo\Core\WordPress_Helpers', 'time_zone_corrected_formatted_date')) {
return WordPress_Helpers::time_zone_corrected_formatted_date($time);
}
if (empty($time)) {
$time = time();
}
return date('Y-m-d H:i:s', $time);
}
}
}