rl-warmup-plugin/includes/class-rl-mailwarmer-message-handler.php
2025-01-15 10:49:39 -06:00

513 lines
20 KiB
PHP

<?php
/**
* Helper functions for the conversation messages
*/
use Symfony\Component\Mime\Email;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport\Transport;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use PhpImap\Mailbox;
if (!defined('ABSPATH')) {
exit;
}
class RL_MailWarmer_Message_Handler {
/**
* Process pending messages.
*/
public static function process_pending_messages() {
// log_to_file("process_pending_messages - Running");
global $wpdb;
// Fetch the next 100 pending messages with scheduled timestamps in the past
$table_name = $wpdb->prefix . 'rl_mailwarmer_messages';
$messages = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM $table_name WHERE status = %s AND scheduled_for_timestamp < %s ORDER BY scheduled_for_timestamp ASC LIMIT 1",
'pending',
current_time('mysql')
),
ARRAY_A
);
if (empty($messages)) {
// log_to_file("process_pending_messages - messages empty");
return;
}
foreach ($messages as $message) {
// log_to_file("==========================================================");
try {
if (!empty($message['first_message']) && $message['first_message']) {
// log_to_file("process_pending_messages - trying send_message");
$result = self::send_message($message);
} else {
// log_to_file("process_pending_messages - trying reply_message");
$result = self::reply_message($message);
}
// Update message status to 'completed' on success
if ($result) {
self::update_message_status($message['id'], 'completed');
} else {
self::update_message_status($message['id'], 'failed');
}
} catch (Exception $e) {
// Handle errors gracefully and log them
log_to_file('Error processing message ID ' . $message['id'] . ': ' . $e->getMessage());
self::update_message_status($message['id'], 'failed');
}
sleep(3);
}
}
/**
* Prepare email data and fetch connection info.
*
* @param array $message The message data.
* @return array Prepared email data and connection info.
* @throws Exception If required data is missing.
*/
private static function prepare_email_data($message) {
// log_to_file("prepare_email_data - Running");
// log_to_file("prepare_email_data - Message: ", $message);
// Ensure 'from' is valid and fetch connection info
if (empty($message['from_email'])) {
throw new Exception(__('Missing "from" address in the message.', 'rl-mailwarmer'));
}
// Find the email-account post matching the 'from' address
$from_post_id = self::find_email_account_by_address($message['from_email']);
if (!$from_post_id) {
throw new Exception(__('No matching email account found for "from" address.', 'rl-mailwarmer'));
}
// Fetch connection details
// log_to_file("prepare_email_data - Getting connection info");
$mail_password = get_post_meta($from_post_id, 'mail_password', true);
$email_provider_id = get_post_meta($from_post_id, 'email_provider', true);
$connection_info = RL_MailWarmer_Email_Helper::get_provider_defaults($email_provider_id);
$connection_info['username'] = $message['from_email'];
$connection_info['mail_password'] = $mail_password;
// log_to_file("prepare_email_data - Connection Info: ", $connection_info);
// Override provider defaults with account-specific settings
foreach (['smtp_server', 'smtp_port', 'smtp_password', 'imap_server', 'imap_port', 'imap_password'] as $key) {
$meta_value = get_post_meta($from_post_id, $key, true);
if (!empty($meta_value)) {
$connection_info[$key] = $meta_value;
}
}
// Handle recipients
// log_to_file("prepare_email_data - Handling recipients");
$to_emails = is_array($message['to_email']) ? $message['to_email'] : explode(',', $message['to_email']);
$cc_emails = is_array($message['cc']) ? $message['cc'] : explode(',', $message['cc']);
return [
'connection_info' => $connection_info,
'to' => array_filter(array_map('trim', $to_emails)),
'cc' => array_filter(array_map('trim', $cc_emails)),
'subject' => $message['subject'],
'body' => $message['body'],
'from' => $message['from_email'],
];
}
/**
* Find the email-account post ID by email address.
*
* @param string $email_address The email address to search for.
* @return int|null The post ID if found, or null.
*/
private static function find_email_account_by_address($email_address) {
// log_to_file("find_email_account_by_address - Searching for: $email_address");
$query = new WP_Query([
'post_type' => 'email-account',
'post_status' => 'publish',
'title' => $email_address,
'fields' => 'ids',
]);
if (!empty($query->posts)) {
return $query->posts[0];
} else {
return new WP_Error('find_email_account_by_address - Unable to find a matching email address');
}
}
/**
* Update the status of a message.
*
* @param int $message_id The ID of the message to update.
* @param string $status The new status ('completed', 'failed', etc.).
* @return void
*/
private static function update_message_status($message_id, $status) {
global $wpdb;
$table_name = $wpdb->prefix . 'rl_mailwarmer_messages';
$wpdb->update(
$table_name,
['status' => $status],
['id' => $message_id],
['%s'],
['%d']
);
}
/**
* Send the first message in a conversation.
*
* @param array $message The message details.
* @return bool True if the message is sent successfully, false otherwise.
* @throws Exception If required fields are missing or an error occurs during sending.
*/
public static function send_message($message) {
// log_to_file("send_message - Running");
// log_to_file("send_message - Message: ", $message);
// Prepare email data and connection info
$email_data = self::prepare_email_data($message);
// log_to_file("send_message - Email Data: ", $email_data);
// Extract connection info
$connection_info = $email_data['connection_info'];
// Check required fields
if (empty($email_data['to']) || empty($email_data['from']) || empty($email_data['subject']) || empty($email_data['body'])) {
throw new Exception(__('Missing required fields for sending the email.', 'rl-mailwarmer'));
}
// Create the SMTP transport
// log_to_file("send_message - Creating Transport");
$transport = new Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport(
$connection_info['smtp_server'],
$connection_info['smtp_port']
);
// Set authentication details
$transport->setUsername($connection_info['username']);
$transport->setPassword($connection_info['mail_password']);
// Create the mailer
$mailer = new Symfony\Component\Mailer\Mailer($transport);
// Build the email
// log_to_file("send_message - Building Mail");
$email = (new Email())
->from($email_data['from'])
->to(...$email_data['to'])
->subject($email_data['subject'])
->html($email_data['body']);
// Add CCs if present
if (!empty($email_data['cc'])) {
$email->cc(...$email_data['cc']);
}
// Attempt to send the email
// log_to_file("send_message - Trying to send");
try {
$mailer->send($email);
log_to_file("send_message - Successfully sent SMTP mail from ", $email_data['from']);
return true; // Email sent successfully
} catch (TransportExceptionInterface $e) {
error_log('Error sending email: ' . $e->getMessage());
return false; // Sending failed
}
}
/**
* Reply to an email message.
*
* @param array $message The message details.
* @return bool True if the message is replied to successfully, false otherwise.
* @throws Exception If required fields are missing or an error occurs.
*/
public static function reply_message($message) {
// Prepare email data and connection info
$email_data = self::prepare_email_data($message);
// log_to_file("reply_message - Email Data: ", $email_data);
// Extract connection info
$connection_info = $email_data['connection_info'];
// Validate required fields
if (empty($email_data['to']) || empty($email_data['from']) || empty($email_data['subject']) || empty($email_data['body'])) {
throw new Exception(__('Missing required fields for replying to the email.', 'rl-mailwarmer'));
}
// Attempt to find the original email via IMAP
// // Attempt to find the original email and reply via IMAP
// try {
// $imap = new \PhpImap\Mailbox(
// sprintf('{%s:%d/imap/ssl}', $connection_info['imap_server'], $connection_info['imap_port']),
// $connection_info['imap_username'],
// $connection_info['imap_password'],
// null,
// 'UTF-8'
// );
// // Search for the email with the matching subject
// $emails = $imap->searchMailbox('SUBJECT "' . addslashes($email_data['subject']) . '"');
// if (!empty($emails)) {
// // Fetch the email data
// $original_email = $imap->getMail($emails[0]);
// // Prepare and send the reply via IMAP
// $imap->reply(
// $emails[0],
// $email_data['body'],
// ['from' => $email_data['from'], 'cc' => $email_data['cc']]
// );
// return true; // Reply sent successfully via IMAP
// }
try {
// log_to_file("reply_message - Trying to reply via IMAP");
$imap = new \PhpImap\Mailbox(
sprintf('{%s:%d/imap/ssl}', $connection_info['imap_server'], $connection_info['imap_port']),
$connection_info['username'],
$connection_info['mail_password'],
null,
'UTF-8'
);
// Search for the email with the matching subject
// log_to_file("reply_message - Searching for message");
$emails = $imap->searchMailbox('SUBJECT "' . addslashes($email_data['subject']) . '"');
if (!empty($emails)) {
// Fetch the email data
$original_email = $imap->getMail($emails[0]);
// log_to_file("reply_message - Message found!");
// log_to_file("reply_message - IMAP Message: ", $original_email);
// Step 2: Send the reply via SMTP
$transport = new Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport(
$connection_info['smtp_server'],
$connection_info['smtp_port']
);
// Set authentication details
$transport->setUsername($connection_info['username']);
$transport->setPassword($connection_info['mail_password']);
$mailer = new Mailer($transport);
$reply_email = (new Email())
->from($email_data['from'])
->to(...$email_data['to'])
->subject('Re: ' . $original_email->subject)
->html($email_data['body']);
// Add headers for threading
$headers = $reply_email->getHeaders();
$headers->addTextHeader('In-Reply-To', $original_email->messageId);
$headers->addTextHeader('References', trim($original_email->headers->references . ' ' . $original_email->messageId));
// ->addHeader('In-Reply-To', $original_email->messageId)
// ->addHeader('References', trim($original_email->headers->references . ' ' . $original_email->messageId));
if (!empty($email_data['cc'])) {
$reply_email->cc(...$email_data['cc']);
}
$mailer->send($reply_email);
log_to_file("reply_message - Successfully sent IMAP/SMTP reply from ", $email_data['from']);
// Step 3: Upload the reply to the Sent folder
$imap_stream = imap_open(
sprintf('{%s:%d/imap/ssl}', $connection_info['imap_server'], $connection_info['imap_port']),
$connection_info['username'],
$connection_info['mail_password']
);
$raw_message = $reply_email->toString(); // Convert the Email object to raw MIME format
imap_append($imap_stream, sprintf('{%s}/Sent', $connection_info['imap_server']), $raw_message);
imap_close($imap_stream);
// Create the reply headers
// $reply_headers = [
// 'In-Reply-To' => $original_email->messageId,
// 'References' => trim($original_email->headers->references . ' ' . $original_email->messageId),
// ];
// // Construct the reply body
// $reply_body = $email_data['body'] . "\n\n" .
// 'On ' . $original_email->date . ', ' . $original_email->fromName . ' <' . $original_email->fromAddress . '> wrote:' . "\n" .
// $original_email->textPlain;
// // Send the reply via IMAP
// log_to_file("reply_message - Sending message via IMAP");
// $imap->addMessageToSentFolder(
// 'To: ' . implode(', ', $email_data['to']) . "\r\n" .
// 'Cc: ' . implode(', ', $email_data['cc']) . "\r\n" .
// 'Subject: Re: ' . $original_email->subject . "\r\n" .
// 'From: ' . $email_data['from'] . "\r\n" .
// 'In-Reply-To: ' . $reply_headers['In-Reply-To'] . "\r\n" .
// 'References: ' . $reply_headers['References'] . "\r\n" .
// "\r\n" .
// $reply_body
// );
// log_to_file("reply_message - Done message via IMAP");
// $mailer = new Mailer($transport);
// $mailer->send($reply);
return true; // Reply sent successfully
}
} catch (Exception $e) {
log_to_file('IMAP Error: ' . $e->getMessage());
}
// Fallback to SMTP if IMAP fails
try {
// log_to_file("reply_message - Falling back to SMTP");
$smtp_reply = (new Email())
->from($email_data['from'])
->to(...$email_data['to'])
->subject($email_data['subject'])
->html($email_data['body']);
// Add CCs if present
if (!empty($email_data['cc'])) {
$smtp_reply->cc(...$email_data['cc']);
}
// Create the SMTP transport
$transport = new Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport(
$connection_info['smtp_server'],
$connection_info['smtp_port']
);
// Set authentication details
$transport->setUsername($connection_info['username']);
$transport->setPassword($connection_info['mail_password']);
// Create the mailer
$mailer = new Symfony\Component\Mailer\Mailer($transport);
$mailer->send($smtp_reply);
log_to_file("reply_message - Sent reply via fallback SMTP from ", $email_data['from']);
// log_to_file('reply_message - SMTP Send Success (?)');
return true; // Fallback SMTP reply sent successfully
} catch (Exception $e) {
log_to_file('reply_message - SMTP Error: ' . $e->getMessage());
return false; // Reply failed
}
}
}
/**
* Add a metabox to the WP dashboard for monitoring and processing the message queue.
*/
add_action('wp_dashboard_setup', function () {
wp_add_dashboard_widget(
'rl_mailwarmer_message_queue',
__('Message Queue', 'rl-mailwarmer'),
'rl_mailwarmer_render_message_queue_widget'
);
});
/**
* Render the Message Queue dashboard widget.
*/
function rl_mailwarmer_render_message_queue_widget() {
global $wpdb;
// Count past-due messages
$table_name = $wpdb->prefix . 'rl_mailwarmer_messages';
$past_due_count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM $table_name WHERE status = %s AND scheduled_for_timestamp < %s",
'pending',
current_time('mysql')
)
);
?>
<div id="rl-mailwarmer-queue">
<p><strong><?php esc_html_e('Past-Due Messages:', 'rl-mailwarmer'); ?></strong> <?php echo esc_html($past_due_count); ?></p>
<button id="process-message-queue" class="button button-primary">
<?php esc_html_e('Process Message Queue', 'rl-mailwarmer'); ?>
</button>
<div id="message-queue-result" style="margin-top: 10px;"></div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const button = document.getElementById('process-message-queue');
const resultDiv = document.getElementById('message-queue-result');
button.addEventListener('click', function () {
button.disabled = true;
resultDiv.textContent = '<?php esc_html_e('Processing...', 'rl-mailwarmer'); ?>';
jQuery.post(ajaxurl, {
action: 'rl_mailwarmer_process_message_queue',
security: '<?php echo esc_js(wp_create_nonce('rl_mailwarmer_queue_nonce')); ?>'
}, function (response) {
button.disabled = false;
if (response.success) {
console.log("Success");
resultDiv.textContent = '<?php esc_html_e('Messages processed:', 'rl-mailwarmer'); ?> ' + response.data.processed_count;
} else {
console.log("Failure");
resultDiv.textContent = '<?php esc_html_e('Error:', 'rl-mailwarmer'); ?> ' + response.data.message;
}
});
});
});
</script>
<?php
}
/**
* AJAX handler to process the message queue.
*/
add_action('wp_ajax_rl_mailwarmer_process_message_queue', function () {
// log_to_file("wp_ajax_rl_mailwarmer_process_message_queue - Running");
// Verify nonce
if (!check_ajax_referer('rl_mailwarmer_queue_nonce', 'security', false)) {
wp_send_json_error(['message' => __('Invalid nonce.', 'rl-mailwarmer')]);
}
// Ensure the user has permission
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => __('Permission denied.', 'rl-mailwarmer')]);
}
try {
// Process pending messages
// log_to_file("wp_ajax_rl_mailwarmer_process_message_queue - Trying process_pending_messages()");
$processed_count = RL_MailWarmer_Message_Handler::process_pending_messages();
wp_send_json_success(['processed_count' => $processed_count]);
} catch (Exception $e) {
wp_send_json_error(['message' => $e->getMessage()]);
}
});